Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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: 2 additions & 0 deletions backend/dev_homeserver.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ experimental_features:
# MSC4222 needed for syncv2 state_after. This allow clients to
# correctly track the state of the room.
msc4222_enabled: true
# sticky events for matrixRTC user state
msc4354_enabled: true

# The maximum allowed duration by which sent events can be delayed, as
# per MSC4140. Must be a positive value if set. Defaults to no
Expand Down
2 changes: 1 addition & 1 deletion dev-backend-docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ services:

synapse:
hostname: homeserver
image: docker.io/matrixdotorg/synapse:latest
image: ghcr.io/element-hq/synapse:msc4354-5
pull_policy: always
environment:
- SYNAPSE_CONFIG_PATH=/data/cfg/homeserver.yaml
Expand Down
4 changes: 4 additions & 0 deletions locales/en/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@
"multi_sfu": "Multi-SFU media transport",
"mute_all_audio": "Mute all audio (participants, reactions, join sounds)",
"show_connection_stats": "Show connection statistics",
"prefer_sticky_events": {
"label": "Prefer sticky events",
"description": "Improves reliability of calls (requires homeserver support)"
},
"url_params": "URL parameters",
"use_new_membership_manager": "Use the new implementation of the call MembershipManager",
"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"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@
"livekit-client": "^2.13.0",
"lodash-es": "^4.17.21",
"loglevel": "^1.9.1",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=develop",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#head=toger5/sticky-events&commit=e7f5bec51b6f70501a025b79fe5021c933385b21",
"matrix-widget-api": "^1.13.0",
"normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3",
Expand Down
85 changes: 56 additions & 29 deletions src/home/useGroupCallRooms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import {
MatrixRTCSessionManagerEvents,
type MatrixRTCSession,
} from "matrix-js-sdk/lib/matrixrtc";
import { logger } from "matrix-js-sdk/lib/logger";

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

Expand Down Expand Up @@ -114,19 +113,49 @@ const roomIsJoinable = (room: Room): boolean => {
}
};

/**
* Determines if a given room has call events in it, and therefore
* is likely to be a call room.
* @param room The Matrix room instance.
* @returns `true` if the room has call events.
*/
const roomHasCallMembershipEvents = (room: Room): boolean => {
switch (room.getMyMembership()) {
case KnownMembership.Join:
return !!room
.getLiveTimeline()
.getState(EventTimeline.FORWARDS)
?.events?.get(EventType.GroupCallMemberPrefix);
case KnownMembership.Knock:
// Assume that a room you've knocked on is able to hold calls
// Check our room membership first, to rule out any rooms
// we can't have a call in.
const myMembership = room.getMyMembership();
if (myMembership === KnownMembership.Knock) {
// Assume that a room you've knocked on is able to hold calls
return true;
} else if (myMembership !== KnownMembership.Join) {
// Otherwise, non-joined rooms should never show up.
return false;
}

// Legacy member state checks (cheaper to check.)
const timeline = room.getLiveTimeline();
if (
timeline
.getState(EventTimeline.FORWARDS)
?.events?.has(EventType.GroupCallMemberPrefix)
) {
return true;
}

// Check for *active* calls using sticky events.
for (const sticky of room._unstable_getStickyEvents()) {
if (sticky.getType() === EventType.GroupCallMemberPrefix) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presumably we should refer to RTCMembership here rather than GroupCallMemberPrefix (though TBH we could probably replace this entire function body with return true; and it would be fine… we're definitely not aiming to support using the SPA in cases where the homeserver is also used by messenger apps… well, maybe we are with the guest access use case, but I don't think we'll care about giving guest access users a home page at all.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

100%.
Lets keep the function in for now. There might be some use for it in the guest case and it is very detangled from the rest of element-call -> not complexifying the codebase. (just makes it larger)

return true;
default:
return false;
}
}

// Otherwise, check recent event history to see if anyone had
// sent a call membership in here.
return timeline.getEvents().some(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no API to get the sticky events? Having the sticky key doesn't make the event sticky.
I am a bit lost with what this method is expecting to do

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this method exactly? Try some heuristic to see if a room is a call room? like type": "org.matrix.msc3417.call"?

Or if there might be an active call?
And now it is checking for possible past sticky events (but potentially not anymore sticky)?
I don't get what this doing and what for

(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.
};

export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
Expand All @@ -140,24 +169,22 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] {
.filter(roomHasCallMembershipEvents)
.filter(roomIsJoinable);
const sortedRooms = sortRooms(client, rooms);
Promise.all(
sortedRooms.map((room) => {
const session = client.matrixRTC.getRoomSession(room);
return {
roomAlias: room.getCanonicalAlias() ?? undefined,
roomName: room.name,
avatarUrl: room.getMxcAvatarUrl()!,
room,
session,
participants: session.memberships
.filter((m) => m.userId)
.map((m) => room.getMember(m.userId!))
.filter((m) => m) as RoomMember[],
};
}),
)
.then((items) => setRooms(items))
.catch(logger.error);
const items = sortedRooms.map((room) => {
const session = client.matrixRTC.getRoomSession(room);
return {
roomAlias: room.getCanonicalAlias() ?? undefined,
roomName: room.name,
avatarUrl: room.getMxcAvatarUrl()!,
room,
session,
participants: session.memberships
.filter((m) => m.sender)
.map((m) => room.getMember(m.sender!))
.filter((m) => m) as RoomMember[],
};
});

setRooms(items);
}

updateRooms();
Expand Down
7 changes: 1 addition & 6 deletions src/room/GroupCallView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,7 @@
UnknownCallError,
} from "../utils/errors.ts";
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
import {
useNewMembershipManager as useNewMembershipManagerSetting,
useSetting,
} from "../settings/settings";
import { useSetting } from "../settings/settings";
import { useTypedEventEmitter } from "../useEvents";
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
import { useAppBarTitle } from "../AppBar.tsx";
Expand Down Expand Up @@ -186,7 +183,6 @@
password: passwordFromUrl,
} = useUrlParams();
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
const [useNewMembershipManager] = useSetting(useNewMembershipManagerSetting);

// Save the password once we start the groupCallView
useEffect(() => {
Expand Down Expand Up @@ -310,7 +306,6 @@
mediaDevices,
latestMuteStates,
setJoined,
useNewMembershipManager,
]);

// TODO refactor this + "joined" to just one callState
Expand Down Expand Up @@ -378,7 +373,7 @@
} catch (e) {
logger.error("Failed to send close action", e);
}
widget.api.transport.stop();

Check failure on line 376 in src/room/GroupCallView.tsx

View workflow job for this annotation

GitHub Actions / Run unit tests

Unhandled error

TypeError: Cannot read properties of undefined (reading 'stop') ❯ src/room/GroupCallView.tsx:376:36 This error originated in "src/room/GroupCallView.test.tsx" test file. It doesn't mean the error was thrown inside the file itself, but while it was running. The latest test that might've caused the error is "GroupCallView plays a leave sound synchronously in widget mode". It might mean one of the following: - The error was thrown, while Vitest was running this test. - If the error occurred after the test had been completed, this was the last documented test before it was thrown.
}
}
});
Expand Down
1 change: 0 additions & 1 deletion src/rtcSessionHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ test("It joins the correct Session", async () => {
expect.objectContaining({
manageMediaKeys: true,
useLegacyMemberEvents: false,
useNewMembershipManager: true,
useExperimentalToDeviceTransport: false,
}),
);
Expand Down
5 changes: 2 additions & 3 deletions src/rtcSessionHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ElementWidgetActions, widget, type WidgetHelpers } from "./widget";
import { MatrixRTCTransportMissingError } from "./utils/errors";
import { getUrlParams } from "./UrlParams";
import { getSFUConfigWithOpenID } from "./livekit/openIDSFU.ts";
import { preferStickyEvents } from "./settings/settings.ts";

const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";

Expand Down Expand Up @@ -118,14 +119,12 @@ export async function enterRTCSession(
transport: LivekitTransport,
options: EnterRTCSessionOptions = {
encryptMedia: true,
useNewMembershipManager: true,
useExperimentalToDeviceTransport: false,
useMultiSfu: true,
},
): Promise<void> {
const {
encryptMedia,
useNewMembershipManager = true,
useExperimentalToDeviceTransport = false,
useMultiSfu = true,
} = options;
Expand All @@ -148,7 +147,6 @@ export async function enterRTCSession(
{
notificationType,
callIntent,
useNewMembershipManager,
manageMediaKeys: encryptMedia,
...(useDeviceSessionMemberEvents !== undefined && {
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
Expand All @@ -164,6 +162,7 @@ export async function enterRTCSession(
membershipEventExpiryMs:
matrixRtcSessionConfig?.membership_event_expiry_ms,
useExperimentalToDeviceTransport,
unstableSendStickyEvents: preferStickyEvents.getValue(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could be nice to have preferStickyEvents be an explicit parameter of enterRtcSession, like multiSfu is, so we can easily turn on sticky events in the middle of a call when dogfooding

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we would still need to rejoin and setup a new membership manager. I am not sure its worth the effort?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure but with the scope.reconcile method already used for starting and stopping the membership managers as needed, this ends up being pretty trivial.

Incidentally I see it as necessary to make the CallViewModel also aware of the relationship between multiSfu and preferStickyEvents (the fact that one force-enables the other), otherwise it would pass the wrong transport to enterRtcSession. So I just committed e313cf0 which handles both these things. Does it look reasonable to you?

},
);
if (widget) {
Expand Down
60 changes: 43 additions & 17 deletions src/settings/DeveloperSettingsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,37 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/

import { type ChangeEvent, type FC, useCallback, useMemo } from "react";
import {
type ChangeEvent,
type FC,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { useTranslation } from "react-i18next";
import {
UNSTABLE_MSC4354_STICKY_EVENTS,
type MatrixClient,
} from "matrix-js-sdk";
import { logger } from "matrix-js-sdk/lib/logger";

import { FieldRow, InputField } from "../input/Input";
import {
useSetting,
duplicateTiles as duplicateTilesSetting,
debugTileLayout as debugTileLayoutSetting,
showConnectionStats as showConnectionStatsSetting,
useNewMembershipManager as useNewMembershipManagerSetting,
useExperimentalToDeviceTransport as useExperimentalToDeviceTransportSetting,
multiSfu as multiSfuSetting,
muteAllAudio as muteAllAudioSetting,
alwaysShowIphoneEarpiece as alwaysShowIphoneEarpieceSetting,
preferStickyEvents as preferStickyEventsSetting,
} from "./settings";
import type { MatrixClient } from "matrix-js-sdk";
import type { Room as LivekitRoom } from "livekit-client";
import styles from "./DeveloperSettingsTab.module.css";
import { useUrlParams } from "../UrlParams";

interface Props {
client: MatrixClient;
livekitRooms?: { room: LivekitRoom; url: string; isLocal?: boolean }[];
Expand All @@ -36,12 +48,24 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRooms }) => {
debugTileLayoutSetting,
);

const [showConnectionStats, setShowConnectionStats] = useSetting(
showConnectionStatsSetting,
const [stickyEventsSupported, setStickyEventsSupported] = useState(false);
useEffect(() => {
client
.doesServerSupportUnstableFeature(UNSTABLE_MSC4354_STICKY_EVENTS)
.then((result) => {
setStickyEventsSupported(result);
})
.catch((ex) => {
logger.warn("Failed to check if sticky events are supported", ex);
});
}, [client]);

const [preferStickyEvents, setPreferStickyEvents] = useSetting(
preferStickyEventsSetting,
);

const [useNewMembershipManager, setNewMembershipManager] = useSetting(
useNewMembershipManagerSetting,
const [showConnectionStats, setShowConnectionStats] = useSetting(
showConnectionStatsSetting,
);

const [alwaysShowIphoneEarpiece, setAlwaysShowIphoneEarpiece] = useSetting(
Expand Down Expand Up @@ -128,29 +152,31 @@ export const DeveloperSettingsTab: FC<Props> = ({ client, livekitRooms }) => {
</FieldRow>
<FieldRow>
<InputField
id="showConnectionStats"
id="preferStickyEvents"
type="checkbox"
label={t("developer_mode.show_connection_stats")}
checked={!!showConnectionStats}
label={t("developer_mode.prefer_sticky_events.label")}
disabled={!stickyEventsSupported}
description={t("developer_mode.prefer_sticky_events.description")}
checked={!!preferStickyEvents}
onChange={useCallback(
(event: ChangeEvent<HTMLInputElement>): void => {
setShowConnectionStats(event.target.checked);
setPreferStickyEvents(event.target.checked);
},
[setShowConnectionStats],
[setPreferStickyEvents],
)}
/>
</FieldRow>
<FieldRow>
<InputField
id="useNewMembershipManager"
id="showConnectionStats"
type="checkbox"
label={t("developer_mode.use_new_membership_manager")}
checked={!!useNewMembershipManager}
label={t("developer_mode.show_connection_stats")}
checked={!!showConnectionStats}
onChange={useCallback(
(event: ChangeEvent<HTMLInputElement>): void => {
setNewMembershipManager(event.target.checked);
setShowConnectionStats(event.target.checked);
},
[setNewMembershipManager],
[setShowConnectionStats],
)}
/>
</FieldRow>
Expand Down
10 changes: 5 additions & 5 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ export const showConnectionStats = new Setting<boolean>(
false,
);

export const preferStickyEvents = new Setting<boolean>(
"prefer-sticky-events",
false,
);

export const audioInput = new Setting<string | undefined>(
"audio-input",
undefined,
Expand Down Expand Up @@ -115,11 +120,6 @@ export const soundEffectVolume = new Setting<number>(
0.5,
);

export const useNewMembershipManager = new Setting<boolean>(
"new-membership-manager",
true,
);

export const useExperimentalToDeviceTransport = new Setting<boolean>(
"experimental-to-device-transport",
true,
Expand Down
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -7545,7 +7545,7 @@ __metadata:
livekit-client: "npm:^2.13.0"
lodash-es: "npm:^4.17.21"
loglevel: "npm:^1.9.1"
matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=develop"
matrix-js-sdk: "github:matrix-org/matrix-js-sdk#head=toger5/sticky-events&commit=e7f5bec51b6f70501a025b79fe5021c933385b21"
matrix-widget-api: "npm:^1.13.0"
normalize.css: "npm:^8.0.1"
observable-hooks: "npm:^4.2.3"
Expand Down Expand Up @@ -10335,9 +10335,9 @@ __metadata:
languageName: node
linkType: hard

"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=develop":
"matrix-js-sdk@github:matrix-org/matrix-js-sdk#head=toger5/sticky-events&commit=e7f5bec51b6f70501a025b79fe5021c933385b21":
version: 38.4.0
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=fd949fe486038099ee111a72b57ce711e85bc352"
resolution: "matrix-js-sdk@https://github.com/matrix-org/matrix-js-sdk.git#commit=e7f5bec51b6f70501a025b79fe5021c933385b21"
dependencies:
"@babel/runtime": "npm:^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm": "npm:^15.3.0"
Expand All @@ -10353,7 +10353,7 @@ __metadata:
sdp-transform: "npm:^2.14.1"
unhomoglyph: "npm:^1.0.6"
uuid: "npm:13"
checksum: 10c0/d334811074726482b58089fef6c9e98a462fbc1d91c63798307648bdc1349d2e154aa31f391690e2c9cd90eee61a3be9fe8872873e7f828b529d9268d2a25b78
checksum: 10c0/7adffdc183affd2d3ee1e8497cad6ca7904a37f98328ff7bc15aa6c1829dc9f9a92f8e1bd6260432a33626ff2a839644de938270163e73438b7294675cd954e4
languageName: node
linkType: hard

Expand Down
Loading