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