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:
Hugh Nimmo-Smith 2024-08-19 16:47:27 +01:00
parent 040288790c
commit 497b38b609
14 changed files with 337 additions and 56 deletions

View File

@ -73,6 +73,7 @@ interface PlatformProperties {
appVersion: string;
matrixBackend: "embedded" | "jssdk";
callBackend: "livekit" | "full-mesh";
cryptoVersion?: string;
}
interface PosthogSettings {
@ -193,6 +194,9 @@ export class PosthogAnalytics {
appVersion,
matrixBackend: widget ? "embedded" : "jssdk",
callBackend: "livekit",
cryptoVersion: widget
? undefined
: window.matrixclient.getCrypto()?.getVersion(),
};
}

View File

@ -16,19 +16,40 @@ limitations under the License.
import { DisconnectReason } from "livekit-client";
import { logger } from "matrix-js-sdk/src/logger";
import { MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
import {
IPosthogEvent,
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;
}
export class CallEndedTracker {
@ -51,6 +72,8 @@ export class CallEndedTracker {
public track(
callId: string,
callParticipantsNow: number,
e2eeType: E2eeType,
rtcSession: MatrixRTCSession,
sendInstantly: boolean,
): void {
PosthogAnalytics.instance.trackEvent<CallEnded>(
@ -60,6 +83,27 @@ 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:
rtcSession.statistics.counters.roomEventEncryptionKeysReceived,
roomEventEncryptionKeysReceivedAverageAge:
rtcSession.statistics.counters.roomEventEncryptionKeysReceived > 0
? rtcSession.statistics.totals
.roomEventEncryptionKeysReceivedTotalAge /
rtcSession.statistics.counters.roomEventEncryptionKeysReceived
: 0,
},
{ send_instantly: sendInstantly },
);
@ -69,13 +113,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),
});
}
}

View File

@ -18,7 +18,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 } from "@vector-im/compound-web";
import { Dropdown, Heading } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { Button } from "@vector-im/compound-web";
@ -45,6 +45,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);
@ -59,6 +70,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();
@ -73,21 +87,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) => {
@ -103,7 +109,7 @@ export const RegisteredView: FC<Props> = ({ client }) => {
}
});
},
[client, history, setJoinExistingCallModalOpen],
[client, history, setJoinExistingCallModalOpen, encryption],
);
const recentRooms = useGroupCallRooms(client);
@ -142,6 +148,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"

View File

@ -18,7 +18,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 } from "@vector-im/compound-web";
import { Button, Dropdown, Heading } from "@vector-im/compound-web";
import { logger } from "matrix-js-sdk/src/logger";
import { useClient } from "../ClientContext";
@ -44,6 +44,18 @@ import { Config } from "../config/Config";
import { E2eeType } from "../e2ee/e2eeType";
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 = () => {
const { setClient } = useClient();
const [loading, setLoading] = useState(false);
@ -52,6 +64,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(
@ -82,13 +97,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;
@ -115,17 +133,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) => {
@ -142,6 +154,7 @@ export const UnauthenticatedView: FC = () => {
history,
setJoinExistingCallModalOpen,
setClient,
encryption,
],
);
@ -204,6 +217,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"

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 => {
@ -191,7 +191,7 @@ export const GroupCallView: FC<Props> = ({
ev: CustomEvent<IWidgetApiRequest>,
): Promise<void> => {
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, {});
};
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
@ -201,12 +201,12 @@ export const GroupCallView: FC<Props> = ({
} else if (widget && !preload && skipLobby) {
const join = async (): Promise<void> => {
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.
join();
}
}, [rtcSession, preload, skipLobby, perParticipantE2EE]);
}, [rtcSession, preload, skipLobby, e2eeSystem]);
const [left, setLeft] = useState(false);
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
@ -223,6 +223,8 @@ export const GroupCallView: FC<Props> = ({
PosthogAnalytics.instance.eventCallEnded.track(
rtcSession.room.roomId,
rtcSession.memberships.length,
matrixInfo.e2eeSystem.kind,
rtcSession,
sendInstantly,
);
@ -237,7 +239,7 @@ export const GroupCallView: FC<Props> = ({
history.push("/");
}
},
[rtcSession, isPasswordlessUser, confineToRoom, history],
[rtcSession, isPasswordlessUser, confineToRoom, history, matrixInfo],
);
useEffect(() => {
@ -262,8 +264,8 @@ export const GroupCallView: FC<Props> = ({
const onReconnect = useCallback(() => {
setLeft(false);
setLeaveError(undefined);
enterRTCSession(rtcSession, perParticipantE2EE);
}, [rtcSession, perParticipantE2EE]);
enterRTCSession(rtcSession, e2eeSystem.kind);
}, [rtcSession, e2eeSystem]);
const joinRule = useJoinRule(rtcSession.room);
@ -316,7 +318,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}

View File

@ -19,6 +19,7 @@ import { expect, test, vi } from "vitest";
import { enterRTCSession } from "../src/rtcSessionHelpers";
import { Config } from "../src/config/Config";
import { E2eeType } from "../src/e2ee/e2eeType";
test("It joins the correct Session", async () => {
const focusFromOlderMembership = {
@ -60,7 +61,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(
[

View File

@ -26,6 +26,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";
@ -97,10 +98,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.
@ -114,7 +118,7 @@ export async function enterRTCSession(
await makePreferredLivekitFoci(rtcSession, livekitAlias),
makeActiveFocus(),
{
manageMediaKeys: encryptMedia,
manageMediaKeys: e2eeType === E2eeType.PER_PARTICIPANT,
...(useDeviceSessionMemberEvents !== undefined && {
useLegacyMemberEvents: !useDeviceSessionMemberEvents,
}),

View File

@ -173,11 +173,24 @@ class UserMedia {
member: RoomMember | undefined,
participant: LocalParticipant | RemoteParticipant,
callEncrypted: boolean,
livekitRoom: LivekitRoom,
) {
this.vm =
participant instanceof LocalParticipant
? new LocalUserMediaViewModel(id, member, participant, callEncrypted)
: new RemoteUserMediaViewModel(id, member, participant, callEncrypted);
? new LocalUserMediaViewModel(
id,
member,
participant,
callEncrypted,
livekitRoom,
)
: new RemoteUserMediaViewModel(
id,
member,
participant,
callEncrypted,
livekitRoom,
);
this.speaker = this.vm.speaking.pipe(
// Require 1 s of continuous speaking to become a speaker, and 60 s of
@ -219,8 +232,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 {
@ -353,7 +373,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) {
@ -361,7 +387,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,
),
];
}
}

View File

@ -20,6 +20,7 @@ import {
VideoSource,
observeParticipantEvents,
observeParticipantMedia,
roomEventSelector,
} from "@livekit/components-core";
import {
LocalParticipant,
@ -30,18 +31,25 @@ import {
Track,
TrackEvent,
facingModeFromLocalTrack,
Room as LivekitRoom,
RoomEvent as LivekitRoomEvent,
} from "livekit-client";
import { RoomMember, RoomMemberEvent } from "matrix-js-sdk/src/matrix";
import {
BehaviorSubject,
Observable,
combineLatest,
debounceTime,
distinctUntilChanged,
distinctUntilKeyChanged,
fromEvent,
map,
merge,
of,
shareReplay,
startWith,
switchMap,
throttleTime,
} from "rxjs";
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 {
/**
* Whether the media belongs to the local user.
@ -99,6 +136,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.
@ -110,10 +151,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(
@ -128,7 +170,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));
}
}
@ -175,6 +227,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
member: RoomMember | undefined,
participant: LocalParticipant | RemoteParticipant,
callEncrypted: boolean,
livekitRoom: LivekitRoom,
) {
super(
id,
@ -183,6 +236,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
callEncrypted,
Track.Source.Microphone,
Track.Source.Camera,
livekitRoom,
);
const media = observeParticipantMedia(participant).pipe(this.scope.state());
@ -232,8 +286,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);
}
}
@ -259,8 +314,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 mute state and volume with LiveKit
combineLatest([this._locallyMuted, this._localVolume], (muted, volume) =>
@ -290,6 +346,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
member: RoomMember | undefined,
participant: LocalParticipant | RemoteParticipant,
callEncrypted: boolean,
livekitRoom: LivekitRoom,
) {
super(
id,
@ -298,6 +355,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
callEncrypted,
Track.Source.ScreenShareAudio,
Track.Source.ScreenShare,
livekitRoom,
);
}
}

View File

@ -19,6 +19,7 @@ import {
ReactNode,
forwardRef,
useCallback,
useMemo,
useState,
} from "react";
import { animated } from "@react-spring/web";
@ -88,6 +89,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);
@ -100,6 +107,8 @@ const UserMediaTile = forwardRef<HTMLDivElement, UserMediaTileProps>(
[vm],
);
const participantId = useMemo(() => vm.participant.identity, [vm]);
const MicIcon = audioEnabled ? MicOnSolidIcon : MicOffSolidIcon;
const [menuOpen, setMenuOpen] = useState(false);
@ -122,6 +131,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, {

View File

@ -94,7 +94,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;
}
@ -115,6 +115,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;

View File

@ -38,8 +38,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;
}
@ -59,6 +62,9 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
nameTagLeadingIcon,
displayName,
primaryButton,
encryptionKeyMissing,
encryptionKeyInvalid,
participantId,
...props
},
ref,
@ -69,7 +75,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}
@ -95,10 +102,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

View File

@ -20,6 +20,7 @@ import {
forwardRef,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
@ -60,7 +61,10 @@ interface SpotlightItemBaseProps {
video: TrackReferenceOrPlaceholder;
member: RoomMember | undefined;
unencryptedWarning: boolean;
encryptionKeyMissing: boolean;
encryptionKeyInvalid: boolean;
displayName: string;
participantId: string;
}
interface SpotlightUserMediaItemBaseProps extends SpotlightItemBaseProps {
@ -127,6 +131,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(() => {
@ -153,6 +168,9 @@ const SpotlightItem = forwardRef<HTMLDivElement, SpotlightItemProps>(
member: vm.member,
unencryptedWarning,
displayName,
encryptionKeyMissing,
encryptionKeyInvalid,
participantId,
};
return vm instanceof ScreenShareViewModel ? (

View File

@ -18,6 +18,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,
@ -217,7 +218,7 @@ export function sanitiseRoomNameInput(input: string): string {
interface CreateRoomResult {
roomId: string;
alias?: string;
password?: string;
encryptionSystem: EncryptionSystem;
}
/**
@ -264,6 +265,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
@ -287,16 +300,22 @@ export async function createRoom(
const result = await createPromise;
let password;
let encryptionSystem: EncryptionSystem;
if (e2ee == E2eeType.SHARED_KEY) {
password = secureRandomBase64Url(16);
const password = secureRandomBase64Url(16);
saveKeyForRoom(result.room_id, password);
encryptionSystem = {
kind: E2eeType.SHARED_KEY,
secret: password,
};
} else {
encryptionSystem = { kind: e2ee };
}
return {
roomId: result.room_id,
alias: e2ee ? undefined : fullAliasFromRoomName(name, client),
password,
encryptionSystem,
};
}