Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-join-existing-calls-without-livekit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: patch
---

Allow users without a locally configured LiveKit focus to join calls started by others.
29 changes: 5 additions & 24 deletions src/app/components/IncomingCallModal.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,11 @@ import type { Room } from '$types/matrix-sdk';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { IncomingCallInternal } from './IncomingCallModal';

const { navigateRoomMock, sendRtcDeclineMock, webRtcSupportedMock, livekitSupportedMock } =
vi.hoisted(() => ({
navigateRoomMock: vi.fn<(roomId: string) => void>(),
sendRtcDeclineMock: vi.fn<(roomId: string, eventId: string) => Promise<void>>(),
webRtcSupportedMock: vi.fn<() => boolean>(),
livekitSupportedMock: vi.fn<() => boolean>(),
}));
const { navigateRoomMock, sendRtcDeclineMock, webRtcSupportedMock } = vi.hoisted(() => ({
navigateRoomMock: vi.fn<(roomId: string) => void>(),
sendRtcDeclineMock: vi.fn<(roomId: string, eventId: string) => Promise<void>>(),
webRtcSupportedMock: vi.fn<() => boolean>(),
}));

vi.mock('$hooks/useMatrixClient', () => ({
useMatrixClient: () => ({
Expand All @@ -19,10 +17,6 @@ vi.mock('$hooks/useMatrixClient', () => ({
}),
}));

vi.mock('$hooks/useLivekitSupport', () => ({
useLivekitSupport: () => livekitSupportedMock(),
}));

vi.mock('$hooks/useCallEmbed', () => ({
useCallEmbed: () => undefined,
}));
Expand Down Expand Up @@ -101,7 +95,6 @@ describe('IncomingCallInternal', () => {
navigateRoomMock.mockReset();
sendRtcDeclineMock.mockReset().mockResolvedValue(undefined);
webRtcSupportedMock.mockReset().mockReturnValue(true);
livekitSupportedMock.mockReset().mockReturnValue(true);
});

it('closes the modal when decline is pressed', async () => {
Expand Down Expand Up @@ -152,16 +145,4 @@ describe('IncomingCallInternal', () => {
});
expect(sendRtcDeclineMock).not.toHaveBeenCalled();
});

it('shows homeserver capability issues and blocks answer when LiveKit is unavailable', () => {
livekitSupportedMock.mockReturnValue(false);
const onClose = vi.fn<() => void>();
render(<IncomingCallInternal room={room} incomingCall={incomingCall} onClose={onClose} />);

expect(
screen.getByText(/homeserver does not expose a livekit call focus/i)
).toBeInTheDocument();
expect(screen.getByRole('button', { name: /answer voice call/i })).toBeDisabled();
expect(screen.getByText(/homeserver call focus is unavailable/i)).toBeInTheDocument();
});
});
5 changes: 1 addition & 4 deletions src/app/components/IncomingCallModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
import { useMemo, type KeyboardEvent as ReactKeyboardEvent } from 'react';
import type { Room } from '$types/matrix-sdk';
import { useMatrixClient } from '$hooks/useMatrixClient';
import { useLivekitSupport } from '$hooks/useLivekitSupport';
import { useRoomName } from '$hooks/useRoomMeta';
import { useCallEmbed } from '$hooks/useCallEmbed';
import { ScreenSize, useScreenSizeContext } from '$hooks/useScreenSize';
Expand Down Expand Up @@ -63,7 +62,6 @@ export function IncomingCallInternal({ room, incomingCall, onClose }: IncomingCa
const screenSize = useScreenSizeContext();
const compact = screenSize === ScreenSize.Mobile;
const roomName = useRoomName(room);
const livekitSupported = useLivekitSupport();
const callEmbed = useCallEmbed();
const { navigateRoom } = useRoomNavigate();
const roomAvatarUrl = getRoomAvatarUrl(mx, room, 96);
Expand Down Expand Up @@ -93,11 +91,10 @@ export function IncomingCallInternal({ room, incomingCall, onClose }: IncomingCa
() =>
getIncomingCallBlockers({
canUseWebRTC,
livekitSupported,
hasCallMemberPermission,
inAnotherCall,
}),
[canUseWebRTC, livekitSupported, hasCallMemberPermission, inAnotherCall]
[canUseWebRTC, hasCallMemberPermission, inAnotherCall]
);

const canAnswer = capabilityIssues.length === 0;
Expand Down
7 changes: 4 additions & 3 deletions src/app/features/call/CallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import * as css from './styles.css';
import { CallMemberRenderer } from './CallMemberCard';
import { PrescreenControls } from './PrescreenControls';
import { callEmbedAtom, callEmbedStartErrorAtom } from '$state/callEmbed';
import { canJoinCall } from './callStartCapabilities';

function LivekitServerMissingMessage() {
return (
Expand Down Expand Up @@ -42,12 +43,12 @@ function JoinMessage({
return <WebRTCMissingError />;
}

if (hasParticipant) return null;

if (livekitSupported === false) {
return <LivekitServerMissingMessage />;
}

if (hasParticipant) return null;

return (
<Text style={{ margin: 'auto' }} size="L400" align="Center">
Voice chat&apos;s empty - be the first to hop in!
Expand Down Expand Up @@ -96,7 +97,7 @@ function CallPrescreen() {
? 'Call setup failed because required call capabilities were rejected.'
: 'Call setup failed while preparing the embedded call app.';

const canJoin = callStartCapabilities.canStart;
const canJoin = canJoinCall(callStartCapabilities, hasParticipant);

return (
<Scroll variant="Surface" hideTrack>
Expand Down
10 changes: 10 additions & 0 deletions src/app/features/call/callStartCapabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ export type CallStartCapabilities = {
inAnotherCall: boolean;
};

export const canJoinCall = (
capabilities: CallStartCapabilities,
hasParticipant: boolean
): boolean =>
capabilities.canStart ||
(hasParticipant &&
capabilities.webRTCSupported &&
capabilities.hasCallMemberPermission &&
!capabilities.inAnotherCall);

type EvaluateCallStartCapabilitiesInput = {
room: Room;
myUserId: string;
Expand Down
9 changes: 1 addition & 8 deletions src/app/features/call/getIncomingCallBlockers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ describe('getIncomingCallBlockers', () => {
expect(
getIncomingCallBlockers({
canUseWebRTC: true,
livekitSupported: true,
hasCallMemberPermission: true,
inAnotherCall: false,
})
Expand All @@ -16,16 +15,10 @@ describe('getIncomingCallBlockers', () => {
it('returns blockers in priority order', () => {
const issues = getIncomingCallBlockers({
canUseWebRTC: false,
livekitSupported: false,
hasCallMemberPermission: false,
inAnotherCall: true,
});

expect(issues.map((issue) => issue.id)).toEqual([
'webrtc',
'livekit',
'permission',
'another_call',
]);
expect(issues.map((issue) => issue.id)).toEqual(['webrtc', 'permission', 'another_call']);
});
});
9 changes: 0 additions & 9 deletions src/app/features/call/getIncomingCallBlockers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@ export type IncomingCallBlocker = {

export type IncomingCallBlockerInput = {
canUseWebRTC: boolean;
livekitSupported: boolean;
hasCallMemberPermission: boolean;
inAnotherCall: boolean;
};

export const getIncomingCallBlockers = ({
canUseWebRTC,
livekitSupported,
hasCallMemberPermission,
inAnotherCall,
}: IncomingCallBlockerInput): IncomingCallBlocker[] => {
Expand All @@ -26,13 +24,6 @@ export const getIncomingCallBlockers = ({
shortReason: 'WebRTC is unavailable in this browser.',
});
}
if (!livekitSupported) {
issues.push({
id: 'livekit',
message: 'Your homeserver does not expose a LiveKit call focus.',
shortReason: 'Homeserver call focus is unavailable.',
});
}
if (!hasCallMemberPermission) {
issues.push({
id: 'permission',
Expand Down
Loading