Compare commits

...
Author SHA1 Message Date
Hugh Nimmo-Smith 01c2efc7ed Merge branch 'livekit' into hughns/to-device-key-distribution 2024-10-30 10:11:00 +00:00
Hugh Nimmo-Smith 8f73f81bc7 Request widget permission to send encryption key to device messages 2024-09-11 16:26:10 +01:00
Hugh Nimmo-Smith cb89bd062c Merge branch 'livekit' into hughns/do-device-key-distribution 2024-09-11 16:12:17 +01:00
Hugh Nimmo-Smith c1161ed047 Bump js-sdk to fix embedded mode 2024-09-11 16:11:15 +01:00
Hugh Nimmo-Smith 55ea373cd0 Intercept matrix_sdk logging via console 2024-09-11 12:20:30 +01:00
Hugh Nimmo-Smith 8e38d66611 Merge branch 'livekit' into hughns/to-device-key-distribution 2024-09-11 10:18:49 +01:00
Hugh Nimmo-Smith 0c762fd510 Rageshake logging improvements
Capture MatrixRTC related prefixed/child loggers from js-sdk.
Add explicit prefix to livekit log entries.
2024-09-11 10:08:35 +01:00
Hugh Nimmo-Smith 4cfdd15c03 Give user feedback if rageshake submission fails
Otherwise it swallows errors like 50x
2024-09-11 09:36:57 +01:00
Hugh Nimmo-Smith 102399ad7d Include the hostname of where EC is running in rageshakes (#2616) 2024-09-09 11:56:44 +01:00
Hugh Nimmo-Smith e502cfb7ce Bump js-sdk 2024-09-09 11:11:28 +01:00
Hugh Nimmo-Smith 20e3f6708a Bump js-sdk 2024-09-06 17:11:10 +01:00
Hugh Nimmo-Smith a5ac5b2808 Merge branch 'livekit' into hughns/to-device-key-distribution 2024-09-06 10:08:44 +01:00
Hugh Nimmo-Smith d219f32111 Bump js-sdk 2024-09-04 18:33:40 +01:00
Hugh Nimmo-Smith cd2937cd8a Bump js-sdk 2024-09-04 18:16:42 +01:00
Hugh Nimmo-Smith 0a3dae0525 Upgrade js-sdk to get forward secrecy 2024-09-04 18:01:48 +01:00
Hugh Nimmo-Smith 7e1fbdb7bd Bump js-sdk 2024-09-04 17:32:55 +01:00
Hugh Nimmo-Smith 2afe179bb5 Merge branch 'livekit' into hughns/do-device-key-distribution 2024-09-04 17:21:28 +01:00
Hugh Nimmo-Smith 38384c22e6 Bump js-sdk 2024-09-04 16:56:58 +01:00
Hugh Nimmo-Smith c5f50e069d Update yarn.lock 2024-09-03 10:25:51 +01:00
Hugh Nimmo-Smith efcabe9014 Merge branch 'hughns/do-device-key-distribution' of https://github.com/element-hq/element-call into hughns/to-device-key-distribution 2024-09-03 10:23:31 +01:00
Hugh Nimmo-Smith 79d09e18ed Use branch of js-sdk 2024-09-03 10:23:22 +01:00
Hugh Nimmo-Smith a33b40ea02 Allow for null MatrixClient 2024-09-03 10:22:17 +01:00
Hugh Nimmo-Smith 9987a45475 Prototyping for to-device key distribution
Show participant ID and some encryption status message
Allow encryption system to be chosen at point of room creation
Send cryptoVersion platform data to Posthog
Send key distribution stats to posthog
Send encryption type for CallStarted and CallEnded events
Update js-sdk
2024-09-03 10:22:17 +01:00
Hugh Nimmo-Smith d060f85939 Use branch of js-sdk 2024-09-03 10:20:53 +01:00
Hugh Nimmo-Smith 4c68122bcd Allow for null MatrixClient 2024-09-02 16:48:57 +01:00
Hugh Nimmo-Smith 497b38b609 Prototyping for to-device key distribution
Show participant ID and some encryption status message
Allow encryption system to be chosen at point of room creation
Send cryptoVersion platform data to Posthog
Send key distribution stats to posthog
Send encryption type for CallStarted and CallEnded events
Update js-sdk
2024-09-02 16:48:57 +01:00
16 changed files with 317 additions and 70 deletions
+1 -1
View File
@@ -84,7 +84,7 @@
"livekit-client": "^2.5.7",
"lodash": "^4.17.21",
"loglevel": "^1.9.1",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#0a29063bc9e61ee70ca43820d4bb91f6a27f1237",
"matrix-js-sdk": "matrix-org/matrix-js-sdk#b0174eccdb0e33f5df5d7b590938daf8ff5c7f7a",
"matrix-widget-api": "^1.8.2",
"normalize.css": "^8.0.1",
"observable-hooks": "^4.2.3",
+33 -1
View File
@@ -14,13 +14,30 @@ import {
PosthogAnalytics,
RegistrationType,
} from "./PosthogAnalytics";
import { E2eeType } from "../e2ee/e2eeType";
type EncryptionScheme = "none" | "shared" | "per_sender";
function mapE2eeType(type: E2eeType): EncryptionScheme {
switch (type) {
case E2eeType.NONE:
return "none";
case E2eeType.SHARED_KEY:
return "shared";
case E2eeType.PER_PARTICIPANT:
return "per_sender";
}
}
interface CallEnded extends IPosthogEvent {
eventName: "CallEnded";
callId: string;
callParticipantsOnLeave: number;
callParticipantsMax: number;
callDuration: number;
encryption: EncryptionScheme;
toDeviceEncryptionKeysSent: number;
toDeviceEncryptionKeysReceived: number;
toDeviceEncryptionKeysReceivedAverageAge: number;
roomEventEncryptionKeysSent: number;
roomEventEncryptionKeysReceived: number;
roomEventEncryptionKeysReceivedAverageAge: number;
@@ -46,6 +63,8 @@ export class CallEndedTracker {
public track(
callId: string,
callParticipantsNow: number,
e2eeType: E2eeType,
rtcSession: MatrixRTCSession,
sendInstantly: boolean,
rtcSession: MatrixRTCSession,
): void {
@@ -56,6 +75,17 @@ export class CallEndedTracker {
callParticipantsMax: this.cache.maxParticipantsCount,
callParticipantsOnLeave: callParticipantsNow,
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
encryption: mapE2eeType(e2eeType),
toDeviceEncryptionKeysSent:
rtcSession.statistics.counters.toDeviceEncryptionKeysSent,
toDeviceEncryptionKeysReceived:
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived,
toDeviceEncryptionKeysReceivedAverageAge:
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived > 0
? rtcSession.statistics.totals
.toDeviceEncryptionKeysReceivedTotalAge /
rtcSession.statistics.counters.toDeviceEncryptionKeysReceived
: 0,
roomEventEncryptionKeysSent:
rtcSession.statistics.counters.roomEventEncryptionKeysSent,
roomEventEncryptionKeysReceived:
@@ -75,13 +105,15 @@ export class CallEndedTracker {
interface CallStarted extends IPosthogEvent {
eventName: "CallStarted";
callId: string;
encryption: EncryptionScheme;
}
export class CallStartedTracker {
public track(callId: string): void {
public track(callId: string, e2eeType: E2eeType): void {
PosthogAnalytics.instance.trackEvent<CallStarted>({
eventName: "CallStarted",
callId: callId,
encryption: mapE2eeType(e2eeType),
});
}
}
+32 -13
View File
@@ -9,7 +9,7 @@ import { useState, useCallback, FormEvent, FormEventHandler, FC } from "react";
import { useHistory } from "react-router-dom";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { useTranslation } from "react-i18next";
import { Heading, Text } from "@vector-im/compound-web";
import { Dropdown, Heading, Text } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { Button } from "@vector-im/compound-web";
@@ -35,6 +35,17 @@ import { useOptInAnalytics } from "../settings/settings";
interface Props {
client: MatrixClient;
}
const encryptionOptions = {
shared: {
label: "Shared key",
e2eeType: E2eeType.SHARED_KEY,
},
sender: {
label: "Per-participant key",
e2eeType: E2eeType.PER_PARTICIPANT,
},
none: { label: "None", e2eeType: E2eeType.NONE },
};
export const RegisteredView: FC<Props> = ({ client }) => {
const [loading, setLoading] = useState(false);
@@ -49,6 +60,9 @@ export const RegisteredView: FC<Props> = ({ client }) => {
[setJoinExistingCallModalOpen],
);
const [encryption, setEncryption] =
useState<keyof typeof encryptionOptions>("shared");
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
(e: FormEvent) => {
e.preventDefault();
@@ -63,21 +77,13 @@ export const RegisteredView: FC<Props> = ({ client }) => {
setError(undefined);
setLoading(true);
const createRoomResult = await createRoom(
const { roomId, encryptionSystem } = await createRoom(
client,
roomName,
E2eeType.SHARED_KEY,
encryptionOptions[encryption].e2eeType,
);
if (!createRoomResult.password)
throw new Error("Failed to create room with shared secret");
history.push(
getRelativeRoomUrl(
createRoomResult.roomId,
{ kind: E2eeType.SHARED_KEY, secret: createRoomResult.password },
roomName,
),
);
history.push(getRelativeRoomUrl(roomId, encryptionSystem, roomName));
}
submit().catch((error) => {
@@ -93,7 +99,7 @@ export const RegisteredView: FC<Props> = ({ client }) => {
}
});
},
[client, history, setJoinExistingCallModalOpen],
[client, history, setJoinExistingCallModalOpen, encryption],
);
const recentRooms = useGroupCallRooms(client);
@@ -132,6 +138,19 @@ export const RegisteredView: FC<Props> = ({ client }) => {
data-testid="home_callName"
/>
<Dropdown
label="Encryption"
defaultValue={encryption}
onValueChange={(x) =>
setEncryption(x as keyof typeof encryptionOptions)
}
values={Object.keys(encryptionOptions).map((value) => [
value,
encryptionOptions[value as keyof typeof encryptionOptions]
.label,
])}
placeholder=""
/>
<Button
type="submit"
size="lg"
+40 -13
View File
@@ -9,7 +9,7 @@ import { FC, useCallback, useState, FormEventHandler } from "react";
import { useHistory } from "react-router-dom";
import { randomString } from "matrix-js-sdk/src/randomstring";
import { Trans, useTranslation } from "react-i18next";
import { Button, Heading, Text } from "@vector-im/compound-web";
import { Button, Dropdown, Heading, Text } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { useClient } from "../ClientContext";
@@ -35,6 +35,18 @@ import { E2eeType } from "../e2ee/e2eeType";
import { useOptInAnalytics } from "../settings/settings";
import { ExternalLink, Link } from "../button/Link";
const encryptionOptions = {
shared: {
label: "Shared key",
e2eeType: E2eeType.SHARED_KEY,
},
sender: {
label: "Per-participant key",
e2eeType: E2eeType.PER_PARTICIPANT,
},
none: { label: "None", e2eeType: E2eeType.NONE },
};
export const UnauthenticatedView: FC = () => {
const { setClient } = useClient();
const [loading, setLoading] = useState(false);
@@ -43,6 +55,9 @@ export const UnauthenticatedView: FC = () => {
const { recaptchaKey, register } = useInteractiveRegistration();
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
const [encryption, setEncryption] =
useState<keyof typeof encryptionOptions>("shared");
const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
useState(false);
const onDismissJoinExistingCallModal = useCallback(
@@ -73,13 +88,16 @@ export const UnauthenticatedView: FC = () => {
true,
);
let createRoomResult;
let roomId;
let encryptionSystem;
try {
createRoomResult = await createRoom(
const res = await createRoom(
client,
roomName,
E2eeType.SHARED_KEY,
encryptionOptions[encryption].e2eeType,
);
roomId = res.roomId;
encryptionSystem = res.encryptionSystem;
} catch (error) {
if (!setClient) {
throw error;
@@ -106,17 +124,11 @@ export const UnauthenticatedView: FC = () => {
if (!setClient) {
throw new Error("setClient is undefined");
}
if (!createRoomResult.password)
throw new Error("Failed to create room with shared secret");
// if (!createRoomResult.password)
// throw new Error("Failed to create room with shared secret");
setClient({ client, session });
history.push(
getRelativeRoomUrl(
createRoomResult.roomId,
{ kind: E2eeType.SHARED_KEY, secret: createRoomResult.password },
roomName,
),
);
history.push(getRelativeRoomUrl(roomId, encryptionSystem, roomName));
}
submit().catch((error) => {
@@ -133,6 +145,7 @@ export const UnauthenticatedView: FC = () => {
history,
setJoinExistingCallModalOpen,
setClient,
encryption,
],
);
@@ -195,6 +208,20 @@ export const UnauthenticatedView: FC = () => {
<ErrorMessage error={error} />
</FieldRow>
)}
<Dropdown
label="Encryption"
defaultValue={encryption}
onValueChange={(x) =>
setEncryption(x as keyof typeof encryptionOptions)
}
values={Object.keys(encryptionOptions).map((value) => [
value,
encryptionOptions[value as keyof typeof encryptionOptions]
.label,
])}
placeholder=""
/>
<Button
type="submit"
size="lg"
+10 -8
View File
@@ -97,7 +97,7 @@ export const GroupCallView: FC<Props> = ({
const { displayName, avatarUrl } = useProfile(client);
const roomName = useRoomName(rtcSession.room);
const roomAvatar = useRoomAvatar(rtcSession.room);
const { perParticipantE2EE, returnToLobby } = useUrlParams();
const { returnToLobby } = useUrlParams();
const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
const matrixInfo = useMemo((): MatrixInfo => {
@@ -182,7 +182,7 @@ export const GroupCallView: FC<Props> = ({
const onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
(async (): Promise<void> => {
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
await enterRTCSession(rtcSession, perParticipantE2EE);
await enterRTCSession(rtcSession, e2eeSystem.kind);
widget!.api.transport.reply(ev.detail, {});
})().catch((e) => {
logger.error("Error joining RTC session", e);
@@ -196,12 +196,12 @@ export const GroupCallView: FC<Props> = ({
// No lobby and no preload: we enter the rtc session right away
(async (): Promise<void> => {
await defaultDeviceSetup({ audioInput: null, videoInput: null });
await enterRTCSession(rtcSession, perParticipantE2EE);
await enterRTCSession(rtcSession, e2eeSystem.kind);
})().catch((e) => {
logger.error("Error joining RTC session", e);
});
}
}, [rtcSession, preload, skipLobby, perParticipantE2EE]);
}, [rtcSession, preload, skipLobby, e2eeSystem]);
const [left, setLeft] = useState(false);
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
@@ -218,6 +218,8 @@ export const GroupCallView: FC<Props> = ({
PosthogAnalytics.instance.eventCallEnded.track(
rtcSession.room.roomId,
rtcSession.memberships.length,
matrixInfo.e2eeSystem.kind,
rtcSession,
sendInstantly,
rtcSession,
);
@@ -237,7 +239,7 @@ export const GroupCallView: FC<Props> = ({
logger.error("Error leaving RTC session", e);
});
},
[rtcSession, isPasswordlessUser, confineToRoom, history],
[rtcSession, isPasswordlessUser, confineToRoom, history, matrixInfo],
);
useEffect(() => {
@@ -264,10 +266,10 @@ export const GroupCallView: FC<Props> = ({
const onReconnect = useCallback(() => {
setLeft(false);
setLeaveError(undefined);
enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => {
enterRTCSession(rtcSession, e2eeSystem.kind).catch((e) => {
logger.error("Error re-entering RTC session on reconnect", e);
});
}, [rtcSession, perParticipantE2EE]);
}, [rtcSession, e2eeSystem]);
const joinRule = useJoinRule(rtcSession.room);
@@ -310,7 +312,7 @@ export const GroupCallView: FC<Props> = ({
client={client}
matrixInfo={matrixInfo}
muteStates={muteStates}
onEnter={() => void enterRTCSession(rtcSession, perParticipantE2EE)}
onEnter={() => void enterRTCSession(rtcSession, e2eeSystem.kind)}
confineToRoom={confineToRoom}
hideHeader={hideHeader}
participantCount={participantCount}
+2 -1
View File
@@ -10,6 +10,7 @@ import { expect, test, vi } from "vitest";
import { enterRTCSession } from "../src/rtcSessionHelpers";
import { Config } from "../src/config/Config";
import { E2eeType } from "../src/e2ee/e2eeType";
import { DEFAULT_CONFIG } from "./config/ConfigOptions";
test("It joins the correct Session", async () => {
@@ -52,7 +53,7 @@ test("It joins the correct Session", async () => {
}),
joinRoomSession: vi.fn(),
}) as unknown as MatrixRTCSession;
await enterRTCSession(mockedSession, false);
await enterRTCSession(mockedSession, E2eeType.NONE);
expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith(
[
+7 -3
View File
@@ -17,6 +17,7 @@ import {
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
import { Config } from "./config/Config";
import { ElementWidgetActions, WidgetHelpers, widget } from "./widget";
import { E2eeType } from "./e2ee/e2eeType";
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
@@ -87,10 +88,13 @@ async function makePreferredLivekitFoci(
export async function enterRTCSession(
rtcSession: MatrixRTCSession,
encryptMedia: boolean,
e2eeType: E2eeType,
): Promise<void> {
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
PosthogAnalytics.instance.eventCallStarted.track(rtcSession.room.roomId);
PosthogAnalytics.instance.eventCallStarted.track(
rtcSession.room.roomId,
e2eeType,
);
// This must be called before we start trying to join the call, as we need to
// have started tracking by the time calls start getting created.
@@ -104,7 +108,7 @@ export async function enterRTCSession(
await makePreferredLivekitFoci(rtcSession, livekitAlias),
makeActiveFocus(),
{
manageMediaKeys: encryptMedia,
manageMediaKeys: e2eeType === E2eeType.PER_PARTICIPANT,
...(useDeviceSessionMemberEvents !== undefined && {
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
}),
+25 -3
View File
@@ -168,6 +168,7 @@ class UserMedia {
member: RoomMember | undefined,
participant: LocalParticipant | RemoteParticipant,
callEncrypted: boolean,
livekitRoom: LivekitRoom,
) {
this.vm = participant.isLocal
? new LocalUserMediaViewModel(
@@ -175,12 +176,14 @@ class UserMedia {
member,
participant as LocalParticipant,
callEncrypted,
livekitRoom,
)
: new RemoteUserMediaViewModel(
id,
member,
participant as RemoteParticipant,
callEncrypted,
livekitRoom,
);
this.speaker = this.vm.speaking.pipe(
@@ -224,8 +227,15 @@ class ScreenShare {
member: RoomMember | undefined,
participant: LocalParticipant | RemoteParticipant,
callEncrypted: boolean,
liveKitRoom: LivekitRoom,
) {
this.vm = new ScreenShareViewModel(id, member, participant, callEncrypted);
this.vm = new ScreenShareViewModel(
id,
member,
participant,
callEncrypted,
liveKitRoom,
);
}
public destroy(): void {
@@ -349,7 +359,13 @@ export class CallViewModel extends ViewModel {
yield [
userMediaId,
prevItems.get(userMediaId) ??
new UserMedia(userMediaId, member, p, this.encrypted),
new UserMedia(
userMediaId,
member,
p,
this.encrypted,
this.livekitRoom,
),
];
if (p.isScreenShareEnabled) {
@@ -357,7 +373,13 @@ export class CallViewModel extends ViewModel {
yield [
screenShareId,
prevItems.get(screenShareId) ??
new ScreenShare(screenShareId, member, p, this.encrypted),
new ScreenShare(
screenShareId,
member,
p,
this.encrypted,
this.livekitRoom,
),
];
}
}
+61 -4
View File
@@ -11,6 +11,7 @@ import {
VideoSource,
observeParticipantEvents,
observeParticipantMedia,
roomEventSelector,
} from "@livekit/components-core";
import {
LocalParticipant,
@@ -21,6 +22,8 @@ import {
Track,
TrackEvent,
facingModeFromLocalTrack,
Room as LivekitRoom,
RoomEvent as LivekitRoomEvent,
} from "livekit-client";
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
import {
@@ -28,13 +31,17 @@ import {
Observable,
Subject,
combineLatest,
debounceTime,
distinctUntilChanged,
distinctUntilKeyChanged,
fromEvent,
map,
merge,
of,
shareReplay,
startWith,
switchMap,
throttleTime,
} from "rxjs";
import { useEffect } from "react";
@@ -79,6 +86,35 @@ function observeTrackReference(
);
}
function encryptionErrorObservable(
room: LivekitRoom,
participant: Participant,
criteria: string,
): Observable<boolean> {
const roomEvents = roomEventSelector(
room,
LivekitRoomEvent.EncryptionError,
).pipe(
map((e) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [err, participantIdentity] = e as any; // FIXME: fudge for type error during build
return (
(participantIdentity == participant.identity &&
err?.message.includes(criteria)) ??
false
);
}),
throttleTime(1000), // Throttle to avoid spamming the UI
);
return merge(
roomEvents,
roomEvents.pipe(
debounceTime(3000), // Wait 3 seconds before clearing the error, toast style
map(() => false),
),
);
}
abstract class BaseMediaViewModel extends ViewModel {
/**
* Whether the media belongs to the local user.
@@ -93,6 +129,10 @@ abstract class BaseMediaViewModel extends ViewModel {
*/
public readonly unencryptedWarning: Observable<boolean>;
public readonly encryptionKeyMissing: Observable<boolean>;
public readonly encryptionKeyInvalid: Observable<boolean>;
public constructor(
/**
* An opaque identifier for this media.
@@ -104,10 +144,11 @@ abstract class BaseMediaViewModel extends ViewModel {
// TODO: Fully separate the data layer from the UI layer by keeping the
// member object internal
public readonly member: RoomMember | undefined,
protected readonly participant: LocalParticipant | RemoteParticipant,
public readonly participant: LocalParticipant | RemoteParticipant,
callEncrypted: boolean,
audioSource: AudioSource,
videoSource: VideoSource,
livekitRoom: LivekitRoom,
) {
super();
const audio = observeTrackReference(participant, audioSource).pipe(
@@ -122,7 +163,17 @@ abstract class BaseMediaViewModel extends ViewModel {
callEncrypted &&
(a.publication?.isEncrypted === false ||
v.publication?.isEncrypted === false),
).pipe(this.scope.state());
).pipe(distinctUntilChanged(), shareReplay(1));
this.encryptionKeyMissing = encryptionErrorObservable(
livekitRoom,
participant,
"MissingKey",
).pipe(startWith(false));
this.encryptionKeyInvalid = encryptionErrorObservable(
livekitRoom,
participant,
"InvalidKey",
).pipe(startWith(false));
}
}
@@ -169,6 +220,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
member: RoomMember | undefined,
participant: LocalParticipant | RemoteParticipant,
callEncrypted: boolean,
livekitRoom: LivekitRoom,
) {
super(
id,
@@ -177,6 +229,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
callEncrypted,
Track.Source.Microphone,
Track.Source.Camera,
livekitRoom,
);
const media = observeParticipantMedia(participant).pipe(this.scope.state());
@@ -226,8 +279,9 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
member: RoomMember | undefined,
participant: LocalParticipant,
callEncrypted: boolean,
livekitRoom: LivekitRoom,
) {
super(id, member, participant, callEncrypted);
super(id, member, participant, callEncrypted, livekitRoom);
}
}
@@ -286,8 +340,9 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
member: RoomMember | undefined,
participant: RemoteParticipant,
callEncrypted: boolean,
livekitRoom: LivekitRoom,
) {
super(id, member, participant, callEncrypted);
super(id, member, participant, callEncrypted, livekitRoom);
// Sync the local volume with LiveKit
this.localVolume
@@ -319,6 +374,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
member: RoomMember | undefined,
participant: LocalParticipant | RemoteParticipant,
callEncrypted: boolean,
livekitRoom: LivekitRoom,
) {
super(
id,
@@ -327,6 +383,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
callEncrypted,
Track.Source.ScreenShareAudio,
Track.Source.ScreenShare,
livekitRoom,
);
}
}
+12
View File
@@ -10,6 +10,7 @@ import {
ReactNode,
forwardRef,
useCallback,
useMemo,
useState,
} from "react";
import { animated } from "@react-spring/web";
@@ -79,6 +80,12 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
const { t } = useTranslation();
const video = useObservableEagerState(vm.video);
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
const encryptionKeyMissing = useObservableEagerState(
vm.encryptionKeyMissing,
);
const encryptionKeyInvalid = useObservableEagerState(
vm.encryptionKeyInvalid,
);
const audioEnabled = useObservableEagerState(vm.audioEnabled);
const videoEnabled = useObservableEagerState(vm.videoEnabled);
const speaking = useObservableEagerState(vm.speaking);
@@ -91,6 +98,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
[vm],
);
const participantId = useMemo(() => vm.participant.identity, [vm]);
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
const [menuOpen, setMenuOpen] = useState(false);
@@ -113,6 +122,9 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
video={video}
member={vm.member}
unencryptedWarning={unencryptedWarning}
encryptionKeyMissing={encryptionKeyMissing}
encryptionKeyInvalid={encryptionKeyInvalid}
participantId={participantId}
videoEnabled={videoEnabled && showVideo}
videoFit={cropVideo ? "cover" : "contain"}
className={classNames(className, styles.tile, {
+19 -1
View File
@@ -85,7 +85,7 @@ unconditionally select the container so we can use cqmin units */
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr auto;
grid-template-areas: ". ." "nameTag button";
grid-template-areas: "status status" "nameTag button";
gap: var(--cpd-space-1x);
place-items: start;
}
@@ -106,6 +106,24 @@ unconditionally select the container so we can use cqmin units */
max-inline-size: 100%;
}
.status {
grid-area: status;
justify-self: center;
align-self: start;
padding: var(--cpd-space-1x);
padding-block: var(--cpd-space-1x);
color: var(--cpd-color-text-primary);
background-color: var(--cpd-color-bg-canvas-default);
display: flex;
align-items: center;
border-radius: var(--cpd-radius-pill-effect);
user-select: none;
overflow: hidden;
box-shadow: var(--small-drop-shadow);
box-sizing: border-box;
max-inline-size: 100%;
}
.nameTag > svg,
.nameTag > span {
flex-shrink: 0;
+24 -3
View File
@@ -29,8 +29,11 @@ interface Props extends ComponentProps<typeof animated.div> {
member: RoomMember | undefined;
videoEnabled: boolean;
unencryptedWarning: boolean;
encryptionKeyMissing: boolean;
encryptionKeyInvalid: boolean;
nameTagLeadingIcon?: ReactNode;
displayName: string;
participantId: string;
primaryButton?: ReactNode;
}
@@ -50,6 +53,9 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
nameTagLeadingIcon,
displayName,
primaryButton,
encryptionKeyMissing,
encryptionKeyInvalid,
participantId,
...props
},
ref,
@@ -60,7 +66,8 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
<animated.div
className={classNames(styles.media, className, {
[styles.mirror]: mirror,
[styles.videoMuted]: !videoEnabled,
[styles.videoMuted]:
!videoEnabled || encryptionKeyInvalid || encryptionKeyMissing,
})}
style={style}
ref={ref}
@@ -86,10 +93,24 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
)}
</div>
<div className={styles.fg}>
<div className={styles.nameTag}>
{encryptionKeyMissing && (
<div className={styles.status}>
<Text as="span" size="sm" weight="medium" className={styles.name}>
Encryption key missing
</Text>
</div>
)}
{encryptionKeyInvalid && (
<div className={styles.status}>
<Text as="span" size="sm" weight="medium" className={styles.name}>
Encryption key invalid
</Text>
</div>
)}
<div className={styles.nameTag} title={participantId}>
{nameTagLeadingIcon}
<Text as="span" size="sm" weight="medium" className={styles.name}>
{displayName}
{displayName} ({participantId})
</Text>
{unencryptedWarning && (
<Tooltip
+18
View File
@@ -11,6 +11,7 @@ import {
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
@@ -51,7 +52,10 @@ interface SpotlightItemBaseProps {
video: TrackReferenceOrPlaceholder;
member: RoomMember | undefined;
unencryptedWarning: boolean;
encryptionKeyMissing: boolean;
encryptionKeyInvalid: boolean;
displayName: string;
participantId: string;
"aria-hidden"?: boolean;
}
@@ -130,6 +134,17 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
const displayName = useDisplayName(vm);
const video = useObservableEagerState(vm.video);
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
const encryptionKeyMissing = useObservableEagerState(
vm.encryptionKeyMissing,
);
const encryptionKeyInvalid = useObservableEagerState(
vm.encryptionKeyInvalid,
);
const participantId = useMemo(
() => vm.participant.identity,
[vm.participant],
);
// Hook this item up to the intersection observer
useEffect(() => {
@@ -156,6 +171,9 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
member: vm.member,
unencryptedWarning,
displayName,
encryptionKeyMissing,
encryptionKeyInvalid,
participantId,
"aria-hidden": ariaHidden,
};
+26 -5
View File
@@ -9,6 +9,7 @@ import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
import { MemoryStore } from "matrix-js-sdk/src/store/memory";
import {
createClient,
EventType,
ICreateClientOpts,
Preset,
Visibility,
@@ -210,7 +211,7 @@ export function sanitiseRoomNameInput(input: string): string {
interface CreateRoomResult {
roomId: string;
alias?: string;
password?: string;
encryptionSystem: EncryptionSystem;
}
/**
@@ -257,6 +258,18 @@ export async function createRoom(
[client.getUserId()!]: 100,
},
},
initial_state:
e2ee === E2eeType.PER_PARTICIPANT
? [
{
type: EventType.RoomEncryption,
state_key: "",
content: {
algorithm: "m.megolm.v1.aes-sha2",
},
},
]
: undefined,
});
// Wait for the room to arrive
@@ -286,16 +299,24 @@ export async function createRoom(
client.on(ClientEvent.Room, onRoom);
});
let password: string | undefined;
const result = await createPromise;
let encryptionSystem: EncryptionSystem;
if (e2ee == E2eeType.SHARED_KEY) {
password = secureRandomBase64Url(16);
saveKeyForRoom(roomId, password);
const password = secureRandomBase64Url(16);
saveKeyForRoom(result.room_id, password);
encryptionSystem = {
kind: E2eeType.SHARED_KEY,
secret: password,
};
} else {
encryptionSystem = { kind: e2ee };
}
return {
roomId,
alias: e2ee ? undefined : fullAliasFromRoomName(name, client),
password,
encryptionSystem,
};
}
+1
View File
@@ -130,6 +130,7 @@ export const widget = ((): WidgetHelpers | null => {
EventType.CallSDPStreamMetadataChanged,
EventType.CallSDPStreamMetadataChangedPrefix,
EventType.CallReplaces,
EventType.CallEncryptionKeysPrefix,
];
const client = createRoomWidgetClient(
+6 -14
View File
@@ -5864,12 +5864,12 @@ matrix-events-sdk@0.0.1:
resolved "https://registry.yarnpkg.com/matrix-events-sdk/-/matrix-events-sdk-0.0.1.tgz#c8c38911e2cb29023b0bbac8d6f32e0de2c957dd"
integrity sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==
matrix-js-sdk@matrix-org/matrix-js-sdk#0a29063bc9e61ee70ca43820d4bb91f6a27f1237:
version "34.9.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/0a29063bc9e61ee70ca43820d4bb91f6a27f1237"
matrix-js-sdk@matrix-org/matrix-js-sdk#b0174eccdb0e33f5df5d7b590938daf8ff5c7f7a:
version "34.4.0"
resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/b0174eccdb0e33f5df5d7b590938daf8ff5c7f7a"
dependencies:
"@babel/runtime" "^7.12.5"
"@matrix-org/matrix-sdk-crypto-wasm" "^9.0.0"
"@matrix-org/matrix-sdk-crypto-wasm" "https://floofy.netlify.app/matrix-org-matrix-sdk-crypto-wasm-v7.0.0-to-device.tgz"
"@matrix-org/olm" "3.2.15"
another-json "^0.2.0"
bs58 "^6.0.0"
@@ -7392,16 +7392,8 @@ streamx@^2.12.0, streamx@^2.12.5, streamx@^2.13.2, streamx@^2.14.0:
optionalDependencies:
bare-events "^2.2.0"
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0:
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0:
name string-width-cjs
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==