* 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…
Reference in New Issue
Block a user