Skip to content

Commit 5526cd7

Browse files
toger5Half-Shotrobintown
authored
Add sticky event support (#3513)
* add sticky event support - use new js-sdk - use custom synapse - don't filter rooms by existing call state events Signed-off-by: Timo K <[email protected]> * enable sticky events in the joinSessionConfig Signed-off-by: Timo K <[email protected]> * Remove unused useNewMembershipmanager setting * Add prefer sticky setting] * Fixup call detection logic to allow sticky events * lint * update docker image * More tidy * update checksum * bump js-sdk fix sticky events type Signed-off-by: Timo K <[email protected]> * fix demo Signed-off-by: Timo K <[email protected]> * always use multi sfu if we are using sticky events. Signed-off-by: Timo K <[email protected]> * review Signed-off-by: Timo K <[email protected]> * lint Signed-off-by: Timo K <[email protected]> * Always consider multi-SFU mode enabled when using sticky events CallViewModel would pass the wrong transport to enterRtcSession when the user enabled sticky events but didn't manually enable multi-SFU mode as well. This likely would've added some confusion to our attempts to test these modes. * Fix test type errors * add todo comment Signed-off-by: Timo K <[email protected]> --------- Signed-off-by: Timo K <[email protected]> Co-authored-by: Half-Shot <[email protected]> Co-authored-by: Robin <[email protected]>
1 parent 4936cdf commit 5526cd7

File tree

12 files changed

+161
-91
lines changed

12 files changed

+161
-91
lines changed

backend/dev_homeserver.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ experimental_features:
3838
# MSC4222 needed for syncv2 state_after. This allow clients to
3939
# correctly track the state of the room.
4040
msc4222_enabled: true
41+
# sticky events for matrixRTC user state
42+
msc4354_enabled: true
4143

4244
# The maximum allowed duration by which sent events can be delayed, as
4345
# per MSC4140. Must be a positive value if set. Defaults to no

dev-backend-docker-compose.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ services:
8888

8989
synapse:
9090
hostname: homeserver
91-
image: docker.io/matrixdotorg/synapse:latest
91+
image: ghcr.io/element-hq/synapse:msc4354-5
9292
pull_policy: always
9393
environment:
9494
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml

locales/en/app.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,9 +74,12 @@
7474
"matrix_id": "Matrix ID: {{id}}",
7575
"multi_sfu": "Multi-SFU media transport",
7676
"mute_all_audio": "Mute all audio (participants, reactions, join sounds)",
77+
"prefer_sticky_events": {
78+
"description": "Improves reliability of calls (requires homeserver support)",
79+
"label": "Prefer sticky events"
80+
},
7781
"show_connection_stats": "Show connection statistics",
7882
"url_params": "URL parameters",
79-
"use_new_membership_manager": "Use the new implementation of the call MembershipManager",
8083
"use_to_device_key_transport": "Use to device key transport. This will fallback to room key transport when another call member sent a room key"
8184
},
8285
"disconnected_banner": "Connectivity to the server has been lost.",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@
109109
"livekit-client": "^2.13.0",
110110
"lodash-es": "^4.17.21",
111111
"loglevel": "^1.9.1",
112-
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=develop",
112+
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/sticky-events&commit=e7f5bec51b6f70501a025b79fe5021c933385b21",
113113
"matrix-widget-api": "^1.13.0",
114114
"normalize.css": "^8.0.1",
115115
"observable-hooks": "^4.2.3",

src/home/useGroupCallRooms.ts

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {
2020
MatrixRTCSessionManagerEvents,
2121
type MatrixRTCSession,
2222
} from "matrix-js-sdk/lib/matrixrtc";
23-
import { logger } from "matrix-js-sdk/lib/logger";
2423

2524
import { getKeyForRoom } from "../e2ee/sharedKeyManagement";
2625

@@ -114,19 +113,49 @@ const roomIsJoinable = (room: Room): boolean => {
114113
}
115114
};
116115

116+
/**
117+
* Determines if a given room has call events in it, and therefore
118+
* is likely to be a call room.
119+
* @param room The Matrix room instance.
120+
* @returns `true` if the room has call events.
121+
*/
117122
const roomHasCallMembershipEvents = (room: Room): boolean => {
118-
switch (room.getMyMembership()) {
119-
case KnownMembership.Join:
120-
return !!room
121-
.getLiveTimeline()
122-
.getState(EventTimeline.FORWARDS)
123-
?.events?.get(EventType.GroupCallMemberPrefix);
124-
case KnownMembership.Knock:
125-
// Assume that a room you've knocked on is able to hold calls
123+
// Check our room membership first, to rule out any rooms
124+
// we can't have a call in.
125+
const myMembership = room.getMyMembership();
126+
if (myMembership === KnownMembership.Knock) {
127+
// Assume that a room you've knocked on is able to hold calls
128+
return true;
129+
} else if (myMembership !== KnownMembership.Join) {
130+
// Otherwise, non-joined rooms should never show up.
131+
return false;
132+
}
133+
134+
// Legacy member state checks (cheaper to check.)
135+
const timeline = room.getLiveTimeline();
136+
if (
137+
timeline
138+
.getState(EventTimeline.FORWARDS)
139+
?.events?.has(EventType.GroupCallMemberPrefix)
140+
) {
141+
return true;
142+
}
143+
144+
// Check for *active* calls using sticky events.
145+
for (const sticky of room._unstable_getStickyEvents()) {
146+
if (sticky.getType() === EventType.RTCMembership) {
126147
return true;
127-
default:
128-
return false;
148+
}
129149
}
150+
151+
// Otherwise, check recent event history to see if anyone had
152+
// sent a call membership in here.
153+
return timeline.getEvents().some(
154+
(e) =>
155+
// Membership events only count if both of these are true
156+
e.unstableStickyInfo && e.getType() === EventType.GroupCallMemberPrefix,
157+
);
158+
// Otherwise, it's *unlikely* this room was ever a call.
130159
};
131160

132161
export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
@@ -140,24 +169,22 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
140169
.filter(roomHasCallMembershipEvents)
141170
.filter(roomIsJoinable);
142171
const sortedRooms = sortRooms(client, rooms);
143-
Promise.all(
144-
sortedRooms.map((room) => {
145-
const session = client.matrixRTC.getRoomSession(room);
146-
return {
147-
roomAlias: room.getCanonicalAlias() ?? undefined,
148-
roomName: room.name,
149-
avatarUrl: room.getMxcAvatarUrl()!,
150-
room,
151-
session,
152-
participants: session.memberships
153-
.filter((m) => m.userId)
154-
.map((m) => room.getMember(m.userId!))
155-
.filter((m) => m) as RoomMember[],
156-
};
157-
}),
158-
)
159-
.then((items) => setRooms(items))
160-
.catch(logger.error);
172+
const items = sortedRooms.map((room) => {
173+
const session = client.matrixRTC.getRoomSession(room);
174+
return {
175+
roomAlias: room.getCanonicalAlias() ?? undefined,
176+
roomName: room.name,
177+
avatarUrl: room.getMxcAvatarUrl()!,
178+
room,
179+
session,
180+
participants: session.memberships
181+
.filter((m) => m.sender)
182+
.map((m) => room.getMember(m.sender!))
183+
.filter((m) => m) as RoomMember[],
184+
};
185+
});
186+
187+
setRooms(items);
161188
}
162189

163190
updateRooms();

src/room/GroupCallView.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,6 @@ import {
7070
UnknownCallError,
7171
} from "../utils/errors.ts";
7272
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
73-
import {
74-
useNewMembershipManager as useNewMembershipManagerSetting,
75-
useSetting,
76-
} from "../settings/settings";
7773
import { useTypedEventEmitter } from "../useEvents";
7874
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
7975
import { useAppBarTitle } from "../AppBar.tsx";
@@ -186,7 +182,6 @@ export const GroupCallView: FC<Props> = ({
186182
password: passwordFromUrl,
187183
} = useUrlParams();
188184
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
189-
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);
190185

191186
// Save the password once we start the groupCallView
192187
useEffect(() => {
@@ -310,7 +305,6 @@ export const GroupCallView: FC<Props> = ({
310305
mediaDevices,
311306
latestMuteStates,
312307
setJoined,
313-
useNewMembershipManager,
314308
]);
315309

316310
// TODO refactor this + "joined" to just one callState

src/rtcSessionHelpers.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ test("It joins the correct Session", async () => {
9696
{
9797
encryptMedia: true,
9898
useMultiSfu: USE_MUTI_SFU,
99+
preferStickyEvents: false,
99100
},
100101
);
101102

@@ -111,7 +112,6 @@ test("It joins the correct Session", async () => {
111112
expect.objectContaining({
112113
manageMediaKeys: true,
113114
useLegacyMemberEvents: false,
114-
useNewMembershipManager: true,
115115
useExperimentalToDeviceTransport: false,
116116
}),
117117
);
@@ -197,6 +197,7 @@ test("It should not fail with configuration error if homeserver config has livek
197197
{
198198
encryptMedia: true,
199199
useMultiSfu: USE_MUTI_SFU,
200+
preferStickyEvents: false,
200201
},
201202
);
202203
});

src/rtcSessionHelpers.ts

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,11 @@ export async function makeTransport(
9999

100100
export interface EnterRTCSessionOptions {
101101
encryptMedia: boolean;
102-
// TODO: remove this flag, the new membership manager is stable enough
103-
useNewMembershipManager?: boolean;
104102
// TODO: remove this flag, to-device transport is stable enough now
105103
useExperimentalToDeviceTransport?: boolean;
106104
/** EXPERIMENTAL: If true, will use the multi-sfu codepath where each member connects to its SFU instead of everyone connecting to an elected on. */
107-
useMultiSfu?: boolean;
105+
useMultiSfu: boolean;
106+
preferStickyEvents: boolean;
108107
}
109108

110109
/**
@@ -116,20 +115,13 @@ export interface EnterRTCSessionOptions {
116115
export async function enterRTCSession(
117116
rtcSession: MatrixRTCSession,
118117
transport: LivekitTransport,
119-
options: EnterRTCSessionOptions = {
120-
encryptMedia: true,
121-
useNewMembershipManager: true,
122-
useExperimentalToDeviceTransport: false,
123-
useMultiSfu: true,
124-
},
125-
): Promise<void> {
126-
const {
118+
{
127119
encryptMedia,
128-
useNewMembershipManager = true,
129120
useExperimentalToDeviceTransport = false,
130-
useMultiSfu = true,
131-
} = options;
132-
121+
useMultiSfu,
122+
preferStickyEvents,
123+
}: EnterRTCSessionOptions,
124+
): Promise<void> {
133125
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
134126
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
135127

@@ -148,7 +140,6 @@ export async function enterRTCSession(
148140
{
149141
notificationType,
150142
callIntent,
151-
useNewMembershipManager,
152143
manageMediaKeys: encryptMedia,
153144
...(useDeviceSessionMemberEvents !== undefined && {
154145
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
@@ -164,6 +155,7 @@ export async function enterRTCSession(
164155
membershipEventExpiryMs:
165156
matrixRtcSessionConfig?.membership_event_expiry_ms,
166157
useExperimentalToDeviceTransport,
158+
unstableSendStickyEvents: preferStickyEvents,
167159
},
168160
);
169161
if (widget) {

src/settings/DeveloperSettingsTab.tsx

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,37 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
55
Please see LICENSE in the repository root for full details.
66
*/
77

8-
import { type ChangeEvent, type FC, useCallback, useMemo } from "react";
8+
import {
9+
type ChangeEvent,
10+
type FC,
11+
useCallback,
12+
useEffect,
13+
useMemo,
14+
useState,
15+
} from "react";
916
import { useTranslation } from "react-i18next";
17+
import {
18+
UNSTABLE_MSC4354_STICKY_EVENTS,
19+
type MatrixClient,
20+
} from "matrix-js-sdk";
21+
import { logger } from "matrix-js-sdk/lib/logger";
1022

1123
import { FieldRow, InputField } from "../input/Input";
1224
import {
1325
useSetting,
1426
duplicateTiles as duplicateTilesSetting,
1527
debugTileLayout as debugTileLayoutSetting,
1628
showConnectionStats as showConnectionStatsSetting,
17-
useNewMembershipManager as useNewMembershipManagerSetting,
1829
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
1930
multiSfu as multiSfuSetting,
2031
muteAllAudio as muteAllAudioSetting,
2132
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
33+
preferStickyEvents as preferStickyEventsSetting,
2234
} from "./settings";
23-
import type { MatrixClient } from "matrix-js-sdk";
2435
import type { Room as LivekitRoom } from "livekit-client";
2536
import styles from "./DeveloperSettingsTab.module.css";
2637
import { useUrlParams } from "../UrlParams";
38+
2739
interface Props {
2840
client: MatrixClient;
2941
livekitRooms?: { room: LivekitRoom; url: string; isLocal?: boolean }[];
@@ -36,12 +48,24 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRooms }) => {
3648
debugTileLayoutSetting,
3749
);
3850

39-
const [showConnectionStats, setShowConnectionStats] = useSetting(
40-
showConnectionStatsSetting,
51+
const [stickyEventsSupported, setStickyEventsSupported] = useState(false);
52+
useEffect(() => {
53+
client
54+
.doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS)
55+
.then((result) => {
56+
setStickyEventsSupported(result);
57+
})
58+
.catch((ex) => {
59+
logger.warn("Failed to check if sticky events are supported", ex);
60+
});
61+
}, [client]);
62+
63+
const [preferStickyEvents, setPreferStickyEvents] = useSetting(
64+
preferStickyEventsSetting,
4165
);
4266

43-
const [useNewMembershipManager, setNewMembershipManager] = useSetting(
44-
useNewMembershipManagerSetting,
67+
const [showConnectionStats, setShowConnectionStats] = useSetting(
68+
showConnectionStatsSetting,
4569
);
4670

4771
const [alwaysShowIphoneEarpiece, setAlwaysShowIphoneEarpiece] = useSetting(
@@ -128,29 +152,31 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRooms }) => {
128152
</FieldRow>
129153
<FieldRow>
130154
<InputField
131-
id="showConnectionStats"
155+
id="preferStickyEvents"
132156
type="checkbox"
133-
label={t("developer_mode.show_connection_stats")}
134-
checked={!!showConnectionStats}
157+
label={t("developer_mode.prefer_sticky_events.label")}
158+
disabled={!stickyEventsSupported}
159+
description={t("developer_mode.prefer_sticky_events.description")}
160+
checked={!!preferStickyEvents}
135161
onChange={useCallback(
136162
(event: ChangeEvent<HTMLInputElement>): void => {
137-
setShowConnectionStats(event.target.checked);
163+
setPreferStickyEvents(event.target.checked);
138164
},
139-
[setShowConnectionStats],
165+
[setPreferStickyEvents],
140166
)}
141167
/>
142168
</FieldRow>
143169
<FieldRow>
144170
<InputField
145-
id="useNewMembershipManager"
171+
id="showConnectionStats"
146172
type="checkbox"
147-
label={t("developer_mode.use_new_membership_manager")}
148-
checked={!!useNewMembershipManager}
173+
label={t("developer_mode.show_connection_stats")}
174+
checked={!!showConnectionStats}
149175
onChange={useCallback(
150176
(event: ChangeEvent<HTMLInputElement>): void => {
151-
setNewMembershipManager(event.target.checked);
177+
setShowConnectionStats(event.target.checked);
152178
},
153-
[setNewMembershipManager],
179+
[setShowConnectionStats],
154180
)}
155181
/>
156182
</FieldRow>
@@ -173,7 +199,9 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRooms }) => {
173199
id="multiSfu"
174200
type="checkbox"
175201
label={t("developer_mode.multi_sfu")}
176-
checked={multiSfu}
202+
// If using sticky events we implicitly prefer use multi-sfu
203+
checked={multiSfu || preferStickyEvents}
204+
disabled={preferStickyEvents}
177205
onChange={useCallback(
178206
(event: ChangeEvent<HTMLInputElement>): void => {
179207
setMultiSfu(event.target.checked);

0 commit comments

Comments
 (0)