mirror of
https://github.com/vector-im/element-call.git
synced 2026-11-04 00:42:28 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dde07f6d54 | ||
|
|
de05b82cfe | ||
|
|
b0e985c014 | ||
|
|
33368ab8da | ||
|
|
d85a4a1929 | ||
|
|
a6930c5aa9 | ||
|
|
e83093feb2 | ||
|
|
c50127ab62 | ||
|
|
c3ae1c516b | ||
|
|
b975dce53e |
Generated
+535
-264
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@
|
||||
padding-top: env(safe-area-inset-top);
|
||||
opacity: 1;
|
||||
transition: opacity 0.15s;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Pseudo-element for the gradient background */
|
||||
@@ -52,7 +53,7 @@
|
||||
". . ."
|
||||
"primaryButton title secondaryButton"
|
||||
". . .";
|
||||
place-items: center;
|
||||
place-items: center stretch;
|
||||
column-gap: var(--cpd-space-2x);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { renderHook } from "@testing-library/react";
|
||||
import {
|
||||
EventType,
|
||||
JoinRule,
|
||||
KnownMembership,
|
||||
type MatrixClient,
|
||||
MatrixEvent,
|
||||
type Room,
|
||||
} from "matrix-js-sdk";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { useGroupCallRooms } from "./useGroupCallRooms";
|
||||
import { mockMatrixRoom } from "../utils/test";
|
||||
|
||||
describe("useGroupCallRooms", () => {
|
||||
test("lists a joined room with legacy call member state", () => {
|
||||
const room = mockCallRoom({ legacyMemberState: true });
|
||||
expect(listedRoomIds([room])).toEqual([room.roomId]);
|
||||
});
|
||||
|
||||
test("lists a joined room with a sticky RTC membership", () => {
|
||||
const room = mockCallRoom({
|
||||
stickyEvents: [new MatrixEvent({ type: EventType.RTCMembership })],
|
||||
});
|
||||
expect(listedRoomIds([room])).toEqual([room.roomId]);
|
||||
});
|
||||
|
||||
test("lists a joined room with an RTC membership in its timeline", () => {
|
||||
const room = mockCallRoom({
|
||||
timelineEvents: [new MatrixEvent({ type: EventType.RTCMembership })],
|
||||
});
|
||||
expect(listedRoomIds([room])).toEqual([room.roomId]);
|
||||
});
|
||||
|
||||
test("lists a knocked room without any call events", () => {
|
||||
const room = mockCallRoom({
|
||||
membership: KnownMembership.Knock,
|
||||
joinRule: JoinRule.Knock,
|
||||
});
|
||||
expect(listedRoomIds([room])).toEqual([room.roomId]);
|
||||
});
|
||||
|
||||
test("omits a joined room without any call events", () => {
|
||||
const room = mockCallRoom({
|
||||
stickyEvents: [new MatrixEvent({ type: EventType.RoomMessage })],
|
||||
timelineEvents: [new MatrixEvent({ type: EventType.RoomMessage })],
|
||||
});
|
||||
expect(listedRoomIds([room])).toEqual([]);
|
||||
});
|
||||
|
||||
test.each([KnownMembership.Invite, KnownMembership.Leave])(
|
||||
"omits a room with %s membership despite call events",
|
||||
(membership) => {
|
||||
const room = mockCallRoom({
|
||||
membership,
|
||||
legacyMemberState: true,
|
||||
stickyEvents: [new MatrixEvent({ type: EventType.RTCMembership })],
|
||||
timelineEvents: [new MatrixEvent({ type: EventType.RTCMembership })],
|
||||
});
|
||||
expect(listedRoomIds([room])).toEqual([]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
function listedRoomIds(rooms: Room[]): string[] {
|
||||
const client = {
|
||||
getRooms: () => rooms,
|
||||
getUserId: () => "@alice:example.org",
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
matrixRTC: {
|
||||
getRoomSession: () => ({ memberships: [] }),
|
||||
on: () => {},
|
||||
off: () => {},
|
||||
},
|
||||
} as unknown as MatrixClient;
|
||||
const { result } = renderHook(() => useGroupCallRooms(client));
|
||||
return result.current.map(({ room }) => room.roomId);
|
||||
}
|
||||
|
||||
let nextRoomId = 0;
|
||||
|
||||
function mockCallRoom({
|
||||
membership = KnownMembership.Join,
|
||||
joinRule = JoinRule.Public,
|
||||
legacyMemberState = false,
|
||||
stickyEvents = [],
|
||||
timelineEvents = [],
|
||||
}: {
|
||||
membership?: KnownMembership;
|
||||
joinRule?: JoinRule;
|
||||
legacyMemberState?: boolean;
|
||||
stickyEvents?: MatrixEvent[];
|
||||
timelineEvents?: MatrixEvent[];
|
||||
}): Room {
|
||||
const stateEvents = new Map(
|
||||
legacyMemberState ? [[EventType.GroupCallMemberPrefix, new Map()]] : [],
|
||||
);
|
||||
return mockMatrixRoom({
|
||||
roomId: `!room${nextRoomId++}:example.org`,
|
||||
name: "Call room",
|
||||
getMyMembership: () => membership,
|
||||
getJoinRule: () => joinRule,
|
||||
hasEncryptionStateEvent: () => true,
|
||||
getCanonicalAlias: () => null,
|
||||
getMxcAvatarUrl: () => null,
|
||||
getLiveTimeline: () =>
|
||||
({
|
||||
getState: () => ({ events: stateEvents }),
|
||||
getEvents: () => timelineEvents,
|
||||
}) as unknown as ReturnType<Room["getLiveTimeline"]>,
|
||||
_unstable_getStickyEvents: () =>
|
||||
stickyEvents as unknown as ReturnType<Room["_unstable_getStickyEvents"]>,
|
||||
});
|
||||
}
|
||||
@@ -141,7 +141,7 @@ const roomHasCallMembershipEvents = (room: Room): boolean => {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check for recent calls using sticky events.
|
||||
// Check for *active* calls using sticky events.
|
||||
for (const sticky of room._unstable_getStickyEvents()) {
|
||||
if (sticky.getType() === EventType.RTCMembership) {
|
||||
return true;
|
||||
@@ -150,9 +150,11 @@ const roomHasCallMembershipEvents = (room: Room): boolean => {
|
||||
|
||||
// Otherwise, check recent event history to see if anyone had
|
||||
// sent a call membership in here.
|
||||
return timeline
|
||||
.getEvents()
|
||||
.some((e) => e.getType() === EventType.RTCMembership);
|
||||
return timeline.getEvents().some(
|
||||
(e) =>
|
||||
// Membership events only count if both of these are true
|
||||
e.unstableStickyInfo && e.getType() === EventType.GroupCallMemberPrefix,
|
||||
);
|
||||
// Otherwise, it's *unlikely* this room was ever a call.
|
||||
};
|
||||
|
||||
|
||||
@@ -5,14 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import EventEmitter from "events";
|
||||
import { expect, test } from "vitest";
|
||||
import { test } from "vitest";
|
||||
import {
|
||||
EventType,
|
||||
type IRoomTimelineData,
|
||||
MatrixEvent,
|
||||
type Room,
|
||||
RoomEvent,
|
||||
RoomStickyEventsEvent,
|
||||
} from "matrix-js-sdk";
|
||||
import { type RTCCallIntent } from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { map, mergeMap, NEVER, type Observable, startWith } from "rxjs";
|
||||
@@ -27,7 +25,6 @@ import {
|
||||
import {
|
||||
type CallNotificationWrapper,
|
||||
createCallNotificationLifecycle$,
|
||||
createReceivedDecline$,
|
||||
type RingAttempt,
|
||||
} from "./CallNotificationLifecycle";
|
||||
import { constant } from "../Behavior";
|
||||
@@ -184,16 +181,22 @@ test("ring attempt can be declined", () => {
|
||||
a: mockRingEvent("$notif1", 30),
|
||||
}),
|
||||
receivedDecline$: hot("--d", {
|
||||
d: new MatrixEvent({
|
||||
type: EventType.RTCDecline,
|
||||
sender: alice.userId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$notif1",
|
||||
d: [
|
||||
new MatrixEvent({
|
||||
type: EventType.RTCDecline,
|
||||
sender: alice.userId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$notif1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{} as Room,
|
||||
undefined,
|
||||
false,
|
||||
{} as IRoomTimelineData,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -213,16 +216,22 @@ test("ring attempt times out if recipient declines too late", () => {
|
||||
a: mockRingEvent("$notif1", 30),
|
||||
}),
|
||||
receivedDecline$: hot("100ms d", {
|
||||
d: new MatrixEvent({
|
||||
type: EventType.RTCDecline,
|
||||
sender: alice.userId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$notif1",
|
||||
d: [
|
||||
new MatrixEvent({
|
||||
type: EventType.RTCDecline,
|
||||
sender: alice.userId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$notif1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{} as Room,
|
||||
undefined,
|
||||
false,
|
||||
{} as IRoomTimelineData,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -242,16 +251,22 @@ test("decline event relating to wrong event is ignored (times out)", () => {
|
||||
a: mockRingEvent("$notif1", 30),
|
||||
}),
|
||||
receivedDecline$: hot("--d", {
|
||||
d: new MatrixEvent({
|
||||
type: EventType.RTCDecline,
|
||||
sender: alice.userId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$other", // <---- WRONG
|
||||
d: [
|
||||
new MatrixEvent({
|
||||
type: EventType.RTCDecline,
|
||||
sender: alice.userId,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$other", // <---- WRONG
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{} as Room,
|
||||
undefined,
|
||||
false,
|
||||
{} as IRoomTimelineData,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -271,16 +286,22 @@ test("decline event from wrong sender is ignored (times out)", () => {
|
||||
a: mockRingEvent("$notif1", 30),
|
||||
}),
|
||||
receivedDecline$: hot("--d", {
|
||||
d: new MatrixEvent({
|
||||
type: EventType.RTCDecline,
|
||||
sender: local.userId, // <---- WRONG
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$notif1",
|
||||
d: [
|
||||
new MatrixEvent({
|
||||
type: EventType.RTCDecline,
|
||||
sender: local.userId, // <---- WRONG
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
rel_type: "m.reference",
|
||||
event_id: "$notif1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
{} as Room,
|
||||
undefined,
|
||||
false,
|
||||
{} as IRoomTimelineData,
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -290,32 +311,3 @@ test("decline event from wrong sender is ignored (times out)", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test("declines are received from the timeline and the sticky section", () => {
|
||||
const room = new EventEmitter();
|
||||
const received: string[] = [];
|
||||
const subscription = createReceivedDecline$(
|
||||
room as unknown as Room,
|
||||
).subscribe((event) => received.push(event.getId()!));
|
||||
const event = (eventId: string, type = EventType.RTCDecline): MatrixEvent =>
|
||||
new MatrixEvent({ event_id: eventId, type, sender: alice.userId });
|
||||
|
||||
const timelineArgs = [room, false, false, {}];
|
||||
room.emit(RoomEvent.Timeline, event("$timeline"), ...timelineArgs);
|
||||
room.emit(
|
||||
RoomEvent.Timeline,
|
||||
event("$message", EventType.RoomMessage),
|
||||
...timelineArgs,
|
||||
);
|
||||
// A gappy sync delivers the decline only in the sticky section
|
||||
room.emit(RoomStickyEventsEvent.Update, [event("$sticky")], [], []);
|
||||
room.emit(
|
||||
RoomStickyEventsEvent.Update,
|
||||
[],
|
||||
[{ previous: event("$old"), current: event("$replacement") }],
|
||||
[event("$removed")],
|
||||
);
|
||||
subscription.unsubscribe();
|
||||
|
||||
expect(received).toEqual(["$timeline", "$sticky", "$replacement"]);
|
||||
});
|
||||
|
||||
@@ -25,17 +25,13 @@ import {
|
||||
EMPTY,
|
||||
race,
|
||||
take,
|
||||
mergeMap,
|
||||
} from "rxjs";
|
||||
import {
|
||||
type EventTimelineSetHandlerMap,
|
||||
EventType,
|
||||
type MatrixEvent,
|
||||
type Room as MatrixRoom,
|
||||
RoomEvent,
|
||||
RoomStickyEventsEvent,
|
||||
} from "matrix-js-sdk";
|
||||
import { type RoomStickyEventsMap } from "matrix-js-sdk/lib/models/room-sticky-events";
|
||||
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
|
||||
|
||||
import { type Behavior, type ObservableScope } from "@element-hq/matrixrtc-sdk";
|
||||
@@ -72,28 +68,12 @@ export function createSentCallNotification$(
|
||||
|
||||
export function createReceivedDecline$(
|
||||
matrixRoom: MatrixRoom,
|
||||
): Observable<MatrixEvent> {
|
||||
// A decline is sticky, so after a gappy sync it arrives only in the sticky
|
||||
// section, which never reaches the timeline. Racing for the first outcome
|
||||
// makes a decline seen in both places harmless.
|
||||
const timeline$ = (
|
||||
): Observable<Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>> {
|
||||
return (
|
||||
fromEvent(matrixRoom, RoomEvent.Timeline) as Observable<
|
||||
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
|
||||
>
|
||||
).pipe(map(([event]) => event));
|
||||
const sticky$ = (
|
||||
fromEvent(matrixRoom, RoomStickyEventsEvent.Update) as Observable<
|
||||
Parameters<RoomStickyEventsMap[RoomStickyEventsEvent.Update]>
|
||||
>
|
||||
).pipe(
|
||||
mergeMap(([added, updated]) => [
|
||||
...added,
|
||||
...updated.map(({ current }) => current),
|
||||
]),
|
||||
);
|
||||
return merge(timeline$, sticky$).pipe(
|
||||
filter((event) => event.getType() === EventType.RTCDecline),
|
||||
);
|
||||
).pipe(filter(([event]) => event.getType() === EventType.RTCDecline));
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
@@ -103,7 +83,9 @@ export interface Props {
|
||||
/** The user ids of the room's members, ours included. */
|
||||
roomMemberUserIds$: Behavior<string[]>;
|
||||
sentCallNotification$: Observable<CallNotificationWrapper | null>;
|
||||
receivedDecline$: Observable<MatrixEvent>;
|
||||
receivedDecline$: Observable<
|
||||
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
|
||||
>;
|
||||
options: { waitForCallPickup?: boolean; autoLeaveWhenOthersLeft?: boolean };
|
||||
localUser: { deviceId: string; userId: string };
|
||||
}
|
||||
@@ -164,7 +146,7 @@ export function createCallNotificationLifecycle$({
|
||||
// Call is declined when we receive a decline event
|
||||
const decline$ = receivedDecline$.pipe(
|
||||
filter(
|
||||
(event) =>
|
||||
([event]) =>
|
||||
event.getRelation()?.rel_type === "m.reference" &&
|
||||
event.getRelation()?.event_id === notificationEvent.event_id &&
|
||||
event.getSender() === recipient,
|
||||
|
||||
Reference in New Issue
Block a user