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 { usePeerRoom, usePeerRoomAction } from 'hooks/usePeerRoom'
import { PeerActions } from 'models/network'
import { UnsentMessage, ReceivedMessage } from 'models/chat'
import { Peer, ReceivedMessage, UnsentMessage } from 'models/chat'
import { MessageForm } from 'components/MessageForm'
import { ChatTranscript } from 'components/ChatTranscript'
import { funAnimalName } from 'fun-animal-names'
import { getPeerName } from 'components/PeerNameDisplay'
import { NotificationService } from 'services/Notification'
import { Audio } from 'services/Audio'
@ -49,10 +50,15 @@ export function Room({
roomId
)
const [sendPeerId, receivePeerId] = usePeerRoomAction<string>(
peerRoom,
PeerActions.PEER_NAME
)
useEffect(() => {
shellContext.setDoShowPeers(true)
peerRoom.onPeerJoin(() => {
peerRoom.onPeerJoin((peerId: string) => {
shellContext.showAlert(`Someone has joined the room`, {
severity: 'success',
})
@ -60,22 +66,53 @@ export function Room({
const newNumberOfPeers = numberOfPeers + 1
setNumberOfPeers(newNumberOfPeers)
shellContext.setNumberOfPeers(newNumberOfPeers)
;(async () => {
try {
await sendPeerId(userId, peerId)
} catch (e) {
console.error(e)
}
})()
})
peerRoom.onPeerLeave(() => {
shellContext.showAlert(`Someone has left the room`, {
severity: 'warning',
})
peerRoom.onPeerLeave((peerId: string) => {
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',
}
)
const newNumberOfPeers = numberOfPeers - 1
setNumberOfPeers(newNumberOfPeers)
shellContext.setNumberOfPeers(newNumberOfPeers)
if (peerExist) {
const peerListClone = [...shellContext.peerList]
peerListClone.splice(peerIndex, 1)
shellContext.setPeerList(peerListClone)
}
})
return () => {
shellContext.setDoShowPeers(false)
}
}, [numberOfPeers, peerRoom, shellContext])
}, [
numberOfPeers,
shellContext.peerList,
peerRoom,
sendPeerId,
shellContext,
userId,
])
const [sendMessage, receiveMessage] = usePeerRoomAction<UnsentMessage>(
peerRoom,
@ -103,6 +140,26 @@ export function Room({
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 => {
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 { drawerWidth } from './Drawer'
import { peerListWidth } from './PeerList'
const Main = styled('main', { shouldForwardProp: prop => prop !== 'open' })<{
open?: boolean
}>(({ theme, open }) => ({
const Main = styled('main', {
shouldForwardProp: prop =>
prop !== 'isDrawerOpen' && prop !== 'isPeerListOpen',
})<{
isDrawerOpen?: boolean
isPeerListOpen?: boolean
}>(({ theme, isDrawerOpen, isPeerListOpen }) => ({
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
marginLeft: `-${drawerWidth}px`,
...(open && {
marginRight: `-${peerListWidth}px`,
...(isDrawerOpen && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginLeft: 0,
}),
...(isPeerListOpen && {
transition: theme.transitions.create('margin', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
marginRight: 0,
}),
}))
interface RouteContentProps extends PropsWithChildren {
isDrawerOpen: boolean
isPeerListOpen: boolean
}
export const RouteContent = ({ children, isDrawerOpen }: RouteContentProps) => {
export const RouteContent = ({
children,
isDrawerOpen,
isPeerListOpen,
}: RouteContentProps) => {
return (
<Main
open={isDrawerOpen}
isDrawerOpen={isDrawerOpen}
isPeerListOpen={isPeerListOpen}
sx={{
display: 'flex',
flexDirection: 'column',

View File

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

View File

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

View File

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

View File

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

View File

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