diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 8ad93fa960..d5856a5702 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -49,6 +49,8 @@ import PerformanceMonitor from "../performance"; import UIStore from "../stores/UIStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; +import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake"; +import ActiveWidgetStore from "../stores/ActiveWidgetStore"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -92,6 +94,7 @@ declare global { mxUIStore: UIStore; mxSetupEncryptionStore?: SetupEncryptionStore; mxRoomScrollStateStore?: RoomScrollStateStore; + mxActiveWidgetStore?: ActiveWidgetStore; mxOnRecaptchaLoaded?: () => void; electron?: Electron; } @@ -223,6 +226,15 @@ declare global { ) => string; isReady: () => boolean; }; + + // 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; + // eslint-disable-next-line no-var, camelcase + var mx_rage_store: IndexedDBLogStore; } /* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 57c922aeb7..3685f7b938 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -786,7 +786,7 @@ async function startMatrixClient(startSyncing = true): Promise { UserActivity.sharedInstance().start(); DMRoomMap.makeShared().start(); IntegrationManagers.sharedInstance().startWatching(); - ActiveWidgetStore.start(); + ActiveWidgetStore.instance.start(); CallHandler.sharedInstance().start(); // Start Mjolnir even though we haven't checked the feature flag yet. Starting @@ -892,7 +892,7 @@ export function stopMatrixClient(unsetClient = true): void { UserActivity.sharedInstance().stop(); TypingStore.sharedInstance().reset(); Presence.stop(); - ActiveWidgetStore.stop(); + ActiveWidgetStore.instance.stop(); IntegrationManagers.sharedInstance().stopWatching(); Mjolnir.sharedInstance().stop(); DeviceListener.sharedInstance().stop(); diff --git a/src/components/views/elements/AppTile.tsx b/src/components/views/elements/AppTile.tsx index ea9ef71626..3f4d75df27 100644 --- a/src/components/views/elements/AppTile.tsx +++ b/src/components/views/elements/AppTile.tsx @@ -163,7 +163,7 @@ export default class AppTile extends React.Component { if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { // Force the widget to be non-persistent (able to be deleted/forgotten) - ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); + ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); PersistedElement.destroyElement(this.persistKey); if (this.sgWidget) this.sgWidget.stop(); } @@ -198,8 +198,8 @@ export default class AppTile extends React.Component { if (this.dispatcherRef) dis.unregister(this.dispatcherRef); // if it's not remaining on screen, get rid of the PersistedElement container - if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { - ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); + if (!ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id)) { + ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); PersistedElement.destroyElement(this.persistKey); } @@ -282,7 +282,7 @@ export default class AppTile extends React.Component { // Delete the widget from the persisted store for good measure. PersistedElement.destroyElement(this.persistKey); - ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); + ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id); if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true }); } diff --git a/src/components/views/elements/PersistentApp.tsx b/src/components/views/elements/PersistentApp.tsx index 1f911659e2..8d0751cc1d 100644 --- a/src/components/views/elements/PersistentApp.tsx +++ b/src/components/views/elements/PersistentApp.tsx @@ -17,7 +17,7 @@ limitations under the License. import React from 'react'; import RoomViewStore from '../../../stores/RoomViewStore'; -import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; +import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore'; import WidgetUtils from '../../../utils/WidgetUtils'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { replaceableComponent } from "../../../utils/replaceableComponent"; @@ -39,13 +39,13 @@ export default class PersistentApp extends React.Component<{}, IState> { this.state = { roomId: RoomViewStore.getRoomId(), - persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), + persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(), }; } public componentDidMount(): void { this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); - ActiveWidgetStore.on('update', this.onActiveWidgetStoreUpdate); + ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership); } @@ -53,7 +53,7 @@ export default class PersistentApp extends React.Component<{}, IState> { if (this.roomStoreToken) { this.roomStoreToken.remove(); } - ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate); + ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate); if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); } @@ -68,23 +68,23 @@ export default class PersistentApp extends React.Component<{}, IState> { private onActiveWidgetStoreUpdate = (): void => { this.setState({ - persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), + persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(), }); }; private onMyMembership = async (room: Room, membership: string): Promise => { - const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); + const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId); if (membership !== "join") { // we're not in the room anymore - delete if (room .roomId === persistentWidgetInRoomId) { - ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); + ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId); } } }; public render(): JSX.Element { if (this.state.persistentWidgetId) { - const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); + const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoom = MatrixClientPeg.get().getRoom(persistentWidgetInRoomId); @@ -96,7 +96,7 @@ export default class PersistentApp extends React.Component<{}, IState> { if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") { // get the widget data const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { - return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); + return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId(); }); const app = WidgetUtils.makeAppConfig( appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), diff --git a/src/rageshake/rageshake.js b/src/rageshake/rageshake.ts similarity index 91% rename from src/rageshake/rageshake.js rename to src/rageshake/rageshake.ts index 0dc36e3a9f..acf77c31c0 100644 --- a/src/rageshake/rageshake.js +++ b/src/rageshake/rageshake.ts @@ -46,12 +46,10 @@ const FLUSH_RATE_MS = 30 * 1000; const MAX_LOG_SIZE = 1024 * 1024 * 5; // 5 MB // A class which monkey-patches the global console and stores log lines. -class ConsoleLogger { - constructor() { - this.logs = ""; - } +export class ConsoleLogger { + private logs = ""; - monkeyPatch(consoleObj) { + public monkeyPatch(consoleObj: Console): void { // Monkey-patch console logging const consoleFunctionsToLevels = { log: "I", @@ -69,14 +67,14 @@ class ConsoleLogger { }); } - log(level, ...args) { + private log(level: string, ...args: (Error | DOMException | object | string)[]): void { // We don't know what locale the user may be running so use ISO strings const ts = new Date().toISOString(); // Convert objects and errors to helpful things args = args.map((arg) => { if (arg instanceof DOMException) { - return arg.message + ` (${arg.name} | ${arg.code}) ` + (arg.stack ? `\n${arg.stack}` : ''); + return arg.message + ` (${arg.name} | ${arg.code})`; } else if (arg instanceof Error) { return arg.message + (arg.stack ? `\n${arg.stack}` : ''); } else if (typeof (arg) === 'object') { @@ -118,7 +116,7 @@ class ConsoleLogger { * @param {boolean} keepLogs True to not delete logs after flushing. * @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 // flushes them to the caller. if (keepLogs) { @@ -131,27 +129,28 @@ class ConsoleLogger { } // A class which stores log lines in an IndexedDB instance. -class IndexedDBLogStore { - constructor(indexedDB, logger) { - this.indexedDB = indexedDB; - this.logger = logger; - this.id = "instance-" + Math.random() + Date.now(); - this.index = 0; - this.db = null; +export class IndexedDBLogStore { + private id: string; + private index = 0; + private db = null; + private flushPromise = null; + private flushAgainPromise = null; - // these promises are cleared as soon as fulfilled - this.flushPromise = null; - // set if flush() is called whilst one is ongoing - this.flushAgainPromise = null; + constructor( + private indexedDB: IDBFactory, + private logger: ConsoleLogger, + ) { + this.id = "instance-" + Math.random() + Date.now(); } /** * @return {Promise} Resolves when the store is ready. */ - connect() { + public connect(): Promise { const req = this.indexedDB.open("logs"); return new Promise((resolve, reject) => { - req.onsuccess = (event) => { + req.onsuccess = (event: Event) => { + // @ts-ignore this.db = event.target.result; // Periodically flush logs to local storage / indexeddb setInterval(this.flush.bind(this), FLUSH_RATE_MS); @@ -160,6 +159,7 @@ class IndexedDBLogStore { req.onerror = (event) => { const err = ( + // @ts-ignore "Failed to open log database: " + event.target.error.name ); console.error(err); @@ -168,6 +168,7 @@ class IndexedDBLogStore { // First time: Setup the object store req.onupgradeneeded = (event) => { + // @ts-ignore const db = event.target.result; const logObjStore = db.createObjectStore("logs", { keyPath: ["id", "index"], @@ -178,7 +179,7 @@ class IndexedDBLogStore { logObjStore.createIndex("id", "id", { unique: false }); logObjStore.add( - this._generateLogEntry( + this.generateLogEntry( new Date() + " ::: Log database was created.", ), ); @@ -186,7 +187,7 @@ class IndexedDBLogStore { const lastModifiedStore = db.createObjectStore("logslastmod", { keyPath: "id", }); - lastModifiedStore.add(this._generateLastModifiedTime()); + lastModifiedStore.add(this.generateLastModifiedTime()); }; }); } @@ -210,7 +211,7 @@ class IndexedDBLogStore { * * @return {Promise} Resolved when the logs have been flushed. */ - flush() { + public flush(): Promise { // check if a flush() operation is ongoing if (this.flushPromise) { if (this.flushAgainPromise) { @@ -227,7 +228,7 @@ 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")); @@ -251,9 +252,9 @@ class IndexedDBLogStore { new Error("Failed to write logs: " + event.target.errorCode), ); }; - objStore.add(this._generateLogEntry(lines)); + objStore.add(this.generateLogEntry(lines)); const lastModStore = txn.objectStore("logslastmod"); - lastModStore.put(this._generateLastModifiedTime()); + lastModStore.put(this.generateLastModifiedTime()); }).then(() => { this.flushPromise = null; }); @@ -270,12 +271,12 @@ 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() { + public async consume(): Promise<{lines: string, id: string}[]> { 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"); return new Promise((resolve, reject) => { @@ -301,7 +302,7 @@ class IndexedDBLogStore { } // 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", @@ -319,8 +320,8 @@ class IndexedDBLogStore { }); } - function deleteLogs(id) { - return new Promise((resolve, reject) => { + function deleteLogs(id: number): Promise { + return new Promise((resolve, reject) => { const txn = db.transaction( ["logs", "logslastmod"], "readwrite", ); @@ -389,7 +390,7 @@ class IndexedDBLogStore { return logs; } - _generateLogEntry(lines) { + private generateLogEntry(lines: string): {id: string, lines: string, index: number} { return { id: this.id, lines: lines, @@ -397,7 +398,7 @@ class IndexedDBLogStore { }; } - _generateLastModifiedTime() { + private generateLastModifiedTime(): {id: string, ts: number} { return { id: this.id, ts: Date.now(), @@ -415,15 +416,19 @@ class IndexedDBLogStore { * @return {Promise} Resolves to an array of whatever you returned from * resultMapper. */ -function selectQuery(store, keyRange, resultMapper) { +function selectQuery( + store: IDBIndex, keyRange: IDBKeyRange, resultMapper: (cursor: IDBCursorWithValue) => T, +): Promise { const query = store.openCursor(keyRange); return new Promise((resolve, reject) => { const results = []; query.onerror = (event) => { + // @ts-ignore reject(new Error("Query failed: " + event.target.errorCode)); }; // collect results query.onsuccess = (event) => { + // @ts-ignore const cursor = event.target.result; if (!cursor) { resolve(results); @@ -442,7 +447,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; } @@ -462,7 +467,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/stores/ActiveWidgetStore.js b/src/stores/ActiveWidgetStore.js deleted file mode 100644 index b270d99693..0000000000 --- a/src/stores/ActiveWidgetStore.js +++ /dev/null @@ -1,110 +0,0 @@ -/* -Copyright 2018 New Vector Ltd - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -import EventEmitter from 'events'; - -import { MatrixClientPeg } from '../MatrixClientPeg'; -import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore"; - -/** - * Stores information about the widgets active in the app right now: - * * What widget is set to remain always-on-screen, if any - * Only one widget may be 'always on screen' at any one time. - * * Negotiated capabilities for active apps - */ -class ActiveWidgetStore extends EventEmitter { - constructor() { - super(); - this._persistentWidgetId = null; - - // What room ID each widget is associated with (if it's a room widget) - this._roomIdByWidgetId = {}; - - this.onRoomStateEvents = this.onRoomStateEvents.bind(this); - - this.dispatcherRef = null; - } - - start() { - MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents); - } - - stop() { - if (MatrixClientPeg.get()) { - MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents); - } - this._roomIdByWidgetId = {}; - } - - onRoomStateEvents(ev, state) { - // XXX: This listens for state events in order to remove the active widget. - // Everything else relies on views listening for events and calling setters - // on this class which is terrible. This store should just listen for events - // and keep itself up to date. - // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) - if (ev.getType() !== 'im.vector.modular.widgets') return; - - if (ev.getStateKey() === this._persistentWidgetId) { - this.destroyPersistentWidget(this._persistentWidgetId); - } - } - - destroyPersistentWidget(id) { - if (id !== this._persistentWidgetId) return; - const toDeleteId = this._persistentWidgetId; - - WidgetMessagingStore.instance.stopMessagingById(id); - - this.setWidgetPersistence(toDeleteId, false); - this.delRoomId(toDeleteId); - } - - setWidgetPersistence(widgetId, val) { - if (this._persistentWidgetId === widgetId && !val) { - this._persistentWidgetId = null; - } else if (this._persistentWidgetId !== widgetId && val) { - this._persistentWidgetId = widgetId; - } - this.emit('update'); - } - - getWidgetPersistence(widgetId) { - return this._persistentWidgetId === widgetId; - } - - getPersistentWidgetId() { - return this._persistentWidgetId; - } - - getRoomId(widgetId) { - return this._roomIdByWidgetId[widgetId]; - } - - setRoomId(widgetId, roomId) { - this._roomIdByWidgetId[widgetId] = roomId; - this.emit('update'); - } - - delRoomId(widgetId) { - delete this._roomIdByWidgetId[widgetId]; - this.emit('update'); - } -} - -if (global.singletonActiveWidgetStore === undefined) { - global.singletonActiveWidgetStore = new ActiveWidgetStore(); -} -export default global.singletonActiveWidgetStore; diff --git a/src/stores/ActiveWidgetStore.ts b/src/stores/ActiveWidgetStore.ts new file mode 100644 index 0000000000..ca50689188 --- /dev/null +++ b/src/stores/ActiveWidgetStore.ts @@ -0,0 +1,112 @@ +/* +Copyright 2018 New Vector Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import EventEmitter from 'events'; +import { MatrixEvent } from "matrix-js-sdk"; + +import { MatrixClientPeg } from '../MatrixClientPeg'; +import { WidgetMessagingStore } from "./widgets/WidgetMessagingStore"; + +export enum ActiveWidgetStoreEvent { + Update = "update", +} + +/** + * Stores information about the widgets active in the app right now: + * * What widget is set to remain always-on-screen, if any + * Only one widget may be 'always on screen' at any one time. + * * Negotiated capabilities for active apps + */ +export default class ActiveWidgetStore extends EventEmitter { + private static internalInstance: ActiveWidgetStore; + private persistentWidgetId: string; + // What room ID each widget is associated with (if it's a room widget) + private roomIdByWidgetId = new Map(); + + public static get instance(): ActiveWidgetStore { + if (!ActiveWidgetStore.internalInstance) { + ActiveWidgetStore.internalInstance = new ActiveWidgetStore(); + } + return ActiveWidgetStore.internalInstance; + } + + public start(): void { + MatrixClientPeg.get().on('RoomState.events', this.onRoomStateEvents); + } + + public stop(): void { + if (MatrixClientPeg.get()) { + MatrixClientPeg.get().removeListener('RoomState.events', this.onRoomStateEvents); + } + this.roomIdByWidgetId.clear(); + } + + private onRoomStateEvents = (ev: MatrixEvent): void => { + // XXX: This listens for state events in order to remove the active widget. + // Everything else relies on views listening for events and calling setters + // on this class which is terrible. This store should just listen for events + // and keep itself up to date. + // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) + if (ev.getType() !== 'im.vector.modular.widgets') return; + + if (ev.getStateKey() === this.persistentWidgetId) { + this.destroyPersistentWidget(this.persistentWidgetId); + } + }; + + public destroyPersistentWidget(id: string): void { + if (id !== this.persistentWidgetId) return; + const toDeleteId = this.persistentWidgetId; + + WidgetMessagingStore.instance.stopMessagingById(id); + + this.setWidgetPersistence(toDeleteId, false); + this.delRoomId(toDeleteId); + } + + public setWidgetPersistence(widgetId: string, val: boolean): void { + if (this.persistentWidgetId === widgetId && !val) { + this.persistentWidgetId = null; + } else if (this.persistentWidgetId !== widgetId && val) { + this.persistentWidgetId = widgetId; + } + this.emit(ActiveWidgetStoreEvent.Update); + } + + public getWidgetPersistence(widgetId: string): boolean { + return this.persistentWidgetId === widgetId; + } + + public getPersistentWidgetId(): string { + return this.persistentWidgetId; + } + + public getRoomId(widgetId: string): string { + return this.roomIdByWidgetId.get(widgetId); + } + + public setRoomId(widgetId: string, roomId: string): void { + this.roomIdByWidgetId.set(widgetId, roomId); + this.emit(ActiveWidgetStoreEvent.Update); + } + + public delRoomId(widgetId: string): void { + this.roomIdByWidgetId.delete(widgetId); + this.emit(ActiveWidgetStoreEvent.Update); + } +} + +window.mxActiveWidgetStore = ActiveWidgetStore.instance; diff --git a/src/stores/WidgetStore.ts b/src/stores/WidgetStore.ts index bd03e2065b..44c8327c04 100644 --- a/src/stores/WidgetStore.ts +++ b/src/stores/WidgetStore.ts @@ -142,14 +142,14 @@ export default class WidgetStore extends AsyncStoreWithClient { // If a persistent widget is active, check to see if it's just been removed. // If it has, it needs to destroyed otherwise unmounting the node won't kill it - const persistentWidgetId = ActiveWidgetStore.getPersistentWidgetId(); + const persistentWidgetId = ActiveWidgetStore.instance.getPersistentWidgetId(); if (persistentWidgetId) { if ( - ActiveWidgetStore.getRoomId(persistentWidgetId) === room.roomId && + ActiveWidgetStore.instance.getRoomId(persistentWidgetId) === room.roomId && !roomInfo.widgets.some(w => w.id === persistentWidgetId) ) { logger.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`); - ActiveWidgetStore.destroyPersistentWidget(persistentWidgetId); + ActiveWidgetStore.instance.destroyPersistentWidget(persistentWidgetId); } } @@ -195,7 +195,7 @@ export default class WidgetStore extends AsyncStoreWithClient { // A persistent conference widget indicates that we're participating const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); - return widgets.some(w => ActiveWidgetStore.getWidgetPersistence(w.id)); + return widgets.some(w => ActiveWidgetStore.instance.getWidgetPersistence(w.id)); } } diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 361b02bc3e..e00c4c6c0b 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -266,7 +266,7 @@ export class StopGapWidget extends EventEmitter { WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging); if (!this.appTileProps.userWidget && this.appTileProps.room) { - ActiveWidgetStore.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId); + ActiveWidgetStore.instance.setRoomId(this.mockWidget.id, this.appTileProps.room.roomId); } // Always attach a handler for ViewRoom, but permission check it internally @@ -319,7 +319,7 @@ export class StopGapWidget extends EventEmitter { if (WidgetType.JITSI.matches(this.mockWidget.type)) { CountlyAnalytics.instance.trackJoinCall(this.appTileProps.room.roomId, true, true); } - ActiveWidgetStore.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value); + ActiveWidgetStore.instance.setWidgetPersistence(this.mockWidget.id, ev.detail.data.value); ev.preventDefault(); this.messaging.transport.reply(ev.detail, {}); // ack } @@ -406,13 +406,13 @@ export class StopGapWidget extends EventEmitter { } public stop(opts = { forceDestroy: false }) { - if (!opts?.forceDestroy && ActiveWidgetStore.getPersistentWidgetId() === this.mockWidget.id) { + if (!opts?.forceDestroy && ActiveWidgetStore.instance.getPersistentWidgetId() === this.mockWidget.id) { logger.log("Skipping destroy - persistent widget"); return; } if (!this.started) return; WidgetMessagingStore.instance.stopMessaging(this.mockWidget); - ActiveWidgetStore.delRoomId(this.mockWidget.id); + ActiveWidgetStore.instance.delRoomId(this.mockWidget.id); if (MatrixClientPeg.get()) { MatrixClientPeg.get().off('event', this.onEvent); diff --git a/src/utils/DirectoryUtils.js b/src/utils/DirectoryUtils.ts similarity index 81% rename from src/utils/DirectoryUtils.js rename to src/utils/DirectoryUtils.ts index 577a6441f8..255ae0e3fd 100644 --- a/src/utils/DirectoryUtils.js +++ b/src/utils/DirectoryUtils.ts @@ -14,9 +14,12 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { IInstance } from "matrix-js-sdk/src/client"; +import { Protocols } from "../components/views/directory/NetworkDropdown"; + // Find a protocol 'instance' with a given instance_id // in the supplied protocols dict -export function instanceForInstanceId(protocols, instanceId) { +export function instanceForInstanceId(protocols: Protocols, instanceId: string): IInstance { if (!instanceId) return null; for (const proto of Object.keys(protocols)) { if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue; @@ -28,7 +31,7 @@ export function instanceForInstanceId(protocols, instanceId) { // given an instance_id, return the name of the protocol for // that instance ID in the supplied protocols dict -export function protocolNameForInstanceId(protocols, instanceId) { +export function protocolNameForInstanceId(protocols: Protocols, instanceId: string): string { if (!instanceId) return null; for (const proto of Object.keys(protocols)) { if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue; diff --git a/src/utils/HostingLink.js b/src/utils/HostingLink.ts similarity index 91% rename from src/utils/HostingLink.js rename to src/utils/HostingLink.ts index 134e045ca2..f8c0f12c3f 100644 --- a/src/utils/HostingLink.js +++ b/src/utils/HostingLink.ts @@ -17,7 +17,7 @@ limitations under the License. import SdkConfig from '../SdkConfig'; import { MatrixClientPeg } from '../MatrixClientPeg'; -export function getHostingLink(campaign) { +export function getHostingLink(campaign: string): string { const hostingLink = SdkConfig.get().hosting_signup_link; if (!hostingLink) return null; if (!campaign) return hostingLink; @@ -27,7 +27,7 @@ export function getHostingLink(campaign) { try { const hostingUrl = new URL(hostingLink); hostingUrl.searchParams.set("utm_campaign", campaign); - return hostingUrl.format(); + return hostingUrl.toString(); } catch (e) { return hostingLink; } diff --git a/src/utils/KeyVerificationStateObserver.js b/src/utils/KeyVerificationStateObserver.ts similarity index 86% rename from src/utils/KeyVerificationStateObserver.js rename to src/utils/KeyVerificationStateObserver.ts index 023cdf3a75..e4fae3f3c8 100644 --- a/src/utils/KeyVerificationStateObserver.js +++ b/src/utils/KeyVerificationStateObserver.ts @@ -17,14 +17,14 @@ limitations under the License. import { MatrixClientPeg } from '../MatrixClientPeg'; import { _t } from '../languageHandler'; -export function getNameForEventRoom(userId, roomId) { +export function getNameForEventRoom(userId: string, roomId: string): string { const client = MatrixClientPeg.get(); const room = client.getRoom(roomId); const member = room && room.getMember(userId); return member ? member.name : userId; } -export function userLabelForEventRoom(userId, roomId) { +export function userLabelForEventRoom(userId: string, roomId: string): string { const name = getNameForEventRoom(userId, roomId); if (name !== userId) { return _t("%(name)s (%(userId)s)", { name, userId }); diff --git a/src/utils/MegolmExportEncryption.js b/src/utils/MegolmExportEncryption.ts similarity index 90% rename from src/utils/MegolmExportEncryption.js rename to src/utils/MegolmExportEncryption.ts index 8e7ee2005d..47c395bfb7 100644 --- a/src/utils/MegolmExportEncryption.js +++ b/src/utils/MegolmExportEncryption.ts @@ -26,17 +26,17 @@ const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle; * Make an Error object which has a friendlyText property which is already * translated and suitable for showing to the user. * - * @param {string} msg message for the exception + * @param {string} message message for the exception * @param {string} friendlyText - * @returns {Error} + * @returns {{message: string, friendlyText: string}} */ -function friendlyError(msg, friendlyText) { - const e = new Error(msg); - e.friendlyText = friendlyText; - return e; +function friendlyError( + message: string, friendlyText: string, +): { message: string, friendlyText: string } { + return { message, friendlyText }; } -function cryptoFailMsg() { +function cryptoFailMsg(): string { return _t('Your browser does not support the required cryptography extensions'); } @@ -49,7 +49,7 @@ function cryptoFailMsg() { * * */ -export async function decryptMegolmKeyFile(data, password) { +export async function decryptMegolmKeyFile(data: ArrayBuffer, password: string): Promise { const body = unpackMegolmKeyFile(data); const brand = SdkConfig.get().brand; @@ -124,7 +124,11 @@ export async function decryptMegolmKeyFile(data, password) { * key-derivation function. * @return {Promise} promise for encrypted output */ -export async function encryptMegolmKeyFile(data, password, options) { +export async function encryptMegolmKeyFile( + data: string, + password: string, + options?: { kdf_rounds?: number }, // eslint-disable-line camelcase +): Promise { options = options || {}; const kdfRounds = options.kdf_rounds || 500000; @@ -196,7 +200,7 @@ export async function encryptMegolmKeyFile(data, password, options) { * @param {String} password password * @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key] */ -async function deriveKeys(salt, iterations, password) { +async function deriveKeys(salt: Uint8Array, iterations: number, password: string): Promise<[CryptoKey, CryptoKey]> { const start = new Date(); let key; @@ -229,7 +233,7 @@ async function deriveKeys(salt, iterations, password) { } const now = new Date(); - logger.log("E2e import/export: deriveKeys took " + (now - start) + "ms"); + logger.log("E2e import/export: deriveKeys took " + (now.getTime() - start.getTime()) + "ms"); const aesKey = keybits.slice(0, 32); const hmacKey = keybits.slice(32); @@ -271,7 +275,7 @@ const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----'; * @param {ArrayBuffer} data input file * @return {Uint8Array} unbase64ed content */ -function unpackMegolmKeyFile(data) { +function unpackMegolmKeyFile(data: ArrayBuffer): Uint8Array { // parse the file as a great big String. This should be safe, because there // should be no non-ASCII characters, and it means that we can do string // comparisons to find the header and footer, and feed it into window.atob. @@ -279,6 +283,7 @@ function unpackMegolmKeyFile(data) { // look for the start line let lineStart = 0; + // eslint-disable-next-line no-constant-condition while (1) { const lineEnd = fileStr.indexOf('\n', lineStart); if (lineEnd < 0) { @@ -297,6 +302,7 @@ function unpackMegolmKeyFile(data) { const dataStart = lineStart; // look for the end line + // eslint-disable-next-line no-constant-condition while (1) { const lineEnd = fileStr.indexOf('\n', lineStart); const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim(); @@ -324,7 +330,7 @@ function unpackMegolmKeyFile(data) { * @param {Uint8Array} data raw data * @return {ArrayBuffer} formatted file */ -function packMegolmKeyFile(data) { +function packMegolmKeyFile(data: Uint8Array): ArrayBuffer { // we split into lines before base64ing, because encodeBase64 doesn't deal // terribly well with large arrays. const LINE_LENGTH = (72 * 4 / 3); @@ -347,7 +353,7 @@ function packMegolmKeyFile(data) { * @param {Uint8Array} uint8Array The data to encode. * @return {string} The base64. */ -function encodeBase64(uint8Array) { +function encodeBase64(uint8Array: Uint8Array): string { // Misinterpt the Uint8Array as Latin-1. // window.btoa expects a unicode string with codepoints in the range 0-255. const latin1String = String.fromCharCode.apply(null, uint8Array); @@ -360,7 +366,7 @@ function encodeBase64(uint8Array) { * @param {string} base64 The base64 to decode. * @return {Uint8Array} The decoded data. */ -function decodeBase64(base64) { +function decodeBase64(base64: string): Uint8Array { // window.atob returns a unicode string with codepoints in the range 0-255. const latin1String = window.atob(base64); // Encode the string as a Uint8Array