Skip to content

Introduce configurable auto leave option #3372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: livekit
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 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
9 changes: 9 additions & 0 deletions src/UrlParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ export interface UrlParams {
* is window.controls.onBackButtonPressed.
*/
header: HeaderStyle;
/**
* If we want the app to automatically leave the call when all other participants
* leave the call.
*
* This is used in settings where EC should behave more like a phone call other than a conference.
* Like in a DM.
*/
telephoneAutoLeave: boolean;
/**
* Whether the controls should be shown. For screen recording no controls can be desired.
*/
Expand Down Expand Up @@ -329,6 +337,7 @@ export const getUrlParams = (
rageshakeSubmitUrl: parser.getParam("rageshakeSubmitUrl"),
sentryDsn: parser.getParam("sentryDsn"),
sentryEnvironment: parser.getParam("sentryEnvironment"),
telephoneAutoLeave: parser.getFlagParam("telephoneAutoLeave"),
};
};

Expand Down
7 changes: 5 additions & 2 deletions src/room/GroupCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -166,15 +166,18 @@ export const GroupCallView: FC<Props> = ({
const { displayName, avatarUrl } = useProfile(client);
const roomName = useRoomName(room);
const roomAvatar = useRoomAvatar(room);
const { perParticipantE2EE, returnToLobby } = useUrlParams();
const {
perParticipantE2EE,
returnToLobby,
password: passwordFromUrl,
} = useUrlParams();
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
const [useExperimentalToDeviceTransport] = useSetting(
useExperimentalToDeviceTransportSetting,
);

// Save the password once we start the groupCallView
const { password: passwordFromUrl } = useUrlParams();
useEffect(() => {
if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl);
}, [passwordFromUrl, room.roomId]);
Expand Down
16 changes: 14 additions & 2 deletions src/room/InCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import useMeasure from "react-use-measure";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import classNames from "classnames";
import { BehaviorSubject, map } from "rxjs";
import { useObservable, useObservableEagerState } from "observable-hooks";
import {
useObservable,
useObservableEagerState,
useSubscription,
} from "observable-hooks";
import { logger } from "matrix-js-sdk/lib/logger";
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
import {
Expand Down Expand Up @@ -158,14 +162,19 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
};
}, [livekitRoom]);

const { telephoneAutoLeave } = useUrlParams();

useEffect(() => {
if (livekitRoom !== undefined) {
const reactionsReader = new ReactionsReader(props.rtcSession);
const vm = new CallViewModel(
props.rtcSession,
livekitRoom,
mediaDevices,
props.e2eeSystem,
{
encryptionSystem: props.e2eeSystem,
autoLeaveWhenOthersLeft: telephoneAutoLeave,
},
connStateObservable$,
reactionsReader.raisedHands$,
reactionsReader.reactions$,
Expand All @@ -182,6 +191,7 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
mediaDevices,
props.e2eeSystem,
connStateObservable$,
telephoneAutoLeave,
]);

if (livekitRoom === undefined || vm === null) return null;
Expand Down Expand Up @@ -302,6 +312,8 @@ export const InCallView: FC<InCallViewProps> = ({
() => void toggleRaisedHand(),
);

useSubscription(vm.autoLeaveWhenOthersLeft$, onLeave);

const windowMode = useObservableEagerState(vm.windowMode$);
const layout = useObservableEagerState(vm.layout$);
const tileStoreGeneration = useObservableEagerState(vm.tileStoreGeneration$);
Expand Down
2 changes: 1 addition & 1 deletion src/state/CallViewModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ function withCallViewModel(
liveKitRoom,
mediaDevices,
{
kind: E2eeType.PER_PARTICIPANT,
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
},
connectionState$,
raisedHands$,
Expand Down
69 changes: 45 additions & 24 deletions src/state/CallViewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ import { shallowEquals } from "../utils/array";
import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
import { type MediaDevices } from "./MediaDevices";

interface CallViewModelOptions {
encryptionSystem: EncryptionSystem;
autoLeaveWhenOthersLeft?: boolean;
}
// How long we wait after a focus switch before showing the real participant
// list again
const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000;
Expand Down Expand Up @@ -461,21 +465,27 @@ export class CallViewModel extends ViewModel {
},
);

/**
* Displaynames for each member of the call. This will disambiguate
* any displaynames that clashes with another member. Only members
* joined to the call are considered here.
*/
public readonly memberDisplaynames$ = merge(
private readonly memberships$: Observable<CallMembership[]> = merge(
// Handle call membership changes.
fromEvent(this.matrixRTCSession, MatrixRTCSessionEvent.MembershipsChanged),
// Handle room membership changes (and displayname updates)
fromEvent(this.matrixRTCSession.room, RoomStateEvent.Members),
).pipe(
startWith(null),
startWith(this.matrixRTCSession.memberships),
map(() => {
return this.matrixRTCSession.memberships;
}),
);

/**
* Displaynames for each member of the call. This will disambiguate
* any displaynames that clashes with another member. Only members
* joined to the call are considered here.
*/
public readonly memberDisplaynames$ = this.memberships$.pipe(
map((memberships) => {
const displaynameMap = new Map<string, string>();
const { room, memberships } = this.matrixRTCSession;
const { room } = this.matrixRTCSession;

// We only consider RTC members for disambiguation as they are the only visible members.
for (const rtcMember of memberships) {
Expand Down Expand Up @@ -577,7 +587,7 @@ export class CallViewModel extends ViewModel {
indexedMediaId,
member,
participant,
this.encryptionSystem,
this.options.encryptionSystem,
this.livekitRoom,
this.memberDisplaynames$.pipe(
map((m) => m.get(matrixIdentifier) ?? "[👻]"),
Expand All @@ -600,7 +610,7 @@ export class CallViewModel extends ViewModel {
screenShareId,
member,
participant,
this.encryptionSystem,
this.options.encryptionSystem,
this.livekitRoom,
this.memberDisplaynames$.pipe(
map((m) => m.get(matrixIdentifier) ?? "[👻]"),
Expand Down Expand Up @@ -641,7 +651,7 @@ export class CallViewModel extends ViewModel {
nonMemberId,
undefined,
participant,
this.encryptionSystem,
this.options.encryptionSystem,
this.livekitRoom,
this.memberDisplaynames$.pipe(
map((m) => m.get(participant.identity) ?? "[👻]"),
Expand Down Expand Up @@ -686,18 +696,29 @@ export class CallViewModel extends ViewModel {
),
);

public readonly memberChanges$ = this.userMedia$
.pipe(map((mediaItems) => mediaItems.map((m) => m.id)))
.pipe(
scan<string[], { ids: string[]; joined: string[]; left: string[] }>(
(prev, ids) => {
const left = prev.ids.filter((id) => !ids.includes(id));
const joined = ids.filter((id) => !prev.ids.includes(id));
return { ids, joined, left };
},
{ ids: [], joined: [], left: [] },
),
);
public readonly memberChanges$ = this.userMedia$.pipe(
map((mediaItems) => mediaItems.map((m) => m.id)),
scan<string[], { ids: string[]; joined: string[]; left: string[] }>(
(prev, ids) => {
const left = prev.ids.filter((id) => !ids.includes(id));
const joined = ids.filter((id) => !prev.ids.includes(id));
return { ids, joined, left };
},
{ ids: [], joined: [], left: [] },
),
);

public readonly allOthersLeft$ = this.memberChanges$.pipe(
map(({ ids, left }) => ids.length === 0 && left.length > 0),
startWith(false),
distinctUntilChanged(),
);

public readonly autoLeaveWhenOthersLeft$ = this.allOthersLeft$.pipe(
distinctUntilChanged(),
filter((leave) => (leave && this.options.autoLeaveWhenOthersLeft) ?? false),
map(() => {}),
);

/**
* List of MediaItems that we want to display, that are of type ScreenShare
Expand Down Expand Up @@ -1383,7 +1404,7 @@ export class CallViewModel extends ViewModel {
private readonly matrixRTCSession: MatrixRTCSession,
private readonly livekitRoom: LivekitRoom,
private readonly mediaDevices: MediaDevices,
private readonly encryptionSystem: EncryptionSystem,
private readonly options: CallViewModelOptions,
private readonly connectionState$: Observable<ECConnectionState>,
private readonly handsRaisedSubject$: Observable<
Record<string, RaisedHandInfo>
Expand Down
2 changes: 1 addition & 1 deletion src/utils/test-viewmodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export function getBasicCallViewModelEnvironment(
liveKitRoom,
mockMediaDevices({}),
{
kind: E2eeType.PER_PARTICIPANT,
encryptionSystem: { kind: E2eeType.PER_PARTICIPANT },
},
of(ConnectionState.Connected),
handRaisedSubject$,
Expand Down