mirror of
https://github.com/vector-im/element-call.git
synced 2024-11-30 00:50:48 +08:00
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
This commit is contained in:
parent
55038065c7
commit
9987a45475
@ -73,6 +73,7 @@ interface PlatformProperties {
|
|||||||
appVersion: string;
|
appVersion: string;
|
||||||
matrixBackend: "embedded" | "jssdk";
|
matrixBackend: "embedded" | "jssdk";
|
||||||
callBackend: "livekit" | "full-mesh";
|
callBackend: "livekit" | "full-mesh";
|
||||||
|
cryptoVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PosthogSettings {
|
interface PosthogSettings {
|
||||||
@ -193,6 +194,9 @@ export class PosthogAnalytics {
|
|||||||
appVersion,
|
appVersion,
|
||||||
matrixBackend: widget ? "embedded" : "jssdk",
|
matrixBackend: widget ? "embedded" : "jssdk",
|
||||||
callBackend: "livekit",
|
callBackend: "livekit",
|
||||||
|
cryptoVersion: widget
|
||||||
|
? undefined
|
||||||
|
: window.matrixclient.getCrypto()?.getVersion(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,19 +16,40 @@ limitations under the License.
|
|||||||
|
|
||||||
import { DisconnectReason } from "livekit-client";
|
import { DisconnectReason } from "livekit-client";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IPosthogEvent,
|
IPosthogEvent,
|
||||||
PosthogAnalytics,
|
PosthogAnalytics,
|
||||||
RegistrationType,
|
RegistrationType,
|
||||||
} from "./PosthogAnalytics";
|
} 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 {
|
interface CallEnded extends IPosthogEvent {
|
||||||
eventName: "CallEnded";
|
eventName: "CallEnded";
|
||||||
callId: string;
|
callId: string;
|
||||||
callParticipantsOnLeave: number;
|
callParticipantsOnLeave: number;
|
||||||
callParticipantsMax: number;
|
callParticipantsMax: number;
|
||||||
callDuration: number;
|
callDuration: number;
|
||||||
|
encryption: EncryptionScheme;
|
||||||
|
toDeviceEncryptionKeysSent: number;
|
||||||
|
toDeviceEncryptionKeysReceived: number;
|
||||||
|
toDeviceEncryptionKeysReceivedAverageAge: number;
|
||||||
|
roomEventEncryptionKeysSent: number;
|
||||||
|
roomEventEncryptionKeysReceived: number;
|
||||||
|
roomEventEncryptionKeysReceivedAverageAge: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CallEndedTracker {
|
export class CallEndedTracker {
|
||||||
@ -51,6 +72,8 @@ export class CallEndedTracker {
|
|||||||
public track(
|
public track(
|
||||||
callId: string,
|
callId: string,
|
||||||
callParticipantsNow: number,
|
callParticipantsNow: number,
|
||||||
|
e2eeType: E2eeType,
|
||||||
|
rtcSession: MatrixRTCSession,
|
||||||
sendInstantly: boolean,
|
sendInstantly: boolean,
|
||||||
): void {
|
): void {
|
||||||
PosthogAnalytics.instance.trackEvent<CallEnded>(
|
PosthogAnalytics.instance.trackEvent<CallEnded>(
|
||||||
@ -60,6 +83,27 @@ export class CallEndedTracker {
|
|||||||
callParticipantsMax: this.cache.maxParticipantsCount,
|
callParticipantsMax: this.cache.maxParticipantsCount,
|
||||||
callParticipantsOnLeave: callParticipantsNow,
|
callParticipantsOnLeave: callParticipantsNow,
|
||||||
callDuration: (Date.now() - this.cache.startTime.getTime()) / 1000,
|
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:
|
||||||
|
rtcSession.statistics.counters.roomEventEncryptionKeysReceived,
|
||||||
|
roomEventEncryptionKeysReceivedAverageAge:
|
||||||
|
rtcSession.statistics.counters.roomEventEncryptionKeysReceived > 0
|
||||||
|
? rtcSession.statistics.totals
|
||||||
|
.roomEventEncryptionKeysReceivedTotalAge /
|
||||||
|
rtcSession.statistics.counters.roomEventEncryptionKeysReceived
|
||||||
|
: 0,
|
||||||
},
|
},
|
||||||
{ send_instantly: sendInstantly },
|
{ send_instantly: sendInstantly },
|
||||||
);
|
);
|
||||||
@ -69,13 +113,15 @@ export class CallEndedTracker {
|
|||||||
interface CallStarted extends IPosthogEvent {
|
interface CallStarted extends IPosthogEvent {
|
||||||
eventName: "CallStarted";
|
eventName: "CallStarted";
|
||||||
callId: string;
|
callId: string;
|
||||||
|
encryption: EncryptionScheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CallStartedTracker {
|
export class CallStartedTracker {
|
||||||
public track(callId: string): void {
|
public track(callId: string, e2eeType: E2eeType): void {
|
||||||
PosthogAnalytics.instance.trackEvent<CallStarted>({
|
PosthogAnalytics.instance.trackEvent<CallStarted>({
|
||||||
eventName: "CallStarted",
|
eventName: "CallStarted",
|
||||||
callId: callId,
|
callId: callId,
|
||||||
|
encryption: mapE2eeType(e2eeType),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import { useState, useCallback, FormEvent, FormEventHandler, FC } from "react";
|
|||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Heading } from "@vector-im/compound-web";
|
import { Dropdown, Heading } from "@vector-im/compound-web";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { Button } from "@vector-im/compound-web";
|
import { Button } from "@vector-im/compound-web";
|
||||||
|
|
||||||
@ -45,6 +45,17 @@ import { useOptInAnalytics } from "../settings/settings";
|
|||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
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 }) => {
|
export const RegisteredView: FC<Props> = ({ client }) => {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -59,6 +70,9 @@ export const RegisteredView: FC<Props> = ({ client }) => {
|
|||||||
[setJoinExistingCallModalOpen],
|
[setJoinExistingCallModalOpen],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [encryption, setEncryption] =
|
||||||
|
useState<keyof typeof encryptionOptions>("shared");
|
||||||
|
|
||||||
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
const onSubmit: FormEventHandler<HTMLFormElement> = useCallback(
|
||||||
(e: FormEvent) => {
|
(e: FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@ -73,21 +87,13 @@ export const RegisteredView: FC<Props> = ({ client }) => {
|
|||||||
setError(undefined);
|
setError(undefined);
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const createRoomResult = await createRoom(
|
const { roomId, encryptionSystem } = await createRoom(
|
||||||
client,
|
client,
|
||||||
roomName,
|
roomName,
|
||||||
E2eeType.SHARED_KEY,
|
encryptionOptions[encryption].e2eeType,
|
||||||
);
|
);
|
||||||
if (!createRoomResult.password)
|
|
||||||
throw new Error("Failed to create room with shared secret");
|
|
||||||
|
|
||||||
history.push(
|
history.push(getRelativeRoomUrl(roomId, encryptionSystem, roomName));
|
||||||
getRelativeRoomUrl(
|
|
||||||
createRoomResult.roomId,
|
|
||||||
{ kind: E2eeType.SHARED_KEY, secret: createRoomResult.password },
|
|
||||||
roomName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
@ -103,7 +109,7 @@ export const RegisteredView: FC<Props> = ({ client }) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[client, history, setJoinExistingCallModalOpen],
|
[client, history, setJoinExistingCallModalOpen, encryption],
|
||||||
);
|
);
|
||||||
|
|
||||||
const recentRooms = useGroupCallRooms(client);
|
const recentRooms = useGroupCallRooms(client);
|
||||||
@ -142,6 +148,19 @@ export const RegisteredView: FC<Props> = ({ client }) => {
|
|||||||
data-testid="home_callName"
|
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
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
@ -18,7 +18,7 @@ import { FC, useCallback, useState, FormEventHandler } from "react";
|
|||||||
import { useHistory } from "react-router-dom";
|
import { useHistory } from "react-router-dom";
|
||||||
import { randomString } from "matrix-js-sdk/src/randomstring";
|
import { randomString } from "matrix-js-sdk/src/randomstring";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Button, Heading } from "@vector-im/compound-web";
|
import { Button, Dropdown, Heading } from "@vector-im/compound-web";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
import { useClient } from "../ClientContext";
|
import { useClient } from "../ClientContext";
|
||||||
@ -44,6 +44,18 @@ import { Config } from "../config/Config";
|
|||||||
import { E2eeType } from "../e2ee/e2eeType";
|
import { E2eeType } from "../e2ee/e2eeType";
|
||||||
import { useOptInAnalytics } from "../settings/settings";
|
import { useOptInAnalytics } from "../settings/settings";
|
||||||
|
|
||||||
|
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 = () => {
|
export const UnauthenticatedView: FC = () => {
|
||||||
const { setClient } = useClient();
|
const { setClient } = useClient();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@ -52,6 +64,9 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
const { recaptchaKey, register } = useInteractiveRegistration();
|
const { recaptchaKey, register } = useInteractiveRegistration();
|
||||||
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
const { execute, reset, recaptchaId } = useRecaptcha(recaptchaKey);
|
||||||
|
|
||||||
|
const [encryption, setEncryption] =
|
||||||
|
useState<keyof typeof encryptionOptions>("shared");
|
||||||
|
|
||||||
const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
|
const [joinExistingCallModalOpen, setJoinExistingCallModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const onDismissJoinExistingCallModal = useCallback(
|
const onDismissJoinExistingCallModal = useCallback(
|
||||||
@ -82,13 +97,16 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
let createRoomResult;
|
let roomId;
|
||||||
|
let encryptionSystem;
|
||||||
try {
|
try {
|
||||||
createRoomResult = await createRoom(
|
const res = await createRoom(
|
||||||
client,
|
client,
|
||||||
roomName,
|
roomName,
|
||||||
E2eeType.SHARED_KEY,
|
encryptionOptions[encryption].e2eeType,
|
||||||
);
|
);
|
||||||
|
roomId = res.roomId;
|
||||||
|
encryptionSystem = res.encryptionSystem;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!setClient) {
|
if (!setClient) {
|
||||||
throw error;
|
throw error;
|
||||||
@ -115,17 +133,11 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
if (!setClient) {
|
if (!setClient) {
|
||||||
throw new Error("setClient is undefined");
|
throw new Error("setClient is undefined");
|
||||||
}
|
}
|
||||||
if (!createRoomResult.password)
|
// if (!createRoomResult.password)
|
||||||
throw new Error("Failed to create room with shared secret");
|
// throw new Error("Failed to create room with shared secret");
|
||||||
|
|
||||||
setClient({ client, session });
|
setClient({ client, session });
|
||||||
history.push(
|
history.push(getRelativeRoomUrl(roomId, encryptionSystem, roomName));
|
||||||
getRelativeRoomUrl(
|
|
||||||
createRoomResult.roomId,
|
|
||||||
{ kind: E2eeType.SHARED_KEY, secret: createRoomResult.password },
|
|
||||||
roomName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
submit().catch((error) => {
|
submit().catch((error) => {
|
||||||
@ -142,6 +154,7 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
history,
|
history,
|
||||||
setJoinExistingCallModalOpen,
|
setJoinExistingCallModalOpen,
|
||||||
setClient,
|
setClient,
|
||||||
|
encryption,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -204,6 +217,20 @@ export const UnauthenticatedView: FC = () => {
|
|||||||
<ErrorMessage error={error} />
|
<ErrorMessage error={error} />
|
||||||
</FieldRow>
|
</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
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
@ -97,7 +97,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
const { displayName, avatarUrl } = useProfile(client);
|
const { displayName, avatarUrl } = useProfile(client);
|
||||||
const roomName = useRoomName(rtcSession.room);
|
const roomName = useRoomName(rtcSession.room);
|
||||||
const roomAvatar = useRoomAvatar(rtcSession.room);
|
const roomAvatar = useRoomAvatar(rtcSession.room);
|
||||||
const { perParticipantE2EE, returnToLobby } = useUrlParams();
|
const { returnToLobby } = useUrlParams();
|
||||||
const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
|
const e2eeSystem = useRoomEncryptionSystem(rtcSession.room.roomId);
|
||||||
|
|
||||||
const matrixInfo = useMemo((): MatrixInfo => {
|
const matrixInfo = useMemo((): MatrixInfo => {
|
||||||
@ -191,7 +191,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
ev: CustomEvent<IWidgetApiRequest>,
|
ev: CustomEvent<IWidgetApiRequest>,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
|
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
|
||||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
await enterRTCSession(rtcSession, e2eeSystem.kind);
|
||||||
await widget!.api.transport.reply(ev.detail, {});
|
await widget!.api.transport.reply(ev.detail, {});
|
||||||
};
|
};
|
||||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||||
@ -201,12 +201,12 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
} else if (widget && !preload && skipLobby) {
|
} else if (widget && !preload && skipLobby) {
|
||||||
const join = async (): Promise<void> => {
|
const join = async (): Promise<void> => {
|
||||||
await defaultDeviceSetup({ audioInput: null, videoInput: null });
|
await defaultDeviceSetup({ audioInput: null, videoInput: null });
|
||||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
await enterRTCSession(rtcSession, e2eeSystem.kind);
|
||||||
};
|
};
|
||||||
// No lobby and no preload: we enter the RTC Session right away.
|
// No lobby and no preload: we enter the RTC Session right away.
|
||||||
join();
|
join();
|
||||||
}
|
}
|
||||||
}, [rtcSession, preload, skipLobby, perParticipantE2EE]);
|
}, [rtcSession, preload, skipLobby, e2eeSystem]);
|
||||||
|
|
||||||
const [left, setLeft] = useState(false);
|
const [left, setLeft] = useState(false);
|
||||||
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
|
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
|
||||||
@ -223,6 +223,8 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
PosthogAnalytics.instance.eventCallEnded.track(
|
PosthogAnalytics.instance.eventCallEnded.track(
|
||||||
rtcSession.room.roomId,
|
rtcSession.room.roomId,
|
||||||
rtcSession.memberships.length,
|
rtcSession.memberships.length,
|
||||||
|
matrixInfo.e2eeSystem.kind,
|
||||||
|
rtcSession,
|
||||||
sendInstantly,
|
sendInstantly,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -237,7 +239,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
history.push("/");
|
history.push("/");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rtcSession, isPasswordlessUser, confineToRoom, history],
|
[rtcSession, isPasswordlessUser, confineToRoom, history, matrixInfo],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -262,8 +264,8 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
const onReconnect = useCallback(() => {
|
const onReconnect = useCallback(() => {
|
||||||
setLeft(false);
|
setLeft(false);
|
||||||
setLeaveError(undefined);
|
setLeaveError(undefined);
|
||||||
enterRTCSession(rtcSession, perParticipantE2EE);
|
enterRTCSession(rtcSession, e2eeSystem.kind);
|
||||||
}, [rtcSession, perParticipantE2EE]);
|
}, [rtcSession, e2eeSystem]);
|
||||||
|
|
||||||
const joinRule = useJoinRule(rtcSession.room);
|
const joinRule = useJoinRule(rtcSession.room);
|
||||||
|
|
||||||
@ -316,7 +318,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
client={client}
|
client={client}
|
||||||
matrixInfo={matrixInfo}
|
matrixInfo={matrixInfo}
|
||||||
muteStates={muteStates}
|
muteStates={muteStates}
|
||||||
onEnter={() => void enterRTCSession(rtcSession, perParticipantE2EE)}
|
onEnter={() => void enterRTCSession(rtcSession, e2eeSystem.kind)}
|
||||||
confineToRoom={confineToRoom}
|
confineToRoom={confineToRoom}
|
||||||
hideHeader={hideHeader}
|
hideHeader={hideHeader}
|
||||||
participantCount={participantCount}
|
participantCount={participantCount}
|
||||||
|
@ -19,6 +19,7 @@ import { expect, test, vi } from "vitest";
|
|||||||
|
|
||||||
import { enterRTCSession } from "../src/rtcSessionHelpers";
|
import { enterRTCSession } from "../src/rtcSessionHelpers";
|
||||||
import { Config } from "../src/config/Config";
|
import { Config } from "../src/config/Config";
|
||||||
|
import { E2eeType } from "../src/e2ee/e2eeType";
|
||||||
|
|
||||||
test("It joins the correct Session", async () => {
|
test("It joins the correct Session", async () => {
|
||||||
const focusFromOlderMembership = {
|
const focusFromOlderMembership = {
|
||||||
@ -60,7 +61,7 @@ test("It joins the correct Session", async () => {
|
|||||||
}),
|
}),
|
||||||
joinRoomSession: vi.fn(),
|
joinRoomSession: vi.fn(),
|
||||||
}) as unknown as MatrixRTCSession;
|
}) as unknown as MatrixRTCSession;
|
||||||
await enterRTCSession(mockedSession, false);
|
await enterRTCSession(mockedSession, E2eeType.NONE);
|
||||||
|
|
||||||
expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith(
|
expect(mockedSession.joinRoomSession).toHaveBeenLastCalledWith(
|
||||||
[
|
[
|
||||||
|
@ -26,6 +26,7 @@ import {
|
|||||||
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "./analytics/PosthogAnalytics";
|
||||||
import { Config } from "./config/Config";
|
import { Config } from "./config/Config";
|
||||||
import { ElementWidgetActions, WidgetHelpers, widget } from "./widget";
|
import { ElementWidgetActions, WidgetHelpers, widget } from "./widget";
|
||||||
|
import { E2eeType } from "./e2ee/e2eeType";
|
||||||
|
|
||||||
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
const FOCI_WK_KEY = "org.matrix.msc4143.rtc_foci";
|
||||||
|
|
||||||
@ -97,10 +98,13 @@ async function makePreferredLivekitFoci(
|
|||||||
|
|
||||||
export async function enterRTCSession(
|
export async function enterRTCSession(
|
||||||
rtcSession: MatrixRTCSession,
|
rtcSession: MatrixRTCSession,
|
||||||
encryptMedia: boolean,
|
e2eeType: E2eeType,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
PosthogAnalytics.instance.eventCallEnded.cacheStartCall(new Date());
|
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
|
// 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.
|
// have started tracking by the time calls start getting created.
|
||||||
@ -114,7 +118,7 @@ export async function enterRTCSession(
|
|||||||
await makePreferredLivekitFoci(rtcSession, livekitAlias),
|
await makePreferredLivekitFoci(rtcSession, livekitAlias),
|
||||||
makeActiveFocus(),
|
makeActiveFocus(),
|
||||||
{
|
{
|
||||||
manageMediaKeys: encryptMedia,
|
manageMediaKeys: e2eeType === E2eeType.PER_PARTICIPANT,
|
||||||
...(useDeviceSessionMemberEvents !== undefined && {
|
...(useDeviceSessionMemberEvents !== undefined && {
|
||||||
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
|
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
|
||||||
}),
|
}),
|
||||||
|
@ -173,11 +173,24 @@ class UserMedia {
|
|||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: LocalParticipant | RemoteParticipant,
|
participant: LocalParticipant | RemoteParticipant,
|
||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
this.vm =
|
this.vm =
|
||||||
participant instanceof LocalParticipant
|
participant instanceof LocalParticipant
|
||||||
? new LocalUserMediaViewModel(id, member, participant, callEncrypted)
|
? new LocalUserMediaViewModel(
|
||||||
: new RemoteUserMediaViewModel(id, member, participant, callEncrypted);
|
id,
|
||||||
|
member,
|
||||||
|
participant,
|
||||||
|
callEncrypted,
|
||||||
|
livekitRoom,
|
||||||
|
)
|
||||||
|
: new RemoteUserMediaViewModel(
|
||||||
|
id,
|
||||||
|
member,
|
||||||
|
participant,
|
||||||
|
callEncrypted,
|
||||||
|
livekitRoom,
|
||||||
|
);
|
||||||
|
|
||||||
this.speaker = this.vm.speaking.pipe(
|
this.speaker = this.vm.speaking.pipe(
|
||||||
// Require 1 s of continuous speaking to become a speaker, and 60 s of
|
// Require 1 s of continuous speaking to become a speaker, and 60 s of
|
||||||
@ -219,8 +232,15 @@ class ScreenShare {
|
|||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: LocalParticipant | RemoteParticipant,
|
participant: LocalParticipant | RemoteParticipant,
|
||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
|
liveKitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
this.vm = new ScreenShareViewModel(id, member, participant, callEncrypted);
|
this.vm = new ScreenShareViewModel(
|
||||||
|
id,
|
||||||
|
member,
|
||||||
|
participant,
|
||||||
|
callEncrypted,
|
||||||
|
liveKitRoom,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
@ -353,7 +373,13 @@ export class CallViewModel extends ViewModel {
|
|||||||
yield [
|
yield [
|
||||||
userMediaId,
|
userMediaId,
|
||||||
prevItems.get(userMediaId) ??
|
prevItems.get(userMediaId) ??
|
||||||
new UserMedia(userMediaId, member, p, this.encrypted),
|
new UserMedia(
|
||||||
|
userMediaId,
|
||||||
|
member,
|
||||||
|
p,
|
||||||
|
this.encrypted,
|
||||||
|
this.livekitRoom,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (p.isScreenShareEnabled) {
|
if (p.isScreenShareEnabled) {
|
||||||
@ -361,7 +387,13 @@ export class CallViewModel extends ViewModel {
|
|||||||
yield [
|
yield [
|
||||||
screenShareId,
|
screenShareId,
|
||||||
prevItems.get(screenShareId) ??
|
prevItems.get(screenShareId) ??
|
||||||
new ScreenShare(screenShareId, member, p, this.encrypted),
|
new ScreenShare(
|
||||||
|
screenShareId,
|
||||||
|
member,
|
||||||
|
p,
|
||||||
|
this.encrypted,
|
||||||
|
this.livekitRoom,
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
VideoSource,
|
VideoSource,
|
||||||
observeParticipantEvents,
|
observeParticipantEvents,
|
||||||
observeParticipantMedia,
|
observeParticipantMedia,
|
||||||
|
roomEventSelector,
|
||||||
} from "@livekit/components-core";
|
} from "@livekit/components-core";
|
||||||
import {
|
import {
|
||||||
LocalParticipant,
|
LocalParticipant,
|
||||||
@ -30,18 +31,25 @@ import {
|
|||||||
Track,
|
Track,
|
||||||
TrackEvent,
|
TrackEvent,
|
||||||
facingModeFromLocalTrack,
|
facingModeFromLocalTrack,
|
||||||
|
Room as LivekitRoom,
|
||||||
|
RoomEvent as LivekitRoomEvent,
|
||||||
} from "livekit-client";
|
} from "livekit-client";
|
||||||
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
|
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
Observable,
|
Observable,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
|
debounceTime,
|
||||||
|
distinctUntilChanged,
|
||||||
distinctUntilKeyChanged,
|
distinctUntilKeyChanged,
|
||||||
fromEvent,
|
fromEvent,
|
||||||
map,
|
map,
|
||||||
|
merge,
|
||||||
of,
|
of,
|
||||||
|
shareReplay,
|
||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
throttleTime,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
@ -85,6 +93,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 {
|
abstract class BaseMediaViewModel extends ViewModel {
|
||||||
/**
|
/**
|
||||||
* Whether the media belongs to the local user.
|
* Whether the media belongs to the local user.
|
||||||
@ -99,6 +136,10 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
*/
|
*/
|
||||||
public readonly unencryptedWarning: Observable<boolean>;
|
public readonly unencryptedWarning: Observable<boolean>;
|
||||||
|
|
||||||
|
public readonly encryptionKeyMissing: Observable<boolean>;
|
||||||
|
|
||||||
|
public readonly encryptionKeyInvalid: Observable<boolean>;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
/**
|
/**
|
||||||
* An opaque identifier for this media.
|
* An opaque identifier for this media.
|
||||||
@ -110,10 +151,11 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
// TODO: Fully separate the data layer from the UI layer by keeping the
|
// TODO: Fully separate the data layer from the UI layer by keeping the
|
||||||
// member object internal
|
// member object internal
|
||||||
public readonly member: RoomMember | undefined,
|
public readonly member: RoomMember | undefined,
|
||||||
protected readonly participant: LocalParticipant | RemoteParticipant,
|
public readonly participant: LocalParticipant | RemoteParticipant,
|
||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
audioSource: AudioSource,
|
audioSource: AudioSource,
|
||||||
videoSource: VideoSource,
|
videoSource: VideoSource,
|
||||||
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
const audio = observeTrackReference(participant, audioSource).pipe(
|
const audio = observeTrackReference(participant, audioSource).pipe(
|
||||||
@ -128,7 +170,17 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
callEncrypted &&
|
callEncrypted &&
|
||||||
(a.publication?.isEncrypted === false ||
|
(a.publication?.isEncrypted === false ||
|
||||||
v.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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,6 +227,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
|||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: LocalParticipant | RemoteParticipant,
|
participant: LocalParticipant | RemoteParticipant,
|
||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
id,
|
id,
|
||||||
@ -183,6 +236,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
|||||||
callEncrypted,
|
callEncrypted,
|
||||||
Track.Source.Microphone,
|
Track.Source.Microphone,
|
||||||
Track.Source.Camera,
|
Track.Source.Camera,
|
||||||
|
livekitRoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
const media = observeParticipantMedia(participant).pipe(this.scope.state());
|
const media = observeParticipantMedia(participant).pipe(this.scope.state());
|
||||||
@ -232,8 +286,9 @@ export class LocalUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: LocalParticipant,
|
participant: LocalParticipant,
|
||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super(id, member, participant, callEncrypted);
|
super(id, member, participant, callEncrypted, livekitRoom);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -259,8 +314,9 @@ export class RemoteUserMediaViewModel extends BaseUserMediaViewModel {
|
|||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: RemoteParticipant,
|
participant: RemoteParticipant,
|
||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super(id, member, participant, callEncrypted);
|
super(id, member, participant, callEncrypted, livekitRoom);
|
||||||
|
|
||||||
// Sync the local mute state and volume with LiveKit
|
// Sync the local mute state and volume with LiveKit
|
||||||
combineLatest([this._locallyMuted, this._localVolume], (muted, volume) =>
|
combineLatest([this._locallyMuted, this._localVolume], (muted, volume) =>
|
||||||
@ -290,6 +346,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
|
|||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: LocalParticipant | RemoteParticipant,
|
participant: LocalParticipant | RemoteParticipant,
|
||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
id,
|
id,
|
||||||
@ -298,6 +355,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
|
|||||||
callEncrypted,
|
callEncrypted,
|
||||||
Track.Source.ScreenShareAudio,
|
Track.Source.ScreenShareAudio,
|
||||||
Track.Source.ScreenShare,
|
Track.Source.ScreenShare,
|
||||||
|
livekitRoom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
ReactNode,
|
ReactNode,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useMemo,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { animated } from "@react-spring/web";
|
import { animated } from "@react-spring/web";
|
||||||
@ -88,6 +89,12 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const video = useObservableEagerState(vm.video);
|
const video = useObservableEagerState(vm.video);
|
||||||
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
|
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
|
||||||
|
const encryptionKeyMissing = useObservableEagerState(
|
||||||
|
vm.encryptionKeyMissing,
|
||||||
|
);
|
||||||
|
const encryptionKeyInvalid = useObservableEagerState(
|
||||||
|
vm.encryptionKeyInvalid,
|
||||||
|
);
|
||||||
const audioEnabled = useObservableEagerState(vm.audioEnabled);
|
const audioEnabled = useObservableEagerState(vm.audioEnabled);
|
||||||
const videoEnabled = useObservableEagerState(vm.videoEnabled);
|
const videoEnabled = useObservableEagerState(vm.videoEnabled);
|
||||||
const speaking = useObservableEagerState(vm.speaking);
|
const speaking = useObservableEagerState(vm.speaking);
|
||||||
@ -100,6 +107,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
[vm],
|
[vm],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const participantId = useMemo(() => vm.participant.identity, [vm]);
|
||||||
|
|
||||||
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
|
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
|
||||||
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
@ -122,6 +131,9 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
|
|||||||
video={video}
|
video={video}
|
||||||
member={vm.member}
|
member={vm.member}
|
||||||
unencryptedWarning={unencryptedWarning}
|
unencryptedWarning={unencryptedWarning}
|
||||||
|
encryptionKeyMissing={encryptionKeyMissing}
|
||||||
|
encryptionKeyInvalid={encryptionKeyInvalid}
|
||||||
|
participantId={participantId}
|
||||||
videoEnabled={videoEnabled && showVideo}
|
videoEnabled={videoEnabled && showVideo}
|
||||||
videoFit={cropVideo ? "cover" : "contain"}
|
videoFit={cropVideo ? "cover" : "contain"}
|
||||||
className={classNames(className, styles.tile, {
|
className={classNames(className, styles.tile, {
|
||||||
|
@ -94,7 +94,7 @@ unconditionally select the container so we can use cqmin units */
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr auto;
|
grid-template-columns: 1fr auto;
|
||||||
grid-template-rows: 1fr auto;
|
grid-template-rows: 1fr auto;
|
||||||
grid-template-areas: ". ." "nameTag button";
|
grid-template-areas: "status status" "nameTag button";
|
||||||
gap: var(--cpd-space-1x);
|
gap: var(--cpd-space-1x);
|
||||||
place-items: start;
|
place-items: start;
|
||||||
}
|
}
|
||||||
@ -115,6 +115,24 @@ unconditionally select the container so we can use cqmin units */
|
|||||||
max-inline-size: 100%;
|
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 > svg,
|
||||||
.nameTag > span {
|
.nameTag > span {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
@ -38,8 +38,11 @@ interface Props extends ComponentProps<typeof animated.div> {
|
|||||||
member: RoomMember | undefined;
|
member: RoomMember | undefined;
|
||||||
videoEnabled: boolean;
|
videoEnabled: boolean;
|
||||||
unencryptedWarning: boolean;
|
unencryptedWarning: boolean;
|
||||||
|
encryptionKeyMissing: boolean;
|
||||||
|
encryptionKeyInvalid: boolean;
|
||||||
nameTagLeadingIcon?: ReactNode;
|
nameTagLeadingIcon?: ReactNode;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
participantId: string;
|
||||||
primaryButton?: ReactNode;
|
primaryButton?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,6 +62,9 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
nameTagLeadingIcon,
|
nameTagLeadingIcon,
|
||||||
displayName,
|
displayName,
|
||||||
primaryButton,
|
primaryButton,
|
||||||
|
encryptionKeyMissing,
|
||||||
|
encryptionKeyInvalid,
|
||||||
|
participantId,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
@ -69,7 +75,8 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
<animated.div
|
<animated.div
|
||||||
className={classNames(styles.media, className, {
|
className={classNames(styles.media, className, {
|
||||||
[styles.mirror]: mirror,
|
[styles.mirror]: mirror,
|
||||||
[styles.videoMuted]: !videoEnabled,
|
[styles.videoMuted]:
|
||||||
|
!videoEnabled || encryptionKeyInvalid || encryptionKeyMissing,
|
||||||
})}
|
})}
|
||||||
style={style}
|
style={style}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
@ -95,10 +102,24 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.fg}>
|
<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}
|
{nameTagLeadingIcon}
|
||||||
<Text as="span" size="sm" weight="medium" className={styles.name}>
|
<Text as="span" size="sm" weight="medium" className={styles.name}>
|
||||||
{displayName}
|
{displayName} ({participantId})
|
||||||
</Text>
|
</Text>
|
||||||
{unencryptedWarning && (
|
{unencryptedWarning && (
|
||||||
<Tooltip
|
<Tooltip
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
forwardRef,
|
forwardRef,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
@ -60,7 +61,10 @@ interface SpotlightItemBaseProps {
|
|||||||
video: TrackReferenceOrPlaceholder;
|
video: TrackReferenceOrPlaceholder;
|
||||||
member: RoomMember | undefined;
|
member: RoomMember | undefined;
|
||||||
unencryptedWarning: boolean;
|
unencryptedWarning: boolean;
|
||||||
|
encryptionKeyMissing: boolean;
|
||||||
|
encryptionKeyInvalid: boolean;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
participantId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SpotlightUserMediaItemBaseProps extends SpotlightItemBaseProps {
|
interface SpotlightUserMediaItemBaseProps extends SpotlightItemBaseProps {
|
||||||
@ -127,6 +131,17 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
|||||||
const displayName = useDisplayName(vm);
|
const displayName = useDisplayName(vm);
|
||||||
const video = useObservableEagerState(vm.video);
|
const video = useObservableEagerState(vm.video);
|
||||||
const unencryptedWarning = useObservableEagerState(vm.unencryptedWarning);
|
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
|
// Hook this item up to the intersection observer
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -153,6 +168,9 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
|
|||||||
member: vm.member,
|
member: vm.member,
|
||||||
unencryptedWarning,
|
unencryptedWarning,
|
||||||
displayName,
|
displayName,
|
||||||
|
encryptionKeyMissing,
|
||||||
|
encryptionKeyInvalid,
|
||||||
|
participantId,
|
||||||
};
|
};
|
||||||
|
|
||||||
return vm instanceof ScreenShareViewModel ? (
|
return vm instanceof ScreenShareViewModel ? (
|
||||||
|
@ -18,6 +18,7 @@ import { IndexedDBStore } from "matrix-js-sdk/src/store/indexeddb";
|
|||||||
import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
import { MemoryStore } from "matrix-js-sdk/src/store/memory";
|
||||||
import {
|
import {
|
||||||
createClient,
|
createClient,
|
||||||
|
EventType,
|
||||||
ICreateClientOpts,
|
ICreateClientOpts,
|
||||||
Preset,
|
Preset,
|
||||||
Visibility,
|
Visibility,
|
||||||
@ -217,7 +218,7 @@ export function sanitiseRoomNameInput(input: string): string {
|
|||||||
interface CreateRoomResult {
|
interface CreateRoomResult {
|
||||||
roomId: string;
|
roomId: string;
|
||||||
alias?: string;
|
alias?: string;
|
||||||
password?: string;
|
encryptionSystem: EncryptionSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -264,6 +265,18 @@ export async function createRoom(
|
|||||||
[client.getUserId()!]: 100,
|
[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
|
// Wait for the room to arrive
|
||||||
@ -287,16 +300,22 @@ export async function createRoom(
|
|||||||
|
|
||||||
const result = await createPromise;
|
const result = await createPromise;
|
||||||
|
|
||||||
let password;
|
let encryptionSystem: EncryptionSystem;
|
||||||
if (e2ee == E2eeType.SHARED_KEY) {
|
if (e2ee == E2eeType.SHARED_KEY) {
|
||||||
password = secureRandomBase64Url(16);
|
const password = secureRandomBase64Url(16);
|
||||||
saveKeyForRoom(result.room_id, password);
|
saveKeyForRoom(result.room_id, password);
|
||||||
|
encryptionSystem = {
|
||||||
|
kind: E2eeType.SHARED_KEY,
|
||||||
|
secret: password,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
encryptionSystem = { kind: e2ee };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
roomId: result.room_id,
|
roomId: result.room_id,
|
||||||
alias: e2ee ? undefined : fullAliasFromRoomName(name, client),
|
alias: e2ee ? undefined : fullAliasFromRoomName(name, client),
|
||||||
password,
|
encryptionSystem,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user