* 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:
parent
87ffd1df56
commit
4cf75b15b0
@ -20,7 +20,10 @@ export const PeerNameDisplay = ({
|
|||||||
return (
|
return (
|
||||||
<Typography component="span" {...rest}>
|
<Typography component="span" {...rest}>
|
||||||
{friendlyName}
|
{friendlyName}
|
||||||
<Typography variant="caption"> ({getPeerName(userId)})</Typography>
|
<Typography variant="caption" {...rest}>
|
||||||
|
{' '}
|
||||||
|
({getPeerName(userId)})
|
||||||
|
</Typography>
|
||||||
</Typography>
|
</Typography>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
@ -54,6 +54,7 @@ export function useRoom(
|
|||||||
const {
|
const {
|
||||||
peerList,
|
peerList,
|
||||||
setPeerList,
|
setPeerList,
|
||||||
|
setPeerConnectionTypes,
|
||||||
tabHasFocus,
|
tabHasFocus,
|
||||||
showAlert,
|
showAlert,
|
||||||
setRoomId,
|
setRoomId,
|
||||||
@ -368,6 +369,12 @@ export function useRoom(
|
|||||||
sendPeerMetadata({ customUsername, userId })
|
sendPeerMetadata({ customUsername, userId })
|
||||||
}, [customUsername, userId, sendPeerMetadata])
|
}, [customUsername, userId, sendPeerMetadata])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
;(async () => {
|
||||||
|
setPeerConnectionTypes(await peerRoom.getPeerConnectionTypes())
|
||||||
|
})()
|
||||||
|
}, [peerList, peerRoom, setPeerConnectionTypes])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isPrivate,
|
isPrivate,
|
||||||
handleInlineMediaUpload,
|
handleInlineMediaUpload,
|
||||||
|
@ -10,12 +10,11 @@ import VolumeUp from '@mui/icons-material/VolumeUp'
|
|||||||
import ListItem from '@mui/material/ListItem'
|
import ListItem from '@mui/material/ListItem'
|
||||||
|
|
||||||
import { PeerListHeader } from 'components/Shell/PeerListHeader'
|
import { PeerListHeader } from 'components/Shell/PeerListHeader'
|
||||||
import { AudioVolume } from 'components/AudioVolume'
|
|
||||||
import { PeerNameDisplay } from 'components/PeerNameDisplay'
|
|
||||||
import { Username } from 'components/Username/Username'
|
import { Username } from 'components/Username/Username'
|
||||||
import { AudioState, Peer } from 'models/chat'
|
import { AudioState, Peer } from 'models/chat'
|
||||||
|
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
|
||||||
|
|
||||||
import { PeerDownloadFileButton } from './PeerDownloadFileButton'
|
import { PeerListItem } from './PeerListItem'
|
||||||
|
|
||||||
export const peerListWidth = 300
|
export const peerListWidth = 300
|
||||||
|
|
||||||
@ -24,6 +23,7 @@ export interface PeerListProps extends PropsWithChildren {
|
|||||||
isPeerListOpen: boolean
|
isPeerListOpen: boolean
|
||||||
onPeerListClose: () => void
|
onPeerListClose: () => void
|
||||||
peerList: Peer[]
|
peerList: Peer[]
|
||||||
|
peerConnectionTypes: Record<string, PeerConnectionType>
|
||||||
audioState: AudioState
|
audioState: AudioState
|
||||||
peerAudios: Record<string, HTMLAudioElement>
|
peerAudios: Record<string, HTMLAudioElement>
|
||||||
}
|
}
|
||||||
@ -33,6 +33,7 @@ export const PeerList = ({
|
|||||||
isPeerListOpen,
|
isPeerListOpen,
|
||||||
onPeerListClose,
|
onPeerListClose,
|
||||||
peerList,
|
peerList,
|
||||||
|
peerConnectionTypes,
|
||||||
audioState,
|
audioState,
|
||||||
peerAudios,
|
peerAudios,
|
||||||
}: PeerListProps) => {
|
}: PeerListProps) => {
|
||||||
@ -72,15 +73,12 @@ export const PeerList = ({
|
|||||||
</ListItemText>
|
</ListItemText>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
{peerList.map((peer: Peer) => (
|
{peerList.map((peer: Peer) => (
|
||||||
<ListItem key={peer.peerId} divider={true}>
|
<PeerListItem
|
||||||
<PeerDownloadFileButton peer={peer} />
|
key={peer.peerId}
|
||||||
<ListItemText>
|
peer={peer}
|
||||||
<PeerNameDisplay>{peer.userId}</PeerNameDisplay>
|
peerConnectionTypes={peerConnectionTypes}
|
||||||
{peer.peerId in peerAudios && (
|
peerAudios={peerAudios}
|
||||||
<AudioVolume audioEl={peerAudios[peer.peerId]} />
|
/>
|
||||||
)}
|
|
||||||
</ListItemText>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
</MuiDrawer>
|
</MuiDrawer>
|
||||||
|
76
src/components/Shell/PeerListItem.tsx
Normal file
76
src/components/Shell/PeerListItem.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
@ -19,6 +19,8 @@ import { AlertOptions } from 'models/shell'
|
|||||||
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
|
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
|
||||||
import { ErrorBoundary } from 'components/ErrorBoundary'
|
import { ErrorBoundary } from 'components/ErrorBoundary'
|
||||||
|
|
||||||
|
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
|
||||||
|
|
||||||
import { Drawer } from './Drawer'
|
import { Drawer } from './Drawer'
|
||||||
import { UpgradeDialog } from './UpgradeDialog'
|
import { UpgradeDialog } from './UpgradeDialog'
|
||||||
import { ShellAppBar } from './ShellAppBar'
|
import { ShellAppBar } from './ShellAppBar'
|
||||||
@ -65,6 +67,9 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
const [password, setPassword] = useState<string | undefined>(undefined)
|
const [password, setPassword] = useState<string | undefined>(undefined)
|
||||||
const [isPeerListOpen, setIsPeerListOpen] = useState(defaultSidebarsOpen)
|
const [isPeerListOpen, setIsPeerListOpen] = useState(defaultSidebarsOpen)
|
||||||
const [peerList, setPeerList] = useState<Peer[]>([]) // except self
|
const [peerList, setPeerList] = useState<Peer[]>([]) // except self
|
||||||
|
const [peerConnectionTypes, setPeerConnectionTypes] = useState<
|
||||||
|
Record<string, PeerConnectionType>
|
||||||
|
>({})
|
||||||
const [tabHasFocus, setTabHasFocus] = useState(true)
|
const [tabHasFocus, setTabHasFocus] = useState(true)
|
||||||
const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED)
|
const [audioState, setAudioState] = useState<AudioState>(AudioState.STOPPED)
|
||||||
const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED)
|
const [videoState, setVideoState] = useState<VideoState>(VideoState.STOPPED)
|
||||||
@ -101,6 +106,8 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
setIsPeerListOpen,
|
setIsPeerListOpen,
|
||||||
peerList,
|
peerList,
|
||||||
setPeerList,
|
setPeerList,
|
||||||
|
peerConnectionTypes,
|
||||||
|
setPeerConnectionTypes,
|
||||||
audioState,
|
audioState,
|
||||||
setAudioState,
|
setAudioState,
|
||||||
videoState,
|
videoState,
|
||||||
@ -120,6 +127,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
password,
|
password,
|
||||||
setPassword,
|
setPassword,
|
||||||
peerList,
|
peerList,
|
||||||
|
peerConnectionTypes,
|
||||||
tabHasFocus,
|
tabHasFocus,
|
||||||
showRoomControls,
|
showRoomControls,
|
||||||
setShowRoomControls,
|
setShowRoomControls,
|
||||||
@ -315,6 +323,7 @@ export const Shell = ({ appNeedsUpdate, children, userPeerId }: ShellProps) => {
|
|||||||
isPeerListOpen={isPeerListOpen}
|
isPeerListOpen={isPeerListOpen}
|
||||||
onPeerListClose={handlePeerListClick}
|
onPeerListClose={handlePeerListClick}
|
||||||
peerList={peerList}
|
peerList={peerList}
|
||||||
|
peerConnectionTypes={peerConnectionTypes}
|
||||||
audioState={audioState}
|
audioState={audioState}
|
||||||
peerAudios={peerAudios}
|
peerAudios={peerAudios}
|
||||||
/>
|
/>
|
||||||
|
@ -2,6 +2,7 @@ import { createContext, Dispatch, SetStateAction } from 'react'
|
|||||||
|
|
||||||
import { AlertOptions } from 'models/shell'
|
import { AlertOptions } from 'models/shell'
|
||||||
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
|
import { AudioState, ScreenShareState, VideoState, Peer } from 'models/chat'
|
||||||
|
import { PeerConnectionType } from 'services/PeerRoom/PeerRoom'
|
||||||
|
|
||||||
interface ShellContextProps {
|
interface ShellContextProps {
|
||||||
tabHasFocus: boolean
|
tabHasFocus: boolean
|
||||||
@ -17,6 +18,10 @@ interface ShellContextProps {
|
|||||||
setIsPeerListOpen: Dispatch<SetStateAction<boolean>>
|
setIsPeerListOpen: Dispatch<SetStateAction<boolean>>
|
||||||
peerList: Peer[]
|
peerList: Peer[]
|
||||||
setPeerList: Dispatch<SetStateAction<Peer[]>>
|
setPeerList: Dispatch<SetStateAction<Peer[]>>
|
||||||
|
peerConnectionTypes: Record<string, PeerConnectionType>
|
||||||
|
setPeerConnectionTypes: Dispatch<
|
||||||
|
SetStateAction<Record<string, PeerConnectionType>>
|
||||||
|
>
|
||||||
audioState: AudioState
|
audioState: AudioState
|
||||||
setAudioState: Dispatch<SetStateAction<AudioState>>
|
setAudioState: Dispatch<SetStateAction<AudioState>>
|
||||||
videoState: VideoState
|
videoState: VideoState
|
||||||
@ -43,6 +48,8 @@ export const ShellContext = createContext<ShellContextProps>({
|
|||||||
setIsPeerListOpen: () => {},
|
setIsPeerListOpen: () => {},
|
||||||
peerList: [],
|
peerList: [],
|
||||||
setPeerList: () => {},
|
setPeerList: () => {},
|
||||||
|
peerConnectionTypes: {},
|
||||||
|
setPeerConnectionTypes: () => {},
|
||||||
audioState: AudioState.STOPPED,
|
audioState: AudioState.STOPPED,
|
||||||
setAudioState: () => {},
|
setAudioState: () => {},
|
||||||
videoState: VideoState.STOPPED,
|
videoState: VideoState.STOPPED,
|
||||||
|
@ -17,6 +17,11 @@ export enum PeerStreamType {
|
|||||||
SCREEN = 'SCREEN',
|
SCREEN = 'SCREEN',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum PeerConnectionType {
|
||||||
|
DIRECT = 'DIRECT',
|
||||||
|
RELAY = 'RELAY',
|
||||||
|
}
|
||||||
|
|
||||||
const streamQueueAddDelay = 1000
|
const streamQueueAddDelay = 1000
|
||||||
|
|
||||||
export class PeerRoom {
|
export class PeerRoom {
|
||||||
@ -112,9 +117,44 @@ export class PeerRoom {
|
|||||||
|
|
||||||
getPeers = () => {
|
getPeers = () => {
|
||||||
const peers = this.room.getPeers()
|
const peers = this.room.getPeers()
|
||||||
|
|
||||||
return Object.keys(peers)
|
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) => {
|
makeAction = <T>(namespace: string) => {
|
||||||
return this.room.makeAction<T>(namespace)
|
return this.room.makeAction<T>(namespace)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user