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
This commit is contained in:
Jeremy Kahn 2023-03-25 14:40:07 -05:00 committed by GitHub
parent 87ffd1df56
commit 4cf75b15b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 153 additions and 13 deletions

View File

@ -20,7 +20,10 @@ export const PeerNameDisplay = ({
return (
<Typography component="span" {...rest}>
{friendlyName}
<Typography variant="caption"> ({getPeerName(userId)})</Typography>
<Typography variant="caption" {...rest}>
{' '}
({getPeerName(userId)})
</Typography>
</Typography>
)
} else {

View File

@ -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,

View File

@ -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<string, PeerConnectionType>
audioState: AudioState
peerAudios: Record<string, HTMLAudioElement>
}
@ -33,6 +33,7 @@ export const PeerList = ({
isPeerListOpen,
onPeerListClose,
peerList,
peerConnectionTypes,
audioState,
peerAudios,
}: PeerListProps) => {
@ -72,15 +73,12 @@ export const PeerList = ({
</ListItemText>
</ListItem>
{peerList.map((peer: Peer) => (
<ListItem key={peer.peerId} divider={true}>
<PeerDownloadFileButton peer={peer} />
<ListItemText>
<PeerNameDisplay>{peer.userId}</PeerNameDisplay>
{peer.peerId in peerAudios && (
<AudioVolume audioEl={peerAudios[peer.peerId]} />
)}
</ListItemText>
</ListItem>
<PeerListItem
key={peer.peerId}
peer={peer}
peerConnectionTypes={peerConnectionTypes}
peerAudios={peerAudios}
/>
))}
</List>
</MuiDrawer>

View File

@ -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<string, PeerConnectionType>
peerAudios: Record<string, HTMLAudioElement>
}
export const PeerListItem = ({
peer,
peerConnectionTypes,
peerAudios,
}: PeerListItemProps): JSX.Element => {
const hasPeerConnection = peer.peerId in peerConnectionTypes
const isPeerConnectionDirect =
peerConnectionTypes[peer.peerId] === PeerConnectionType.DIRECT
return (
<ListItem key={peer.peerId} divider={true}>
<PeerDownloadFileButton peer={peer} />
<ListItemText>
{hasPeerConnection ? (
<Tooltip
title={
isPeerConnectionDirect ? (
<>
You are connected directly to{' '}
<PeerNameDisplay
sx={{ fontSize: 'inherit', fontWeight: 'inherit' }}
>
{peer.userId}
</PeerNameDisplay>
</>
) : (
<>
You are connected to{' '}
<PeerNameDisplay
sx={{ fontSize: 'inherit', fontWeight: 'inherit' }}
>
{peer.userId}
</PeerNameDisplay>{' '}
via a relay server. Your connection is still private and
encrypted, but performance may be degraded.
</>
)
}
>
<Box component="span" sx={{ pr: 1, cursor: 'pointer' }}>
{isPeerConnectionDirect ? (
<SyncAltIcon color="success" />
) : (
<NetworkPingIcon color="warning" />
)}
</Box>
</Tooltip>
) : null}
<PeerNameDisplay>{peer.userId}</PeerNameDisplay>
{peer.peerId in peerAudios && (
<AudioVolume audioEl={peerAudios[peer.peerId]} />
)}
</ListItemText>
</ListItem>
)
}

View File

@ -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<string | undefined>(undefined)
const [isPeerListOpen, setIsPeerListOpen] = useState(defaultSidebarsOpen)
const [peerList, setPeerList] = useState<Peer[]>([]) // except self
const [peerConnectionTypes, setPeerConnectionTypes] = useState<
Record<string, PeerConnectionType>
>({})
const [tabHasFocus, setTabHasFocus] = useState(true)
const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED)
const [videoState, setVideoState] = useState<VideoState>(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}
/>

View File

@ -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<SetStateAction<boolean>>
peerList: Peer[]
setPeerList: Dispatch<SetStateAction<Peer[]>>
peerConnectionTypes: Record<string, PeerConnectionType>
setPeerConnectionTypes: Dispatch<
SetStateAction<Record<string, PeerConnectionType>>
>
audioState: AudioState
setAudioState: Dispatch<SetStateAction<AudioState>>
videoState: VideoState
@ -43,6 +48,8 @@ export const ShellContext = createContext<ShellContextProps>({
setIsPeerListOpen: () => {},
peerList: [],
setPeerList: () => {},
peerConnectionTypes: {},
setPeerConnectionTypes: () => {},
audioState: AudioState.STOPPED,
setAudioState: () => {},
videoState: VideoState.STOPPED,

View File

@ -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<string, PeerConnectionType> = {}
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 = <T>(namespace: string) => {
return this.room.makeAction<T>(namespace)
}