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:
Michael Telatynski 2020-07-24 08:18:20 +01:00
commit c578026474
46 changed files with 1487 additions and 1521 deletions

View file

@ -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": [

View file

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

View file

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

View file

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

View file

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

View file

@ -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');
} }

View file

@ -24,6 +24,7 @@ import { RoomListStoreClass } from "../stores/room-list/RoomListStore";
import { PlatformPeg } from "../PlatformPeg"; import { PlatformPeg } from "../PlatformPeg";
import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore"; import RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers"; import {IntegrationManagers} from "../integrations/IntegrationManagers";
import {ModalManager} from "../Modal";
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

View file

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

View file

@ -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(' ');
} }

View file

@ -1,5 +1,6 @@
/* /*
Copyright 2015, 2016 OpenMarket Ltd Copyright 2015, 2016 OpenMarket Ltd
Copyright 2020 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
@ -17,6 +18,8 @@ limitations under the License.
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import classNames from 'classnames';
import Analytics from './Analytics'; import Analytics from './Analytics';
import dis from './dispatcher/dispatcher'; import dis from './dispatcher/dispatcher';
import {defer} from './utils/promise'; import {defer} from './utils/promise';
@ -25,36 +28,48 @@ import AsyncWrapper from './AsyncWrapper';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
class ModalManager { interface IModal<T extends any[]> {
constructor() { elem: React.ReactNode;
this._counter = 0; className?: string;
beforeClosePromise?: Promise<boolean>;
closeReason?: string;
onBeforeClose?(reason?: string): Promise<boolean>;
onFinished(...args: T): void;
close(...args: T): void;
}
// 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;

View file

@ -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("Were excited to announce Riot is now Element"); description = _t("Were 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,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = (

View file

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

View file

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

View file

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

View file

@ -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) {

View file

@ -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");
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 isnt bridging messages to any platforms. <a>Learn more.</a>": "This room isnt bridging messages to any platforms. <a>Learn more.</a>", "This room isnt bridging messages to any platforms. <a>Learn more.</a>": "This room isnt 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",

View file

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

View file

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

View file

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

View 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++;
}
}
}

View file

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

View file

@ -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) {

View file

@ -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) {

View file

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

View file

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

View file

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

View file

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

View file

@ -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)) {

View file

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

View file

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

View file

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

View file

@ -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>');
}); });
}); });

1788
yarn.lock

File diff suppressed because it is too large Load diff