diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index a9b9cca2..1f410adb 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -248,7 +248,6 @@ export const InCallView: FC = ({ () => void toggleRaisedHand(), ); - // const allLivekitRooms = useBehavior(vm.allLivekitRooms$); const audioParticipants = useBehavior(vm.audioParticipants$); const participantCount = useBehavior(vm.participantCount$); const reconnecting = useBehavior(vm.reconnecting$); @@ -262,9 +261,8 @@ export const InCallView: FC = ({ const earpieceMode = useBehavior(vm.earpieceMode$); const audioOutputSwitcher = useBehavior(vm.audioOutputSwitcher$); const sharingScreen = useBehavior(vm.sharingScreen$); - const localUserIsAlone = useBehavior(vm.localUserIsAlone$); - const oneOnOneMember = useBehavior(vm.isOneOnOneWith$); + const ringOverlay = useBehavior(vm.ringOverlay$); const fatalCallError = useBehavior(vm.configError$); // Stop the rendering and throw for the error boundary if (fatalCallError) throw fatalCallError; @@ -301,33 +299,26 @@ export const InCallView: FC = ({ // Waiting UI overlay const waitingOverlay: JSX.Element | null = useMemo(() => { - // No overlay if not in ringing state - if (callPickupState !== "ringing" || localUserIsAlone) return null; - - const name = oneOnOneMember ? oneOnOneMember.userId : matrixRoom.roomId; - const id = oneOnOneMember ? oneOnOneMember.userId : matrixRoom.roomId; - const text = oneOnOneMember - ? `Waiting for ${name ?? oneOnOneMember.userId} to join…` - : "Waiting for other participants…"; - const avatarMxc = oneOnOneMember - ? (oneOnOneMember.getMxcAvatarUrl?.() ?? undefined) - : (matrixRoom.getMxcAvatarUrl() ?? undefined); - - return ( + return ringOverlay ? (
- +
- {text} + {ringOverlay.text}
- ); - }, [callPickupState, localUserIsAlone, matrixRoom, oneOnOneMember]); + ) : null; + }, [ringOverlay]); // 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 diff --git a/src/state/CallViewModel/CallViewModel.ts b/src/state/CallViewModel/CallViewModel.ts index 063a953e..f1332286 100644 --- a/src/state/CallViewModel/CallViewModel.ts +++ b/src/state/CallViewModel/CallViewModel.ts @@ -12,7 +12,7 @@ import { type Room as LivekitRoom, type RoomOptions, } from "livekit-client"; -import { type RoomMember, type Room as MatrixRoom } from "matrix-js-sdk"; +import { type Room as MatrixRoom } from "matrix-js-sdk"; import { combineLatest, distinctUntilChanged, @@ -115,6 +115,7 @@ import { createSentCallNotification$, } from "./CallNotificationLifecycle.ts"; import { + createDMMember$, createMatrixMemberMetadata$, createRoomMembers$, } from "./remoteMembers/MatrixMemberMetadata.ts"; @@ -244,11 +245,14 @@ export class CallViewModel { public handsRaised$: Behavior>; /** List of reactions. Keys are: membership.membershipId (currently predefined as: `${membershipEvent.userId}:${membershipEvent.deviceId}`)*/ public reactions$: Behavior>; - public isOneOnOneWith$: Behavior | null>; - public localUserIsAlone$: Behavior; + + public ringOverlay$: Behavior; // sounds and events public joinSoundEffect$: Observable; public leaveSoundEffect$: Observable; @@ -483,7 +487,9 @@ export class CallViewModel { // ------------------------------------------------------------------------ // callLifecycle - const callLifecycle = createCallNotificationLifecycle$({ + // TODO if we are in "unknown" state we need a loading rendering (or empty screen) + // Otherwise it looks like we already connected and only than the ringing starts which is weird. + const { callPickupState$, autoLeave$ } = createCallNotificationLifecycle$({ scope: scope, memberships$: memberships$, sentCallNotification$: createSentCallNotification$( @@ -505,21 +511,8 @@ export class CallViewModel { matrixRoomMembers$, ); - /** - * Returns the Member {userId, getMxcAvatarUrl, rawDisplayName} of the other user in the call, if it's a one-on-one call. - */ - const isOneOnOneWith$ = scope.behavior( - matrixRoomMembers$.pipe( - map((roomMembersMap) => { - const otherMembers = Array.from(roomMembersMap.values()).filter( - (member) => member.userId !== userId, - ); - return otherMembers.length === 1 ? otherMembers[0] : null; - }), - ), - ); - - const localUserIsAlone$ = scope.behavior( + const dmMember$ = createDMMember$(scope, matrixRoomMembers$, matrixRoom); + const noUserToCallInRoom$ = scope.behavior( matrixRoomMembers$.pipe( map( (roomMembersMap) => @@ -529,6 +522,30 @@ export class CallViewModel { ), ); + const ringOverlay$ = scope.behavior( + combineLatest([noUserToCallInRoom$, dmMember$, callPickupState$]).pipe( + map(([noUserToCallInRoom, dmMember, callPickupState]) => { + // No overlay if not in ringing state + if (callPickupState !== "ringing" || noUserToCallInRoom) return null; + + const name = dmMember ? dmMember.rawDisplayName : matrixRoom.name; + const id = dmMember ? dmMember.userId : matrixRoom.roomId; + const text = dmMember + ? `Waiting for ${name} to join…` + : "Waiting for other participants…"; + const avatarMxc = dmMember + ? (dmMember.getMxcAvatarUrl?.() ?? undefined) + : (matrixRoom.getMxcAvatarUrl() ?? undefined); + return { + name: name ?? id, + idForAvatar: id, + text, + avatarMxc, + }; + }), + ), + ); + // CODESMELL? // This is functionally the same Observable as leave$, except here it's // hoisted to the top of the class. This enables the cyclic dependency between @@ -763,13 +780,8 @@ export class CallViewModel { matrixLivekitMembers$.pipe(map((ms) => ms.value.length)), ); - // only public to expose to the view. - // TODO if we are in "unknown" state we need a loading rendering (or empty screen) - // Otherwise it looks like we already connected and only than the ringing starts which is weird. - const callPickupState$ = callLifecycle.callPickupState$; - const leaveSoundEffect$ = combineLatest([ - callLifecycle.callPickupState$, + callPickupState$, userMedia$, ]).pipe( // Until the call is successful, do not play a leave sound. @@ -804,7 +816,7 @@ export class CallViewModel { const leave$: Observable<"user" | "timeout" | "decline" | "allOthersLeft"> = merge( - callLifecycle.autoLeave$, + autoLeave$, merge(userHangup$, widgetHangup$).pipe(map(() => "user" as const)), ).pipe( scope.share, @@ -1430,8 +1442,9 @@ export class CallViewModel { const join = localMembership.requestConnect; join(); // TODO-MULTI-SFU: Use this view model for the lobby as well, and only call this once 'join' is clicked? - this.autoLeave$ = callLifecycle.autoLeave$; + this.autoLeave$ = autoLeave$; this.callPickupState$ = callPickupState$; + this.ringOverlay$ = ringOverlay$; this.leave$ = leave$; this.hangup = (): void => userHangup$.next(); this.join = join; @@ -1446,8 +1459,6 @@ export class CallViewModel { this.configError$ = localMembership.configError$; this.participantCount$ = participantCount$; this.audioParticipants$ = audioParticipants$; - this.isOneOnOneWith$ = isOneOnOneWith$; - this.localUserIsAlone$ = localUserIsAlone$; this.handsRaised$ = handsRaised$; this.reactions$ = reactions$; diff --git a/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.ts b/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.ts index e6a968f2..91363f90 100644 --- a/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.ts +++ b/src/state/CallViewModel/remoteMembers/MatrixMemberMetadata.ts @@ -53,6 +53,32 @@ export function createRoomMembers$( roomToMembersMap(matrixRoom), ); } + +/** + * creates the member that this DM is with in case it is a DM (two members) otherwise null + */ +export function createDMMember$( + scope: ObservableScope, + roomMembers$: Behavior, + matrixRoom: MatrixRoom, +): Behavior | null> { + // We cannot use the normal direct check from matrix since we do not have access to the account data. + // use primitive member count === 2 check instead. + return scope.behavior( + roomMembers$.pipe( + map((membersMap) => { + // primitive appraoch do to no access to account data. + const isDM = membersMap.size === 2; + if (!isDM) return null; + return matrixRoom.getMember(matrixRoom.guessDMUserId()); + }), + ), + ); +} + /** * Displayname for each member of the call. This will disambiguate * any displayname that clashes with another member. Only members