* feat: add Username component * feat: set custom username state * feat: update custom username on input blur * feat: inform peers of username updates * feat: display username for peers * feat: show static name in parentheses * feat: use display name in message notification * feat: remove username display from Shell Drawer * feat: persist customUsername
This commit is contained in:
		
							parent
							
								
									870a13eac1
								
							
						
					
					
						commit
						dfe510e642
					
				| @ -54,6 +54,7 @@ test('persists user settings if none were already persisted', async () => { | ||||
|   expect(mockSetItem).toHaveBeenCalledWith(PersistedStorageKeys.USER_SETTINGS, { | ||||
|     colorMode: 'dark', | ||||
|     userId: 'abc123', | ||||
|     customUsername: '', | ||||
|     playSoundOnNewMessage: true, | ||||
|     showNotificationOnNewMessage: true, | ||||
|   }) | ||||
|  | ||||
| @ -39,6 +39,7 @@ function Bootstrap({ | ||||
|   const [hasLoadedSettings, setHasLoadedSettings] = useState(false) | ||||
|   const [userSettings, setUserSettings] = useState<UserSettings>({ | ||||
|     userId: getUuid(), | ||||
|     customUsername: '', | ||||
|     colorMode: 'dark', | ||||
|     playSoundOnNewMessage: true, | ||||
|     showNotificationOnNewMessage: true, | ||||
| @ -100,8 +101,8 @@ function Bootstrap({ | ||||
|     <Router> | ||||
|       <StorageContext.Provider value={storageContextValue}> | ||||
|         <SettingsContext.Provider value={settingsContextValue}> | ||||
|           <Shell appNeedsUpdate={appNeedsUpdate} userPeerId={userId}> | ||||
|             {hasLoadedSettings ? ( | ||||
|           {hasLoadedSettings ? ( | ||||
|             <Shell appNeedsUpdate={appNeedsUpdate} userPeerId={userId}> | ||||
|               <Routes> | ||||
|                 {[routes.ROOT, routes.INDEX_HTML].map(path => ( | ||||
|                   <Route | ||||
| @ -129,10 +130,10 @@ function Bootstrap({ | ||||
|                   element={<Navigate to={routes.ROOT} replace />} | ||||
|                 /> | ||||
|               </Routes> | ||||
|             ) : ( | ||||
|               <></> | ||||
|             )} | ||||
|           </Shell> | ||||
|             </Shell> | ||||
|           ) : ( | ||||
|             <></> | ||||
|           )} | ||||
|         </SettingsContext.Provider> | ||||
|       </StorageContext.Provider> | ||||
|     </Router> | ||||
|  | ||||
| @ -1,9 +1,11 @@ | ||||
| import { render, screen } from '@testing-library/react' | ||||
| import { SettingsContext } from 'contexts/SettingsContext' | ||||
| import { funAnimalName } from 'fun-animal-names' | ||||
| 
 | ||||
| import { ReceivedMessage, UnsentMessage } from 'models/chat' | ||||
| import { userSettingsContextStubFactory } from 'test-utils/stubs/settingsContext' | ||||
| 
 | ||||
| import { Message } from './Message' | ||||
| import { Message, MessageProps } from './Message' | ||||
| 
 | ||||
| const mockUserId = 'user-123' | ||||
| 
 | ||||
| @ -22,10 +24,20 @@ const mockReceivedMessage: ReceivedMessage = { | ||||
|   timeReceived: 2, | ||||
| } | ||||
| 
 | ||||
| const userSettingsStub = userSettingsContextStubFactory({ | ||||
|   userId: mockUserId, | ||||
| }) | ||||
| 
 | ||||
| const MockMessage = (props: MessageProps) => ( | ||||
|   <SettingsContext.Provider value={userSettingsStub}> | ||||
|     <Message {...props} /> | ||||
|   </SettingsContext.Provider> | ||||
| ) | ||||
| 
 | ||||
| describe('Message', () => { | ||||
|   test('renders unsent message text', () => { | ||||
|     render( | ||||
|       <Message | ||||
|       <MockMessage | ||||
|         message={mockUnsentMessage} | ||||
|         userId={mockUserId} | ||||
|         showAuthor={false} | ||||
| @ -37,7 +49,7 @@ describe('Message', () => { | ||||
| 
 | ||||
|   test('renders received message text', () => { | ||||
|     render( | ||||
|       <Message | ||||
|       <MockMessage | ||||
|         message={mockReceivedMessage} | ||||
|         userId={mockUserId} | ||||
|         showAuthor={false} | ||||
| @ -49,7 +61,7 @@ describe('Message', () => { | ||||
| 
 | ||||
|   test('renders message author', () => { | ||||
|     render( | ||||
|       <Message | ||||
|       <MockMessage | ||||
|         message={mockReceivedMessage} | ||||
|         userId={mockUserId} | ||||
|         showAuthor={true} | ||||
|  | ||||
| @ -1,5 +1,6 @@ | ||||
| import Typography, { TypographyProps } from '@mui/material/Typography' | ||||
| 
 | ||||
| import { usePeerNameDisplay } from './usePeerNameDisplay' | ||||
| import { getPeerName } from './getPeerName' | ||||
| 
 | ||||
| interface PeerNameDisplayProps extends TypographyProps { | ||||
| @ -7,12 +8,26 @@ interface PeerNameDisplayProps extends TypographyProps { | ||||
| } | ||||
| 
 | ||||
| export const PeerNameDisplay = ({ | ||||
|   children, | ||||
|   children: userId, | ||||
|   ...rest | ||||
| }: PeerNameDisplayProps) => { | ||||
|   return ( | ||||
|     <Typography component="span" {...rest}> | ||||
|       {getPeerName(children)} | ||||
|     </Typography> | ||||
|   ) | ||||
|   const { getCustomUsername, getFriendlyName } = usePeerNameDisplay() | ||||
| 
 | ||||
|   const friendlyName = getFriendlyName(userId) | ||||
|   const customUsername = getCustomUsername(userId) | ||||
| 
 | ||||
|   if (customUsername === friendlyName) { | ||||
|     return ( | ||||
|       <Typography component="span" {...rest}> | ||||
|         {friendlyName} | ||||
|         <Typography variant="caption"> ({getPeerName(userId)})</Typography> | ||||
|       </Typography> | ||||
|     ) | ||||
|   } else { | ||||
|     return ( | ||||
|       <Typography component="span" {...rest}> | ||||
|         {getPeerName(userId)} | ||||
|       </Typography> | ||||
|     ) | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,2 +1,3 @@ | ||||
| export * from './PeerNameDisplay' | ||||
| export * from './usePeerNameDisplay' | ||||
| export * from './getPeerName' | ||||
|  | ||||
							
								
								
									
										52
									
								
								src/components/PeerNameDisplay/usePeerNameDisplay.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/PeerNameDisplay/usePeerNameDisplay.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,52 @@ | ||||
| import { useContext } from 'react' | ||||
| import { SettingsContext } from 'contexts/SettingsContext' | ||||
| import { ShellContext } from 'contexts/ShellContext' | ||||
| 
 | ||||
| import { getPeerName } from './getPeerName' | ||||
| 
 | ||||
| export const usePeerNameDisplay = () => { | ||||
|   const { getUserSettings } = useContext(SettingsContext) | ||||
|   const { peerList, customUsername: selfCustomUsername } = | ||||
|     useContext(ShellContext) | ||||
| 
 | ||||
|   const { userId: selfUserId } = getUserSettings() | ||||
| 
 | ||||
|   const isPeerSelf = (userId: string) => selfUserId === userId | ||||
| 
 | ||||
|   const getPeer = (userId: string) => | ||||
|     peerList.find(peer => peer.userId === userId) | ||||
| 
 | ||||
|   const getCustomUsername = (userId: string) => | ||||
|     isPeerSelf(userId) | ||||
|       ? selfCustomUsername | ||||
|       : getPeer(userId)?.customUsername ?? '' | ||||
| 
 | ||||
|   const getFriendlyName = (userId: string) => { | ||||
|     const customUsername = getCustomUsername(userId) | ||||
|     const friendlyName = customUsername || getPeerName(userId) | ||||
| 
 | ||||
|     return friendlyName | ||||
|   } | ||||
| 
 | ||||
|   const getDisplayUsername = (userId: string) => { | ||||
|     const friendlyName = getFriendlyName(userId) | ||||
|     const customUsername = getCustomUsername(userId) | ||||
| 
 | ||||
|     let displayUsername: string | ||||
| 
 | ||||
|     if (customUsername === friendlyName) { | ||||
|       displayUsername = `${friendlyName} (${getPeerName(userId)})` | ||||
|     } else { | ||||
|       displayUsername = getPeerName(userId) | ||||
|     } | ||||
| 
 | ||||
|     return displayUsername | ||||
|   } | ||||
| 
 | ||||
|   return { | ||||
|     getCustomUsername, | ||||
|     isPeerSelf, | ||||
|     getFriendlyName, | ||||
|     getDisplayUsername, | ||||
|   } | ||||
| } | ||||
| @ -2,13 +2,12 @@ import { useEffect, useRef } from 'react' | ||||
| import Paper from '@mui/material/Paper' | ||||
| import Tooltip from '@mui/material/Tooltip' | ||||
| 
 | ||||
| import { getPeerName } from 'components/PeerNameDisplay' | ||||
| import { PeerNameDisplay } from 'components/PeerNameDisplay' | ||||
| import { VideoStreamType } from 'models/chat' | ||||
| 
 | ||||
| import { SelectedPeerStream } from './RoomVideoDisplay' | ||||
| 
 | ||||
| interface PeerVideoProps { | ||||
|   isSelectedVideo?: boolean | ||||
|   isSelfVideo?: boolean | ||||
|   numberOfVideos: number | ||||
|   onVideoClick?: ( | ||||
| @ -30,7 +29,6 @@ const nextPerfectSquare = (base: number) => { | ||||
| } | ||||
| 
 | ||||
| export const PeerVideo = ({ | ||||
|   isSelectedVideo, | ||||
|   isSelfVideo, | ||||
|   numberOfVideos, | ||||
|   onVideoClick, | ||||
| @ -81,7 +79,7 @@ export const PeerVideo = ({ | ||||
|       elevation={10} | ||||
|     > | ||||
|       <Tooltip | ||||
|         title={getPeerName(userId)} | ||||
|         title={<PeerNameDisplay>{userId}</PeerNameDisplay>} | ||||
|         placement="top" | ||||
|         componentsProps={{ | ||||
|           tooltip: { sx: { position: 'absolute', top: '25px' } }, | ||||
|  | ||||
| @ -3,11 +3,19 @@ import { waitFor, render, screen } from '@testing-library/react' | ||||
| import userEvent from '@testing-library/user-event' | ||||
| import { MemoryRouter as Router, Route, Routes } from 'react-router-dom' | ||||
| 
 | ||||
| import { userSettingsContextStubFactory } from 'test-utils/stubs/settingsContext' | ||||
| 
 | ||||
| import { SettingsContext } from 'contexts/SettingsContext' | ||||
| 
 | ||||
| import { Room } from './' | ||||
| 
 | ||||
| const mockUserId = 'user-id' | ||||
| const mockRoomId = 'room-123' | ||||
| 
 | ||||
| const userSettingsStub = userSettingsContextStubFactory({ | ||||
|   userId: mockUserId, | ||||
| }) | ||||
| 
 | ||||
| window.AudioContext = jest.fn().mockImplementation() | ||||
| const mockGetUuid = jest.fn() | ||||
| const mockMessagedSender = jest | ||||
| @ -35,9 +43,11 @@ jest.mock('trystero', () => ({ | ||||
| const RouteStub = ({ children }: PropsWithChildren) => { | ||||
|   return ( | ||||
|     <Router initialEntries={['/public/abc123']}> | ||||
|       <Routes> | ||||
|         <Route path="/public/:roomId" element={children}></Route> | ||||
|       </Routes> | ||||
|       <SettingsContext.Provider value={userSettingsStub}> | ||||
|         <Routes> | ||||
|           <Route path="/public/:roomId" element={children}></Route> | ||||
|         </Routes> | ||||
|       </SettingsContext.Provider> | ||||
|     </Router> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -134,7 +134,6 @@ export const RoomVideoDisplay = ({ | ||||
|       {selectedPeerStream && ( | ||||
|         <Box sx={{ height: '80%', width: '100%' }}> | ||||
|           <PeerVideo | ||||
|             isSelectedVideo | ||||
|             numberOfVideos={numberOfVideos} | ||||
|             onVideoClick={handleVideoClick} | ||||
|             userId={selectedPeerStream.peerId} | ||||
|  | ||||
| @ -20,7 +20,7 @@ import { | ||||
|   isInlineMedia, | ||||
|   FileOfferMetadata, | ||||
| } from 'models/chat' | ||||
| import { getPeerName } from 'components/PeerNameDisplay' | ||||
| import { getPeerName, usePeerNameDisplay } from 'components/PeerNameDisplay' | ||||
| import { NotificationService } from 'services/Notification' | ||||
| import { Audio as AudioService } from 'services/Audio' | ||||
| import { PeerRoom, PeerHookType } from 'services/PeerRoom' | ||||
| @ -36,6 +36,11 @@ interface UseRoomConfig { | ||||
|   getUuid?: typeof uuid | ||||
| } | ||||
| 
 | ||||
| interface UserMetadata { | ||||
|   userId: string | ||||
|   customUsername: string | ||||
| } | ||||
| 
 | ||||
| export function useRoom( | ||||
|   { password, ...roomConfig }: BaseRoomConfig & TorrentRoomConfig, | ||||
|   { roomId, userId, getUuid = uuid }: UseRoomConfig | ||||
| @ -57,6 +62,7 @@ export function useRoom( | ||||
|     setRoomId, | ||||
|     setPassword, | ||||
|     setIsPeerListOpen, | ||||
|     customUsername, | ||||
|   } = useContext(ShellContext) | ||||
| 
 | ||||
|   const settingsContext = useContext(SettingsContext) | ||||
| @ -68,6 +74,8 @@ export function useRoom( | ||||
|     () => new AudioService(process.env.PUBLIC_URL + '/sounds/new-message.aac') | ||||
|   ) | ||||
| 
 | ||||
|   const { getDisplayUsername } = usePeerNameDisplay() | ||||
| 
 | ||||
|   const setMessageLog = (messages: Array<Message | InlineMedia>) => { | ||||
|     if (messages.length > messageTranscriptSizeLimit) { | ||||
|       const evictedMessages = messages.slice( | ||||
| @ -181,10 +189,8 @@ export function useRoom( | ||||
|     if (isShowingMessages) setUnreadMessages(0) | ||||
|   }, [isShowingMessages, setUnreadMessages]) | ||||
| 
 | ||||
|   const [sendPeerId, receivePeerId] = usePeerRoomAction<string>( | ||||
|     peerRoom, | ||||
|     PeerActions.PEER_NAME | ||||
|   ) | ||||
|   const [sendPeerMetadata, receivePeerMetadata] = | ||||
|     usePeerRoomAction<UserMetadata>(peerRoom, PeerActions.PEER_METADATA) | ||||
| 
 | ||||
|   const [sendMessageTranscript, receiveMessageTranscript] = usePeerRoomAction< | ||||
|     Array<ReceivedMessage | ReceivedInlineMedia> | ||||
| @ -217,14 +223,16 @@ export function useRoom( | ||||
|     setIsMessageSending(false) | ||||
|   } | ||||
| 
 | ||||
|   receivePeerId((userId: string, peerId: string) => { | ||||
|   receivePeerMetadata(({ userId, customUsername }, peerId: string) => { | ||||
|     const peerIndex = peerList.findIndex(peer => peer.peerId === peerId) | ||||
| 
 | ||||
|     if (peerIndex === -1) { | ||||
|       setPeerList([ | ||||
|         ...peerList, | ||||
|         { | ||||
|           peerId, | ||||
|           userId, | ||||
|           customUsername, | ||||
|           audioState: AudioState.STOPPED, | ||||
|           videoState: VideoState.STOPPED, | ||||
|           screenShareState: ScreenShareState.NOT_SHARING, | ||||
| @ -232,9 +240,18 @@ export function useRoom( | ||||
|         }, | ||||
|       ]) | ||||
|     } else { | ||||
|       const oldUsername = | ||||
|         peerList[peerIndex].customUsername || getPeerName(userId) | ||||
|       const newUsername = customUsername || getPeerName(userId) | ||||
| 
 | ||||
|       const newPeerList = [...peerList] | ||||
|       newPeerList[peerIndex].userId = userId | ||||
|       const newPeer = { ...newPeerList[peerIndex], userId, customUsername } | ||||
|       newPeerList[peerIndex] = newPeer | ||||
|       setPeerList(newPeerList) | ||||
| 
 | ||||
|       if (oldUsername !== newUsername) { | ||||
|         showAlert(`${oldUsername} is now ${newUsername}`) | ||||
|       } | ||||
|     } | ||||
|   }) | ||||
| 
 | ||||
| @ -257,8 +274,10 @@ export function useRoom( | ||||
|       } | ||||
| 
 | ||||
|       if (userSettings.showNotificationOnNewMessage) { | ||||
|         const displayUsername = getDisplayUsername(message.authorId) | ||||
| 
 | ||||
|         NotificationService.showNotification( | ||||
|           `${getPeerName(message.authorId)}: ${message.text}` | ||||
|           `${displayUsername}: ${message.text}` | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
| @ -275,7 +294,9 @@ export function useRoom( | ||||
|     setNumberOfPeers(newNumberOfPeers) | ||||
|     ;(async () => { | ||||
|       try { | ||||
|         const promises: Promise<any>[] = [sendPeerId(userId, peerId)] | ||||
|         const promises: Promise<any>[] = [ | ||||
|           sendPeerMetadata({ userId, customUsername }, peerId), | ||||
|         ] | ||||
| 
 | ||||
|         if (!isPrivate) { | ||||
|           promises.push( | ||||
| @ -293,9 +314,10 @@ export function useRoom( | ||||
|   peerRoom.onPeerLeave(PeerHookType.NEW_PEER, (peerId: string) => { | ||||
|     const peerIndex = peerList.findIndex(peer => peer.peerId === peerId) | ||||
|     const peerExist = peerIndex !== -1 | ||||
| 
 | ||||
|     showAlert( | ||||
|       `${ | ||||
|         peerExist ? getPeerName(peerList[peerIndex].userId) : 'Someone' | ||||
|         peerExist ? getDisplayUsername(peerList[peerIndex].userId) : 'Someone' | ||||
|       } has left the room`,
 | ||||
|       { | ||||
|         severity: 'warning', | ||||
| @ -353,7 +375,7 @@ export function useRoom( | ||||
| 
 | ||||
|       if (userSettings.showNotificationOnNewMessage) { | ||||
|         NotificationService.showNotification( | ||||
|           `${getPeerName(inlineMedia.authorId)} shared media` | ||||
|           `${getDisplayUsername(inlineMedia.authorId)} shared media` | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
| @ -361,6 +383,10 @@ export function useRoom( | ||||
|     setMessageLog([...messageLog, { ...inlineMedia, timeReceived: Date.now() }]) | ||||
|   }) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     sendPeerMetadata({ customUsername, userId }) | ||||
|   }, [customUsername, userId, sendPeerMetadata]) | ||||
| 
 | ||||
|   return { | ||||
|     isPrivate, | ||||
|     handleInlineMediaUpload, | ||||
|  | ||||
| @ -3,7 +3,6 @@ import { Link } from 'react-router-dom' | ||||
| import { Theme } from '@mui/material/styles' | ||||
| import MuiDrawer from '@mui/material/Drawer' | ||||
| import List from '@mui/material/List' | ||||
| import Typography from '@mui/material/Typography' | ||||
| import Divider from '@mui/material/Divider' | ||||
| import IconButton from '@mui/material/IconButton' | ||||
| import ChevronLeftIcon from '@mui/icons-material/ChevronLeft' | ||||
| @ -21,7 +20,6 @@ import ReportIcon from '@mui/icons-material/Report' | ||||
| 
 | ||||
| import { routes } from 'config/routes' | ||||
| import { SettingsContext } from 'contexts/SettingsContext' | ||||
| import { PeerNameDisplay } from 'components/PeerNameDisplay' | ||||
| 
 | ||||
| import { DrawerHeader } from './DrawerHeader' | ||||
| 
 | ||||
| @ -35,7 +33,6 @@ export interface DrawerProps extends PropsWithChildren { | ||||
|   onHomeLinkClick: () => void | ||||
|   onSettingsLinkClick: () => void | ||||
|   theme: Theme | ||||
|   userPeerId: string | ||||
| } | ||||
| 
 | ||||
| export const Drawer = ({ | ||||
| @ -46,7 +43,6 @@ export const Drawer = ({ | ||||
|   onHomeLinkClick, | ||||
|   onSettingsLinkClick, | ||||
|   theme, | ||||
|   userPeerId, | ||||
| }: DrawerProps) => { | ||||
|   const settingsContext = useContext(SettingsContext) | ||||
|   const colorMode = settingsContext.getUserSettings().colorMode | ||||
| @ -80,22 +76,6 @@ export const Drawer = ({ | ||||
|         </IconButton> | ||||
|       </DrawerHeader> | ||||
|       <Divider /> | ||||
|       <ListItem disablePadding> | ||||
|         <ListItemText | ||||
|           sx={{ | ||||
|             padding: '1em 1.5em', | ||||
|           }} | ||||
|           primary={ | ||||
|             <Typography> | ||||
|               Your user name:{' '} | ||||
|               <PeerNameDisplay sx={{ fontWeight: 'bold' }}> | ||||
|                 {userPeerId} | ||||
|               </PeerNameDisplay> | ||||
|             </Typography> | ||||
|           } | ||||
|         /> | ||||
|       </ListItem> | ||||
|       <Divider /> | ||||
|       <List role="navigation"> | ||||
|         <Link to={routes.ROOT} onClick={onHomeLinkClick}> | ||||
|           <ListItem disablePadding> | ||||
|  | ||||
| @ -11,7 +11,7 @@ import { Peer } from 'models/chat' | ||||
| import { ShellContext } from 'contexts/ShellContext' | ||||
| 
 | ||||
| import './PeerDownloadFileButton.sass' | ||||
| import { getPeerName } from 'components/PeerNameDisplay/getPeerName' | ||||
| import { usePeerNameDisplay } from 'components/PeerNameDisplay/usePeerNameDisplay' | ||||
| 
 | ||||
| interface PeerDownloadFileButtonProps { | ||||
|   peer: Peer | ||||
| @ -23,6 +23,7 @@ export const PeerDownloadFileButton = ({ | ||||
|   const [isDownloading, setIsDownloading] = useState(false) | ||||
|   const [downloadProgress, setDownloadProgress] = useState<number | null>(null) | ||||
|   const shellContext = useContext(ShellContext) | ||||
|   const { getDisplayUsername } = usePeerNameDisplay() | ||||
|   const { offeredFileId } = peer | ||||
| 
 | ||||
|   const onProgress = (progress: number) => { | ||||
| @ -67,7 +68,9 @@ export const PeerDownloadFileButton = ({ | ||||
|         /> | ||||
|       ) : ( | ||||
|         <Tooltip | ||||
|           title={`Download files being offered by ${getPeerName(peer.userId)}`} | ||||
|           title={`Download files being offered by ${getDisplayUsername( | ||||
|             peer.userId | ||||
|           )}`}
 | ||||
|         > | ||||
|           <Fab color="primary" size="small" onClick={handleDownloadFileClick}> | ||||
|             <Download /> | ||||
|  | ||||
| @ -12,6 +12,7 @@ import ListItem from '@mui/material/ListItem' | ||||
| import { PeerListHeader } from 'components/Shell/PeerListHeader' | ||||
| import { AudioVolume } from 'components/AudioVolume' | ||||
| import { PeerNameDisplay } from 'components/PeerNameDisplay' | ||||
| import { Username } from 'components/Username/Username' | ||||
| import { AudioState, Peer } from 'models/chat' | ||||
| 
 | ||||
| import { PeerDownloadFileButton } from './PeerDownloadFileButton' | ||||
| @ -67,7 +68,7 @@ export const PeerList = ({ | ||||
|             </ListItemIcon> | ||||
|           )} | ||||
|           <ListItemText> | ||||
|             <PeerNameDisplay>{userId}</PeerNameDisplay> (you) | ||||
|             <Username userId={userId} /> | ||||
|           </ListItemText> | ||||
|         </ListItem> | ||||
|         {peerList.map((peer: Peer) => ( | ||||
|  | ||||
| @ -1,13 +1,27 @@ | ||||
| import { waitFor, render, screen } from '@testing-library/react' | ||||
| import userEvent from '@testing-library/user-event' | ||||
| import { SettingsContext } from 'contexts/SettingsContext' | ||||
| import { MemoryRouter as Router } from 'react-router-dom' | ||||
| import { userSettingsContextStubFactory } from 'test-utils/stubs/settingsContext' | ||||
| 
 | ||||
| import { Shell, ShellProps } from './Shell' | ||||
| 
 | ||||
| const ShellStub = (overrides: Partial<ShellProps> = {}) => { | ||||
| const mockUserPeerId = 'abc123' | ||||
| 
 | ||||
| const userSettingsStub = userSettingsContextStubFactory({ | ||||
|   userId: mockUserPeerId, | ||||
| }) | ||||
| 
 | ||||
| const ShellStub = (shellProps: Partial<ShellProps> = {}) => { | ||||
|   return ( | ||||
|     <Router> | ||||
|       <Shell appNeedsUpdate={false} userPeerId="abc123" {...overrides} /> | ||||
|       <SettingsContext.Provider value={userSettingsStub}> | ||||
|         <Shell | ||||
|           appNeedsUpdate={false} | ||||
|           userPeerId={mockUserPeerId} | ||||
|           {...shellProps} | ||||
|         /> | ||||
|       </SettingsContext.Provider> | ||||
|     </Router> | ||||
|   ) | ||||
| } | ||||
|  | ||||
| @ -33,7 +33,7 @@ export interface ShellProps extends PropsWithChildren { | ||||
| } | ||||
| 
 | ||||
| export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|   const settingsContext = useContext(SettingsContext) | ||||
|   const { getUserSettings, updateUserSettings } = useContext(SettingsContext) | ||||
|   const [isAlertShowing, setIsAlertShowing] = useState(false) | ||||
|   const [isDrawerOpen, setIsDrawerOpen] = useState(false) | ||||
|   const [isQRCodeDialogOpen, setIsQRCodeDialogOpen] = useState(false) | ||||
| @ -56,6 +56,9 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|   const [screenState, setScreenState] = useState<ScreenShareState>( | ||||
|     ScreenShareState.NOT_SHARING | ||||
|   ) | ||||
|   const [customUsername, setCustomUsername] = useState( | ||||
|     getUserSettings().customUsername | ||||
|   ) | ||||
|   const [peerAudios, setPeerAudios] = useState< | ||||
|     Record<string, HTMLAudioElement> | ||||
|   >({}) | ||||
| @ -94,6 +97,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|       setScreenState, | ||||
|       peerAudios, | ||||
|       setPeerAudios, | ||||
|       customUsername, | ||||
|       setCustomUsername, | ||||
|     }), | ||||
|     [ | ||||
|       isPeerListOpen, | ||||
| @ -119,10 +124,12 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|       setScreenState, | ||||
|       peerAudios, | ||||
|       setPeerAudios, | ||||
|       customUsername, | ||||
|       setCustomUsername, | ||||
|     ] | ||||
|   ) | ||||
| 
 | ||||
|   const colorMode = settingsContext.getUserSettings().colorMode | ||||
|   const { colorMode } = getUserSettings() | ||||
| 
 | ||||
|   const theme = useMemo( | ||||
|     () => | ||||
| @ -145,6 +152,12 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|     setIsAlertShowing(false) | ||||
|   } | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     if (customUsername === getUserSettings().customUsername) return | ||||
| 
 | ||||
|     updateUserSettings({ customUsername }) | ||||
|   }, [customUsername, getUserSettings, updateUserSettings]) | ||||
| 
 | ||||
|   useEffect(() => { | ||||
|     document.title = title | ||||
|   }, [title]) | ||||
| @ -314,7 +327,6 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { | ||||
|             onHomeLinkClick={handleHomeLinkClick} | ||||
|             onSettingsLinkClick={handleSettingsLinkClick} | ||||
|             theme={theme} | ||||
|             userPeerId={userPeerId} | ||||
|           /> | ||||
|           <RouteContent | ||||
|             isDrawerOpen={isDrawerOpen} | ||||
|  | ||||
							
								
								
									
										65
									
								
								src/components/Username/Username.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/components/Username/Username.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,65 @@ | ||||
| import { useState, useContext, ChangeEvent, SyntheticEvent } from 'react' | ||||
| import TextField from '@mui/material/TextField' | ||||
| import FormControl from '@mui/material/FormControl' | ||||
| import FormHelperText from '@mui/material/FormHelperText' | ||||
| 
 | ||||
| import { ShellContext } from 'contexts/ShellContext' | ||||
| import { getPeerName } from 'components/PeerNameDisplay/getPeerName' | ||||
| 
 | ||||
| interface UsernameProps { | ||||
|   userId: string | ||||
| } | ||||
| 
 | ||||
| const maxCustomUsernameLength = 30 | ||||
| 
 | ||||
| export const Username = ({ userId }: UsernameProps) => { | ||||
|   const userName = getPeerName(userId) | ||||
| 
 | ||||
|   const { customUsername, setCustomUsername, showAlert } = | ||||
|     useContext(ShellContext) | ||||
|   const [inflightCustomUsername, setInflightCustomUsername] = | ||||
|     useState(customUsername) | ||||
| 
 | ||||
|   const handleChange = (evt: ChangeEvent<HTMLInputElement>) => { | ||||
|     setInflightCustomUsername(evt.target.value) | ||||
|   } | ||||
| 
 | ||||
|   const updateCustomUsername = () => { | ||||
|     const trimmedUsername = inflightCustomUsername.trim() | ||||
|     setCustomUsername(trimmedUsername) | ||||
| 
 | ||||
|     if (trimmedUsername.length) { | ||||
|       showAlert(`Username changed to "${trimmedUsername}"`, { | ||||
|         severity: 'success', | ||||
|       }) | ||||
|     } else { | ||||
|       showAlert(`Username reset`, { severity: 'success' }) | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   const handleSubmit = (evt: SyntheticEvent<HTMLFormElement>) => { | ||||
|     evt.preventDefault() | ||||
|     updateCustomUsername() | ||||
|   } | ||||
| 
 | ||||
|   const handleBlur = () => { | ||||
|     updateCustomUsername() | ||||
|   } | ||||
| 
 | ||||
|   return ( | ||||
|     <form onSubmit={handleSubmit}> | ||||
|       <FormControl sx={{ width: '100%' }}> | ||||
|         <TextField | ||||
|           onChange={handleChange} | ||||
|           onBlur={handleBlur} | ||||
|           variant="outlined" | ||||
|           label={`${userName}`} | ||||
|           sx={{ width: '100%' }} | ||||
|           value={inflightCustomUsername} | ||||
|           inputProps={{ maxLength: maxCustomUsernameLength }} | ||||
|         /> | ||||
|         <FormHelperText>Your username</FormHelperText> | ||||
|       </FormControl> | ||||
|     </form> | ||||
|   ) | ||||
| } | ||||
							
								
								
									
										1
									
								
								src/components/Username/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/Username/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| export * from './Username' | ||||
| @ -2,7 +2,7 @@ import { createContext } from 'react' | ||||
| 
 | ||||
| import { UserSettings } from 'models/settings' | ||||
| 
 | ||||
| interface SettingsContextProps { | ||||
| export interface SettingsContextProps { | ||||
|   updateUserSettings: (settings: Partial<UserSettings>) => Promise<void> | ||||
|   getUserSettings: () => UserSettings | ||||
| } | ||||
| @ -11,6 +11,7 @@ export const SettingsContext = createContext<SettingsContextProps>({ | ||||
|   updateUserSettings: () => Promise.resolve(), | ||||
|   getUserSettings: () => ({ | ||||
|     userId: '', | ||||
|     customUsername: '', | ||||
|     colorMode: 'dark', | ||||
|     playSoundOnNewMessage: true, | ||||
|     showNotificationOnNewMessage: true, | ||||
|  | ||||
| @ -28,6 +28,8 @@ interface ShellContextProps { | ||||
|   setScreenState: Dispatch<SetStateAction<ScreenShareState>> | ||||
|   peerAudios: Record<string, HTMLAudioElement> | ||||
|   setPeerAudios: Dispatch<SetStateAction<Record<string, HTMLAudioElement>>> | ||||
|   customUsername: string | ||||
|   setCustomUsername: Dispatch<SetStateAction<string>> | ||||
| } | ||||
| 
 | ||||
| export const ShellContext = createContext<ShellContextProps>({ | ||||
| @ -55,4 +57,6 @@ export const ShellContext = createContext<ShellContextProps>({ | ||||
|   setScreenState: () => {}, | ||||
|   peerAudios: {}, | ||||
|   setPeerAudios: () => {}, | ||||
|   customUsername: '', | ||||
|   setCustomUsername: () => {}, | ||||
| }) | ||||
|  | ||||
| @ -44,6 +44,7 @@ export enum ScreenShareState { | ||||
| export interface Peer { | ||||
|   peerId: string | ||||
|   userId: string | ||||
|   customUsername: string | ||||
|   audioState: AudioState | ||||
|   videoState: VideoState | ||||
|   screenShareState: ScreenShareState | ||||
|  | ||||
| @ -3,7 +3,7 @@ export enum PeerActions { | ||||
|   MESSAGE = 'MESSAGE', | ||||
|   MEDIA_MESSAGE = 'MEDIA_MSG', | ||||
|   MESSAGE_TRANSCRIPT = 'MSG_XSCRIPT', | ||||
|   PEER_NAME = 'PEER_NAME', | ||||
|   PEER_METADATA = 'PEER_META', | ||||
|   AUDIO_CHANGE = 'AUDIO_CHANGE', | ||||
|   VIDEO_CHANGE = 'VIDEO_CHANGE', | ||||
|   SCREEN_SHARE = 'SCREEN_SHARE', | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| export interface UserSettings { | ||||
|   colorMode: 'dark' | 'light' | ||||
|   userId: string | ||||
|   customUsername: string | ||||
|   playSoundOnNewMessage: boolean | ||||
|   showNotificationOnNewMessage: boolean | ||||
| } | ||||
|  | ||||
| @ -58,7 +58,7 @@ export function Home({ userId }: HomeProps) { | ||||
|         </Link> | ||||
|         <form onSubmit={handleFormSubmit} className="max-w-xl mx-auto"> | ||||
|           <Typography sx={{ mb: 2 }}> | ||||
|             Your user name:{' '} | ||||
|             Your username:{' '} | ||||
|             <PeerNameDisplay paragraph={false} sx={{ fontWeight: 'bold' }}> | ||||
|               {userId} | ||||
|             </PeerNameDisplay> | ||||
|  | ||||
							
								
								
									
										20
									
								
								src/test-utils/stubs/settingsContext.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/test-utils/stubs/settingsContext.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | ||||
| import { SettingsContextProps } from 'contexts/SettingsContext' | ||||
| import { UserSettings } from 'models/settings' | ||||
| 
 | ||||
| export const userSettingsContextStubFactory = ( | ||||
|   userSettingsOverrides: Partial<UserSettings> = {} | ||||
| ) => { | ||||
|   const userSettingsStub: SettingsContextProps = { | ||||
|     updateUserSettings: () => Promise.resolve(), | ||||
|     getUserSettings: () => ({ | ||||
|       userId: '', | ||||
|       customUsername: '', | ||||
|       colorMode: 'dark', | ||||
|       playSoundOnNewMessage: true, | ||||
|       showNotificationOnNewMessage: true, | ||||
|       ...userSettingsOverrides, | ||||
|     }), | ||||
|   } | ||||
| 
 | ||||
|   return userSettingsStub | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user