Support changing your own name and handling name change events

This commit is contained in:
Gabe Kangas 2022-05-26 13:52:04 -07:00
parent 5a51b2d779
commit 1d213b71d4
No known key found for this signature in database
GPG key ID: 9A56337728BC81EA
12 changed files with 147 additions and 100 deletions

View file

@ -86,6 +86,9 @@ func (s *Server) userNameChanged(eventData chatClientEvent) {
receivedEvent.User = savedUser
receivedEvent.ClientID = eventData.client.id
webhooks.SendChatEventUsernameChanged(receivedEvent)
// Resend the client's user so their username is in sync.
eventData.client.sendConnectedClientInfo()
}
func (s *Server) userMessageSent(eventData chatClientEvent) {

View file

@ -1,11 +1,11 @@
import { ChatMessage } from '../../interfaces/chat-message.model';
/* eslint-disable react/no-danger */
interface Props {
// eslint-disable-next-line react/no-unused-prop-types
message: ChatMessage;
body: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function ChatSystemMessage(props: Props) {
return <div>Component goes here</div>;
export default function ChatActionMessage(props: Props) {
const { body } = props;
return <div dangerouslySetInnerHTML={{ __html: body }} />;
}

View file

@ -3,10 +3,11 @@ import { Virtuoso } from 'react-virtuoso';
import { useRef } from 'react';
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 { ChatMessage } from '../../../interfaces/chat-message.model';
import { ChatUserMessage } from '..';
import ChatActionMessage from '../ChatActionMessage';
interface Props {
messages: ChatMessage[];
@ -19,10 +20,20 @@ export default function ChatContainer(props: Props) {
const chatContainerRef = useRef(null);
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 => {
switch (message.type) {
case MessageType.CHAT:
return <ChatUserMessage message={message} showModeratorMenu={false} />;
case MessageType.NAME_CHANGE:
return getNameChangeViewForMessage(message);
default:
return null;
}

View file

@ -1,25 +1,44 @@
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Input, Button } from 'antd';
import { MessageType } from '../../interfaces/socket-events';
import WebsocketService from '../../services/websocket-service';
// import { setLocalStorage } from '../../utils/helpers';
import { websocketServiceAtom } from '../stores/ClientConfigStore';
import { websocketServiceAtom, chatDisplayNameAtom } from '../stores/ClientConfigStore';
/* eslint-disable @typescript-eslint/no-unused-vars */
interface Props {}
export default function NameChangeModal(props: Props) {
const websocketService = useRecoilValue<WebsocketService>(websocketServiceAtom);
const chatDisplayName = useRecoilValue<string>(chatDisplayNameAtom);
const [newName, setNewName] = useState<any>(chatDisplayName);
// const handleNameChange = () => {
// // Send name change
// const nameChange = {
// type: SOCKET_MESSAGE_TYPES.NAME_CHANGE,
// newName,
// };
// websocketService.send(nameChange);
const handleNameChange = () => {
const nameChange = {
type: MessageType.NAME_CHANGE,
newName,
};
websocketService.send(nameChange);
};
// // Store it locally
// setLocalStorage(KEY_USERNAME, newName);
// };
const saveEnabled =
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>
);
}

View file

@ -12,7 +12,7 @@ import appStateModel, {
AppStateOptions,
makeEmptyAppState,
} from './application-state';
import { setLocalStorage, getLocalStorage } from '../../utils/helpers';
import { setLocalStorage, getLocalStorage } from '../../utils/localStorage';
import {
ConnectedClientInfoEvent,
MessageType,
@ -23,6 +23,7 @@ import {
import handleChatMessage from './eventhandlers/handleChatMessage';
import handleConnectedClientInfoMessage from './eventhandlers/connected-client-info-handler';
import ServerStatusService from '../../services/status-service';
import handleNameChangeEvent from './eventhandlers/handleNameChangeEvent';
const SERVER_STATUS_POLL_DURATION = 5000;
const ACCESS_TOKEN_KEY = 'accessToken';
@ -207,6 +208,9 @@ export function ClientConfigStore() {
case MessageType.CHAT:
handleChatMessage(message as ChatEvent, chatMessages, setChatMessages);
break;
case MessageType.NAME_CHANGE:
handleNameChangeEvent(message as ChatEvent, chatMessages, setChatMessages);
break;
default:
console.error('Unknown socket message type: ', message.type);
}

View file

@ -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);
}

View file

@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil';
import VideoJS from './player';
import ViewerPing from './viewer-ping';
import VideoPoster from './VideoPoster';
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
import { getLocalStorage, setLocalStorage } from '../../utils/localStorage';
import { isVideoPlayingAtom } from '../stores/ClientConfigStore';
const PLAYER_VOLUME = 'owncast_volume';

View file

@ -31,3 +31,8 @@ export interface ChatEvent extends SocketEvent {
user: User;
body: string;
}
export interface NameChangeEvent extends SocketEvent {
user: User;
oldName: string;
}

View file

@ -1,4 +1,3 @@
import { message } from 'antd';
import { MessageType, SocketEvent } from '../interfaces/socket-events';
export interface SocketMessage {
@ -76,41 +75,45 @@ export default class WebsocketService {
// Optimization where multiple events can be sent within a
// single websocket message. So split them if needed.
const messages = e.data.split('\n');
let message: SocketEvent;
let socketEvent: SocketEvent;
// eslint-disable-next-line no-plusplus
for (let i = 0; i < messages.length; i++) {
try {
message = JSON.parse(messages[i]);
socketEvent = JSON.parse(messages[i]);
if (this.handleMessage) {
this.handleMessage(message);
this.handleMessage(socketEvent);
}
} catch (e) {
console.error(e, e.data);
return;
}
if (!message.type) {
console.error('No type provided', message);
if (!socketEvent.type) {
console.error('No type provided', socketEvent);
return;
}
// Send PONGs
if (message.type === MessageType.PING) {
if (socketEvent.type === MessageType.PING) {
this.sendPong();
return;
}
}
}
isConnected(): boolean {
return this.websocket?.readyState === this.websocket?.OPEN;
}
// 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.
if (!message.type || !MessageType[message.type]) {
console.warn(`Outbound message: Unknown socket message type: "${message.type}" sent.`);
if (!socketEvent.type || !MessageType[socketEvent.type]) {
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);
}

View file

@ -1,5 +1,6 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import NameChangeModal from '../components/modals/NameChangeModal';
export default {
@ -9,7 +10,11 @@ export default {
} as ComponentMeta<typeof NameChangeModal>;
// 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
export const Basic = Template.bind({});

View file

@ -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
export function addNewlines(str) {
return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
@ -52,9 +6,8 @@ export function addNewlines(str) {
export function pluralize(string, count) {
if (count === 1) {
return string;
} else {
return string + 's';
}
return `${string}s`;
}
// Trying to determine if browser is mobile/tablet.
@ -66,7 +19,7 @@ export function hasTouchScreen() {
} else if ('msMaxTouchPoints' in navigator) {
hasTouch = navigator.msMaxTouchPoints > 0;
} else {
var mQ = window.matchMedia && matchMedia('(pointer:coarse)');
const mQ = window.matchMedia && matchMedia('(pointer:coarse)');
if (mQ && mQ.media === '(pointer:coarse)') {
hasTouch = !!mQ.matches;
} else if ('orientation' in window) {
@ -79,20 +32,6 @@ export function hasTouchScreen() {
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) {
return String(pad.repeat(size) + text).slice(-size);
}
@ -116,7 +55,7 @@ export function parseSecondsToDurationString(seconds = 0) {
}
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
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
@ -129,7 +68,7 @@ export function doesObjectSupportFunction(object, functionName) {
export function classNames(json) {
const classes = [];
Object.entries(json).map(function (item) {
Object.entries(json).map(item => {
const [key, value] = item;
if (value) {
classes.push(key);
@ -208,7 +147,7 @@ export function paginateArray(items, page, perPage) {
previousPage: page - 1 ? page - 1 : null,
nextPage: totalPages > page ? page + 1 : null,
total: items.length,
totalPages: totalPages,
totalPages,
items: paginatedItems,
};
}

47
web/utils/localStorage.ts Normal file
View 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,
);
}