From 190c57e85359965de49845bae9437dacffa356d2 Mon Sep 17 00:00:00 2001 From: Timo K Date: Mon, 6 Jun 2022 22:42:48 +0200 Subject: [PATCH] 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 (