Merge branch 'livekit' into toger5/loading_border

This commit is contained in:
Hugh Nimmo-Smith
2024-12-06 11:57:41 +00:00
19 changed files with 106 additions and 204 deletions

View File

@@ -42,7 +42,7 @@ jobs:
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
uses: docker/metadata-action@369eb591f429131d6889c46b94e711f089e6ca96 # v5.6.1
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: ${{ inputs.docker_tags}}
@@ -51,7 +51,7 @@ jobs:
uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 # v3.7.1
- name: Build and push Docker image
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
uses: docker/build-push-action@48aba3b46d1b1fec4febb7c5d0c644b249a11355 # v6.10.0
with:
context: .
platforms: linux/amd64,linux/arm64

View File

@@ -92,6 +92,9 @@ work:
experimental_features:
# MSC3266: Room summary API. Used for knocking over federation
msc3266_enabled: true
# MSC4222 needed for syncv2 state_after. This allow clients to
# correctly track the state of the room.
msc4222_enabled: true
# The maximum allowed duration by which sent events can be delayed, as
# per MSC4140.
@@ -109,6 +112,10 @@ summary contains the room join rules. We need that to decide if the user gets
prompted with the option to knock ("Request to join call"), a cannot join error or the
join view.
MSC4222 allow clients to opt-in to a change of the sync v2 API that allows them
to correctly track the state of the room. This is required by Element Call to
track room state reliably.
Element Call requires a Livekit SFU alongside a [Livekit JWT
service](https://github.com/element-hq/lk-jwt-service) to work. The url to the
Livekit JWT service can either be configured in the config of Element Call
@@ -213,7 +220,7 @@ To add a new translation key you can do these steps:
1. Add the new key entry to the code where the new key is used: `t("some_new_key")`
1. Run `yarn i18n` to extract the new key and update the translation files. This
will add a skeleton entry to the `locales/en-GB/app.json` file:
will add a skeleton entry to the `locales/en/app.json` file:
```jsonc
{
...
@@ -221,7 +228,7 @@ To add a new translation key you can do these steps:
...
}
```
1. Update the skeleton entry in the `locales/en-GB/app.json` file with
1. Update the skeleton entry in the `locales/en/app.json` file with
the English translation:
```jsonc

View File

@@ -25,6 +25,9 @@ trusted_key_servers:
experimental_features:
# MSC3266: Room summary API. Used for knocking over federation
msc3266_enabled: true
# MSC4222 needed for syncv2 state_after. This allow clients to
# correctly track the state of the room.
msc4222_enabled: true
# The maximum allowed duration by which sent events can be delayed, as
# per MSC4140. Must be a positive value if set. Defaults to no

View File

@@ -21,7 +21,7 @@ export default {
},
],
},
locales: ["en-GB"],
locales: ["en"],
output: "locales/$LOCALE/$NAMESPACE.json",
input: ["src/**/*.{ts,tsx}"],
sort: true,

View File

@@ -7,13 +7,13 @@
"features": ["plural_postfix_us", "filter_untranslated"],
"files": [
{
"pattern": "locales/en-GB/*.json",
"pattern": "locales/en/*.json",
"lang": "inherited"
},
{
"group": "existing",
"pattern": "locales/*/*.json",
"excludes": ["locales/en-GB/*.json"],
"excludes": ["locales/en/*.json"],
"lang": "${autodetectLang}"
}
]
@@ -25,9 +25,6 @@
"output": "locales/${langLsrDash}/${file}"
}
],
"includeSourceLang": "${includeSourceLang|false}",
"langAliases": {
"en": "en_GB"
}
"includeSourceLang": "${includeSourceLang|false}"
}
}

View File

@@ -195,6 +195,7 @@
"expand": "Expand",
"mute_for_me": "Mute for me",
"muted_for_me": "Muted for me",
"volume": "Volume"
"volume": "Volume",
"waiting_for_media": "Waiting for media..."
}
}

View File

@@ -53,7 +53,7 @@
"@types/grecaptcha": "^3.0.9",
"@types/jsdom": "^21.1.7",
"@types/lodash-es": "^4.17.12",
"@types/node": "^20.0.0",
"@types/node": "^22.0.0",
"@types/pako": "^2.0.3",
"@types/qrcode": "^1.5.5",
"@types/react-dom": "^18.3.0",
@@ -63,7 +63,7 @@
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@use-gesture/react": "^10.2.11",
"@vector-im/compound-design-tokens": "^1.9.1",
"@vector-im/compound-design-tokens": "^2.0.0",
"@vector-im/compound-web": "^7.2.0",
"@vitejs/plugin-basic-ssl": "^1.0.1",
"@vitejs/plugin-react": "^4.0.1",

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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 {

View File

@@ -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

View File

@@ -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 };
}),

View File

@@ -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;

View File

@@ -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();
});
});

View File

@@ -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}>

View File

@@ -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: {

View File

@@ -3051,12 +3051,12 @@
dependencies:
undici-types "~6.19.8"
"@types/node@^20.0.0":
version "20.17.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.9.tgz#5f141d4b7ee125cdee5faefe28de095398865bab"
integrity sha512-0JOXkRyLanfGPE2QRCwgxhzlBAvaRdCNMcvbd7jFfpmD4eEXll7LRwy5ymJmyeZqk7Nh7eD2LeUyQ68BbndmXw==
"@types/node@^22.0.0":
version "22.10.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.10.1.tgz#41ffeee127b8975a05f8c4f83fb89bcb2987d766"
integrity sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==
dependencies:
undici-types "~6.19.2"
undici-types "~6.20.0"
"@types/normalize-package-data@^2.4.0":
version "2.4.4"
@@ -3307,12 +3307,10 @@
dependencies:
"@use-gesture/core" "10.3.1"
"@vector-im/compound-design-tokens@^1.9.1":
version "1.9.2"
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-1.9.2.tgz#0b76e5475da3bc36443f7dc87951b937b5013d6f"
integrity sha512-gQmK4dHR2iws3ZskDv8Il6A4/rvQV7TPSmEOXLsahDhBTInWqexXeQnNRSt9Z5DsLPrkxL3/KoCt9lfYu/yiag==
dependencies:
prettier "^3.3.3"
"@vector-im/compound-design-tokens@^2.0.0":
version "2.1.1"
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-2.1.1.tgz#d6175a99fe4b97688464126f255386990f3048d6"
integrity sha512-QnUi2K14D9KTXxcLQKUU3V75cforZLMwhaaJDNftT8F5mG86950hAM+qhgDNEpEU+pkTffQj0/g/5859YmqWzQ==
"@vector-im/compound-web@^7.2.0":
version "7.4.0"
@@ -7007,7 +7005,7 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
prettier@^3.0.0, prettier@^3.3.3:
prettier@^3.0.0:
version "3.4.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.4.1.tgz#e211d451d6452db0a291672ca9154bc8c2579f7b"
integrity sha512-G+YdqtITVZmOJje6QkXQWzl3fSfMxFwm1tjTyo9exhkmWSqC4Yhd1+lug++IlR2mvRVAxEDDWYkQdeSztajqgg==
@@ -8262,11 +8260,16 @@ underscore.string@~3.3.4:
sprintf-js "^1.1.1"
util-deprecate "^1.0.2"
undici-types@~6.19.2, undici-types@~6.19.8:
undici-types@~6.19.8:
version "6.19.8"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
undici-types@~6.20.0:
version "6.20.0"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.20.0.tgz#8171bf22c1f588d1554d55bf204bc624af388433"
integrity sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==
undici@^5.25.4:
version "5.28.4"
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.4.tgz#6b280408edb6a1a604a9b20340f45b422e373068"
@@ -8411,9 +8414,9 @@ value-or-function@^4.0.0:
integrity sha512-aeVK81SIuT6aMJfNo9Vte8Dw0/FZINGBV8BfCraGtqVxIeLAEhJyoWs8SmvRVmXfGss2PmmOwZCuBPbZR+IYWg==
vaul@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.0.tgz#7da4bc965e0b184ada632f1208096b0f5575d920"
integrity sha512-YhO/bikcauk48hzhMhvIvT+U87cuCbNbKk9fF4Ou5UkI9t2KkBMernmdP37pCzF15hrv55fcny1YhexK8h6GVQ==
version "1.1.1"
resolved "https://registry.yarnpkg.com/vaul/-/vaul-1.1.1.tgz#93aceaad16f7c53aacf28a2609b2dd43b5a91fa0"
integrity sha512-+ejzF6ffQKPcfgS7uOrGn017g39F8SO4yLPXbBhpC7a0H+oPqPna8f1BUfXaz8eU4+pxbQcmjxW+jWBSbxjaFg==
dependencies:
"@radix-ui/react-dialog" "^1.1.1"