mirror of
https://github.com/vector-im/element-call.git
synced 2026-07-18 18:59:23 +00:00
Show decryption errors as status overlay on video tiles
This commit is contained in:
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -18,5 +18,8 @@
|
|||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
}
|
},
|
||||||
|
"i18n-ally.localesPaths": [
|
||||||
|
"public/locales"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
"i18next": "^23.0.0",
|
"i18next": "^23.0.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.0",
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"i18next-http-backend": "^2.0.0",
|
"i18next-http-backend": "^2.0.0",
|
||||||
"livekit-client": "^2.0.2",
|
"livekit-client": "github:hughns/client-sdk-js#4c7c7e72c3a980347a0bacf479b4c0ba39f8b19d",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#0fe53876ec555482bbeb086fd4dec8de7b2e21eb",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#0fe53876ec555482bbeb086fd4dec8de7b2e21eb",
|
||||||
"matrix-widget-api": "^1.8.2",
|
"matrix-widget-api": "^1.8.2",
|
||||||
@@ -128,4 +128,4 @@
|
|||||||
"vite-plugin-svgr": "^4.0.0",
|
"vite-plugin-svgr": "^4.0.0",
|
||||||
"vitest": "^2.0.0"
|
"vitest": "^2.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -241,8 +241,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 {
|
||||||
@@ -437,6 +444,7 @@ export class CallViewModel extends ViewModel {
|
|||||||
member,
|
member,
|
||||||
p,
|
p,
|
||||||
this.encrypted,
|
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,20 +31,26 @@ 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,
|
distinctUntilChanged,
|
||||||
distinctUntilKeyChanged,
|
distinctUntilKeyChanged,
|
||||||
|
filter,
|
||||||
fromEvent,
|
fromEvent,
|
||||||
map,
|
map,
|
||||||
|
merge,
|
||||||
of,
|
of,
|
||||||
shareReplay,
|
shareReplay,
|
||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
throttleTime,
|
||||||
} from "rxjs";
|
} from "rxjs";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { CallMembership } from "matrix-js-sdk/src/matrixrtc";
|
import { CallMembership } from "matrix-js-sdk/src/matrixrtc";
|
||||||
@@ -89,6 +96,29 @@ function observeTrackReference(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function encryptionErrorObservable(
|
||||||
|
room: LivekitRoom,
|
||||||
|
participant: Participant,
|
||||||
|
): Observable<[Error | undefined]> {
|
||||||
|
const roomEvents = roomEventSelector(
|
||||||
|
room,
|
||||||
|
LivekitRoomEvent.EncryptionError,
|
||||||
|
).pipe(
|
||||||
|
filter(([err, participantIdentity]) => {
|
||||||
|
return participantIdentity == participant.identity;
|
||||||
|
}),
|
||||||
|
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(() => [undefined]),
|
||||||
|
),
|
||||||
|
).pipe(map(([x]) => [x as Error]));
|
||||||
|
}
|
||||||
|
|
||||||
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.
|
||||||
@@ -106,6 +136,8 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
*/
|
*/
|
||||||
public readonly unencryptedWarning: Observable<boolean>;
|
public readonly unencryptedWarning: Observable<boolean>;
|
||||||
|
|
||||||
|
public readonly lastEncryptionError: Observable<Error | undefined>;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
/**
|
/**
|
||||||
* An opaque identifier for this media.
|
* An opaque identifier for this media.
|
||||||
@@ -124,6 +156,7 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
audioSource: AudioSource,
|
audioSource: AudioSource,
|
||||||
videoSource: VideoSource,
|
videoSource: VideoSource,
|
||||||
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
if (participant) {
|
if (participant) {
|
||||||
@@ -136,9 +169,14 @@ abstract class BaseMediaViewModel extends ViewModel {
|
|||||||
(a.publication?.isEncrypted === false ||
|
(a.publication?.isEncrypted === false ||
|
||||||
v?.publication?.isEncrypted === false),
|
v?.publication?.isEncrypted === false),
|
||||||
).pipe(distinctUntilChanged(), shareReplay(1));
|
).pipe(distinctUntilChanged(), shareReplay(1));
|
||||||
|
this.lastEncryptionError = encryptionErrorObservable(livekitRoom, participant).pipe(
|
||||||
|
map(([error]) => error as Error),
|
||||||
|
startWith(undefined),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
this.video = of(undefined);
|
this.video = of(undefined);
|
||||||
this.unencryptedWarning = of(false);
|
this.unencryptedWarning = of(false);
|
||||||
|
this.lastEncryptionError = of(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,6 +219,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
|||||||
member: RoomMember | undefined,
|
member: RoomMember | undefined,
|
||||||
participant: LocalParticipant | RemoteParticipant | undefined,
|
participant: LocalParticipant | RemoteParticipant | undefined,
|
||||||
callEncrypted: boolean,
|
callEncrypted: boolean,
|
||||||
|
livekitRoom: LivekitRoom,
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
id,
|
id,
|
||||||
@@ -189,6 +228,7 @@ abstract class BaseUserMediaViewModel extends BaseMediaViewModel {
|
|||||||
callEncrypted,
|
callEncrypted,
|
||||||
Track.Source.Microphone,
|
Track.Source.Microphone,
|
||||||
Track.Source.Camera,
|
Track.Source.Camera,
|
||||||
|
livekitRoom,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (participant) {
|
if (participant) {
|
||||||
@@ -309,6 +349,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,
|
||||||
@@ -317,6 +358,7 @@ export class ScreenShareViewModel extends BaseMediaViewModel {
|
|||||||
callEncrypted,
|
callEncrypted,
|
||||||
Track.Source.ScreenShareAudio,
|
Track.Source.ScreenShareAudio,
|
||||||
Track.Source.ScreenShare,
|
Track.Source.ScreenShare,
|
||||||
|
livekitRoom,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
import { TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
import { TrackReferenceOrPlaceholder } from "@livekit/components-core";
|
||||||
import { animated } from "@react-spring/web";
|
import { animated } from "@react-spring/web";
|
||||||
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
import { RoomMember } from "matrix-js-sdk/src/matrix";
|
||||||
import { ComponentProps, ReactNode, forwardRef } from "react";
|
import { ComponentProps, ReactNode, forwardRef, useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { VideoTrack } from "@livekit/components-react";
|
import { VideoTrack } from "@livekit/components-react";
|
||||||
@@ -41,6 +41,7 @@ interface Props extends ComponentProps<typeof animated.div> {
|
|||||||
nameTagLeadingIcon?: ReactNode;
|
nameTagLeadingIcon?: ReactNode;
|
||||||
displayName: string;
|
displayName: string;
|
||||||
primaryButton?: ReactNode;
|
primaryButton?: ReactNode;
|
||||||
|
lastEncryptionError?: Error;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MediaView = forwardRef<HTMLDivElement, Props>(
|
export const MediaView = forwardRef<HTMLDivElement, Props>(
|
||||||
@@ -59,12 +60,18 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
nameTagLeadingIcon,
|
nameTagLeadingIcon,
|
||||||
displayName,
|
displayName,
|
||||||
primaryButton,
|
primaryButton,
|
||||||
|
lastEncryptionError,
|
||||||
...props
|
...props
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const statusText = useMemo<string | undefined>(
|
||||||
|
() => lastEncryptionError?.message,
|
||||||
|
[lastEncryptionError],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<animated.div
|
<animated.div
|
||||||
className={classNames(styles.media, className, {
|
className={classNames(styles.media, className, {
|
||||||
@@ -95,6 +102,11 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.fg}>
|
<div className={styles.fg}>
|
||||||
|
{statusText ? (
|
||||||
|
<div className={styles.status}>
|
||||||
|
<span>{statusText}</span>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
{!video ? (
|
{!video ? (
|
||||||
<div className={styles.status}>
|
<div className={styles.status}>
|
||||||
<span>{t("no_media_available")}</span>
|
<span>{t("no_media_available")}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user