mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-19 06:20:25 +00:00
move even more into the vm.
This commit is contained in:
@@ -248,7 +248,6 @@ export const InCallView: FC<InCallViewProps> = ({
|
||||
() => 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<InCallViewProps> = ({
|
||||
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<InCallViewProps> = ({
|
||||
|
||||
// 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 ? (
|
||||
<div className={classNames(overlayStyles.bg, waitingStyles.overlay)}>
|
||||
<div
|
||||
className={classNames(overlayStyles.content, waitingStyles.content)}
|
||||
>
|
||||
<div className={waitingStyles.pulse}>
|
||||
<Avatar id={id} name={name} src={avatarMxc} size={AvatarSize.XL} />
|
||||
<Avatar
|
||||
id={ringOverlay.idForAvatar}
|
||||
name={ringOverlay.name}
|
||||
src={ringOverlay.avatarMxc}
|
||||
size={AvatarSize.XL}
|
||||
/>
|
||||
</div>
|
||||
<Text size="md" className={waitingStyles.text}>
|
||||
{text}
|
||||
{ringOverlay.text}
|
||||
</Text>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}, [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
|
||||
|
||||
@@ -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<Record<string, RaisedHandInfo>>;
|
||||
/** List of reactions. Keys are: membership.membershipId (currently predefined as: `${membershipEvent.userId}:${membershipEvent.deviceId}`)*/
|
||||
public reactions$: Behavior<Record<string, ReactionOption>>;
|
||||
public isOneOnOneWith$: Behavior<Pick<
|
||||
RoomMember,
|
||||
"userId" | "getMxcAvatarUrl" | "rawDisplayName"
|
||||
> | null>;
|
||||
public localUserIsAlone$: Behavior<boolean>;
|
||||
|
||||
public ringOverlay$: Behavior<null | {
|
||||
name: string;
|
||||
/** roomId or userId for the avatar generation. */
|
||||
idForAvatar: string;
|
||||
text: string;
|
||||
avatarMxc?: string;
|
||||
}>;
|
||||
// sounds and events
|
||||
public joinSoundEffect$: Observable<void>;
|
||||
public leaveSoundEffect$: Observable<void>;
|
||||
@@ -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$;
|
||||
|
||||
@@ -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<RoomMemberMap>,
|
||||
matrixRoom: MatrixRoom,
|
||||
): Behavior<Pick<
|
||||
RoomMember,
|
||||
"userId" | "getMxcAvatarUrl" | "rawDisplayName"
|
||||
> | 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
|
||||
|
||||
Reference in New Issue
Block a user