Merge branches 'develop' and 't3chguy/fix/13641' of github.com:matrix-org/matrix-react-sdk into t3chguy/fix/13641
Conflicts: src/editor/parts.ts
This commit is contained in:
commit
c578026474
46 changed files with 1487 additions and 1521 deletions
144
package.json
144
package.json
|
@ -53,119 +53,119 @@
|
||||||
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
|
"test:e2e": "./test/end-to-end-tests/run.sh --riot-url http://localhost:8080"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.8.3",
|
"@babel/runtime": "^7.10.5",
|
||||||
"await-lock": "^2.0.1",
|
"await-lock": "^2.0.1",
|
||||||
"blueimp-canvas-to-blob": "^3.5.0",
|
"blueimp-canvas-to-blob": "^3.27.0",
|
||||||
"browser-encrypt-attachment": "^0.3.0",
|
"browser-encrypt-attachment": "^0.3.0",
|
||||||
"browser-request": "^0.3.3",
|
"browser-request": "^0.3.3",
|
||||||
"classnames": "^2.1.2",
|
"classnames": "^2.2.6",
|
||||||
"commonmark": "^0.28.1",
|
"commonmark": "^0.29.1",
|
||||||
"counterpart": "^0.18.0",
|
"counterpart": "^0.18.6",
|
||||||
"create-react-class": "^15.6.0",
|
"create-react-class": "^15.6.3",
|
||||||
"diff-dom": "^4.1.3",
|
"diff-dom": "^4.1.6",
|
||||||
"diff-match-patch": "^1.0.4",
|
"diff-match-patch": "^1.0.5",
|
||||||
"emojibase-data": "^5.0.1",
|
"emojibase-data": "^5.0.1",
|
||||||
"emojibase-regex": "^4.0.1",
|
"emojibase-regex": "^4.0.1",
|
||||||
"escape-html": "^1.0.3",
|
"escape-html": "^1.0.3",
|
||||||
"file-saver": "^1.3.3",
|
"file-saver": "^1.3.8",
|
||||||
"filesize": "3.5.6",
|
"filesize": "3.6.1",
|
||||||
"flux": "2.1.1",
|
"flux": "2.1.1",
|
||||||
"focus-visible": "^5.0.2",
|
"focus-visible": "^5.1.0",
|
||||||
"fuse.js": "^2.2.0",
|
"fuse.js": "^2.7.4",
|
||||||
"gfm.css": "^1.1.1",
|
"gfm.css": "^1.1.2",
|
||||||
"glob-to-regexp": "^0.4.1",
|
"glob-to-regexp": "^0.4.1",
|
||||||
"highlight.js": "^9.15.8",
|
"highlight.js": "^10.1.2",
|
||||||
"html-entities": "^1.2.1",
|
"html-entities": "^1.3.1",
|
||||||
"is-ip": "^2.0.0",
|
"is-ip": "^2.0.0",
|
||||||
"linkifyjs": "^2.1.6",
|
"linkifyjs": "^2.1.9",
|
||||||
"lodash": "^4.17.14",
|
"lodash": "^4.17.19",
|
||||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||||
"minimist": "^1.2.0",
|
"minimist": "^1.2.5",
|
||||||
"pako": "^1.0.5",
|
"pako": "^1.0.11",
|
||||||
"parse5": "^5.1.1",
|
"parse5": "^5.1.1",
|
||||||
"png-chunks-extract": "^1.0.0",
|
"png-chunks-extract": "^1.0.0",
|
||||||
"project-name-generator": "^2.1.7",
|
"project-name-generator": "^2.1.7",
|
||||||
"prop-types": "^15.5.8",
|
"prop-types": "^15.7.2",
|
||||||
"qrcode": "^1.4.4",
|
"qrcode": "^1.4.4",
|
||||||
"qs": "^6.6.0",
|
"qs": "^6.9.4",
|
||||||
"re-resizable": "^6.5.2",
|
"re-resizable": "^6.5.4",
|
||||||
"react": "^16.9.0",
|
"react": "^16.13.1",
|
||||||
"react-beautiful-dnd": "^4.0.1",
|
"react-beautiful-dnd": "^4.0.1",
|
||||||
"react-dom": "^16.9.0",
|
"react-dom": "^16.13.1",
|
||||||
"react-focus-lock": "^2.2.1",
|
"react-focus-lock": "^2.4.1",
|
||||||
"react-transition-group": "^4.4.1",
|
"react-transition-group": "^4.4.1",
|
||||||
"resize-observer-polyfill": "^1.5.0",
|
"resize-observer-polyfill": "^1.5.1",
|
||||||
"sanitize-html": "^1.18.4",
|
"sanitize-html": "^1.27.1",
|
||||||
"text-encoding-utf-8": "^1.0.1",
|
"text-encoding-utf-8": "^1.0.2",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"velocity-animate": "^1.5.2",
|
"velocity-animate": "^1.5.2",
|
||||||
"what-input": "^5.2.6",
|
"what-input": "^5.2.10",
|
||||||
"zxcvbn": "^4.4.2"
|
"zxcvbn": "^4.4.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.7.5",
|
"@babel/cli": "^7.10.5",
|
||||||
"@babel/core": "^7.7.5",
|
"@babel/core": "^7.10.5",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.7.4",
|
"@babel/plugin-proposal-class-properties": "^7.10.4",
|
||||||
"@babel/plugin-proposal-decorators": "^7.7.4",
|
"@babel/plugin-proposal-decorators": "^7.10.5",
|
||||||
"@babel/plugin-proposal-export-default-from": "^7.7.4",
|
"@babel/plugin-proposal-export-default-from": "^7.10.4",
|
||||||
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
|
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
|
||||||
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
|
"@babel/plugin-proposal-object-rest-spread": "^7.10.4",
|
||||||
"@babel/plugin-transform-flow-comments": "^7.7.4",
|
"@babel/plugin-transform-flow-comments": "^7.10.4",
|
||||||
"@babel/plugin-transform-runtime": "^7.8.3",
|
"@babel/plugin-transform-runtime": "^7.10.5",
|
||||||
"@babel/preset-env": "^7.7.6",
|
"@babel/preset-env": "^7.10.4",
|
||||||
"@babel/preset-flow": "^7.7.4",
|
"@babel/preset-flow": "^7.10.4",
|
||||||
"@babel/preset-react": "^7.7.4",
|
"@babel/preset-react": "^7.10.4",
|
||||||
"@babel/preset-typescript": "^7.7.4",
|
"@babel/preset-typescript": "^7.10.4",
|
||||||
"@babel/register": "^7.7.4",
|
"@babel/register": "^7.10.5",
|
||||||
"@peculiar/webcrypto": "^1.0.22",
|
"@peculiar/webcrypto": "^1.1.2",
|
||||||
"@types/classnames": "^2.2.10",
|
"@types/classnames": "^2.2.10",
|
||||||
"@types/counterpart": "^0.18.1",
|
"@types/counterpart": "^0.18.1",
|
||||||
"@types/flux": "^3.1.9",
|
"@types/flux": "^3.1.9",
|
||||||
"@types/linkifyjs": "^2.1.3",
|
"@types/linkifyjs": "^2.1.3",
|
||||||
"@types/lodash": "^4.14.152",
|
"@types/lodash": "^4.14.158",
|
||||||
"@types/modernizr": "^3.5.3",
|
"@types/modernizr": "^3.5.3",
|
||||||
"@types/node": "^12.12.41",
|
"@types/node": "^12.12.51",
|
||||||
"@types/qrcode": "^1.3.4",
|
"@types/qrcode": "^1.3.4",
|
||||||
"@types/react": "^16.9",
|
"@types/react": "^16.9",
|
||||||
"@types/react-dom": "^16.9.8",
|
"@types/react-dom": "^16.9.8",
|
||||||
"@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/eslint-plugin": "^3.7.0",
|
||||||
"@typescript-eslint/parser": "^3.4.0",
|
"@typescript-eslint/parser": "^3.7.0",
|
||||||
"babel-eslint": "^10.0.3",
|
"babel-eslint": "^10.1.0",
|
||||||
"babel-jest": "^24.9.0",
|
"babel-jest": "^24.9.0",
|
||||||
"chokidar": "^3.3.1",
|
"chokidar": "^3.4.1",
|
||||||
"concurrently": "^4.0.1",
|
"concurrently": "^4.1.2",
|
||||||
"enzyme": "^3.10.0",
|
"enzyme": "^3.11.0",
|
||||||
"enzyme-adapter-react-16": "^1.15.1",
|
"enzyme-adapter-react-16": "^1.15.2",
|
||||||
"eslint": "7.3.1",
|
"eslint": "7.5.0",
|
||||||
"eslint-config-google": "^0.7.1",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-matrix-org": "^0.1.2",
|
"eslint-config-matrix-org": "^0.1.2",
|
||||||
"eslint-plugin-babel": "^5.2.1",
|
"eslint-plugin-babel": "^5.3.1",
|
||||||
"eslint-plugin-flowtype": "^2.30.0",
|
"eslint-plugin-flowtype": "^2.50.3",
|
||||||
"eslint-plugin-jest": "^23.0.4",
|
"eslint-plugin-jest": "^23.18.0",
|
||||||
"eslint-plugin-react": "^7.7.0",
|
"eslint-plugin-react": "^7.20.3",
|
||||||
"eslint-plugin-react-hooks": "^2.0.1",
|
"eslint-plugin-react-hooks": "^2.5.1",
|
||||||
"estree-walker": "^0.5.0",
|
"estree-walker": "^0.9.0",
|
||||||
"file-loader": "^3.0.1",
|
"file-loader": "^3.0.1",
|
||||||
"flow-parser": "^0.57.3",
|
"flow-parser": "0.57.3",
|
||||||
"glob": "^5.0.14",
|
"glob": "^5.0.15",
|
||||||
"jest": "^24.9.0",
|
"jest": "^24.9.0",
|
||||||
"jest-canvas-mock": "^2.2.0",
|
"jest-canvas-mock": "^2.2.0",
|
||||||
"lolex": "^5.1.2",
|
"lolex": "^5.1.2",
|
||||||
"matrix-mock-request": "^1.2.3",
|
"matrix-mock-request": "^1.2.3",
|
||||||
"matrix-react-test-utils": "^0.2.2",
|
"matrix-react-test-utils": "^0.2.2",
|
||||||
"react-test-renderer": "^16.9.0",
|
"react-test-renderer": "^16.13.1",
|
||||||
"rimraf": "^2.4.3",
|
"rimraf": "^2.7.1",
|
||||||
"source-map-loader": "^0.2.3",
|
"source-map-loader": "^0.2.4",
|
||||||
"stylelint": "^9.10.1",
|
"stylelint": "^9.10.1",
|
||||||
"stylelint-config-standard": "^18.2.0",
|
"stylelint-config-standard": "^18.3.0",
|
||||||
"stylelint-scss": "^3.9.0",
|
"stylelint-scss": "^3.18.0",
|
||||||
"typescript": "^3.7.3",
|
"typescript": "^3.9.7",
|
||||||
"walk": "^2.3.9",
|
"walk": "^2.3.14",
|
||||||
"webpack": "^4.20.2",
|
"webpack": "^4.43.0",
|
||||||
"webpack-cli": "^3.1.1"
|
"webpack-cli": "^3.3.12"
|
||||||
},
|
},
|
||||||
"jest": {
|
"jest": {
|
||||||
"testMatch": [
|
"testMatch": [
|
||||||
|
|
|
@ -135,12 +135,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_roomListWrapper {
|
.mx_LeftPanel_roomListWrapper {
|
||||||
// Create a flexbox to ensure the containing items cause appropriate overflow.
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
min-height: 0;
|
|
||||||
margin-top: 10px; // so we're not up against the search/filter
|
margin-top: 10px; // so we're not up against the search/filter
|
||||||
|
|
||||||
&.mx_LeftPanel_roomListWrapper_stickyBottom {
|
&.mx_LeftPanel_roomListWrapper_stickyBottom {
|
||||||
|
@ -153,14 +148,8 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_LeftPanel_actualRoomListContainer {
|
.mx_LeftPanel_actualRoomListContainer {
|
||||||
flex-grow: 1; // fill the available space
|
|
||||||
overflow-y: auto;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 100%;
|
|
||||||
position: relative; // for sticky headers
|
position: relative; // for sticky headers
|
||||||
|
height: 100%; // ensure scrolling still works
|
||||||
// Create a flexbox to trick the layout engine
|
|
||||||
display: flex;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,4 +17,5 @@ limitations under the License.
|
||||||
.mx_MessageTimestamp {
|
.mx_MessageTimestamp {
|
||||||
color: $event-timestamp-color;
|
color: $event-timestamp-color;
|
||||||
font-size: $font-10px;
|
font-size: $font-10px;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,5 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_RoomList {
|
.mx_RoomList {
|
||||||
width: calc(100% - 16px); // 16px of artificial right-side margin (8px is overflowed from the sublists)
|
padding-right: 7px; // width of the scrollbar, to line things up
|
||||||
|
|
||||||
// Create a column-based flexbox for the sublists. That's pretty much all we have to
|
|
||||||
// worry about in this stylesheet.
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
flex-wrap: nowrap; // let the column overflow
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
.mx_RoomSublist {
|
.mx_RoomSublist {
|
||||||
// The sublist is a column of rows, essentially
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
width: 100%;
|
|
||||||
|
|
||||||
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
|
|
||||||
|
|
||||||
.mx_RoomSublist_headerContainer {
|
.mx_RoomSublist_headerContainer {
|
||||||
// Create a flexbox to make alignment easy
|
// Create a flexbox to make alignment easy
|
||||||
|
|
|
@ -221,10 +221,6 @@ limitations under the License.
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/favorite.svg');
|
||||||
}
|
}
|
||||||
|
|
||||||
.mx_RoomTile_iconFavorite::before {
|
|
||||||
mask-image: url('$(res)/img/feather-customised/favourites.svg');
|
|
||||||
}
|
|
||||||
|
|
||||||
.mx_RoomTile_iconArrowDown::before {
|
.mx_RoomTile_iconArrowDown::before {
|
||||||
mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
|
mask-image: url('$(res)/img/element-icons/roomlist/low-priority.svg');
|
||||||
}
|
}
|
||||||
|
|
2
src/@types/global.d.ts
vendored
2
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";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
@ -41,6 +42,7 @@ declare global {
|
||||||
mxRoomListLayoutStore: RoomListLayoutStore;
|
mxRoomListLayoutStore: RoomListLayoutStore;
|
||||||
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;
|
||||||
|
|
|
@ -184,7 +184,7 @@ const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to mat
|
||||||
if (typeof attribs.class !== 'undefined') {
|
if (typeof attribs.class !== 'undefined') {
|
||||||
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
// Filter out all classes other than ones starting with language- for syntax highlighting.
|
||||||
const classes = attribs.class.split(/\s/).filter(function(cl) {
|
const classes = attribs.class.split(/\s/).filter(function(cl) {
|
||||||
return cl.startsWith('language-');
|
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||||
});
|
});
|
||||||
attribs.class = classes.join(' ');
|
attribs.class = classes.join(' ');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
// The modal to prioritise over all others. If this is set, only show
|
interface IHandle<T extends any[]> {
|
||||||
// this modal. Remove all other modals from the stack when this modal
|
finished: Promise<T>;
|
||||||
// is closed.
|
close(...args: T): void;
|
||||||
this._priorityModal = null;
|
}
|
||||||
// The modal to keep open underneath other modals if possible. Useful
|
|
||||||
// for cases like Settings where the modal should remain open while the
|
|
||||||
// user is prompted for more information/errors.
|
|
||||||
this._staticModal = null;
|
|
||||||
// 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.
|
|
||||||
this._modals = [
|
|
||||||
/* {
|
|
||||||
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);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
hasDialogs() {
|
interface IOptions<T extends any[]> {
|
||||||
return this._priorityModal || this._staticModal || this._modals.length > 0;
|
onBeforeClose?: IModal<T>["onBeforeClose"];
|
||||||
}
|
}
|
||||||
|
|
||||||
getOrCreateContainer() {
|
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
|
||||||
|
// this modal. Remove all other modals from the stack when this modal
|
||||||
|
// is closed.
|
||||||
|
private priorityModal: IModal<any> = null;
|
||||||
|
// The modal to keep open underneath other modals if possible. Useful
|
||||||
|
// for cases like Settings where the modal should remain open while the
|
||||||
|
// user is prompted for more information/errors.
|
||||||
|
private staticModal: IModal<any> = null;
|
||||||
|
// 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.
|
||||||
|
private modals: IModal<any>[] = [];
|
||||||
|
|
||||||
|
private static 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;
|
|
@ -114,6 +114,11 @@ export default class RebrandListener {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onOneTimeToastDismiss = async () => {
|
||||||
|
localStorage.setItem('mx_rename_dialog_dismissed', 'true');
|
||||||
|
this.recheck();
|
||||||
|
};
|
||||||
|
|
||||||
onNagTimerFired = () => {
|
onNagTimerFired = () => {
|
||||||
this._reshowTimer = null;
|
this._reshowTimer = null;
|
||||||
this.nagAgainAt = null;
|
this.nagAgainAt = null;
|
||||||
|
@ -143,10 +148,14 @@ export default class RebrandListener {
|
||||||
|
|
||||||
if (nagToast || oneTimeToast) {
|
if (nagToast || oneTimeToast) {
|
||||||
let description;
|
let description;
|
||||||
|
let rejectLabel = null;
|
||||||
|
let onReject = null;
|
||||||
if (nagToast) {
|
if (nagToast) {
|
||||||
description = _t("Use your account to sign in to the latest version");
|
description = _t("Use your account to sign in to the latest version");
|
||||||
} else {
|
} else {
|
||||||
description = _t("We’re excited to announce Riot is now Element");
|
description = _t("We’re excited to announce Riot is now Element");
|
||||||
|
rejectLabel = _t("Dismiss");
|
||||||
|
onReject = this.onOneTimeToastDismiss;
|
||||||
}
|
}
|
||||||
|
|
||||||
ToastStore.sharedInstance().addOrReplaceToast({
|
ToastStore.sharedInstance().addOrReplaceToast({
|
||||||
|
@ -157,6 +166,8 @@ export default class RebrandListener {
|
||||||
description,
|
description,
|
||||||
acceptLabel: _t("Learn More"),
|
acceptLabel: _t("Learn More"),
|
||||||
onAccept: nagToast ? this.onNagToastLearnMore : this.onOneTimeToastLearnMore,
|
onAccept: nagToast ? this.onNagToastLearnMore : this.onOneTimeToastLearnMore,
|
||||||
|
rejectLabel,
|
||||||
|
onReject,
|
||||||
},
|
},
|
||||||
component: GenericToast,
|
component: GenericToast,
|
||||||
priority: 20,
|
priority: 20,
|
||||||
|
|
|
@ -34,27 +34,6 @@ export function shouldShowMentionBadge(roomNotifState) {
|
||||||
return MENTION_BADGE_STATES.includes(roomNotifState);
|
return MENTION_BADGE_STATES.includes(roomNotifState);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function countRoomsWithNotif(rooms) {
|
|
||||||
return rooms.reduce((result, room, index) => {
|
|
||||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
|
||||||
const highlight = room.getUnreadNotificationCount('highlight') > 0;
|
|
||||||
const notificationCount = room.getUnreadNotificationCount();
|
|
||||||
|
|
||||||
const notifBadges = notificationCount > 0 && shouldShowNotifBadge(roomNotifState);
|
|
||||||
const mentionBadges = highlight && shouldShowMentionBadge(roomNotifState);
|
|
||||||
const isInvite = room.hasMembershipState(MatrixClientPeg.get().credentials.userId, 'invite');
|
|
||||||
const badges = notifBadges || mentionBadges || isInvite;
|
|
||||||
|
|
||||||
if (badges) {
|
|
||||||
result.count++;
|
|
||||||
if (highlight) {
|
|
||||||
result.highlight = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, {count: 0, highlight: false});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function aggregateNotificationCount(rooms) {
|
export function aggregateNotificationCount(rooms) {
|
||||||
return rooms.reduce((result, room) => {
|
return rooms.reduce((result, room) => {
|
||||||
const roomNotifState = getRoomNotifsState(room.roomId);
|
const roomNotifState = getRoomNotifsState(room.roomId);
|
||||||
|
|
|
@ -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") {
|
||||||
|
|
|
@ -35,6 +35,8 @@ import RoomListStore, { LISTS_UPDATE_EVENT } from "../../stores/room-list/RoomLi
|
||||||
import {Key} from "../../Keyboard";
|
import {Key} from "../../Keyboard";
|
||||||
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
import IndicatorScrollbar from "../structures/IndicatorScrollbar";
|
||||||
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
|
||||||
|
import { OwnProfileStore } from "../../stores/OwnProfileStore";
|
||||||
|
import { MatrixClientPeg } from "../../MatrixClientPeg";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
isMinimized: boolean;
|
isMinimized: boolean;
|
||||||
|
@ -59,6 +61,7 @@ const cssClasses = [
|
||||||
export default class LeftPanel extends React.Component<IProps, IState> {
|
export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
|
||||||
private tagPanelWatcherRef: string;
|
private tagPanelWatcherRef: string;
|
||||||
|
private bgImageWatcherRef: string;
|
||||||
private focusedElement = null;
|
private focusedElement = null;
|
||||||
private isDoingStickyHeaders = false;
|
private isDoingStickyHeaders = false;
|
||||||
|
|
||||||
|
@ -73,6 +76,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
|
OwnProfileStore.instance.on(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||||
|
this.bgImageWatcherRef = SettingsStore.watchSetting(
|
||||||
|
"RoomList.backgroundImage", null, this.onBackgroundImageUpdate);
|
||||||
this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
this.tagPanelWatcherRef = SettingsStore.watchSetting("TagPanel.enableTagPanel", null, () => {
|
||||||
this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
this.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
|
||||||
});
|
});
|
||||||
|
@ -84,8 +90,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public componentWillUnmount() {
|
public componentWillUnmount() {
|
||||||
SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
|
SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
|
||||||
|
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
|
||||||
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate);
|
||||||
|
OwnProfileStore.instance.off(UPDATE_EVENT, this.onBackgroundImageUpdate);
|
||||||
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
this.props.resizeNotifier.off("middlePanelResizedNoisy", this.onResize);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +116,20 @@ export default class LeftPanel extends React.Component<IProps, IState> {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private onBackgroundImageUpdate = () => {
|
||||||
|
// Note: we do this in the LeftPanel as it uses this variable most prominently.
|
||||||
|
const avatarSize = 32; // arbitrary
|
||||||
|
let avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
||||||
|
const settingBgMxc = SettingsStore.getValue("RoomList.backgroundImage");
|
||||||
|
if (settingBgMxc) {
|
||||||
|
avatarUrl = MatrixClientPeg.get().mxcUrlToHttp(settingBgMxc, avatarSize, avatarSize);
|
||||||
|
}
|
||||||
|
const avatarUrlProp = `url(${avatarUrl})`;
|
||||||
|
if (document.body.style.getPropertyValue("--avatar-url") !== avatarUrlProp) {
|
||||||
|
document.body.style.setProperty("--avatar-url", avatarUrlProp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private handleStickyHeaders(list: HTMLDivElement) {
|
private handleStickyHeaders(list: HTMLDivElement) {
|
||||||
if (this.isDoingStickyHeaders) return;
|
if (this.isDoingStickyHeaders) return;
|
||||||
this.isDoingStickyHeaders = true;
|
this.isDoingStickyHeaders = true;
|
||||||
|
|
|
@ -58,7 +58,6 @@ import { messageForSyncError } from '../../utils/ErrorUtils';
|
||||||
import ResizeNotifier from "../../utils/ResizeNotifier";
|
import ResizeNotifier from "../../utils/ResizeNotifier";
|
||||||
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
|
||||||
import DMRoomMap from '../../utils/DMRoomMap';
|
import DMRoomMap from '../../utils/DMRoomMap';
|
||||||
import { countRoomsWithNotif } from '../../RoomNotifs';
|
|
||||||
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
|
||||||
import { FontWatcher } from '../../settings/watchers/FontWatcher';
|
import { FontWatcher } from '../../settings/watchers/FontWatcher';
|
||||||
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
import { storeRoomAliasInCache } from '../../RoomAliasCache';
|
||||||
|
@ -75,6 +74,7 @@ import {
|
||||||
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
|
||||||
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
|
||||||
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
import ErrorDialog from "../views/dialogs/ErrorDialog";
|
||||||
|
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
|
||||||
|
|
||||||
/** constants for MatrixChat.state.view */
|
/** constants for MatrixChat.state.view */
|
||||||
export enum Views {
|
export enum Views {
|
||||||
|
@ -1844,21 +1844,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateStatusIndicator(state: string, prevState: string) {
|
updateStatusIndicator(state: string, prevState: string) {
|
||||||
// only count visible rooms to not torment the user with notification counts in rooms they can't see
|
const notificationState = RoomNotificationStateStore.instance.globalState;
|
||||||
// it will include highlights from the previous version of the room internally
|
const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here
|
||||||
const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getVisibleRooms()).count;
|
|
||||||
|
|
||||||
if (PlatformPeg.get()) {
|
if (PlatformPeg.get()) {
|
||||||
PlatformPeg.get().setErrorStatus(state === 'ERROR');
|
PlatformPeg.get().setErrorStatus(state === 'ERROR');
|
||||||
PlatformPeg.get().setNotificationCount(notifCount);
|
PlatformPeg.get().setNotificationCount(numUnreadRooms);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.subTitleStatus = '';
|
this.subTitleStatus = '';
|
||||||
if (state === "ERROR") {
|
if (state === "ERROR") {
|
||||||
this.subTitleStatus += `[${_t("Offline")}] `;
|
this.subTitleStatus += `[${_t("Offline")}] `;
|
||||||
}
|
}
|
||||||
if (notifCount > 0) {
|
if (numUnreadRooms > 0) {
|
||||||
this.subTitleStatus += `[${notifCount}]`;
|
this.subTitleStatus += `[${numUnreadRooms}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setPageSubtitle();
|
this.setPageSubtitle();
|
||||||
|
|
|
@ -648,7 +648,9 @@ export default createReactClass({
|
||||||
|
|
||||||
if (scrollState.stuckAtBottom) {
|
if (scrollState.stuckAtBottom) {
|
||||||
const sn = this._getScrollNode();
|
const sn = this._getScrollNode();
|
||||||
sn.scrollTop = sn.scrollHeight;
|
if (sn.scrollTop !== sn.scrollHeight) {
|
||||||
|
sn.scrollTop = sn.scrollHeight;
|
||||||
|
}
|
||||||
} else if (scrollState.trackedScrollToken) {
|
} else if (scrollState.trackedScrollToken) {
|
||||||
const itemlist = this._itemlist.current;
|
const itemlist = this._itemlist.current;
|
||||||
const trackedNode = this._getTrackedNode();
|
const trackedNode = this._getTrackedNode();
|
||||||
|
@ -657,7 +659,10 @@ export default createReactClass({
|
||||||
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
|
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
|
||||||
this._bottomGrowth += bottomDiff;
|
this._bottomGrowth += bottomDiff;
|
||||||
scrollState.bottomOffset = newBottomOffset;
|
scrollState.bottomOffset = newBottomOffset;
|
||||||
itemlist.style.height = `${this._getListHeight()}px`;
|
const newHeight = `${this._getListHeight()}px`;
|
||||||
|
if (itemlist.style.height !== newHeight) {
|
||||||
|
itemlist.style.height = newHeight;
|
||||||
|
}
|
||||||
debuglog("balancing height because messages below viewport grew by", bottomDiff);
|
debuglog("balancing height because messages below viewport grew by", bottomDiff);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -694,12 +699,16 @@ export default createReactClass({
|
||||||
const height = Math.max(minHeight, contentHeight);
|
const height = Math.max(minHeight, contentHeight);
|
||||||
this._pages = Math.ceil(height / PAGE_SIZE);
|
this._pages = Math.ceil(height / PAGE_SIZE);
|
||||||
this._bottomGrowth = 0;
|
this._bottomGrowth = 0;
|
||||||
const newHeight = this._getListHeight();
|
const newHeight = `${this._getListHeight()}px`;
|
||||||
|
|
||||||
const scrollState = this.scrollState;
|
const scrollState = this.scrollState;
|
||||||
if (scrollState.stuckAtBottom) {
|
if (scrollState.stuckAtBottom) {
|
||||||
itemlist.style.height = `${newHeight}px`;
|
if (itemlist.style.height !== newHeight) {
|
||||||
sn.scrollTop = sn.scrollHeight;
|
itemlist.style.height = newHeight;
|
||||||
|
}
|
||||||
|
if (sn.scrollTop !== sn.scrollHeight){
|
||||||
|
sn.scrollTop = sn.scrollHeight;
|
||||||
|
}
|
||||||
debuglog("updateHeight to", newHeight);
|
debuglog("updateHeight to", newHeight);
|
||||||
} else if (scrollState.trackedScrollToken) {
|
} else if (scrollState.trackedScrollToken) {
|
||||||
const trackedNode = this._getTrackedNode();
|
const trackedNode = this._getTrackedNode();
|
||||||
|
@ -709,7 +718,9 @@ export default createReactClass({
|
||||||
// the currently filled piece of the timeline
|
// the currently filled piece of the timeline
|
||||||
if (trackedNode) {
|
if (trackedNode) {
|
||||||
const oldTop = trackedNode.offsetTop;
|
const oldTop = trackedNode.offsetTop;
|
||||||
itemlist.style.height = `${newHeight}px`;
|
if (itemlist.style.height !== newHeight) {
|
||||||
|
itemlist.style.height = newHeight;
|
||||||
|
}
|
||||||
const newTop = trackedNode.offsetTop;
|
const newTop = trackedNode.offsetTop;
|
||||||
const topDiff = newTop - oldTop;
|
const topDiff = newTop - oldTop;
|
||||||
// important to scroll by a relative amount as
|
// important to scroll by a relative amount as
|
||||||
|
|
|
@ -306,9 +306,6 @@ export default class UserMenu extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const avatarSize = 32; // should match border-radius of the avatar
|
const avatarSize = 32; // should match border-radius of the avatar
|
||||||
const {body} = document;
|
|
||||||
const avatarUrl = OwnProfileStore.instance.getHttpAvatarUrl(avatarSize);
|
|
||||||
body.style.setProperty("--avatar-url", `url('${avatarUrl}')`);
|
|
||||||
|
|
||||||
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
|
let name = <span className="mx_UserMenu_userName">{OwnProfileStore.instance.displayName}</span>;
|
||||||
let buttons = (
|
let buttons = (
|
||||||
|
|
|
@ -134,7 +134,7 @@ const BaseAvatar = (props: IProps) => {
|
||||||
aria-hidden="true" />
|
aria-hidden="true" />
|
||||||
);
|
);
|
||||||
|
|
||||||
if (onClick !== null) {
|
if (onClick) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
{...otherProps}
|
{...otherProps}
|
||||||
|
@ -162,7 +162,7 @@ const BaseAvatar = (props: IProps) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onClick !== null) {
|
if (onClick) {
|
||||||
return (
|
return (
|
||||||
<AccessibleButton
|
<AccessibleButton
|
||||||
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
|
||||||
|
@ -196,4 +196,4 @@ const BaseAvatar = (props: IProps) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default BaseAvatar;
|
export default BaseAvatar;
|
||||||
export type BaseAvatarType = React.FC<IProps>;
|
export type BaseAvatarType = React.FC<IProps>;
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default class DecoratedRoomAvatar extends React.PureComponent<IProps, ISt
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
|
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,9 @@ import {Action} from "../../../dispatcher/actions";
|
||||||
import {DefaultTagID} from "../../../stores/room-list/models";
|
import {DefaultTagID} from "../../../stores/room-list/models";
|
||||||
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
import RoomListStore from "../../../stores/room-list/RoomListStore";
|
||||||
|
|
||||||
|
// we have a number of types defined from the Matrix spec which can't reasonably be altered here.
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
export const KIND_DM = "dm";
|
export const KIND_DM = "dm";
|
||||||
export const KIND_INVITE = "invite";
|
export const KIND_INVITE = "invite";
|
||||||
|
|
||||||
|
|
|
@ -107,7 +107,7 @@ export default createReactClass({
|
||||||
} else {
|
} else {
|
||||||
// Only syntax highlight if there's a class starting with language-
|
// Only syntax highlight if there's a class starting with language-
|
||||||
const classes = blocks[i].className.split(/\s+/).filter(function(cl) {
|
const classes = blocks[i].className.split(/\s+/).filter(function(cl) {
|
||||||
return cl.startsWith('language-');
|
return cl.startsWith('language-') && !cl.startsWith('language-_');
|
||||||
});
|
});
|
||||||
|
|
||||||
if (classes.length != 0) {
|
if (classes.length != 0) {
|
||||||
|
|
|
@ -219,7 +219,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
|
||||||
};
|
};
|
||||||
|
|
||||||
private showPlaceholder() {
|
private showPlaceholder() {
|
||||||
this.editorRef.current.style.setProperty("--placeholder", `'${this.props.placeholder}'`);
|
// escape single quotes
|
||||||
|
const placeholder = this.props.placeholder.replace(/'/g, '\\\'');
|
||||||
|
this.editorRef.current.style.setProperty("--placeholder", `'${placeholder}'`);
|
||||||
this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty");
|
this.editorRef.current.classList.add("mx_BasicMessageComposer_inputEmpty");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -210,7 +210,7 @@ export default class RoomList extends React.Component<IProps, IState> {
|
||||||
if (unread) {
|
if (unread) {
|
||||||
// filter to only notification rooms (and our current active room so we can index properly)
|
// filter to only notification rooms (and our current active room so we can index properly)
|
||||||
listRooms = listRooms.filter(r => {
|
listRooms = listRooms.filter(r => {
|
||||||
const state = RoomNotificationStateStore.instance.getRoomState(r, t);
|
const state = RoomNotificationStateStore.instance.getRoomState(r);
|
||||||
return state.room.roomId === roomId || state.isUnread;
|
return state.room.roomId === roomId || state.isUnread;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -308,7 +308,7 @@ export default class RoomList extends React.Component<IProps, IState> {
|
||||||
startAsHidden={aesthetics.defaultHidden}
|
startAsHidden={aesthetics.defaultHidden}
|
||||||
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
|
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
|
||||||
onAddRoom={onAddRoomFn}
|
onAddRoom={onAddRoomFn}
|
||||||
addRoomLabel={aesthetics.addRoomLabel}
|
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
|
||||||
isMinimized={this.props.isMinimized}
|
isMinimized={this.props.isMinimized}
|
||||||
onResize={this.props.onResize}
|
onResize={this.props.onResize}
|
||||||
extraBadTilesThatShouldntExist={extraTiles}
|
extraBadTilesThatShouldntExist={extraTiles}
|
||||||
|
|
|
@ -120,7 +120,7 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
hover: false,
|
hover: false,
|
||||||
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room, this.props.tag),
|
notificationState: RoomNotificationStateStore.instance.getRoomState(this.props.room),
|
||||||
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
selected: ActiveRoomObserver.activeRoomId === this.props.room.roomId,
|
||||||
notificationsMenuPosition: null,
|
notificationsMenuPosition: null,
|
||||||
generalMenuPosition: null,
|
generalMenuPosition: null,
|
||||||
|
@ -231,11 +231,11 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
|
|
||||||
if (tagId === DefaultTagID.Favourite) {
|
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
|
||||||
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
|
||||||
const isFavourite = roomTags.includes(DefaultTagID.Favourite);
|
const isApplied = RoomListStore.instance.getTagsForRoom(this.props.room).includes(tagId);
|
||||||
const removeTag = isFavourite ? DefaultTagID.Favourite : DefaultTagID.LowPriority;
|
const removeTag = isApplied ? tagId : inverseTag;
|
||||||
const addTag = isFavourite ? null : DefaultTagID.Favourite;
|
const addTag = isApplied ? null : tagId;
|
||||||
dis.dispatch(RoomListActions.tagRoom(
|
dis.dispatch(RoomListActions.tagRoom(
|
||||||
MatrixClientPeg.get(),
|
MatrixClientPeg.get(),
|
||||||
this.props.room,
|
this.props.room,
|
||||||
|
@ -387,13 +387,6 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
||||||
private renderGeneralMenu(): React.ReactElement {
|
private renderGeneralMenu(): React.ReactElement {
|
||||||
if (!this.showContextMenu) return null; // no menu to show
|
if (!this.showContextMenu) return null; // no menu to show
|
||||||
|
|
||||||
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
|
||||||
|
|
||||||
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
|
||||||
const favouriteIconClassName = isFavorite ? "mx_RoomTile_iconFavorite" : "mx_RoomTile_iconStar";
|
|
||||||
const favouriteLabelClassName = isFavorite ? "mx_RoomTile_contextMenu_activeRow" : "";
|
|
||||||
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
|
|
||||||
|
|
||||||
let contextMenu = null;
|
let contextMenu = null;
|
||||||
if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
|
if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
|
@ -409,19 +402,36 @@ export default class RoomTile extends React.Component<IProps, IState> {
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
);
|
);
|
||||||
} else if (this.state.generalMenuPosition) {
|
} else if (this.state.generalMenuPosition) {
|
||||||
|
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
|
||||||
|
|
||||||
|
const isFavorite = roomTags.includes(DefaultTagID.Favourite);
|
||||||
|
const favouriteLabel = isFavorite ? _t("Favourited") : _t("Favourite");
|
||||||
|
|
||||||
|
const isLowPriority = roomTags.includes(DefaultTagID.LowPriority);
|
||||||
|
const lowPriorityLabel = _t("Low Priority");
|
||||||
|
|
||||||
contextMenu = (
|
contextMenu = (
|
||||||
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
|
<ContextMenu {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
|
||||||
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
|
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
|
||||||
<div className="mx_IconizedContextMenu_optionList">
|
<div className="mx_IconizedContextMenu_optionList">
|
||||||
<MenuItemCheckbox
|
<MenuItemCheckbox
|
||||||
className={favouriteLabelClassName}
|
className={isFavorite ? "mx_RoomTile_contextMenu_activeRow" : ""}
|
||||||
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
|
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
|
||||||
active={isFavorite}
|
active={isFavorite}
|
||||||
label={favouriteLabel}
|
label={favouriteLabel}
|
||||||
>
|
>
|
||||||
<span className={classNames("mx_IconizedContextMenu_icon", favouriteIconClassName)} />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconStar" />
|
||||||
<span className="mx_IconizedContextMenu_label">{favouriteLabel}</span>
|
<span className="mx_IconizedContextMenu_label">{favouriteLabel}</span>
|
||||||
</MenuItemCheckbox>
|
</MenuItemCheckbox>
|
||||||
|
<MenuItemCheckbox
|
||||||
|
className={isLowPriority ? "mx_RoomTile_contextMenu_activeRow" : ""}
|
||||||
|
onClick={(e) => this.onTagRoom(e, DefaultTagID.LowPriority)}
|
||||||
|
active={isLowPriority}
|
||||||
|
label={lowPriorityLabel}
|
||||||
|
>
|
||||||
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconArrowDown" />
|
||||||
|
<span className="mx_IconizedContextMenu_label">{lowPriorityLabel}</span>
|
||||||
|
</MenuItemCheckbox>
|
||||||
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
|
<MenuItem onClick={this.onOpenRoomSettings} label={_t("Settings")}>
|
||||||
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSettings" />
|
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSettings" />
|
||||||
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
|
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>
|
||||||
|
|
|
@ -22,10 +22,6 @@ import * as sdk from "../../../../..";
|
||||||
import AccessibleButton from "../../../elements/AccessibleButton";
|
import AccessibleButton from "../../../elements/AccessibleButton";
|
||||||
import Modal from "../../../../../Modal";
|
import Modal from "../../../../../Modal";
|
||||||
import dis from "../../../../../dispatcher/dispatcher";
|
import dis from "../../../../../dispatcher/dispatcher";
|
||||||
import RoomListStore from "../../../../../stores/room-list/RoomListStore";
|
|
||||||
import RoomListActions from "../../../../../actions/RoomListActions";
|
|
||||||
import { DefaultTagID } from '../../../../../stores/room-list/models';
|
|
||||||
import LabelledToggleSwitch from '../../../elements/LabelledToggleSwitch';
|
|
||||||
|
|
||||||
export default class AdvancedRoomSettingsTab extends React.Component {
|
export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
@ -36,13 +32,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
const room = MatrixClientPeg.get().getRoom(props.roomId);
|
|
||||||
const roomTags = RoomListStore.instance.getTagsForRoom(room);
|
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
// This is eventually set to the value of room.getRecommendedVersion()
|
// This is eventually set to the value of room.getRecommendedVersion()
|
||||||
upgradeRecommendation: null,
|
upgradeRecommendation: null,
|
||||||
isLowPriorityRoom: roomTags.includes(DefaultTagID.LowPriority),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,25 +86,6 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
this.props.closeSettingsFn();
|
this.props.closeSettingsFn();
|
||||||
};
|
};
|
||||||
|
|
||||||
_onToggleLowPriorityTag = (e) => {
|
|
||||||
this.setState({
|
|
||||||
isLowPriorityRoom: !this.state.isLowPriorityRoom,
|
|
||||||
});
|
|
||||||
|
|
||||||
const removeTag = this.state.isLowPriorityRoom ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
|
|
||||||
const addTag = this.state.isLowPriorityRoom ? null : DefaultTagID.LowPriority;
|
|
||||||
const client = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
dis.dispatch(RoomListActions.tagRoom(
|
|
||||||
client,
|
|
||||||
client.getRoom(this.props.roomId),
|
|
||||||
removeTag,
|
|
||||||
addTag,
|
|
||||||
undefined,
|
|
||||||
0,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
const room = client.getRoom(this.props.roomId);
|
const room = client.getRoom(this.props.roomId);
|
||||||
|
@ -183,17 +156,6 @@ export default class AdvancedRoomSettingsTab extends React.Component {
|
||||||
{_t("Open Devtools")}
|
{_t("Open Devtools")}
|
||||||
</AccessibleButton>
|
</AccessibleButton>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx_SettingsTab_section mx_SettingsTab_subsectionText'>
|
|
||||||
<span className='mx_SettingsTab_subheading'>{_t('Make this room low priority')}</span>
|
|
||||||
<LabelledToggleSwitch
|
|
||||||
value={this.state.isLowPriorityRoom}
|
|
||||||
onChange={this._onToggleLowPriorityTag}
|
|
||||||
label={_t(
|
|
||||||
"Low priority rooms show up at the bottom of your room list" +
|
|
||||||
" in a dedicated section at the bottom of your room list",
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,7 @@ function parseCodeBlock(n: HTMLElement, partCreator: PartCreator) {
|
||||||
let language = "";
|
let language = "";
|
||||||
if (n.firstChild && n.firstChild.nodeName === "CODE") {
|
if (n.firstChild && n.firstChild.nodeName === "CODE") {
|
||||||
for (const className of (<HTMLElement>n.firstChild).classList) {
|
for (const className of (<HTMLElement>n.firstChild).classList) {
|
||||||
if (className.startsWith("language-")) {
|
if (className.startsWith("language-") && !className.startsWith("language-_")) {
|
||||||
language = className.substr("language-".length);
|
language = className.substr("language-".length);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -186,7 +186,7 @@ abstract class PlainBasePart extends BasePart {
|
||||||
}
|
}
|
||||||
// when not pasting or dropping text, reject characters that should start a pill candidate
|
// when not pasting or dropping text, reject characters that should start a pill candidate
|
||||||
if (inputType !== "insertFromPaste" && inputType !== "insertFromDrop") {
|
if (inputType !== "insertFromPaste" && inputType !== "insertFromDrop") {
|
||||||
if (chr !== "@" && chr !== "#" && chr !== ":") {
|
if (chr !== "@" && chr !== "#" && chr !== ":" && chr !== "+") {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// only split if the previous character is a space
|
// only split if the previous character is a space
|
||||||
|
@ -467,6 +467,7 @@ export class PartCreator {
|
||||||
case "#":
|
case "#":
|
||||||
case "@":
|
case "@":
|
||||||
case ":":
|
case ":":
|
||||||
|
case "+":
|
||||||
return this.pillCandidate("");
|
return this.pillCandidate("");
|
||||||
case "\n":
|
case "\n":
|
||||||
return new NewlinePart();
|
return new NewlinePart();
|
||||||
|
|
|
@ -162,10 +162,10 @@ export function renderModel(editor: HTMLDivElement, model: EditorModel) {
|
||||||
lines.forEach((parts, i) => {
|
lines.forEach((parts, i) => {
|
||||||
// find first (and remove anything else) div without className
|
// find first (and remove anything else) div without className
|
||||||
// (as browsers insert these in contenteditable) line container
|
// (as browsers insert these in contenteditable) line container
|
||||||
let lineContainer = editor.children[i];
|
let lineContainer = editor.childNodes[i];
|
||||||
while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
|
while (lineContainer && ((<Element>lineContainer).tagName !== "DIV" || !!(<Element>lineContainer).className)) {
|
||||||
editor.removeChild(lineContainer);
|
editor.removeChild(lineContainer);
|
||||||
lineContainer = editor.children[i];
|
lineContainer = editor.childNodes[i];
|
||||||
}
|
}
|
||||||
if (!lineContainer) {
|
if (!lineContainer) {
|
||||||
lineContainer = document.createElement("div");
|
lineContainer = document.createElement("div");
|
||||||
|
|
|
@ -437,50 +437,10 @@
|
||||||
"%(senderName)s started a call": "%(senderName)s started a call",
|
"%(senderName)s started a call": "%(senderName)s started a call",
|
||||||
"Waiting for answer": "Waiting for answer",
|
"Waiting for answer": "Waiting for answer",
|
||||||
"%(senderName)s is calling": "%(senderName)s is calling",
|
"%(senderName)s is calling": "%(senderName)s is calling",
|
||||||
"You created the room": "You created the room",
|
"* %(senderName)s %(emote)s": "* %(senderName)s %(emote)s",
|
||||||
"%(senderName)s created the room": "%(senderName)s created the room",
|
|
||||||
"You made the chat encrypted": "You made the chat encrypted",
|
|
||||||
"%(senderName)s made the chat encrypted": "%(senderName)s made the chat encrypted",
|
|
||||||
"You made history visible to new members": "You made history visible to new members",
|
|
||||||
"%(senderName)s made history visible to new members": "%(senderName)s made history visible to new members",
|
|
||||||
"You made history visible to anyone": "You made history visible to anyone",
|
|
||||||
"%(senderName)s made history visible to anyone": "%(senderName)s made history visible to anyone",
|
|
||||||
"You made history visible to future members": "You made history visible to future members",
|
|
||||||
"%(senderName)s made history visible to future members": "%(senderName)s made history visible to future members",
|
|
||||||
"You were invited": "You were invited",
|
|
||||||
"%(targetName)s was invited": "%(targetName)s was invited",
|
|
||||||
"You left": "You left",
|
|
||||||
"%(targetName)s left": "%(targetName)s left",
|
|
||||||
"You were kicked (%(reason)s)": "You were kicked (%(reason)s)",
|
|
||||||
"%(targetName)s was kicked (%(reason)s)": "%(targetName)s was kicked (%(reason)s)",
|
|
||||||
"You were kicked": "You were kicked",
|
|
||||||
"%(targetName)s was kicked": "%(targetName)s was kicked",
|
|
||||||
"You rejected the invite": "You rejected the invite",
|
|
||||||
"%(targetName)s rejected the invite": "%(targetName)s rejected the invite",
|
|
||||||
"You were uninvited": "You were uninvited",
|
|
||||||
"%(targetName)s was uninvited": "%(targetName)s was uninvited",
|
|
||||||
"You were banned (%(reason)s)": "You were banned (%(reason)s)",
|
|
||||||
"%(targetName)s was banned (%(reason)s)": "%(targetName)s was banned (%(reason)s)",
|
|
||||||
"You were banned": "You were banned",
|
|
||||||
"%(targetName)s was banned": "%(targetName)s was banned",
|
|
||||||
"You joined": "You joined",
|
|
||||||
"%(targetName)s joined": "%(targetName)s joined",
|
|
||||||
"You changed your name": "You changed your name",
|
|
||||||
"%(targetName)s changed their name": "%(targetName)s changed their name",
|
|
||||||
"You changed your avatar": "You changed your avatar",
|
|
||||||
"%(targetName)s changed their avatar": "%(targetName)s changed their avatar",
|
|
||||||
"%(senderName)s %(emote)s": "%(senderName)s %(emote)s",
|
|
||||||
"%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
|
"%(senderName)s: %(message)s": "%(senderName)s: %(message)s",
|
||||||
"You changed the room name": "You changed the room name",
|
|
||||||
"%(senderName)s changed the room name": "%(senderName)s changed the room name",
|
|
||||||
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
"%(senderName)s: %(reaction)s": "%(senderName)s: %(reaction)s",
|
||||||
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
"%(senderName)s: %(stickerName)s": "%(senderName)s: %(stickerName)s",
|
||||||
"You uninvited %(targetName)s": "You uninvited %(targetName)s",
|
|
||||||
"%(senderName)s uninvited %(targetName)s": "%(senderName)s uninvited %(targetName)s",
|
|
||||||
"You invited %(targetName)s": "You invited %(targetName)s",
|
|
||||||
"%(senderName)s invited %(targetName)s": "%(senderName)s invited %(targetName)s",
|
|
||||||
"You changed the room topic": "You changed the room topic",
|
|
||||||
"%(senderName)s changed the room topic": "%(senderName)s changed the room topic",
|
|
||||||
"New spinner design": "New spinner design",
|
"New spinner design": "New spinner design",
|
||||||
"Message Pinning": "Message Pinning",
|
"Message Pinning": "Message Pinning",
|
||||||
"Custom user status messages": "Custom user status messages",
|
"Custom user status messages": "Custom user status messages",
|
||||||
|
@ -967,8 +927,6 @@
|
||||||
"Room version:": "Room version:",
|
"Room version:": "Room version:",
|
||||||
"Developer options": "Developer options",
|
"Developer options": "Developer options",
|
||||||
"Open Devtools": "Open Devtools",
|
"Open Devtools": "Open Devtools",
|
||||||
"Make this room low priority": "Make this room low priority",
|
|
||||||
"Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list": "Low priority rooms show up at the bottom of your room list in a dedicated section at the bottom of your room list",
|
|
||||||
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "This room is bridging messages to the following platforms. <a>Learn more.</a>",
|
"This room is bridging messages to the following platforms. <a>Learn more.</a>": "This room is bridging messages to the following platforms. <a>Learn more.</a>",
|
||||||
"This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "This room isn’t bridging messages to any platforms. <a>Learn more.</a>",
|
"This room isn’t bridging messages to any platforms. <a>Learn more.</a>": "This room isn’t bridging messages to any platforms. <a>Learn more.</a>",
|
||||||
"Bridges": "Bridges",
|
"Bridges": "Bridges",
|
||||||
|
@ -1216,10 +1174,11 @@
|
||||||
"All messages": "All messages",
|
"All messages": "All messages",
|
||||||
"Mentions & Keywords": "Mentions & Keywords",
|
"Mentions & Keywords": "Mentions & Keywords",
|
||||||
"Notification options": "Notification options",
|
"Notification options": "Notification options",
|
||||||
"Favourited": "Favourited",
|
|
||||||
"Favourite": "Favourite",
|
|
||||||
"Leave Room": "Leave Room",
|
"Leave Room": "Leave Room",
|
||||||
"Forget Room": "Forget Room",
|
"Forget Room": "Forget Room",
|
||||||
|
"Favourited": "Favourited",
|
||||||
|
"Favourite": "Favourite",
|
||||||
|
"Low Priority": "Low Priority",
|
||||||
"Room options": "Room options",
|
"Room options": "Room options",
|
||||||
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
|
||||||
"%(count)s unread messages including mentions.|one": "1 unread mention.",
|
"%(count)s unread messages including mentions.|one": "1 unread mention.",
|
||||||
|
@ -1912,7 +1871,6 @@
|
||||||
"Mentions only": "Mentions only",
|
"Mentions only": "Mentions only",
|
||||||
"Leave": "Leave",
|
"Leave": "Leave",
|
||||||
"Forget": "Forget",
|
"Forget": "Forget",
|
||||||
"Low Priority": "Low Priority",
|
|
||||||
"Direct Chat": "Direct Chat",
|
"Direct Chat": "Direct Chat",
|
||||||
"Clear status": "Clear status",
|
"Clear status": "Clear status",
|
||||||
"Update status": "Update status",
|
"Update status": "Update status",
|
||||||
|
|
|
@ -166,6 +166,10 @@ export const SETTINGS = {
|
||||||
displayName: _td("Show info about bridges in room settings"),
|
displayName: _td("Show info about bridges in room settings"),
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
"RoomList.backgroundImage": {
|
||||||
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
"baseFontSize": {
|
"baseFontSize": {
|
||||||
displayName: _td("Font size"),
|
displayName: _td("Font size"),
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
|
|
@ -42,7 +42,7 @@ export const UPDATE_EVENT = "update";
|
||||||
* help prevent lock conflicts.
|
* help prevent lock conflicts.
|
||||||
*/
|
*/
|
||||||
export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
||||||
private storeState: T;
|
private storeState: Readonly<T>;
|
||||||
private lock = new AwaitLock();
|
private lock = new AwaitLock();
|
||||||
private readonly dispatcherRef: string;
|
private readonly dispatcherRef: string;
|
||||||
|
|
||||||
|
@ -62,7 +62,7 @@ export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
||||||
* The current state of the store. Cannot be mutated.
|
* The current state of the store. Cannot be mutated.
|
||||||
*/
|
*/
|
||||||
protected get state(): T {
|
protected get state(): T {
|
||||||
return Object.freeze(this.storeState);
|
return this.storeState;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -79,7 +79,7 @@ export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
||||||
protected async updateState(newState: T | Object) {
|
protected async updateState(newState: T | Object) {
|
||||||
await this.lock.acquireAsync();
|
await this.lock.acquireAsync();
|
||||||
try {
|
try {
|
||||||
this.storeState = Object.assign(<T>{}, this.storeState, newState);
|
this.storeState = Object.freeze(Object.assign(<T>{}, this.storeState, newState));
|
||||||
this.emit(UPDATE_EVENT, this);
|
this.emit(UPDATE_EVENT, this);
|
||||||
} finally {
|
} finally {
|
||||||
await this.lock.release();
|
await this.lock.release();
|
||||||
|
@ -94,7 +94,7 @@ export abstract class AsyncStore<T extends Object> extends EventEmitter {
|
||||||
protected async reset(newState: T | Object = null, quiet = false) {
|
protected async reset(newState: T | Object = null, quiet = false) {
|
||||||
await this.lock.acquireAsync();
|
await this.lock.acquireAsync();
|
||||||
try {
|
try {
|
||||||
this.storeState = <T>(newState || {});
|
this.storeState = Object.freeze(<T>(newState || {}));
|
||||||
if (!quiet) this.emit(UPDATE_EVENT, this);
|
if (!quiet) this.emit(UPDATE_EVENT, this);
|
||||||
} finally {
|
} finally {
|
||||||
await this.lock.release();
|
await this.lock.release();
|
||||||
|
|
|
@ -21,21 +21,36 @@ import { DefaultTagID, TagID } from "../room-list/models";
|
||||||
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
|
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import { Room } from "matrix-js-sdk/src/models/room";
|
||||||
import { RoomNotificationState } from "./RoomNotificationState";
|
import { RoomNotificationState } from "./RoomNotificationState";
|
||||||
|
import { SummarizedNotificationState } from "./SummarizedNotificationState";
|
||||||
const INSPECIFIC_TAG = "INSPECIFIC_TAG";
|
|
||||||
type INSPECIFIC_TAG = "INSPECIFIC_TAG";
|
|
||||||
|
|
||||||
interface IState {}
|
interface IState {}
|
||||||
|
|
||||||
export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
private static internalInstance = new RoomNotificationStateStore();
|
private static internalInstance = new RoomNotificationStateStore();
|
||||||
|
|
||||||
private roomMap = new Map<Room, Map<TagID | INSPECIFIC_TAG, RoomNotificationState>>();
|
private roomMap = new Map<Room, RoomNotificationState>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super(defaultDispatcher, {});
|
super(defaultDispatcher, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a snapshot of notification state for all visible rooms. The number of states recorded
|
||||||
|
* on the SummarizedNotificationState is equivalent to rooms.
|
||||||
|
*/
|
||||||
|
public get globalState(): SummarizedNotificationState {
|
||||||
|
// If we're not ready yet, just return an empty state
|
||||||
|
if (!this.matrixClient) return new SummarizedNotificationState();
|
||||||
|
|
||||||
|
// Only count visible rooms to not torment the user with notification counts in rooms they can't see.
|
||||||
|
// This will include highlights from the previous version of the room internally
|
||||||
|
const globalState = new SummarizedNotificationState();
|
||||||
|
for (const room of this.matrixClient.getVisibleRooms()) {
|
||||||
|
globalState.add(this.getRoomState(room));
|
||||||
|
}
|
||||||
|
return globalState;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new list notification state. The consumer is expected to set the rooms
|
* Creates a new list notification state. The consumer is expected to set the rooms
|
||||||
* on the notification state, and destroy the state when it no longer needs it.
|
* on the notification state, and destroy the state when it no longer needs it.
|
||||||
|
@ -49,7 +64,7 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
// TODO: Update if/when invites move out of the room list.
|
// TODO: Update if/when invites move out of the room list.
|
||||||
const useTileCount = tagId === DefaultTagID.Invite;
|
const useTileCount = tagId === DefaultTagID.Invite;
|
||||||
const getRoomFn: FetchRoomFn = (room: Room) => {
|
const getRoomFn: FetchRoomFn = (room: Room) => {
|
||||||
return this.getRoomState(room, tagId);
|
return this.getRoomState(room);
|
||||||
};
|
};
|
||||||
return new ListNotificationState(useTileCount, tagId, getRoomFn);
|
return new ListNotificationState(useTileCount, tagId, getRoomFn);
|
||||||
}
|
}
|
||||||
|
@ -59,22 +74,13 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
* attempt to destroy the returned state as it may be shared with other
|
* attempt to destroy the returned state as it may be shared with other
|
||||||
* consumers.
|
* consumers.
|
||||||
* @param room The room to get the notification state for.
|
* @param room The room to get the notification state for.
|
||||||
* @param inTagId Optional tag ID to scope the notification state to.
|
|
||||||
* @returns The room's notification state.
|
* @returns The room's notification state.
|
||||||
*/
|
*/
|
||||||
public getRoomState(room: Room, inTagId?: TagID): RoomNotificationState {
|
public getRoomState(room: Room): RoomNotificationState {
|
||||||
if (!this.roomMap.has(room)) {
|
if (!this.roomMap.has(room)) {
|
||||||
this.roomMap.set(room, new Map<TagID | INSPECIFIC_TAG, RoomNotificationState>());
|
this.roomMap.set(room, new RoomNotificationState(room));
|
||||||
}
|
}
|
||||||
|
return this.roomMap.get(room);
|
||||||
const targetTag = inTagId ? inTagId : INSPECIFIC_TAG;
|
|
||||||
|
|
||||||
const forRoomMap = this.roomMap.get(room);
|
|
||||||
if (!forRoomMap.has(targetTag)) {
|
|
||||||
forRoomMap.set(inTagId ? inTagId : INSPECIFIC_TAG, new RoomNotificationState(room));
|
|
||||||
}
|
|
||||||
|
|
||||||
return forRoomMap.get(targetTag);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get instance(): RoomNotificationStateStore {
|
public static get instance(): RoomNotificationStateStore {
|
||||||
|
@ -82,10 +88,8 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onNotReady(): Promise<any> {
|
protected async onNotReady(): Promise<any> {
|
||||||
for (const roomMap of this.roomMap.values()) {
|
for (const roomState of this.roomMap.values()) {
|
||||||
for (const roomState of roomMap.values()) {
|
roomState.destroy();
|
||||||
roomState.destroy();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
62
src/stores/notifications/SummarizedNotificationState.ts
Normal file
62
src/stores/notifications/SummarizedNotificationState.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
/*
|
||||||
|
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { NotificationColor } from "./NotificationColor";
|
||||||
|
import { NotificationState } from "./NotificationState";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summarizes a number of states into a unique snapshot. To populate, call
|
||||||
|
* the add() function with the notification states to be included.
|
||||||
|
*
|
||||||
|
* Useful for community notification counts, global notification counts, etc.
|
||||||
|
*/
|
||||||
|
export class SummarizedNotificationState extends NotificationState {
|
||||||
|
private totalStatesWithUnread = 0;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._symbol = null;
|
||||||
|
this._count = 0;
|
||||||
|
this._color = NotificationColor.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get numUnreadStates(): number {
|
||||||
|
return this.totalStatesWithUnread;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Append a notification state to this snapshot, taking the loudest NotificationColor
|
||||||
|
* of the two. By default this will not adopt the symbol of the other notification
|
||||||
|
* state to prevent the count from being lost in typical usage.
|
||||||
|
* @param other The other notification state to append.
|
||||||
|
* @param includeSymbol If true, the notification state's symbol will be taken if one
|
||||||
|
* is present.
|
||||||
|
*/
|
||||||
|
public add(other: NotificationState, includeSymbol = false) {
|
||||||
|
if (other.symbol && includeSymbol) {
|
||||||
|
this._symbol = other.symbol;
|
||||||
|
}
|
||||||
|
if (other.count) {
|
||||||
|
this._count += other.count;
|
||||||
|
}
|
||||||
|
if (other.color > this.color) {
|
||||||
|
this._color = other.color;
|
||||||
|
}
|
||||||
|
if (other.hasUnreadCount) {
|
||||||
|
this.totalStatesWithUnread++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,42 +19,20 @@ import { ActionPayload } from "../../dispatcher/payloads";
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { MessageEventPreview } from "./previews/MessageEventPreview";
|
import { MessageEventPreview } from "./previews/MessageEventPreview";
|
||||||
import { NameEventPreview } from "./previews/NameEventPreview";
|
|
||||||
import { TagID } from "./models";
|
import { TagID } from "./models";
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
import { TopicEventPreview } from "./previews/TopicEventPreview";
|
|
||||||
import { MembershipEventPreview } from "./previews/MembershipEventPreview";
|
|
||||||
import { HistoryVisibilityEventPreview } from "./previews/HistoryVisibilityEventPreview";
|
|
||||||
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
|
import { CallInviteEventPreview } from "./previews/CallInviteEventPreview";
|
||||||
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
|
import { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
|
||||||
import { CallHangupEvent } from "./previews/CallHangupEvent";
|
import { CallHangupEvent } from "./previews/CallHangupEvent";
|
||||||
import { EncryptionEventPreview } from "./previews/EncryptionEventPreview";
|
|
||||||
import { ThirdPartyInviteEventPreview } from "./previews/ThirdPartyInviteEventPreview";
|
|
||||||
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
import { StickerEventPreview } from "./previews/StickerEventPreview";
|
||||||
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
|
||||||
import { CreationEventPreview } from "./previews/CreationEventPreview";
|
import { UPDATE_EVENT } from "../AsyncStore";
|
||||||
|
|
||||||
const PREVIEWS = {
|
const PREVIEWS = {
|
||||||
'm.room.message': {
|
'm.room.message': {
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new MessageEventPreview(),
|
previewer: new MessageEventPreview(),
|
||||||
},
|
},
|
||||||
'm.room.name': {
|
|
||||||
isState: true,
|
|
||||||
previewer: new NameEventPreview(),
|
|
||||||
},
|
|
||||||
'm.room.topic': {
|
|
||||||
isState: true,
|
|
||||||
previewer: new TopicEventPreview(),
|
|
||||||
},
|
|
||||||
'm.room.member': {
|
|
||||||
isState: true,
|
|
||||||
previewer: new MembershipEventPreview(),
|
|
||||||
},
|
|
||||||
'm.room.history_visibility': {
|
|
||||||
isState: true,
|
|
||||||
previewer: new HistoryVisibilityEventPreview(),
|
|
||||||
},
|
|
||||||
'm.call.invite': {
|
'm.call.invite': {
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new CallInviteEventPreview(),
|
previewer: new CallInviteEventPreview(),
|
||||||
|
@ -67,14 +45,6 @@ const PREVIEWS = {
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new CallHangupEvent(),
|
previewer: new CallHangupEvent(),
|
||||||
},
|
},
|
||||||
'm.room.encryption': {
|
|
||||||
isState: true,
|
|
||||||
previewer: new EncryptionEventPreview(),
|
|
||||||
},
|
|
||||||
'm.room.third_party_invite': {
|
|
||||||
isState: true,
|
|
||||||
previewer: new ThirdPartyInviteEventPreview(),
|
|
||||||
},
|
|
||||||
'm.sticker': {
|
'm.sticker': {
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new StickerEventPreview(),
|
previewer: new StickerEventPreview(),
|
||||||
|
@ -83,10 +53,6 @@ const PREVIEWS = {
|
||||||
isState: false,
|
isState: false,
|
||||||
previewer: new ReactionEventPreview(),
|
previewer: new ReactionEventPreview(),
|
||||||
},
|
},
|
||||||
'm.room.create': {
|
|
||||||
isState: true,
|
|
||||||
previewer: new CreationEventPreview(),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The maximum number of events we're willing to look back on to get a preview.
|
// The maximum number of events we're willing to look back on to get a preview.
|
||||||
|
@ -97,12 +63,15 @@ type TAG_ANY = "im.vector.any";
|
||||||
const TAG_ANY: TAG_ANY = "im.vector.any";
|
const TAG_ANY: TAG_ANY = "im.vector.any";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
[roomId: string]: Map<TagID | TAG_ANY, string | null>; // null indicates the preview is empty / irrelevant
|
// Empty because we don't actually use the state
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||||
private static internalInstance = new MessagePreviewStore();
|
private static internalInstance = new MessagePreviewStore();
|
||||||
|
|
||||||
|
// null indicates the preview is empty / irrelevant
|
||||||
|
private previews = new Map<string, Map<TagID|TAG_ANY, string|null>>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super(defaultDispatcher, {});
|
super(defaultDispatcher, {});
|
||||||
}
|
}
|
||||||
|
@ -120,10 +89,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||||
public getPreviewForRoom(room: Room, inTagId: TagID): string {
|
public getPreviewForRoom(room: Room, inTagId: TagID): string {
|
||||||
if (!room) return null; // invalid room, just return nothing
|
if (!room) return null; // invalid room, just return nothing
|
||||||
|
|
||||||
const val = this.state[room.roomId];
|
if (!this.previews.has(room.roomId)) this.generatePreview(room, inTagId);
|
||||||
if (!val) this.generatePreview(room, inTagId);
|
|
||||||
|
|
||||||
const previews = this.state[room.roomId];
|
const previews = this.previews.get(room.roomId);
|
||||||
if (!previews) return null;
|
if (!previews) return null;
|
||||||
|
|
||||||
if (!previews.has(inTagId)) {
|
if (!previews.has(inTagId)) {
|
||||||
|
@ -136,11 +104,10 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||||
const events = room.timeline;
|
const events = room.timeline;
|
||||||
if (!events) return; // should only happen in tests
|
if (!events) return; // should only happen in tests
|
||||||
|
|
||||||
let map = this.state[room.roomId];
|
let map = this.previews.get(room.roomId);
|
||||||
if (!map) {
|
if (!map) {
|
||||||
map = new Map<TagID | TAG_ANY, string | null>();
|
map = new Map<TagID | TAG_ANY, string | null>();
|
||||||
|
this.previews.set(room.roomId, map);
|
||||||
// We set the state later with the map, so no need to send an update now
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the tags so we know what to generate
|
// Set the tags so we know what to generate
|
||||||
|
@ -176,16 +143,16 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
// Update state for good measure - causes emit for update
|
// We've muted the underlying Map, so just emit that we've changed.
|
||||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
this.previews.set(room.roomId, map);
|
||||||
this.updateState({[room.roomId]: map});
|
this.emit(UPDATE_EVENT, this);
|
||||||
}
|
}
|
||||||
return; // we're done
|
return; // we're done
|
||||||
}
|
}
|
||||||
|
|
||||||
// At this point, we didn't generate a preview so clear it
|
// At this point, we didn't generate a preview so clear it
|
||||||
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
|
this.previews.set(room.roomId, new Map<TagID|TAG_ANY, string|null>());
|
||||||
this.updateState({[room.roomId]: null});
|
this.emit(UPDATE_EVENT, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onAction(payload: ActionPayload) {
|
protected async onAction(payload: ActionPayload) {
|
||||||
|
@ -193,7 +160,7 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
|
||||||
|
|
||||||
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
if (payload.action === 'MatrixActions.Room.timeline' || payload.action === 'MatrixActions.Event.decrypted') {
|
||||||
const event = payload.event; // TODO: Type out the dispatcher
|
const event = payload.event; // TODO: Type out the dispatcher
|
||||||
if (!Object.keys(this.state).includes(event.getRoomId())) return; // not important
|
if (!this.previews.has(event.getRoomId())) return; // not important
|
||||||
this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
|
this.generatePreview(this.matrixClient.getRoom(event.getRoomId()), TAG_ANY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,6 +168,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async onAction(payload: ActionPayload) {
|
protected async onAction(payload: ActionPayload) {
|
||||||
|
// If we're not remotely ready, don't even bother scheduling the dispatch handling.
|
||||||
|
// This is repeated in the handler just in case things change between a decision here and
|
||||||
|
// when the timer fires.
|
||||||
|
const logicallyReady = this.matrixClient && this.initialListsGenerated;
|
||||||
|
if (!logicallyReady) return;
|
||||||
|
|
||||||
// When we're running tests we can't reliably use setImmediate out of timing concerns.
|
// When we're running tests we can't reliably use setImmediate out of timing concerns.
|
||||||
// As such, we use a more synchronous model.
|
// As such, we use a more synchronous model.
|
||||||
if (RoomListStoreClass.TEST_MODE) {
|
if (RoomListStoreClass.TEST_MODE) {
|
||||||
|
|
|
@ -90,7 +90,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
||||||
private getRoomCategory(room: Room): NotificationColor {
|
private getRoomCategory(room: Room): NotificationColor {
|
||||||
// It's fine for us to call this a lot because it's cached, and we shouldn't be
|
// It's fine for us to call this a lot because it's cached, and we shouldn't be
|
||||||
// wasting anything by doing so as the store holds single references
|
// wasting anything by doing so as the store holds single references
|
||||||
const state = RoomNotificationStateStore.instance.getRoomState(room, this.tagId);
|
const state = RoomNotificationStateStore.instance.getRoomState(room);
|
||||||
return state.color;
|
return state.color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +123,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
|
||||||
const category = this.getRoomCategory(room);
|
const category = this.getRoomCategory(room);
|
||||||
this.alterCategoryPositionBy(category, 1, this.indices);
|
this.alterCategoryPositionBy(category, 1, this.indices);
|
||||||
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
|
||||||
|
await this.sortCategory(category);
|
||||||
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
} else if (cause === RoomUpdateCause.RoomRemoved) {
|
||||||
const roomIdx = this.getRoomIndex(room);
|
const roomIdx = this.getRoomIndex(room);
|
||||||
if (roomIdx === -1) {
|
if (roomIdx === -1) {
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IPreview } from "./IPreview";
|
|
||||||
import { TagID } from "../models";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { getSenderName, isSelf } from "./utils";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
export class CreationEventPreview implements IPreview {
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
|
||||||
if (isSelf(event)) {
|
|
||||||
return _t("You created the room");
|
|
||||||
} else {
|
|
||||||
return _t("%(senderName)s created the room", {senderName: getSenderName(event)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IPreview } from "./IPreview";
|
|
||||||
import { TagID } from "../models";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { getSenderName, isSelf } from "./utils";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
export class EncryptionEventPreview implements IPreview {
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
|
||||||
if (isSelf(event)) {
|
|
||||||
return _t("You made the chat encrypted");
|
|
||||||
} else {
|
|
||||||
return _t("%(senderName)s made the chat encrypted", {senderName: getSenderName(event)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IPreview } from "./IPreview";
|
|
||||||
import { TagID } from "../models";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { getSenderName, isSelf } from "./utils";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
export class HistoryVisibilityEventPreview implements IPreview {
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
|
||||||
const visibility = event.getContent()['history_visibility'];
|
|
||||||
const isUs = isSelf(event);
|
|
||||||
|
|
||||||
if (visibility === 'invited' || visibility === 'joined') {
|
|
||||||
return isUs
|
|
||||||
? _t("You made history visible to new members")
|
|
||||||
: _t("%(senderName)s made history visible to new members", {senderName: getSenderName(event)});
|
|
||||||
} else if (visibility === 'world_readable') {
|
|
||||||
return isUs
|
|
||||||
? _t("You made history visible to anyone")
|
|
||||||
: _t("%(senderName)s made history visible to anyone", {senderName: getSenderName(event)});
|
|
||||||
} else { // shared, default
|
|
||||||
return isUs
|
|
||||||
? _t("You made history visible to future members")
|
|
||||||
: _t("%(senderName)s made history visible to future members", {senderName: getSenderName(event)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,90 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IPreview } from "./IPreview";
|
|
||||||
import { TagID } from "../models";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { getTargetName, isSelfTarget } from "./utils";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
export class MembershipEventPreview implements IPreview {
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
|
||||||
const newMembership = event.getContent()['membership'];
|
|
||||||
const oldMembership = event.getPrevContent()['membership'];
|
|
||||||
const reason = event.getContent()['reason'];
|
|
||||||
const isUs = isSelfTarget(event);
|
|
||||||
|
|
||||||
if (newMembership === 'invite') {
|
|
||||||
return isUs
|
|
||||||
? _t("You were invited")
|
|
||||||
: _t("%(targetName)s was invited", {targetName: getTargetName(event)});
|
|
||||||
} else if (newMembership === 'leave' && oldMembership !== 'invite') {
|
|
||||||
if (event.getSender() === event.getStateKey()) {
|
|
||||||
return isUs
|
|
||||||
? _t("You left")
|
|
||||||
: _t("%(targetName)s left", {targetName: getTargetName(event)});
|
|
||||||
} else {
|
|
||||||
if (reason) {
|
|
||||||
return isUs
|
|
||||||
? _t("You were kicked (%(reason)s)", {reason})
|
|
||||||
: _t("%(targetName)s was kicked (%(reason)s)", {targetName: getTargetName(event), reason});
|
|
||||||
} else {
|
|
||||||
return isUs
|
|
||||||
? _t("You were kicked")
|
|
||||||
: _t("%(targetName)s was kicked", {targetName: getTargetName(event)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (newMembership === 'leave' && oldMembership === 'invite') {
|
|
||||||
if (event.getSender() === event.getStateKey()) {
|
|
||||||
return isUs
|
|
||||||
? _t("You rejected the invite")
|
|
||||||
: _t("%(targetName)s rejected the invite", {targetName: getTargetName(event)});
|
|
||||||
} else {
|
|
||||||
return isUs
|
|
||||||
? _t("You were uninvited")
|
|
||||||
: _t("%(targetName)s was uninvited", {targetName: getTargetName(event)});
|
|
||||||
}
|
|
||||||
} else if (newMembership === 'ban') {
|
|
||||||
if (reason) {
|
|
||||||
return isUs
|
|
||||||
? _t("You were banned (%(reason)s)", {reason})
|
|
||||||
: _t("%(targetName)s was banned (%(reason)s)", {targetName: getTargetName(event), reason});
|
|
||||||
} else {
|
|
||||||
return isUs
|
|
||||||
? _t("You were banned")
|
|
||||||
: _t("%(targetName)s was banned", {targetName: getTargetName(event)});
|
|
||||||
}
|
|
||||||
} else if (newMembership === 'join' && oldMembership !== 'join') {
|
|
||||||
return isUs
|
|
||||||
? _t("You joined")
|
|
||||||
: _t("%(targetName)s joined", {targetName: getTargetName(event)});
|
|
||||||
} else {
|
|
||||||
const isDisplayNameChange = event.getContent()['displayname'] !== event.getPrevContent()['displayname'];
|
|
||||||
const isAvatarChange = event.getContent()['avatar_url'] !== event.getPrevContent()['avatar_url'];
|
|
||||||
if (isDisplayNameChange) {
|
|
||||||
return isUs
|
|
||||||
? _t("You changed your name")
|
|
||||||
: _t("%(targetName)s changed their name", {targetName: getTargetName(event)});
|
|
||||||
} else if (isAvatarChange) {
|
|
||||||
return isUs
|
|
||||||
? _t("You changed your avatar")
|
|
||||||
: _t("%(targetName)s changed their avatar", {targetName: getTargetName(event)});
|
|
||||||
} else {
|
|
||||||
return null; // no change
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -59,7 +59,7 @@ export class MessageEventPreview implements IPreview {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msgtype === 'm.emote') {
|
if (msgtype === 'm.emote') {
|
||||||
return _t("%(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
|
return _t("* %(senderName)s %(emote)s", {senderName: getSenderName(event), emote: body});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) {
|
||||||
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IPreview } from "./IPreview";
|
|
||||||
import { TagID } from "../models";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { getSenderName, isSelf } from "./utils";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
export class NameEventPreview implements IPreview {
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
|
||||||
if (isSelf(event)) {
|
|
||||||
return _t("You changed the room name");
|
|
||||||
} else {
|
|
||||||
return _t("%(senderName)s changed the room name", {senderName: getSenderName(event)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,42 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IPreview } from "./IPreview";
|
|
||||||
import { TagID } from "../models";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { getSenderName, isSelf } from "./utils";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
import { isValid3pidInvite } from "../../../RoomInvite";
|
|
||||||
|
|
||||||
export class ThirdPartyInviteEventPreview implements IPreview {
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
|
||||||
if (!isValid3pidInvite(event)) {
|
|
||||||
const targetName = event.getPrevContent().display_name || _t("Someone");
|
|
||||||
if (isSelf(event)) {
|
|
||||||
return _t("You uninvited %(targetName)s", {targetName});
|
|
||||||
} else {
|
|
||||||
return _t("%(senderName)s uninvited %(targetName)s", {senderName: getSenderName(event), targetName});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const targetName = event.getContent().display_name;
|
|
||||||
if (isSelf(event)) {
|
|
||||||
return _t("You invited %(targetName)s", {targetName});
|
|
||||||
} else {
|
|
||||||
return _t("%(senderName)s invited %(targetName)s", {senderName: getSenderName(event), targetName});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { IPreview } from "./IPreview";
|
|
||||||
import { TagID } from "../models";
|
|
||||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
|
||||||
import { getSenderName, isSelf } from "./utils";
|
|
||||||
import { _t } from "../../../languageHandler";
|
|
||||||
|
|
||||||
export class TopicEventPreview implements IPreview {
|
|
||||||
public getTextFor(event: MatrixEvent, tagId?: TagID): string {
|
|
||||||
if (isSelf(event)) {
|
|
||||||
return _t("You changed the room topic");
|
|
||||||
} else {
|
|
||||||
return _t("%(senderName)s changed the room topic", {senderName: getSenderName(event)});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -205,9 +205,8 @@ describe("<TextualBody />", () => {
|
||||||
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
expect(content.html()).toBe('<span class="mx_EventTile_body markdown-body" dir="auto">' +
|
||||||
'Hey <span>' +
|
'Hey <span>' +
|
||||||
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
|
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
|
||||||
'<img src="mxc://avatar.url/image.png" style="width: 16px; height: 16px;" ' +
|
'<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
|
||||||
'title="@member:domain.bla" alt="" aria-hidden="true" role="button" tabindex="0" ' +
|
'style="width: 16px; height: 16px;" title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
|
||||||
'class="mx_AccessibleButton mx_BaseAvatar mx_BaseAvatar_image">Member</a>' +
|
|
||||||
'</span></span>');
|
'</span></span>');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue