Merge pull request #387 from toger5/ts_settings
typescript `src/settings`
This commit is contained in:
commit
05e786e3d6
4 changed files with 247 additions and 110 deletions
|
@ -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>
|
|
@ -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;
|
||||||
|
};
|
||||||
|
};
|
|
@ -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]);
|
||||||
|
|
|
@ -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,12 +211,13 @@ 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,
|
audioInput,
|
||||||
audioInputs,
|
audioInputs,
|
Loading…
Add table
Reference in a new issue