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)
}