diff --git a/codecov.yaml b/codecov.yaml index 30c038ec..0f42ad4e 100644 --- a/codecov.yaml +++ b/codecov.yaml @@ -9,5 +9,7 @@ coverage: informational: true patch: default: - # Expect 80% coverage on all lines that a PR touches + # Encourage (but don't enforce) 80% coverage on all lines that a PR + # touches target: 80% + informational: true diff --git a/package.json b/package.json index 50218d28..1084d270 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,6 @@ "@babel/preset-env": "^7.22.20", "@babel/preset-react": "^7.22.15", "@babel/preset-typescript": "^7.23.0", - "@juggle/resize-observer": "^3.3.1", "@livekit/components-core": "^0.11.0", "@livekit/components-react": "^2.0.0", "@opentelemetry/api": "^1.4.0", @@ -53,8 +52,8 @@ "@types/react-router-dom": "^5.3.3", "@types/sdp-transform": "^2.4.5", "@types/uuid": "10", - "@typescript-eslint/eslint-plugin": "^7.0.0", - "@typescript-eslint/parser": "^7.0.0", + "@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.0.0", "@vector-im/compound-web": "^6.0.0", @@ -84,7 +83,7 @@ "livekit-client": "^2.0.2", "lodash": "^4.17.21", "loglevel": "^1.9.1", - "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#cb9614c5165b0c2627670d7d1a59dc3667ed0e2e", + "matrix-js-sdk": "matrix-org/matrix-js-sdk#cb9614c5165b0c2627670d7d1a59dc3667ed0e2e", "matrix-widget-api": "^1.8.2", "normalize.css": "^8.0.1", "observable-hooks": "^4.2.3", diff --git a/public/locales/de/app.json b/public/locales/de/app.json index 997fa7a0..359b352c 100644 --- a/public/locales/de/app.json +++ b/public/locales/de/app.json @@ -58,8 +58,6 @@ "disconnected_banner": "Die Verbindung zum Server wurde getrennt.", "full_screen_view_description": "<0>Übermittelte Problemberichte helfen uns, Fehler zu beheben.", "full_screen_view_h1": "<0>Hoppla, etwas ist schiefgelaufen.", - "group_call_loader_failed_heading": "Anruf nicht gefunden", - "group_call_loader_failed_text": "Anrufe sind nun Ende-zu-Ende-verschlüsselt und müssen auf der Startseite erstellt werden. Damit stellen wir sicher, dass alle denselben Schlüssel verwenden.", "hangup_button_label": "Anruf beenden", "header_label": "Element Call-Startseite", "header_participants_label": "Teilnehmende", diff --git a/public/locales/et/app.json b/public/locales/et/app.json index 77c5a2fa..2ef7a76b 100644 --- a/public/locales/et/app.json +++ b/public/locales/et/app.json @@ -54,8 +54,6 @@ "disconnected_banner": "Võrguühendus serveriga on katkenud.", "full_screen_view_description": "<0>Kui saadad meile vealogid, siis on lihtsam vea põhjust otsida.", "full_screen_view_h1": "<0>Ohoo, midagi on nüüd katki.", - "group_call_loader_failed_heading": "Kõnet ei leidu", - "group_call_loader_failed_text": "Kõned on nüüd läbivalt krüptitud ning need pead looma kodulehelt. Sellega tagad, et kõik kasutavad samu krüptovõtmeid.", "hangup_button_label": "Lõpeta kõne", "header_participants_label": "Osalejad", "invite_modal": { diff --git a/public/locales/fr/app.json b/public/locales/fr/app.json index 20a46e65..b5ddd1e1 100644 --- a/public/locales/fr/app.json +++ b/public/locales/fr/app.json @@ -52,8 +52,6 @@ "disconnected_banner": "La connexion avec le serveur a été perdue.", "full_screen_view_description": "<0>Soumettre les journaux de débogage nous aidera à déterminer le problème.", "full_screen_view_h1": "<0>Oups, quelque chose s’est mal passé.", - "group_call_loader_failed_heading": "Appel non trouvé", - "group_call_loader_failed_text": "Les appels sont maintenant chiffrés de bout-en-bout et doivent être créés depuis la page d’accueil. Cela permet d’être sûr que tout le monde utilise la même clé de chiffrement.", "hangup_button_label": "Terminer l’appel", "header_label": "Accueil Element Call", "invite_modal": { diff --git a/public/locales/id/app.json b/public/locales/id/app.json index 82cd740e..ce5cee36 100644 --- a/public/locales/id/app.json +++ b/public/locales/id/app.json @@ -52,8 +52,6 @@ "disconnected_banner": "Koneksi ke server telah hilang.", "full_screen_view_description": "<0>Mengirim catatan pengawakutuan akan membantu kami melacak masalahnya.", "full_screen_view_h1": "<0>Aduh, ada yang salah.", - "group_call_loader_failed_heading": "Panggilan tidak ditemukan", - "group_call_loader_failed_text": "Panggilan sekarang terenkripsi secara ujung ke ujung dan harus dibuat dari laman beranda. Ini memastikan bahwa semuanya menggunakan kunci enkripsi yang sama.", "hangup_button_label": "Akhiri panggilan", "header_label": "Beranda Element Call", "header_participants_label": "Peserta", diff --git a/public/locales/it/app.json b/public/locales/it/app.json index 4dea5944..7819fd30 100644 --- a/public/locales/it/app.json +++ b/public/locales/it/app.json @@ -50,8 +50,6 @@ "disconnected_banner": "La connessione al server è stata persa.", "full_screen_view_description": "<0>L'invio di registri di debug ci aiuterà ad individuare il problema.", "full_screen_view_h1": "<0>Ops, qualcosa è andato storto.", - "group_call_loader_failed_heading": "Chiamata non trovata", - "group_call_loader_failed_text": "Le chiamate ora sono cifrate end-to-end e devono essere create dalla pagina principale. Ciò assicura che chiunque usi la stessa chiave di crittografia.", "hangup_button_label": "Termina chiamata", "header_label": "Inizio di Element Call", "header_participants_label": "Partecipanti", diff --git a/public/locales/pl/app.json b/public/locales/pl/app.json index ef77d3b0..47375d60 100644 --- a/public/locales/pl/app.json +++ b/public/locales/pl/app.json @@ -55,8 +55,6 @@ "disconnected_banner": "Utracono połączenie z serwerem.", "full_screen_view_description": "<0>Wysłanie dzienników debuggowania pomoże nam ustalić przyczynę problemu.", "full_screen_view_h1": "<0>Ojej, coś poszło nie tak.", - "group_call_loader_failed_heading": "Nie znaleziono połączenia", - "group_call_loader_failed_text": "Połączenia są teraz szyfrowane end-to-end i muszą zostać utworzone ze strony głównej. Pomaga to upewnić się, że każdy korzysta z tego samego klucza szyfrującego.", "hangup_button_label": "Zakończ połączenie", "header_label": "Strona główna Element Call", "header_participants_label": "Uczestnicy", diff --git a/public/locales/sk/app.json b/public/locales/sk/app.json index c3381bd2..7888c256 100644 --- a/public/locales/sk/app.json +++ b/public/locales/sk/app.json @@ -53,8 +53,6 @@ "disconnected_banner": "Spojenie so serverom sa stratilo.", "full_screen_view_description": "<0>Odoslanie záznamov ladenia nám pomôže nájsť problém.", "full_screen_view_h1": "<0>Hups, niečo sa pokazilo.", - "group_call_loader_failed_heading": "Hovor nebol nájdený", - "group_call_loader_failed_text": "Hovory sú teraz end-to-end šifrované a je potrebné ich vytvoriť z domovskej stránky. To pomáha zabezpečiť, aby všetci používali rovnaký šifrovací kľúč.", "hangup_button_label": "Ukončiť hovor", "header_label": "Domov Element Call", "header_participants_label": "Účastníci", diff --git a/public/locales/uk/app.json b/public/locales/uk/app.json index dc2dd176..dc2587ba 100644 --- a/public/locales/uk/app.json +++ b/public/locales/uk/app.json @@ -55,8 +55,6 @@ "disconnected_banner": "Втрачено зв'язок з сервером.", "full_screen_view_description": "<0>Надсилання журналів налагодження допоможе нам виявити проблему.", "full_screen_view_h1": "<0>Йой, щось пішло не за планом.", - "group_call_loader_failed_heading": "Виклик не знайдено", - "group_call_loader_failed_text": "Відтепер виклики захищено наскрізним шифруванням, і їх потрібно створювати з домашньої сторінки. Це допомагає переконатися, що всі користувачі використовують один і той самий ключ шифрування.", "hangup_button_label": "Завершити виклик", "header_label": "Домівка Element Call", "header_participants_label": "Учасники", diff --git a/public/locales/zh-Hans/app.json b/public/locales/zh-Hans/app.json index 15bb9e41..86a926c8 100644 --- a/public/locales/zh-Hans/app.json +++ b/public/locales/zh-Hans/app.json @@ -53,8 +53,6 @@ "disconnected_banner": "与服务器的连接中断。", "full_screen_view_description": "<0>提交日志以帮助我们修复问题。", "full_screen_view_h1": "<0>哎哟,出问题了。", - "group_call_loader_failed_heading": "未找到通话", - "group_call_loader_failed_text": "现在,通话是端对端加密的,需要从主页创建。这有助于确保每个人都使用相同的加密密钥。", "hangup_button_label": "通话结束", "header_label": "Element Call主页", "join_existing_call_modal": { diff --git a/public/locales/zh-Hant/app.json b/public/locales/zh-Hant/app.json index bdd52669..c6484d4a 100644 --- a/public/locales/zh-Hant/app.json +++ b/public/locales/zh-Hant/app.json @@ -55,8 +55,6 @@ "disconnected_banner": "到伺服器的連線已遺失。", "full_screen_view_description": "<0>送出除錯紀錄,可幫助我們修正問題。", "full_screen_view_h1": "<0>喔喔,有些地方怪怪的。", - "group_call_loader_failed_heading": "找不到通話", - "group_call_loader_failed_text": "通話現在是端對端加密的,必須從首頁建立。這有助於確保每個人都使用相同的加密金鑰。", "hangup_button_label": "結束通話", "header_label": "Element Call 首頁", "header_participants_label": "參與者", diff --git a/src/analytics/PosthogSpanProcessor.ts b/src/analytics/PosthogSpanProcessor.ts index 8904b3e1..59960c92 100644 --- a/src/analytics/PosthogSpanProcessor.ts +++ b/src/analytics/PosthogSpanProcessor.ts @@ -72,7 +72,7 @@ export class PosthogSpanProcessor implements SpanProcessor { try { return JSON.parse(data); } catch (e) { - logger.warn("Invalid prev call data", data); + logger.warn("Invalid prev call data", data, "error:", e); return null; } } diff --git a/src/home/useGroupCallRooms.ts b/src/home/useGroupCallRooms.ts index e4cfa109..4c481543 100644 --- a/src/home/useGroupCallRooms.ts +++ b/src/home/useGroupCallRooms.ts @@ -134,7 +134,7 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { useEffect(() => { function updateRooms(): void { - // We want to show all rooms that historically had a call and which we are (can become) part of. + // We want to show all rooms that historically had a call and which we are (or can become) part of. const rooms = client .getRooms() .filter(roomHasCallMembershipEvents) @@ -142,7 +142,6 @@ export function useGroupCallRooms(client: MatrixClient): GroupCallRoom[] { const sortedRooms = sortRooms(client, rooms); const items = sortedRooms.map((room) => { const session = client.matrixRTC.getRoomSession(room); - session.memberships; return { roomAlias: room.getCanonicalAlias() ?? undefined, roomName: room.name, diff --git a/src/livekit/useECConnectionState.ts b/src/livekit/useECConnectionState.ts index c2bd0aa7..9afb2578 100644 --- a/src/livekit/useECConnectionState.ts +++ b/src/livekit/useECConnectionState.ts @@ -115,6 +115,7 @@ async function doConnect( await connectAndPublish(livekitRoom, sfuConfig, preCreatedAudioTrack, []); } catch (e) { preCreatedAudioTrack?.stop(); + logger.warn("Stopped precreated audio tracks.", e); } } diff --git a/src/otel/OTelCallAbstractMediaStreamSpan.ts b/src/otel/OTelCallAbstractMediaStreamSpan.ts index c34f963c..3ecb6b28 100644 --- a/src/otel/OTelCallAbstractMediaStreamSpan.ts +++ b/src/otel/OTelCallAbstractMediaStreamSpan.ts @@ -67,7 +67,7 @@ export abstract class OTelCallAbstractMediaStreamSpan { }); } - public abstract update(data: Object): void; + public abstract update(data: object): void; public end(): void { this.trackSpans.forEach((tSpan) => { diff --git a/src/otel/ObjectFlattener.ts b/src/otel/ObjectFlattener.ts index 76de9e3a..6c0326bf 100644 --- a/src/otel/ObjectFlattener.ts +++ b/src/otel/ObjectFlattener.ts @@ -74,7 +74,7 @@ export class ObjectFlattener { } public static flattenObjectRecursive( - obj: Object, + obj: object, flatObject: Attributes, prefix: string, depth: number, diff --git a/src/room/VideoPreview.tsx b/src/room/VideoPreview.tsx index 5899a8bf..6a10c682 100644 --- a/src/room/VideoPreview.tsx +++ b/src/room/VideoPreview.tsx @@ -16,7 +16,6 @@ limitations under the License. import { useEffect, useMemo, useRef, FC, ReactNode, useCallback } from "react"; import useMeasure from "react-use-measure"; -import { ResizeObserver } from "@juggle/resize-observer"; import { usePreviewTracks } from "@livekit/components-react"; import { LocalVideoTrack, Track } from "livekit-client"; import classNames from "classnames"; @@ -51,7 +50,7 @@ export const VideoPreview: FC = ({ muteStates, children, }) => { - const [previewRef, previewBounds] = useMeasure({ polyfill: ResizeObserver }); + const [previewRef, previewBounds] = useMeasure(); const devices = useMediaDevices(); diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index 0b0365a7..bf6909fb 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -505,7 +505,9 @@ function tryInitStorage(): Promise { let indexedDB; try { indexedDB = window.indexedDB; - } catch (e) {} + } catch (e) { + logger.warn("Could not get indexDB from window.", e); + } if (indexedDB) { global.mx_rage_store = new IndexedDBLogStore( diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 9c181ccf..47ca9816 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -30,7 +30,10 @@ export class Setting { try { initialValue = JSON.parse(storedValue); } catch (e) { - logger.warn(`Invalid value stored for setting ${key}: ${storedValue}`); + logger.warn( + `Invalid value stored for setting ${key}: ${storedValue}.`, + e, + ); } } diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index 3a98b831..52d1ca21 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -18,9 +18,13 @@ import { ComponentProps, useCallback, useEffect, useState } from "react"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore import pako from "pako"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; -import { ClientEvent } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; +import { + ClientEvent, + Crypto, + MatrixClient, + MatrixEvent, +} from "matrix-js-sdk/src/matrix"; import { getLogsForReport } from "./rageshake"; import { useClient } from "../ClientContext"; @@ -35,6 +39,84 @@ const gzip = (text: string): Blob => { return new Blob([pako.gzip(buf)]); }; +/** + * Collects crypto related information. + */ +async function collectCryptoInfo( + cryptoApi: Crypto.CryptoApi, + body: FormData, +): Promise { + body.append("crypto_version", cryptoApi.getVersion()); + + const ownDeviceKeys = await cryptoApi.getOwnDeviceKeys(); + const keys = [ + `curve25519:${ownDeviceKeys.curve25519}`, + `ed25519:${ownDeviceKeys.ed25519}`, + ]; + + body.append("device_keys", keys.join(", ")); + + // add cross-signing status information + const crossSigningStatus = await cryptoApi.getCrossSigningStatus(); + + body.append( + "cross_signing_ready", + String(await cryptoApi.isCrossSigningReady()), + ); + body.append( + "cross_signing_key", + (await cryptoApi.getCrossSigningKeyId()) ?? "n/a", + ); + body.append( + "cross_signing_privkey_in_secret_storage", + String(crossSigningStatus.privateKeysInSecretStorage), + ); + + body.append( + "cross_signing_master_privkey_cached", + String(crossSigningStatus.privateKeysCachedLocally.masterKey), + ); + body.append( + "cross_signing_self_signing_privkey_cached", + String(crossSigningStatus.privateKeysCachedLocally.selfSigningKey), + ); + body.append( + "cross_signing_user_signing_privkey_cached", + String(crossSigningStatus.privateKeysCachedLocally.userSigningKey), + ); +} + +/** + * Collects information about secret storage and backup. + */ +async function collectRecoveryInfo( + client: MatrixClient, + cryptoApi: Crypto.CryptoApi, + body: FormData, +): Promise { + const secretStorage = client.secretStorage; + body.append( + "secret_storage_ready", + String(await cryptoApi.isSecretStorageReady()), + ); + body.append( + "secret_storage_key_in_account", + String(await secretStorage.hasKey()), + ); + + body.append( + "session_backup_key_in_secret_storage", + String(!!(await client.isKeyBackupKeyStored())), + ); + const sessionBackupKeyFromCache = + await cryptoApi.getSessionBackupPrivateKey(); + body.append("session_backup_key_cached", String(!!sessionBackupKeyFromCache)); + body.append( + "session_backup_key_well_formed", + String(sessionBackupKeyFromCache instanceof Uint8Array), + ); +} + interface RageShakeSubmitOptions { sendLogs: boolean; rageshakeRequestId?: string; @@ -85,7 +167,9 @@ export function useSubmitRageshake(): { try { // MDN claims broad support across browsers touchInput = String(window.matchMedia("(pointer: coarse)").matches); - } catch (e) {} + } catch (e) { + logger.warn("Could not get coarse pointer for rageshake submit.", e); + } let description = opts.rageshakeRequestId ? `Rageshake ${opts.rageshakeRequestId}` @@ -118,90 +202,10 @@ export function useSubmitRageshake(): { body.append("room_id", opts.roomId); } - if (client.isCryptoEnabled()) { - const keys = [`ed25519:${client.getDeviceEd25519Key()}`]; - if (client.getDeviceCurve25519Key) { - keys.push(`curve25519:${client.getDeviceCurve25519Key()}`); - } - body.append("device_keys", keys.join(", ")); - body.append("cross_signing_key", client.getCrossSigningId()!); - - // add cross-signing status information - const crossSigning = client.crypto!.crossSigningInfo; - const secretStorage = client.crypto!.secretStorage; - - body.append( - "cross_signing_ready", - String(await client.isCrossSigningReady()), - ); - body.append( - "cross_signing_supported_by_hs", - String( - await client.doesServerSupportUnstableFeature( - "org.matrix.e2e_cross_signing", - ), - ), - ); - body.append("cross_signing_key", crossSigning.getId()!); - body.append( - "cross_signing_privkey_in_secret_storage", - String( - !!(await crossSigning.isStoredInSecretStorage(secretStorage)), - ), - ); - - const pkCache = client.getCrossSigningCacheCallbacks(); - body.append( - "cross_signing_master_privkey_cached", - String( - !!( - pkCache?.getCrossSigningKeyCache && - (await pkCache.getCrossSigningKeyCache("master")) - ), - ), - ); - body.append( - "cross_signing_self_signing_privkey_cached", - String( - !!( - pkCache?.getCrossSigningKeyCache && - (await pkCache.getCrossSigningKeyCache("self_signing")) - ), - ), - ); - body.append( - "cross_signing_user_signing_privkey_cached", - String( - !!( - pkCache?.getCrossSigningKeyCache && - (await pkCache.getCrossSigningKeyCache("user_signing")) - ), - ), - ); - - body.append( - "secret_storage_ready", - String(await client.isSecretStorageReady()), - ); - body.append( - "secret_storage_key_in_account", - String(!!(await secretStorage.hasKey())), - ); - - body.append( - "session_backup_key_in_secret_storage", - String(!!(await client.isKeyBackupKeyStored())), - ); - const sessionBackupKeyFromCache = - await client.crypto!.getSessionBackupPrivateKey(); - body.append( - "session_backup_key_cached", - String(!!sessionBackupKeyFromCache), - ); - body.append( - "session_backup_key_well_formed", - String(sessionBackupKeyFromCache instanceof Uint8Array), - ); + const crypto = client.getCrypto(); + if (crypto) { + await collectCryptoInfo(crypto, body); + await collectRecoveryInfo(client, crypto, body); } } @@ -216,7 +220,9 @@ export function useSubmitRageshake(): { "storageManager_persisted", String(await navigator.storage.persisted()), ); - } catch (e) {} + } catch (e) { + logger.warn("coulr not get navigator peristed storage", e); + } } else if (document.hasStorageAccess) { // Safari try { @@ -224,7 +230,9 @@ export function useSubmitRageshake(): { "storageManager_persisted", String(await document.hasStorageAccess()), ); - } catch (e) {} + } catch (e) { + logger.warn("could not get storage access", e); + } } if (navigator.storage && navigator.storage.estimate) { @@ -244,7 +252,9 @@ export function useSubmitRageshake(): { ); }); } - } catch (e) {} + } catch (e) { + logger.warn("could not obatain storage estimate", e); + } } if (opts.sendLogs) { diff --git a/src/state/MediaViewModel.test.ts b/src/state/MediaViewModel.test.ts new file mode 100644 index 00000000..f65b7775 --- /dev/null +++ b/src/state/MediaViewModel.test.ts @@ -0,0 +1,132 @@ +/* +Copyright 2024 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { RoomMember } from "matrix-js-sdk/src/matrix"; +import { expect, test, vi } from "vitest"; +import { LocalParticipant, RemoteParticipant } from "livekit-client"; + +import { + LocalUserMediaViewModel, + RemoteUserMediaViewModel, +} from "./MediaViewModel"; +import { withTestScheduler } from "../utils/test"; + +function withLocal(continuation: (vm: LocalUserMediaViewModel) => void): void { + const member = {} as unknown as RoomMember; + const vm = new LocalUserMediaViewModel( + "a", + member, + {} as unknown as LocalParticipant, + true, + ); + try { + continuation(vm); + } finally { + vm.destroy(); + } +} + +function withRemote( + participant: Partial, + continuation: (vm: RemoteUserMediaViewModel) => void, +): void { + const member = {} as unknown as RoomMember; + const vm = new RemoteUserMediaViewModel( + "a", + member, + { setVolume() {}, ...participant } as RemoteParticipant, + true, + ); + try { + continuation(vm); + } finally { + vm.destroy(); + } +} + +test("set a participant's volume", () => { + const setVolumeSpy = vi.fn(); + withRemote({ setVolume: setVolumeSpy }, (vm) => + withTestScheduler(({ expectObservable, schedule }) => { + schedule("-a|", { + a() { + vm.setLocalVolume(0.8); + expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8); + }, + }); + expectObservable(vm.localVolume).toBe("ab", { a: 1, b: 0.8 }); + }), + ); +}); + +test("mute and unmute a participant", () => { + const setVolumeSpy = vi.fn(); + withRemote({ setVolume: setVolumeSpy }, (vm) => + withTestScheduler(({ expectObservable, schedule }) => { + schedule("-abc|", { + a() { + vm.toggleLocallyMuted(); + expect(setVolumeSpy).toHaveBeenLastCalledWith(0); + }, + b() { + vm.setLocalVolume(0.8); + expect(setVolumeSpy).toHaveBeenLastCalledWith(0); + }, + c() { + vm.toggleLocallyMuted(); + expect(setVolumeSpy).toHaveBeenLastCalledWith(0.8); + }, + }); + expectObservable(vm.locallyMuted).toBe("ab-c", { + a: false, + b: true, + c: false, + }); + }), + ); +}); + +test("toggle fit/contain for a participant's video", () => { + withRemote({}, (vm) => + withTestScheduler(({ expectObservable, schedule }) => { + schedule("-ab|", { + a: () => vm.toggleFitContain(), + b: () => vm.toggleFitContain(), + }); + expectObservable(vm.cropVideo).toBe("abc", { + a: true, + b: false, + c: true, + }); + }), + ); +}); + +test("local media remembers whether it should always be shown", () => { + withLocal((vm) => + withTestScheduler(({ expectObservable, schedule }) => { + schedule("-a|", { a: () => vm.setAlwaysShow(false) }); + expectObservable(vm.alwaysShow).toBe("ab", { a: true, b: false }); + }), + ); + // Next local media should start out *not* always shown + withLocal((vm) => + withTestScheduler(({ expectObservable, schedule }) => { + schedule("-a|", { a: () => vm.setAlwaysShow(true) }); + expectObservable(vm.alwaysShow).toBe("ab", { a: false, b: true }); + }), + ); +}); diff --git a/src/utils/matrix.ts b/src/utils/matrix.ts index 0ad1097b..3658d162 100644 --- a/src/utils/matrix.ts +++ b/src/utils/matrix.ts @@ -87,7 +87,9 @@ export async function initClient( let indexedDB: IDBFactory | undefined; try { indexedDB = window.indexedDB; - } catch (e) {} + } catch (e) { + logger.warn("Could not get indexDB from window.", e); + } // options we always pass to the client (stuff that we need in order to work) const baseOpts = { diff --git a/src/utils/test.ts b/src/utils/test.ts index f3d7874a..43329be0 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -1,5 +1,5 @@ /* -Copyright 2023 New Vector Ltd +Copyright 2023-2024 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,7 +13,9 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ -import { vi } from "vitest"; +import { map } from "rxjs"; +import { RunHelpers, TestScheduler } from "rxjs/testing"; +import { expect, vi } from "vitest"; export function withFakeTimers(continuation: () => void): void { vi.useFakeTimers(); @@ -23,3 +25,36 @@ export function withFakeTimers(continuation: () => void): void { vi.useRealTimers(); } } + +export interface OurRunHelpers extends RunHelpers { + /** + * Schedules a sequence of actions to happen, as described by a marble + * diagram. + */ + schedule: (marbles: string, actions: Record void>) => void; +} + +/** + * Run Observables with a scheduler that virtualizes time, for testing purposes. + */ +export function withTestScheduler( + continuation: (helpers: OurRunHelpers) => void, +): void { + new TestScheduler((actual, expected) => { + expect(actual).deep.equals(expected); + }).run((helpers) => + continuation({ + ...helpers, + schedule(marbles, actions) { + const actionsObservable = helpers + .cold(marbles) + .pipe(map((value) => actions[value]())); + const results = Object.fromEntries( + Object.keys(actions).map((value) => [value, undefined] as const), + ); + // Run the actions and verify that none of them error + helpers.expectObservable(actionsObservable).toBe(marbles, results); + }, + }), + ); +} diff --git a/tsconfig.json b/tsconfig.json index 0c970386..0f0c9c94 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -17,6 +17,7 @@ "resolveJsonModule": true, // Workaround for https://github.com/microsoft/TypeScript/issues/55132 "useDefineForClassFields": false, + "allowImportingTsExtensions": true, "paths": { // These imports within @livekit/components-core and // @livekit/components-react are broken under the "bundler" module diff --git a/vite.config.js b/vite.config.js index cad583da..adea0d96 100644 --- a/vite.config.js +++ b/vite.config.js @@ -76,7 +76,6 @@ export default defineConfig(({ mode }) => { "react-dom", "matrix-js-sdk", "react-use-measure", - "@juggle/resize-observer", // These packages modify the document based on some module-level global // state, and don't play nicely with duplicate copies of themselves // https://github.com/radix-ui/primitives/issues/1241#issuecomment-1847837850 diff --git a/yarn.lock b/yarn.lock index 4a9a8072..d2689fb3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1864,11 +1864,6 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" -"@juggle/resize-observer@^3.3.1": - version "3.4.0" - resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" - integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== - "@livekit/components-core@0.11.2", "@livekit/components-core@^0.11.0": version "0.11.2" resolved "https://registry.yarnpkg.com/@livekit/components-core/-/components-core-0.11.2.tgz#fded2e207155e4737ed52830d48b75ae2eaaf449" @@ -2988,85 +2983,85 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== -"@typescript-eslint/eslint-plugin@^7.0.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.18.0.tgz#b16d3cf3ee76bf572fdf511e79c248bdec619ea3" - integrity sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw== +"@typescript-eslint/eslint-plugin@^8.0.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz#726627fad16d41d20539637efee8c2329fe6be32" + integrity sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA== dependencies: "@eslint-community/regexpp" "^4.10.0" - "@typescript-eslint/scope-manager" "7.18.0" - "@typescript-eslint/type-utils" "7.18.0" - "@typescript-eslint/utils" "7.18.0" - "@typescript-eslint/visitor-keys" "7.18.0" + "@typescript-eslint/scope-manager" "8.3.0" + "@typescript-eslint/type-utils" "8.3.0" + "@typescript-eslint/utils" "8.3.0" + "@typescript-eslint/visitor-keys" "8.3.0" graphemer "^1.4.0" ignore "^5.3.1" natural-compare "^1.4.0" ts-api-utils "^1.3.0" -"@typescript-eslint/parser@^7.0.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-7.18.0.tgz#83928d0f1b7f4afa974098c64b5ce6f9051f96a0" - integrity sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg== +"@typescript-eslint/parser@^8.0.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.3.0.tgz#3c72c32bc909cb91ce3569e7d11d729ad84deafa" + integrity sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ== dependencies: - "@typescript-eslint/scope-manager" "7.18.0" - "@typescript-eslint/types" "7.18.0" - "@typescript-eslint/typescript-estree" "7.18.0" - "@typescript-eslint/visitor-keys" "7.18.0" + "@typescript-eslint/scope-manager" "8.3.0" + "@typescript-eslint/types" "8.3.0" + "@typescript-eslint/typescript-estree" "8.3.0" + "@typescript-eslint/visitor-keys" "8.3.0" debug "^4.3.4" -"@typescript-eslint/scope-manager@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-7.18.0.tgz#c928e7a9fc2c0b3ed92ab3112c614d6bd9951c83" - integrity sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA== +"@typescript-eslint/scope-manager@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz#834301d2e70baf924c26818b911bdc40086f7468" + integrity sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg== dependencies: - "@typescript-eslint/types" "7.18.0" - "@typescript-eslint/visitor-keys" "7.18.0" + "@typescript-eslint/types" "8.3.0" + "@typescript-eslint/visitor-keys" "8.3.0" -"@typescript-eslint/type-utils@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-7.18.0.tgz#2165ffaee00b1fbbdd2d40aa85232dab6998f53b" - integrity sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA== +"@typescript-eslint/type-utils@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz#c1ae6af8c21a27254321016b052af67ddb44a9ac" + integrity sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg== dependencies: - "@typescript-eslint/typescript-estree" "7.18.0" - "@typescript-eslint/utils" "7.18.0" + "@typescript-eslint/typescript-estree" "8.3.0" + "@typescript-eslint/utils" "8.3.0" debug "^4.3.4" ts-api-utils "^1.3.0" -"@typescript-eslint/types@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-7.18.0.tgz#b90a57ccdea71797ffffa0321e744f379ec838c9" - integrity sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ== +"@typescript-eslint/types@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-8.3.0.tgz#378e62447c2d7028236e55a81d3391026600563b" + integrity sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw== -"@typescript-eslint/typescript-estree@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-7.18.0.tgz#b5868d486c51ce8f312309ba79bdb9f331b37931" - integrity sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA== +"@typescript-eslint/typescript-estree@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz#3e3d38af101ba61a8568f034733b72bfc9f176b9" + integrity sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA== dependencies: - "@typescript-eslint/types" "7.18.0" - "@typescript-eslint/visitor-keys" "7.18.0" + "@typescript-eslint/types" "8.3.0" + "@typescript-eslint/visitor-keys" "8.3.0" debug "^4.3.4" - globby "^11.1.0" + fast-glob "^3.3.2" is-glob "^4.0.3" minimatch "^9.0.4" semver "^7.6.0" ts-api-utils "^1.3.0" -"@typescript-eslint/utils@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-7.18.0.tgz#bca01cde77f95fc6a8d5b0dbcbfb3d6ca4be451f" - integrity sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw== +"@typescript-eslint/utils@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-8.3.0.tgz#b10972319deac5959c7a7075d0cf2b5e1de7ec08" + integrity sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA== dependencies: "@eslint-community/eslint-utils" "^4.4.0" - "@typescript-eslint/scope-manager" "7.18.0" - "@typescript-eslint/types" "7.18.0" - "@typescript-eslint/typescript-estree" "7.18.0" + "@typescript-eslint/scope-manager" "8.3.0" + "@typescript-eslint/types" "8.3.0" + "@typescript-eslint/typescript-estree" "8.3.0" -"@typescript-eslint/visitor-keys@7.18.0": - version "7.18.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-7.18.0.tgz#0564629b6124d67607378d0f0332a0495b25e7d7" - integrity sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg== +"@typescript-eslint/visitor-keys@8.3.0": + version "8.3.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz#320d747d107af1eef1eb43fbc4ccdbddda13068b" + integrity sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA== dependencies: - "@typescript-eslint/types" "7.18.0" + "@typescript-eslint/types" "8.3.0" eslint-visitor-keys "^3.4.3" "@ungap/structured-clone@^1.2.0": @@ -3343,11 +3338,6 @@ array-includes@^3.1.7: get-intrinsic "^1.2.1" is-string "^1.0.7" -array-union@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" - integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== - array.prototype.findlast@^1.2.5: version "1.2.5" resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" @@ -4120,13 +4110,6 @@ dijkstrajs@^1.0.1: resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23" integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA== -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -4725,7 +4708,7 @@ fast-fifo@^1.3.2: resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" integrity sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ== -fast-glob@^3.2.9, fast-glob@^3.3.2: +fast-glob@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== @@ -5046,18 +5029,6 @@ globalthis@^1.0.3: define-properties "^1.2.1" gopd "^1.0.1" -globby@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" - integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== - dependencies: - array-union "^2.1.0" - dir-glob "^3.0.1" - fast-glob "^3.2.9" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^3.0.0" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -6011,7 +5982,7 @@ merge-stream@^2.0.0: resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== -merge2@^1.3.0, merge2@^1.4.1: +merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== @@ -7455,11 +7426,6 @@ signal-exit@^4.0.1, signal-exit@^4.1.0: resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - smol-toml@^1.1.4: version "1.3.0" resolved "https://registry.yarnpkg.com/smol-toml/-/smol-toml-1.3.0.tgz#5200e251fffadbb72570c84e9776d2a3eca48143"