Support changing your own name and handling name change events
This commit is contained in:
parent
5a51b2d779
commit
1d213b71d4
@ -86,6 +86,9 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
|
|||||||
receivedEvent.User = savedUser
|
receivedEvent.User = savedUser
|
||||||
receivedEvent.ClientID = eventData.client.id
|
receivedEvent.ClientID = eventData.client.id
|
||||||
webhooks.SendChatEventUsernameChanged(receivedEvent)
|
webhooks.SendChatEventUsernameChanged(receivedEvent)
|
||||||
|
|
||||||
|
// Resend the client's user so their username is in sync.
|
||||||
|
eventData.client.sendConnectedClientInfo()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) userMessageSent(eventData chatClientEvent) {
|
func (s *Server) userMessageSent(eventData chatClientEvent) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { ChatMessage } from '../../interfaces/chat-message.model';
|
/* eslint-disable react/no-danger */
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
// eslint-disable-next-line react/no-unused-prop-types
|
body: string;
|
||||||
message: ChatMessage;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export default function ChatSystemMessage(props: Props) {
|
export default function ChatActionMessage(props: Props) {
|
||||||
return <div>Component goes here</div>;
|
const { body } = props;
|
||||||
|
|
||||||
|
return <div dangerouslySetInnerHTML={{ __html: body }} />;
|
||||||
}
|
}
|
||||||
|
@ -3,10 +3,11 @@ import { Virtuoso } from 'react-virtuoso';
|
|||||||
import { useRef } from 'react';
|
import { useRef } from 'react';
|
||||||
import { LoadingOutlined } from '@ant-design/icons';
|
import { LoadingOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
import { MessageType } from '../../../interfaces/socket-events';
|
import { MessageType, NameChangeEvent } from '../../../interfaces/socket-events';
|
||||||
import s from './ChatContainer.module.scss';
|
import s from './ChatContainer.module.scss';
|
||||||
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||||
import { ChatUserMessage } from '..';
|
import { ChatUserMessage } from '..';
|
||||||
|
import ChatActionMessage from '../ChatActionMessage';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
messages: ChatMessage[];
|
messages: ChatMessage[];
|
||||||
@ -19,10 +20,20 @@ export default function ChatContainer(props: Props) {
|
|||||||
const chatContainerRef = useRef(null);
|
const chatContainerRef = useRef(null);
|
||||||
const spinIcon = <LoadingOutlined style={{ fontSize: '32px' }} spin />;
|
const spinIcon = <LoadingOutlined style={{ fontSize: '32px' }} spin />;
|
||||||
|
|
||||||
|
const getNameChangeViewForMessage = (message: NameChangeEvent) => {
|
||||||
|
const { oldName } = message;
|
||||||
|
const { user } = message;
|
||||||
|
const { displayName } = user;
|
||||||
|
const body = `<strong>${oldName}</strong> is now known as <strong>${displayName}</strong>`;
|
||||||
|
return <ChatActionMessage body={body} />;
|
||||||
|
};
|
||||||
|
|
||||||
const getViewForMessage = message => {
|
const getViewForMessage = message => {
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case MessageType.CHAT:
|
case MessageType.CHAT:
|
||||||
return <ChatUserMessage message={message} showModeratorMenu={false} />;
|
return <ChatUserMessage message={message} showModeratorMenu={false} />;
|
||||||
|
case MessageType.NAME_CHANGE:
|
||||||
|
return getNameChangeViewForMessage(message);
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,44 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { Input, Button } from 'antd';
|
||||||
|
import { MessageType } from '../../interfaces/socket-events';
|
||||||
import WebsocketService from '../../services/websocket-service';
|
import WebsocketService from '../../services/websocket-service';
|
||||||
// import { setLocalStorage } from '../../utils/helpers';
|
import { websocketServiceAtom, chatDisplayNameAtom } from '../stores/ClientConfigStore';
|
||||||
import { websocketServiceAtom } from '../stores/ClientConfigStore';
|
|
||||||
|
|
||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
interface Props {}
|
interface Props {}
|
||||||
|
|
||||||
export default function NameChangeModal(props: Props) {
|
export default function NameChangeModal(props: Props) {
|
||||||
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
|
||||||
|
const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
|
||||||
|
const [newName, setNewName] = useState<any>(chatDisplayName);
|
||||||
|
|
||||||
// const handleNameChange = () => {
|
const handleNameChange = () => {
|
||||||
// // Send name change
|
const nameChange = {
|
||||||
// const nameChange = {
|
type: MessageType.NAME_CHANGE,
|
||||||
// type: SOCKET_MESSAGE_TYPES.NAME_CHANGE,
|
newName,
|
||||||
// newName,
|
};
|
||||||
// };
|
websocketService.send(nameChange);
|
||||||
// websocketService.send(nameChange);
|
};
|
||||||
|
|
||||||
// // Store it locally
|
const saveEnabled =
|
||||||
// setLocalStorage(KEY_USERNAME, newName);
|
newName !== chatDisplayName && newName !== '' && websocketService?.isConnected();
|
||||||
// };
|
|
||||||
|
|
||||||
return <div>Name change modal component goes here</div>;
|
return (
|
||||||
|
<div>
|
||||||
|
Your chat display name is what people see when you send chat messages. Other information can
|
||||||
|
go here to mention auth, and stuff.
|
||||||
|
<Input
|
||||||
|
value={newName}
|
||||||
|
onChange={e => setNewName(e.target.value)}
|
||||||
|
placeholder="Your chat display name"
|
||||||
|
maxLength={10}
|
||||||
|
showCount
|
||||||
|
defaultValue={chatDisplayName}
|
||||||
|
/>
|
||||||
|
<Button disabled={!saveEnabled} onClick={handleNameChange}>
|
||||||
|
Change name
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import appStateModel, {
|
|||||||
AppStateOptions,
|
AppStateOptions,
|
||||||
makeEmptyAppState,
|
makeEmptyAppState,
|
||||||
} from './application-state';
|
} from './application-state';
|
||||||
import { setLocalStorage, getLocalStorage } from '../../utils/helpers';
|
import { setLocalStorage, getLocalStorage } from '../../utils/localStorage';
|
||||||
import {
|
import {
|
||||||
ConnectedClientInfoEvent,
|
ConnectedClientInfoEvent,
|
||||||
MessageType,
|
MessageType,
|
||||||
@ -23,6 +23,7 @@ import {
|
|||||||
import handleChatMessage from './eventhandlers/handleChatMessage';
|
import handleChatMessage from './eventhandlers/handleChatMessage';
|
||||||
import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler';
|
import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler';
|
||||||
import ServerStatusService from '../../services/status-service';
|
import ServerStatusService from '../../services/status-service';
|
||||||
|
import handleNameChangeEvent from './eventhandlers/handleNameChangeEvent';
|
||||||
|
|
||||||
const SERVER_STATUS_POLL_DURATION = 5000;
|
const SERVER_STATUS_POLL_DURATION = 5000;
|
||||||
const ACCESS_TOKEN_KEY = 'accessToken';
|
const ACCESS_TOKEN_KEY = 'accessToken';
|
||||||
@ -207,6 +208,9 @@ export function ClientConfigStore() {
|
|||||||
case MessageType.CHAT:
|
case MessageType.CHAT:
|
||||||
handleChatMessage(message as ChatEvent, chatMessages, setChatMessages);
|
handleChatMessage(message as ChatEvent, chatMessages, setChatMessages);
|
||||||
break;
|
break;
|
||||||
|
case MessageType.NAME_CHANGE:
|
||||||
|
handleNameChangeEvent(message as ChatEvent, chatMessages, setChatMessages);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
console.error('Unknown socket message type: ', message.type);
|
console.error('Unknown socket message type: ', message.type);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import { ChatMessage } from '../../../interfaces/chat-message.model';
|
||||||
|
import { ChatEvent } from '../../../interfaces/socket-events';
|
||||||
|
|
||||||
|
export default function handleNameChangeEvent(
|
||||||
|
message: ChatEvent,
|
||||||
|
messages: ChatMessage[],
|
||||||
|
setChatMessages,
|
||||||
|
) {
|
||||||
|
const updatedMessages = [...messages, message];
|
||||||
|
setChatMessages(updatedMessages);
|
||||||
|
}
|
@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil';
|
|||||||
import VideoJS from './player';
|
import VideoJS from './player';
|
||||||
import ViewerPing from './viewer-ping';
|
import ViewerPing from './viewer-ping';
|
||||||
import VideoPoster from './VideoPoster';
|
import VideoPoster from './VideoPoster';
|
||||||
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
|
import { getLocalStorage, setLocalStorage } from '../../utils/localStorage';
|
||||||
import { isVideoPlayingAtom } from '../stores/ClientConfigStore';
|
import { isVideoPlayingAtom } from '../stores/ClientConfigStore';
|
||||||
|
|
||||||
const PLAYER_VOLUME = 'owncast_volume';
|
const PLAYER_VOLUME = 'owncast_volume';
|
||||||
|
@ -31,3 +31,8 @@ export interface ChatEvent extends SocketEvent {
|
|||||||
user: User;
|
user: User;
|
||||||
body: string;
|
body: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NameChangeEvent extends SocketEvent {
|
||||||
|
user: User;
|
||||||
|
oldName: string;
|
||||||
|
}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { message } from 'antd';
|
|
||||||
import { MessageType, SocketEvent } from '../interfaces/socket-events';
|
import { MessageType, SocketEvent } from '../interfaces/socket-events';
|
||||||
|
|
||||||
export interface SocketMessage {
|
export interface SocketMessage {
|
||||||
@ -76,41 +75,45 @@ export default class WebsocketService {
|
|||||||
// Optimization where multiple events can be sent within a
|
// Optimization where multiple events can be sent within a
|
||||||
// single websocket message. So split them if needed.
|
// single websocket message. So split them if needed.
|
||||||
const messages = e.data.split('\n');
|
const messages = e.data.split('\n');
|
||||||
let message: SocketEvent;
|
let socketEvent: SocketEvent;
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
for (let i = 0; i < messages.length; i++) {
|
for (let i = 0; i < messages.length; i++) {
|
||||||
try {
|
try {
|
||||||
message = JSON.parse(messages[i]);
|
socketEvent = JSON.parse(messages[i]);
|
||||||
if (this.handleMessage) {
|
if (this.handleMessage) {
|
||||||
this.handleMessage(message);
|
this.handleMessage(socketEvent);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e, e.data);
|
console.error(e, e.data);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!message.type) {
|
if (!socketEvent.type) {
|
||||||
console.error('No type provided', message);
|
console.error('No type provided', socketEvent);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send PONGs
|
// Send PONGs
|
||||||
if (message.type === MessageType.PING) {
|
if (socketEvent.type === MessageType.PING) {
|
||||||
this.sendPong();
|
this.sendPong();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isConnected(): boolean {
|
||||||
|
return this.websocket?.readyState === this.websocket?.OPEN;
|
||||||
|
}
|
||||||
|
|
||||||
// Outbound: Other components can pass an object to `send`.
|
// Outbound: Other components can pass an object to `send`.
|
||||||
send(message: any) {
|
send(socketEvent: any) {
|
||||||
// Sanity check that what we're sending is a valid type.
|
// Sanity check that what we're sending is a valid type.
|
||||||
if (!message.type || !MessageType[message.type]) {
|
if (!socketEvent.type || !MessageType[socketEvent.type]) {
|
||||||
console.warn(`Outbound message: Unknown socket message type: "${message.type}" sent.`);
|
console.warn(`Outbound message: Unknown socket message type: "${socketEvent.type}" sent.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const messageJSON = JSON.stringify(message);
|
const messageJSON = JSON.stringify(socketEvent);
|
||||||
this.websocket.send(messageJSON);
|
this.websocket.send(messageJSON);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
import NameChangeModal from '../components/modals/NameChangeModal';
|
import NameChangeModal from '../components/modals/NameChangeModal';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -9,7 +10,11 @@ export default {
|
|||||||
} as ComponentMeta<typeof NameChangeModal>;
|
} as ComponentMeta<typeof NameChangeModal>;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const Template: ComponentStory<typeof NameChangeModal> = args => <NameChangeModal />;
|
const Template: ComponentStory<typeof NameChangeModal> = args => (
|
||||||
|
<RecoilRoot>
|
||||||
|
<NameChangeModal />
|
||||||
|
</RecoilRoot>
|
||||||
|
);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export const Basic = Template.bind({});
|
export const Basic = Template.bind({});
|
||||||
|
@ -1,49 +1,3 @@
|
|||||||
import { ORIENTATION_LANDSCAPE, ORIENTATION_PORTRAIT } from './constants.js';
|
|
||||||
|
|
||||||
export function getLocalStorage(key) {
|
|
||||||
try {
|
|
||||||
return localStorage.getItem(key);
|
|
||||||
} catch (e) {}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setLocalStorage(key, value) {
|
|
||||||
try {
|
|
||||||
if (value !== '' && value !== null) {
|
|
||||||
localStorage.setItem(key, value);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
} catch (e) {}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function clearLocalStorage(key) {
|
|
||||||
localStorage.removeItem(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// jump down to the max height of a div, with a slight delay
|
|
||||||
export function jumpToBottom(element, behavior) {
|
|
||||||
if (!element) return;
|
|
||||||
|
|
||||||
if (!behavior) {
|
|
||||||
behavior = document.visibilityState === 'visible' ? 'smooth' : 'instant';
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
element.scrollTo({
|
|
||||||
top: element.scrollHeight,
|
|
||||||
left: 0,
|
|
||||||
behavior: behavior,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
50,
|
|
||||||
element,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert newlines to <br>s
|
// convert newlines to <br>s
|
||||||
export function addNewlines(str) {
|
export function addNewlines(str) {
|
||||||
return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
|
return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
|
||||||
@ -52,9 +6,8 @@ export function addNewlines(str) {
|
|||||||
export function pluralize(string, count) {
|
export function pluralize(string, count) {
|
||||||
if (count === 1) {
|
if (count === 1) {
|
||||||
return string;
|
return string;
|
||||||
} else {
|
|
||||||
return string + 's';
|
|
||||||
}
|
}
|
||||||
|
return `${string}s`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trying to determine if browser is mobile/tablet.
|
// Trying to determine if browser is mobile/tablet.
|
||||||
@ -66,7 +19,7 @@ export function hasTouchScreen() {
|
|||||||
} else if ('msMaxTouchPoints' in navigator) {
|
} else if ('msMaxTouchPoints' in navigator) {
|
||||||
hasTouch = navigator.msMaxTouchPoints > 0;
|
hasTouch = navigator.msMaxTouchPoints > 0;
|
||||||
} else {
|
} else {
|
||||||
var mQ = window.matchMedia && matchMedia('(pointer:coarse)');
|
const mQ = window.matchMedia && matchMedia('(pointer:coarse)');
|
||||||
if (mQ && mQ.media === '(pointer:coarse)') {
|
if (mQ && mQ.media === '(pointer:coarse)') {
|
||||||
hasTouch = !!mQ.matches;
|
hasTouch = !!mQ.matches;
|
||||||
} else if ('orientation' in window) {
|
} else if ('orientation' in window) {
|
||||||
@ -79,20 +32,6 @@ export function hasTouchScreen() {
|
|||||||
return hasTouch;
|
return hasTouch;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOrientation(forTouch = false) {
|
|
||||||
// chrome mobile gives misleading matchMedia result when keyboard is up
|
|
||||||
if (forTouch && window.screen && window.screen.orientation) {
|
|
||||||
return window.screen.orientation.type.match('portrait')
|
|
||||||
? ORIENTATION_PORTRAIT
|
|
||||||
: ORIENTATION_LANDSCAPE;
|
|
||||||
} else {
|
|
||||||
// all other cases
|
|
||||||
return window.matchMedia('(orientation: portrait)').matches
|
|
||||||
? ORIENTATION_PORTRAIT
|
|
||||||
: ORIENTATION_LANDSCAPE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function padLeft(text, pad, size) {
|
export function padLeft(text, pad, size) {
|
||||||
return String(pad.repeat(size) + text).slice(-size);
|
return String(pad.repeat(size) + text).slice(-size);
|
||||||
}
|
}
|
||||||
@ -116,7 +55,7 @@ export function parseSecondsToDurationString(seconds = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function setVHvar() {
|
export function setVHvar() {
|
||||||
var vh = window.innerHeight * 0.01;
|
const vh = window.innerHeight * 0.01;
|
||||||
// Then we set the value in the --vh custom property to the root of the document
|
// Then we set the value in the --vh custom property to the root of the document
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
}
|
}
|
||||||
@ -129,7 +68,7 @@ export function doesObjectSupportFunction(object, functionName) {
|
|||||||
export function classNames(json) {
|
export function classNames(json) {
|
||||||
const classes = [];
|
const classes = [];
|
||||||
|
|
||||||
Object.entries(json).map(function (item) {
|
Object.entries(json).map(item => {
|
||||||
const [key, value] = item;
|
const [key, value] = item;
|
||||||
if (value) {
|
if (value) {
|
||||||
classes.push(key);
|
classes.push(key);
|
||||||
@ -208,7 +147,7 @@ export function paginateArray(items, page, perPage) {
|
|||||||
previousPage: page - 1 ? page - 1 : null,
|
previousPage: page - 1 ? page - 1 : null,
|
||||||
nextPage: totalPages > page ? page + 1 : null,
|
nextPage: totalPages > page ? page + 1 : null,
|
||||||
total: items.length,
|
total: items.length,
|
||||||
totalPages: totalPages,
|
totalPages,
|
||||||
items: paginatedItems,
|
items: paginatedItems,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
47
web/utils/localStorage.ts
Normal file
47
web/utils/localStorage.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
export const LOCAL_STORAGE_KEYS = {
|
||||||
|
username: 'username',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLocalStorage(key) {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem(key);
|
||||||
|
} catch (e) {}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocalStorage(key, value) {
|
||||||
|
try {
|
||||||
|
if (value !== '' && value !== null) {
|
||||||
|
localStorage.setItem(key, value);
|
||||||
|
} else {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch (e) {}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLocalStorage(key) {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// jump down to the max height of a div, with a slight delay
|
||||||
|
export function jumpToBottom(element, behavior) {
|
||||||
|
if (!element) return;
|
||||||
|
|
||||||
|
if (!behavior) {
|
||||||
|
behavior = document.visibilityState === 'visible' ? 'smooth' : 'instant';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
element.scrollTo({
|
||||||
|
top: element.scrollHeight,
|
||||||
|
left: 0,
|
||||||
|
behavior,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
50,
|
||||||
|
element,
|
||||||
|
);
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user