mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-16 23:45:50 +00:00
Compare commits
26
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01c2efc7ed | ||
|
|
8f73f81bc7 | ||
|
|
cb89bd062c | ||
|
|
c1161ed047 | ||
|
|
55ea373cd0 | ||
|
|
8e38d66611 | ||
|
|
0c762fd510 | ||
|
|
4cfdd15c03 | ||
|
|
102399ad7d | ||
|
|
e502cfb7ce | ||
|
|
20e3f6708a | ||
|
|
a5ac5b2808 | ||
|
|
d219f32111 | ||
|
|
cd2937cd8a | ||
|
|
0a3dae0525 | ||
|
|
7e1fbdb7bd | ||
|
|
2afe179bb5 | ||
|
|
38384c22e6 | ||
|
|
c5f50e069d | ||
|
|
efcabe9014 | ||
|
|
79d09e18ed | ||
|
|
a33b40ea02 | ||
|
|
9987a45475 | ||
|
|
d060f85939 | ||
|
|
4c68122bcd | ||
|
|
497b38b609 |
+1
-1
@@ -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",
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(
|
||||
[
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ export const widget = ((): WidgetHelpers | null => {
|
||||
EventType.CallSDPStreamMetadataChanged,
|
||||
EventType.CallSDPStreamMetadataChangedPrefix,
|
||||
EventType.CallReplaces,
|
||||
EventType.CallEncryptionKeysPrefix,
|
||||
];
|
||||
|
||||
const client = createRoomWidgetClient(
|
||||
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user