Merge branches 'develop' and 't3chguy/room-list/13981' of github.com:matrix-org/matrix-react-sdk into t3chguy/room-list/13981

 Conflicts:
	src/@types/global.d.ts
This commit is contained in:
Michael Telatynski 2020-07-21 17:56:30 +01:00
commit 95854a2f67
48 changed files with 1031 additions and 487 deletions

View file

@ -11,111 +11,36 @@ const path = require('path');
const matrixJsSdkPath = path.join(path.dirname(require.resolve('matrix-js-sdk')), '..'); const matrixJsSdkPath = path.join(path.dirname(require.resolve('matrix-js-sdk')), '..');
module.exports = { module.exports = {
extends: ["matrix-org", "matrix-org/react-legacy"],
parser: "babel-eslint", parser: "babel-eslint",
extends: [matrixJsSdkPath + "/.eslintrc.js"],
plugins: [ env: {
"react", browser: true,
"react-hooks", node: true,
"flowtype", },
"babel"
],
globals: { globals: {
LANGUAGES_FILE: "readonly", LANGUAGES_FILE: "readonly",
}, },
env: {
es6: true,
},
parserOptions: {
ecmaFeatures: {
jsx: true,
legacyDecorators: true,
}
},
rules: { rules: {
// eslint's built in no-invalid-this rule breaks with class properties // Things we do that break the ideal style
"no-invalid-this": "off", "no-constant-condition": "off",
// so we replace it with a version that is class property aware "prefer-promise-reject-errors": "off",
"babel/no-invalid-this": "error", "no-async-promise-executor": "off",
"quotes": "off",
"indent": "off",
},
// We appear to follow this most of the time, so let's enforce it instead overrides: [{
// of occasionally following it (or catching it in review) files: ["src/**/*.{ts, tsx}"],
"keyword-spacing": "error", "extends": ["matrix-org/ts"],
"rules": {
// We disable this while we're transitioning
"@typescript-eslint/no-explicit-any": "off",
// We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off",
/** react **/ "quotes": "off",
// This just uses the react plugin to help eslint known when "no-extra-boolean-cast": "off",
// variables have been used in JSX
"react/jsx-uses-vars": "error",
// Don't mark React as unused if we're using JSX
"react/jsx-uses-react": "error",
// bind or arrow function in props causes performance issues
// (but we currently use them in some places)
// It's disabled here, but we should using it sparingly.
"react/jsx-no-bind": "off",
"react/jsx-key": ["error"],
// Components in JSX should always be defined.
"react/jsx-no-undef": "error",
// Assert no spacing in JSX curly brackets
// <Element prop={ consideredError} prop={notConsideredError} />
//
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-curly-spacing.md
//
// Disabled for now - if anything we'd like to *enforce* spacing in JSX
// curly brackets for legibility, but in practice it's not clear that the
// consistency particularly improves legibility here. --Matthew
//
// "react/jsx-curly-spacing": ["error", {"when": "never", "children": {"when": "always"}}],
// Assert spacing before self-closing JSX tags, and no spacing before or
// after the closing slash, and no spacing after the opening bracket of
// the opening tag or closing tag.
//
// https://github.com/yannickcr/eslint-plugin-react/blob/HEAD/docs/rules/jsx-tag-spacing.md
"react/jsx-tag-spacing": ["error"],
/** flowtype **/
"flowtype/require-parameter-type": ["warn", {
"excludeArrowFunctions": true,
}],
"flowtype/define-flow-type": "warn",
"flowtype/require-return-type": ["warn",
"always",
{
"annotateUndefined": "never",
"excludeArrowFunctions": true,
} }
],
"flowtype/space-after-type-colon": ["warn", "always"],
"flowtype/space-before-type-colon": ["warn", "never"],
/*
* things that are errors in the js-sdk config that the current
* code does not adhere to, turned down to warn
*/
"max-len": ["warn", {
// apparently people believe the length limit shouldn't apply
// to JSX.
ignorePattern: '^\\s*<',
ignoreComments: true,
ignoreRegExpLiterals: true,
code: 120,
}], }],
"valid-jsdoc": ["warn"],
"new-cap": ["warn"],
"key-spacing": ["warn"],
"prefer-const": ["warn"],
// crashes currently: https://github.com/eslint/eslint/issues/6274
"generator-star-spacing": "off",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
},
settings: {
flowtype: {
onlyFilesWithFlowAnnotation: true
},
},
}; };

View file

@ -45,9 +45,8 @@
"start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all", "start": "echo THIS IS FOR LEGACY PURPOSES ONLY. && yarn start:all",
"start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"", "start:all": "concurrently --kill-others-on-fail --prefix \"{time} [{name}]\" -n build,reskindex \"yarn start:build\" \"yarn reskindex:watch\"",
"start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"", "start:build": "babel src -w -s -d lib --verbose --extensions \".ts,.js\"",
"lint": "yarn lint:types && yarn lint:ts && yarn lint:js && yarn lint:style", "lint": "yarn lint:types && yarn lint:js && yarn lint:style",
"lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test", "lint:js": "eslint --max-warnings 0 --ignore-path .eslintignore.errorfiles src test",
"lint:ts": "tslint --project ./tsconfig.json -t stylish",
"lint:types": "tsc --noEmit --jsx react", "lint:types": "tsc --noEmit --jsx react",
"lint:style": "stylelint 'res/css/**/*.scss'", "lint:style": "stylelint 'res/css/**/*.scss'",
"test": "jest", "test": "jest",
@ -132,14 +131,17 @@
"@types/react-transition-group": "^4.4.0", "@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^1.23.3", "@types/sanitize-html": "^1.23.3",
"@types/zxcvbn": "^4.4.0", "@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"babel-jest": "^24.9.0", "babel-jest": "^24.9.0",
"chokidar": "^3.3.1", "chokidar": "^3.3.1",
"concurrently": "^4.0.1", "concurrently": "^4.0.1",
"enzyme": "^3.10.0", "enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1", "enzyme-adapter-react-16": "^1.15.1",
"eslint": "^5.12.0", "eslint": "7.3.1",
"eslint-config-google": "^0.7.1", "eslint-config-google": "^0.7.1",
"eslint-config-matrix-org": "^0.1.2",
"eslint-plugin-babel": "^5.2.1", "eslint-plugin-babel": "^5.2.1",
"eslint-plugin-flowtype": "^2.30.0", "eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-jest": "^23.0.4", "eslint-plugin-jest": "^23.0.4",
@ -160,7 +162,6 @@
"stylelint": "^9.10.1", "stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0", "stylelint-config-standard": "^18.2.0",
"stylelint-scss": "^3.9.0", "stylelint-scss": "^3.9.0",
"tslint": "^5.20.1",
"typescript": "^3.7.3", "typescript": "^3.7.3",
"walk": "^2.3.9", "walk": "^2.3.9",
"webpack": "^4.20.2", "webpack": "^4.20.2",

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// TODO: Update design for custom tags to match new designs
.mx_LeftPanel_tagPanelContainer { .mx_LeftPanel_tagPanelContainer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -50,7 +52,7 @@ limitations under the License.
background-color: $accent-color-alt; background-color: $accent-color-alt;
width: 5px; width: 5px;
position: absolute; position: absolute;
left: -15px; left: -9px;
border-radius: 0 3px 3px 0; border-radius: 0 3px 3px 0;
top: 2px; // 10 [padding-top] - (56 - 40)/2 top: 12px; // just feels right (see comment above about designs needing to be updated)
} }

View file

@ -24,6 +24,7 @@ import { RoomListStoreClass } from "../stores/room-list/RoomListStore";
import { PlatformPeg } from "../PlatformPeg"; import { PlatformPeg } from "../PlatformPeg";
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {IntegrationManagers} from "../integrations/IntegrationManagers";
import {ModalManager} from "../Modal";
import {ActiveRoomObserver} from "../ActiveRoomObserver"; import {ActiveRoomObserver} from "../ActiveRoomObserver";
declare global { declare global {
@ -34,16 +35,16 @@ declare global {
init: () => Promise<void>; init: () => Promise<void>;
}; };
mx_ContentMessages: ContentMessages; mxContentMessages: ContentMessages;
mx_ToastStore: ToastStore; mxToastStore: ToastStore;
mx_DeviceListener: DeviceListener; mxDeviceListener: DeviceListener;
mx_RebrandListener: RebrandListener; mxRebrandListener: RebrandListener;
mx_RoomListStore: RoomListStoreClass; mxRoomListStore: RoomListStoreClass;
mx_RoomListLayoutStore: RoomListLayoutStore; mxRoomListLayoutStore: RoomListLayoutStore;
mx_ActiveRoomObserver: ActiveRoomObserver; mx_ActiveRoomObserver: ActiveRoomObserver;
mxPlatformPeg: PlatformPeg; mxPlatformPeg: PlatformPeg;
mxIntegrationManagers: typeof IntegrationManagers; mxIntegrationManagers: typeof IntegrationManagers;
singletonModalManager: ModalManager;
} }
// workaround for https://github.com/microsoft/TypeScript/issues/30933 // workaround for https://github.com/microsoft/TypeScript/issues/30933

View file

@ -386,7 +386,7 @@ export default class ContentMessages {
const isQuoting = Boolean(RoomViewStore.getQuotingEvent()); const isQuoting = Boolean(RoomViewStore.getQuotingEvent());
if (isQuoting) { if (isQuoting) {
const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog"); const QuestionDialog = sdk.getComponent("dialogs.QuestionDialog");
const {finished} = Modal.createTrackedDialog('Upload Reply Warning', '', QuestionDialog, { const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Reply Warning', '', QuestionDialog, {
title: _t('Replying With Files'), title: _t('Replying With Files'),
description: ( description: (
<div>{_t( <div>{_t(
@ -397,7 +397,7 @@ export default class ContentMessages {
hasCancelButton: true, hasCancelButton: true,
button: _t("Continue"), button: _t("Continue"),
}); });
const [shouldUpload]: [boolean] = await finished; const [shouldUpload] = await finished;
if (!shouldUpload) return; if (!shouldUpload) return;
} }
@ -420,12 +420,12 @@ export default class ContentMessages {
if (tooBigFiles.length > 0) { if (tooBigFiles.length > 0) {
const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog"); const UploadFailureDialog = sdk.getComponent("dialogs.UploadFailureDialog");
const {finished} = Modal.createTrackedDialog('Upload Failure', '', UploadFailureDialog, { const {finished} = Modal.createTrackedDialog<[boolean]>('Upload Failure', '', UploadFailureDialog, {
badFiles: tooBigFiles, badFiles: tooBigFiles,
totalFiles: files.length, totalFiles: files.length,
contentMessages: this, contentMessages: this,
}); });
const [shouldContinue]: [boolean] = await finished; const [shouldContinue] = await finished;
if (!shouldContinue) return; if (!shouldContinue) return;
} }
@ -437,12 +437,12 @@ export default class ContentMessages {
for (let i = 0; i < okFiles.length; ++i) { for (let i = 0; i < okFiles.length; ++i) {
const file = okFiles[i]; const file = okFiles[i];
if (!uploadAll) { if (!uploadAll) {
const {finished} = Modal.createTrackedDialog('Upload Files confirmation', '', UploadConfirmDialog, { const {finished} = Modal.createTrackedDialog<[boolean, boolean]>('Upload Files confirmation', '', UploadConfirmDialog, {
file, file,
currentIndex: i, currentIndex: i,
totalFiles: okFiles.length, totalFiles: okFiles.length,
}); });
const [shouldContinue, shouldUploadAll]: [boolean, boolean] = await finished; const [shouldContinue, shouldUploadAll] = await finished;
if (!shouldContinue) break; if (!shouldContinue) break;
if (shouldUploadAll) { if (shouldUploadAll) {
uploadAll = true; uploadAll = true;
@ -621,9 +621,9 @@ export default class ContentMessages {
} }
static sharedInstance() { static sharedInstance() {
if (window.mx_ContentMessages === undefined) { if (window.mxContentMessages === undefined) {
window.mx_ContentMessages = new ContentMessages(); window.mxContentMessages = new ContentMessages();
} }
return window.mx_ContentMessages; return window.mxContentMessages;
} }
} }

View file

@ -17,16 +17,16 @@ limitations under the License.
import {MatrixClientPeg} from './MatrixClientPeg'; import {MatrixClientPeg} from './MatrixClientPeg';
import { import {
hideToast as hideBulkUnverifiedSessionsToast, hideToast as hideBulkUnverifiedSessionsToast,
showToast as showBulkUnverifiedSessionsToast showToast as showBulkUnverifiedSessionsToast,
} from "./toasts/BulkUnverifiedSessionsToast"; } from "./toasts/BulkUnverifiedSessionsToast";
import { import {
hideToast as hideSetupEncryptionToast, hideToast as hideSetupEncryptionToast,
Kind as SetupKind, Kind as SetupKind,
showToast as showSetupEncryptionToast showToast as showSetupEncryptionToast,
} from "./toasts/SetupEncryptionToast"; } from "./toasts/SetupEncryptionToast";
import { import {
hideToast as hideUnverifiedSessionsToast, hideToast as hideUnverifiedSessionsToast,
showToast as showUnverifiedSessionsToast showToast as showUnverifiedSessionsToast,
} from "./toasts/UnverifiedSessionToast"; } from "./toasts/UnverifiedSessionToast";
import {privateShouldBeEncrypted} from "./createRoom"; import {privateShouldBeEncrypted} from "./createRoom";
@ -48,8 +48,8 @@ export default class DeviceListener {
private displayingToastsForDeviceIds = new Set<string>(); private displayingToastsForDeviceIds = new Set<string>();
static sharedInstance() { static sharedInstance() {
if (!window.mx_DeviceListener) window.mx_DeviceListener = new DeviceListener(); if (!window.mxDeviceListener) window.mxDeviceListener = new DeviceListener();
return window.mx_DeviceListener; return window.mxDeviceListener;
} }
start() { start() {

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,6 +18,8 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames';
import Analytics from './Analytics'; import Analytics from './Analytics';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import {defer} from './utils/promise'; import {defer} from './utils/promise';
@ -25,36 +28,48 @@ import AsyncWrapper from './AsyncWrapper';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
class ModalManager { interface IModal<T extends any[]> {
constructor() { elem: React.ReactNode;
this._counter = 0; className?: string;
beforeClosePromise?: Promise<boolean>;
closeReason?: string;
onBeforeClose?(reason?: string): Promise<boolean>;
onFinished(...args: T): void;
close(...args: T): void;
}
interface IHandle<T extends any[]> {
finished: Promise<T>;
close(...args: T): void;
}
interface IProps<T extends any[]> {
onFinished?(...args: T): void;
// TODO improve typing here once all Modals are TS and we can exhaustively check the props
[key: string]: any;
}
interface IOptions<T extends any[]> {
onBeforeClose?: IModal<T>["onBeforeClose"];
}
type ParametersWithoutFirst<T extends (...args: any) => any> = T extends (a: any, ...args: infer P) => any ? P : never;
export class ModalManager {
private counter = 0;
// The modal to prioritise over all others. If this is set, only show // The modal to prioritise over all others. If this is set, only show
// this modal. Remove all other modals from the stack when this modal // this modal. Remove all other modals from the stack when this modal
// is closed. // is closed.
this._priorityModal = null; private priorityModal: IModal<any> = null;
// The modal to keep open underneath other modals if possible. Useful // The modal to keep open underneath other modals if possible. Useful
// for cases like Settings where the modal should remain open while the // for cases like Settings where the modal should remain open while the
// user is prompted for more information/errors. // user is prompted for more information/errors.
this._staticModal = null; private staticModal: IModal<any> = null;
// A list of the modals we have stacked up, with the most recent at [0] // A list of the modals we have stacked up, with the most recent at [0]
// Neither the static nor priority modal will be in this list. // Neither the static nor priority modal will be in this list.
this._modals = [ private modals: IModal<any>[] = [];
/* {
elem: React component for this dialog
onFinished: caller-supplied onFinished callback
className: CSS class for the dialog wrapper div
} */
];
this.onBackgroundClick = this.onBackgroundClick.bind(this); private static getOrCreateContainer() {
}
hasDialogs() {
return this._priorityModal || this._staticModal || this._modals.length > 0;
}
getOrCreateContainer() {
let container = document.getElementById(DIALOG_CONTAINER_ID); let container = document.getElementById(DIALOG_CONTAINER_ID);
if (!container) { if (!container) {
@ -66,7 +81,7 @@ class ModalManager {
return container; return container;
} }
getOrCreateStaticContainer() { private static getOrCreateStaticContainer() {
let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID); let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID);
if (!container) { if (!container) {
@ -78,63 +93,99 @@ class ModalManager {
return container; return container;
} }
createTrackedDialog(analyticsAction, analyticsInfo, ...rest) { public hasDialogs() {
return this.priorityModal || this.staticModal || this.modals.length > 0;
}
public createTrackedDialog<T extends any[]>(
analyticsAction: string,
analyticsInfo: string,
...rest: Parameters<ModalManager["createDialog"]>
) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.createDialog(...rest); return this.createDialog<T>(...rest);
} }
appendTrackedDialog(analyticsAction, analyticsInfo, ...rest) { public appendTrackedDialog<T extends any[]>(
analyticsAction: string,
analyticsInfo: string,
...rest: Parameters<ModalManager["appendDialog"]>
) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.appendDialog(...rest); return this.appendDialog<T>(...rest);
} }
createDialog(Element, ...rest) { public createDialog<T extends any[]>(
return this.createDialogAsync(Promise.resolve(Element), ...rest); Element: React.ComponentType,
...rest: ParametersWithoutFirst<ModalManager["createDialogAsync"]>
) {
return this.createDialogAsync<T>(Promise.resolve(Element), ...rest);
} }
appendDialog(Element, ...rest) { public appendDialog<T extends any[]>(
return this.appendDialogAsync(Promise.resolve(Element), ...rest); Element: React.ComponentType,
...rest: ParametersWithoutFirst<ModalManager["appendDialogAsync"]>
) {
return this.appendDialogAsync<T>(Promise.resolve(Element), ...rest);
} }
createTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { public createTrackedDialogAsync<T extends any[]>(
analyticsAction: string,
analyticsInfo: string,
...rest: Parameters<ModalManager["appendDialogAsync"]>
) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.createDialogAsync(...rest); return this.createDialogAsync<T>(...rest);
} }
appendTrackedDialogAsync(analyticsAction, analyticsInfo, ...rest) { public appendTrackedDialogAsync<T extends any[]>(
analyticsAction: string,
analyticsInfo: string,
...rest: Parameters<ModalManager["appendDialogAsync"]>
) {
Analytics.trackEvent('Modal', analyticsAction, analyticsInfo); Analytics.trackEvent('Modal', analyticsAction, analyticsInfo);
return this.appendDialogAsync(...rest); return this.appendDialogAsync<T>(...rest);
} }
_buildModal(prom, props, className, options) { private buildModal<T extends any[]>(
const modal = {}; prom: Promise<React.ComponentType>,
props?: IProps<T>,
className?: string,
options?: IOptions<T>
) {
const modal: IModal<T> = {
onFinished: props ? props.onFinished : null,
onBeforeClose: options.onBeforeClose,
beforeClosePromise: null,
closeReason: null,
className,
// these will be set below but we need an object reference to pass to getCloseFn before we can do that
elem: null,
close: null,
};
// never call this from onFinished() otherwise it will loop // never call this from onFinished() otherwise it will loop
const [closeDialog, onFinishedProm] = this._getCloseFn(modal, props); const [closeDialog, onFinishedProm] = this.getCloseFn<T>(modal, props);
// don't attempt to reuse the same AsyncWrapper for different dialogs, // don't attempt to reuse the same AsyncWrapper for different dialogs,
// otherwise we'll get confused. // otherwise we'll get confused.
const modalCount = this._counter++; const modalCount = this.counter++;
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished // FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the dialog from a button click! // property set here so you can't close the dialog from a button click!
modal.elem = ( modal.elem = <AsyncWrapper key={modalCount} prom={prom} {...props} onFinished={closeDialog} />;
<AsyncWrapper key={modalCount} prom={prom} {...props}
onFinished={closeDialog} />
);
modal.onFinished = props ? props.onFinished : null;
modal.className = className;
modal.onBeforeClose = options.onBeforeClose;
modal.beforeClosePromise = null;
modal.close = closeDialog; modal.close = closeDialog;
modal.closeReason = null;
return {modal, closeDialog, onFinishedProm}; return {modal, closeDialog, onFinishedProm};
} }
_getCloseFn(modal, props) { private getCloseFn<T extends any[]>(
const deferred = defer(); modal: IModal<T>,
return [async (...args) => { props: IProps<T>
): [IHandle<T>["close"], IHandle<T>["finished"]] {
const deferred = defer<T>();
return [async (...args: T) => {
if (modal.beforeClosePromise) { if (modal.beforeClosePromise) {
await modal.beforeClosePromise; await modal.beforeClosePromise;
} else if (modal.onBeforeClose) { } else if (modal.onBeforeClose) {
@ -147,26 +198,26 @@ class ModalManager {
} }
deferred.resolve(args); deferred.resolve(args);
if (props && props.onFinished) props.onFinished.apply(null, args); if (props && props.onFinished) props.onFinished.apply(null, args);
const i = this._modals.indexOf(modal); const i = this.modals.indexOf(modal);
if (i >= 0) { if (i >= 0) {
this._modals.splice(i, 1); this.modals.splice(i, 1);
} }
if (this._priorityModal === modal) { if (this.priorityModal === modal) {
this._priorityModal = null; this.priorityModal = null;
// XXX: This is destructive // XXX: This is destructive
this._modals = []; this.modals = [];
} }
if (this._staticModal === modal) { if (this.staticModal === modal) {
this._staticModal = null; this.staticModal = null;
// XXX: This is destructive // XXX: This is destructive
this._modals = []; this.modals = [];
} }
this._reRender(); this.reRender();
}, deferred.promise]; }, deferred.promise];
} }
@ -207,38 +258,49 @@ class ModalManager {
* @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog * @param {onBeforeClose} options.onBeforeClose a callback to decide whether to close the dialog
* @returns {object} Object with 'close' parameter being a function that will close the dialog * @returns {object} Object with 'close' parameter being a function that will close the dialog
*/ */
createDialogAsync(prom, props, className, isPriorityModal, isStaticModal, options = {}) { private createDialogAsync<T extends any[]>(
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options); prom: Promise<React.ComponentType>,
props?: IProps<T>,
className?: string,
isPriorityModal = false,
isStaticModal = false,
options: IOptions<T> = {}
): IHandle<T> {
const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, options);
if (isPriorityModal) { if (isPriorityModal) {
// XXX: This is destructive // XXX: This is destructive
this._priorityModal = modal; this.priorityModal = modal;
} else if (isStaticModal) { } else if (isStaticModal) {
// This is intentionally destructive // This is intentionally destructive
this._staticModal = modal; this.staticModal = modal;
} else { } else {
this._modals.unshift(modal); this.modals.unshift(modal);
} }
this._reRender(); this.reRender();
return { return {
close: closeDialog, close: closeDialog,
finished: onFinishedProm, finished: onFinishedProm,
}; };
} }
appendDialogAsync(prom, props, className) { private appendDialogAsync<T extends any[]>(
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {}); prom: Promise<React.ComponentType>,
props?: IProps<T>,
className?: string
): IHandle<T> {
const {modal, closeDialog, onFinishedProm} = this.buildModal<T>(prom, props, className, {});
this._modals.push(modal); this.modals.push(modal);
this._reRender(); this.reRender();
return { return {
close: closeDialog, close: closeDialog,
finished: onFinishedProm, finished: onFinishedProm,
}; };
} }
onBackgroundClick() { private onBackgroundClick = () => {
const modal = this._getCurrentModal(); const modal = this.getCurrentModal();
if (!modal) { if (!modal) {
return; return;
} }
@ -249,21 +311,21 @@ class ModalManager {
modal.closeReason = "backgroundClick"; modal.closeReason = "backgroundClick";
modal.close(); modal.close();
modal.closeReason = null; modal.closeReason = null;
};
private getCurrentModal(): IModal<any> {
return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal);
} }
_getCurrentModal() { private reRender() {
return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal); if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
}
_reRender() {
if (this._modals.length === 0 && !this._priorityModal && !this._staticModal) {
// If there is no modal to render, make all of Riot available // If there is no modal to render, make all of Riot available
// to screen reader users again // to screen reader users again
dis.dispatch({ dis.dispatch({
action: 'aria_unhide_main_app', action: 'aria_unhide_main_app',
}); });
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
return; return;
} }
@ -274,49 +336,48 @@ class ModalManager {
action: 'aria_hide_main_app', action: 'aria_hide_main_app',
}); });
if (this._staticModal) { if (this.staticModal) {
const classes = "mx_Dialog_wrapper mx_Dialog_staticWrapper " const classes = classNames("mx_Dialog_wrapper mx_Dialog_staticWrapper", this.staticModal.className);
+ (this._staticModal.className ? this._staticModal.className : '');
const staticDialog = ( const staticDialog = (
<div className={classes}> <div className={classes}>
<div className="mx_Dialog"> <div className="mx_Dialog">
{ this._staticModal.elem } { this.staticModal.elem }
</div> </div>
<div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick}></div> <div className="mx_Dialog_background mx_Dialog_staticBackground" onClick={this.onBackgroundClick} />
</div> </div>
); );
ReactDOM.render(staticDialog, this.getOrCreateStaticContainer()); ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer());
} else { } else {
// This is safe to call repeatedly if we happen to do that // This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
} }
const modal = this._getCurrentModal(); const modal = this.getCurrentModal();
if (modal !== this._staticModal) { if (modal !== this.staticModal) {
const classes = "mx_Dialog_wrapper " const classes = classNames("mx_Dialog_wrapper", modal.className, {
+ (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '') mx_Dialog_wrapperWithStaticUnder: this.staticModal,
+ (modal.className ? modal.className : ''); });
const dialog = ( const dialog = (
<div className={classes}> <div className={classes}>
<div className="mx_Dialog"> <div className="mx_Dialog">
{modal.elem} {modal.elem}
</div> </div>
<div className="mx_Dialog_background" onClick={this.onBackgroundClick}></div> <div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
</div> </div>
); );
ReactDOM.render(dialog, this.getOrCreateContainer()); ReactDOM.render(dialog, ModalManager.getOrCreateContainer());
} else { } else {
// This is safe to call repeatedly if we happen to do that // This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer()); ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
} }
} }
} }
if (!global.singletonModalManager) { if (!window.singletonModalManager) {
global.singletonModalManager = new ModalManager(); window.singletonModalManager = new ModalManager();
} }
export default global.singletonModalManager; export default window.singletonModalManager;

View file

@ -67,8 +67,8 @@ export default class RebrandListener {
private nagAgainAt?: number = null; private nagAgainAt?: number = null;
static sharedInstance() { static sharedInstance() {
if (!window.mx_RebrandListener) window.mx_RebrandListener = new RebrandListener(); if (!window.mxRebrandListener) window.mxRebrandListener = new RebrandListener();
return window.mx_RebrandListener; return window.mxRebrandListener;
} }
constructor() { constructor() {

View file

@ -401,14 +401,16 @@ export const Commands = [
// If we need an identity server but don't have one, things // If we need an identity server but don't have one, things
// get a bit more complex here, but we try to show something // get a bit more complex here, but we try to show something
// meaningful. // meaningful.
let finished = Promise.resolve(); let prom = Promise.resolve();
if ( if (
getAddressType(address) === 'email' && getAddressType(address) === 'email' &&
!MatrixClientPeg.get().getIdentityServerUrl() !MatrixClientPeg.get().getIdentityServerUrl()
) { ) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl(); const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) { if (defaultIdentityServerUrl) {
({ finished } = Modal.createTrackedDialog('Slash Commands', 'Identity server', const { finished } = Modal.createTrackedDialog<[boolean]>(
'Slash Commands',
'Identity server',
QuestionDialog, { QuestionDialog, {
title: _t("Use an identity server"), title: _t("Use an identity server"),
description: <p>{_t( description: <p>{_t(
@ -421,9 +423,9 @@ export const Commands = [
)}</p>, )}</p>,
button: _t("Continue"), button: _t("Continue"),
}, },
)); );
finished = finished.then(([useDefault]: any) => { prom = finished.then(([useDefault]) => {
if (useDefault) { if (useDefault) {
useDefaultIdentityServer(); useDefaultIdentityServer();
return; return;
@ -435,7 +437,7 @@ export const Commands = [
} }
} }
const inviter = new MultiInviter(roomId); const inviter = new MultiInviter(roomId);
return success(finished.then(() => { return success(prom.then(() => {
return inviter.invite([address]); return inviter.invite([address]);
}).then(() => { }).then(() => {
if (inviter.getCompletionState(address) !== "invited") { if (inviter.getCompletionState(address) !== "invited") {

View file

@ -108,7 +108,7 @@ export default class RoomListActions {
) { ) {
const promiseToDelete = matrixClient.deleteRoomTag( const promiseToDelete = matrixClient.deleteRoomTag(
roomId, oldTag, roomId, oldTag,
).catch(function (err) { ).catch(function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to remove tag " + oldTag + " from room: " + err); console.error("Failed to remove tag " + oldTag + " from room: " + err);
Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to remove tag from room', '', ErrorDialog, {
@ -128,7 +128,7 @@ export default class RoomListActions {
// at least be an empty object. // at least be an empty object.
metaData = metaData || {}; metaData = metaData || {};
const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function (err) { const promiseToAdd = matrixClient.setRoomTag(roomId, newTag, metaData).catch(function(err) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog"); const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
console.error("Failed to add tag " + newTag + " to room: " + err); console.error("Failed to add tag " + newTag + " to room: " + err);
Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, { Modal.createTrackedDialog('Failed to add tag to room', '', ErrorDialog, {

View file

@ -22,7 +22,6 @@ import { AsyncActionPayload } from "../dispatcher/payloads";
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
export default class TagOrderActions { export default class TagOrderActions {
/** /**
* Creates an action thunk that will do an asynchronous request to * Creates an action thunk that will do an asynchronous request to
* move a tag in TagOrderStore to destinationIx. * move a tag in TagOrderStore to destinationIx.

View file

@ -115,7 +115,7 @@ export default class QueryMatcher<T extends Object> {
const index = resultKey.indexOf(query); const index = resultKey.indexOf(query);
if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) { if (index !== -1 && (!this._options.shouldMatchPrefix || index === 0)) {
matches.push( matches.push(
...candidates.map((candidate) => ({index, ...candidate})) ...candidates.map((candidate) => ({index, ...candidate})),
); );
} }
} }

View file

@ -72,17 +72,17 @@ class CustomRoomTagTile extends React.Component {
const tag = this.props.tag; const tag = this.props.tag;
const avatarHeight = 40; const avatarHeight = 40;
const className = classNames({ const className = classNames({
CustomRoomTagPanel_tileSelected: tag.selected, "CustomRoomTagPanel_tileSelected": tag.selected,
}); });
const name = tag.name; const name = tag.name;
const badge = tag.badge; const badgeNotifState = tag.badgeNotifState;
let badgeElement; let badgeElement;
if (badge) { if (badgeNotifState) {
const badgeClasses = classNames({ const badgeClasses = classNames({
"mx_TagTile_badge": true, "mx_TagTile_badge": true,
"mx_TagTile_badgeHighlight": badge.highlight, "mx_TagTile_badgeHighlight": badgeNotifState.hasMentions,
}); });
badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badge.count)}</div>); badgeElement = (<div className={badgeClasses}>{FormattingUtils.formatCount(badgeNotifState.count)}</div>);
} }
return ( return (

View file

@ -17,6 +17,7 @@ limitations under the License.
import * as React from "react"; import * as React from "react";
import { createRef } from "react"; import { createRef } from "react";
import TagPanel from "./TagPanel"; import TagPanel from "./TagPanel";
import CustomRoomTagPanel from "./CustomRoomTagPanel";
import classNames from "classnames"; import classNames from "classnames";
import dis from "../../dispatcher/dispatcher"; import dis from "../../dispatcher/dispatcher";
import { _t } from "../../languageHandler"; import { _t } from "../../languageHandler";
@ -361,6 +362,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
const tagPanel = !this.state.showTagPanel ? null : ( const tagPanel = !this.state.showTagPanel ? null : (
<div className="mx_LeftPanel_tagPanelContainer"> <div className="mx_LeftPanel_tagPanelContainer">
<TagPanel/> <TagPanel/>
{SettingsStore.isFeatureEnabled("feature_custom_tags") ? <CustomRoomTagPanel /> : null}
</div> </div>
); );

View file

@ -704,6 +704,7 @@ export default class AppTile extends React.Component {
_onReloadWidgetClick() { _onReloadWidgetClick() {
// Reload iframe in this way to avoid cross-origin restrictions // Reload iframe in this way to avoid cross-origin restrictions
// eslint-disable-next-line no-self-assign
this._appFrame.current.src = this._appFrame.current.src; this._appFrame.current.src = this._appFrame.current.src;
} }

View file

@ -156,7 +156,7 @@ export default createReactClass({
<AccessibleButton <AccessibleButton
className="mx_TagTile_context_button" className="mx_TagTile_context_button"
onClick={this.openMenu} onClick={this.openMenu}
ref={this.props.contextMenuButtonRef} inputRef={this.props.contextMenuButtonRef}
> >
{"\u00B7\u00B7\u00B7"} {"\u00B7\u00B7\u00B7"}
</AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />; </AccessibleButton> : <div ref={this.props.contextMenuButtonRef} />;

View file

@ -58,7 +58,7 @@ export default createReactClass({
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>, 'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,
}) })
); );
} else if (accountEnabled) { } else {
previewsForAccount = ( previewsForAccount = (
_t("You have <a>disabled</a> URL previews by default.", {}, { _t("You have <a>disabled</a> URL previews by default.", {}, {
'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>, 'a': (sub)=><a onClick={this._onClickUserSettings} href=''>{ sub }</a>,

View file

@ -26,7 +26,7 @@ import { ResizeNotifier } from "../../../utils/ResizeNotifier";
import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore"; import RoomListStore, { LISTS_UPDATE_EVENT } from "../../../stores/room-list/RoomListStore";
import RoomViewStore from "../../../stores/RoomViewStore"; import RoomViewStore from "../../../stores/RoomViewStore";
import { ITagMap } from "../../../stores/room-list/algorithms/models"; import { ITagMap } from "../../../stores/room-list/algorithms/models";
import { DefaultTagID, TagID } from "../../../stores/room-list/models"; import { DefaultTagID, isCustomTag, TagID } from "../../../stores/room-list/models";
import dis from "../../../dispatcher/dispatcher"; import dis from "../../../dispatcher/dispatcher";
import defaultDispatcher from "../../../dispatcher/dispatcher"; import defaultDispatcher from "../../../dispatcher/dispatcher";
import RoomSublist from "./RoomSublist"; import RoomSublist from "./RoomSublist";
@ -41,6 +41,7 @@ import { Action } from "../../../dispatcher/actions";
import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload"; import { ViewRoomDeltaPayload } from "../../../dispatcher/payloads/ViewRoomDeltaPayload";
import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore"; import { RoomNotificationStateStore } from "../../../stores/notifications/RoomNotificationStateStore";
import SettingsStore from "../../../settings/SettingsStore"; import SettingsStore from "../../../settings/SettingsStore";
import CustomRoomTagStore from "../../../stores/CustomRoomTagStore";
interface IProps { interface IProps {
onKeyDown: (ev: React.KeyboardEvent) => void; onKeyDown: (ev: React.KeyboardEvent) => void;
@ -77,6 +78,7 @@ const ALWAYS_VISIBLE_TAGS: TagID[] = [
interface ITagAesthetics { interface ITagAesthetics {
sectionLabel: string; sectionLabel: string;
sectionLabelRaw?: string;
addRoomLabel?: string; addRoomLabel?: string;
onAddRoom?: (dispatcher: Dispatcher<ActionPayload>) => void; onAddRoom?: (dispatcher: Dispatcher<ActionPayload>) => void;
isInvite: boolean; isInvite: boolean;
@ -130,9 +132,22 @@ const TAG_AESTHETICS: {
}, },
}; };
function customTagAesthetics(tagId: TagID): ITagAesthetics {
if (tagId.startsWith("u.")) {
tagId = tagId.substring(2);
}
return {
sectionLabel: _td("Custom Tag"),
sectionLabelRaw: tagId,
isInvite: false,
defaultHidden: false,
};
}
export default class RoomList extends React.Component<IProps, IState> { export default class RoomList extends React.Component<IProps, IState> {
private searchFilter: NameFilterCondition = new NameFilterCondition(); private searchFilter: NameFilterCondition = new NameFilterCondition();
private dispatcherRef; private dispatcherRef;
private customTagStoreRef;
constructor(props: IProps) { constructor(props: IProps) {
super(props); super(props);
@ -161,12 +176,14 @@ export default class RoomList extends React.Component<IProps, IState> {
public componentDidMount(): void { public componentDidMount(): void {
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.updateLists);
this.customTagStoreRef = CustomRoomTagStore.addListener(this.updateLists);
this.updateLists(); // trigger the first update this.updateLists(); // trigger the first update
} }
public componentWillUnmount() { public componentWillUnmount() {
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.updateLists);
defaultDispatcher.unregister(this.dispatcherRef); defaultDispatcher.unregister(this.dispatcherRef);
if (this.customTagStoreRef) this.customTagStoreRef.remove();
} }
private onAction = (payload: ActionPayload) => { private onAction = (payload: ActionPayload) => {
@ -257,12 +274,18 @@ export default class RoomList extends React.Component<IProps, IState> {
private renderSublists(): React.ReactElement[] { private renderSublists(): React.ReactElement[] {
const components: React.ReactElement[] = []; const components: React.ReactElement[] = [];
for (const orderedTagId of TAG_ORDER) { const tagOrder = TAG_ORDER.reduce((p, c) => {
if (CUSTOM_TAGS_BEFORE_TAG === orderedTagId) { if (c === CUSTOM_TAGS_BEFORE_TAG) {
// Populate custom tags if needed const customTags = Object.keys(this.state.sublists)
// TODO: Custom tags: https://github.com/vector-im/riot-web/issues/14091 .filter(t => isCustomTag(t))
.filter(t => CustomRoomTagStore.getTags()[t]); // isSelected
p.push(...customTags);
} }
p.push(c);
return p;
}, [] as TagID[]);
for (const orderedTagId of tagOrder) {
const orderedRooms = this.state.sublists[orderedTagId] || []; const orderedRooms = this.state.sublists[orderedTagId] || [];
const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null; const extraTiles = orderedTagId === DefaultTagID.Invite ? this.renderCommunityInvites() : null;
const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0); const totalTiles = orderedRooms.length + (extraTiles ? extraTiles.length : 0);
@ -270,7 +293,9 @@ export default class RoomList extends React.Component<IProps, IState> {
continue; // skip tag - not needed continue; // skip tag - not needed
} }
const aesthetics: ITagAesthetics = TAG_AESTHETICS[orderedTagId]; const aesthetics: ITagAesthetics = isCustomTag(orderedTagId)
? customTagAesthetics(orderedTagId)
: TAG_AESTHETICS[orderedTagId];
if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`); if (!aesthetics) throw new Error(`Tag ${orderedTagId} does not have aesthetics`);
const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null; const onAddRoomFn = aesthetics.onAddRoom ? () => aesthetics.onAddRoom(dis) : null;
@ -281,7 +306,7 @@ export default class RoomList extends React.Component<IProps, IState> {
forRooms={true} forRooms={true}
rooms={orderedRooms} rooms={orderedRooms}
startAsHidden={aesthetics.defaultHidden} startAsHidden={aesthetics.defaultHidden}
label={_t(aesthetics.sectionLabel)} label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn} onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel} addRoomLabel={aesthetics.addRoomLabel}
isMinimized={this.props.isMinimized} isMinimized={this.props.isMinimized}

View file

@ -29,6 +29,9 @@ import {getAddressType} from "./UserAddress";
const E2EE_WK_KEY = "im.vector.riot.e2ee"; const E2EE_WK_KEY = "im.vector.riot.e2ee";
// we define a number of interfaces which take their names from the js-sdk
/* eslint-disable camelcase */
// TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them // TODO move these interfaces over to js-sdk once it has been typescripted enough to accept them
enum Visibility { enum Visibility {
Public = "public", Public = "public",

View file

@ -17,7 +17,6 @@ limitations under the License.
import {CARET_NODE_CHAR, isCaretNode} from "./render"; import {CARET_NODE_CHAR, isCaretNode} from "./render";
import DocumentOffset from "./offset"; import DocumentOffset from "./offset";
import EditorModel from "./model";
type Predicate = (node: Node) => boolean; type Predicate = (node: Node) => boolean;
type Callback = (node: Node) => void; type Callback = (node: Node) => void;

View file

@ -30,7 +30,7 @@ import {Caret} from "./caret";
* @param {object?} diff an object with `removed` and `added` strings * @param {object?} diff an object with `removed` and `added` strings
*/ */
/** /**
* @callback TransformCallback * @callback TransformCallback
* @param {DocumentPosition?} caretPosition the position where the caret should be position * @param {DocumentPosition?} caretPosition the position where the caret should be position
* @param {string?} inputType the inputType of the DOM input event * @param {string?} inputType the inputType of the DOM input event

View file

@ -22,7 +22,7 @@ import {Room} from "matrix-js-sdk/src/models/room";
import AutocompleteWrapperModel, { import AutocompleteWrapperModel, {
GetAutocompleterComponent, GetAutocompleterComponent,
UpdateCallback, UpdateCallback,
UpdateQuery UpdateQuery,
} from "./autocomplete"; } from "./autocomplete";
import * as Avatar from "../Avatar"; import * as Avatar from "../Avatar";

View file

@ -31,7 +31,8 @@ export function mdSerialize(model: EditorModel) {
return html + part.text; return html + part.text;
case "room-pill": case "room-pill":
case "user-pill": case "user-pill":
return html + `[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`; return html +
`[${part.text.replace(/[[\\\]]/g, c => "\\" + c)}](${makeGenericPermalink(part.resourceId)})`;
} }
}, ""); }, "");
} }

View file

@ -30,7 +30,7 @@ export const useAccountData = <T extends {}>(cli: MatrixClient, eventType: strin
const handler = useCallback((event) => { const handler = useCallback((event) => {
if (event.getType() !== eventType) return; if (event.getType() !== eventType) return;
setValue(event.getContent()); setValue(event.getContent());
}, [cli, eventType]); }, [eventType]);
useEventEmitter(cli, "accountData", handler); useEventEmitter(cli, "accountData", handler);
return value || {} as T; return value || {} as T;
@ -43,7 +43,7 @@ export const useRoomAccountData = <T extends {}>(room: Room, eventType: string)
const handler = useCallback((event) => { const handler = useCallback((event) => {
if (event.getType() !== eventType) return; if (event.getType() !== eventType) return;
setValue(event.getContent()); setValue(event.getContent());
}, [room, eventType]); }, [eventType]);
useEventEmitter(room, "Room.accountData", handler); useEventEmitter(room, "Room.accountData", handler);
return value || {} as T; return value || {} as T;

View file

@ -1156,6 +1156,7 @@
"Low priority": "Low priority", "Low priority": "Low priority",
"System Alerts": "System Alerts", "System Alerts": "System Alerts",
"Historical": "Historical", "Historical": "Historical",
"Custom Tag": "Custom Tag",
"This room": "This room", "This room": "This room",
"Joining room …": "Joining room …", "Joining room …": "Joining room …",
"Loading …": "Loading …", "Loading …": "Loading …",

View file

@ -14,6 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
// The following interfaces take their names and member names from seshat and the spec
/* eslint-disable camelcase */
export interface MatrixEvent { export interface MatrixEvent {
type: string; type: string;
sender: string; sender: string;
@ -21,7 +24,7 @@ export interface MatrixEvent {
event_id: string; event_id: string;
origin_server_ts: number; origin_server_ts: number;
unsigned?: {}; unsigned?: {};
room_id: string; roomId: string;
} }
export interface MatrixProfile { export interface MatrixProfile {

View file

@ -123,7 +123,12 @@ export class IntegrationManagers {
const apiUrl = data['api_url']; const apiUrl = data['api_url'];
if (!apiUrl || !uiUrl) return; if (!apiUrl || !uiUrl) return;
const manager = new IntegrationManagerInstance(Kind.Account, apiUrl, uiUrl, w['id'] || w['state_key'] || ''); const manager = new IntegrationManagerInstance(
Kind.Account,
apiUrl,
uiUrl,
w['id'] || w['state_key'] || '',
);
this.managers.push(manager); this.managers.push(manager);
}); });
this.primaryManager = null; // reset primary this.primaryManager = null; // reset primary

View file

@ -16,7 +16,7 @@ limitations under the License.
*/ */
import {PushRuleVectorState, State} from "./PushRuleVectorState"; import {PushRuleVectorState, State} from "./PushRuleVectorState";
import {IExtendedPushRule, IPushRuleSet, IRuleSets} from "./types"; import {IExtendedPushRule, IRuleSets} from "./types";
export interface IContentRules { export interface IContentRules {
vectorState: State; vectorState: State;

View file

@ -22,10 +22,12 @@ export enum NotificationSetting {
} }
export interface ISoundTweak { export interface ISoundTweak {
// eslint-disable-next-line camelcase
set_tweak: "sound"; set_tweak: "sound";
value: string; value: string;
} }
export interface IHighlightTweak { export interface IHighlightTweak {
// eslint-disable-next-line camelcase
set_tweak: "highlight"; set_tweak: "highlight";
value?: boolean; value?: boolean;
} }
@ -86,6 +88,7 @@ export enum RuleIds {
export interface IPushRule { export interface IPushRule {
enabled: boolean; enabled: boolean;
// eslint-disable-next-line camelcase
rule_id: RuleIds | string; rule_id: RuleIds | string;
actions: Action[]; actions: Action[];
default: boolean; default: boolean;

View file

@ -19,7 +19,6 @@ import SettingsStore, {SettingLevel} from '../SettingsStore';
import IWatcher from "./Watcher"; import IWatcher from "./Watcher";
import { toPx } from '../../utils/units'; import { toPx } from '../../utils/units';
import { Action } from '../../dispatcher/actions'; import { Action } from '../../dispatcher/actions';
import { UpdateSystemFontPayload } from '../../dispatcher/payloads/UpdateSystemFontPayload';
export class FontWatcher implements IWatcher { export class FontWatcher implements IWatcher {
public static readonly MIN_SIZE = 8; public static readonly MIN_SIZE = 8;

View file

@ -171,5 +171,4 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
} }
} }
} }
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2019 New Vector Ltd Copyright 2019 New Vector Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -13,15 +14,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import dis from '../dispatcher/dispatcher'; import dis from '../dispatcher/dispatcher';
import * as RoomNotifs from '../RoomNotifs';
import EventEmitter from 'events'; import EventEmitter from 'events';
import { throttle } from "lodash"; import {throttle} from "lodash";
import SettingsStore from "../settings/SettingsStore"; import SettingsStore from "../settings/SettingsStore";
import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore"; import RoomListStore, {LISTS_UPDATE_EVENT} from "./room-list/RoomListStore";
import {RoomNotificationStateStore} from "./notifications/RoomNotificationStateStore";
// TODO: All of this needs updating for new custom tags: https://github.com/vector-im/riot-web/issues/14091 import {isCustomTag} from "./room-list/models";
const STANDARD_TAGS_REGEX = /^(m\.(favourite|lowpriority|server_notice)|im\.vector\.fake\.(invite|recent|direct|archived))$/;
function commonPrefix(a, b) { function commonPrefix(a, b) {
const len = Math.min(a.length, b.length); const len = Math.min(a.length, b.length);
@ -84,8 +84,6 @@ class CustomRoomTagStore extends EventEmitter {
} }
getSortedTags() { getSortedTags() {
const roomLists = RoomListStore.instance.orderedLists;
const tagNames = Object.keys(this._state.tags).sort(); const tagNames = Object.keys(this._state.tags).sort();
const prefixes = tagNames.map((name, i) => { const prefixes = tagNames.map((name, i) => {
const isFirst = i === 0; const isFirst = i === 0;
@ -97,14 +95,14 @@ class CustomRoomTagStore extends EventEmitter {
return longestPrefix; return longestPrefix;
}); });
return tagNames.map((name, i) => { return tagNames.map((name, i) => {
const notifs = RoomNotifs.aggregateNotificationCount(roomLists[name]); const notifs = RoomNotificationStateStore.instance.getListState(name);
let badge; let badgeNotifState;
if (notifs.count !== 0) { if (notifs.hasUnreadCount) {
badge = notifs; badgeNotifState = notifs;
} }
const avatarLetter = name.substr(prefixes[i].length, 1); const avatarLetter = name.substr(prefixes[i].length, 1);
const selected = this._state.tags[name]; const selected = this._state.tags[name];
return {name, avatarLetter, badge, selected}; return {name, avatarLetter, badgeNotifState, selected};
}); });
} }
@ -139,16 +137,12 @@ class CustomRoomTagStore extends EventEmitter {
return; return;
} }
const newTagNames = Object.keys(RoomListStore.instance.orderedLists) const newTagNames = Object.keys(RoomListStore.instance.orderedLists).filter(t => isCustomTag(t)).sort();
.filter((tagName) => {
return !tagName.match(STANDARD_TAGS_REGEX);
}).sort();
const prevTags = this._state && this._state.tags; const prevTags = this._state && this._state.tags;
const newTags = newTagNames.reduce((newTags, tagName) => { return newTagNames.reduce((c, tagName) => {
newTags[tagName] = (prevTags && prevTags[tagName]) || false; c[tagName] = (prevTags && prevTags[tagName]) || false;
return newTags; return c;
}, {}); }, {});
return newTags;
} }
} }

View file

@ -37,8 +37,8 @@ export default class ToastStore extends EventEmitter {
private countSeen = 0; private countSeen = 0;
static sharedInstance() { static sharedInstance() {
if (!window.mx_ToastStore) window.mx_ToastStore = new ToastStore(); if (!window.mxToastStore) window.mxToastStore = new ToastStore();
return window.mx_ToastStore; return window.mxToastStore;
} }
reset() { reset() {

View file

@ -70,4 +70,4 @@ export default class RoomListLayoutStore extends AsyncStoreWithClient<IState> {
} }
} }
window.mx_RoomListLayoutStore = RoomListLayoutStore.instance; window.mxRoomListLayoutStore = RoomListLayoutStore.instance;

View file

@ -17,8 +17,7 @@ limitations under the License.
import { MatrixClient } from "matrix-js-sdk/src/client"; import { MatrixClient } from "matrix-js-sdk/src/client";
import SettingsStore from "../../settings/SettingsStore"; import SettingsStore from "../../settings/SettingsStore";
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models"; import { DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
import TagOrderStore from "../TagOrderStore";
import { Room } from "matrix-js-sdk/src/models/room"; import { Room } from "matrix-js-sdk/src/models/room";
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models"; import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
import { ActionPayload } from "../../dispatcher/payloads"; import { ActionPayload } from "../../dispatcher/payloads";
@ -527,25 +526,28 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
public async regenerateAllLists({trigger = true}) { public async regenerateAllLists({trigger = true}) {
console.warn("Regenerating all room lists"); console.warn("Regenerating all room lists");
const rooms = this.matrixClient.getVisibleRooms();
const customTags = new Set<TagID>();
if (this.state.tagsEnabled) {
for (const room of rooms) {
if (!room.tags) continue;
const tags = Object.keys(room.tags).filter(t => isCustomTag(t));
tags.forEach(t => customTags.add(t));
}
}
const sorts: ITagSortingMap = {}; const sorts: ITagSortingMap = {};
const orders: IListOrderingMap = {}; const orders: IListOrderingMap = {};
for (const tagId of OrderedDefaultTagIDs) { const allTags = [...OrderedDefaultTagIDs, ...Array.from(customTags)];
for (const tagId of allTags) {
sorts[tagId] = this.calculateTagSorting(tagId); sorts[tagId] = this.calculateTagSorting(tagId);
orders[tagId] = this.calculateListOrder(tagId); orders[tagId] = this.calculateListOrder(tagId);
RoomListLayoutStore.instance.ensureLayoutExists(tagId); RoomListLayoutStore.instance.ensureLayoutExists(tagId);
} }
if (this.state.tagsEnabled) {
// TODO: Fix custom tags: https://github.com/vector-im/riot-web/issues/14091
const roomTags = TagOrderStore.getOrderedTags() || [];
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log("rtags", roomTags);
}
await this.algorithm.populateTags(sorts, orders); await this.algorithm.populateTags(sorts, orders);
await this.algorithm.setKnownRooms(this.matrixClient.getVisibleRooms()); await this.algorithm.setKnownRooms(rooms);
this.initialListsGenerated = true; this.initialListsGenerated = true;
@ -606,4 +608,4 @@ export default class RoomListStore {
} }
} }
window.mx_RoomListStore = RoomListStore.instance; window.mxRoomListStore = RoomListStore.instance;

View file

@ -20,10 +20,9 @@ import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
import { arrayDiff, arrayHasDiff } from "../../utils/arrays"; import { arrayDiff, arrayHasDiff } from "../../utils/arrays";
/** /**
* Watches for changes in tags/groups to manage filters on the provided RoomListStore * Watches for changes in groups to manage filters on the provided RoomListStore
*/ */
export class TagWatcher { export class TagWatcher {
// TODO: Support custom tags, somehow: https://github.com/vector-im/riot-web/issues/14091
private filters = new Map<string, CommunityFilterCondition>(); private filters = new Map<string, CommunityFilterCondition>();
constructor(private store: RoomListStoreClass) { constructor(private store: RoomListStoreClass) {
@ -43,8 +42,6 @@ export class TagWatcher {
} }
const newFilters = new Map<string, CommunityFilterCondition>(); const newFilters = new Map<string, CommunityFilterCondition>();
// TODO: Support custom tags, somehow: https://github.com/vector-im/riot-web/issues/14091
const filterableTags = newTags.filter(t => t.startsWith("+")); const filterableTags = newTags.filter(t => t.startsWith("+"));
for (const tag of filterableTags) { for (const tag of filterableTags) {
@ -64,8 +61,6 @@ export class TagWatcher {
// Update the room list store's filters // Update the room list store's filters
const diff = arrayDiff(lastTags, newTags); const diff = arrayDiff(lastTags, newTags);
for (const tag of diff.added) { for (const tag of diff.added) {
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
// Ref https://github.com/vector-im/riot-web/issues/14091
const filter = newFilters.get(tag); const filter = newFilters.get(tag);
if (!filter) continue; if (!filter) continue;

View file

@ -27,7 +27,7 @@ import {
ITagMap, ITagMap,
ITagSortingMap, ITagSortingMap,
ListAlgorithm, ListAlgorithm,
SortAlgorithm SortAlgorithm,
} from "./models"; } from "./models";
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition"; import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition";
import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership"; import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
@ -419,7 +419,9 @@ export class Algorithm extends EventEmitter {
if (!updatedTag || updatedTag === sticky.tag) { if (!updatedTag || updatedTag === sticky.tag) {
if (SettingsStore.getValue("advancedRoomListLogging")) { if (SettingsStore.getValue("advancedRoomListLogging")) {
// TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602 // TODO: Remove debug: https://github.com/vector-im/riot-web/issues/14602
console.log(`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`); console.log(
`Inserting sticky room ${sticky.room.roomId} at position ${sticky.position} in ${sticky.tag}`,
);
} }
this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room); this._cachedStickyRooms[sticky.tag].splice(sticky.position, 0, sticky.room);
} }
@ -563,9 +565,6 @@ export class Algorithm extends EventEmitter {
} }
public getTagsForRoom(room: Room): TagID[] { public getTagsForRoom(room: Room): TagID[] {
// XXX: This duplicates a lot of logic from setKnownRooms above, but has a slightly
// different use case and therefore different performance curve
const tags: TagID[] = []; const tags: TagID[] = [];
const membership = getEffectiveMembership(room.getMyMembership()); const membership = getEffectiveMembership(room.getMyMembership());

View file

@ -218,7 +218,12 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
} }
// noinspection JSMethodCanBeStatic // noinspection JSMethodCanBeStatic
private moveRoomIndexes(nRooms: number, fromCategory: NotificationColor, toCategory: NotificationColor, indices: ICategoryIndex) { private moveRoomIndexes(
nRooms: number,
fromCategory: NotificationColor,
toCategory: NotificationColor,
indices: ICategoryIndex,
) {
// We have to update the index of the category *after* the from/toCategory variables // We have to update the index of the category *after* the from/toCategory variables
// in order to update the indices correctly. Because the room is moving from/to those // in order to update the indices correctly. Because the room is moving from/to those
// categories, the next category's index will change - not the category we're modifying. // categories, the next category's index will change - not the category we're modifying.
@ -257,7 +262,9 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
if (indices[lastCat] > indices[thisCat]) { if (indices[lastCat] > indices[thisCat]) {
// "should never happen" disclaimer goes here // "should never happen" disclaimer goes here
console.warn(`!! Room list index corruption: ${lastCat} (i:${indices[lastCat]}) is greater than ${thisCat} (i:${indices[thisCat]}) - category indices are likely desynced from reality`); console.warn(
`!! Room list index corruption: ${lastCat} (i:${indices[lastCat]}) is greater ` +
`than ${thisCat} (i:${indices[thisCat]}) - category indices are likely desynced from reality`);
// TODO: Regenerate index when this happens: https://github.com/vector-im/riot-web/issues/14234 // TODO: Regenerate index when this happens: https://github.com/vector-im/riot-web/issues/14234
} }

View file

@ -25,7 +25,6 @@ import { Room } from "matrix-js-sdk/src/models/room";
* additional behavioural changes are present. * additional behavioural changes are present.
*/ */
export class NaturalAlgorithm extends OrderingAlgorithm { export class NaturalAlgorithm extends OrderingAlgorithm {
public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) { public constructor(tagId: TagID, initialSortingAlgorithm: SortAlgorithm) {
super(tagId, initialSortingAlgorithm); super(tagId, initialSortingAlgorithm);
} }
@ -57,7 +56,11 @@ export class NaturalAlgorithm extends OrderingAlgorithm {
// TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14457 // TODO: Optimize this to avoid useless operations: https://github.com/vector-im/riot-web/issues/14457
// For example, we can skip updates to alphabetic (sometimes) and manually ordered tags // For example, we can skip updates to alphabetic (sometimes) and manually ordered tags
this.cachedOrderedRooms = await sortRoomsWithAlgorithm(this.cachedOrderedRooms, this.tagId, this.sortingAlgorithm); this.cachedOrderedRooms = await sortRoomsWithAlgorithm(
this.cachedOrderedRooms,
this.tagId,
this.sortingAlgorithm,
);
return true; return true;
} finally { } finally {

View file

@ -36,7 +36,11 @@ const ALGORITHM_FACTORIES: { [algorithm in ListAlgorithm]: AlgorithmFactory } =
* @param {SortAlgorithm} initSort The initial sorting algorithm for the ordering algorithm. * @param {SortAlgorithm} initSort The initial sorting algorithm for the ordering algorithm.
* @returns {Algorithm} The algorithm instance. * @returns {Algorithm} The algorithm instance.
*/ */
export function getListAlgorithmInstance(algorithm: ListAlgorithm, tagId: TagID, initSort: SortAlgorithm): OrderingAlgorithm { export function getListAlgorithmInstance(
algorithm: ListAlgorithm,
tagId: TagID,
initSort: SortAlgorithm,
): OrderingAlgorithm {
if (!ALGORITHM_FACTORIES[algorithm]) { if (!ALGORITHM_FACTORIES[algorithm]) {
throw new Error(`${algorithm} is not a known algorithm`); throw new Error(`${algorithm} is not a known algorithm`);
} }

View file

@ -14,6 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { isEnumValue } from "../../utils/enums";
export enum DefaultTagID { export enum DefaultTagID {
Invite = "im.vector.fake.invite", Invite = "im.vector.fake.invite",
Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms Untagged = "im.vector.fake.recent", // legacy: used to just be 'recent rooms' but now it's all untagged rooms
@ -36,6 +38,10 @@ export const OrderedDefaultTagIDs = [
export type TagID = string | DefaultTagID; export type TagID = string | DefaultTagID;
export function isCustomTag(tagId: TagID): boolean {
return !isEnumValue(DefaultTagID, tagId);
}
export enum RoomUpdateCause { export enum RoomUpdateCause {
Timeline = "TIMELINE", Timeline = "TIMELINE",
PossibleTagChange = "POSSIBLE_TAG_CHANGE", PossibleTagChange = "POSSIBLE_TAG_CHANGE",

View file

@ -25,3 +25,13 @@ export function getEnumValues<T>(e: any): T[] {
.filter(k => ['string', 'number'].includes(typeof(e[k]))) .filter(k => ['string', 'number'].includes(typeof(e[k])))
.map(k => e[k]); .map(k => e[k]);
} }
/**
* Determines if a given value is a valid value for the provided enum.
* @param e The enum to check against.
* @param val The value to search for.
* @returns True if the enum contains the value.
*/
export function isEnumValue<T>(e: T, val: string | number): boolean {
return getEnumValues(e).includes(val);
}

View file

@ -113,7 +113,7 @@ export class WidgetApi extends EventEmitter {
// Finalization needs to be async, so postpone with a promise // Finalization needs to be async, so postpone with a promise
let finalizePromise = Promise.resolve(); let finalizePromise = Promise.resolve();
const wait = (promise) => { const wait = (promise) => {
finalizePromise = finalizePromise.then(value => promise); finalizePromise = finalizePromise.then(() => promise);
}; };
this.emit('terminate', wait); this.emit('terminate', wait);
Promise.resolve(finalizePromise).then(() => { Promise.resolve(finalizePromise).then(() => {

810
yarn.lock

File diff suppressed because it is too large Load diff