From 4cf75b15b0e9ac6dc131e0c302250bd2bb648e14 Mon Sep 17 00:00:00 2001 From: Jeremy Kahn Date: Sat, 25 Mar 2023 14:40:07 -0500 Subject: [PATCH] feat: [closes #15] Show connection type (#117) * feat: [#15] implement getPeerConnectionTypes * feat: [#15] display connection type icon * refactor: extract PeerListItem to its own file * feat: [#15] show connection details via tooltip * fix: style stable peer name --- .../PeerNameDisplay/PeerNameDisplay.tsx | 5 +- src/components/Room/useRoom.ts | 7 ++ src/components/Shell/PeerList.tsx | 22 +++--- src/components/Shell/PeerListItem.tsx | 76 +++++++++++++++++++ src/components/Shell/Shell.tsx | 9 +++ src/contexts/ShellContext.ts | 7 ++ src/services/PeerRoom/PeerRoom.ts | 40 ++++++++++ 7 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 src/components/Shell/PeerListItem.tsx diff --git a/src/components/PeerNameDisplay/PeerNameDisplay.tsx b/src/components/PeerNameDisplay/PeerNameDisplay.tsx index b6cb994..5e1f8f6 100644 --- a/src/components/PeerNameDisplay/PeerNameDisplay.tsx +++ b/src/components/PeerNameDisplay/PeerNameDisplay.tsx @@ -20,7 +20,10 @@ export const PeerNameDisplay = ({ return ( {friendlyName} - ({getPeerName(userId)}) + + {' '} + ({getPeerName(userId)}) + ) } else { diff --git a/src/components/Room/useRoom.ts b/src/components/Room/useRoom.ts index d93ed46..cf7dfc4 100644 --- a/src/components/Room/useRoom.ts +++ b/src/components/Room/useRoom.ts @@ -54,6 +54,7 @@ export function useRoom( const { peerList, setPeerList, + setPeerConnectionTypes, tabHasFocus, showAlert, setRoomId, @@ -368,6 +369,12 @@ export function useRoom( sendPeerMetadata({ customUsername, userId }) }, [customUsername, userId, sendPeerMetadata]) + useEffect(() => { + ;(async () => { + setPeerConnectionTypes(await peerRoom.getPeerConnectionTypes()) + })() + }, [peerList, peerRoom, setPeerConnectionTypes]) + return { isPrivate, handleInlineMediaUpload, diff --git a/src/components/Shell/PeerList.tsx b/src/components/Shell/PeerList.tsx index 242c8d4..4b2b951 100644 --- a/src/components/Shell/PeerList.tsx +++ b/src/components/Shell/PeerList.tsx @@ -10,12 +10,11 @@ import VolumeUp from '@mui/icons-material/VolumeUp' 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 { PeerConnectionType } from 'services/PeerRoom/PeerRoom' -import { PeerDownloadFileButton } from './PeerDownloadFileButton' +import { PeerListItem } from './PeerListItem' export const peerListWidth = 300 @@ -24,6 +23,7 @@ export interface PeerListProps extends PropsWithChildren { isPeerListOpen: boolean onPeerListClose: () => void peerList: Peer[] + peerConnectionTypes: Record audioState: AudioState peerAudios: Record } @@ -33,6 +33,7 @@ export const PeerList = ({ isPeerListOpen, onPeerListClose, peerList, + peerConnectionTypes, audioState, peerAudios, }: PeerListProps) => { @@ -72,15 +73,12 @@ export const PeerList = ({ {peerList.map((peer: Peer) => ( - - - - {peer.userId} - {peer.peerId in peerAudios && ( - - )} - - + ))} diff --git a/src/components/Shell/PeerListItem.tsx b/src/components/Shell/PeerListItem.tsx new file mode 100644 index 0000000..e425fcd --- /dev/null +++ b/src/components/Shell/PeerListItem.tsx @@ -0,0 +1,76 @@ +import { Box } from '@mui/system' +import ListItemText from '@mui/material/ListItemText' +import SyncAltIcon from '@mui/icons-material/SyncAlt' +import NetworkPingIcon from '@mui/icons-material/NetworkPing' +import ListItem from '@mui/material/ListItem' +import { Tooltip } from '@mui/material' + +import { AudioVolume } from 'components/AudioVolume' +import { PeerNameDisplay } from 'components/PeerNameDisplay' +import { Peer } from 'models/chat' +import { PeerConnectionType } from 'services/PeerRoom/PeerRoom' + +import { PeerDownloadFileButton } from './PeerDownloadFileButton' + +interface PeerListItemProps { + peer: Peer + peerConnectionTypes: Record + peerAudios: Record +} +export const PeerListItem = ({ + peer, + peerConnectionTypes, + peerAudios, +}: PeerListItemProps): JSX.Element => { + const hasPeerConnection = peer.peerId in peerConnectionTypes + + const isPeerConnectionDirect = + peerConnectionTypes[peer.peerId] === PeerConnectionType.DIRECT + + return ( + + + + {hasPeerConnection ? ( + + You are connected directly to{' '} + + {peer.userId} + + + ) : ( + <> + You are connected to{' '} + + {peer.userId} + {' '} + via a relay server. Your connection is still private and + encrypted, but performance may be degraded. + + ) + } + > + + {isPeerConnectionDirect ? ( + + ) : ( + + )} + + + ) : null} + {peer.userId} + {peer.peerId in peerAudios && ( + + )} + + + ) +} diff --git a/src/components/Shell/Shell.tsx b/src/components/Shell/Shell.tsx index 791ba32..459d75d 100644 --- a/src/components/Shell/Shell.tsx +++ b/src/components/Shell/Shell.tsx @@ -19,6 +19,8 @@ import { AlertOptions } from 'models/shell' import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat' import { ErrorBoundary } from 'components/ErrorBoundary' +import { PeerConnectionType } from 'services/PeerRoom/PeerRoom' + import { Drawer } from './Drawer' import { UpgradeDialog } from './UpgradeDialog' import { ShellAppBar } from './ShellAppBar' @@ -65,6 +67,9 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { const [password, setPassword] = useState(undefined) const [isPeerListOpen, setIsPeerListOpen] = useState(defaultSidebarsOpen) const [peerList, setPeerList] = useState([]) // except self + const [peerConnectionTypes, setPeerConnectionTypes] = useState< + Record + >({}) const [tabHasFocus, setTabHasFocus] = useState(true) const [audioState, setAudioState] = useState(AudioState.STOPPED) const [videoState, setVideoState] = useState(VideoState.STOPPED) @@ -101,6 +106,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { setIsPeerListOpen, peerList, setPeerList, + peerConnectionTypes, + setPeerConnectionTypes, audioState, setAudioState, videoState, @@ -120,6 +127,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { password, setPassword, peerList, + peerConnectionTypes, tabHasFocus, showRoomControls, setShowRoomControls, @@ -315,6 +323,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => { isPeerListOpen={isPeerListOpen} onPeerListClose={handlePeerListClick} peerList={peerList} + peerConnectionTypes={peerConnectionTypes} audioState={audioState} peerAudios={peerAudios} /> diff --git a/src/contexts/ShellContext.ts b/src/contexts/ShellContext.ts index d2589c2..5c2393d 100644 --- a/src/contexts/ShellContext.ts +++ b/src/contexts/ShellContext.ts @@ -2,6 +2,7 @@ import { createContext, Dispatch, SetStateAction } from 'react' import { AlertOptions } from 'models/shell' import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat' +import { PeerConnectionType } from 'services/PeerRoom/PeerRoom' interface ShellContextProps { tabHasFocus: boolean @@ -17,6 +18,10 @@ interface ShellContextProps { setIsPeerListOpen: Dispatch> peerList: Peer[] setPeerList: Dispatch> + peerConnectionTypes: Record + setPeerConnectionTypes: Dispatch< + SetStateAction> + > audioState: AudioState setAudioState: Dispatch> videoState: VideoState @@ -43,6 +48,8 @@ export const ShellContext = createContext({ setIsPeerListOpen: () => {}, peerList: [], setPeerList: () => {}, + peerConnectionTypes: {}, + setPeerConnectionTypes: () => {}, audioState: AudioState.STOPPED, setAudioState: () => {}, videoState: VideoState.STOPPED, diff --git a/src/services/PeerRoom/PeerRoom.ts b/src/services/PeerRoom/PeerRoom.ts index 98ac7ba..19c937a 100644 --- a/src/services/PeerRoom/PeerRoom.ts +++ b/src/services/PeerRoom/PeerRoom.ts @@ -17,6 +17,11 @@ export enum PeerStreamType { SCREEN = 'SCREEN', } +export enum PeerConnectionType { + DIRECT = 'DIRECT', + RELAY = 'RELAY', +} + const streamQueueAddDelay = 1000 export class PeerRoom { @@ -112,9 +117,44 @@ export class PeerRoom { getPeers = () => { const peers = this.room.getPeers() + return Object.keys(peers) } + getPeerConnectionTypes = async () => { + const peers = this.room.getPeers() + + const peerConnections: Record = {} + + await Promise.all( + Object.entries(peers).map(async ([peerId, rtcPeerConnection]) => { + const stats = await rtcPeerConnection.getStats() + let selectedLocalCandidate + + // https://stackoverflow.com/a/61571171/470685 + for (const { type, state, localCandidateId } of stats.values()) + if ( + type === 'candidate-pair' && + state === 'succeeded' && + localCandidateId + ) { + selectedLocalCandidate = localCandidateId + break + } + + const isRelay = + !!selectedLocalCandidate && + stats.get(selectedLocalCandidate)?.candidateType === 'relay' + + peerConnections[peerId] = isRelay + ? PeerConnectionType.RELAY + : PeerConnectionType.DIRECT + }) + ) + + return peerConnections + } + makeAction = (namespace: string) => { return this.room.makeAction(namespace) }