Merge remote-tracking branch 'upstream/develop' into task/messages-ts

This commit is contained in:
Šimon Brandner 2021-09-27 10:32:25 +02:00
commit 6a88ac900c
No known key found for this signature in database
GPG key ID: 55C211A1226CB17D
13 changed files with 219 additions and 191 deletions

View file

@ -49,6 +49,8 @@ import PerformanceMonitor from "../performance";
import UIStore from "../stores/UIStore"; import UIStore from "../stores/UIStore";
import { SetupEncryptionStore } from "../stores/SetupEncryptionStore"; import { SetupEncryptionStore } from "../stores/SetupEncryptionStore";
import { RoomScrollStateStore } from "../stores/RoomScrollStateStore"; import { RoomScrollStateStore } from "../stores/RoomScrollStateStore";
import { ConsoleLogger, IndexedDBLogStore } from "../rageshake/rageshake";
import ActiveWidgetStore from "../stores/ActiveWidgetStore";
/* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @typescript-eslint/naming-convention */
@ -92,6 +94,7 @@ declare global {
mxUIStore: UIStore; mxUIStore: UIStore;
mxSetupEncryptionStore?: SetupEncryptionStore; mxSetupEncryptionStore?: SetupEncryptionStore;
mxRoomScrollStateStore?: RoomScrollStateStore; mxRoomScrollStateStore?: RoomScrollStateStore;
mxActiveWidgetStore?: ActiveWidgetStore;
mxOnRecaptchaLoaded?: () => void; mxOnRecaptchaLoaded?: () => void;
electron?: Electron; electron?: Electron;
} }
@ -223,6 +226,15 @@ declare global {
) => string; ) => string;
isReady: () => boolean; 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<void>;
// eslint-disable-next-line no-var, camelcase
var mx_rage_initStoragePromise: Promise<void>;
// eslint-disable-next-line no-var, camelcase
var mx_rage_store: IndexedDBLogStore;
} }
/* eslint-enable @typescript-eslint/naming-convention */ /* eslint-enable @typescript-eslint/naming-convention */

View file

@ -786,7 +786,7 @@ async function startMatrixClient(startSyncing = true): Promise<void> {
UserActivity.sharedInstance().start(); UserActivity.sharedInstance().start();
DMRoomMap.makeShared().start(); DMRoomMap.makeShared().start();
IntegrationManagers.sharedInstance().startWatching(); IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.start(); ActiveWidgetStore.instance.start();
CallHandler.sharedInstance().start(); CallHandler.sharedInstance().start();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting // 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(); UserActivity.sharedInstance().stop();
TypingStore.sharedInstance().reset(); TypingStore.sharedInstance().reset();
Presence.stop(); Presence.stop();
ActiveWidgetStore.stop(); ActiveWidgetStore.instance.stop();
IntegrationManagers.sharedInstance().stopWatching(); IntegrationManagers.sharedInstance().stopWatching();
Mjolnir.sharedInstance().stop(); Mjolnir.sharedInstance().stop();
DeviceListener.sharedInstance().stop(); DeviceListener.sharedInstance().stop();

View file

@ -163,7 +163,7 @@ export default class AppTile extends React.Component<IProps, IState> {
if (this.state.hasPermissionToLoad && !hasPermissionToLoad) { if (this.state.hasPermissionToLoad && !hasPermissionToLoad) {
// Force the widget to be non-persistent (able to be deleted/forgotten) // 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); PersistedElement.destroyElement(this.persistKey);
if (this.sgWidget) this.sgWidget.stop(); if (this.sgWidget) this.sgWidget.stop();
} }
@ -198,8 +198,8 @@ export default class AppTile extends React.Component<IProps, IState> {
if (this.dispatcherRef) dis.unregister(this.dispatcherRef); if (this.dispatcherRef) dis.unregister(this.dispatcherRef);
// if it's not remaining on screen, get rid of the PersistedElement container // if it's not remaining on screen, get rid of the PersistedElement container
if (!ActiveWidgetStore.getWidgetPersistence(this.props.app.id)) { if (!ActiveWidgetStore.instance.getWidgetPersistence(this.props.app.id)) {
ActiveWidgetStore.destroyPersistentWidget(this.props.app.id); ActiveWidgetStore.instance.destroyPersistentWidget(this.props.app.id);
PersistedElement.destroyElement(this.persistKey); PersistedElement.destroyElement(this.persistKey);
} }
@ -282,7 +282,7 @@ export default class AppTile extends React.Component<IProps, IState> {
// Delete the widget from the persisted store for good measure. // Delete the widget from the persisted store for good measure.
PersistedElement.destroyElement(this.persistKey); 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 }); if (this.sgWidget) this.sgWidget.stop({ forceDestroy: true });
} }

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from 'react'; import React from 'react';
import RoomViewStore from '../../../stores/RoomViewStore'; import RoomViewStore from '../../../stores/RoomViewStore';
import ActiveWidgetStore from '../../../stores/ActiveWidgetStore'; import ActiveWidgetStore, { ActiveWidgetStoreEvent } from '../../../stores/ActiveWidgetStore';
import WidgetUtils from '../../../utils/WidgetUtils'; import WidgetUtils from '../../../utils/WidgetUtils';
import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { MatrixClientPeg } from '../../../MatrixClientPeg';
import { replaceableComponent } from "../../../utils/replaceableComponent"; import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -39,13 +39,13 @@ export default class PersistentApp extends React.Component<{}, IState> {
this.state = { this.state = {
roomId: RoomViewStore.getRoomId(), roomId: RoomViewStore.getRoomId(),
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
}; };
} }
public componentDidMount(): void { public componentDidMount(): void {
this.roomStoreToken = RoomViewStore.addListener(this.onRoomViewStoreUpdate); 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); MatrixClientPeg.get().on("Room.myMembership", this.onMyMembership);
} }
@ -53,7 +53,7 @@ export default class PersistentApp extends React.Component<{}, IState> {
if (this.roomStoreToken) { if (this.roomStoreToken) {
this.roomStoreToken.remove(); this.roomStoreToken.remove();
} }
ActiveWidgetStore.removeListener('update', this.onActiveWidgetStoreUpdate); ActiveWidgetStore.instance.removeListener(ActiveWidgetStoreEvent.Update, this.onActiveWidgetStoreUpdate);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership); MatrixClientPeg.get().removeListener("Room.myMembership", this.onMyMembership);
} }
@ -68,23 +68,23 @@ export default class PersistentApp extends React.Component<{}, IState> {
private onActiveWidgetStoreUpdate = (): void => { private onActiveWidgetStoreUpdate = (): void => {
this.setState({ this.setState({
persistentWidgetId: ActiveWidgetStore.getPersistentWidgetId(), persistentWidgetId: ActiveWidgetStore.instance.getPersistentWidgetId(),
}); });
}; };
private onMyMembership = async (room: Room, membership: string): Promise<void> => { private onMyMembership = async (room: Room, membership: string): Promise<void> => {
const persistentWidgetInRoomId = ActiveWidgetStore.getRoomId(this.state.persistentWidgetId); const persistentWidgetInRoomId = ActiveWidgetStore.instance.getRoomId(this.state.persistentWidgetId);
if (membership !== "join") { if (membership !== "join") {
// we're not in the room anymore - delete // we're not in the room anymore - delete
if (room .roomId === persistentWidgetInRoomId) { if (room .roomId === persistentWidgetInRoomId) {
ActiveWidgetStore.destroyPersistentWidget(this.state.persistentWidgetId); ActiveWidgetStore.instance.destroyPersistentWidget(this.state.persistentWidgetId);
} }
} }
}; };
public render(): JSX.Element { public render(): JSX.Element {
if (this.state.persistentWidgetId) { 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); 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") { if (this.state.roomId !== persistentWidgetInRoomId && myMembership === "join") {
// get the widget data // get the widget data
const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => { const appEvent = WidgetUtils.getRoomWidgets(persistentWidgetInRoom).find((ev) => {
return ev.getStateKey() === ActiveWidgetStore.getPersistentWidgetId(); return ev.getStateKey() === ActiveWidgetStore.instance.getPersistentWidgetId();
}); });
const app = WidgetUtils.makeAppConfig( const app = WidgetUtils.makeAppConfig(
appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(), appEvent.getStateKey(), appEvent.getContent(), appEvent.getSender(),

View file

@ -46,12 +46,10 @@ const FLUSH_RATE_MS = 30 * 1000;
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. // A class which monkey-patches the global console and stores log lines.
class ConsoleLogger { export class ConsoleLogger {
constructor() { private logs = "";
this.logs = "";
}
monkeyPatch(consoleObj) { public monkeyPatch(consoleObj: Console): void {
// Monkey-patch console logging // Monkey-patch console logging
const consoleFunctionsToLevels = { const consoleFunctionsToLevels = {
log: "I", 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 // 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();
// Convert objects and errors to helpful things // Convert objects and errors to helpful things
args = args.map((arg) => { args = args.map((arg) => {
if (arg instanceof DOMException) { 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) { } 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') {
@ -118,7 +116,7 @@ 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) {
@ -131,27 +129,28 @@ class ConsoleLogger {
} }
// A class which stores log lines in an IndexedDB instance. // A class which stores log lines in an IndexedDB instance.
class IndexedDBLogStore { export class IndexedDBLogStore {
constructor(indexedDB, logger) { private id: string;
this.indexedDB = indexedDB; private index = 0;
this.logger = logger; private db = null;
this.id = "instance-" + Math.random() + Date.now(); private flushPromise = null;
this.index = 0; private flushAgainPromise = null;
this.db = null;
// these promises are cleared as soon as fulfilled constructor(
this.flushPromise = null; private indexedDB: IDBFactory,
// set if flush() is called whilst one is ongoing private logger: ConsoleLogger,
this.flushAgainPromise = null; ) {
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
this.db = event.target.result; this.db = event.target.result;
// Periodically flush logs to local storage / indexeddb // Periodically flush logs to local storage / indexeddb
setInterval(this.flush.bind(this), FLUSH_RATE_MS); setInterval(this.flush.bind(this), FLUSH_RATE_MS);
@ -160,6 +159,7 @@ class IndexedDBLogStore {
req.onerror = (event) => { req.onerror = (event) => {
const err = ( const err = (
// @ts-ignore
"Failed to open log database: " + event.target.error.name "Failed to open log database: " + event.target.error.name
); );
console.error(err); console.error(err);
@ -168,6 +168,7 @@ class IndexedDBLogStore {
// First time: Setup the object store // First time: Setup the object store
req.onupgradeneeded = (event) => { req.onupgradeneeded = (event) => {
// @ts-ignore
const db = event.target.result; const db = event.target.result;
const logObjStore = db.createObjectStore("logs", { const logObjStore = db.createObjectStore("logs", {
keyPath: ["id", "index"], keyPath: ["id", "index"],
@ -178,7 +179,7 @@ class IndexedDBLogStore {
logObjStore.createIndex("id", "id", { unique: false }); logObjStore.createIndex("id", "id", { unique: false });
logObjStore.add( logObjStore.add(
this._generateLogEntry( this.generateLogEntry(
new Date() + " ::: Log database was created.", new Date() + " ::: Log database was created.",
), ),
); );
@ -186,7 +187,7 @@ class IndexedDBLogStore {
const lastModifiedStore = db.createObjectStore("logslastmod", { const lastModifiedStore = db.createObjectStore("logslastmod", {
keyPath: "id", 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. * @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) {
@ -227,7 +228,7 @@ 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"));
@ -251,9 +252,9 @@ class IndexedDBLogStore {
new Error("Failed to write logs: " + event.target.errorCode), new Error("Failed to write logs: " + event.target.errorCode),
); );
}; };
objStore.add(this._generateLogEntry(lines)); objStore.add(this.generateLogEntry(lines));
const lastModStore = txn.objectStore("logslastmod"); const lastModStore = txn.objectStore("logslastmod");
lastModStore.put(this._generateLastModifiedTime()); lastModStore.put(this.generateLastModifiedTime());
}).then(() => { }).then(() => {
this.flushPromise = null; this.flushPromise = null;
}); });
@ -270,12 +271,12 @@ 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<{lines: string, id: string}[]> {
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.transaction("logs", "readonly").objectStore("logs"); const objectStore = db.transaction("logs", "readonly").objectStore("logs");
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -301,7 +302,7 @@ 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.transaction("logslastmod", "readonly").objectStore( const o = db.transaction("logslastmod", "readonly").objectStore(
"logslastmod", "logslastmod",
@ -319,8 +320,8 @@ 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( const txn = db.transaction(
["logs", "logslastmod"], "readwrite", ["logs", "logslastmod"], "readwrite",
); );
@ -389,7 +390,7 @@ class IndexedDBLogStore {
return logs; return logs;
} }
_generateLogEntry(lines) { private generateLogEntry(lines: string): {id: string, lines: string, index: number} {
return { return {
id: this.id, id: this.id,
lines: lines, lines: lines,
@ -397,7 +398,7 @@ class IndexedDBLogStore {
}; };
} }
_generateLastModifiedTime() { private generateLastModifiedTime(): {id: string, ts: number} {
return { return {
id: this.id, id: this.id,
ts: Date.now(), ts: Date.now(),
@ -415,15 +416,19 @@ 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: IDBIndex, 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 = [];
query.onerror = (event) => { query.onerror = (event) => {
// @ts-ignore
reject(new Error("Query failed: " + event.target.errorCode)); reject(new Error("Query failed: " + event.target.errorCode));
}; };
// collect results // collect results
query.onsuccess = (event) => { query.onsuccess = (event) => {
// @ts-ignore
const cursor = event.target.result; const cursor = event.target.result;
if (!cursor) { if (!cursor) {
resolve(results); resolve(results);
@ -442,7 +447,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;
} }
@ -462,7 +467,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;
} }

View file

@ -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;

View file

@ -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<string, string>();
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;

View file

@ -142,14 +142,14 @@ export default class WidgetStore extends AsyncStoreWithClient<IState> {
// If a persistent widget is active, check to see if it's just been removed. // 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 // 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 (persistentWidgetId) {
if ( if (
ActiveWidgetStore.getRoomId(persistentWidgetId) === room.roomId && ActiveWidgetStore.instance.getRoomId(persistentWidgetId) === room.roomId &&
!roomInfo.widgets.some(w => w.id === persistentWidgetId) !roomInfo.widgets.some(w => w.id === persistentWidgetId)
) { ) {
logger.log(`Persistent widget ${persistentWidgetId} removed from room ${room.roomId}: destroying.`); 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<IState> {
// A persistent conference widget indicates that we're participating // A persistent conference widget indicates that we're participating
const widgets = roomInfo.widgets.filter(w => WidgetType.JITSI.matches(w.type)); 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));
} }
} }

View file

@ -266,7 +266,7 @@ export class StopGapWidget extends EventEmitter {
WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging); WidgetMessagingStore.instance.storeMessaging(this.mockWidget, this.messaging);
if (!this.appTileProps.userWidget && this.appTileProps.room) { 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 // 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)) { if (WidgetType.JITSI.matches(this.mockWidget.type)) {
CountlyAnalytics.instance.trackJoinCall(this.appTileProps.room.roomId, true, true); 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(); ev.preventDefault();
this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack this.messaging.transport.reply(ev.detail, <IWidgetApiRequestEmptyData>{}); // ack
} }
@ -406,13 +406,13 @@ export class StopGapWidget extends EventEmitter {
} }
public stop(opts = { forceDestroy: false }) { 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"); logger.log("Skipping destroy - persistent widget");
return; return;
} }
if (!this.started) return; if (!this.started) return;
WidgetMessagingStore.instance.stopMessaging(this.mockWidget); WidgetMessagingStore.instance.stopMessaging(this.mockWidget);
ActiveWidgetStore.delRoomId(this.mockWidget.id); ActiveWidgetStore.instance.delRoomId(this.mockWidget.id);
if (MatrixClientPeg.get()) { if (MatrixClientPeg.get()) {
MatrixClientPeg.get().off('event', this.onEvent); MatrixClientPeg.get().off('event', this.onEvent);

View file

@ -14,9 +14,12 @@ See the License for the specific language governing permissions and
limitations under the License. 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 // Find a protocol 'instance' with a given instance_id
// in the supplied protocols dict // in the supplied protocols dict
export function instanceForInstanceId(protocols, instanceId) { export function instanceForInstanceId(protocols: Protocols, instanceId: string): IInstance {
if (!instanceId) return null; if (!instanceId) return null;
for (const proto of Object.keys(protocols)) { for (const proto of Object.keys(protocols)) {
if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue; 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 // given an instance_id, return the name of the protocol for
// that instance ID in the supplied protocols dict // 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; if (!instanceId) return null;
for (const proto of Object.keys(protocols)) { for (const proto of Object.keys(protocols)) {
if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue; if (!protocols[proto].instances && protocols[proto].instances instanceof Array) continue;

View file

@ -17,7 +17,7 @@ limitations under the License.
import SdkConfig from '../SdkConfig'; import SdkConfig from '../SdkConfig';
import { MatrixClientPeg } from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
export function getHostingLink(campaign) { export function getHostingLink(campaign: string): string {
const hostingLink = SdkConfig.get().hosting_signup_link; const hostingLink = SdkConfig.get().hosting_signup_link;
if (!hostingLink) return null; if (!hostingLink) return null;
if (!campaign) return hostingLink; if (!campaign) return hostingLink;
@ -27,7 +27,7 @@ export function getHostingLink(campaign) {
try { try {
const hostingUrl = new URL(hostingLink); const hostingUrl = new URL(hostingLink);
hostingUrl.searchParams.set("utm_campaign", campaign); hostingUrl.searchParams.set("utm_campaign", campaign);
return hostingUrl.format(); return hostingUrl.toString();
} catch (e) { } catch (e) {
return hostingLink; return hostingLink;
} }

View file

@ -17,14 +17,14 @@ limitations under the License.
import { MatrixClientPeg } from '../MatrixClientPeg'; import { MatrixClientPeg } from '../MatrixClientPeg';
import { _t } from '../languageHandler'; import { _t } from '../languageHandler';
export function getNameForEventRoom(userId, roomId) { export function getNameForEventRoom(userId: string, roomId: string): string {
const client = MatrixClientPeg.get(); const client = MatrixClientPeg.get();
const room = client.getRoom(roomId); const room = client.getRoom(roomId);
const member = room && room.getMember(userId); const member = room && room.getMember(userId);
return member ? member.name : userId; return member ? member.name : userId;
} }
export function userLabelForEventRoom(userId, roomId) { export function userLabelForEventRoom(userId: string, roomId: string): string {
const name = getNameForEventRoom(userId, roomId); const name = getNameForEventRoom(userId, roomId);
if (name !== userId) { if (name !== userId) {
return _t("%(name)s (%(userId)s)", { name, userId }); return _t("%(name)s (%(userId)s)", { name, userId });

View file

@ -26,17 +26,17 @@ const subtleCrypto = window.crypto.subtle || window.crypto.webkitSubtle;
* Make an Error object which has a friendlyText property which is already * Make an Error object which has a friendlyText property which is already
* translated and suitable for showing to the user. * 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 * @param {string} friendlyText
* @returns {Error} * @returns {{message: string, friendlyText: string}}
*/ */
function friendlyError(msg, friendlyText) { function friendlyError(
const e = new Error(msg); message: string, friendlyText: string,
e.friendlyText = friendlyText; ): { message: string, friendlyText: string } {
return e; return { message, friendlyText };
} }
function cryptoFailMsg() { function cryptoFailMsg(): string {
return _t('Your browser does not support the required cryptography extensions'); 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<string> {
const body = unpackMegolmKeyFile(data); const body = unpackMegolmKeyFile(data);
const brand = SdkConfig.get().brand; const brand = SdkConfig.get().brand;
@ -124,7 +124,11 @@ export async function decryptMegolmKeyFile(data, password) {
* key-derivation function. * key-derivation function.
* @return {Promise<ArrayBuffer>} promise for encrypted output * @return {Promise<ArrayBuffer>} 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<ArrayBuffer> {
options = options || {}; options = options || {};
const kdfRounds = options.kdf_rounds || 500000; const kdfRounds = options.kdf_rounds || 500000;
@ -196,7 +200,7 @@ export async function encryptMegolmKeyFile(data, password, options) {
* @param {String} password password * @param {String} password password
* @return {Promise<[CryptoKey, CryptoKey]>} promise for [aes key, hmac key] * @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(); const start = new Date();
let key; let key;
@ -229,7 +233,7 @@ async function deriveKeys(salt, iterations, password) {
} }
const now = new Date(); 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 aesKey = keybits.slice(0, 32);
const hmacKey = keybits.slice(32); const hmacKey = keybits.slice(32);
@ -271,7 +275,7 @@ const TRAILER_LINE = '-----END MEGOLM SESSION DATA-----';
* @param {ArrayBuffer} data input file * @param {ArrayBuffer} data input file
* @return {Uint8Array} unbase64ed content * @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 // 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 // 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. // 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 // look for the start line
let lineStart = 0; let lineStart = 0;
// eslint-disable-next-line no-constant-condition
while (1) { while (1) {
const lineEnd = fileStr.indexOf('\n', lineStart); const lineEnd = fileStr.indexOf('\n', lineStart);
if (lineEnd < 0) { if (lineEnd < 0) {
@ -297,6 +302,7 @@ function unpackMegolmKeyFile(data) {
const dataStart = lineStart; const dataStart = lineStart;
// look for the end line // look for the end line
// eslint-disable-next-line no-constant-condition
while (1) { while (1) {
const lineEnd = fileStr.indexOf('\n', lineStart); const lineEnd = fileStr.indexOf('\n', lineStart);
const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim(); const line = fileStr.slice(lineStart, lineEnd < 0 ? undefined : lineEnd).trim();
@ -324,7 +330,7 @@ function unpackMegolmKeyFile(data) {
* @param {Uint8Array} data raw data * @param {Uint8Array} data raw data
* @return {ArrayBuffer} formatted file * @return {ArrayBuffer} formatted file
*/ */
function packMegolmKeyFile(data) { function packMegolmKeyFile(data: Uint8Array): ArrayBuffer {
// we split into lines before base64ing, because encodeBase64 doesn't deal // we split into lines before base64ing, because encodeBase64 doesn't deal
// terribly well with large arrays. // terribly well with large arrays.
const LINE_LENGTH = (72 * 4 / 3); const LINE_LENGTH = (72 * 4 / 3);
@ -347,7 +353,7 @@ function packMegolmKeyFile(data) {
* @param {Uint8Array} uint8Array The data to encode. * @param {Uint8Array} uint8Array The data to encode.
* @return {string} The base64. * @return {string} The base64.
*/ */
function encodeBase64(uint8Array) { function encodeBase64(uint8Array: Uint8Array): string {
// Misinterpt the Uint8Array as Latin-1. // Misinterpt the Uint8Array as Latin-1.
// window.btoa expects a unicode string with codepoints in the range 0-255. // window.btoa expects a unicode string with codepoints in the range 0-255.
const latin1String = String.fromCharCode.apply(null, uint8Array); const latin1String = String.fromCharCode.apply(null, uint8Array);
@ -360,7 +366,7 @@ function encodeBase64(uint8Array) {
* @param {string} base64 The base64 to decode. * @param {string} base64 The base64 to decode.
* @return {Uint8Array} The decoded data. * @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. // window.atob returns a unicode string with codepoints in the range 0-255.
const latin1String = window.atob(base64); const latin1String = window.atob(base64);
// Encode the string as a Uint8Array // Encode the string as a Uint8Array