Introduce condigurable auto leave option

This commit is contained in:
Timo
2025-06-27 14:06:29 +02:00
committed by Timo K
parent c0aab96968
commit b523e999a0
2 changed files with 77 additions and 56 deletions

View File

@@ -25,7 +25,11 @@ import useMeasure from "react-use-measure";
import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc";
import classNames from "classnames"; import classNames from "classnames";
import { BehaviorSubject, map } from "rxjs"; import { BehaviorSubject, map } from "rxjs";
import { useObservable } from "observable-hooks"; import {
useObservable,
useObservableEagerState,
useSubscription,
} from "observable-hooks";
import { logger } from "matrix-js-sdk/lib/logger"; import { logger } from "matrix-js-sdk/lib/logger";
import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport";
import { import {
@@ -140,11 +144,11 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
useEffect(() => { useEffect(() => {
logger.info( logger.info(
`[Lifecycle] InCallView Component mounted, livekitroom state ${livekitRoom?.state}`, `[Lifecycle] InCallView Component mounted, livekit room state ${livekitRoom?.state}`,
); );
return (): void => { return (): void => {
logger.info( logger.info(
`[Lifecycle] InCallView Component unmounted, livekitroom state ${livekitRoom?.state}`, `[Lifecycle] InCallView Component unmounted, livekit room state ${livekitRoom?.state}`,
); );
livekitRoom livekitRoom
?.disconnect() ?.disconnect()
@@ -166,7 +170,10 @@ export const ActiveCall: FC<ActiveCallProps> = (props) => {
props.rtcSession, props.rtcSession,
livekitRoom, livekitRoom,
mediaDevices, mediaDevices,
props.e2eeSystem, {
encryptionSystem: props.e2eeSystem,
autoLeaveWhenOthersLeft: undefined,
},
connStateObservable$, connStateObservable$,
reactionsReader.raisedHands$, reactionsReader.raisedHands$,
reactionsReader.reactions$, reactionsReader.reactions$,
@@ -313,6 +320,7 @@ export const InCallView: FC<InCallViewProps> = ({
const earpieceMode = useBehavior(vm.earpieceMode$); const earpieceMode = useBehavior(vm.earpieceMode$);
const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$); const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$);
const switchCamera = useSwitchCamera(vm.localVideo$); const switchCamera = useSwitchCamera(vm.localVideo$);
useSubscription(vm.autoLeaveWhenOthersLeft$, onLeave);
// Ideally we could detect taps by listening for click events and checking // Ideally we could detect taps by listening for click events and checking
// that the pointerType of the event is "touch", but this isn't yet supported // that the pointerType of the event is "touch", but this isn't yet supported

View File

@@ -96,6 +96,10 @@ import { calculateDisplayName, shouldDisambiguate } from "../utils/displayname";
import { type MediaDevices } from "./MediaDevices"; import { type MediaDevices } from "./MediaDevices";
import { type Behavior } from "./Behavior"; import { type Behavior } from "./Behavior";
interface CallViewModelOptions {
encryptionSystem: EncryptionSystem;
autoLeaveWhenOthersLeft?: boolean;
}
// How long we wait after a focus switch before showing the real participant // How long we wait after a focus switch before showing the real participant
// list again // list again
const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000; const POST_FOCUS_PARTICIPANT_UPDATE_DELAY_MS = 3000;
@@ -473,49 +477,47 @@ export class CallViewModel extends ViewModel {
), ),
); );
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(this.matrixRTCSession.memberships),
map(() => {
return this.matrixRTCSession.memberships;
}),
);
/** /**
* Displaynames for each member of the call. This will disambiguate * Displaynames for each member of the call. This will disambiguate
* any displaynames that clashes with another member. Only members * any displaynames that clashes with another member. Only members
* joined to the call are considered here. * joined to the call are considered here.
*/ */
public readonly memberDisplaynames$ = this.scope.behavior( public readonly memberDisplaynames$ = this.memberships$.pipe(
merge( map((memberships) => {
// Handle call membership changes. const displaynameMap = new Map<string, string>();
fromEvent( const { room } = this.matrixRTCSession;
this.matrixRTCSession,
MatrixRTCSessionEvent.MembershipsChanged,
),
// Handle room membership changes (and displayname updates)
fromEvent(this.matrixRTCSession.room, RoomStateEvent.Members),
).pipe(
startWith(null),
map(() => {
const displaynameMap = new Map<string, string>();
const { room, memberships } = this.matrixRTCSession;
// We only consider RTC members for disambiguation as they are the only visible members. // We only consider RTC members for disambiguation as they are the only visible members.
for (const rtcMember of memberships) { for (const rtcMember of memberships) {
const matrixIdentifier = `${rtcMember.sender}:${rtcMember.deviceId}`; const matrixIdentifier = `${rtcMember.sender}:${rtcMember.deviceId}`;
const { member } = getRoomMemberFromRtcMember(rtcMember, room); const { member } = getRoomMemberFromRtcMember(rtcMember, room);
if (!member) { if (!member) {
logger.error( logger.error("Could not find member for media id:", matrixIdentifier);
"Could not find member for media id:", continue;
matrixIdentifier,
);
continue;
}
const disambiguate = shouldDisambiguate(member, memberships, room);
displaynameMap.set(
matrixIdentifier,
calculateDisplayName(member, disambiguate),
);
} }
return displaynameMap; const disambiguate = shouldDisambiguate(member, memberships, room);
}), displaynameMap.set(
// It turns out that doing the disambiguation above is rather expensive on Safari (10x slower matrixIdentifier,
// than on Chrome/Firefox). This means it is important that we multicast the result so that we calculateDisplayName(member, disambiguate),
// don't do this work more times than we need to. This is achieved by converting to a behavior: );
), }
return displaynameMap;
}),
// It turns out that doing the disambiguation above is rather expensive on Safari (10x slower
// than on Chrome/Firefox). This means it is important that we multicast the result so that we
// don't do this work more times than we need to. This is achieved by converting to a behavior:
); );
public readonly handsRaised$ = this.scope.behavior(this.handsRaisedSubject$); public readonly handsRaised$ = this.scope.behavior(this.handsRaisedSubject$);
@@ -612,7 +614,7 @@ export class CallViewModel extends ViewModel {
indexedMediaId, indexedMediaId,
member, member,
participant, participant,
this.encryptionSystem, this.options.encryptionSystem,
this.livekitRoom, this.livekitRoom,
this.memberDisplaynames$.pipe( this.memberDisplaynames$.pipe(
map((m) => m.get(matrixIdentifier) ?? "[👻]"), map((m) => m.get(matrixIdentifier) ?? "[👻]"),
@@ -635,7 +637,7 @@ export class CallViewModel extends ViewModel {
screenShareId, screenShareId,
member, member,
participant, participant,
this.encryptionSystem, this.options.encryptionSystem,
this.livekitRoom, this.livekitRoom,
this.memberDisplaynames$.pipe( this.memberDisplaynames$.pipe(
map((m) => m.get(matrixIdentifier) ?? "[👻]"), map((m) => m.get(matrixIdentifier) ?? "[👻]"),
@@ -676,7 +678,7 @@ export class CallViewModel extends ViewModel {
nonMemberId, nonMemberId,
undefined, undefined,
participant, participant,
this.encryptionSystem, this.options.encryptionSystem,
this.livekitRoom, this.livekitRoom,
this.memberDisplaynames$.pipe( this.memberDisplaynames$.pipe(
map( map(
@@ -726,18 +728,29 @@ export class CallViewModel extends ViewModel {
), ),
); );
public readonly memberChanges$ = this.userMedia$ public readonly memberChanges$ = this.userMedia$.pipe(
.pipe(map((mediaItems) => mediaItems.map((m) => m.id))) map((mediaItems) => mediaItems.map((m) => m.id)),
.pipe( scan<string[], { ids: string[]; joined: string[]; left: string[] }>(
scan<string[], { ids: string[]; joined: string[]; left: string[] }>( (prev, ids) => {
(prev, ids) => { const left = prev.ids.filter((id) => !ids.includes(id));
const left = prev.ids.filter((id) => !ids.includes(id)); const joined = ids.filter((id) => !prev.ids.includes(id));
const joined = ids.filter((id) => !prev.ids.includes(id)); return { ids, joined, left };
return { ids, joined, left }; },
}, { 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 * List of MediaItems that we want to display, that are of type ScreenShare
@@ -1426,7 +1439,7 @@ export class CallViewModel extends ViewModel {
private readonly matrixRTCSession: MatrixRTCSession, private readonly matrixRTCSession: MatrixRTCSession,
private readonly livekitRoom: LivekitRoom, private readonly livekitRoom: LivekitRoom,
private readonly mediaDevices: MediaDevices, private readonly mediaDevices: MediaDevices,
private readonly encryptionSystem: EncryptionSystem, private readonly options: CallViewModelOptions,
private readonly connectionState$: Observable<ECConnectionState>, private readonly connectionState$: Observable<ECConnectionState>,
private readonly handsRaisedSubject$: Observable< private readonly handsRaisedSubject$: Observable<
Record<string, RaisedHandInfo> Record<string, RaisedHandInfo>