mirror of
https://github.com/vector-im/element-call.git
synced 2026-03-31 07:00:26 +00:00
Merge branch 'livekit' into toger5/loading_border
This commit is contained in:
2
src/@types/i18next.d.ts
vendored
2
src/@types/i18next.d.ts
vendored
@@ -7,7 +7,7 @@ Please see LICENSE in the repository root for full details.
|
||||
|
||||
import "i18next";
|
||||
// import all namespaces (for the default language, only)
|
||||
import app from "../../locales/en-GB/app.json";
|
||||
import app from "../../locales/en/app.json";
|
||||
|
||||
declare module "i18next" {
|
||||
interface CustomTypeOptions {
|
||||
|
||||
@@ -22,7 +22,7 @@ export abstract class TranslatedError extends Error {
|
||||
messageKey: ParseKeys<DefaultNamespace, TOptions>,
|
||||
translationFn: TFunction<DefaultNamespace>,
|
||||
) {
|
||||
super(translationFn(messageKey, { lng: "en-GB" } as TOptions));
|
||||
super(translationFn(messageKey, { lng: "en" } as TOptions));
|
||||
this.translatedMessage = translationFn(messageKey);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,11 @@ layer(compound);
|
||||
--background-gradient: url("graphics/backgroundGradient.svg");
|
||||
}
|
||||
|
||||
:root,
|
||||
[class*="cpd-theme-"] {
|
||||
--video-tile-background: var(--cpd-color-bg-subtle-secondary);
|
||||
}
|
||||
|
||||
.cpd-theme-dark {
|
||||
--cpd-color-border-accent: var(--cpd-color-green-1100);
|
||||
--stopgap-color-on-solid-accent: var(--cpd-color-text-primary);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { platform } from "./Platform";
|
||||
|
||||
// This generates a map of locale names to their URL (based on import.meta.url), which looks like this:
|
||||
// {
|
||||
// "../locales/en-GB/app.json": "/whatever/assets/root/locales/en-aabbcc.json",
|
||||
// "../locales/en/app.json": "/whatever/assets/root/locales/en-aabbcc.json",
|
||||
// ...
|
||||
// }
|
||||
const locales = import.meta.glob<string>("../locales/*/*.json", {
|
||||
@@ -41,7 +41,7 @@ const getLocaleUrl = (
|
||||
const supportedLngs = [
|
||||
...new Set(
|
||||
Object.keys(locales).map((url) => {
|
||||
// The URLs are of the form ../locales/en-GB/app.json
|
||||
// The URLs are of the form ../locales/en/app.json
|
||||
// This extracts the language code from the URL
|
||||
const lang = url.match(/\/([^/]+)\/[^/]+\.json$/)?.[1];
|
||||
if (!lang) {
|
||||
@@ -133,7 +133,7 @@ export class Initializer {
|
||||
.use(languageDetector)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
fallbackLng: "en-GB",
|
||||
fallbackLng: "en",
|
||||
defaultNS: "app",
|
||||
keySeparator: ".",
|
||||
nsSeparator: false,
|
||||
|
||||
@@ -18,8 +18,7 @@ Please see LICENSE in the repository root for full details.
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
background-color: black;
|
||||
background-color: var(--cpd-color-bg-subtle-primary);
|
||||
background-color: var(--video-tile-background);
|
||||
}
|
||||
|
||||
video.mirror {
|
||||
@@ -35,7 +34,7 @@ video.mirror {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
background-color: var(--video-tile-background);
|
||||
}
|
||||
|
||||
.buttonBar {
|
||||
|
||||
@@ -43,7 +43,6 @@ import {
|
||||
ECConnectionState,
|
||||
} from "../livekit/useECConnectionState";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { showNonMemberTiles } from "../settings/settings";
|
||||
|
||||
vi.mock("@livekit/components-core");
|
||||
|
||||
@@ -637,49 +636,6 @@ test("participants must have a MatrixRTCSession to be visible", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("shows participants without MatrixRTCSession when enabled in settings", () => {
|
||||
// enable the setting:
|
||||
showNonMemberTiles.setValue(true);
|
||||
withTestScheduler(({ hot, expectObservable }) => {
|
||||
const scenarioInputMarbles = " abc";
|
||||
const expectedLayoutMarbles = "abc";
|
||||
|
||||
withCallViewModel(
|
||||
hot(scenarioInputMarbles, {
|
||||
a: [],
|
||||
b: [aliceParticipant],
|
||||
c: [aliceParticipant, bobParticipant],
|
||||
}),
|
||||
of([]),
|
||||
of(ConnectionState.Connected),
|
||||
new Map(),
|
||||
(vm) => {
|
||||
vm.setGridMode("grid");
|
||||
expectObservable(summarizeLayout(vm.layout)).toBe(
|
||||
expectedLayoutMarbles,
|
||||
{
|
||||
a: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0"],
|
||||
},
|
||||
b: {
|
||||
type: "one-on-one",
|
||||
local: "local:0",
|
||||
remote: `${aliceId}:0`,
|
||||
},
|
||||
c: {
|
||||
type: "grid",
|
||||
spotlight: undefined,
|
||||
grid: ["local:0", `${aliceId}:0`, `${bobId}:0`],
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("should show at least one tile per MatrixRTCSession", () => {
|
||||
withTestScheduler(({ hot, expectObservable }) => {
|
||||
// iterate through some combinations of MatrixRTC memberships
|
||||
|
||||
@@ -66,7 +66,7 @@ import {
|
||||
} from "./MediaViewModel";
|
||||
import { accumulate, finalizeValue } from "../utils/observable";
|
||||
import { ObservableScope } from "./ObservableScope";
|
||||
import { duplicateTiles, showNonMemberTiles } from "../settings/settings";
|
||||
import { duplicateTiles } from "../settings/settings";
|
||||
import { isFirefox } from "../Platform";
|
||||
import { setPipEnabled } from "../controls";
|
||||
import { GridTileViewModel, SpotlightTileViewModel } from "./TileViewModel";
|
||||
@@ -427,8 +427,6 @@ export class CallViewModel extends ViewModel {
|
||||
},
|
||||
);
|
||||
|
||||
private readonly nonMemberItemCount = new BehaviorSubject<number>(0);
|
||||
|
||||
/**
|
||||
* List of MediaItems that we want to display
|
||||
*/
|
||||
@@ -443,7 +441,6 @@ export class CallViewModel extends ViewModel {
|
||||
this.matrixRTCSession,
|
||||
MatrixRTCSessionEvent.MembershipsChanged,
|
||||
).pipe(startWith(null)),
|
||||
showNonMemberTiles.value,
|
||||
]).pipe(
|
||||
scan(
|
||||
(
|
||||
@@ -453,7 +450,6 @@ export class CallViewModel extends ViewModel {
|
||||
{ participant: localParticipant },
|
||||
duplicateTiles,
|
||||
_membershipsChanged,
|
||||
showNonMemberTiles,
|
||||
],
|
||||
) => {
|
||||
const newItems = new Map(
|
||||
@@ -491,17 +487,9 @@ export class CallViewModel extends ViewModel {
|
||||
}
|
||||
for (let i = 0; i < 1 + duplicateTiles; i++) {
|
||||
const indexedMediaId = `${livekitParticipantId}:${i}`;
|
||||
let prevMedia = prevItems.get(indexedMediaId);
|
||||
const prevMedia = prevItems.get(indexedMediaId);
|
||||
if (prevMedia && prevMedia instanceof UserMedia) {
|
||||
prevMedia.updateParticipant(participant);
|
||||
if (prevMedia.vm.member === undefined) {
|
||||
// We have a previous media created because of the `debugShowNonMember` flag.
|
||||
// In this case we actually replace the media item.
|
||||
// This "hack" never occurs if we do not use the `debugShowNonMember` debugging
|
||||
// option and if we always find a room member for each rtc member (which also
|
||||
// only fails if we have a fundamental problem)
|
||||
prevMedia = undefined;
|
||||
}
|
||||
}
|
||||
yield [
|
||||
indexedMediaId,
|
||||
@@ -537,63 +525,7 @@ export class CallViewModel extends ViewModel {
|
||||
}.bind(this)(),
|
||||
);
|
||||
|
||||
// Generate non member items (items without a corresponding MatrixRTC member)
|
||||
// Those items should not be rendered, they are participants in livekit that do not have a corresponding
|
||||
// matrix rtc members. This cannot be any good:
|
||||
// - A malicious user impersonates someone
|
||||
// - Someone injects abusive content
|
||||
// - The user cannot have encryption keys so it makes no sense to participate
|
||||
// We can only trust users that have a matrixRTC member event.
|
||||
//
|
||||
// This is still available as a debug option. This can be useful
|
||||
// - If one wants to test scalability using the livekit cli.
|
||||
// - If an experimental project does not yet do the matrixRTC bits.
|
||||
// - If someone wants to debug if the LK connection works but matrixRTC room state failed to arrive.
|
||||
const newNonMemberItems = showNonMemberTiles
|
||||
? new Map(
|
||||
function* (this: CallViewModel): Iterable<[string, MediaItem]> {
|
||||
for (const participant of remoteParticipants) {
|
||||
for (let i = 0; i < 1 + duplicateTiles; i++) {
|
||||
const maybeNonMemberParticipantId =
|
||||
participant.identity + ":" + i;
|
||||
if (!newItems.has(maybeNonMemberParticipantId)) {
|
||||
const nonMemberId = maybeNonMemberParticipantId;
|
||||
yield [
|
||||
nonMemberId,
|
||||
// We create UserMedia with or without a participant.
|
||||
// This will be the initial value of a BehaviourSubject.
|
||||
// Once a participant appears we will update the BehaviourSubject. (see above)
|
||||
prevItems.get(nonMemberId) ??
|
||||
new UserMedia(
|
||||
nonMemberId,
|
||||
undefined,
|
||||
participant,
|
||||
this.encryptionSystem,
|
||||
this.livekitRoom,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
}.bind(this)(),
|
||||
)
|
||||
: new Map();
|
||||
if (newNonMemberItems.size > 0) {
|
||||
logger.debug("Added NonMember items: ", newNonMemberItems);
|
||||
}
|
||||
|
||||
const newNonMemberItemCount =
|
||||
newNonMemberItems.size / (1 + duplicateTiles);
|
||||
if (this.nonMemberItemCount.value !== newNonMemberItemCount)
|
||||
this.nonMemberItemCount.next(newNonMemberItemCount);
|
||||
|
||||
const combinedNew = new Map([
|
||||
...newNonMemberItems.entries(),
|
||||
...newItems.entries(),
|
||||
]);
|
||||
|
||||
for (const [id, t] of prevItems) if (!combinedNew.has(id)) t.destroy();
|
||||
return combinedNew;
|
||||
return newItems;
|
||||
},
|
||||
new Map<string, MediaItem>(),
|
||||
),
|
||||
@@ -723,49 +655,42 @@ export class CallViewModel extends ViewModel {
|
||||
this.scope.state(),
|
||||
);
|
||||
|
||||
private readonly pip: Observable<UserMediaViewModel | null> =
|
||||
this.screenShares.pipe(
|
||||
switchMap((screenShares) => {
|
||||
if (screenShares.length > 0) {
|
||||
return this.spotlightSpeaker;
|
||||
}
|
||||
private readonly pip: Observable<UserMediaViewModel | null> = combineLatest([
|
||||
this.screenShares,
|
||||
this.spotlightSpeaker,
|
||||
this.mediaItems,
|
||||
]).pipe(
|
||||
switchMap(([screenShares, spotlight, mediaItems]) => {
|
||||
if (screenShares.length > 0) {
|
||||
return this.spotlightSpeaker;
|
||||
}
|
||||
if (!spotlight || spotlight.local) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return this.spotlightSpeaker.pipe(
|
||||
switchMap((speaker) => {
|
||||
if (!speaker || speaker.local) {
|
||||
return of(null);
|
||||
}
|
||||
const localUserMedia = mediaItems.find(
|
||||
(m) => m.vm instanceof LocalUserMediaViewModel,
|
||||
) as UserMedia | undefined;
|
||||
|
||||
return this.mediaItems.pipe(
|
||||
switchMap((mediaItems) => {
|
||||
const localUserMedia = mediaItems.find(
|
||||
(m) => m.vm instanceof LocalUserMediaViewModel,
|
||||
) as UserMedia | undefined;
|
||||
const localUserMediaViewModel = localUserMedia?.vm as
|
||||
| LocalUserMediaViewModel
|
||||
| undefined;
|
||||
|
||||
const localUserMediaViewModel = localUserMedia?.vm as
|
||||
| LocalUserMediaViewModel
|
||||
| undefined;
|
||||
if (!localUserMediaViewModel) {
|
||||
return of(null);
|
||||
}
|
||||
return localUserMediaViewModel.alwaysShow.pipe(
|
||||
map((alwaysShow) => {
|
||||
if (alwaysShow) {
|
||||
return localUserMediaViewModel;
|
||||
}
|
||||
|
||||
if (!localUserMediaViewModel) {
|
||||
return of(null);
|
||||
}
|
||||
|
||||
return localUserMediaViewModel.alwaysShow.pipe(
|
||||
map((alwaysShow) => {
|
||||
if (alwaysShow) {
|
||||
return localUserMediaViewModel;
|
||||
}
|
||||
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
this.scope.state(),
|
||||
);
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
}),
|
||||
this.scope.state(),
|
||||
);
|
||||
|
||||
private readonly hasRemoteScreenShares: Observable<boolean> =
|
||||
this.spotlight.pipe(
|
||||
@@ -878,15 +803,16 @@ export class CallViewModel extends ViewModel {
|
||||
this.mediaItems.pipe(
|
||||
map((mediaItems) => {
|
||||
if (mediaItems.length !== 2) return null;
|
||||
const local = mediaItems.find((vm) => vm.vm.local)!
|
||||
.vm as LocalUserMediaViewModel;
|
||||
const local = mediaItems.find((vm) => vm.vm.local)?.vm as
|
||||
| LocalUserMediaViewModel
|
||||
| undefined;
|
||||
const remote = mediaItems.find((vm) => !vm.vm.local)?.vm as
|
||||
| RemoteUserMediaViewModel
|
||||
| undefined;
|
||||
// There might not be a remote tile if there are screen shares, or if
|
||||
// only the local user is in the call and they're using the duplicate
|
||||
// tiles option
|
||||
if (remote === undefined) return null;
|
||||
if (!remote || !local) return null;
|
||||
|
||||
return { type: "one-on-one", local, remote };
|
||||
}),
|
||||
|
||||
@@ -15,7 +15,7 @@ Please see LICENSE in the repository root for full details.
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
object-fit: contain;
|
||||
background-color: var(--cpd-color-bg-subtle-primary);
|
||||
background-color: var(--video-tile-background);
|
||||
/* This transform is a no-op, but it forces Firefox to use a different
|
||||
rendering path, one that actually clips the corners of <video> elements into
|
||||
the intended rounded shape. We can remove this if Firefox stops being broken. */
|
||||
@@ -35,7 +35,7 @@ Please see LICENSE in the repository root for full details.
|
||||
}
|
||||
|
||||
.bg {
|
||||
background-color: var(--cpd-color-bg-subtle-secondary);
|
||||
background-color: var(--video-tile-background);
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
border-radius: inherit;
|
||||
|
||||
@@ -75,7 +75,7 @@ describe("MediaView", () => {
|
||||
<MediaView {...baseProps} video={undefined} localParticipant={false} />,
|
||||
);
|
||||
expect(screen.getByRole("img", { name: "some name" })).toBeVisible();
|
||||
expect(screen.getByTestId("videoTile")).toBeVisible();
|
||||
expect(screen.getByText("video_tile.waiting_for_media")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -120,6 +120,11 @@ export const MediaView = forwardRef<HTMLDivElement, Props>(
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!video && !localParticipant && (
|
||||
<div className={styles.status}>
|
||||
{t("video_tile.waiting_for_media")}
|
||||
</div>
|
||||
)}
|
||||
{/* TODO: Bring this back once encryption status is less broken */}
|
||||
{/*encryptionStatus !== EncryptionStatus.Okay && (
|
||||
<div className={styles.status}>
|
||||
|
||||
@@ -17,20 +17,20 @@ import "vitest-axe/extend-expect";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
|
||||
import EN_GB from "../locales/en-GB/app.json";
|
||||
import EN from "../locales/en/app.json";
|
||||
import { Config } from "./config/Config";
|
||||
|
||||
// Bare-minimum i18n config
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: "en-GB",
|
||||
fallbackLng: "en-GB",
|
||||
supportedLngs: ["en-GB"],
|
||||
lng: "en",
|
||||
fallbackLng: "en",
|
||||
supportedLngs: ["en"],
|
||||
// We embed the translations, so that it never needs to fetch
|
||||
resources: {
|
||||
"en-GB": {
|
||||
app: EN_GB,
|
||||
en: {
|
||||
app: EN,
|
||||
},
|
||||
},
|
||||
interpolation: {
|
||||
|
||||
Reference in New Issue
Block a user