Merge pull request #387 from toger5/ts_settings

typescript `src/settings`
This commit is contained in:
Timo 2022-06-28 12:08:05 +02:00 committed by GitHub
commit 05e786e3d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 247 additions and 110 deletions

View file

@ -15,6 +15,8 @@ limitations under the License.
*/ */
import React from "react"; import React from "react";
import { Item } from "@react-stately/collections";
import { Modal } from "../Modal"; import { Modal } from "../Modal";
import styles from "./SettingsModal.module.css"; import styles from "./SettingsModal.module.css";
import { TabContainer, TabItem } from "../tabs/Tabs"; import { TabContainer, TabItem } from "../tabs/Tabs";
@ -22,7 +24,6 @@ import { ReactComponent as AudioIcon } from "../icons/Audio.svg";
import { ReactComponent as VideoIcon } from "../icons/Video.svg"; import { ReactComponent as VideoIcon } from "../icons/Video.svg";
import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg"; import { ReactComponent as DeveloperIcon } from "../icons/Developer.svg";
import { SelectInput } from "../input/SelectInput"; import { SelectInput } from "../input/SelectInput";
import { Item } from "@react-stately/collections";
import { useMediaHandler } from "./useMediaHandler"; import { useMediaHandler } from "./useMediaHandler";
import { useSpatialAudio, useShowInspector } from "./useSetting"; import { useSpatialAudio, useShowInspector } from "./useSetting";
import { FieldRow, InputField } from "../input/Input"; import { FieldRow, InputField } from "../input/Input";
@ -30,7 +31,13 @@ import { Button } from "../button";
import { useDownloadDebugLog } from "./submit-rageshake"; import { useDownloadDebugLog } from "./submit-rageshake";
import { Body } from "../typography/Typography"; import { Body } from "../typography/Typography";
export const SettingsModal = (props) => { interface Props {
setShowInspector: boolean;
showInspector: boolean;
[rest: string]: unknown;
}
export const SettingsModal = (props: Props) => {
const { const {
audioInput, audioInput,
audioInputs, audioInputs,
@ -42,6 +49,7 @@ export const SettingsModal = (props) => {
audioOutputs, audioOutputs,
setAudioOutput, setAudioOutput,
} = useMediaHandler(); } = useMediaHandler();
const [spatialAudio, setSpatialAudio] = useSpatialAudio(); const [spatialAudio, setSpatialAudio] = useSpatialAudio();
const [showInspector, setShowInspector] = useShowInspector(); const [showInspector, setShowInspector] = useShowInspector();
@ -91,7 +99,9 @@ export const SettingsModal = (props) => {
type="checkbox" type="checkbox"
checked={spatialAudio} 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.)" 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.)"
onChange={(e) => setSpatialAudio(e.target.checked)} onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
setSpatialAudio(event.target.checked)
}
/> />
</FieldRow> </FieldRow>
</TabItem> </TabItem>
@ -133,7 +143,9 @@ export const SettingsModal = (props) => {
label="Show Call Inspector" label="Show Call Inspector"
type="checkbox" type="checkbox"
checked={showInspector} checked={showInspector}
onChange={(e) => setShowInspector(e.target.checked)} onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setShowInspector(e.target.checked)
}
/> />
</FieldRow> </FieldRow>
<FieldRow> <FieldRow>

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* /*
Copyright 2017 OpenMarket Ltd Copyright 2017 OpenMarket Ltd
Copyright 2018 New Vector Ltd Copyright 2018 New Vector Ltd
@ -37,19 +38,33 @@ limitations under the License.
// actually timestamps. We then purge the remaining logs. We also do this // actually timestamps. We then purge the remaining logs. We also do this
// purge on startup to prevent logs from accumulating. // purge on startup to prevent logs from accumulating.
// the frequency with which we flush to indexeddb
import { logger } from "matrix-js-sdk/src/logger"; 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; const FLUSH_RATE_MS = 30 * 1000;
// the length of log data we keep in indexeddb (and include in the reports) // the length of log data we keep in indexeddb (and include in the reports)
const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB
// A class which monkey-patches the global console and stores log lines. type LogFunction = (
export class ConsoleLogger { ...args: (Error | DOMException | object | string)[]
logs = ""; ) => void;
type LogFunctionName = "log" | "info" | "warn" | "error";
monkeyPatch(consoleObj) { // A class which monkey-patches the global console and stores log lines.
interface LogEntry {
id: string;
lines: string;
index?: number;
}
export class ConsoleLogger {
private logs = "";
private originalFunctions: { [key in LogFunctionName]?: LogFunction } = {};
public monkeyPatch(consoleObj: Console): void {
// Monkey-patch console logging // Monkey-patch console logging
const consoleFunctionsToLevels = { const consoleFunctionsToLevels = {
log: "I", log: "I",
@ -60,6 +75,7 @@ export class ConsoleLogger {
Object.keys(consoleFunctionsToLevels).forEach((fnName) => { Object.keys(consoleFunctionsToLevels).forEach((fnName) => {
const level = consoleFunctionsToLevels[fnName]; const level = consoleFunctionsToLevels[fnName];
const originalFn = consoleObj[fnName].bind(consoleObj); const originalFn = consoleObj[fnName].bind(consoleObj);
this.originalFunctions[fnName] = originalFn;
consoleObj[fnName] = (...args) => { consoleObj[fnName] = (...args) => {
this.log(level, ...args); this.log(level, ...args);
originalFn(...args); originalFn(...args);
@ -67,7 +83,17 @@ export class ConsoleLogger {
}); });
} }
log(level, ...args) { public bypassRageshake(
fnName: LogFunctionName,
...args: (Error | DOMException | object | string)[]
): void {
this.originalFunctions[fnName](...args);
}
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 // We don't know what locale the user may be running so use ISO strings
const ts = new Date().toISOString(); const ts = new Date().toISOString();
@ -78,21 +104,7 @@ export class ConsoleLogger {
} else if (arg instanceof Error) { } else if (arg instanceof Error) {
return arg.message + (arg.stack ? `\n${arg.stack}` : ""); return arg.message + (arg.stack ? `\n${arg.stack}` : "");
} else if (typeof arg === "object") { } else if (typeof arg === "object") {
try { return JSON.stringify(arg, getCircularReplacer());
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 "<object>";
}
return value;
});
}
} else { } else {
return arg; return arg;
} }
@ -116,7 +128,7 @@ export class ConsoleLogger {
* @param {boolean} keepLogs True to not delete logs after flushing. * @param {boolean} keepLogs True to not delete logs after flushing.
* @return {string} \n delimited log lines to flush. * @return {string} \n delimited log lines to flush.
*/ */
flush(keepLogs) { public flush(keepLogs?: boolean): string {
// The ConsoleLogger doesn't care how these end up on disk, it just // The ConsoleLogger doesn't care how these end up on disk, it just
// flushes them to the caller. // flushes them to the caller.
if (keepLogs) { if (keepLogs) {
@ -130,24 +142,23 @@ export class ConsoleLogger {
// A class which stores log lines in an IndexedDB instance. // A class which stores log lines in an IndexedDB instance.
export class IndexedDBLogStore { export class IndexedDBLogStore {
index = 0; private index = 0;
db = null; private db: IDBDatabase = null;
flushPromise = null; private flushPromise: Promise<void> = null;
flushAgainPromise = null; private flushAgainPromise: Promise<void> = null;
private id: string;
constructor(indexedDB, logger) { constructor(private indexedDB: IDBFactory, private logger: ConsoleLogger) {
this.indexedDB = indexedDB; this.id = "instance-" + randomString(16);
this.logger = logger;
this.id = "instance-" + Math.random() + Date.now();
} }
/** /**
* @return {Promise} Resolves when the store is ready. * @return {Promise} Resolves when the store is ready.
*/ */
connect() { public connect(): Promise<void> {
const req = this.indexedDB.open("logs"); const req = this.indexedDB.open("logs");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
req.onsuccess = (event) => { req.onsuccess = (event: Event) => {
// @ts-ignore // @ts-ignore
this.db = event.target.result; this.db = event.target.result;
// Periodically flush logs to local storage / indexeddb // Periodically flush logs to local storage / indexeddb
@ -206,7 +217,7 @@ export class IndexedDBLogStore {
* *
* @return {Promise} Resolved when the logs have been flushed. * @return {Promise} Resolved when the logs have been flushed.
*/ */
flush() { public flush(): Promise<void> {
// check if a flush() operation is ongoing // check if a flush() operation is ongoing
if (this.flushPromise) { if (this.flushPromise) {
if (this.flushAgainPromise) { if (this.flushAgainPromise) {
@ -225,7 +236,7 @@ export class IndexedDBLogStore {
} }
// there is no flush promise or there was but it has finished, so do // 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. // a brand new one, destroying the chain which may have been built up.
this.flushPromise = new Promise((resolve, reject) => { this.flushPromise = new Promise<void>((resolve, reject) => {
if (!this.db) { if (!this.db) {
// not connected yet or user rejected access for us to r/w to the db. // not connected yet or user rejected access for us to r/w to the db.
reject(new Error("No connected database")); reject(new Error("No connected database"));
@ -243,6 +254,7 @@ export class IndexedDBLogStore {
}; };
txn.onerror = (event) => { txn.onerror = (event) => {
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)); reject(new Error("Failed to write logs: " + event.target.errorCode));
}; };
objStore.add(this.generateLogEntry(lines)); objStore.add(this.generateLogEntry(lines));
@ -264,12 +276,12 @@ export class IndexedDBLogStore {
* log ID). The objects have said log ID in an "id" field and "lines" which * 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. * is a big string with all the new-line delimited logs.
*/ */
async consume() { public async consume(): Promise<LogEntry[]> {
const db = this.db; const db = this.db;
// Returns: a string representing the concatenated logs for this ID. // Returns: a string representing the concatenated logs for this ID.
// Stops adding log fragments when the size exceeds maxSize // Stops adding log fragments when the size exceeds maxSize
function fetchLogs(id, maxSize) { function fetchLogs(id: string, maxSize: number): Promise<string> {
const objectStore = db const objectStore = db
.transaction("logs", "readonly") .transaction("logs", "readonly")
.objectStore("logs"); .objectStore("logs");
@ -280,9 +292,11 @@ export class IndexedDBLogStore {
.openCursor(IDBKeyRange.only(id), "prev"); .openCursor(IDBKeyRange.only(id), "prev");
let lines = ""; let lines = "";
query.onerror = (event) => { query.onerror = (event) => {
// @ts-ignore
reject(new Error("Query failed: " + event.target.errorCode)); reject(new Error("Query failed: " + event.target.errorCode));
}; };
query.onsuccess = (event) => { query.onsuccess = (event) => {
// @ts-ignore
const cursor = event.target.result; const cursor = event.target.result;
if (!cursor) { if (!cursor) {
resolve(lines); resolve(lines);
@ -299,12 +313,12 @@ export class IndexedDBLogStore {
} }
// Returns: A sorted array of log IDs. (newest first) // Returns: A sorted array of log IDs. (newest first)
function fetchLogIds() { function fetchLogIds(): Promise<string[]> {
// To gather all the log IDs, query for all records in logslastmod. // To gather all the log IDs, query for all records in logslastmod.
const o = db const o = db
.transaction("logslastmod", "readonly") .transaction("logslastmod", "readonly")
.objectStore("logslastmod"); .objectStore("logslastmod");
return selectQuery(o, undefined, (cursor) => { return selectQuery<{ ts: number; id: string }>(o, undefined, (cursor) => {
return { return {
id: cursor.value.id, id: cursor.value.id,
ts: cursor.value.ts, ts: cursor.value.ts,
@ -319,13 +333,14 @@ export class IndexedDBLogStore {
}); });
} }
function deleteLogs(id) { function deleteLogs(id: number): Promise<void> {
return new Promise((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const txn = db.transaction(["logs", "logslastmod"], "readwrite"); const txn = db.transaction(["logs", "logslastmod"], "readwrite");
const o = txn.objectStore("logs"); const o = txn.objectStore("logs");
// only load the key path, not the data which may be huge // only load the key path, not the data which may be huge
const query = o.index("id").openKeyCursor(IDBKeyRange.only(id)); const query = o.index("id").openKeyCursor(IDBKeyRange.only(id));
query.onsuccess = (event) => { query.onsuccess = (event) => {
// @ts-ignore
const cursor = event.target.result; const cursor = event.target.result;
if (!cursor) { if (!cursor) {
return; return;
@ -340,6 +355,7 @@ export class IndexedDBLogStore {
reject( reject(
new Error( new Error(
"Failed to delete logs for " + "Failed to delete logs for " +
// @ts-ignore
`'${id}' : ${event.target.errorCode}` `'${id}' : ${event.target.errorCode}`
) )
); );
@ -352,7 +368,7 @@ export class IndexedDBLogStore {
const allLogIds = await fetchLogIds(); const allLogIds = await fetchLogIds();
let removeLogIds = []; let removeLogIds = [];
const logs = []; const logs: LogEntry[] = [];
let size = 0; let size = 0;
for (let i = 0; i < allLogIds.length; i++) { for (let i = 0; i < allLogIds.length; i++) {
const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size); const lines = await fetchLogs(allLogIds[i], MAX_LOG_SIZE - size);
@ -390,7 +406,7 @@ export class IndexedDBLogStore {
return logs; return logs;
} }
generateLogEntry(lines) { private generateLogEntry(lines: string): LogEntry {
return { return {
id: this.id, id: this.id,
lines: lines, lines: lines,
@ -398,7 +414,7 @@ export class IndexedDBLogStore {
}; };
} }
generateLastModifiedTime() { private generateLastModifiedTime(): { id: string; ts: number } {
return { return {
id: this.id, id: this.id,
ts: Date.now(), ts: Date.now(),
@ -416,7 +432,11 @@ export class IndexedDBLogStore {
* @return {Promise<T[]>} Resolves to an array of whatever you returned from * @return {Promise<T[]>} Resolves to an array of whatever you returned from
* resultMapper. * resultMapper.
*/ */
function selectQuery(store, keyRange, resultMapper) { function selectQuery<T>(
store: IDBObjectStore,
keyRange: IDBKeyRange,
resultMapper: (cursor: IDBCursorWithValue) => T
): Promise<T[]> {
const query = store.openCursor(keyRange); const query = store.openCursor(keyRange);
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const results = []; const results = [];
@ -437,6 +457,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<void>;
// eslint-disable-next-line no-var, camelcase
var mx_rage_initStoragePromise: Promise<void>;
}
/** /**
* Configure rage shaking support for sending bug reports. * Configure rage shaking support for sending bug reports.
@ -445,7 +475,7 @@ function selectQuery(store, keyRange, resultMapper) {
* be set up immediately for the logs. * be set up immediately for the logs.
* @return {Promise} Resolves when set up. * @return {Promise} Resolves when set up.
*/ */
export function init(setUpPersistence = true) { export function init(setUpPersistence = true): Promise<void> {
if (global.mx_rage_initPromise) { if (global.mx_rage_initPromise) {
return global.mx_rage_initPromise; return global.mx_rage_initPromise;
} }
@ -465,7 +495,7 @@ export function init(setUpPersistence = true) {
* then this no-ops. * then this no-ops.
* @return {Promise} Resolves when complete. * @return {Promise} Resolves when complete.
*/ */
export function tryInitStorage() { export function tryInitStorage(): Promise<void> {
if (global.mx_rage_initStoragePromise) { if (global.mx_rage_initStoragePromise) {
return global.mx_rage_initStoragePromise; return global.mx_rage_initStoragePromise;
} }
@ -491,7 +521,7 @@ export function tryInitStorage() {
return global.mx_rage_initStoragePromise; return global.mx_rage_initStoragePromise;
} }
export function flush() { export function flush(): Promise<void> {
if (!global.mx_rage_store) { if (!global.mx_rage_store) {
return; return;
} }
@ -502,7 +532,7 @@ export function flush() {
* Clean up old logs. * Clean up old logs.
* @return {Promise} Resolves if cleaned logs. * @return {Promise} Resolves if cleaned logs.
*/ */
export async function cleanup() { export async function cleanup(): Promise<void> {
if (!global.mx_rage_store) { if (!global.mx_rage_store) {
return; return;
} }
@ -512,9 +542,9 @@ export async function cleanup() {
/** /**
* Get a recent snapshot of the logs, ready for attaching to a bug report * 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<LogEntry[]> {
if (!global.mx_rage_logger) { if (!global.mx_rage_logger) {
throw new Error("No console logger, did you forget to call init()?"); throw new Error("No console logger, did you forget to call init()?");
} }
@ -523,7 +553,7 @@ export async function getLogsForReport() {
if (global.mx_rage_store) { if (global.mx_rage_store) {
// flush most recent logs // flush most recent logs
await global.mx_rage_store.flush(); await global.mx_rage_store.flush();
return await global.mx_rage_store.consume(); return global.mx_rage_store.consume();
} else { } else {
return [ return [
{ {
@ -533,3 +563,24 @@ export async function getLogsForReport() {
]; ];
} }
} }
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: unknown): unknown => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "<$ cycle-trimmed $>";
}
seen.add(value);
}
return value;
};
};

View file

@ -15,14 +15,31 @@ limitations under the License.
*/ */
import { useCallback, useContext, useEffect, useState } from "react"; import { useCallback, useContext, useEffect, useState } from "react";
import { getLogsForReport } from "./rageshake";
import pako from "pako"; 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 { getLogsForReport } from "./rageshake";
import { useClient } from "../ClientContext"; import { useClient } from "../ClientContext";
import { InspectorContext } from "../room/GroupCallInspector"; import { InspectorContext } from "../room/GroupCallInspector";
import { useModalTriggerState } from "../Modal"; import { useModalTriggerState } from "../Modal";
export function useSubmitRageshake() { interface RageShakeSubmitOptions {
const { client } = useClient(); description: string;
roomId: string;
label: string;
sendLogs: boolean;
rageshakeRequestId: string;
}
export function useSubmitRageshake(): {
submitRageshake: (opts: RageShakeSubmitOptions) => Promise<void>;
sending: boolean;
sent: boolean;
error: Error;
} {
const client: MatrixClient = useClient().client;
const [{ json }] = useContext(InspectorContext); const [{ json }] = useContext(InspectorContext);
const [{ sending, sent, error }, setState] = useState({ const [{ sending, sent, error }, setState] = useState({
@ -57,9 +74,12 @@ export function useSubmitRageshake() {
opts.description || "User did not supply any additional text." opts.description || "User did not supply any additional text."
); );
body.append("app", "matrix-video-chat"); 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("user_agent", userAgent);
body.append("installed_pwa", false); body.append("installed_pwa", "false");
body.append("touch_input", touchInput); body.append("touch_input", touchInput);
if (client) { if (client) {
@ -181,7 +201,11 @@ export function useSubmitRageshake() {
if (navigator.storage && navigator.storage.estimate) { if (navigator.storage && navigator.storage.estimate) {
try { 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_quota", String(estimate.quota));
body.append("storageManager_usage", String(estimate.usage)); body.append("storageManager_usage", String(estimate.usage));
if (estimate.usageDetails) { if (estimate.usageDetails) {
@ -201,7 +225,6 @@ export function useSubmitRageshake() {
for (const entry of logs) { for (const entry of logs) {
// encode as UTF-8 // encode as UTF-8
let buf = new TextEncoder().encode(entry.lines); let buf = new TextEncoder().encode(entry.lines);
// compress // compress
buf = pako.gzip(buf); buf = pako.gzip(buf);
@ -225,7 +248,7 @@ export function useSubmitRageshake() {
} }
await fetch( await fetch(
import.meta.env.VITE_RAGESHAKE_SUBMIT_URL || (import.meta.env.VITE_RAGESHAKE_SUBMIT_URL as string) ||
"https://element.io/bugreports/submit", "https://element.io/bugreports/submit",
{ {
method: "POST", method: "POST",
@ -250,7 +273,7 @@ export function useSubmitRageshake() {
}; };
} }
export function useDownloadDebugLog() { export function useDownloadDebugLog(): () => void {
const [{ json }] = useContext(InspectorContext); const [{ json }] = useContext(InspectorContext);
const downloadDebugLog = useCallback(() => { const downloadDebugLog = useCallback(() => {
@ -271,7 +294,10 @@ export function useDownloadDebugLog() {
return downloadDebugLog; return downloadDebugLog;
} }
export function useRageshakeRequest() { export function useRageshakeRequest(): (
roomId: string,
rageshakeRequestId: string
) => void {
const { client } = useClient(); const { client } = useClient();
const sendRageshakeRequest = useCallback( const sendRageshakeRequest = useCallback(
@ -285,14 +311,27 @@ export function useRageshakeRequest() {
return sendRageshakeRequest; return sendRageshakeRequest;
} }
interface ModalProps {
isOpen: boolean;
onClose: () => void;
}
interface ModalPropsWithId extends ModalProps {
rageshakeRequestId: string;
}
export function useRageshakeRequestModal(roomId) { export function useRageshakeRequestModal(roomId: string): {
const { modalState, modalProps } = useModalTriggerState(); modalState: OverlayTriggerState;
const { client } = useClient(); modalProps: ModalPropsWithId;
const [rageshakeRequestId, setRageshakeRequestId] = useState(); } {
const { modalState, modalProps } = useModalTriggerState() as {
modalState: OverlayTriggerState;
modalProps: ModalProps;
};
const client: MatrixClient = useClient().client;
const [rageshakeRequestId, setRageshakeRequestId] = useState<string>();
useEffect(() => { useEffect(() => {
const onEvent = (event) => { const onEvent = (event: MatrixEvent) => {
const type = event.getType(); const type = event.getType();
if ( if (
@ -305,10 +344,10 @@ export function useRageshakeRequestModal(roomId) {
} }
}; };
client.on("event", onEvent); client.on(ClientEvent.Event, onEvent);
return () => { return () => {
client.removeListener("event", onEvent); client.removeListener(ClientEvent.Event, onEvent);
}; };
}, [modalState.open, roomId, client, modalState]); }, [modalState.open, roomId, client, modalState]);

View file

@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
/* /*
Copyright 2022 Matrix.org Foundation C.I.C. Copyright 2022 Matrix.org Foundation C.I.C.
@ -14,6 +15,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { MatrixClient } from "matrix-js-sdk";
import { MediaHandlerEvent } from "matrix-js-sdk/src/webrtc/mediaHandler";
import React, { import React, {
useState, useState,
useEffect, useEffect,
@ -23,9 +26,27 @@ import React, {
createContext, createContext,
} from "react"; } 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<MediaHandlerContextInterface>(undefined);
interface MediaPreferences {
audioInput?: string;
videoInput?: string;
audioOutput?: string;
}
function getMediaPreferences(): MediaPreferences {
const mediaPreferences = localStorage.getItem("matrix-media-preferences"); const mediaPreferences = localStorage.getItem("matrix-media-preferences");
if (mediaPreferences) { if (mediaPreferences) {
@ -39,8 +60,8 @@ function getMediaPreferences() {
} }
} }
function updateMediaPreferences(newPreferences) { function updateMediaPreferences(newPreferences: MediaPreferences): void {
const oldPreferences = getMediaPreferences(newPreferences); const oldPreferences = getMediaPreferences();
localStorage.setItem( localStorage.setItem(
"matrix-media-preferences", "matrix-media-preferences",
@ -50,8 +71,11 @@ function updateMediaPreferences(newPreferences) {
}) })
); );
} }
interface Props {
export function MediaHandlerProvider({ client, children }) { client: MatrixClient;
children: JSX.Element[];
}
export function MediaHandlerProvider({ client, children }: Props): JSX.Element {
const [ const [
{ {
audioInput, audioInput,
@ -72,7 +96,9 @@ export function MediaHandlerProvider({ client, children }) {
); );
return { return {
// @ts-ignore, ignore that audioInput is a private members of mediaHandler
audioInput: mediaHandler.audioInput, audioInput: mediaHandler.audioInput,
// @ts-ignore, ignore that videoInput is a private members of mediaHandler
videoInput: mediaHandler.videoInput, videoInput: mediaHandler.videoInput,
audioOutput: undefined, audioOutput: undefined,
audioInputs: [], audioInputs: [],
@ -84,7 +110,7 @@ export function MediaHandlerProvider({ client, children }) {
useEffect(() => { useEffect(() => {
const mediaHandler = client.getMediaHandler(); const mediaHandler = client.getMediaHandler();
function updateDevices() { function updateDevices(): void {
navigator.mediaDevices.enumerateDevices().then((devices) => { navigator.mediaDevices.enumerateDevices().then((devices) => {
const mediaPreferences = getMediaPreferences(); const mediaPreferences = getMediaPreferences();
@ -92,9 +118,10 @@ export function MediaHandlerProvider({ client, children }) {
(device) => device.kind === "audioinput" (device) => device.kind === "audioinput"
); );
const audioConnected = audioInputs.some( const audioConnected = audioInputs.some(
// @ts-ignore
(device) => device.deviceId === mediaHandler.audioInput (device) => device.deviceId === mediaHandler.audioInput
); );
// @ts-ignore
let audioInput = mediaHandler.audioInput; let audioInput = mediaHandler.audioInput;
if (!audioConnected && audioInputs.length > 0) { if (!audioConnected && audioInputs.length > 0) {
@ -105,9 +132,11 @@ export function MediaHandlerProvider({ client, children }) {
(device) => device.kind === "videoinput" (device) => device.kind === "videoinput"
); );
const videoConnected = videoInputs.some( const videoConnected = videoInputs.some(
// @ts-ignore
(device) => device.deviceId === mediaHandler.videoInput (device) => device.deviceId === mediaHandler.videoInput
); );
// @ts-ignore
let videoInput = mediaHandler.videoInput; let videoInput = mediaHandler.videoInput;
if (!videoConnected && videoInputs.length > 0) { if (!videoConnected && videoInputs.length > 0) {
@ -129,7 +158,9 @@ export function MediaHandlerProvider({ client, children }) {
} }
if ( if (
// @ts-ignore
mediaHandler.videoInput !== videoInput || mediaHandler.videoInput !== videoInput ||
// @ts-ignore
mediaHandler.audioInput !== audioInput mediaHandler.audioInput !== audioInput
) { ) {
mediaHandler.setMediaInputs(audioInput, videoInput); mediaHandler.setMediaInputs(audioInput, videoInput);
@ -149,18 +180,21 @@ export function MediaHandlerProvider({ client, children }) {
} }
updateDevices(); updateDevices();
mediaHandler.on("local_streams_changed", updateDevices); mediaHandler.on(MediaHandlerEvent.LocalStreamsChanged, updateDevices);
navigator.mediaDevices.addEventListener("devicechange", updateDevices); navigator.mediaDevices.addEventListener("devicechange", updateDevices);
return () => { return () => {
mediaHandler.removeListener("local_streams_changed", updateDevices); mediaHandler.removeListener(
MediaHandlerEvent.LocalStreamsChanged,
updateDevices
);
navigator.mediaDevices.removeEventListener("devicechange", updateDevices); navigator.mediaDevices.removeEventListener("devicechange", updateDevices);
mediaHandler.stopAllStreams(); mediaHandler.stopAllStreams();
}; };
}, [client]); }, [client]);
const setAudioInput = useCallback( const setAudioInput: (deviceId: string) => void = useCallback(
(deviceId) => { (deviceId: string) => {
updateMediaPreferences({ audioInput: deviceId }); updateMediaPreferences({ audioInput: deviceId });
setState((prevState) => ({ ...prevState, audioInput: deviceId })); setState((prevState) => ({ ...prevState, audioInput: deviceId }));
client.getMediaHandler().setAudioInput(deviceId); client.getMediaHandler().setAudioInput(deviceId);
@ -168,7 +202,7 @@ export function MediaHandlerProvider({ client, children }) {
[client] [client]
); );
const setVideoInput = useCallback( const setVideoInput: (deviceId: string) => void = useCallback(
(deviceId) => { (deviceId) => {
updateMediaPreferences({ videoInput: deviceId }); updateMediaPreferences({ videoInput: deviceId });
setState((prevState) => ({ ...prevState, videoInput: deviceId })); setState((prevState) => ({ ...prevState, videoInput: deviceId }));
@ -177,35 +211,36 @@ export function MediaHandlerProvider({ client, children }) {
[client] [client]
); );
const setAudioOutput = useCallback((deviceId) => { const setAudioOutput: (deviceId: string) => void = useCallback((deviceId) => {
updateMediaPreferences({ audioOutput: deviceId }); updateMediaPreferences({ audioOutput: deviceId });
setState((prevState) => ({ ...prevState, audioOutput: deviceId })); setState((prevState) => ({ ...prevState, audioOutput: deviceId }));
}, []); }, []);
const context = useMemo( const context: MediaHandlerContextInterface =
() => ({ useMemo<MediaHandlerContextInterface>(
audioInput, () => ({
audioInputs, audioInput,
setAudioInput, audioInputs,
videoInput, setAudioInput,
videoInputs, videoInput,
setVideoInput, videoInputs,
audioOutput, setVideoInput,
audioOutputs, audioOutput,
setAudioOutput, audioOutputs,
}), setAudioOutput,
[ }),
audioInput, [
audioInputs, audioInput,
setAudioInput, audioInputs,
videoInput, setAudioInput,
videoInputs, videoInput,
setVideoInput, videoInputs,
audioOutput, setVideoInput,
audioOutputs, audioOutput,
setAudioOutput, audioOutputs,
] setAudioOutput,
); ]
);
return ( return (
<MediaHandlerContext.Provider value={context}> <MediaHandlerContext.Provider value={context}>