diff --git a/src/components/Room/Room.tsx b/src/components/Room/Room.tsx index f8c100c..b6194df 100644 --- a/src/components/Room/Room.tsx +++ b/src/components/Room/Room.tsx @@ -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( + 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( 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() diff --git a/src/components/Shell/PeerList.tsx b/src/components/Shell/PeerList.tsx new file mode 100644 index 0000000..ea13f92 --- /dev/null +++ b/src/components/Shell/PeerList.tsx @@ -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 ( + + + + + + + + + + + {userId} (you) + + + {peerList.map((peer: Peer) => ( + + {peer.userId} + + ))} + + + + ) +} diff --git a/src/components/Shell/PeerListHeader.tsx b/src/components/Shell/PeerListHeader.tsx new file mode 100644 index 0000000..ad82d69 --- /dev/null +++ b/src/components/Shell/PeerListHeader.tsx @@ -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', +})) diff --git a/src/components/Shell/RouteContent.tsx b/src/components/Shell/RouteContent.tsx index 6db90f6..3870bc6 100644 --- a/src/components/Shell/RouteContent.tsx +++ b/src/components/Shell/RouteContent.tsx @@ -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 (
{ const [title, setTitle] = useState('') const [alertText, setAlertText] = useState('') const [numberOfPeers, setNumberOfPeers] = useState(1) + const [isPeerListOpen, setIsPeerListOpen] = useState(false) + const [peerList, setPeerList] = useState([]) // 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) => { /> { theme={theme} userPeerId={userPeerId} /> - - + {children} + diff --git a/src/components/Shell/ShellAppBar.tsx b/src/components/Shell/ShellAppBar.tsx index b68edca..ee9074a 100644 --- a/src/components/Shell/ShellAppBar.tsx +++ b/src/components/Shell/ShellAppBar.tsx @@ -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', -})(({ theme, open }) => ({ + shouldForwardProp: prop => + prop !== 'isDrawerOpen' && prop !== 'isPeerListOpen', +})(({ 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 + onDrawerOpen: () => void + onLinkButtonClick: () => Promise 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 ( - + @@ -83,14 +102,18 @@ export const ShellAppBar = ({ color="inherit" aria-label="Copy current URL" sx={{ ml: 'auto' }} - onClick={handleLinkButtonClick} + onClick={onLinkButtonClick} > {doShowPeers ? ( - - + + ) : null} diff --git a/src/contexts/ShellContext.ts b/src/contexts/ShellContext.ts index beedb52..5df57a9 100644 --- a/src/contexts/ShellContext.ts +++ b/src/contexts/ShellContext.ts @@ -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> setTitle: Dispatch> showAlert: (message: string, options?: AlertOptions) => void + isPeerListOpen: boolean + setIsPeerListOpen: Dispatch> + peerList: Peer[] + setPeerList: Dispatch> } export const ShellContext = createContext({ @@ -18,4 +23,8 @@ export const ShellContext = createContext({ setNumberOfPeers: () => {}, setTitle: () => {}, showAlert: () => {}, + isPeerListOpen: false, + setIsPeerListOpen: () => {}, + peerList: [], + setPeerList: () => {}, }) diff --git a/src/models/chat.ts b/src/models/chat.ts index f33d918..f7bb210 100644 --- a/src/models/chat.ts +++ b/src/models/chat.ts @@ -5,6 +5,11 @@ export interface UnsentMessage { authorId: string } +export interface Peer { + peerId: string + userId: string +} + export interface ReceivedMessage extends UnsentMessage { timeReceived: number } diff --git a/src/models/network.ts b/src/models/network.ts index c997c7e..e5afb6e 100644 --- a/src/models/network.ts +++ b/src/models/network.ts @@ -1,3 +1,4 @@ export enum PeerActions { MESSAGE = 'MESSAGE', + PEER_NAME = 'PEER_NAME', }