Skip to content
Merged
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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@
"linkify-react": "^4.3.3",
"linkifyjs": "^4.3.3",
"marked": "^18.0.5",
"matrix-js-sdk": "^38.4.0",
"matrix-js-sdk": "^41.9.0",
"matrix-widget-api": "^1.17.0",
"pdfjs-dist": "^6.1.200",
"react": "^18.3.1",
Expand Down
60 changes: 29 additions & 31 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/app/components/message/Reply.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Chip, Text, as, color, toRem } from 'folds';
import type { EventTimelineSet, IMentions, Room, SessionMembershipData } from '$types/matrix-sdk';
import type { EventTimelineSet, IMentions, Room } from '$types/matrix-sdk';
import { EventType, MsgType } from '$types/matrix-sdk';
import type { MouseEventHandler, ReactNode } from 'react';
import { useCallback, useMemo } from 'react';
Expand Down Expand Up @@ -403,7 +403,7 @@ export const Reply = as<'div', ReplyProps>(
image = timelineIcon(Hash);
bodyJSX = ' changed room avatar';
} else if (eventType === EventType.GroupCallMemberPrefix && !!replyEvent) {
const callJoined = replyEvent.getContent<SessionMembershipData>().application;
const callJoined = replyEvent.getContent<Record<string, unknown>>().application;
image = callJoined ? timelineIcon(Phone) : timelineIcon(PhoneDisconnect);
bodyJSX = callJoined ? ' joined the call' : ' ended the call';
} else if (eventType === EventType.RoomRedaction && replyEvent) {
Expand Down
4 changes: 2 additions & 2 deletions src/app/features/call/CallMemberCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { CallMembership, SessionMembershipData } from '$types/matrix-sdk';
import type { CallMembership } from '$types/matrix-sdk';
import { useState } from 'react';
import { Avatar, Box, Text } from 'folds';
import {
Expand All @@ -20,7 +20,7 @@ import { getMouseEventCords } from '../../utils/dom';
import * as css from './styles.css';

interface MemberWithMembershipData {
membershipData?: SessionMembershipData & {
membershipData?: Record<string, unknown> & {
'm.call.intent': 'video' | 'audio';
};
}
Expand Down
40 changes: 12 additions & 28 deletions src/app/features/call/callMembershipState.test.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,37 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Room } from '$types/matrix-sdk';
import { MatrixRTCSession } from '$types/matrix-sdk';
import { describe, expect, it } from 'vitest';
import {
getCallMembershipPresence,
isCallActive,
isIncomingCallActive,
isOutgoingCallPending,
type SessionDescription,
} from './callMembershipState';

const MY_USER_ID = '@self:example.org';
const SESSION_DESCRIPTION = {} as SessionDescription;
const room = { roomId: '!room:example.org' } as unknown as Room;

describe('callMembershipState', () => {
afterEach(() => {
vi.restoreAllMocks();
});

it('detects incoming call when remote members exist without self', () => {
vi.spyOn(MatrixRTCSession, 'sessionMembershipsForRoom').mockReturnValue([
{ userId: '@remote:example.org' },
] as never);
const memberships = [{ userId: '@remote:example.org' }];

expect(isIncomingCallActive(MY_USER_ID, room, SESSION_DESCRIPTION)).toBe(true);
expect(isCallActive(MY_USER_ID, room, SESSION_DESCRIPTION)).toBe(false);
expect(isOutgoingCallPending(MY_USER_ID, room, SESSION_DESCRIPTION)).toBe(false);
expect(isIncomingCallActive(MY_USER_ID, memberships)).toBe(true);
expect(isCallActive(MY_USER_ID, memberships)).toBe(false);
expect(isOutgoingCallPending(MY_USER_ID, memberships)).toBe(false);
});

it('detects active call when self and remote members exist', () => {
vi.spyOn(MatrixRTCSession, 'sessionMembershipsForRoom').mockReturnValue([
{ userId: MY_USER_ID },
{ userId: '@remote:example.org' },
] as never);
const memberships = [{ userId: MY_USER_ID }, { userId: '@remote:example.org' }];

expect(isIncomingCallActive(MY_USER_ID, room, SESSION_DESCRIPTION)).toBe(false);
expect(isCallActive(MY_USER_ID, room, SESSION_DESCRIPTION)).toBe(true);
expect(isOutgoingCallPending(MY_USER_ID, room, SESSION_DESCRIPTION)).toBe(false);
expect(isIncomingCallActive(MY_USER_ID, memberships)).toBe(false);
expect(isCallActive(MY_USER_ID, memberships)).toBe(true);
expect(isOutgoingCallPending(MY_USER_ID, memberships)).toBe(false);
});

it('detects pending outgoing call when only self is present', () => {
vi.spyOn(MatrixRTCSession, 'sessionMembershipsForRoom').mockReturnValue([
{ userId: MY_USER_ID },
] as never);
const memberships = [{ userId: MY_USER_ID }];

expect(getCallMembershipPresence(MY_USER_ID, room, SESSION_DESCRIPTION)).toEqual({
expect(getCallMembershipPresence(MY_USER_ID, memberships)).toEqual({
hasSelfMember: true,
remoteMemberCount: 0,
});
expect(isOutgoingCallPending(MY_USER_ID, room, SESSION_DESCRIPTION)).toBe(true);
expect(isOutgoingCallPending(MY_USER_ID, memberships)).toBe(true);
});
});
52 changes: 8 additions & 44 deletions src/app/features/call/callMembershipState.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,14 @@
import { MatrixRTCSession } from '$types/matrix-sdk';
import type { Room } from '$types/matrix-sdk';

export type SessionDescription = Parameters<typeof MatrixRTCSession.sessionMembershipsForRoom>[1];

type RtcMembership = { userId?: string; sender?: string };

export type CallMembershipPresence = {
hasSelfMember: boolean;
remoteMemberCount: number;
};

const getRoomMemberships = (room: Room, sessionDescription: SessionDescription) =>
MatrixRTCSession.sessionMembershipsForRoom(room, sessionDescription) as RtcMembership[];

export const getCallMembershipPresence = (
mxUserId: string,
room: Room,
sessionDescription: SessionDescription
memberships: RtcMembership[]
): CallMembershipPresence => {
const memberships = getRoomMemberships(room, sessionDescription);
const remoteMemberCount = memberships.filter(
(membership) => (membership.userId || membership.sender) !== mxUserId
).length;
Expand All @@ -29,54 +19,28 @@ export const getCallMembershipPresence = (
return { hasSelfMember, remoteMemberCount };
};

export const isIncomingCallActive = (
mxUserId: string,
room: Room,
sessionDescription: SessionDescription
): boolean => {
const { hasSelfMember, remoteMemberCount } = getCallMembershipPresence(
mxUserId,
room,
sessionDescription
);
export const isIncomingCallActive = (mxUserId: string, memberships: RtcMembership[]): boolean => {
const { hasSelfMember, remoteMemberCount } = getCallMembershipPresence(mxUserId, memberships);

return remoteMemberCount > 0 && !hasSelfMember;
};

export const isCallActive = (
mxUserId: string,
room: Room,
sessionDescription: SessionDescription
): boolean => {
const { hasSelfMember, remoteMemberCount } = getCallMembershipPresence(
mxUserId,
room,
sessionDescription
);
export const isCallActive = (mxUserId: string, memberships: RtcMembership[]): boolean => {
const { hasSelfMember, remoteMemberCount } = getCallMembershipPresence(mxUserId, memberships);

return hasSelfMember && remoteMemberCount > 0;
};

export const isOutgoingCallPending = (
mxUserId: string,
room: Room,
sessionDescription: SessionDescription
): boolean => {
const { hasSelfMember, remoteMemberCount } = getCallMembershipPresence(
mxUserId,
room,
sessionDescription
);
export const isOutgoingCallPending = (mxUserId: string, memberships: RtcMembership[]): boolean => {
const { hasSelfMember, remoteMemberCount } = getCallMembershipPresence(mxUserId, memberships);

return hasSelfMember && remoteMemberCount === 0;
};

export const getRemoteRtcMemberUserIds = (
mxUserId: string,
room: Room,
sessionDescription: SessionDescription
memberships: RtcMembership[]
): Set<string> => {
const memberships = getRoomMemberships(room, sessionDescription);
return new Set(
memberships
.map((membership) => membership.userId || membership.sender)
Expand Down
Loading
Loading