feat: [closes #14] Display peer list (#28)

* feat: [#14] Display peer list

Co-authored-by: Jeremy Kahn <jeremyckahn@gmail.com>
This commit is contained in:
Flaykz 2022-10-05 01:08:38 +11:00 committed by GitHub
parent c38a203f07
commit 26618c0309
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 247 additions and 30 deletions

View File

@ -9,9 +9,10 @@ import { ShellContext } from 'contexts/ShellContext'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { usePeerRoom, usePeerRoomAction } from 'hooks/usePeerRoom' import { usePeerRoom, usePeerRoomAction } from 'hooks/usePeerRoom'
import { PeerActions } from 'models/network' import { PeerActions } from 'models/network'
import { UnsentMessage, ReceivedMessage } from 'models/chat' import { Peer, ReceivedMessage, UnsentMessage } from 'models/chat'
import { MessageForm } from 'components/MessageForm' import { MessageForm } from 'components/MessageForm'
import { ChatTranscript } from 'components/ChatTranscript' import { ChatTranscript } from 'components/ChatTranscript'
import { funAnimalName } from 'fun-animal-names'
import { getPeerName } from 'components/PeerNameDisplay' import { getPeerName } from 'components/PeerNameDisplay'
import { NotificationService } from 'services/Notification' import { NotificationService } from 'services/Notification'
import { Audio } from 'services/Audio' import { Audio } from 'services/Audio'
@ -49,10 +50,15 @@ export function Room({
roomId roomId
) )
const [sendPeerId, receivePeerId] = usePeerRoomAction<string>(
peerRoom,
PeerActions.PEER_NAME
)
useEffect(() => { useEffect(() => {
shellContext.setDoShowPeers(true) shellContext.setDoShowPeers(true)
peerRoom.onPeerJoin(() => { peerRoom.onPeerJoin((peerId: string) => {
shellContext.showAlert(`Someone has joined the room`, { shellContext.showAlert(`Someone has joined the room`, {
severity: 'success', severity: 'success',
}) })
@ -60,22 +66,53 @@ export function Room({
const newNumberOfPeers = numberOfPeers + 1 const newNumberOfPeers = numberOfPeers + 1
setNumberOfPeers(newNumberOfPeers) setNumberOfPeers(newNumberOfPeers)
shellContext.setNumberOfPeers(newNumberOfPeers) shellContext.setNumberOfPeers(newNumberOfPeers)
;(async () => {
try {
await sendPeerId(userId, peerId)
} catch (e) {
console.error(e)
}
})()
}) })
peerRoom.onPeerLeave(() => { peerRoom.onPeerLeave((peerId: string) => {
shellContext.showAlert(`Someone has left the room`, { const peerIndex = shellContext.peerList.findIndex(
peer => peer.peerId === peerId
)
const peerExist = peerIndex !== -1
shellContext.showAlert(
`${
peerExist
? funAnimalName(shellContext.peerList[peerIndex].userId)
: 'Someone'
} has left the room`,
{
severity: 'warning', severity: 'warning',
}) }
)
const newNumberOfPeers = numberOfPeers - 1 const newNumberOfPeers = numberOfPeers - 1
setNumberOfPeers(newNumberOfPeers) setNumberOfPeers(newNumberOfPeers)
shellContext.setNumberOfPeers(newNumberOfPeers) shellContext.setNumberOfPeers(newNumberOfPeers)
if (peerExist) {
const peerListClone = [...shellContext.peerList]
peerListClone.splice(peerIndex, 1)
shellContext.setPeerList(peerListClone)
}
}) })
return () => { return () => {
shellContext.setDoShowPeers(false) shellContext.setDoShowPeers(false)
} }
}, [numberOfPeers, peerRoom, shellContext]) }, [
numberOfPeers,
shellContext.peerList,
peerRoom,
sendPeerId,
shellContext,
userId,
])
const [sendMessage, receiveMessage] = usePeerRoomAction<UnsentMessage>( const [sendMessage, receiveMessage] = usePeerRoomAction<UnsentMessage>(
peerRoom, peerRoom,
@ -103,6 +140,26 @@ export function Room({
setIsMessageSending(false) setIsMessageSending(false)
} }
const upsertToPeerList = (peerToAdd: Peer) => {
const peerIndex = shellContext.peerList.findIndex(
peer => peer.peerId === peerToAdd.peerId
)
if (peerIndex === -1) {
shellContext.setPeerList([
...shellContext.peerList,
{ peerId: peerToAdd.peerId, userId: peerToAdd.userId },
])
} else {
const peerListClone = [...shellContext.peerList]
peerListClone[peerIndex].userId = peerToAdd.userId
shellContext.setPeerList(peerListClone)
}
}
receivePeerId((userId: string, peerId?: string) => {
if (peerId) upsertToPeerList({ peerId, userId })
})
receiveMessage(message => { receiveMessage(message => {
const userSettings = settingsContext.getUserSettings() const userSettings = settingsContext.getUserSettings()

View File

@ -0,0 +1,65 @@
import { PropsWithChildren } from 'react'
import MuiDrawer from '@mui/material/Drawer'
import List from '@mui/material/List'
import Divider from '@mui/material/Divider'
import IconButton from '@mui/material/IconButton'
import ChevronRightIcon from '@mui/icons-material/ChevronRight'
import ListItemButton from '@mui/material/ListItemButton'
import Typography from '@mui/material/Typography'
import { PeerListHeader } from 'components/Shell/PeerListHeader'
import { PeerNameDisplay } from 'components/PeerNameDisplay'
import { Peer } from 'models/chat'
export const peerListWidth = 240
export interface PeerListProps extends PropsWithChildren {
userId: string
isPeerListOpen: boolean
onPeerListClose: () => void
peerList: Peer[]
}
export const PeerList = ({
userId,
isPeerListOpen,
onPeerListClose,
peerList,
}: PeerListProps) => {
return (
<MuiDrawer
sx={{
width: peerListWidth,
flexShrink: 0,
'& .MuiDrawer-paper': {
width: peerListWidth,
boxSizing: 'border-box',
},
}}
variant="persistent"
anchor="right"
open={isPeerListOpen}
>
<PeerListHeader>
<IconButton onClick={onPeerListClose} aria-label="Close peer list">
<ChevronRightIcon />
</IconButton>
</PeerListHeader>
<Divider />
<List>
<ListItemButton>
<Typography>
<PeerNameDisplay>{userId}</PeerNameDisplay> (you)
</Typography>
</ListItemButton>
{peerList.map((peer: Peer) => (
<ListItemButton key={peer.peerId}>
<PeerNameDisplay>{peer.userId}</PeerNameDisplay>
</ListItemButton>
))}
</List>
<Divider />
</MuiDrawer>
)
}

View File

@ -0,0 +1,10 @@
import { styled } from '@mui/material/styles'
export const PeerListHeader = styled('div')(({ theme }) => ({
display: 'flex',
alignItems: 'center',
padding: theme.spacing(0, 1),
// necessary for content to be below app bar
...theme.mixins.toolbar,
justifyContent: 'flex-start',
}))

View File

@ -4,32 +4,51 @@ import { styled } from '@mui/material/styles'
import { DrawerHeader } from './DrawerHeader' import { DrawerHeader } from './DrawerHeader'
import { drawerWidth } from './Drawer' import { drawerWidth } from './Drawer'
import { peerListWidth } from './PeerList'
const Main = styled('main', { shouldForwardProp: prop => prop !== 'open' })<{ const Main = styled('main', {
open?: boolean shouldForwardProp: prop =>
}>(({ theme, open }) => ({ prop !== 'isDrawerOpen' && prop !== 'isPeerListOpen',
})<{
isDrawerOpen?: boolean
isPeerListOpen?: boolean
}>(({ theme, isDrawerOpen, isPeerListOpen }) => ({
transition: theme.transitions.create('margin', { transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen, duration: theme.transitions.duration.leavingScreen,
}), }),
marginLeft: `-${drawerWidth}px`, marginLeft: `-${drawerWidth}px`,
...(open && { marginRight: `-${peerListWidth}px`,
...(isDrawerOpen && {
transition: theme.transitions.create('margin', { transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut, easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen, duration: theme.transitions.duration.enteringScreen,
}), }),
marginLeft: 0, marginLeft: 0,
}), }),
...(isPeerListOpen && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginRight: 0,
}),
})) }))
interface RouteContentProps extends PropsWithChildren { interface RouteContentProps extends PropsWithChildren {
isDrawerOpen: boolean isDrawerOpen: boolean
isPeerListOpen: boolean
} }
export const RouteContent = ({ children, isDrawerOpen }: RouteContentProps) => { export const RouteContent = ({
children,
isDrawerOpen,
isPeerListOpen,
}: RouteContentProps) => {
return ( return (
<Main <Main
open={isDrawerOpen} isDrawerOpen={isDrawerOpen}
isPeerListOpen={isPeerListOpen}
sx={{ sx={{
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',

View File

@ -15,6 +15,7 @@ import { AlertColor } from '@mui/material/Alert'
import { ShellContext } from 'contexts/ShellContext' import { ShellContext } from 'contexts/ShellContext'
import { SettingsContext } from 'contexts/SettingsContext' import { SettingsContext } from 'contexts/SettingsContext'
import { AlertOptions } from 'models/shell' import { AlertOptions } from 'models/shell'
import { Peer } from 'models/chat'
import { ErrorBoundary } from 'components/ErrorBoundary' import { ErrorBoundary } from 'components/ErrorBoundary'
import { Drawer } from './Drawer' import { Drawer } from './Drawer'
@ -22,6 +23,7 @@ import { UpgradeDialog } from './UpgradeDialog'
import { ShellAppBar } from './ShellAppBar' import { ShellAppBar } from './ShellAppBar'
import { NotificationArea } from './NotificationArea' import { NotificationArea } from './NotificationArea'
import { RouteContent } from './RouteContent' import { RouteContent } from './RouteContent'
import { PeerList } from './PeerList'
export interface ShellProps extends PropsWithChildren { export interface ShellProps extends PropsWithChildren {
userPeerId: string userPeerId: string
@ -37,6 +39,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
const [title, setTitle] = useState('') const [title, setTitle] = useState('')
const [alertText, setAlertText] = useState('') const [alertText, setAlertText] = useState('')
const [numberOfPeers, setNumberOfPeers] = useState(1) const [numberOfPeers, setNumberOfPeers] = useState(1)
const [isPeerListOpen, setIsPeerListOpen] = useState(false)
const [peerList, setPeerList] = useState<Peer[]>([]) // except you
const [tabHasFocus, setTabHasFocus] = useState(true) const [tabHasFocus, setTabHasFocus] = useState(true)
const showAlert = useCallback< const showAlert = useCallback<
@ -55,9 +59,15 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setNumberOfPeers, setNumberOfPeers,
setTitle, setTitle,
showAlert, showAlert,
isPeerListOpen,
setIsPeerListOpen,
peerList,
setPeerList,
}), }),
[ [
isPeerListOpen,
numberOfPeers, numberOfPeers,
peerList,
tabHasFocus, tabHasFocus,
setDoShowPeers, setDoShowPeers,
setNumberOfPeers, setNumberOfPeers,
@ -112,6 +122,10 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setIsDrawerOpen(true) setIsDrawerOpen(true)
} }
const handlePeerListOpen = () => {
setIsPeerListOpen(true)
}
const handleLinkButtonClick = async () => { const handleLinkButtonClick = async () => {
await navigator.clipboard.writeText(window.location.href) await navigator.clipboard.writeText(window.location.href)
@ -124,6 +138,10 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
setIsDrawerOpen(false) setIsDrawerOpen(false)
} }
const handlePeerListClose = () => {
setIsPeerListOpen(false)
}
const handleHomeLinkClick = () => { const handleHomeLinkClick = () => {
setIsDrawerOpen(false) setIsDrawerOpen(false)
} }
@ -156,11 +174,13 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
/> />
<ShellAppBar <ShellAppBar
doShowPeers={doShowPeers} doShowPeers={doShowPeers}
handleDrawerOpen={handleDrawerOpen} onDrawerOpen={handleDrawerOpen}
handleLinkButtonClick={handleLinkButtonClick} onLinkButtonClick={handleLinkButtonClick}
isDrawerOpen={isDrawerOpen} isDrawerOpen={isDrawerOpen}
isPeerListOpen={isPeerListOpen}
numberOfPeers={numberOfPeers} numberOfPeers={numberOfPeers}
title={title} title={title}
onPeerListOpen={handlePeerListOpen}
/> />
<Drawer <Drawer
isDrawerOpen={isDrawerOpen} isDrawerOpen={isDrawerOpen}
@ -171,10 +191,18 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
theme={theme} theme={theme}
userPeerId={userPeerId} userPeerId={userPeerId}
/> />
<RouteContent
<RouteContent isDrawerOpen={isDrawerOpen}> isDrawerOpen={isDrawerOpen}
isPeerListOpen={isPeerListOpen}
>
<ErrorBoundary>{children}</ErrorBoundary> <ErrorBoundary>{children}</ErrorBoundary>
</RouteContent> </RouteContent>
<PeerList
userId={userPeerId}
isPeerListOpen={isPeerListOpen}
onPeerListClose={handlePeerListClose}
peerList={peerList}
/>
</Box> </Box>
</ThemeProvider> </ThemeProvider>
</ShellContext.Provider> </ShellContext.Provider>

View File

@ -9,19 +9,22 @@ import MenuIcon from '@mui/icons-material/Menu'
import LinkIcon from '@mui/icons-material/Link' import LinkIcon from '@mui/icons-material/Link'
import { drawerWidth } from './Drawer' import { drawerWidth } from './Drawer'
import { peerListWidth } from './PeerList'
interface AppBarProps extends MuiAppBarProps { interface AppBarProps extends MuiAppBarProps {
open?: boolean isDrawerOpen?: boolean
isPeerListOpen?: boolean
} }
export const AppBar = styled(MuiAppBar, { export const AppBar = styled(MuiAppBar, {
shouldForwardProp: prop => prop !== 'open', shouldForwardProp: prop =>
})<AppBarProps>(({ theme, open }) => ({ prop !== 'isDrawerOpen' && prop !== 'isPeerListOpen',
})<AppBarProps>(({ theme, isDrawerOpen, isPeerListOpen }) => ({
transition: theme.transitions.create(['margin', 'width'], { transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.sharp, easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen, duration: theme.transitions.duration.leavingScreen,
}), }),
...(open && { ...(isDrawerOpen && {
width: `calc(100% - ${drawerWidth}px)`, width: `calc(100% - ${drawerWidth}px)`,
marginLeft: `${drawerWidth}px`, marginLeft: `${drawerWidth}px`,
transition: theme.transitions.create(['margin', 'width'], { transition: theme.transitions.create(['margin', 'width'], {
@ -29,27 +32,43 @@ export const AppBar = styled(MuiAppBar, {
duration: theme.transitions.duration.enteringScreen, duration: theme.transitions.duration.enteringScreen,
}), }),
}), }),
...(isPeerListOpen && {
width: `calc(100% - ${peerListWidth}px)`,
marginRight: `${peerListWidth}px`,
transition: theme.transitions.create(['margin', 'width'], {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
}),
})) }))
interface ShellAppBarProps { interface ShellAppBarProps {
doShowPeers: boolean doShowPeers: boolean
handleDrawerOpen: () => void onDrawerOpen: () => void
handleLinkButtonClick: () => Promise<void> onLinkButtonClick: () => Promise<void>
isDrawerOpen: boolean isDrawerOpen: boolean
isPeerListOpen: boolean
numberOfPeers: number numberOfPeers: number
title: string title: string
onPeerListOpen: () => void
} }
export const ShellAppBar = ({ export const ShellAppBar = ({
doShowPeers, doShowPeers,
handleDrawerOpen, onDrawerOpen,
handleLinkButtonClick, onLinkButtonClick,
isDrawerOpen, isDrawerOpen,
isPeerListOpen,
numberOfPeers, numberOfPeers,
title, title,
onPeerListOpen,
}: ShellAppBarProps) => { }: ShellAppBarProps) => {
return ( return (
<AppBar position="fixed" open={isDrawerOpen}> <AppBar
position="fixed"
isDrawerOpen={isDrawerOpen}
isPeerListOpen={isPeerListOpen}
>
<Toolbar <Toolbar
variant="regular" variant="regular"
sx={{ sx={{
@ -64,7 +83,7 @@ export const ShellAppBar = ({
color="inherit" color="inherit"
aria-label="Open menu" aria-label="Open menu"
sx={{ mr: 2, ...(isDrawerOpen && { display: 'none' }) }} sx={{ mr: 2, ...(isDrawerOpen && { display: 'none' }) }}
onClick={handleDrawerOpen} onClick={onDrawerOpen}
> >
<MenuIcon /> <MenuIcon />
</IconButton> </IconButton>
@ -83,14 +102,18 @@ export const ShellAppBar = ({
color="inherit" color="inherit"
aria-label="Copy current URL" aria-label="Copy current URL"
sx={{ ml: 'auto' }} sx={{ ml: 'auto' }}
onClick={handleLinkButtonClick} onClick={onLinkButtonClick}
> >
<LinkIcon /> <LinkIcon />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
{doShowPeers ? ( {doShowPeers ? (
<Tooltip title="Number of peers in the room"> <Tooltip title="Click to show peer list">
<StepIcon icon={numberOfPeers} sx={{ ml: 2 }} /> <StepIcon
icon={numberOfPeers}
onClick={onPeerListOpen}
sx={{ ml: 2 }}
/>
</Tooltip> </Tooltip>
) : null} ) : null}
</Toolbar> </Toolbar>

View File

@ -1,6 +1,7 @@
import { createContext, Dispatch, SetStateAction } from 'react' import { createContext, Dispatch, SetStateAction } from 'react'
import { AlertOptions } from 'models/shell' import { AlertOptions } from 'models/shell'
import { Peer } from 'models/chat'
interface ShellContextProps { interface ShellContextProps {
numberOfPeers: number numberOfPeers: number
@ -9,6 +10,10 @@ interface ShellContextProps {
setNumberOfPeers: Dispatch<SetStateAction<number>> setNumberOfPeers: Dispatch<SetStateAction<number>>
setTitle: Dispatch<SetStateAction<string>> setTitle: Dispatch<SetStateAction<string>>
showAlert: (message: string, options?: AlertOptions) => void showAlert: (message: string, options?: AlertOptions) => void
isPeerListOpen: boolean
setIsPeerListOpen: Dispatch<SetStateAction<boolean>>
peerList: Peer[]
setPeerList: Dispatch<SetStateAction<Peer[]>>
} }
export const ShellContext = createContext<ShellContextProps>({ export const ShellContext = createContext<ShellContextProps>({
@ -18,4 +23,8 @@ export const ShellContext = createContext<ShellContextProps>({
setNumberOfPeers: () => {}, setNumberOfPeers: () => {},
setTitle: () => {}, setTitle: () => {},
showAlert: () => {}, showAlert: () => {},
isPeerListOpen: false,
setIsPeerListOpen: () => {},
peerList: [],
setPeerList: () => {},
}) })

View File

@ -5,6 +5,11 @@ export interface UnsentMessage {
authorId: string authorId: string
} }
export interface Peer {
peerId: string
userId: string
}
export interface ReceivedMessage extends UnsentMessage { export interface ReceivedMessage extends UnsentMessage {
timeReceived: number timeReceived: number
} }

View File

@ -1,3 +1,4 @@
export enum PeerActions { export enum PeerActions {
MESSAGE = 'MESSAGE', MESSAGE = 'MESSAGE',
PEER_NAME = 'PEER_NAME',
} }