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:
commit
95854a2f67
48 changed files with 1031 additions and 487 deletions
121
.eslintrc.js
121
.eslintrc.js
|
@ -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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
15
src/@types/global.d.ts
vendored
15
src/@types/global.d.ts
vendored
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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;
|
|
@ -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() {
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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})),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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} />;
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
|
@ -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)})`;
|
||||||
}
|
}
|
||||||
}, "");
|
}, "");
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 …",
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -171,5 +171,4 @@ export class BreadcrumbsStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -70,4 +70,4 @@ export default class RoomListLayoutStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.mx_RoomListLayoutStore = RoomListLayoutStore.instance;
|
window.mxRoomListLayoutStore = RoomListLayoutStore.instance;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
|
@ -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(() => {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue