From 785eca72895b65ecec7169d183d061abb25ef8a5 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 6 Jun 2022 22:33:13 +0200 Subject: [PATCH 01/29] typescript src/profile --- .../{ProfileModal.jsx => ProfileModal.tsx} | 18 +++++++++---- src/profile/{useProfile.js => useProfile.ts} | 25 ++++++++++++++++--- 2 files changed, 34 insertions(+), 9 deletions(-) rename src/profile/{ProfileModal.jsx => ProfileModal.tsx} (87%) rename src/profile/{useProfile.js => useProfile.ts} (83%) diff --git a/src/profile/ProfileModal.jsx b/src/profile/ProfileModal.tsx similarity index 87% rename from src/profile/ProfileModal.jsx rename to src/profile/ProfileModal.tsx index b0dee0e..5711ef1 100644 --- a/src/profile/ProfileModal.jsx +++ b/src/profile/ProfileModal.tsx @@ -15,6 +15,8 @@ limitations under the License. */ import React, { useCallback, useEffect, useState } from "react"; +import { MatrixClient } from "matrix-js-sdk"; + import { Button } from "../button"; import { useProfile } from "./useProfile"; import { FieldRow, InputField, ErrorMessage } from "../input/Input"; @@ -22,7 +24,12 @@ import { Modal, ModalContent } from "../Modal"; import { AvatarInputField } from "../input/AvatarInputField"; import styles from "./ProfileModal.module.css"; -export function ProfileModal({ client, ...rest }) { +interface Props { + client: MatrixClient; + onClose: () => {}; + [rest: string]: unknown; +} +export function ProfileModal({ client, ...rest }: Props) { const { onClose } = rest; const { success, @@ -51,12 +58,13 @@ export function ProfileModal({ client, ...rest }) { e.preventDefault(); const data = new FormData(e.target); const displayName = data.get("displayName"); - const avatar = data.get("avatar"); - + const avatar: File | string = data.get("avatar"); + const avatarSize = + typeof avatar == "string" ? avatar.length : avatar.size; saveProfile({ displayName, - avatar: avatar && avatar.size > 0 ? avatar : undefined, - removeAvatar: removeAvatar && (!avatar || avatar.size === 0), + avatar: avatar && avatarSize > 0 ? avatar : undefined, + removeAvatar: removeAvatar && (!avatar || avatarSize === 0), }); }, [saveProfile, removeAvatar] diff --git a/src/profile/useProfile.js b/src/profile/useProfile.ts similarity index 83% rename from src/profile/useProfile.js rename to src/profile/useProfile.ts index 2207dde..6ae5024 100644 --- a/src/profile/useProfile.js +++ b/src/profile/useProfile.ts @@ -14,9 +14,26 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixClient, User } from "matrix-js-sdk"; import { useState, useCallback, useEffect } from "react"; -export function useProfile(client) { +interface ProfileResult { + loading: boolean; + error: Error; + displayName: string; + avatarUrl: string; + saveProfile: ({ + displayName, + avatar, + removeAvatar, + }: { + displayName: string; + avatar: any; + removeAvatar: boolean; + }) => Promise; + success: boolean; +} +export function useProfile(client: MatrixClient): ProfileResult { const [{ loading, displayName, avatarUrl, error, success }, setState] = useState(() => { const user = client?.getUser(client.getUserId()); @@ -31,7 +48,7 @@ export function useProfile(client) { }); useEffect(() => { - const onChangeUser = (_event, { displayName, avatarUrl }) => { + const onChangeUser = (_event: any, { displayName, avatarUrl }: any) => { setState({ success: false, loading: false, @@ -41,7 +58,7 @@ export function useProfile(client) { }); }; - let user; + let user: User; if (client) { const userId = client.getUserId(); @@ -71,7 +88,7 @@ export function useProfile(client) { try { await client.setDisplayName(displayName); - let mxcAvatarUrl; + let mxcAvatarUrl: string; if (removeAvatar) { await client.setAvatarUrl(""); From 190c57e85359965de49845bae9437dacffa356d2 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 6 Jun 2022 22:42:48 +0200 Subject: [PATCH 02/29] typescript src/settings --- .../{SettingsModal.jsx => SettingsModal.tsx} | 24 ++- src/settings/{rageshake.js => rageshake.ts} | 156 ++++++++++++------ ...ubmit-rageshake.js => submit-rageshake.ts} | 63 +++++-- ...seMediaHandler.jsx => useMediaHandler.tsx} | 103 ++++++++---- 4 files changed, 236 insertions(+), 110 deletions(-) rename src/settings/{SettingsModal.jsx => SettingsModal.tsx} (89%) rename src/settings/{rageshake.js => rageshake.ts} (81%) rename src/settings/{submit-rageshake.js => submit-rageshake.ts} (85%) rename src/settings/{useMediaHandler.jsx => useMediaHandler.tsx} (70%) diff --git a/src/settings/SettingsModal.jsx b/src/settings/SettingsModal.tsx similarity index 89% rename from src/settings/SettingsModal.jsx rename to src/settings/SettingsModal.tsx index ad2aeef..0da14d6 100644 --- a/src/settings/SettingsModal.jsx +++ b/src/settings/SettingsModal.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /* Copyright 2022 Matrix.org Foundation C.I.C. @@ -15,6 +16,8 @@ limitations under the License. */ import React from "react"; +import { Item } from "@react-stately/collections"; + import { Modal } from "../Modal"; import styles from "./SettingsModal.module.css"; import { TabContainer, TabItem } from "../tabs/Tabs"; @@ -22,15 +25,23 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg"; import { ReactComponent as VideoIcon } from "../icons/Video.svg"; import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg"; import { SelectInput } from "../input/SelectInput"; -import { Item } from "@react-stately/collections"; -import { useMediaHandler } from "./useMediaHandler"; +import { + MediaHandlerContextInterface, + useMediaHandler, +} from "./useMediaHandler"; import { useSpatialAudio, useShowInspector } from "./useSetting"; import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; import { useDownloadDebugLog } from "./submit-rageshake"; import { Body } from "../typography/Typography"; -export const SettingsModal = (props) => { +interface Props { + setShowInspector: boolean; + showInspector: boolean; + [rest: string]: unknown; +} + +export const SettingsModal = (props: Props) => { const { audioInput, audioInputs, @@ -42,6 +53,7 @@ export const SettingsModal = (props) => { audioOutputs, setAudioOutput, } = useMediaHandler(); + const [spatialAudio, setSpatialAudio] = useSpatialAudio(); const [showInspector, setShowInspector] = useShowInspector(); @@ -90,7 +102,8 @@ export const SettingsModal = (props) => { label="Spatial audio (experimental)" type="checkbox" checked={spatialAudio} - onChange={(e) => setSpatialAudio(e.target.checked)} + // @ts-ignore + onChange={(event: Event) => setSpatialAudio(event.target.checked)} /> @@ -132,7 +145,8 @@ export const SettingsModal = (props) => { label="Show Call Inspector" type="checkbox" checked={showInspector} - onChange={(e) => setShowInspector(e.target.checked)} + // @ts-ignore + onChange={(e: Event) => setShowInspector(e.target.checked)} /> diff --git a/src/settings/rageshake.js b/src/settings/rageshake.ts similarity index 81% rename from src/settings/rageshake.js rename to src/settings/rageshake.ts index c9e855e..702ecb4 100644 --- a/src/settings/rageshake.js +++ b/src/settings/rageshake.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /* Copyright 2017 OpenMarket Ltd Copyright 2018 New Vector Ltd @@ -37,37 +38,60 @@ limitations under the License. // actually timestamps. We then purge the remaining logs. We also do this // purge on startup to prevent logs from accumulating. -// the frequency with which we flush to indexeddb import { logger } from "matrix-js-sdk/src/logger"; +// the frequency with which we flush to indexeddb const FLUSH_RATE_MS = 30 * 1000; // the length of log data we keep in indexeddb (and include in the reports) const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB // A class which monkey-patches the global console and stores log lines. + +interface LogEntry { + id: string; + lines: Array; + index: number; +} + +interface Cursor { + id: string; + ts: number; +} + +// interface CustomEventTarget extends EventTarget { +// result: Cursor; +// } export class ConsoleLogger { logs = ""; - monkeyPatch(consoleObj) { + monkeyPatch(consoleObj: Console): void { // Monkey-patch console logging - const consoleFunctionsToLevels = { + + const consoleFunctionsToLevels: { [level: string]: string } = { log: "I", info: "I", warn: "W", error: "E", }; - Object.keys(consoleFunctionsToLevels).forEach((fnName) => { - const level = consoleFunctionsToLevels[fnName]; - const originalFn = consoleObj[fnName].bind(consoleObj); - consoleObj[fnName] = (...args) => { - this.log(level, ...args); - originalFn(...args); - }; - }); - } - log(level, ...args) { + Object.keys(consoleFunctionsToLevels).forEach( + (fnName: "log" | "info" | "warn" | "error") => { + const level = consoleFunctionsToLevels[fnName]; + const originalFn = consoleObj[fnName].bind(consoleObj); + consoleObj[fnName] = (...args: unknown[]) => { + this.log(level, ...args); + originalFn(...args); + }; + } + ); + } + // these functions get overwritten by the monkey patch + error(...args: unknown[]): void {} + warn(...args: unknown[]): void {} + info(...args: unknown[]): void {} + + log(level: string, ...args: unknown[]): void { // We don't know what locale the user may be running so use ISO strings const ts = new Date().toISOString(); @@ -113,10 +137,10 @@ export class ConsoleLogger { /** * Retrieve log lines to flush to disk. - * @param {boolean} keepLogs True to not delete logs after flushing. + * @param {boolean} keepLogs True to not delete logs after flushing. Defaults to false. * @return {string} \n delimited log lines to flush. */ - flush(keepLogs) { + flush(keepLogs = false): string { // The ConsoleLogger doesn't care how these end up on disk, it just // flushes them to the caller. if (keepLogs) { @@ -131,11 +155,14 @@ export class ConsoleLogger { // A class which stores log lines in an IndexedDB instance. export class IndexedDBLogStore { index = 0; - db = null; - flushPromise = null; - flushAgainPromise = null; + db: IDBDatabase = null; + flushPromise: Promise = null; + flushAgainPromise: Promise = null; + indexedDB: IDBFactory; + logger: ConsoleLogger; + id: string; - constructor(indexedDB, logger) { + constructor(indexedDB: IDBFactory, logger: ConsoleLogger) { this.indexedDB = indexedDB; this.logger = logger; this.id = "instance-" + Math.random() + Date.now(); @@ -144,10 +171,10 @@ export class IndexedDBLogStore { /** * @return {Promise} Resolves when the store is ready. */ - connect() { + connect(): Promise { const req = this.indexedDB.open("logs"); - return new Promise((resolve, reject) => { - req.onsuccess = (event) => { + return new Promise((resolve, reject) => { + req.onsuccess = (event: Event) => { // @ts-ignore this.db = event.target.result; // Periodically flush logs to local storage / indexeddb @@ -155,16 +182,16 @@ export class IndexedDBLogStore { resolve(); }; - req.onerror = (event) => { + req.onerror = (event: Event) => { const err = // @ts-ignore "Failed to open log database: " + event.target.error.name; - logger.error(err); + this.logger.error(err); reject(new Error(err)); }; // First time: Setup the object store - req.onupgradeneeded = (event) => { + req.onupgradeneeded = (event: IDBVersionChangeEvent) => { // @ts-ignore const db = event.target.result; const logObjStore = db.createObjectStore("logs", { @@ -176,7 +203,7 @@ export class IndexedDBLogStore { logObjStore.createIndex("id", "id", { unique: false }); logObjStore.add( - this.generateLogEntry(new Date() + " ::: Log database was created.") + this.generateLogEntry([new Date() + " ::: Log database was created."]) ); const lastModifiedStore = db.createObjectStore("logslastmod", { @@ -206,7 +233,7 @@ export class IndexedDBLogStore { * * @return {Promise} Resolved when the logs have been flushed. */ - flush() { + flush(): Promise { // check if a flush() operation is ongoing if (this.flushPromise) { if (this.flushAgainPromise) { @@ -225,7 +252,7 @@ export class IndexedDBLogStore { } // there is no flush promise or there was but it has finished, so do // a brand new one, destroying the chain which may have been built up. - this.flushPromise = new Promise((resolve, reject) => { + this.flushPromise = new Promise((resolve, reject) => { if (!this.db) { // not connected yet or user rejected access for us to r/w to the db. reject(new Error("No connected database")); @@ -242,10 +269,11 @@ export class IndexedDBLogStore { resolve(); }; txn.onerror = (event) => { - logger.error("Failed to flush logs : ", event); + this.logger.error("Failed to flush logs : ", event); + // @ts-ignore reject(new Error("Failed to write logs: " + event.target.errorCode)); }; - objStore.add(this.generateLogEntry(lines)); + objStore.add(this.generateLogEntry(lines.split("\n"))); const lastModStore = txn.objectStore("logslastmod"); lastModStore.put(this.generateLastModifiedTime()); }).then(() => { @@ -264,12 +292,13 @@ export class IndexedDBLogStore { * log ID). The objects have said log ID in an "id" field and "lines" which * is a big string with all the new-line delimited logs. */ - async consume() { + + async consume(): Promise { const db = this.db; // Returns: a string representing the concatenated logs for this ID. // Stops adding log fragments when the size exceeds maxSize - function fetchLogs(id, maxSize) { + function fetchLogs(id: string, maxSize: number): Promise { const objectStore = db .transaction("logs", "readonly") .objectStore("logs"); @@ -280,34 +309,35 @@ export class IndexedDBLogStore { .openCursor(IDBKeyRange.only(id), "prev"); let lines = ""; query.onerror = (event) => { + // @ts-ignore reject(new Error("Query failed: " + event.target.errorCode)); }; query.onsuccess = (event) => { + // @ts-ignore const cursor = event.target.result; if (!cursor) { - resolve(lines); + resolve(lines.split("\n")); return; // end of results } lines = cursor.value.lines + lines; if (lines.length >= maxSize) { - resolve(lines); + resolve(lines.split("\n")); } else { cursor.continue(); } }; }); } - // Returns: A sorted array of log IDs. (newest first) - function fetchLogIds() { + function fetchLogIds(): Promise { // To gather all the log IDs, query for all records in logslastmod. const o = db .transaction("logslastmod", "readonly") .objectStore("logslastmod"); - return selectQuery(o, undefined, (cursor) => { + return selectQuery(o, undefined, (cursor: Cursor) => { return { - id: cursor.value.id, - ts: cursor.value.ts, + id: cursor.id, + ts: cursor.ts, }; }).then((res) => { // Sort IDs by timestamp (newest first) @@ -318,14 +348,14 @@ export class IndexedDBLogStore { .map((a) => a.id); }); } - - function deleteLogs(id) { + function deleteLogs(id: string): Promise { return new Promise((resolve, reject) => { const txn = db.transaction(["logs", "logslastmod"], "readwrite"); const o = txn.objectStore("logs"); // only load the key path, not the data which may be huge const query = o.index("id").openKeyCursor(IDBKeyRange.only(id)); query.onsuccess = (event) => { + // @ts-ignore const cursor = event.target.result; if (!cursor) { return; @@ -340,6 +370,7 @@ export class IndexedDBLogStore { reject( new Error( "Failed to delete logs for " + + // @ts-ignore `'${id}' : ${event.target.errorCode}` ) ); @@ -351,11 +382,14 @@ export class IndexedDBLogStore { } const allLogIds = await fetchLogIds(); - let removeLogIds = []; + let removeLogIds: string[] = []; const logs = []; let size = 0; for (let i = 0; i < allLogIds.length; i++) { - const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size); + const lines: string[] = await fetchLogs( + allLogIds[i], + MAX_LOG_SIZE - size + ); // always add the log file: fetchLogs will truncate once the maxSize we give it is // exceeded, so we'll go over the max but only by one fragment's worth. @@ -375,22 +409,22 @@ export class IndexedDBLogStore { } } if (removeLogIds.length > 0) { - logger.log("Removing logs: ", removeLogIds); + this.logger.log("Removing logs: ", removeLogIds); // Don't await this because it's non-fatal if we can't clean up // logs. Promise.all(removeLogIds.map((id) => deleteLogs(id))).then( () => { - logger.log(`Removed ${removeLogIds.length} old logs.`); + this.logger.log(`Removed ${removeLogIds.length} old logs.`); }, (err) => { - logger.error(err); + this.logger.error(err); } ); } return logs; } - generateLogEntry(lines) { + generateLogEntry(lines: string[]): LogEntry { return { id: this.id, lines: lines, @@ -416,18 +450,22 @@ export class IndexedDBLogStore { * @return {Promise} Resolves to an array of whatever you returned from * resultMapper. */ -function selectQuery(store, keyRange, resultMapper) { +function selectQuery( + store: IDBObjectStore, + keyRange: IDBKeyRange, + resultMapper: (arg: Cursor) => Cursor +): Promise { const query = store.openCursor(keyRange); return new Promise((resolve, reject) => { - const results = []; - query.onerror = (event) => { + const results: Cursor[] = []; + query.onerror = (event: Event) => { // @ts-ignore reject(new Error("Query failed: " + event.target.errorCode)); }; // collect results - query.onsuccess = (event) => { + query.onsuccess = (event: Event) => { // @ts-ignore - const cursor = event.target.result; + const cursor = event.target.result?.value; if (!cursor) { resolve(results); return; // end of results @@ -437,6 +475,16 @@ function selectQuery(store, keyRange, resultMapper) { }; }); } +declare global { + // eslint-disable-next-line no-var, camelcase + var mx_rage_store: IndexedDBLogStore; + // eslint-disable-next-line no-var, camelcase + var mx_rage_logger: ConsoleLogger; + // eslint-disable-next-line no-var, camelcase + var mx_rage_initPromise: Promise; + // eslint-disable-next-line no-var, camelcase + var mx_rage_initStoragePromise: Promise; +} /** * Configure rage shaking support for sending bug reports. @@ -445,7 +493,7 @@ function selectQuery(store, keyRange, resultMapper) { * be set up immediately for the logs. * @return {Promise} Resolves when set up. */ -export function init(setUpPersistence = true) { +export function init(setUpPersistence = true): Promise { if (global.mx_rage_initPromise) { return global.mx_rage_initPromise; } @@ -465,7 +513,7 @@ export function init(setUpPersistence = true) { * then this no-ops. * @return {Promise} Resolves when complete. */ -export function tryInitStorage() { +export function tryInitStorage(): Promise { if (global.mx_rage_initStoragePromise) { return global.mx_rage_initStoragePromise; } diff --git a/src/settings/submit-rageshake.js b/src/settings/submit-rageshake.ts similarity index 85% rename from src/settings/submit-rageshake.js rename to src/settings/submit-rageshake.ts index 3c10e57..0072b0a 100644 --- a/src/settings/submit-rageshake.js +++ b/src/settings/submit-rageshake.ts @@ -15,14 +15,30 @@ limitations under the License. */ import { useCallback, useContext, useEffect, useState } from "react"; -import { getLogsForReport } from "./rageshake"; import pako from "pako"; +import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk"; +import { OverlayTriggerState } from "@react-stately/overlays"; + +import { getLogsForReport } from "./rageshake"; import { useClient } from "../ClientContext"; import { InspectorContext } from "../room/GroupCallInspector"; import { useModalTriggerState } from "../Modal"; -export function useSubmitRageshake() { - const { client } = useClient(); +interface RageShakeSubmitOptions { + description: string; + roomId: string; + label: string; + sendLogs: boolean; + rageshakeRequestId: string; +} + +export function useSubmitRageshake(): { + submitRageshake: (opts: RageShakeSubmitOptions) => Promise; + sending: boolean; + sent: boolean; + error: Error; +} { + const client: MatrixClient = useClient().client; const [{ json }] = useContext(InspectorContext); const [{ sending, sent, error }, setState] = useState({ @@ -57,9 +73,12 @@ export function useSubmitRageshake() { opts.description || "User did not supply any additional text." ); body.append("app", "matrix-video-chat"); - body.append("version", import.meta.env.VITE_APP_VERSION || "dev"); + body.append( + "version", + (import.meta.env.VITE_APP_VERSION as string) || "dev" + ); body.append("user_agent", userAgent); - body.append("installed_pwa", false); + body.append("installed_pwa", "false"); body.append("touch_input", touchInput); if (client) { @@ -181,7 +200,11 @@ export function useSubmitRageshake() { if (navigator.storage && navigator.storage.estimate) { try { - const estimate = await navigator.storage.estimate(); + const estimate: { + quota?: number; + usage?: number; + usageDetails?: { [x: string]: unknown }; + } = await navigator.storage.estimate(); body.append("storageManager_quota", String(estimate.quota)); body.append("storageManager_usage", String(estimate.usage)); if (estimate.usageDetails) { @@ -200,7 +223,11 @@ export function useSubmitRageshake() { for (const entry of logs) { // encode as UTF-8 - let buf = new TextEncoder().encode(entry.lines); + let buf = new TextEncoder().encode( + typeof entry.lines == "string" + ? entry.lines + : entry.lines.join("\n") + ); // compress buf = pako.gzip(buf); @@ -225,7 +252,7 @@ export function useSubmitRageshake() { } await fetch( - import.meta.env.VITE_RAGESHAKE_SUBMIT_URL || + (import.meta.env.VITE_RAGESHAKE_SUBMIT_URL as string) || "https://element.io/bugreports/submit", { method: "POST", @@ -250,7 +277,7 @@ export function useSubmitRageshake() { }; } -export function useDownloadDebugLog() { +export function useDownloadDebugLog(): () => void { const [{ json }] = useContext(InspectorContext); const downloadDebugLog = useCallback(() => { @@ -271,7 +298,10 @@ export function useDownloadDebugLog() { return downloadDebugLog; } -export function useRageshakeRequest() { +export function useRageshakeRequest(): ( + roomId: string, + rageshakeRequestId: string +) => void { const { client } = useClient(); const sendRageshakeRequest = useCallback( @@ -286,13 +316,16 @@ export function useRageshakeRequest() { return sendRageshakeRequest; } -export function useRageshakeRequestModal(roomId) { +export function useRageshakeRequestModal(roomId: string): { + modalState: OverlayTriggerState; + modalProps: any; +} { const { modalState, modalProps } = useModalTriggerState(); - const { client } = useClient(); + const client: MatrixClient = useClient().client; const [rageshakeRequestId, setRageshakeRequestId] = useState(); useEffect(() => { - const onEvent = (event) => { + const onEvent = (event: MatrixEvent) => { const type = event.getType(); if ( @@ -305,10 +338,10 @@ export function useRageshakeRequestModal(roomId) { } }; - client.on("event", onEvent); + client.on(ClientEvent.Event, onEvent); return () => { - client.removeListener("event", onEvent); + client.removeListener(ClientEvent.Event, onEvent); }; }, [modalState.open, roomId, client, modalState]); diff --git a/src/settings/useMediaHandler.jsx b/src/settings/useMediaHandler.tsx similarity index 70% rename from src/settings/useMediaHandler.jsx rename to src/settings/useMediaHandler.tsx index a9c4a76..b8de0f7 100644 --- a/src/settings/useMediaHandler.jsx +++ b/src/settings/useMediaHandler.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment */ /* Copyright 2022 Matrix.org Foundation C.I.C. @@ -14,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { MatrixClient } from "matrix-js-sdk"; import React, { useState, useEffect, @@ -23,9 +25,27 @@ import React, { createContext, } from "react"; -const MediaHandlerContext = createContext(); +export interface MediaHandlerContextInterface { + audioInput: string; + audioInputs: MediaDeviceInfo[]; + setAudioInput: (deviceId: string) => void; + videoInput: string; + videoInputs: MediaDeviceInfo[]; + setVideoInput: (deviceId: string) => void; + audioOutput: string; + audioOutputs: MediaDeviceInfo[]; + setAudioOutput: (deviceId: string) => void; +} -function getMediaPreferences() { +const MediaHandlerContext = + createContext(undefined); + +interface MediaPreferences { + audioInput?: string; + videoInput?: string; + audioOutput?: string; +} +function getMediaPreferences(): MediaPreferences { const mediaPreferences = localStorage.getItem("matrix-media-preferences"); if (mediaPreferences) { @@ -39,8 +59,8 @@ function getMediaPreferences() { } } -function updateMediaPreferences(newPreferences) { - const oldPreferences = getMediaPreferences(newPreferences); +function updateMediaPreferences(newPreferences: MediaPreferences): void { + const oldPreferences = getMediaPreferences(); localStorage.setItem( "matrix-media-preferences", @@ -50,8 +70,11 @@ function updateMediaPreferences(newPreferences) { }) ); } - -export function MediaHandlerProvider({ client, children }) { +interface Props { + client: MatrixClient; + children: JSX.Element[]; +} +export function MediaHandlerProvider({ client, children }: Props): JSX.Element { const [ { audioInput, @@ -72,7 +95,9 @@ export function MediaHandlerProvider({ client, children }) { ); return { + // @ts-ignore audioInput: mediaHandler.audioInput, + // @ts-ignore videoInput: mediaHandler.videoInput, audioOutput: undefined, audioInputs: [], @@ -84,7 +109,7 @@ export function MediaHandlerProvider({ client, children }) { useEffect(() => { const mediaHandler = client.getMediaHandler(); - function updateDevices() { + function updateDevices(): void { navigator.mediaDevices.enumerateDevices().then((devices) => { const mediaPreferences = getMediaPreferences(); @@ -92,9 +117,10 @@ export function MediaHandlerProvider({ client, children }) { (device) => device.kind === "audioinput" ); const audioConnected = audioInputs.some( + // @ts-ignore (device) => device.deviceId === mediaHandler.audioInput ); - + // @ts-ignore let audioInput = mediaHandler.audioInput; if (!audioConnected && audioInputs.length > 0) { @@ -105,9 +131,11 @@ export function MediaHandlerProvider({ client, children }) { (device) => device.kind === "videoinput" ); const videoConnected = videoInputs.some( + // @ts-ignore (device) => device.deviceId === mediaHandler.videoInput ); + // @ts-ignore let videoInput = mediaHandler.videoInput; if (!videoConnected && videoInputs.length > 0) { @@ -129,7 +157,9 @@ export function MediaHandlerProvider({ client, children }) { } if ( + // @ts-ignore mediaHandler.videoInput !== videoInput || + // @ts-ignore mediaHandler.audioInput !== audioInput ) { mediaHandler.setMediaInputs(audioInput, videoInput); @@ -159,8 +189,8 @@ export function MediaHandlerProvider({ client, children }) { }; }, [client]); - const setAudioInput = useCallback( - (deviceId) => { + const setAudioInput: (deviceId: string) => void = useCallback( + (deviceId: string) => { updateMediaPreferences({ audioInput: deviceId }); setState((prevState) => ({ ...prevState, audioInput: deviceId })); client.getMediaHandler().setAudioInput(deviceId); @@ -168,7 +198,7 @@ export function MediaHandlerProvider({ client, children }) { [client] ); - const setVideoInput = useCallback( + const setVideoInput: (deviceId: string) => void = useCallback( (deviceId) => { updateMediaPreferences({ videoInput: deviceId }); setState((prevState) => ({ ...prevState, videoInput: deviceId })); @@ -177,35 +207,36 @@ export function MediaHandlerProvider({ client, children }) { [client] ); - const setAudioOutput = useCallback((deviceId) => { + const setAudioOutput: (deviceId: any) => void = useCallback((deviceId) => { updateMediaPreferences({ audioOutput: deviceId }); setState((prevState) => ({ ...prevState, audioOutput: deviceId })); }, []); - const context = useMemo( - () => ({ - audioInput, - audioInputs, - setAudioInput, - videoInput, - videoInputs, - setVideoInput, - audioOutput, - audioOutputs, - setAudioOutput, - }), - [ - audioInput, - audioInputs, - setAudioInput, - videoInput, - videoInputs, - setVideoInput, - audioOutput, - audioOutputs, - setAudioOutput, - ] - ); + const context: MediaHandlerContextInterface = + useMemo( + () => ({ + audioInput, + audioInputs, + setAudioInput, + videoInput, + videoInputs, + setVideoInput, + audioOutput, + audioOutputs, + setAudioOutput, + }), + [ + audioInput, + audioInputs, + setAudioInput, + videoInput, + videoInputs, + setVideoInput, + audioOutput, + audioOutputs, + setAudioOutput, + ] + ); return ( From 23098131b8ab5af8665e630d10e2e7a65a929a93 Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 8 Jun 2022 16:36:22 +0200 Subject: [PATCH 03/29] couple of cleanups ModalProps fixes LogEntry interface missing return promise --- src/settings/rageshake.ts | 28 +++++++++++++--------------- src/settings/submit-rageshake.ts | 27 +++++++++++++++++---------- 2 files changed, 30 insertions(+), 25 deletions(-) diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index 702ecb4..d895f53 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -50,8 +50,8 @@ const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB interface LogEntry { id: string; - lines: Array; - index: number; + lines: string; + index?: number; } interface Cursor { @@ -59,9 +59,6 @@ interface Cursor { ts: number; } -// interface CustomEventTarget extends EventTarget { -// result: Cursor; -// } export class ConsoleLogger { logs = ""; @@ -86,6 +83,7 @@ export class ConsoleLogger { } ); } + // these functions get overwritten by the monkey patch error(...args: unknown[]): void {} warn(...args: unknown[]): void {} @@ -203,7 +201,7 @@ export class IndexedDBLogStore { logObjStore.createIndex("id", "id", { unique: false }); logObjStore.add( - this.generateLogEntry([new Date() + " ::: Log database was created."]) + this.generateLogEntry(new Date() + " ::: Log database was created.") ); const lastModifiedStore = db.createObjectStore("logslastmod", { @@ -273,7 +271,7 @@ export class IndexedDBLogStore { // @ts-ignore reject(new Error("Failed to write logs: " + event.target.errorCode)); }; - objStore.add(this.generateLogEntry(lines.split("\n"))); + objStore.add(this.generateLogEntry(lines)); const lastModStore = txn.objectStore("logslastmod"); lastModStore.put(this.generateLastModifiedTime()); }).then(() => { @@ -424,7 +422,7 @@ export class IndexedDBLogStore { return logs; } - generateLogEntry(lines: string[]): LogEntry { + generateLogEntry(lines: string): LogEntry { return { id: this.id, lines: lines, @@ -432,7 +430,7 @@ export class IndexedDBLogStore { }; } - generateLastModifiedTime() { + generateLastModifiedTime(): Cursor { return { id: this.id, ts: Date.now(), @@ -539,7 +537,7 @@ export function tryInitStorage(): Promise { return global.mx_rage_initStoragePromise; } -export function flush() { +export function flush(): Promise { if (!global.mx_rage_store) { return; } @@ -550,7 +548,7 @@ export function flush() { * Clean up old logs. * @return {Promise} Resolves if cleaned logs. */ -export async function cleanup() { +export async function cleanup(): Promise { if (!global.mx_rage_store) { return; } @@ -560,9 +558,9 @@ export async function cleanup() { /** * Get a recent snapshot of the logs, ready for attaching to a bug report * - * @return {Array<{lines: string, id, string}>} list of log data + * @return {LogEntry[]} list of log data */ -export async function getLogsForReport() { +export async function getLogsForReport(): Promise { if (!global.mx_rage_logger) { throw new Error("No console logger, did you forget to call init()?"); } @@ -571,13 +569,13 @@ export async function getLogsForReport() { if (global.mx_rage_store) { // flush most recent logs await global.mx_rage_store.flush(); - return await global.mx_rage_store.consume(); + return (await global.mx_rage_store.consume()) as LogEntry[]; } else { return [ { lines: global.mx_rage_logger.flush(true), id: "-", }, - ]; + ] as LogEntry[]; } } diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index 0072b0a..c51ad7e 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -16,8 +16,10 @@ limitations under the License. import { useCallback, useContext, useEffect, useState } from "react"; import pako from "pako"; -import { ClientEvent, MatrixClient, MatrixEvent } from "matrix-js-sdk"; +import { MatrixEvent } from "matrix-js-sdk"; import { OverlayTriggerState } from "@react-stately/overlays"; +import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client"; +import { stringToBase } from "matrix-js-sdk/src/utils"; import { getLogsForReport } from "./rageshake"; import { useClient } from "../ClientContext"; @@ -223,12 +225,7 @@ export function useSubmitRageshake(): { for (const entry of logs) { // encode as UTF-8 - let buf = new TextEncoder().encode( - typeof entry.lines == "string" - ? entry.lines - : entry.lines.join("\n") - ); - + let buf = new TextEncoder().encode(entry.lines); // compress buf = pako.gzip(buf); @@ -315,14 +312,24 @@ export function useRageshakeRequest(): ( return sendRageshakeRequest; } +interface ModalProps { + isOpen: boolean; + onClose: () => void; +} +interface ModalPropsWithId extends ModalProps { + rageshakeRequestId: string; +} export function useRageshakeRequestModal(roomId: string): { modalState: OverlayTriggerState; - modalProps: any; + modalProps: ModalPropsWithId; } { - const { modalState, modalProps } = useModalTriggerState(); + const { modalState, modalProps } = useModalTriggerState() as { + modalState: OverlayTriggerState; + modalProps: ModalProps; + }; const client: MatrixClient = useClient().client; - const [rageshakeRequestId, setRageshakeRequestId] = useState(); + const [rageshakeRequestId, setRageshakeRequestId] = useState(); useEffect(() => { const onEvent = (event: MatrixEvent) => { From 0aa29f775cb523cec8d8af3a45f2de7ada1d6e5f Mon Sep 17 00:00:00 2001 From: Timo K Date: Wed, 8 Jun 2022 17:22:46 +0200 Subject: [PATCH 04/29] linter --- src/settings/SettingsModal.tsx | 5 +---- src/settings/submit-rageshake.ts | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index e94e113..69e5484 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -25,10 +25,7 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg"; import { ReactComponent as VideoIcon } from "../icons/Video.svg"; import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg"; import { SelectInput } from "../input/SelectInput"; -import { - MediaHandlerContextInterface, - useMediaHandler, -} from "./useMediaHandler"; +import { useMediaHandler } from "./useMediaHandler"; import { useSpatialAudio, useShowInspector } from "./useSetting"; import { FieldRow, InputField } from "../input/Input"; import { Button } from "../button"; diff --git a/src/settings/submit-rageshake.ts b/src/settings/submit-rageshake.ts index c51ad7e..20b0785 100644 --- a/src/settings/submit-rageshake.ts +++ b/src/settings/submit-rageshake.ts @@ -19,7 +19,6 @@ import pako from "pako"; import { MatrixEvent } from "matrix-js-sdk"; import { OverlayTriggerState } from "@react-stately/overlays"; import { MatrixClient, ClientEvent } from "matrix-js-sdk/src/client"; -import { stringToBase } from "matrix-js-sdk/src/utils"; import { getLogsForReport } from "./rageshake"; import { useClient } from "../ClientContext"; From 60ed54d6d3b4c67024eb320cb339c5c78d8038dd Mon Sep 17 00:00:00 2001 From: Timo K Date: Sat, 11 Jun 2022 14:28:30 +0200 Subject: [PATCH 05/29] change rageshake.ts to be more similar to the matrix-js version --- src/settings/rageshake.ts | 181 +++++++++++++++++++------------------- 1 file changed, 91 insertions(+), 90 deletions(-) diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index d895f53..885e491 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -39,6 +39,7 @@ limitations under the License. // purge on startup to prevent logs from accumulating. import { logger } from "matrix-js-sdk/src/logger"; +import { randomString } from "matrix-js-sdk/src/randomstring"; // the frequency with which we flush to indexeddb const FLUSH_RATE_MS = 30 * 1000; @@ -46,6 +47,11 @@ const FLUSH_RATE_MS = 30 * 1000; // the length of log data we keep in indexeddb (and include in the reports) const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB +type LogFunction = ( + ...args: (Error | DOMException | object | string)[] +) => void; +type LogFunctionName = "log" | "info" | "warn" | "error"; + // A class which monkey-patches the global console and stores log lines. interface LogEntry { @@ -54,42 +60,40 @@ interface LogEntry { index?: number; } -interface Cursor { - id: string; - ts: number; -} - export class ConsoleLogger { - logs = ""; + private logs = ""; + private originalFunctions: { [key in LogFunctionName]?: LogFunction } = {}; - monkeyPatch(consoleObj: Console): void { + public monkeyPatch(consoleObj: Console): void { // Monkey-patch console logging - - const consoleFunctionsToLevels: { [level: string]: string } = { + const consoleFunctionsToLevels = { log: "I", info: "I", warn: "W", error: "E", }; - - Object.keys(consoleFunctionsToLevels).forEach( - (fnName: "log" | "info" | "warn" | "error") => { - const level = consoleFunctionsToLevels[fnName]; - const originalFn = consoleObj[fnName].bind(consoleObj); - consoleObj[fnName] = (...args: unknown[]) => { - this.log(level, ...args); - originalFn(...args); - }; - } - ); + Object.keys(consoleFunctionsToLevels).forEach((fnName) => { + const level = consoleFunctionsToLevels[fnName]; + const originalFn = consoleObj[fnName].bind(consoleObj); + this.originalFunctions[fnName] = originalFn; + consoleObj[fnName] = (...args) => { + this.log(level, ...args); + originalFn(...args); + }; + }); } - // these functions get overwritten by the monkey patch - error(...args: unknown[]): void {} - warn(...args: unknown[]): void {} - info(...args: unknown[]): void {} + public bypassRageshake( + fnName: LogFunctionName, + ...args: (Error | DOMException | object | string)[] + ): void { + this.originalFunctions[fnName](...args); + } - log(level: string, ...args: unknown[]): void { + public log( + level: string, + ...args: (Error | DOMException | object | string)[] + ): void { // We don't know what locale the user may be running so use ISO strings const ts = new Date().toISOString(); @@ -100,21 +104,7 @@ export class ConsoleLogger { } else if (arg instanceof Error) { return arg.message + (arg.stack ? `\n${arg.stack}` : ""); } else if (typeof arg === "object") { - try { - return JSON.stringify(arg); - } catch (e) { - // In development, it can be useful to log complex cyclic - // objects to the console for inspection. This is fine for - // the console, but default `stringify` can't handle that. - // We workaround this by using a special replacer function - // to only log values of the root object and avoid cycles. - return JSON.stringify(arg, (key, value) => { - if (key && typeof value === "object") { - return ""; - } - return value; - }); - } + return JSON.stringify(arg, getCircularReplacer()); } else { return arg; } @@ -135,10 +125,10 @@ export class ConsoleLogger { /** * Retrieve log lines to flush to disk. - * @param {boolean} keepLogs True to not delete logs after flushing. Defaults to false. + * @param {boolean} keepLogs True to not delete logs after flushing. * @return {string} \n delimited log lines to flush. */ - flush(keepLogs = false): string { + public flush(keepLogs?: boolean): string { // The ConsoleLogger doesn't care how these end up on disk, it just // flushes them to the caller. if (keepLogs) { @@ -152,26 +142,22 @@ export class ConsoleLogger { // A class which stores log lines in an IndexedDB instance. export class IndexedDBLogStore { - index = 0; - db: IDBDatabase = null; - flushPromise: Promise = null; - flushAgainPromise: Promise = null; - indexedDB: IDBFactory; - logger: ConsoleLogger; - id: string; + private index = 0; + private db: IDBDatabase = null; + private flushPromise: Promise = null; + private flushAgainPromise: Promise = null; + private id: string; - constructor(indexedDB: IDBFactory, logger: ConsoleLogger) { - this.indexedDB = indexedDB; - this.logger = logger; - this.id = "instance-" + Math.random() + Date.now(); + constructor(private indexedDB: IDBFactory, private logger: ConsoleLogger) { + this.id = "instance-" + randomString(16); } /** * @return {Promise} Resolves when the store is ready. */ - connect(): Promise { + public connect(): Promise { const req = this.indexedDB.open("logs"); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { req.onsuccess = (event: Event) => { // @ts-ignore this.db = event.target.result; @@ -180,16 +166,16 @@ export class IndexedDBLogStore { resolve(); }; - req.onerror = (event: Event) => { + req.onerror = (event) => { const err = // @ts-ignore "Failed to open log database: " + event.target.error.name; - this.logger.error(err); + logger.error(err); reject(new Error(err)); }; // First time: Setup the object store - req.onupgradeneeded = (event: IDBVersionChangeEvent) => { + req.onupgradeneeded = (event) => { // @ts-ignore const db = event.target.result; const logObjStore = db.createObjectStore("logs", { @@ -231,7 +217,7 @@ export class IndexedDBLogStore { * * @return {Promise} Resolved when the logs have been flushed. */ - flush(): Promise { + public flush(): Promise { // check if a flush() operation is ongoing if (this.flushPromise) { if (this.flushAgainPromise) { @@ -267,7 +253,7 @@ export class IndexedDBLogStore { resolve(); }; txn.onerror = (event) => { - this.logger.error("Failed to flush logs : ", event); + logger.error("Failed to flush logs : ", event); // @ts-ignore reject(new Error("Failed to write logs: " + event.target.errorCode)); }; @@ -290,13 +276,12 @@ export class IndexedDBLogStore { * log ID). The objects have said log ID in an "id" field and "lines" which * is a big string with all the new-line delimited logs. */ - - async consume(): Promise { + public async consume(): Promise { const db = this.db; // Returns: a string representing the concatenated logs for this ID. // Stops adding log fragments when the size exceeds maxSize - function fetchLogs(id: string, maxSize: number): Promise { + function fetchLogs(id: string, maxSize: number): Promise { const objectStore = db .transaction("logs", "readonly") .objectStore("logs"); @@ -314,28 +299,29 @@ export class IndexedDBLogStore { // @ts-ignore const cursor = event.target.result; if (!cursor) { - resolve(lines.split("\n")); + resolve(lines); return; // end of results } lines = cursor.value.lines + lines; if (lines.length >= maxSize) { - resolve(lines.split("\n")); + resolve(lines); } else { cursor.continue(); } }; }); } + // Returns: A sorted array of log IDs. (newest first) function fetchLogIds(): Promise { // To gather all the log IDs, query for all records in logslastmod. const o = db .transaction("logslastmod", "readonly") .objectStore("logslastmod"); - return selectQuery(o, undefined, (cursor: Cursor) => { + return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => { return { - id: cursor.id, - ts: cursor.ts, + id: cursor.value.id, + ts: cursor.value.ts, }; }).then((res) => { // Sort IDs by timestamp (newest first) @@ -346,8 +332,9 @@ export class IndexedDBLogStore { .map((a) => a.id); }); } - function deleteLogs(id: string): Promise { - return new Promise((resolve, reject) => { + + function deleteLogs(id: number): Promise { + return new Promise((resolve, reject) => { const txn = db.transaction(["logs", "logslastmod"], "readwrite"); const o = txn.objectStore("logs"); // only load the key path, not the data which may be huge @@ -380,14 +367,11 @@ export class IndexedDBLogStore { } const allLogIds = await fetchLogIds(); - let removeLogIds: string[] = []; - const logs = []; + let removeLogIds = []; + const logs: LogEntry[] = []; let size = 0; for (let i = 0; i < allLogIds.length; i++) { - const lines: string[] = await fetchLogs( - allLogIds[i], - MAX_LOG_SIZE - size - ); + const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size); // always add the log file: fetchLogs will truncate once the maxSize we give it is // exceeded, so we'll go over the max but only by one fragment's worth. @@ -407,22 +391,22 @@ export class IndexedDBLogStore { } } if (removeLogIds.length > 0) { - this.logger.log("Removing logs: ", removeLogIds); + logger.log("Removing logs: ", removeLogIds); // Don't await this because it's non-fatal if we can't clean up // logs. Promise.all(removeLogIds.map((id) => deleteLogs(id))).then( () => { - this.logger.log(`Removed ${removeLogIds.length} old logs.`); + logger.log(`Removed ${removeLogIds.length} old logs.`); }, (err) => { - this.logger.error(err); + logger.error(err); } ); } return logs; } - generateLogEntry(lines: string): LogEntry { + private generateLogEntry(lines: string): LogEntry { return { id: this.id, lines: lines, @@ -430,7 +414,7 @@ export class IndexedDBLogStore { }; } - generateLastModifiedTime(): Cursor { + private generateLastModifiedTime(): { id: string; ts: number } { return { id: this.id, ts: Date.now(), @@ -448,22 +432,22 @@ export class IndexedDBLogStore { * @return {Promise} Resolves to an array of whatever you returned from * resultMapper. */ -function selectQuery( +function selectQuery( store: IDBObjectStore, keyRange: IDBKeyRange, - resultMapper: (arg: Cursor) => Cursor -): Promise { + resultMapper: (cursor: IDBCursorWithValue) => T +): Promise { const query = store.openCursor(keyRange); return new Promise((resolve, reject) => { - const results: Cursor[] = []; - query.onerror = (event: Event) => { + const results = []; + query.onerror = (event) => { // @ts-ignore reject(new Error("Query failed: " + event.target.errorCode)); }; // collect results - query.onsuccess = (event: Event) => { + query.onsuccess = (event) => { // @ts-ignore - const cursor = event.target.result?.value; + const cursor = event.target.result; if (!cursor) { resolve(results); return; // end of results @@ -569,13 +553,30 @@ export async function getLogsForReport(): Promise { if (global.mx_rage_store) { // flush most recent logs await global.mx_rage_store.flush(); - return (await global.mx_rage_store.consume()) as LogEntry[]; + return global.mx_rage_store.consume(); } else { return [ { lines: global.mx_rage_logger.flush(true), id: "-", }, - ] as LogEntry[]; + ]; } } + +type StringifyReplacer = (this: any, key: string, value: any) => any; + +// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references +// Injects `<$ cycle-trimmed $>` wherever it cuts a cyclical object relationship +const getCircularReplacer = (): StringifyReplacer => { + const seen = new WeakSet(); + return (key: string, value: any): any => { + if (typeof value === "object" && value !== null) { + if (seen.has(value)) { + return "<$ cycle-trimmed $>"; + } + seen.add(value); + } + return value; + }; +}; From 9b2e99c559c941c681bc12770ab1140e137e272a Mon Sep 17 00:00:00 2001 From: Timo K Date: Sat, 11 Jun 2022 14:28:54 +0200 Subject: [PATCH 06/29] use React.ChangeEvent in SettingsModal --- src/settings/SettingsModal.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/settings/SettingsModal.tsx b/src/settings/SettingsModal.tsx index 69e5484..1e45d55 100644 --- a/src/settings/SettingsModal.tsx +++ b/src/settings/SettingsModal.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/ban-ts-comment */ /* Copyright 2022 Matrix.org Foundation C.I.C. @@ -100,8 +99,9 @@ export const SettingsModal = (props: Props) => { type="checkbox" checked={spatialAudio} description="This will make a speaker's audio seem as if it is coming from where their tile is positioned on screen. (Experimental feature: this may impact the stability of audio.)" - // @ts-ignore - onChange={(event: Event) => setSpatialAudio(event.target.checked)} + onChange={(event: React.ChangeEvent) => + setSpatialAudio(event.target.checked) + } /> @@ -143,8 +143,9 @@ export const SettingsModal = (props: Props) => { label="Show Call Inspector" type="checkbox" checked={showInspector} - // @ts-ignore - onChange={(e: Event) => setShowInspector(e.target.checked)} + onChange={(e: React.ChangeEvent) => + setShowInspector(e.target.checked) + } /> From 885e93394829b53dc6316e6cd459527ebda8ef4c Mon Sep 17 00:00:00 2001 From: Timo K Date: Sat, 11 Jun 2022 14:29:26 +0200 Subject: [PATCH 07/29] fixes in useMediaHandler --- src/settings/useMediaHandler.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/settings/useMediaHandler.tsx b/src/settings/useMediaHandler.tsx index b8de0f7..bd2acd3 100644 --- a/src/settings/useMediaHandler.tsx +++ b/src/settings/useMediaHandler.tsx @@ -16,6 +16,7 @@ limitations under the License. */ import { MatrixClient } from "matrix-js-sdk"; +import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler"; import React, { useState, useEffect, @@ -95,9 +96,9 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { ); return { - // @ts-ignore + // @ts-ignore, ignore that audioInput is a private members of mediaHandler audioInput: mediaHandler.audioInput, - // @ts-ignore + // @ts-ignore, ignore that videoInput is a private members of mediaHandler videoInput: mediaHandler.videoInput, audioOutput: undefined, audioInputs: [], @@ -179,11 +180,14 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { } updateDevices(); - mediaHandler.on("local_streams_changed", updateDevices); + mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, updateDevices); navigator.mediaDevices.addEventListener("devicechange", updateDevices); return () => { - mediaHandler.removeListener("local_streams_changed", updateDevices); + mediaHandler.removeListener( + MediaHandlerEvent.LocalStreamsChanged, + updateDevices + ); navigator.mediaDevices.removeEventListener("devicechange", updateDevices); mediaHandler.stopAllStreams(); }; @@ -207,7 +211,7 @@ export function MediaHandlerProvider({ client, children }: Props): JSX.Element { [client] ); - const setAudioOutput: (deviceId: any) => void = useCallback((deviceId) => { + const setAudioOutput: (deviceId: string) => void = useCallback((deviceId) => { updateMediaPreferences({ audioOutput: deviceId }); setState((prevState) => ({ ...prevState, audioOutput: deviceId })); }, []); From 46f1f0f8e94e402d86185d7f38a4a4514ea85938 Mon Sep 17 00:00:00 2001 From: Timo K Date: Sat, 11 Jun 2022 14:32:25 +0200 Subject: [PATCH 08/29] remove explicit any --- src/settings/rageshake.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/settings/rageshake.ts b/src/settings/rageshake.ts index 885e491..72b5068 100644 --- a/src/settings/rageshake.ts +++ b/src/settings/rageshake.ts @@ -564,13 +564,17 @@ export async function getLogsForReport(): Promise { } } -type StringifyReplacer = (this: any, key: string, value: any) => any; +type StringifyReplacer = ( + this: unknown, + key: string, + value: unknown +) => unknown; // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references // Injects `<$ cycle-trimmed $>` wherever it cuts a cyclical object relationship const getCircularReplacer = (): StringifyReplacer => { const seen = new WeakSet(); - return (key: string, value: any): any => { + return (key: string, value: unknown): unknown => { if (typeof value === "object" && value !== null) { if (seen.has(value)) { return "<$ cycle-trimmed $>"; From 17a31e090417acf84f2dde9078796761523d2d5d Mon Sep 17 00:00:00 2001 From: Timo K Date: Sat, 11 Jun 2022 15:14:00 +0200 Subject: [PATCH 09/29] typing profile folder --- src/profile/ProfileModal.tsx | 8 ++++- src/profile/useProfile.ts | 68 +++++++++++++++++++++++------------- 2 files changed, 50 insertions(+), 26 deletions(-) diff --git a/src/profile/ProfileModal.tsx b/src/profile/ProfileModal.tsx index 5711ef1..26b99e9 100644 --- a/src/profile/ProfileModal.tsx +++ b/src/profile/ProfileModal.tsx @@ -57,10 +57,16 @@ export function ProfileModal({ client, ...rest }: Props) { (e) => { e.preventDefault(); const data = new FormData(e.target); - const displayName = data.get("displayName"); + const displayNameDataEntry = data.get("displayName"); const avatar: File | string = data.get("avatar"); + const avatarSize = typeof avatar == "string" ? avatar.length : avatar.size; + const displayName = + typeof displayNameDataEntry == "string" + ? displayNameDataEntry + : displayNameDataEntry.name; + saveProfile({ displayName, avatar: avatar && avatarSize > 0 ? avatar : undefined, diff --git a/src/profile/useProfile.ts b/src/profile/useProfile.ts index 6ae5024..4eadcd8 100644 --- a/src/profile/useProfile.ts +++ b/src/profile/useProfile.ts @@ -14,28 +14,36 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { MatrixClient, User } from "matrix-js-sdk"; +import { + FileType, + MatrixClient, + MatrixEvent, + User, + UserEvent, +} from "matrix-js-sdk"; import { useState, useCallback, useEffect } from "react"; -interface ProfileResult { - loading: boolean; - error: Error; +interface ProfileLoadState { + success?: boolean; + loading?: boolean; displayName: string; avatarUrl: string; - saveProfile: ({ - displayName, - avatar, - removeAvatar, - }: { - displayName: string; - avatar: any; - removeAvatar: boolean; - }) => Promise; - success: boolean; + error?: Error; } -export function useProfile(client: MatrixClient): ProfileResult { + +type ProfileSaveCallback = ({ + displayName, + avatar, + removeAvatar, +}: { + displayName: string; + avatar: FileType; + removeAvatar: boolean; +}) => Promise; + +export function useProfile(client: MatrixClient) { const [{ loading, displayName, avatarUrl, error, success }, setState] = - useState(() => { + useState(() => { const user = client?.getUser(client.getUserId()); return { @@ -48,7 +56,10 @@ export function useProfile(client: MatrixClient): ProfileResult { }); useEffect(() => { - const onChangeUser = (_event: any, { displayName, avatarUrl }: any) => { + const onChangeUser = ( + _event: MatrixEvent, + { displayName, avatarUrl }: User + ) => { setState({ success: false, loading: false, @@ -63,19 +74,19 @@ export function useProfile(client: MatrixClient): ProfileResult { if (client) { const userId = client.getUserId(); user = client.getUser(userId); - user.on("User.displayName", onChangeUser); - user.on("User.avatarUrl", onChangeUser); + user.on(UserEvent.DisplayName, onChangeUser); + user.on(UserEvent.AvatarUrl, onChangeUser); } return () => { if (user) { - user.removeListener("User.displayName", onChangeUser); - user.removeListener("User.avatarUrl", onChangeUser); + user.removeListener(UserEvent.DisplayName, onChangeUser); + user.removeListener(UserEvent.AvatarUrl, onChangeUser); } }; }, [client]); - const saveProfile = useCallback( + const saveProfile = useCallback( async ({ displayName, avatar, removeAvatar }) => { if (client) { setState((prev) => ({ @@ -104,11 +115,11 @@ export function useProfile(client: MatrixClient): ProfileResult { loading: false, success: true, })); - } catch (error) { + } catch (error: unknown) { setState((prev) => ({ ...prev, loading: false, - error, + error: error instanceof Error ? error : Error(error as string), success: false, })); } @@ -119,5 +130,12 @@ export function useProfile(client: MatrixClient): ProfileResult { [client] ); - return { loading, error, displayName, avatarUrl, saveProfile, success }; + return { + loading, + error, + displayName, + avatarUrl, + saveProfile, + success, + }; } From ee43fcc91fffeecafb618a6b73082d3a8a01b937 Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Mon, 13 Jun 2022 13:31:44 -0400 Subject: [PATCH 10/29] Make AEC work with spatial audio on Chrome --- package.json | 1 + src/room/InCallView.jsx | 9 +-- src/video-grid/VideoTileContainer.jsx | 2 + src/video-grid/useMediaStream.js | 93 ++++++++++++++++++++++++--- yarn.lock | 5 ++ 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 4f0309e..9c4b8fc 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "react-router-dom": "^5.2.0", "react-use-clipboard": "^1.0.7", "react-use-measure": "^2.1.1", + "sdp-transform": "^2.14.1", "unique-names-generator": "^4.6.0" }, "devDependencies": { diff --git a/src/room/InCallView.jsx b/src/room/InCallView.jsx index de9f5bc..4a4c1cb 100644 --- a/src/room/InCallView.jsx +++ b/src/room/InCallView.jsx @@ -42,6 +42,7 @@ import { usePreventScroll } from "@react-aria/overlays"; import { useMediaHandler } from "../settings/useMediaHandler"; import { useShowInspector } from "../settings/useSetting"; import { useModalTriggerState } from "../Modal"; +import { useAudioContext } from "../video-grid/useMediaStream"; const canScreenshare = "getDisplayMedia" in navigator.mediaDevices; // There is currently a bug in Safari our our code with cloning and sending MediaStreams @@ -70,12 +71,10 @@ export function InCallView({ usePreventScroll(); const [layout, setLayout] = useVideoGridLayout(screenshareFeeds.length > 0); + const [audioContext, audioDestination, audioRef] = useAudioContext(); const { audioOutput } = useMediaHandler(); const [showInspector] = useShowInspector(); - const audioContext = useRef(); - if (!audioContext.current) audioContext.current = new AudioContext(); - const { modalState: feedbackModalState, modalProps: feedbackModalProps } = useModalTriggerState(); @@ -139,6 +138,7 @@ export function InCallView({ return (
+