diff --git a/src/room/GroupCallView.test.tsx b/src/room/GroupCallView.test.tsx index 712b6d98..87e93499 100644 --- a/src/room/GroupCallView.test.tsx +++ b/src/room/GroupCallView.test.tsx @@ -118,6 +118,7 @@ function createGroupCallView(widget: WidgetHelpers | null): { skipLobby={false} hideHeader={true} rtcSession={rtcSession as unknown as MatrixRTCSession} + isJoined muteStates={muteState} widget={widget} /> diff --git a/src/room/GroupCallView.tsx b/src/room/GroupCallView.tsx index ee1208c2..6203c675 100644 --- a/src/room/GroupCallView.tsx +++ b/src/room/GroupCallView.tsx @@ -46,7 +46,6 @@ import { } from "../livekit/MediaDevicesContext"; import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships"; import { enterRTCSession, leaveRTCSession } from "../rtcSessionHelpers"; -import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState"; import { useRoomEncryptionSystem } from "../e2ee/sharedKeyManagement"; import { useRoomAvatar } from "./useRoomAvatar"; import { useRoomName } from "./useRoomName"; @@ -74,6 +73,7 @@ interface Props { skipLobby: boolean; hideHeader: boolean; rtcSession: MatrixRTCSession; + isJoined: boolean; muteStates: MuteStates; widget: WidgetHelpers | null; } @@ -86,11 +86,11 @@ export const GroupCallView: FC = ({ skipLobby, hideHeader, rtcSession, + isJoined, muteStates, widget, }) => { const memberships = useMatrixRTCSessionMemberships(rtcSession); - const isJoined = useMatrixRTCSessionJoinState(rtcSession); const leaveSoundContext = useLatest( useAudioContext({ sounds: callEventAudioSounds, diff --git a/src/room/MuteStates.test.tsx b/src/room/MuteStates.test.tsx index 99f7eaf8..0d21df3b 100644 --- a/src/room/MuteStates.test.tsx +++ b/src/room/MuteStates.test.tsx @@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details. */ import { afterAll, afterEach, describe, expect, it, vi } from "vitest"; -import { type FC, useCallback, useState, type ReactNode } from "react"; +import { type FC, useCallback, useState } from "react"; import { render, screen } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import userEvent from "@testing-library/user-event"; @@ -20,8 +20,12 @@ import { } from "../livekit/MediaDevicesContext"; import { mockConfig } from "../utils/test"; -function TestComponent(): ReactNode { - const muteStates = useMuteStates(); +interface TestComponentProps { + isJoined?: boolean; +} + +const TestComponent: FC = ({ isJoined = false }) => { + const muteStates = useMuteStates(isJoined); const onToggleAudio = useCallback( () => muteStates.audio.setEnabled?.(!muteStates.audio.enabled), [muteStates], @@ -37,7 +41,7 @@ function TestComponent(): ReactNode { ); -} +}; const mockMicrophone: MediaDeviceInfo = { deviceId: "", @@ -134,7 +138,7 @@ describe("useMuteStates", () => { expect(screen.getByTestId("video-enabled").textContent).toBe("false"); }); - it("should be enabled by default", () => { + it("enables devices by default in the lobby", () => { mockConfig(); render( @@ -148,6 +152,22 @@ describe("useMuteStates", () => { expect(screen.getByTestId("video-enabled").textContent).toBe("true"); }); + it("disables devices by default in the call", () => { + // Disabling new devices in the call ensures that connecting a webcam + // mid-call won't cause it to suddenly be enabled without user input + mockConfig(); + + render( + + + + + , + ); + expect(screen.getByTestId("audio-enabled").textContent).toBe("false"); + expect(screen.getByTestId("video-enabled").textContent).toBe("false"); + }); + it("uses defaults from config", () => { mockConfig({ media_devices: { diff --git a/src/room/MuteStates.ts b/src/room/MuteStates.ts index 13227378..90557bf9 100644 --- a/src/room/MuteStates.ts +++ b/src/room/MuteStates.ts @@ -74,17 +74,17 @@ function useMuteState( ); } -export function useMuteStates(): MuteStates { +export function useMuteStates(isJoined: boolean): MuteStates { const devices = useMediaDevices(); const { skipLobby } = useUrlParams(); const audio = useMuteState(devices.audioInput, () => { - return Config.get().media_devices.enable_audio && !skipLobby; + return Config.get().media_devices.enable_audio && !skipLobby && !isJoined; }); const video = useMuteState( devices.videoInput, - () => Config.get().media_devices.enable_video && !skipLobby, + () => Config.get().media_devices.enable_video && !skipLobby && !isJoined, ); useEffect(() => { diff --git a/src/room/RoomPage.tsx b/src/room/RoomPage.tsx index dcebf44b..b52e5f1f 100644 --- a/src/room/RoomPage.tsx +++ b/src/room/RoomPage.tsx @@ -40,6 +40,7 @@ import { useOptInAnalytics } from "../settings/settings"; import { Config } from "../config/Config"; import { Link } from "../button/Link"; import { ErrorView } from "../ErrorView"; +import { useMatrixRTCSessionJoinState } from "../useMatrixRTCSessionJoinState"; export const RoomPage: FC = () => { const { @@ -66,7 +67,10 @@ export const RoomPage: FC = () => { const { avatarUrl, displayName: userDisplayName } = useProfile(client); const groupCallState = useLoadGroupCall(client, roomIdOrAlias, viaServers); - const muteStates = useMuteStates(); + const isJoined = useMatrixRTCSessionJoinState( + groupCallState.kind === "loaded" ? groupCallState.rtcSession : undefined, + ); + const muteStates = useMuteStates(isJoined); useEffect(() => { // If we've finished loading, are not already authed and we've been given a display name as @@ -111,6 +115,7 @@ export const RoomPage: FC = () => { widget={widget} client={client!} rtcSession={groupCallState.rtcSession} + isJoined={isJoined} isPasswordlessUser={passwordlessUser} confineToRoom={confineToRoom} preload={preload} diff --git a/src/useMatrixRTCSessionJoinState.ts b/src/useMatrixRTCSessionJoinState.ts index 0bdaa25d..eac94d63 100644 --- a/src/useMatrixRTCSessionJoinState.ts +++ b/src/useMatrixRTCSessionJoinState.ts @@ -10,35 +10,33 @@ import { type MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; export function useMatrixRTCSessionJoinState( - rtcSession: MatrixRTCSession, + rtcSession: MatrixRTCSession | undefined, ): boolean { - const [isJoined, setJoined] = useState(rtcSession.isJoined()); - - const onJoinStateChanged = useCallback( - (isJoined: boolean) => { - logger.info( - `Session in room ${rtcSession.room.roomId} changed to ${ - isJoined ? "joined" : "left" - }`, - ); - setJoined(isJoined); - }, - [rtcSession], - ); + const [, setNumUpdates] = useState(0); useEffect(() => { - rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged); + if (rtcSession !== undefined) { + const onJoinStateChanged = (isJoined: boolean): void => { + logger.info( + `Session in room ${rtcSession.room.roomId} changed to ${ + isJoined ? "joined" : "left" + }`, + ); + setNumUpdates((n) => n + 1); // Force an update + }; + rtcSession.on(MatrixRTCSessionEvent.JoinStateChanged, onJoinStateChanged); - return (): void => { - rtcSession.off( - MatrixRTCSessionEvent.JoinStateChanged, - onJoinStateChanged, - ); - }; - }, [rtcSession, onJoinStateChanged]); + return (): void => { + rtcSession.off( + MatrixRTCSessionEvent.JoinStateChanged, + onJoinStateChanged, + ); + }; + } + }, [rtcSession]); - return isJoined; + return rtcSession?.isJoined() ?? false; }