Sticky event handling fixes

This commit is contained in:
Johannes Marbach
2026-10-16 14:47:10 +02:00
parent 8be229a438
commit 9c64316b42
3 changed files with 99 additions and 75 deletions
+4 -6
View File
@@ -141,7 +141,7 @@ const roomHasCallMembershipEvents = (room: Room): boolean => {
return true;
}
// Check for *active* calls using sticky events.
// Check for recent calls using sticky events.
for (const sticky of room._unstable_getStickyEvents()) {
if (sticky.getType() === EventType.RTCMembership) {
return true;
@@ -150,11 +150,9 @@ 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) =>
// Membership events only count if both of these are true
e.unstableStickyInfo && e.getType() === EventType.GroupCallMemberPrefix,
);
return timeline
.getEvents()
.some((e) => e.getType() === EventType.RTCMembership);
// Otherwise, it's *unlikely* this room was ever a call.
};
@@ -5,12 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { test } from "vitest";
import EventEmitter from "events";
import { expect, 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";
@@ -25,6 +27,7 @@ import {
import {
type CallNotificationWrapper,
createCallNotificationLifecycle$,
createReceivedDecline$,
type RingAttempt,
} from "./CallNotificationLifecycle";
import { constant } from "../Behavior";
@@ -181,22 +184,16 @@ 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,
],
},
}),
}),
});
@@ -216,22 +213,16 @@ 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,
],
},
}),
}),
});
@@ -251,22 +242,16 @@ 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,
],
},
}),
}),
});
@@ -286,22 +271,16 @@ 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,
],
},
}),
}),
});
@@ -311,3 +290,32 @@ 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,13 +25,17 @@ 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";
@@ -68,12 +72,28 @@ export function createSentCallNotification$(
export function createReceivedDecline$(
matrixRoom: MatrixRoom,
): Observable<Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>> {
return (
): 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$ = (
fromEvent(matrixRoom, RoomEvent.Timeline) as Observable<
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
>
).pipe(filter(([event]) => event.getType() === EventType.RTCDecline));
).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),
);
}
export interface Props {
@@ -83,9 +103,7 @@ export interface Props {
/** The user ids of the room's members, ours included. */
roomMemberUserIds$: Behavior<string[]>;
sentCallNotification$: Observable<CallNotificationWrapper | null>;
receivedDecline$: Observable<
Parameters<EventTimelineSetHandlerMap[RoomEvent.Timeline]>
>;
receivedDecline$: Observable<MatrixEvent>;
options: { waitForCallPickup?: boolean; autoLeaveWhenOthersLeft?: boolean };
localUser: { deviceId: string; userId: string };
}
@@ -146,7 +164,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,