mirror of
				https://github.com/owncast/owncast.git
				synced 2025-11-01 02:44:31 +08:00 
			
		
		
		
	Support changing your own name and handling name change events
This commit is contained in:
		| @ -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) { | ||||
|  | ||||
| @ -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 }} />; | ||||
| } | ||||
|  | ||||
| @ -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; | ||||
|     } | ||||
|  | ||||
| @ -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> | ||||
|   ); | ||||
| } | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
|  | ||||
| @ -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 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'; | ||||
|  | ||||
| @ -31,3 +31,8 @@ export interface ChatEvent extends SocketEvent { | ||||
|   user: User; | ||||
|   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'; | ||||
|  | ||||
| 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); | ||||
|   } | ||||
|  | ||||
|  | ||||
| @ -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({}); | ||||
|  | ||||
| @ -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
									
								
							
							
						
						
									
										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, | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 Gabe Kangas
					Gabe Kangas