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"
},
"dependencies": {
"@babel/runtime": "^7.8.3",
"@babel/runtime": "^7.10.5",
"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-request": "^0.3.3",
"classnames": "^2.1.2",
"commonmark": "^0.28.1",
"counterpart": "^0.18.0",
"create-react-class": "^15.6.0",
"diff-dom": "^4.1.3",
"diff-match-patch": "^1.0.4",
"classnames": "^2.2.6",
"commonmark": "^0.29.1",
"counterpart": "^0.18.6",
"create-react-class": "^15.6.3",
"diff-dom": "^4.1.6",
"diff-match-patch": "^1.0.5",
"emojibase-data": "^5.0.1",
"emojibase-regex": "^4.0.1",
"escape-html": "^1.0.3",
"file-saver": "^1.3.3",
"filesize": "3.5.6",
"file-saver": "^1.3.8",
"filesize": "3.6.1",
"flux": "2.1.1",
"focus-visible": "^5.0.2",
"fuse.js": "^2.2.0",
"gfm.css": "^1.1.1",
"focus-visible": "^5.1.0",
"fuse.js": "^2.7.4",
"gfm.css": "^1.1.2",
"glob-to-regexp": "^0.4.1",
"highlight.js": "^9.15.8",
"html-entities": "^1.2.1",
"highlight.js": "^10.1.2",
"html-entities": "^1.3.1",
"is-ip": "^2.0.0",
"linkifyjs": "^2.1.6",
"lodash": "^4.17.14",
"linkifyjs": "^2.1.9",
"lodash": "^4.17.19",
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
"minimist": "^1.2.0",
"pako": "^1.0.5",
"minimist": "^1.2.5",
"pako": "^1.0.11",
"parse5": "^5.1.1",
"png-chunks-extract": "^1.0.0",
"project-name-generator": "^2.1.7",
"prop-types": "^15.5.8",
"prop-types": "^15.7.2",
"qrcode": "^1.4.4",
"qs": "^6.6.0",
"re-resizable": "^6.5.2",
"react": "^16.9.0",
"qs": "^6.9.4",
"re-resizable": "^6.5.4",
"react": "^16.13.1",
"react-beautiful-dnd": "^4.0.1",
"react-dom": "^16.9.0",
"react-focus-lock": "^2.2.1",
"react-dom": "^16.13.1",
"react-focus-lock": "^2.4.1",
"react-transition-group": "^4.4.1",
"resize-observer-polyfill": "^1.5.0",
"sanitize-html": "^1.18.4",
"text-encoding-utf-8": "^1.0.1",
"resize-observer-polyfill": "^1.5.1",
"sanitize-html": "^1.27.1",
"text-encoding-utf-8": "^1.0.2",
"url": "^0.11.0",
"velocity-animate": "^1.5.2",
"what-input": "^5.2.6",
"what-input": "^5.2.10",
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/cli": "^7.7.5",
"@babel/core": "^7.7.5",
"@babel/plugin-proposal-class-properties": "^7.7.4",
"@babel/plugin-proposal-decorators": "^7.7.4",
"@babel/plugin-proposal-export-default-from": "^7.7.4",
"@babel/plugin-proposal-numeric-separator": "^7.7.4",
"@babel/plugin-proposal-object-rest-spread": "^7.7.4",
"@babel/plugin-transform-flow-comments": "^7.7.4",
"@babel/plugin-transform-runtime": "^7.8.3",
"@babel/preset-env": "^7.7.6",
"@babel/preset-flow": "^7.7.4",
"@babel/preset-react": "^7.7.4",
"@babel/preset-typescript": "^7.7.4",
"@babel/register": "^7.7.4",
"@peculiar/webcrypto": "^1.0.22",
"@babel/cli": "^7.10.5",
"@babel/core": "^7.10.5",
"@babel/plugin-proposal-class-properties": "^7.10.4",
"@babel/plugin-proposal-decorators": "^7.10.5",
"@babel/plugin-proposal-export-default-from": "^7.10.4",
"@babel/plugin-proposal-numeric-separator": "^7.10.4",
"@babel/plugin-proposal-object-rest-spread": "^7.10.4",
"@babel/plugin-transform-flow-comments": "^7.10.4",
"@babel/plugin-transform-runtime": "^7.10.5",
"@babel/preset-env": "^7.10.4",
"@babel/preset-flow": "^7.10.4",
"@babel/preset-react": "^7.10.4",
"@babel/preset-typescript": "^7.10.4",
"@babel/register": "^7.10.5",
"@peculiar/webcrypto": "^1.1.2",
"@types/classnames": "^2.2.10",
"@types/counterpart": "^0.18.1",
"@types/flux": "^3.1.9",
"@types/linkifyjs": "^2.1.3",
"@types/lodash": "^4.14.152",
"@types/lodash": "^4.14.158",
"@types/modernizr": "^3.5.3",
"@types/node": "^12.12.41",
"@types/node": "^12.12.51",
"@types/qrcode": "^1.3.4",
"@types/react": "^16.9",
"@types/react-dom": "^16.9.8",
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^1.23.3",
"@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^3.4.0",
"@typescript-eslint/parser": "^3.4.0",
"babel-eslint": "^10.0.3",
"@typescript-eslint/eslint-plugin": "^3.7.0",
"@typescript-eslint/parser": "^3.7.0",
"babel-eslint": "^10.1.0",
"babel-jest": "^24.9.0",
"chokidar": "^3.3.1",
"concurrently": "^4.0.1",
"enzyme": "^3.10.0",
"enzyme-adapter-react-16": "^1.15.1",
"eslint": "7.3.1",
"eslint-config-google": "^0.7.1",
"chokidar": "^3.4.1",
"concurrently": "^4.1.2",
"enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.2",
"eslint": "7.5.0",
"eslint-config-google": "^0.14.0",
"eslint-config-matrix-org": "^0.1.2",
"eslint-plugin-babel": "^5.2.1",
"eslint-plugin-flowtype": "^2.30.0",
"eslint-plugin-jest": "^23.0.4",
"eslint-plugin-react": "^7.7.0",
"eslint-plugin-react-hooks": "^2.0.1",
"estree-walker": "^0.5.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-flowtype": "^2.50.3",
"eslint-plugin-jest": "^23.18.0",
"eslint-plugin-react": "^7.20.3",
"eslint-plugin-react-hooks": "^2.5.1",
"estree-walker": "^0.9.0",
"file-loader": "^3.0.1",
"flow-parser": "^0.57.3",
"glob": "^5.0.14",
"flow-parser": "0.57.3",
"glob": "^5.0.15",
"jest": "^24.9.0",
"jest-canvas-mock": "^2.2.0",
"lolex": "^5.1.2",
"matrix-mock-request": "^1.2.3",
"matrix-react-test-utils": "^0.2.2",
"react-test-renderer": "^16.9.0",
"rimraf": "^2.4.3",
"source-map-loader": "^0.2.3",
"react-test-renderer": "^16.13.1",
"rimraf": "^2.7.1",
"source-map-loader": "^0.2.4",
"stylelint": "^9.10.1",
"stylelint-config-standard": "^18.2.0",
"stylelint-scss": "^3.9.0",
"typescript": "^3.7.3",
"walk": "^2.3.9",
"webpack": "^4.20.2",
"webpack-cli": "^3.1.1"
"stylelint-config-standard": "^18.3.0",
"stylelint-scss": "^3.18.0",
"typescript": "^3.9.7",
"walk": "^2.3.14",
"webpack": "^4.43.0",
"webpack-cli": "^3.3.12"
},
"jest": {
"testMatch": [

View file

@ -135,12 +135,7 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
}
.mx_LeftPanel_roomListWrapper {
// Create a flexbox to ensure the containing items cause appropriate overflow.
display: flex;
flex-grow: 1;
overflow: hidden;
min-height: 0;
margin-top: 10px; // so we're not up against the search/filter
&.mx_LeftPanel_roomListWrapper_stickyBottom {
@ -153,14 +148,8 @@ $tagPanelWidth: 56px; // only applies in this file, used for calculations
}
.mx_LeftPanel_actualRoomListContainer {
flex-grow: 1; // fill the available space
overflow-y: auto;
width: 100%;
max-width: 100%;
position: relative; // for sticky headers
// Create a flexbox to trick the layout engine
display: flex;
height: 100%; // ensure scrolling still works
}
}

View file

@ -17,4 +17,5 @@ limitations under the License.
.mx_MessageTimestamp {
color: $event-timestamp-color;
font-size: $font-10px;
font-variant-numeric: tabular-nums;
}

View file

@ -15,11 +15,5 @@ limitations under the License.
*/
.mx_RoomList {
width: calc(100% - 16px); // 16px of artificial right-side margin (8px is overflowed from the sublists)
// 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
padding-right: 7px; // width of the scrollbar, to line things up
}

View file

@ -15,15 +15,8 @@ limitations under the License.
*/
.mx_RoomSublist {
// The sublist is a column of rows, essentially
display: flex;
flex-direction: column;
margin-left: 8px;
margin-bottom: 4px;
width: 100%;
flex-shrink: 0; // to convince safari's layout engine the flexbox is fine
.mx_RoomSublist_headerContainer {
// 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');
}
.mx_RoomTile_iconFavorite::before {
mask-image: url('$(res)/img/feather-customised/favourites.svg');
}
.mx_RoomTile_iconArrowDown::before {
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 RoomListLayoutStore from "../stores/room-list/RoomListLayoutStore";
import {IntegrationManagers} from "../integrations/IntegrationManagers";
import {ModalManager} from "../Modal";
declare global {
interface Window {
@ -41,6 +42,7 @@ declare global {
mxRoomListLayoutStore: RoomListLayoutStore;
mxPlatformPeg: PlatformPeg;
mxIntegrationManagers: typeof IntegrationManagers;
singletonModalManager: ModalManager;
}
// workaround for https://github.com/microsoft/TypeScript/issues/30933

View file

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

View file

@ -184,7 +184,7 @@ const transformTags: sanitizeHtml.IOptions["transformTags"] = { // custom to mat
if (typeof attribs.class !== 'undefined') {
// Filter out all classes other than ones starting with language- for syntax highlighting.
const classes = attribs.class.split(/\s/).filter(function(cl) {
return cl.startsWith('language-');
return cl.startsWith('language-') && !cl.startsWith('language-_');
});
attribs.class = classes.join(' ');
}

View file

@ -1,5 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
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.
@ -17,6 +18,8 @@ limitations under the License.
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';
import Analytics from './Analytics';
import dis from './dispatcher/dispatcher';
import {defer} from './utils/promise';
@ -25,36 +28,48 @@ import AsyncWrapper from './AsyncWrapper';
const DIALOG_CONTAINER_ID = "mx_Dialog_Container";
const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer";
class ModalManager {
constructor() {
this._counter = 0;
interface IModal<T extends any[]> {
elem: React.ReactNode;
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
// this modal. Remove all other modals from the stack when this modal
// is closed.
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
} */
];
interface IHandle<T extends any[]> {
finished: Promise<T>;
close(...args: T): void;
}
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() {
return this._priorityModal || this._staticModal || this._modals.length > 0;
}
interface IOptions<T extends any[]> {
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);
if (!container) {
@ -66,7 +81,7 @@ class ModalManager {
return container;
}
getOrCreateStaticContainer() {
private static getOrCreateStaticContainer() {
let container = document.getElementById(STATIC_DIALOG_CONTAINER_ID);
if (!container) {
@ -78,63 +93,99 @@ class ModalManager {
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);
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);
return this.appendDialog(...rest);
return this.appendDialog<T>(...rest);
}
createDialog(Element, ...rest) {
return this.createDialogAsync(Promise.resolve(Element), ...rest);
public createDialog<T extends any[]>(
Element: React.ComponentType,
...rest: ParametersWithoutFirst<ModalManager["createDialogAsync"]>
) {
return this.createDialogAsync<T>(Promise.resolve(Element), ...rest);
}
appendDialog(Element, ...rest) {
return this.appendDialogAsync(Promise.resolve(Element), ...rest);
public appendDialog<T extends any[]>(
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);
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);
return this.appendDialogAsync(...rest);
return this.appendDialogAsync<T>(...rest);
}
_buildModal(prom, props, className, options) {
const modal = {};
private buildModal<T extends any[]>(
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
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,
// otherwise we'll get confused.
const modalCount = this._counter++;
const modalCount = this.counter++;
// FIXME: If a dialog uses getDefaultProps it clobbers the onFinished
// property set here so you can't close the dialog from a button click!
modal.elem = (
<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.elem = <AsyncWrapper key={modalCount} prom={prom} {...props} onFinished={closeDialog} />;
modal.close = closeDialog;
modal.closeReason = null;
return {modal, closeDialog, onFinishedProm};
}
_getCloseFn(modal, props) {
const deferred = defer();
return [async (...args) => {
private getCloseFn<T extends any[]>(
modal: IModal<T>,
props: IProps<T>
): [IHandle<T>["close"], IHandle<T>["finished"]] {
const deferred = defer<T>();
return [async (...args: T) => {
if (modal.beforeClosePromise) {
await modal.beforeClosePromise;
} else if (modal.onBeforeClose) {
@ -147,26 +198,26 @@ class ModalManager {
}
deferred.resolve(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) {
this._modals.splice(i, 1);
this.modals.splice(i, 1);
}
if (this._priorityModal === modal) {
this._priorityModal = null;
if (this.priorityModal === modal) {
this.priorityModal = null;
// XXX: This is destructive
this._modals = [];
this.modals = [];
}
if (this._staticModal === modal) {
this._staticModal = null;
if (this.staticModal === modal) {
this.staticModal = null;
// XXX: This is destructive
this._modals = [];
this.modals = [];
}
this._reRender();
this.reRender();
}, deferred.promise];
}
@ -207,38 +258,49 @@ class ModalManager {
* @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
*/
createDialogAsync(prom, props, className, isPriorityModal, isStaticModal, options = {}) {
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, options);
private createDialogAsync<T extends any[]>(
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) {
// XXX: This is destructive
this._priorityModal = modal;
this.priorityModal = modal;
} else if (isStaticModal) {
// This is intentionally destructive
this._staticModal = modal;
this.staticModal = modal;
} else {
this._modals.unshift(modal);
this.modals.unshift(modal);
}
this._reRender();
this.reRender();
return {
close: closeDialog,
finished: onFinishedProm,
};
}
appendDialogAsync(prom, props, className) {
const {modal, closeDialog, onFinishedProm} = this._buildModal(prom, props, className, {});
private appendDialogAsync<T extends any[]>(
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._reRender();
this.modals.push(modal);
this.reRender();
return {
close: closeDialog,
finished: onFinishedProm,
};
}
onBackgroundClick() {
const modal = this._getCurrentModal();
private onBackgroundClick = () => {
const modal = this.getCurrentModal();
if (!modal) {
return;
}
@ -249,21 +311,21 @@ class ModalManager {
modal.closeReason = "backgroundClick";
modal.close();
modal.closeReason = null;
};
private getCurrentModal(): IModal<any> {
return this.priorityModal ? this.priorityModal : (this.modals[0] || this.staticModal);
}
_getCurrentModal() {
return this._priorityModal ? this._priorityModal : (this._modals[0] || this._staticModal);
}
_reRender() {
if (this._modals.length === 0 && !this._priorityModal && !this._staticModal) {
private reRender() {
if (this.modals.length === 0 && !this.priorityModal && !this.staticModal) {
// If there is no modal to render, make all of Riot available
// to screen reader users again
dis.dispatch({
action: 'aria_unhide_main_app',
});
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer());
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
return;
}
@ -274,49 +336,48 @@ class ModalManager {
action: 'aria_hide_main_app',
});
if (this._staticModal) {
const classes = "mx_Dialog_wrapper mx_Dialog_staticWrapper "
+ (this._staticModal.className ? this._staticModal.className : '');
if (this.staticModal) {
const classes = classNames("mx_Dialog_wrapper mx_Dialog_staticWrapper", this.staticModal.className);
const staticDialog = (
<div className={classes}>
<div className="mx_Dialog">
{ this._staticModal.elem }
{ this.staticModal.elem }
</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>
);
ReactDOM.render(staticDialog, this.getOrCreateStaticContainer());
ReactDOM.render(staticDialog, ModalManager.getOrCreateStaticContainer());
} else {
// This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(this.getOrCreateStaticContainer());
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateStaticContainer());
}
const modal = this._getCurrentModal();
if (modal !== this._staticModal) {
const classes = "mx_Dialog_wrapper "
+ (this._staticModal ? "mx_Dialog_wrapperWithStaticUnder " : '')
+ (modal.className ? modal.className : '');
const modal = this.getCurrentModal();
if (modal !== this.staticModal) {
const classes = classNames("mx_Dialog_wrapper", modal.className, {
mx_Dialog_wrapperWithStaticUnder: this.staticModal,
});
const dialog = (
<div className={classes}>
<div className="mx_Dialog">
{modal.elem}
</div>
<div className="mx_Dialog_background" onClick={this.onBackgroundClick}></div>
<div className="mx_Dialog_background" onClick={this.onBackgroundClick} />
</div>
);
ReactDOM.render(dialog, this.getOrCreateContainer());
ReactDOM.render(dialog, ModalManager.getOrCreateContainer());
} else {
// This is safe to call repeatedly if we happen to do that
ReactDOM.unmountComponentAtNode(this.getOrCreateContainer());
ReactDOM.unmountComponentAtNode(ModalManager.getOrCreateContainer());
}
}
}
if (!global.singletonModalManager) {
global.singletonModalManager = new ModalManager();
if (!window.singletonModalManager) {
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 = () => {
this._reshowTimer = null;
this.nagAgainAt = null;
@ -143,10 +148,14 @@ export default class RebrandListener {
if (nagToast || oneTimeToast) {
let description;
let rejectLabel = null;
let onReject = null;
if (nagToast) {
description = _t("Use your account to sign in to the latest version");
} else {
description = _t("Were excited to announce Riot is now Element");
rejectLabel = _t("Dismiss");
onReject = this.onOneTimeToastDismiss;
}
ToastStore.sharedInstance().addOrReplaceToast({
@ -157,6 +166,8 @@ export default class RebrandListener {
description,
acceptLabel: _t("Learn More"),
onAccept: nagToast ? this.onNagToastLearnMore : this.onOneTimeToastLearnMore,
rejectLabel,
onReject,
},
component: GenericToast,
priority: 20,

View file

@ -34,27 +34,6 @@ export function shouldShowMentionBadge(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) {
return rooms.reduce((result, room) => {
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
// get a bit more complex here, but we try to show something
// meaningful.
let finished = Promise.resolve();
let prom = Promise.resolve();
if (
getAddressType(address) === 'email' &&
!MatrixClientPeg.get().getIdentityServerUrl()
) {
const defaultIdentityServerUrl = getDefaultIdentityServerUrl();
if (defaultIdentityServerUrl) {
({ finished } = Modal.createTrackedDialog('Slash Commands', 'Identity server',
const { finished } = Modal.createTrackedDialog<[boolean]>(
'Slash Commands',
'Identity server',
QuestionDialog, {
title: _t("Use an identity server"),
description: <p>{_t(
@ -421,9 +423,9 @@ export const Commands = [
)}</p>,
button: _t("Continue"),
},
));
);
finished = finished.then(([useDefault]: any) => {
prom = finished.then(([useDefault]) => {
if (useDefault) {
useDefaultIdentityServer();
return;
@ -435,7 +437,7 @@ export const Commands = [
}
}
const inviter = new MultiInviter(roomId);
return success(finished.then(() => {
return success(prom.then(() => {
return inviter.invite([address]);
}).then(() => {
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 IndicatorScrollbar from "../structures/IndicatorScrollbar";
import AccessibleTooltipButton from "../views/elements/AccessibleTooltipButton";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { MatrixClientPeg } from "../../MatrixClientPeg";
interface IProps {
isMinimized: boolean;
@ -59,6 +61,7 @@ const cssClasses = [
export default class LeftPanel extends React.Component<IProps, IState> {
private listContainerRef: React.RefObject<HTMLDivElement> = createRef();
private tagPanelWatcherRef: string;
private bgImageWatcherRef: string;
private focusedElement = null;
private isDoingStickyHeaders = false;
@ -73,6 +76,9 @@ export default class LeftPanel extends React.Component<IProps, IState> {
BreadcrumbsStore.instance.on(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.setState({showTagPanel: SettingsStore.getValue("TagPanel.enableTagPanel")});
});
@ -84,8 +90,10 @@ export default class LeftPanel extends React.Component<IProps, IState> {
public componentWillUnmount() {
SettingsStore.unwatchSetting(this.tagPanelWatcherRef);
SettingsStore.unwatchSetting(this.bgImageWatcherRef);
BreadcrumbsStore.instance.off(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);
}
@ -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) {
if (this.isDoingStickyHeaders) return;
this.isDoingStickyHeaders = true;

View file

@ -58,7 +58,6 @@ import { messageForSyncError } from '../../utils/ErrorUtils';
import ResizeNotifier from "../../utils/ResizeNotifier";
import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils";
import DMRoomMap from '../../utils/DMRoomMap';
import { countRoomsWithNotif } from '../../RoomNotifs';
import ThemeWatcher from "../../settings/watchers/ThemeWatcher";
import { FontWatcher } from '../../settings/watchers/FontWatcher';
import { storeRoomAliasInCache } from '../../RoomAliasCache';
@ -75,6 +74,7 @@ import {
import {showToast as showNotificationsToast} from "../../toasts/DesktopNotificationsToast";
import { OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import ErrorDialog from "../views/dialogs/ErrorDialog";
import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotificationStateStore";
/** constants for MatrixChat.state.view */
export enum Views {
@ -1844,21 +1844,20 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
updateStatusIndicator(state: string, prevState: string) {
// only count visible rooms to not torment the user with notification counts in rooms they can't see
// it will include highlights from the previous version of the room internally
const notifCount = countRoomsWithNotif(MatrixClientPeg.get().getVisibleRooms()).count;
const notificationState = RoomNotificationStateStore.instance.globalState;
const numUnreadRooms = notificationState.numUnreadStates; // we know that states === rooms here
if (PlatformPeg.get()) {
PlatformPeg.get().setErrorStatus(state === 'ERROR');
PlatformPeg.get().setNotificationCount(notifCount);
PlatformPeg.get().setNotificationCount(numUnreadRooms);
}
this.subTitleStatus = '';
if (state === "ERROR") {
this.subTitleStatus += `[${_t("Offline")}] `;
}
if (notifCount > 0) {
this.subTitleStatus += `[${notifCount}]`;
if (numUnreadRooms > 0) {
this.subTitleStatus += `[${numUnreadRooms}]`;
}
this.setPageSubtitle();

View file

@ -648,7 +648,9 @@ export default createReactClass({
if (scrollState.stuckAtBottom) {
const sn = this._getScrollNode();
sn.scrollTop = sn.scrollHeight;
if (sn.scrollTop !== sn.scrollHeight) {
sn.scrollTop = sn.scrollHeight;
}
} else if (scrollState.trackedScrollToken) {
const itemlist = this._itemlist.current;
const trackedNode = this._getTrackedNode();
@ -657,7 +659,10 @@ export default createReactClass({
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
this._bottomGrowth += bottomDiff;
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);
}
}
@ -694,12 +699,16 @@ export default createReactClass({
const height = Math.max(minHeight, contentHeight);
this._pages = Math.ceil(height / PAGE_SIZE);
this._bottomGrowth = 0;
const newHeight = this._getListHeight();
const newHeight = `${this._getListHeight()}px`;
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
itemlist.style.height = `${newHeight}px`;
sn.scrollTop = sn.scrollHeight;
if (itemlist.style.height !== newHeight) {
itemlist.style.height = newHeight;
}
if (sn.scrollTop !== sn.scrollHeight){
sn.scrollTop = sn.scrollHeight;
}
debuglog("updateHeight to", newHeight);
} else if (scrollState.trackedScrollToken) {
const trackedNode = this._getTrackedNode();
@ -709,7 +718,9 @@ export default createReactClass({
// the currently filled piece of the timeline
if (trackedNode) {
const oldTop = trackedNode.offsetTop;
itemlist.style.height = `${newHeight}px`;
if (itemlist.style.height !== newHeight) {
itemlist.style.height = newHeight;
}
const newTop = trackedNode.offsetTop;
const topDiff = newTop - oldTop;
// 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() {
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 buttons = (

View file

@ -134,7 +134,7 @@ const BaseAvatar = (props: IProps) => {
aria-hidden="true" />
);
if (onClick !== null) {
if (onClick) {
return (
<AccessibleButton
{...otherProps}
@ -162,7 +162,7 @@ const BaseAvatar = (props: IProps) => {
}
}
if (onClick !== null) {
if (onClick) {
return (
<AccessibleButton
className={classNames("mx_BaseAvatar mx_BaseAvatar_image", className)}
@ -196,4 +196,4 @@ const BaseAvatar = (props: IProps) => {
};
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);
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 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_INVITE = "invite";

View file

@ -107,7 +107,7 @@ export default createReactClass({
} else {
// Only syntax highlight if there's a class starting with language-
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) {

View file

@ -219,7 +219,9 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
};
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");
}

View file

@ -210,7 +210,7 @@ export default class RoomList extends React.Component<IProps, IState> {
if (unread) {
// filter to only notification rooms (and our current active room so we can index properly)
listRooms = listRooms.filter(r => {
const state = RoomNotificationStateStore.instance.getRoomState(r, t);
const state = RoomNotificationStateStore.instance.getRoomState(r);
return state.room.roomId === roomId || state.isUnread;
});
}
@ -308,7 +308,7 @@ export default class RoomList extends React.Component<IProps, IState> {
startAsHidden={aesthetics.defaultHidden}
label={aesthetics.sectionLabelRaw ? aesthetics.sectionLabelRaw : _t(aesthetics.sectionLabel)}
onAddRoom={onAddRoomFn}
addRoomLabel={aesthetics.addRoomLabel}
addRoomLabel={aesthetics.addRoomLabel ? _t(aesthetics.addRoomLabel) : aesthetics.addRoomLabel}
isMinimized={this.props.isMinimized}
onResize={this.props.onResize}
extraBadTilesThatShouldntExist={extraTiles}

View file

@ -120,7 +120,7 @@ export default class RoomTile extends React.Component<IProps, IState> {
this.state = {
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,
notificationsMenuPosition: null,
generalMenuPosition: null,
@ -231,11 +231,11 @@ export default class RoomTile extends React.Component<IProps, IState> {
ev.preventDefault();
ev.stopPropagation();
if (tagId === DefaultTagID.Favourite) {
const roomTags = RoomListStore.instance.getTagsForRoom(this.props.room);
const isFavourite = roomTags.includes(DefaultTagID.Favourite);
const removeTag = isFavourite ? DefaultTagID.Favourite : DefaultTagID.LowPriority;
const addTag = isFavourite ? null : DefaultTagID.Favourite;
if (tagId === DefaultTagID.Favourite || tagId === DefaultTagID.LowPriority) {
const inverseTag = tagId === DefaultTagID.Favourite ? DefaultTagID.LowPriority : DefaultTagID.Favourite;
const isApplied = RoomListStore.instance.getTagsForRoom(this.props.room).includes(tagId);
const removeTag = isApplied ? tagId : inverseTag;
const addTag = isApplied ? null : tagId;
dis.dispatch(RoomListActions.tagRoom(
MatrixClientPeg.get(),
this.props.room,
@ -387,13 +387,6 @@ export default class RoomTile extends React.Component<IProps, IState> {
private renderGeneralMenu(): React.ReactElement {
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;
if (this.state.generalMenuPosition && this.props.tag === DefaultTagID.Archived) {
contextMenu = (
@ -409,19 +402,36 @@ export default class RoomTile extends React.Component<IProps, IState> {
</ContextMenu>
);
} 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 {...contextMenuBelow(this.state.generalMenuPosition)} onFinished={this.onCloseGeneralMenu}>
<div className="mx_IconizedContextMenu mx_IconizedContextMenu_compact mx_RoomTile_contextMenu">
<div className="mx_IconizedContextMenu_optionList">
<MenuItemCheckbox
className={favouriteLabelClassName}
className={isFavorite ? "mx_RoomTile_contextMenu_activeRow" : ""}
onClick={(e) => this.onTagRoom(e, DefaultTagID.Favourite)}
active={isFavorite}
label={favouriteLabel}
>
<span className={classNames("mx_IconizedContextMenu_icon", favouriteIconClassName)} />
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconStar" />
<span className="mx_IconizedContextMenu_label">{favouriteLabel}</span>
</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")}>
<span className="mx_IconizedContextMenu_icon mx_RoomTile_iconSettings" />
<span className="mx_IconizedContextMenu_label">{_t("Settings")}</span>

View file

@ -22,10 +22,6 @@ import * as sdk from "../../../../..";
import AccessibleButton from "../../../elements/AccessibleButton";
import Modal from "../../../../../Modal";
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 {
static propTypes = {
@ -36,13 +32,9 @@ export default class AdvancedRoomSettingsTab extends React.Component {
constructor(props) {
super(props);
const room = MatrixClientPeg.get().getRoom(props.roomId);
const roomTags = RoomListStore.instance.getTagsForRoom(room);
this.state = {
// This is eventually set to the value of room.getRecommendedVersion()
upgradeRecommendation: null,
isLowPriorityRoom: roomTags.includes(DefaultTagID.LowPriority),
};
}
@ -94,25 +86,6 @@ export default class AdvancedRoomSettingsTab extends React.Component {
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() {
const client = MatrixClientPeg.get();
const room = client.getRoom(this.props.roomId);
@ -183,17 +156,6 @@ export default class AdvancedRoomSettingsTab extends React.Component {
{_t("Open Devtools")}
</AccessibleButton>
</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>
);
}

View file

@ -64,7 +64,7 @@ function parseCodeBlock(n: HTMLElement, partCreator: PartCreator) {
let language = "";
if (n.firstChild && n.firstChild.nodeName === "CODE") {
for (const className of (<HTMLElement>n.firstChild).classList) {
if (className.startsWith("language-")) {
if (className.startsWith("language-") && !className.startsWith("language-_")) {
language = className.substr("language-".length);
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
if (inputType !== "insertFromPaste" && inputType !== "insertFromDrop") {
if (chr !== "@" && chr !== "#" && chr !== ":") {
if (chr !== "@" && chr !== "#" && chr !== ":" && chr !== "+") {
return true;
}
// only split if the previous character is a space
@ -467,6 +467,7 @@ export class PartCreator {
case "#":
case "@":
case ":":
case "+":
return this.pillCandidate("");
case "\n":
return new NewlinePart();

View file

@ -162,10 +162,10 @@ export function renderModel(editor: HTMLDivElement, model: EditorModel) {
lines.forEach((parts, i) => {
// find first (and remove anything else) div without className
// (as browsers insert these in contenteditable) line container
let lineContainer = editor.children[i];
while (lineContainer && (lineContainer.tagName !== "DIV" || !!lineContainer.className)) {
let lineContainer = editor.childNodes[i];
while (lineContainer && ((<Element>lineContainer).tagName !== "DIV" || !!(<Element>lineContainer).className)) {
editor.removeChild(lineContainer);
lineContainer = editor.children[i];
lineContainer = editor.childNodes[i];
}
if (!lineContainer) {
lineContainer = document.createElement("div");

View file

@ -437,50 +437,10 @@
"%(senderName)s started a call": "%(senderName)s started a call",
"Waiting for answer": "Waiting for answer",
"%(senderName)s is calling": "%(senderName)s is calling",
"You created the room": "You created the room",
"%(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 %(emote)s": "* %(senderName)s %(emote)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: %(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",
"Message Pinning": "Message Pinning",
"Custom user status messages": "Custom user status messages",
@ -967,8 +927,6 @@
"Room version:": "Room version:",
"Developer options": "Developer options",
"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 isnt bridging messages to any platforms. <a>Learn more.</a>": "This room isnt bridging messages to any platforms. <a>Learn more.</a>",
"Bridges": "Bridges",
@ -1216,10 +1174,11 @@
"All messages": "All messages",
"Mentions & Keywords": "Mentions & Keywords",
"Notification options": "Notification options",
"Favourited": "Favourited",
"Favourite": "Favourite",
"Leave Room": "Leave Room",
"Forget Room": "Forget Room",
"Favourited": "Favourited",
"Favourite": "Favourite",
"Low Priority": "Low Priority",
"Room options": "Room options",
"%(count)s unread messages including mentions.|other": "%(count)s unread messages including mentions.",
"%(count)s unread messages including mentions.|one": "1 unread mention.",
@ -1912,7 +1871,6 @@
"Mentions only": "Mentions only",
"Leave": "Leave",
"Forget": "Forget",
"Low Priority": "Low Priority",
"Direct Chat": "Direct Chat",
"Clear status": "Clear status",
"Update status": "Update status",

View file

@ -166,6 +166,10 @@ export const SETTINGS = {
displayName: _td("Show info about bridges in room settings"),
default: false,
},
"RoomList.backgroundImage": {
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
default: null,
},
"baseFontSize": {
displayName: _td("Font size"),
supportedLevels: LEVELS_ACCOUNT_SETTINGS,

View file

@ -42,7 +42,7 @@ export const UPDATE_EVENT = "update";
* help prevent lock conflicts.
*/
export abstract class AsyncStore<T extends Object> extends EventEmitter {
private storeState: T;
private storeState: Readonly<T>;
private lock = new AwaitLock();
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.
*/
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) {
await this.lock.acquireAsync();
try {
this.storeState = Object.assign(<T>{}, this.storeState, newState);
this.storeState = Object.freeze(Object.assign(<T>{}, this.storeState, newState));
this.emit(UPDATE_EVENT, this);
} finally {
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) {
await this.lock.acquireAsync();
try {
this.storeState = <T>(newState || {});
this.storeState = Object.freeze(<T>(newState || {}));
if (!quiet) this.emit(UPDATE_EVENT, this);
} finally {
await this.lock.release();

View file

@ -21,21 +21,36 @@ import { DefaultTagID, TagID } from "../room-list/models";
import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomNotificationState } from "./RoomNotificationState";
const INSPECIFIC_TAG = "INSPECIFIC_TAG";
type INSPECIFIC_TAG = "INSPECIFIC_TAG";
import { SummarizedNotificationState } from "./SummarizedNotificationState";
interface IState {}
export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
private static internalInstance = new RoomNotificationStateStore();
private roomMap = new Map<Room, Map<TagID | INSPECIFIC_TAG, RoomNotificationState>>();
private roomMap = new Map<Room, RoomNotificationState>();
private constructor() {
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
* 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.
const useTileCount = tagId === DefaultTagID.Invite;
const getRoomFn: FetchRoomFn = (room: Room) => {
return this.getRoomState(room, tagId);
return this.getRoomState(room);
};
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
* consumers.
* @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.
*/
public getRoomState(room: Room, inTagId?: TagID): RoomNotificationState {
public getRoomState(room: Room): RoomNotificationState {
if (!this.roomMap.has(room)) {
this.roomMap.set(room, new Map<TagID | INSPECIFIC_TAG, RoomNotificationState>());
this.roomMap.set(room, new RoomNotificationState(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);
return this.roomMap.get(room);
}
public static get instance(): RoomNotificationStateStore {
@ -82,10 +88,8 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
}
protected async onNotReady(): Promise<any> {
for (const roomMap of this.roomMap.values()) {
for (const roomState of roomMap.values()) {
roomState.destroy();
}
for (const roomState of this.roomMap.values()) {
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 defaultDispatcher from "../../dispatcher/dispatcher";
import { MessageEventPreview } from "./previews/MessageEventPreview";
import { NameEventPreview } from "./previews/NameEventPreview";
import { TagID } from "./models";
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 { CallAnswerEventPreview } from "./previews/CallAnswerEventPreview";
import { CallHangupEvent } from "./previews/CallHangupEvent";
import { EncryptionEventPreview } from "./previews/EncryptionEventPreview";
import { ThirdPartyInviteEventPreview } from "./previews/ThirdPartyInviteEventPreview";
import { StickerEventPreview } from "./previews/StickerEventPreview";
import { ReactionEventPreview } from "./previews/ReactionEventPreview";
import { CreationEventPreview } from "./previews/CreationEventPreview";
import { UPDATE_EVENT } from "../AsyncStore";
const PREVIEWS = {
'm.room.message': {
isState: false,
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': {
isState: false,
previewer: new CallInviteEventPreview(),
@ -67,14 +45,6 @@ const PREVIEWS = {
isState: false,
previewer: new CallHangupEvent(),
},
'm.room.encryption': {
isState: true,
previewer: new EncryptionEventPreview(),
},
'm.room.third_party_invite': {
isState: true,
previewer: new ThirdPartyInviteEventPreview(),
},
'm.sticker': {
isState: false,
previewer: new StickerEventPreview(),
@ -83,10 +53,6 @@ const PREVIEWS = {
isState: false,
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.
@ -97,12 +63,15 @@ type TAG_ANY = "im.vector.any";
const TAG_ANY: TAG_ANY = "im.vector.any";
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> {
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() {
super(defaultDispatcher, {});
}
@ -120,10 +89,9 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
public getPreviewForRoom(room: Room, inTagId: TagID): string {
if (!room) return null; // invalid room, just return nothing
const val = this.state[room.roomId];
if (!val) this.generatePreview(room, inTagId);
if (!this.previews.has(room.roomId)) this.generatePreview(room, inTagId);
const previews = this.state[room.roomId];
const previews = this.previews.get(room.roomId);
if (!previews) return null;
if (!previews.has(inTagId)) {
@ -136,11 +104,10 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
const events = room.timeline;
if (!events) return; // should only happen in tests
let map = this.state[room.roomId];
let map = this.previews.get(room.roomId);
if (!map) {
map = new Map<TagID | TAG_ANY, string | null>();
// We set the state later with the map, so no need to send an update now
this.previews.set(room.roomId, map);
}
// Set the tags so we know what to generate
@ -176,16 +143,16 @@ export class MessagePreviewStore extends AsyncStoreWithClient<IState> {
}
if (changed) {
// Update state for good measure - causes emit for update
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: map});
// We've muted the underlying Map, so just emit that we've changed.
this.previews.set(room.roomId, map);
this.emit(UPDATE_EVENT, this);
}
return; // we're done
}
// At this point, we didn't generate a preview so clear it
// noinspection JSIgnoredPromiseFromCall - the AsyncStore handles concurrent calls
this.updateState({[room.roomId]: null});
this.previews.set(room.roomId, new Map<TagID|TAG_ANY, string|null>());
this.emit(UPDATE_EVENT, this);
}
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') {
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);
}
}

View file

@ -168,6 +168,12 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
}
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.
// As such, we use a more synchronous model.
if (RoomListStoreClass.TEST_MODE) {

View file

@ -90,7 +90,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
private getRoomCategory(room: Room): NotificationColor {
// 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
const state = RoomNotificationStateStore.instance.getRoomState(room, this.tagId);
const state = RoomNotificationStateStore.instance.getRoomState(room);
return state.color;
}
@ -123,6 +123,7 @@ export class ImportanceAlgorithm extends OrderingAlgorithm {
const category = this.getRoomCategory(room);
this.alterCategoryPositionBy(category, 1, this.indices);
this.cachedOrderedRooms.splice(this.indices[category], 0, room); // splice in the new room (pre-adjusted)
await this.sortCategory(category);
} else if (cause === RoomUpdateCause.RoomRemoved) {
const roomIdx = this.getRoomIndex(room);
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') {
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)) {

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">' +
'Hey <span>' +
'<a class="mx_Pill mx_UserPill" title="@user:server">' +
'<img src="mxc://avatar.url/image.png" style="width: 16px; height: 16px;" ' +
'title="@member:domain.bla" alt="" aria-hidden="true" role="button" tabindex="0" ' +
'class="mx_AccessibleButton mx_BaseAvatar mx_BaseAvatar_image">Member</a>' +
'<img class="mx_BaseAvatar mx_BaseAvatar_image" src="mxc://avatar.url/image.png" ' +
'style="width: 16px; height: 16px;" title="@member:domain.bla" alt="" aria-hidden="true">Member</a>' +
'</span></span>');
});
});

1788
yarn.lock

File diff suppressed because it is too large Load diff