Merge branch 'develop' of github.com:matrix-org/matrix-react-sdk into spaces-jump-to-room

This commit is contained in:
Jaiwanth 2021-06-30 11:20:01 +05:30
commit a99b24ef83
666 changed files with 5083 additions and 5561 deletions

View file

@ -1,7 +1,9 @@
module.exports = {
extends: ["matrix-org", "matrix-org/react-legacy"],
parser: "babel-eslint",
plugins: ["matrix-org"],
extends: [
"plugin:matrix-org/babel",
"plugin:matrix-org/react",
],
env: {
browser: true,
node: true,
@ -15,12 +17,32 @@ module.exports = {
"prefer-promise-reject-errors": "off",
"no-async-promise-executor": "off",
"quotes": "off",
},
"no-extra-boolean-cast": "off",
// Bind or arrow functions in props causes performance issues (but we
// currently use them in some places).
// It's disabled here, but we should using it sparingly.
"react/jsx-no-bind": "off",
"react/jsx-key": ["error"],
},
overrides: [{
"files": ["src/**/*.{ts,tsx}", "test/**/*.{ts,tsx}"],
"extends": ["matrix-org/ts"],
"rules": {
files: [
"src/**/*.{ts,tsx}",
"test/**/*.{ts,tsx}",
],
extends: [
"plugin:matrix-org/typescript",
"plugin:matrix-org/react",
],
rules: {
// Things we do that break the ideal style
"prefer-promise-reject-errors": "off",
"quotes": "off",
"no-extra-boolean-cast": "off",
// Remove Babel things manually due to override limitations
"@babel/no-invalid-this": ["off"],
// We're okay being explicit at the moment
"@typescript-eslint/no-empty-interface": "off",
// We disable this while we're transitioning
@ -28,8 +50,6 @@ module.exports = {
// We'd rather not do this but we do
"@typescript-eslint/ban-ts-comment": "off",
"quotes": "off",
"no-extra-boolean-cast": "off",
"no-restricted-properties": [
"error",
...buildRestrictedPropertiesOptions(

View file

@ -1,6 +0,0 @@
[include]
src/**/*.js
test/**/*.js
[ignore]
node_modules/

View file

@ -10,7 +10,6 @@ module.exports = {
],
}],
"@babel/preset-typescript",
"@babel/preset-flow",
"@babel/preset-react",
],
"plugins": [
@ -19,7 +18,6 @@ module.exports = {
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-object-rest-spread",
"@babel/plugin-transform-flow-comments",
"@babel/plugin-syntax-dynamic-import",
"@babel/plugin-transform-runtime",
],

View file

@ -104,16 +104,16 @@
"devDependencies": {
"@babel/cli": "^7.12.10",
"@babel/core": "^7.12.10",
"@babel/eslint-parser": "^7.12.10",
"@babel/eslint-plugin": "^7.12.10",
"@babel/parser": "^7.12.11",
"@babel/plugin-proposal-class-properties": "^7.12.1",
"@babel/plugin-proposal-decorators": "^7.12.12",
"@babel/plugin-proposal-export-default-from": "^7.12.1",
"@babel/plugin-proposal-numeric-separator": "^7.12.7",
"@babel/plugin-proposal-object-rest-spread": "^7.12.1",
"@babel/plugin-transform-flow-comments": "^7.12.1",
"@babel/plugin-transform-runtime": "^7.12.10",
"@babel/preset-env": "^7.12.11",
"@babel/preset-flow": "^7.12.1",
"@babel/preset-react": "^7.12.10",
"@babel/preset-typescript": "^7.12.7",
"@babel/register": "^7.12.10",
@ -139,18 +139,16 @@
"@types/react-transition-group": "^4.4.0",
"@types/sanitize-html": "^2.3.1",
"@types/zxcvbn": "^4.4.0",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"@wojtekmaj/enzyme-adapter-react-17": "^0.6.1",
"babel-eslint": "^10.1.0",
"babel-jest": "^26.6.3",
"chokidar": "^3.5.1",
"concurrently": "^5.3.0",
"enzyme": "^3.11.0",
"eslint": "7.18.0",
"eslint-config-matrix-org": "^0.2.0",
"eslint-plugin-babel": "^5.3.1",
"eslint-plugin-flowtype": "^5.2.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-matrix-org": "github:matrix-org/eslint-plugin-matrix-org#main",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"glob": "^7.1.6",

View file

@ -134,12 +134,15 @@ limitations under the License.
.mx_Toast_buttons {
float: right;
display: flex;
gap: 5px;
.mx_AccessibleButton {
min-width: 96px;
box-sizing: border-box;
}
.mx_AccessibleButton + .mx_AccessibleButton {
margin-left: 5px;
}
}
.mx_Toast_description {

View file

@ -16,6 +16,7 @@ limitations under the License.
import "matrix-js-sdk/src/@types/global"; // load matrix-js-sdk's type extensions first
import * as ModernizrStatic from "modernizr";
import ContentMessages from "../ContentMessages";
import { IMatrixClientPeg } from "../MatrixClientPeg";
import ToastStore from "../stores/ToastStore";
@ -127,11 +128,24 @@ declare global {
setSinkId(outputId: string);
}
// Add Chrome-specific `instant` ScrollBehaviour
type _ScrollBehavior = ScrollBehavior | "instant";
interface _ScrollOptions {
behavior?: _ScrollBehavior;
}
interface _ScrollIntoViewOptions extends _ScrollOptions {
block?: ScrollLogicalPosition;
inline?: ScrollLogicalPosition;
}
interface Element {
// Safari & IE11 only have this prefixed: we used prefixed versions
// previously so let's continue to support them for now
webkitRequestFullScreen(options?: FullscreenOptions): Promise<void>;
msRequestFullscreen(options?: FullscreenOptions): Promise<void>;
scrollIntoView(arg?: boolean | _ScrollIntoViewOptions): void;
}
interface Error {

View file

@ -189,7 +189,6 @@ export default class AddThreepid {
// pop up an interactive auth dialog
const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog");
const dialogAesthetics = {
[SSOAuthEntry.PHASE_PREAUTH]: {
title: _t("Use Single Sign On to continue"),

View file

@ -348,7 +348,7 @@ export default abstract class BasePlatform {
/**
* Create and store a pickle key for encrypting libolm objects.
* @param {string} userId the user ID for the user that the pickle key is for.
* @param {string} userId the device ID that the pickle key is for.
* @param {string} deviceId the device ID that the pickle key is for.
* @returns {string|null} the pickle key, or null if the platform does not
* support storing pickle keys.
*/

View file

@ -80,7 +80,7 @@ import CountlyAnalytics from "./CountlyAnalytics";
import { UIFeature } from "./settings/UIFeature";
import { CallError } from "matrix-js-sdk/src/webrtc/call";
import { logger } from 'matrix-js-sdk/src/logger';
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker"
import DesktopCapturerSourcePicker from "./components/views/elements/DesktopCapturerSourcePicker";
import { Action } from './dispatcher/actions';
import VoipUserMapper from './VoipUserMapper';
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from './widgets/ManagedHybrid';
@ -166,7 +166,7 @@ export default class CallHandler extends EventEmitter {
static sharedInstance() {
if (!window.mxCallHandler) {
window.mxCallHandler = new CallHandler()
window.mxCallHandler = new CallHandler();
}
return window.mxCallHandler;
@ -185,7 +185,7 @@ export default class CallHandler extends EventEmitter {
const nativeUser = this.assertedIdentityNativeUsers[call.callId];
if (nativeUser) {
const room = findDMForUser(MatrixClientPeg.get(), nativeUser);
if (room) return room.roomId
if (room) return room.roomId;
}
}
@ -299,7 +299,7 @@ export default class CallHandler extends EventEmitter {
action: 'incoming_call',
call: call,
}, true);
}
};
getCallForRoom(roomId: string): MatrixCall {
return this.calls.get(roomId) || null;
@ -816,7 +816,7 @@ export default class CallHandler extends EventEmitter {
Analytics.trackEvent('voip', 'receiveCall', 'type', call.type);
console.log("Adding call for room ", mappedRoomId);
this.calls.set(mappedRoomId, call)
this.calls.set(mappedRoomId, call);
this.emit(CallHandlerEvent.CallsChanged, this.calls);
this.setCallListeners(call);
@ -872,7 +872,7 @@ export default class CallHandler extends EventEmitter {
this.dialNumber(payload.number);
break;
}
}
};
private async dialNumber(number: string) {
const results = await this.pstnLookup(number);

View file

@ -338,8 +338,8 @@ const getRoomStats = (roomId: string) => {
"is_encrypted": cli?.isRoomEncrypted(roomId),
// eslint-disable-next-line camelcase
"is_public": room?.currentState.getStateEvents("m.room.join_rules", "")?.getContent()?.join_rule === "public",
}
}
};
};
// async wrapper for regex-powered String.prototype.replace
const strReplaceAsync = async (str: string, regex: RegExp, fn: (...args: string[]) => Promise<string>) => {
@ -414,7 +414,7 @@ export default class CountlyAnalytics {
this.anonymous = anonymous;
if (anonymous) {
await this.changeUserKey(randomString(64))
await this.changeUserKey(randomString(64));
} else {
await this.changeUserKey(await hashHex(MatrixClientPeg.get().getUserId()), true);
}
@ -438,7 +438,7 @@ export default class CountlyAnalytics {
await this.track("Opt-Out" );
this.endSession();
window.clearInterval(this.heartbeatIntervalId);
window.clearTimeout(this.activityIntervalId)
window.clearTimeout(this.activityIntervalId);
this.baseUrl = null;
// remove listeners bound in trackSessions()
window.removeEventListener("beforeunload", this.endSession);
@ -669,7 +669,7 @@ export default class CountlyAnalytics {
count,
platform: this.appPlatform,
app_version: this.appVersion,
}
};
this.pendingEvents.push(ev);
if (this.pendingEvents.length > MAX_PENDING_EVENTS) {
@ -680,7 +680,7 @@ export default class CountlyAnalytics {
private getOrientation = (): Orientation => {
return window.matchMedia("(orientation: landscape)").matches
? Orientation.Landscape
: Orientation.Portrait
: Orientation.Portrait;
};
private reportOrientation = () => {
@ -749,7 +749,7 @@ export default class CountlyAnalytics {
const request: Parameters<typeof CountlyAnalytics.prototype.request>[0] = {
begin_session: 1,
user_details: JSON.stringify(userDetails),
}
};
const metrics = this.getMetrics();
if (metrics) {
@ -773,7 +773,7 @@ export default class CountlyAnalytics {
private endSession = () => {
if (this.sessionStarted) {
window.removeEventListener("resize", this.reportOrientation)
window.removeEventListener("resize", this.reportOrientation);
this.reportViewDuration();
this.request({

View file

@ -138,7 +138,7 @@ export function getHtmlText(insaneHtml: string): string {
selfClosing: [],
allowedSchemes: [],
disallowedTagsMode: 'discard',
})
});
}
/**

View file

@ -156,7 +156,7 @@ const messageComposerBindings = (): KeyBinding<MessageComposerAction>[] => {
}
}
return bindings;
}
};
const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
return [
@ -207,7 +207,7 @@ const autocompleteBindings = (): KeyBinding<AutocompleteAction>[] => {
},
},
];
}
};
const roomListBindings = (): KeyBinding<RoomListAction>[] => {
return [
@ -248,7 +248,7 @@ const roomListBindings = (): KeyBinding<RoomListAction>[] => {
},
},
];
}
};
const roomBindings = (): KeyBinding<RoomAction>[] => {
const bindings: KeyBinding<RoomAction>[] = [
@ -312,7 +312,7 @@ const roomBindings = (): KeyBinding<RoomAction>[] => {
}
return bindings;
}
};
const navigationBindings = (): KeyBinding<NavigationAction>[] => {
return [
@ -396,7 +396,7 @@ const navigationBindings = (): KeyBinding<NavigationAction>[] => {
},
},
];
}
};
export const defaultBindingsProvider: IKeyBindingsProvider = {
getMessageComposerBindings: messageComposerBindings,
@ -404,4 +404,4 @@ export const defaultBindingsProvider: IKeyBindingsProvider = {
getRoomListBindings: roomListBindings,
getRoomBindings: roomBindings,
getNavigationBindings: navigationBindings,
}
};

View file

@ -140,12 +140,12 @@ export type KeyCombo = {
ctrlKey?: boolean;
metaKey?: boolean;
shiftKey?: boolean;
}
};
export type KeyBinding<T extends string> = {
action: T;
keyCombo: KeyCombo;
}
};
/**
* Helper method to check if a KeyboardEvent matches a KeyCombo

View file

@ -20,7 +20,7 @@ limitations under the License.
import { createClient } from 'matrix-js-sdk/src/matrix';
import { InvalidStoreError } from "matrix-js-sdk/src/errors";
import { MatrixClient } from "matrix-js-sdk/src/client";
import {decryptAES, encryptAES} from "matrix-js-sdk/src/crypto/aes";
import { decryptAES, encryptAES, IEncryptedPayload } from "matrix-js-sdk/src/crypto/aes";
import { IMatrixClientCreds, MatrixClientPeg } from './MatrixClientPeg';
import SecurityCustomisations from "./customisations/Security";
@ -303,7 +303,7 @@ export interface IStoredSession {
hsUrl: string;
isUrl: string;
hasAccessToken: boolean;
accessToken: string | object;
accessToken: string | IEncryptedPayload;
userId: string;
deviceId: string;
isGuest: boolean;

View file

@ -190,7 +190,6 @@ export default class Login {
}
}
/**
* Send a login request to the given server, and format the response
* as a MatrixClientCreds

View file

@ -15,7 +15,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';

View file

@ -78,7 +78,7 @@ class Presence {
this.setState(State.Online);
this.unavailableTimer.restart();
}
}
};
/**
* Set the presence state.

View file

@ -104,7 +104,6 @@ export function setDMRoom(roomId: string, userId: string): Promise<void> {
dmRoomMap[userId] = roomList;
}
return MatrixClientPeg.get().setAccountData('m.direct', dmRoomMap);
}

View file

@ -208,7 +208,6 @@ Example:
]
}
membership_state AND bot_options
--------------------------------
Get the content of the "m.room.member" or "m.room.bot.options" state event respectively.

View file

@ -135,7 +135,7 @@ async function getSecretStorageKey(
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) {
console.log("Using key from security customisations (secret storage)")
console.log("Using key from security customisations (secret storage)");
cacheSecretStorageKey(keyId, keyInfo, keyFromCustomisations);
return [keyId, keyFromCustomisations];
}
@ -185,7 +185,7 @@ export async function getDehydrationKey(
): Promise<Uint8Array> {
const keyFromCustomisations = SecurityCustomisations.getSecretStorageKey?.();
if (keyFromCustomisations) {
console.log("Using key from security customisations (dehydration)")
console.log("Using key from security customisations (dehydration)");
return keyFromCustomisations;
}

View file

@ -1,4 +1,3 @@
//@flow
/*
Copyright 2017 Aviral Dasgupta

View file

@ -47,7 +47,7 @@ import { EffectiveMembership, getEffectiveMembership, leaveRoomBehaviour } from
import SdkConfig from "./SdkConfig";
import SettingsStore from "./settings/SettingsStore";
import { UIFeature } from "./settings/UIFeature";
import {CHAT_EFFECTS} from "./effects"
import { CHAT_EFFECTS } from "./effects";
import CallHandler from "./CallHandler";
import { guessAndSetDMRoom } from "./Rooms";
@ -1176,7 +1176,7 @@ export const Commands = [
})());
},
category: CommandCategories.effects,
})
});
}),
];

View file

@ -112,7 +112,7 @@ function textForMemberEvent(ev): () => string | null {
targetName,
reason,
})
: _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName })
: _t('%(senderName)s withdrew %(targetName)s\'s invitation', { senderName, targetName });
} else if (prevContent.membership === "join") {
return () => reason
? _t('%(senderName)s kicked %(targetName)s: %(reason)s', {
@ -492,7 +492,7 @@ const onPinnedMessagesClick = (): void => {
phase: RightPanelPhases.PinnedMessages,
allowClose: false,
});
}
};
function textForPinnedEvent(event: MatrixEvent, allowJSX: boolean): () => string | JSX.Element | null {
if (!SettingsStore.getValue("feature_pinning")) return null;

View file

@ -1,458 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2017 New Vector Ltd
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.
*/
const DEBUG = 0;
// utility to turn #rrggbb or rgb(r,g,b) into [red,green,blue]
function colorToRgb(color) {
if (!color) {
return [0, 0, 0];
}
if (color[0] === '#') {
color = color.slice(1);
if (color.length === 3) {
color = color[0] + color[0] +
color[1] + color[1] +
color[2] + color[2];
}
const val = parseInt(color, 16);
const r = (val >> 16) & 255;
const g = (val >> 8) & 255;
const b = val & 255;
return [r, g, b];
} else {
const match = color.match(/rgb\((.*?),(.*?),(.*?)\)/);
if (match) {
return [
parseInt(match[1]),
parseInt(match[2]),
parseInt(match[3]),
];
}
}
return [0, 0, 0];
}
// utility to turn [red,green,blue] into #rrggbb
function rgbToColor(rgb) {
const val = (rgb[0] << 16) | (rgb[1] << 8) | rgb[2];
return '#' + (0x1000000 + val).toString(16).slice(1);
}
class Tinter {
constructor() {
// The default colour keys to be replaced as referred to in CSS
// (should be overridden by .mx_theme_accentColor and .mx_theme_secondaryAccentColor)
this.keyRgb = [
"rgb(118, 207, 166)", // Vector Green
"rgb(234, 245, 240)", // Vector Light Green
"rgb(211, 239, 225)", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
];
// Some algebra workings for calculating the tint % of Vector Green & Light Green
// x * 118 + (1 - x) * 255 = 234
// x * 118 + 255 - 255 * x = 234
// x * 118 - x * 255 = 234 - 255
// (255 - 118) x = 255 - 234
// x = (255 - 234) / (255 - 118) = 0.16
// The colour keys to be replaced as referred to in SVGs
this.keyHex = [
"#76CFA6", // Vector Green
"#EAF5F0", // Vector Light Green
"#D3EFE1", // roomsublist-label-bg-color (20% Green overlaid on Light Green)
"#FFFFFF", // white highlights of the SVGs (for switching to dark theme)
"#000000", // black lowlights of the SVGs (for switching to dark theme)
];
// track the replacement colours actually being used
// defaults to our keys.
this.colors = [
this.keyHex[0],
this.keyHex[1],
this.keyHex[2],
this.keyHex[3],
this.keyHex[4],
];
// track the most current tint request inputs (which may differ from the
// end result stored in this.colors
this.currentTint = [
undefined,
undefined,
undefined,
undefined,
undefined,
];
this.cssFixups = [
// { theme: {
// style: a style object that should be fixed up taken from a stylesheet
// attr: name of the attribute to be clobbered, e.g. 'color'
// index: ordinal of primary, secondary or tertiary
// },
// }
];
// CSS attributes to be fixed up
this.cssAttrs = [
"color",
"backgroundColor",
"borderColor",
"borderTopColor",
"borderBottomColor",
"borderLeftColor",
];
this.svgAttrs = [
"fill",
"stroke",
];
// List of functions to call when the tint changes.
this.tintables = [];
// the currently loaded theme (if any)
this.theme = undefined;
// whether to force a tint (e.g. after changing theme)
this.forceTint = false;
}
/**
* Register a callback to fire when the tint changes.
* This is used to rewrite the tintable SVGs with the new tint.
*
* It's not possible to unregister a tintable callback. So this can only be
* used to register a static callback. If a set of tintables will change
* over time then the best bet is to register a single callback for the
* entire set.
*
* To ensure the tintable work happens at least once, it is also called as
* part of registration.
*
* @param {Function} tintable Function to call when the tint changes.
*/
registerTintable(tintable) {
this.tintables.push(tintable);
tintable();
}
getKeyRgb() {
return this.keyRgb;
}
tint(primaryColor, secondaryColor, tertiaryColor) {
return;
// eslint-disable-next-line no-unreachable
this.currentTint[0] = primaryColor;
this.currentTint[1] = secondaryColor;
this.currentTint[2] = tertiaryColor;
this.calcCssFixups();
if (DEBUG) {
console.log("Tinter.tint(" + primaryColor + ", " +
secondaryColor + ", " +
tertiaryColor + ")");
}
if (!primaryColor) {
primaryColor = this.keyRgb[0];
secondaryColor = this.keyRgb[1];
tertiaryColor = this.keyRgb[2];
}
if (!secondaryColor) {
const x = 0.16; // average weighting factor calculated from vector green & light green
const rgb = colorToRgb(primaryColor);
rgb[0] = x * rgb[0] + (1 - x) * 255;
rgb[1] = x * rgb[1] + (1 - x) * 255;
rgb[2] = x * rgb[2] + (1 - x) * 255;
secondaryColor = rgbToColor(rgb);
}
if (!tertiaryColor) {
const x = 0.19;
const rgb1 = colorToRgb(primaryColor);
const rgb2 = colorToRgb(secondaryColor);
rgb1[0] = x * rgb1[0] + (1 - x) * rgb2[0];
rgb1[1] = x * rgb1[1] + (1 - x) * rgb2[1];
rgb1[2] = x * rgb1[2] + (1 - x) * rgb2[2];
tertiaryColor = rgbToColor(rgb1);
}
if (this.forceTint == false &&
this.colors[0] === primaryColor &&
this.colors[1] === secondaryColor &&
this.colors[2] === tertiaryColor) {
return;
}
this.forceTint = false;
this.colors[0] = primaryColor;
this.colors[1] = secondaryColor;
this.colors[2] = tertiaryColor;
if (DEBUG) {
console.log("Tinter.tint final: (" + primaryColor + ", " +
secondaryColor + ", " +
tertiaryColor + ")");
}
// go through manually fixing up the stylesheets.
this.applyCssFixups();
// tell all the SVGs to go fix themselves up
// we don't do this as a dispatch otherwise it will visually lag
this.tintables.forEach(function(tintable) {
tintable();
});
}
tintSvgWhite(whiteColor) {
this.currentTint[3] = whiteColor;
if (!whiteColor) {
whiteColor = this.colors[3];
}
if (this.colors[3] === whiteColor) {
return;
}
this.colors[3] = whiteColor;
this.tintables.forEach(function(tintable) {
tintable();
});
}
tintSvgBlack(blackColor) {
this.currentTint[4] = blackColor;
if (!blackColor) {
blackColor = this.colors[4];
}
if (this.colors[4] === blackColor) {
return;
}
this.colors[4] = blackColor;
this.tintables.forEach(function(tintable) {
tintable();
});
}
setTheme(theme) {
this.theme = theme;
// update keyRgb from the current theme CSS itself, if it defines it
if (document.getElementById('mx_theme_accentColor')) {
this.keyRgb[0] = window.getComputedStyle(
document.getElementById('mx_theme_accentColor')).color;
}
if (document.getElementById('mx_theme_secondaryAccentColor')) {
this.keyRgb[1] = window.getComputedStyle(
document.getElementById('mx_theme_secondaryAccentColor')).color;
}
if (document.getElementById('mx_theme_tertiaryAccentColor')) {
this.keyRgb[2] = window.getComputedStyle(
document.getElementById('mx_theme_tertiaryAccentColor')).color;
}
this.calcCssFixups();
this.forceTint = true;
this.tint(this.currentTint[0], this.currentTint[1], this.currentTint[2]);
if (theme === 'dark') {
// abuse the tinter to change all the SVG's #fff to #2d2d2d
// XXX: obviously this shouldn't be hardcoded here.
this.tintSvgWhite('#2d2d2d');
this.tintSvgBlack('#dddddd');
} else {
this.tintSvgWhite('#ffffff');
this.tintSvgBlack('#000000');
}
}
calcCssFixups() {
// cache our fixups
if (this.cssFixups[this.theme]) return;
if (DEBUG) {
console.debug("calcCssFixups start for " + this.theme + " (checking " +
document.styleSheets.length +
" stylesheets)");
}
this.cssFixups[this.theme] = [];
for (let i = 0; i < document.styleSheets.length; i++) {
const ss = document.styleSheets[i];
try {
if (!ss) continue; // well done safari >:(
// Chromium apparently sometimes returns null here; unsure why.
// see $14534907369972FRXBx:matrix.org in HQ
// ...ah, it's because there's a third party extension like
// privacybadger inserting its own stylesheet in there with a
// resource:// URI or something which results in a XSS error.
// See also #vector:matrix.org/$145357669685386ebCfr:matrix.org
// ...except some browsers apparently return stylesheets without
// hrefs, which we have no choice but ignore right now
// XXX seriously? we are hardcoding the name of vector's CSS file in
// here?
//
// Why do we need to limit it to vector's CSS file anyway - if there
// are other CSS files affecting the doc don't we want to apply the
// same transformations to them?
//
// Iterating through the CSS looking for matches to hack on feels
// pretty horrible anyway. And what if the application skin doesn't use
// Vector Green as its primary color?
// --richvdh
// Yes, tinting assumes that you are using the Element skin for now.
// The right solution will be to move the CSS over to react-sdk.
// And yes, the default assets for the base skin might as well use
// Vector Green as any other colour.
// --matthew
// stylesheets we don't have permission to access (eg. ones from extensions) have a null
// href and will throw exceptions if we try to access their rules.
if (!ss.href || !ss.href.match(new RegExp('/theme-' + this.theme + '.css$'))) continue;
if (ss.disabled) continue;
if (!ss.cssRules) continue;
if (DEBUG) console.debug("calcCssFixups checking " + ss.cssRules.length + " rules for " + ss.href);
for (let j = 0; j < ss.cssRules.length; j++) {
const rule = ss.cssRules[j];
if (!rule.style) continue;
if (rule.selectorText && rule.selectorText.match(/#mx_theme/)) continue;
for (let k = 0; k < this.cssAttrs.length; k++) {
const attr = this.cssAttrs[k];
for (let l = 0; l < this.keyRgb.length; l++) {
if (rule.style[attr] === this.keyRgb[l]) {
this.cssFixups[this.theme].push({
style: rule.style,
attr: attr,
index: l,
});
}
}
}
}
} catch (e) {
// Catch any random exceptions that happen here: all sorts of things can go
// wrong with this (nulls, SecurityErrors) and mostly it's for other
// stylesheets that we don't want to proces anyway. We should not propagate an
// exception out since this will cause the app to fail to start.
console.log("Failed to calculate CSS fixups for a stylesheet: " + ss.href, e);
}
}
if (DEBUG) {
console.log("calcCssFixups end (" +
this.cssFixups[this.theme].length +
" fixups)");
}
}
applyCssFixups() {
if (DEBUG) {
console.log("applyCssFixups start (" +
this.cssFixups[this.theme].length +
" fixups)");
}
for (let i = 0; i < this.cssFixups[this.theme].length; i++) {
const cssFixup = this.cssFixups[this.theme][i];
try {
cssFixup.style[cssFixup.attr] = this.colors[cssFixup.index];
} catch (e) {
// Firefox Quantum explodes if you manually edit the CSS in the
// inspector and then try to do a tint, as apparently all the
// fixups are then stale.
console.error("Failed to apply cssFixup in Tinter! ", e.name);
}
}
if (DEBUG) console.log("applyCssFixups end");
}
// XXX: we could just move this all into TintableSvg, but as it's so similar
// to the CSS fixup stuff in Tinter (just that the fixups are stored in TintableSvg)
// keeping it here for now.
calcSvgFixups(svgs) {
// go through manually fixing up SVG colours.
// we could do this by stylesheets, but keeping the stylesheets
// updated would be a PITA, so just brute-force search for the
// key colour; cache the element and apply.
if (DEBUG) console.log("calcSvgFixups start for " + svgs);
const fixups = [];
for (let i = 0; i < svgs.length; i++) {
let svgDoc;
try {
svgDoc = svgs[i].contentDocument;
} catch (e) {
let msg = 'Failed to get svg.contentDocument of ' + svgs[i].toString();
if (e.message) {
msg += e.message;
}
if (e.stack) {
msg += ' | stack: ' + e.stack;
}
console.error(msg);
}
if (!svgDoc) continue;
const tags = svgDoc.getElementsByTagName("*");
for (let j = 0; j < tags.length; j++) {
const tag = tags[j];
for (let k = 0; k < this.svgAttrs.length; k++) {
const attr = this.svgAttrs[k];
for (let l = 0; l < this.keyHex.length; l++) {
if (tag.getAttribute(attr) &&
tag.getAttribute(attr).toUpperCase() === this.keyHex[l]) {
fixups.push({
node: tag,
attr: attr,
index: l,
});
}
}
}
}
}
if (DEBUG) console.log("calcSvgFixups end");
return fixups;
}
applySvgFixups(fixups) {
if (DEBUG) console.log("applySvgFixups start for " + fixups);
for (let i = 0; i < fixups.length; i++) {
const svgFixup = fixups[i];
svgFixup.node.setAttribute(svgFixup.attr, this.colors[svgFixup.index]);
}
if (DEBUG) console.log("applySvgFixups end");
}
}
if (global.singletonTinter === undefined) {
global.singletonTinter = new Tinter();
}
export default global.singletonTinter;

View file

@ -68,7 +68,6 @@ export default class CommandProvider extends AutocompleteProvider {
}
}
return matches.filter(cmd => cmd.isEnabled()).map((result) => {
let completion = result.getCommand() + ' ';
const usedAlias = result.aliases.find(alias => `/${alias}` === command[1]);

View file

@ -15,12 +15,12 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { HTMLAttributes } from "react";
import React, { HTMLAttributes, WheelEvent } from "react";
interface IProps extends HTMLAttributes<HTMLDivElement> {
interface IProps extends Omit<HTMLAttributes<HTMLDivElement>, "onScroll"> {
className?: string;
onScroll?: () => void;
onWheel?: () => void;
onScroll?: (event: Event) => void;
onWheel?: (event: WheelEvent) => void;
style?: React.CSSProperties
tabIndex?: number,
wrappedRef?: (ref: HTMLDivElement) => void;

View file

@ -16,9 +16,13 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Filter } from 'matrix-js-sdk/src/filter';
import { EventTimelineSet } from "matrix-js-sdk/src/models/event-timeline-set";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from 'matrix-js-sdk/src/models/room';
import { TimelineWindow } from 'matrix-js-sdk/src/timeline-window';
import * as sdk from '../../index';
import { MatrixClientPeg } from '../../MatrixClientPeg';
import EventIndexPeg from "../../indexing/EventIndexPeg";
@ -28,25 +32,33 @@ import {RightPanelPhases} from "../../stores/RightPanelStorePhases";
import DesktopBuildsNotice, { WarningKind } from "../views/elements/DesktopBuildsNotice";
import { replaceableComponent } from "../../utils/replaceableComponent";
import ResizeNotifier from '../../utils/ResizeNotifier';
interface IProps {
roomId: string;
onClose: () => void;
resizeNotifier: ResizeNotifier
}
interface IState {
timelineSet: EventTimelineSet;
}
/*
* Component which shows the filtered file using a TimelinePanel
*/
@replaceableComponent("structures.FilePanel")
class FilePanel extends React.Component {
static propTypes = {
roomId: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
};
class FilePanel extends React.Component<IProps, IState> {
// This is used to track if a decrypted event was a live event and should be
// added to the timeline.
decryptingEvents = new Set();
private decryptingEvents = new Set<string>();
public noRoom: boolean;
state = {
timelineSet: null,
};
onRoomTimeline = (ev, room, toStartOfTimeline, removed, data) => {
private onRoomTimeline = (ev: MatrixEvent, room: Room, toStartOfTimeline: true, removed: true, data: any): void => {
if (room?.roomId !== this.props?.roomId) return;
if (toStartOfTimeline || !data || !data.liveEvent || ev.isRedacted()) return;
@ -60,7 +72,7 @@ class FilePanel extends React.Component {
}
};
onEventDecrypted = (ev, err) => {
private onEventDecrypted = (ev: MatrixEvent, err?: any): void => {
if (ev.getRoomId() !== this.props.roomId) return;
const eventId = ev.getId();
@ -70,7 +82,7 @@ class FilePanel extends React.Component {
this.addEncryptedLiveEvent(ev);
};
addEncryptedLiveEvent(ev, toStartOfTimeline) {
public addEncryptedLiveEvent(ev: MatrixEvent): void {
if (!this.state.timelineSet) return;
const timeline = this.state.timelineSet.getLiveTimeline();
@ -84,7 +96,7 @@ class FilePanel extends React.Component {
}
}
async componentDidMount() {
public async componentDidMount(): Promise<void> {
const client = MatrixClientPeg.get();
await this.updateTimelineSet(this.props.roomId);
@ -105,7 +117,7 @@ class FilePanel extends React.Component {
}
}
componentWillUnmount() {
public componentWillUnmount(): void {
const client = MatrixClientPeg.get();
if (client === null) return;
@ -117,7 +129,7 @@ class FilePanel extends React.Component {
}
}
async fetchFileEventsServer(room) {
public async fetchFileEventsServer(room: Room): Promise<void> {
const client = MatrixClientPeg.get();
const filter = new Filter(client.credentials.userId);
@ -141,7 +153,7 @@ class FilePanel extends React.Component {
return timelineSet;
}
onPaginationRequest = (timelineWindow, direction, limit) => {
private onPaginationRequest = (timelineWindow: TimelineWindow, direction: string, limit: number): void => {
const client = MatrixClientPeg.get();
const eventIndex = EventIndexPeg.get();
const roomId = this.props.roomId;
@ -159,7 +171,7 @@ class FilePanel extends React.Component {
}
};
async updateTimelineSet(roomId: string) {
public async updateTimelineSet(roomId: string): Promise<void> {
const client = MatrixClientPeg.get();
const room = client.getRoom(roomId);
const eventIndex = EventIndexPeg.get();
@ -195,7 +207,7 @@ class FilePanel extends React.Component {
}
}
render() {
public render() {
if (MatrixClientPeg.get().isGuest()) {
return <BaseCard
className="mx_FilePanel mx_RoomView_messageListWrapper"

View file

@ -126,12 +126,11 @@ class CategoryRoomList extends React.Component {
};
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ?
(<AccessibleButton className="mx_GroupView_featuredThings_addButton"
onClick={this.onAddRoomsToSummaryClicked}
>
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
<img src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
<div className="mx_GroupView_featuredThings_addButton_label">
{ _t('Add a Room') }
</div>
@ -300,10 +299,9 @@ class RoleUserList extends React.Component {
};
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const addButton = this.props.editing ?
(<AccessibleButton className="mx_GroupView_featuredThings_addButton" onClick={this.onAddUsersClicked}>
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
<img src={require("../../../res/img/icons-create-room.svg")} width="64" height="64" />
<div className="mx_GroupView_featuredThings_addButton_label">
{ _t('Add a User') }
</div>
@ -363,7 +361,10 @@ class FeaturedUser extends React.Component {
"Failed to remove a user from the summary of %(groupId)s",
{ groupId: this.props.groupId },
),
description: _t("The user '%(displayName)s' could not be removed from the summary.", {displayName}),
description: _t(
"The user '%(displayName)s' could not be removed from the summary.",
{ displayName },
),
},
);
});
@ -855,7 +856,6 @@ export default class GroupView extends React.Component {
_getRoomsNode() {
const RoomDetailList = sdk.getComponent('rooms.RoomDetailList');
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const TintableSvg = sdk.getComponent('elements.TintableSvg');
const Spinner = sdk.getComponent('elements.Spinner');
const TooltipButton = sdk.getComponent('elements.TooltipButton');
@ -871,7 +871,7 @@ export default class GroupView extends React.Component {
onClick={this._onAddRoomsClick}
>
<div className="mx_GroupView_rooms_header_addRow_button">
<TintableSvg src={require("../../../res/img/icons-room-add.svg")} width="24" height="24" />
<img src={require("../../../res/img/icons-room-add.svg")} width="24" height="24" />
</div>
<div className="mx_GroupView_rooms_header_addRow_label">
{ _t('Add rooms to this community') }

View file

@ -117,7 +117,6 @@ const HomePage: React.FC<IProps> = ({ justRegistered = false }) => {
</React.Fragment>;
}
return <AutoHideScrollbar className="mx_HomePage mx_HomePage_default">
<div className="mx_HomePage_default_wrapper">
{ introSection }

View file

@ -35,7 +35,7 @@ export default class HostSignupAction extends React.PureComponent<IProps, IState
private openDialog = async () => {
this.props.onClick?.();
await HostSignupStore.instance.setHostSignupActive(true);
}
};
public render(): React.ReactNode {
const hostSignupConfig = SdkConfig.get().hostSignup;

View file

@ -70,7 +70,6 @@ export default class IndicatorScrollbar extends React.Component {
this._autoHideScrollbar = autoHideScrollbar;
}
componentDidUpdate(prevProps) {
const prevLen = prevProps && prevProps.children && prevProps.children.length || 0;
const curLen = this.props.children && this.props.children.length || 0;

View file

@ -127,7 +127,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private onDialPad = () => {
dis.fire(Action.OpenDialPad);
}
};
private onExplore = () => {
dis.fire(Action.ViewRoomDirectory);
@ -136,7 +136,7 @@ export default class LeftPanel extends React.Component<IProps, IState> {
private refreshStickyHeaders = () => {
if (!this.listContainerRef.current) return; // ignore: no headers to sticky
this.handleStickyHeaders(this.listContainerRef.current);
}
};
private onBreadcrumbsUpdate = () => {
const newVal = BreadcrumbsStore.instance.visible;

View file

@ -319,7 +319,7 @@ class LoggedInView extends React.Component<IProps, IState> {
this.setState({
usageLimitDismissed: true,
});
}
};
_calculateServerLimitToast(syncError: IState["syncErrorData"], usageLimitEventContent?: IUsageLimit) {
const error = syncError && syncError.error && syncError.error.errcode === "M_RESOURCE_LIMIT_EXCEEDED";

View file

@ -34,7 +34,6 @@ import dis from "../../dispatcher/dispatcher";
import Notifier from '../../Notifier';
import Modal from "../../Modal";
import Tinter from "../../Tinter";
import * as sdk from '../../index';
import { showRoomInviteDialog, showStartChatInviteDialog } from '../../RoomInvite';
import * as Rooms from '../../Rooms';
@ -283,11 +282,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.pageChanging = false;
// check we have the right tint applied for this theme.
// N.B. we don't call the whole of setTheme() here as we may be
// racing with the theme CSS download finishing from index.js
Tinter.tint();
// For PersistentElement
this.state.resizeNotifier.on("middlePanelResized", this.dispatchTimelineResize);
@ -668,7 +662,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
this.createRoom(payload.public, payload.defaultName);
break;
case 'view_create_group': {
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog")
let CreateGroupDialog = sdk.getComponent("dialogs.CreateGroupDialog");
if (SettingsStore.getValue("feature_communities_v2_prototypes")) {
CreateGroupDialog = CreateCommunityPrototypeDialog;
}
@ -1131,8 +1125,14 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
description: (
<span>
{ isSpace
? _t("Are you sure you want to leave the space '%(spaceName)s'?", {spaceName: roomToLeave.name})
: _t("Are you sure you want to leave the room '%(roomName)s'?", {roomName: roomToLeave.name}) }
? _t(
"Are you sure you want to leave the space '%(spaceName)s'?",
{ spaceName: roomToLeave.name },
)
: _t(
"Are you sure you want to leave the room '%(roomName)s'?",
{ roomName: roomToLeave.name },
)}
{ warnings }
</span>
),
@ -1263,7 +1263,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// HACK: This is a pretty brutal way of threading the invite back through
// our systems, but it's the safest we have for now.
const params = ThreepidInviteStore.instance.translateToWireFormat(threepidInvite);
this.showScreen(`room/${threepidInvite.roomId}`, params)
this.showScreen(`room/${threepidInvite.roomId}`, params);
} else {
// The user has just logged in after registering,
// so show the homepage.
@ -1573,10 +1573,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
});
}
});
// Fire the tinter right on startup to ensure the default theme is applied
// A later sync can/will correct the tint to be the right value for the user
const colorScheme = SettingsStore.getValue("roomColor");
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
}
/**

View file

@ -123,7 +123,7 @@ export default class MyGroups extends React.Component {
</div>
{/*<div className="mx_MyGroups_joinBox mx_MyGroups_headerCard">
<AccessibleButton className='mx_MyGroups_headerCard_button' onClick={this._onJoinGroupClick}>
<TintableSvg src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
<img src={require("../../../res/img/icons-create-room.svg")} width="50" height="50" />
</AccessibleButton>
<div className="mx_MyGroups_headerCard_content">
<div className="mx_MyGroups_headerCard_header">

View file

@ -22,6 +22,7 @@ import BaseCard from "../views/right_panel/BaseCard";
import { replaceableComponent } from "../../utils/replaceableComponent";
import TimelinePanel from "./TimelinePanel";
import Spinner from "../views/elements/Spinner";
import { TileShape } from "../views/rooms/EventTile";
interface IProps {
onClose(): void;
@ -48,7 +49,7 @@ export default class NotificationPanel extends React.PureComponent<IProps> {
manageReadMarkers={false}
timelineSet={timelineSet}
showUrlPreview={false}
tileShape="notif"
tileShape={TileShape.Notif}
empty={emptyState}
alwaysShowTimestamps={true}
/>

View file

@ -45,7 +45,6 @@ import ScrollPanel from "./ScrollPanel";
import Spinner from "../views/elements/Spinner";
import { ActionPayload } from "../../dispatcher/payloads";
const MAX_NAME_LENGTH = 80;
const MAX_TOPIC_LENGTH = 800;
@ -95,7 +94,7 @@ interface IPublicRoomsRequest {
@replaceableComponent("structures.RoomDirectory")
export default class RoomDirectory extends React.Component<IProps, IState> {
private readonly startTime: number;
private unmounted = false
private unmounted = false;
private nextBatch: string = null;
private filterTimeout: NodeJS.Timeout;
private protocols: Protocols;
@ -207,9 +206,9 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
this.getMoreRooms();
};
private getMoreRooms() {
if (this.state.selectedCommunityId) return Promise.resolve(); // no more rooms
if (!MatrixClientPeg.get()) return Promise.resolve();
private getMoreRooms(): Promise<boolean> {
if (this.state.selectedCommunityId) return Promise.resolve(false); // no more rooms
if (!MatrixClientPeg.get()) return Promise.resolve(false);
this.setState({
loading: true,
@ -239,12 +238,12 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
// if the filter or server has changed since this request was sent,
// throw away the result (don't even clear the busy flag
// since we must still have a request in flight)
return;
return false;
}
if (this.unmounted) {
// if we've been unmounted, we don't care either.
return;
return false;
}
if (this.state.filterString) {
@ -264,14 +263,13 @@ export default class RoomDirectory extends React.Component<IProps, IState> {
filterString != this.state.filterString ||
roomServer != this.state.roomServer ||
nextBatch != this.nextBatch) {
// as above: we don't care about errors for old
// requests either
return;
// as above: we don't care about errors for old requests either
return false;
}
if (this.unmounted) {
// if we've been unmounted, we don't care either.
return;
return false;
}
console.error("Failed to get publicRooms: %s", JSON.stringify(err));

View file

@ -37,7 +37,6 @@ import Modal from '../../Modal';
import * as sdk from '../../index';
import CallHandler, { PlaceCallType } from '../../CallHandler';
import dis from '../../dispatcher/dispatcher';
import Tinter from '../../Tinter';
import rateLimitedFunc from '../../ratelimitedfunc';
import * as Rooms from '../../Rooms';
import eventSearch, { searchPagination } from '../../Searching';
@ -287,7 +286,7 @@ export default class RoomView extends React.Component<IProps, IState> {
if (this.state.room) {
this.checkWidgets(this.state.room);
}
}
};
private checkWidgets = (room) => {
this.setState({
@ -679,10 +678,6 @@ export default class RoomView extends React.Component<IProps, IState> {
// cancel any pending calls to the rate_limited_funcs
this.updateRoomMembers.cancelPendingCall();
// no need to do this as Dir & Settings are now overlays. It just burnt CPU.
// console.log("Tinter.tint from RoomView.unmount");
// Tinter.tint(); // reset colourscheme
for (const watcher of this.settingWatchers) {
SettingsStore.unwatchSetting(watcher);
}
@ -698,7 +693,7 @@ export default class RoomView extends React.Component<IProps, IState> {
replyingToEvent: this.state.replyToEvent,
});
}
}
};
private onRightPanelStoreUpdate = () => {
this.setState({
@ -1062,10 +1057,6 @@ export default class RoomView extends React.Component<IProps, IState> {
private updateTint() {
const room = this.state.room;
if (!room) return;
console.log("Tinter.tint from updateTint");
const colorScheme = SettingsStore.getValue("roomColor", room.roomId);
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
}
private onAccountData = (event: MatrixEvent) => {
@ -1079,12 +1070,7 @@ export default class RoomView extends React.Component<IProps, IState> {
private onRoomAccountData = (event: MatrixEvent, room: Room) => {
if (room.roomId == this.state.roomId) {
const type = event.getType();
if (type === "org.matrix.room.color_scheme") {
const colorScheme = event.getContent();
// XXX: we should validate the event
console.log("Tinter.tint from onRoomAccountData");
Tinter.tint(colorScheme.primary_color, colorScheme.secondary_color);
} else if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
if (type === "org.matrix.room.preview_urls" || type === "im.vector.web.settings") {
// non-e2ee url previews are stored in legacy event type `org.matrix.room.preview_urls`
this.updatePreviewUrlVisibility(room);
}
@ -1157,7 +1143,7 @@ export default class RoomView extends React.Component<IProps, IState> {
}
}
private onSearchResultsFillRequest = (backwards: boolean) => {
private onSearchResultsFillRequest = (backwards: boolean): Promise<boolean> => {
if (!backwards) {
return Promise.resolve(false);
}
@ -1323,7 +1309,7 @@ export default class RoomView extends React.Component<IProps, IState> {
this.handleSearchResult(searchPromise);
};
private handleSearchResult(searchPromise: Promise<any>) {
private handleSearchResult(searchPromise: Promise<any>): Promise<boolean> {
// keep a record of the current search id, so that if the search terms
// change before we get a response, we can ignore the results.
const localSearchId = this.searchId;
@ -1336,7 +1322,7 @@ export default class RoomView extends React.Component<IProps, IState> {
debuglog("search complete");
if (this.unmounted || !this.state.searching || this.searchId != localSearchId) {
console.error("Discarding stale search results");
return;
return false;
}
// postgres on synapse returns us precise details of the strings
@ -1368,6 +1354,7 @@ export default class RoomView extends React.Component<IProps, IState> {
description: ((error && error.message) ? error.message :
_t("Server may be unavailable, overloaded, or search timed out :(")),
});
return false;
}).finally(() => {
this.setState({
searchInProgress: false,

View file

@ -1,5 +1,5 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2015 - 2021 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.
@ -14,17 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {createRef} from "react";
import PropTypes from 'prop-types';
import React, { createRef, CSSProperties, ReactNode, SyntheticEvent, KeyboardEvent } from "react";
import Timer from '../../utils/Timer';
import AutoHideScrollbar from "./AutoHideScrollbar";
import { replaceableComponent } from "../../utils/replaceableComponent";
import { getKeyBindingsManager, RoomAction } from "../../KeyBindingsManager";
import ResizeNotifier from "../../utils/ResizeNotifier";
const DEBUG_SCROLL = false;
// The amount of extra scroll distance to allow prior to unfilling.
// See _getExcessHeight.
// See getExcessHeight.
const UNPAGINATION_PADDING = 6000;
// The number of milliseconds to debounce calls to onUnfillRequest, to prevent
// many scroll events causing many unfilling requests.
@ -43,6 +43,75 @@ if (DEBUG_SCROLL) {
debuglog = function() {};
}
interface IProps {
/* stickyBottom: if set to true, then once the user hits the bottom of
* the list, any new children added to the list will cause the list to
* scroll down to show the new element, rather than preserving the
* existing view.
*/
stickyBottom?: boolean;
/* startAtBottom: if set to true, the view is assumed to start
* scrolled to the bottom.
* XXX: It's likely this is unnecessary and can be derived from
* stickyBottom, but I'm adding an extra parameter to ensure
* behaviour stays the same for other uses of ScrollPanel.
* If so, let's remove this parameter down the line.
*/
startAtBottom?: boolean;
/* className: classnames to add to the top-level div
*/
className?: string;
/* style: styles to add to the top-level div
*/
style?: CSSProperties;
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
*/
resizeNotifier?: ResizeNotifier;
/* fixedChildren: allows for children to be passed which are rendered outside
* of the wrapper
*/
fixedChildren?: ReactNode;
/* onFillRequest(backwards): a callback which is called on scroll when
* the user nears the start (backwards = true) or end (backwards =
* false) of the list.
*
* This should return a promise; no more calls will be made until the
* promise completes.
*
* The promise should resolve to true if there is more data to be
* retrieved in this direction (in which case onFillRequest may be
* called again immediately), or false if there is no more data in this
* directon (at this time) - which will stop the pagination cycle until
* the user scrolls again.
*/
onFillRequest?(backwards: boolean): Promise<boolean>;
/* onUnfillRequest(backwards): a callback which is called on scroll when
* there are children elements that are far out of view and could be removed
* without causing pagination to occur.
*
* This function should accept a boolean, which is true to indicate the back/top
* of the panel and false otherwise, and a scroll token, which refers to the
* first element to remove if removing from the front/bottom, and last element
* to remove if removing from the back/top.
*/
onUnfillRequest?(backwards: boolean, scrollToken: string): void;
/* onScroll: a callback which is called whenever any scroll happens.
*/
onScroll?(event: Event): void;
/* onUserScroll: callback which is called when the user interacts with the room timeline
*/
onUserScroll?(event: SyntheticEvent): void;
}
/* This component implements an intelligent scrolling list.
*
* It wraps a list of <li> children; when items are added to the start or end
@ -84,97 +153,54 @@ if (DEBUG_SCROLL) {
* offset as normal.
*/
export interface IScrollState {
stuckAtBottom: boolean;
trackedNode?: HTMLElement;
trackedScrollToken?: string;
bottomOffset?: number;
pixelOffset?: number;
}
interface IPreventShrinkingState {
offsetFromBottom: number;
offsetNode: HTMLElement;
}
@replaceableComponent("structures.ScrollPanel")
export default class ScrollPanel extends React.Component {
static propTypes = {
/* stickyBottom: if set to true, then once the user hits the bottom of
* the list, any new children added to the list will cause the list to
* scroll down to show the new element, rather than preserving the
* existing view.
*/
stickyBottom: PropTypes.bool,
/* startAtBottom: if set to true, the view is assumed to start
* scrolled to the bottom.
* XXX: It's likely this is unnecessary and can be derived from
* stickyBottom, but I'm adding an extra parameter to ensure
* behaviour stays the same for other uses of ScrollPanel.
* If so, let's remove this parameter down the line.
*/
startAtBottom: PropTypes.bool,
/* onFillRequest(backwards): a callback which is called on scroll when
* the user nears the start (backwards = true) or end (backwards =
* false) of the list.
*
* This should return a promise; no more calls will be made until the
* promise completes.
*
* The promise should resolve to true if there is more data to be
* retrieved in this direction (in which case onFillRequest may be
* called again immediately), or false if there is no more data in this
* directon (at this time) - which will stop the pagination cycle until
* the user scrolls again.
*/
onFillRequest: PropTypes.func,
/* onUnfillRequest(backwards): a callback which is called on scroll when
* there are children elements that are far out of view and could be removed
* without causing pagination to occur.
*
* This function should accept a boolean, which is true to indicate the back/top
* of the panel and false otherwise, and a scroll token, which refers to the
* first element to remove if removing from the front/bottom, and last element
* to remove if removing from the back/top.
*/
onUnfillRequest: PropTypes.func,
/* onScroll: a callback which is called whenever any scroll happens.
*/
onScroll: PropTypes.func,
/* onUserScroll: callback which is called when the user interacts with the room timeline
*/
onUserScroll: PropTypes.func,
/* className: classnames to add to the top-level div
*/
className: PropTypes.string,
/* style: styles to add to the top-level div
*/
style: PropTypes.object,
/* resizeNotifier: ResizeNotifier to know when middle column has changed size
*/
resizeNotifier: PropTypes.object,
/* fixedChildren: allows for children to be passed which are rendered outside
* of the wrapper
*/
fixedChildren: PropTypes.node,
};
export default class ScrollPanel extends React.Component<IProps> {
static defaultProps = {
stickyBottom: true,
startAtBottom: true,
onFillRequest: function(backwards) { return Promise.resolve(false); },
onUnfillRequest: function(backwards, scrollToken) {},
onFillRequest: function(backwards: boolean) { return Promise.resolve(false); },
onUnfillRequest: function(backwards: boolean, scrollToken: string) {},
onScroll: function() {},
};
constructor(props) {
super(props);
private readonly pendingFillRequests: Record<"b" | "f", boolean> = {
b: null,
f: null,
};
private readonly itemlist = createRef<HTMLOListElement>();
private unmounted = false;
private scrollTimeout: Timer;
private isFilling: boolean;
private fillRequestWhileRunning: boolean;
private scrollState: IScrollState;
private preventShrinkingState: IPreventShrinkingState;
private unfillDebouncer: NodeJS.Timeout;
private bottomGrowth: number;
private pages: number;
private heightUpdateInProgress: boolean;
private divScroll: HTMLDivElement;
this._pendingFillRequests = {b: null, f: null};
constructor(props, context) {
super(props, context);
if (this.props.resizeNotifier) {
this.props.resizeNotifier.on("middlePanelResizedNoisy", this.onResize);
}
this.resetScrollState();
this._itemlist = createRef();
}
componentDidMount() {
@ -203,18 +229,18 @@ export default class ScrollPanel extends React.Component {
}
}
onScroll = ev => {
private onScroll = ev => {
// skip scroll events caused by resizing
if (this.props.resizeNotifier && this.props.resizeNotifier.isResizing) return;
debuglog("onScroll", this._getScrollNode().scrollTop);
this._scrollTimeout.restart();
this._saveScrollState();
debuglog("onScroll", this.getScrollNode().scrollTop);
this.scrollTimeout.restart();
this.saveScrollState();
this.updatePreventShrinking();
this.props.onScroll(ev);
this.checkFillState();
};
onResize = () => {
private onResize = () => {
debuglog("onResize");
this.checkScroll();
// update preventShrinkingState if present
@ -225,11 +251,11 @@ export default class ScrollPanel extends React.Component {
// after an update to the contents of the panel, check that the scroll is
// where it ought to be, and set off pagination requests if necessary.
checkScroll = () => {
public checkScroll = () => {
if (this.unmounted) {
return;
}
this._restoreSavedScrollState();
this.restoreSavedScrollState();
this.checkFillState();
};
@ -238,8 +264,8 @@ export default class ScrollPanel extends React.Component {
// note that this is independent of the 'stuckAtBottom' state - it is simply
// about whether the content is scrolled down right now, irrespective of
// whether it will stay that way when the children update.
isAtBottom = () => {
const sn = this._getScrollNode();
public isAtBottom = () => {
const sn = this.getScrollNode();
// fractional values (both too big and too small)
// for scrollTop happen on certain browsers/platforms
// when scrolled all the way down. E.g. Chrome 72 on debian.
@ -278,10 +304,10 @@ export default class ScrollPanel extends React.Component {
// |#########| - |
// |#########| |
// `---------' -
_getExcessHeight(backwards) {
const sn = this._getScrollNode();
const contentHeight = this._getMessagesHeight();
const listHeight = this._getListHeight();
private getExcessHeight(backwards: boolean): number {
const sn = this.getScrollNode();
const contentHeight = this.getMessagesHeight();
const listHeight = this.getListHeight();
const clippedHeight = contentHeight - listHeight;
const unclippedScrollTop = sn.scrollTop + clippedHeight;
@ -293,13 +319,13 @@ export default class ScrollPanel extends React.Component {
}
// check the scroll state and send out backfill requests if necessary.
checkFillState = async (depth=0) => {
public checkFillState = async (depth = 0): Promise<void> => {
if (this.unmounted) {
return;
}
const isFirstCall = depth === 0;
const sn = this._getScrollNode();
const sn = this.getScrollNode();
// if there is less than a screenful of messages above or below the
// viewport, try to get some more messages.
@ -330,17 +356,17 @@ export default class ScrollPanel extends React.Component {
// do make a note when a new request comes in while already running one,
// so we can trigger a new chain of calls once done.
if (isFirstCall) {
if (this._isFilling) {
debuglog("_isFilling: not entering while request is ongoing, marking for a subsequent request");
this._fillRequestWhileRunning = true;
if (this.isFilling) {
debuglog("isFilling: not entering while request is ongoing, marking for a subsequent request");
this.fillRequestWhileRunning = true;
return;
}
debuglog("_isFilling: setting");
this._isFilling = true;
debuglog("isFilling: setting");
this.isFilling = true;
}
const itemlist = this._itemlist.current;
const firstTile = itemlist && itemlist.firstElementChild;
const itemlist = this.itemlist.current;
const firstTile = itemlist && itemlist.firstElementChild as HTMLElement;
const contentTop = firstTile && firstTile.offsetTop;
const fillPromises = [];
@ -348,13 +374,13 @@ export default class ScrollPanel extends React.Component {
// try backward filling
if (!firstTile || (sn.scrollTop - contentTop) < sn.clientHeight) {
// need to back-fill
fillPromises.push(this._maybeFill(depth, true));
fillPromises.push(this.maybeFill(depth, true));
}
// if scrollTop gets to 2 screens from the end (so 1 screen below viewport),
// try forward filling
if ((sn.scrollHeight - sn.scrollTop) < sn.clientHeight * 2) {
// need to forward-fill
fillPromises.push(this._maybeFill(depth, false));
fillPromises.push(this.maybeFill(depth, false));
}
if (fillPromises.length) {
@ -365,26 +391,26 @@ export default class ScrollPanel extends React.Component {
}
}
if (isFirstCall) {
debuglog("_isFilling: clearing");
this._isFilling = false;
debuglog("isFilling: clearing");
this.isFilling = false;
}
if (this._fillRequestWhileRunning) {
this._fillRequestWhileRunning = false;
if (this.fillRequestWhileRunning) {
this.fillRequestWhileRunning = false;
this.checkFillState();
}
};
// check if unfilling is possible and send an unfill request if necessary
_checkUnfillState(backwards) {
let excessHeight = this._getExcessHeight(backwards);
private checkUnfillState(backwards: boolean): void {
let excessHeight = this.getExcessHeight(backwards);
if (excessHeight <= 0) {
return;
}
const origExcessHeight = excessHeight;
const tiles = this._itemlist.current.children;
const tiles = this.itemlist.current.children;
// The scroll token of the first/last tile to be unpaginated
let markerScrollToken = null;
@ -413,11 +439,11 @@ export default class ScrollPanel extends React.Component {
if (markerScrollToken) {
// Use a debouncer to prevent multiple unfill calls in quick succession
// This is to make the unfilling process less aggressive
if (this._unfillDebouncer) {
clearTimeout(this._unfillDebouncer);
if (this.unfillDebouncer) {
clearTimeout(this.unfillDebouncer);
}
this._unfillDebouncer = setTimeout(() => {
this._unfillDebouncer = null;
this.unfillDebouncer = setTimeout(() => {
this.unfillDebouncer = null;
debuglog("unfilling now", backwards, origExcessHeight);
this.props.onUnfillRequest(backwards, markerScrollToken);
}, UNFILL_REQUEST_DEBOUNCE_MS);
@ -425,9 +451,9 @@ export default class ScrollPanel extends React.Component {
}
// check if there is already a pending fill request. If not, set one off.
_maybeFill(depth, backwards) {
private maybeFill(depth: number, backwards: boolean): Promise<void> {
const dir = backwards ? 'b' : 'f';
if (this._pendingFillRequests[dir]) {
if (this.pendingFillRequests[dir]) {
debuglog("Already a "+dir+" fill in progress - not starting another");
return;
}
@ -436,7 +462,7 @@ export default class ScrollPanel extends React.Component {
// onFillRequest can end up calling us recursively (via onScroll
// events) so make sure we set this before firing off the call.
this._pendingFillRequests[dir] = true;
this.pendingFillRequests[dir] = true;
// wait 1ms before paginating, because otherwise
// this will block the scroll event handler for +700ms
@ -445,13 +471,13 @@ export default class ScrollPanel extends React.Component {
return new Promise(resolve => setTimeout(resolve, 1)).then(() => {
return this.props.onFillRequest(backwards);
}).finally(() => {
this._pendingFillRequests[dir] = false;
this.pendingFillRequests[dir] = false;
}).then((hasMoreResults) => {
if (this.unmounted) {
return;
}
// Unpaginate once filling is complete
this._checkUnfillState(!backwards);
this.checkUnfillState(!backwards);
debuglog(""+dir+" fill complete; hasMoreResults:"+hasMoreResults);
if (hasMoreResults) {
@ -477,7 +503,7 @@ export default class ScrollPanel extends React.Component {
* the number of pixels the bottom of the tracked child is above the
* bottom of the scroll panel.
*/
getScrollState = () => this.scrollState;
public getScrollState = (): IScrollState => this.scrollState;
/* reset the saved scroll state.
*
@ -491,35 +517,35 @@ export default class ScrollPanel extends React.Component {
* no use if no children exist yet, or if you are about to replace the
* child list.)
*/
resetScrollState = () => {
public resetScrollState = (): void => {
this.scrollState = {
stuckAtBottom: this.props.startAtBottom,
};
this._bottomGrowth = 0;
this._pages = 0;
this._scrollTimeout = new Timer(100);
this._heightUpdateInProgress = false;
this.bottomGrowth = 0;
this.pages = 0;
this.scrollTimeout = new Timer(100);
this.heightUpdateInProgress = false;
};
/**
* jump to the top of the content.
*/
scrollToTop = () => {
this._getScrollNode().scrollTop = 0;
this._saveScrollState();
public scrollToTop = (): void => {
this.getScrollNode().scrollTop = 0;
this.saveScrollState();
};
/**
* jump to the bottom of the content.
*/
scrollToBottom = () => {
public scrollToBottom = (): void => {
// the easiest way to make sure that the scroll state is correctly
// saved is to do the scroll, then save the updated state. (Calculating
// it ourselves is hard, and we can't rely on an onScroll callback
// happening, since there may be no user-visible change here).
const sn = this._getScrollNode();
const sn = this.getScrollNode();
sn.scrollTop = sn.scrollHeight;
this._saveScrollState();
this.saveScrollState();
};
/**
@ -527,18 +553,18 @@ export default class ScrollPanel extends React.Component {
*
* @param {number} mult: -1 to page up, +1 to page down
*/
scrollRelative = mult => {
const scrollNode = this._getScrollNode();
public scrollRelative = (mult: number): void => {
const scrollNode = this.getScrollNode();
const delta = mult * scrollNode.clientHeight * 0.9;
scrollNode.scrollBy(0, delta);
this._saveScrollState();
this.saveScrollState();
};
/**
* Scroll up/down in response to a scroll key
* @param {object} ev the keyboard event
*/
handleScrollKey = ev => {
public handleScrollKey = (ev: KeyboardEvent) => {
let isScrolling = false;
const roomAction = getKeyBindingsManager().getRoomAction(ev);
switch (roomAction) {
@ -575,17 +601,17 @@ export default class ScrollPanel extends React.Component {
* node (specifically, the bottom of it) will be positioned. If omitted, it
* defaults to 0.
*/
scrollToToken = (scrollToken, pixelOffset, offsetBase) => {
public scrollToToken = (scrollToken: string, pixelOffset: number, offsetBase: number): void => {
pixelOffset = pixelOffset || 0;
offsetBase = offsetBase || 0;
// set the trackedScrollToken so we can get the node through _getTrackedNode
// set the trackedScrollToken so we can get the node through getTrackedNode
this.scrollState = {
stuckAtBottom: false,
trackedScrollToken: scrollToken,
};
const trackedNode = this._getTrackedNode();
const scrollNode = this._getScrollNode();
const trackedNode = this.getTrackedNode();
const scrollNode = this.getScrollNode();
if (trackedNode) {
// set the scrollTop to the position we want.
// note though, that this might not succeed if the combination of offsetBase and pixelOffset
@ -595,34 +621,34 @@ export default class ScrollPanel extends React.Component {
// enough so it ends up in the top of the viewport.
debuglog("scrollToken: setting scrollTop", { offsetBase, pixelOffset, offsetTop: trackedNode.offsetTop });
scrollNode.scrollTop = (trackedNode.offsetTop - (scrollNode.clientHeight * offsetBase)) + pixelOffset;
this._saveScrollState();
this.saveScrollState();
}
};
_saveScrollState() {
private saveScrollState(): void {
if (this.props.stickyBottom && this.isAtBottom()) {
this.scrollState = { stuckAtBottom: true };
debuglog("saved stuckAtBottom state");
return;
}
const scrollNode = this._getScrollNode();
const scrollNode = this.getScrollNode();
const viewportBottom = scrollNode.scrollHeight - (scrollNode.scrollTop + scrollNode.clientHeight);
const itemlist = this._itemlist.current;
const itemlist = this.itemlist.current;
const messages = itemlist.children;
let node = null;
// TODO: do a binary search here, as items are sorted by offsetTop
// loop backwards, from bottom-most message (as that is the most common case)
for (let i = messages.length - 1; i >= 0; --i) {
if (!messages[i].dataset.scrollTokens) {
if (!(messages[i] as HTMLElement).dataset.scrollTokens) {
continue;
}
node = messages[i];
// break at the first message (coming from the bottom)
// that has it's offsetTop above the bottom of the viewport.
if (this._topFromBottom(node) > viewportBottom) {
if (this.topFromBottom(node) > viewportBottom) {
// Use this node as the scrollToken
break;
}
@ -634,7 +660,7 @@ export default class ScrollPanel extends React.Component {
}
const scrollToken = node.dataset.scrollTokens.split(',')[0];
debuglog("saving anchored scroll state to message", node && node.innerText, scrollToken);
const bottomOffset = this._topFromBottom(node);
const bottomOffset = this.topFromBottom(node);
this.scrollState = {
stuckAtBottom: false,
trackedNode: node,
@ -644,35 +670,35 @@ export default class ScrollPanel extends React.Component {
};
}
async _restoreSavedScrollState() {
private async restoreSavedScrollState(): Promise<void> {
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
const sn = this._getScrollNode();
const sn = this.getScrollNode();
if (sn.scrollTop !== sn.scrollHeight) {
sn.scrollTop = sn.scrollHeight;
}
} else if (scrollState.trackedScrollToken) {
const itemlist = this._itemlist.current;
const trackedNode = this._getTrackedNode();
const itemlist = this.itemlist.current;
const trackedNode = this.getTrackedNode();
if (trackedNode) {
const newBottomOffset = this._topFromBottom(trackedNode);
const newBottomOffset = this.topFromBottom(trackedNode);
const bottomDiff = newBottomOffset - scrollState.bottomOffset;
this._bottomGrowth += bottomDiff;
this.bottomGrowth += bottomDiff;
scrollState.bottomOffset = newBottomOffset;
const newHeight = `${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);
}
}
if (!this._heightUpdateInProgress) {
this._heightUpdateInProgress = true;
if (!this.heightUpdateInProgress) {
this.heightUpdateInProgress = true;
try {
await this._updateHeight();
await this.updateHeight();
} finally {
this._heightUpdateInProgress = false;
this.heightUpdateInProgress = false;
}
} else {
debuglog("not updating height because request already in progress");
@ -680,11 +706,11 @@ export default class ScrollPanel extends React.Component {
}
// need a better name that also indicates this will change scrollTop? Rebalance height? Reveal content?
async _updateHeight() {
private async updateHeight(): Promise<void> {
// wait until user has stopped scrolling
if (this._scrollTimeout.isRunning()) {
if (this.scrollTimeout.isRunning()) {
debuglog("updateHeight waiting for scrolling to end ... ");
await this._scrollTimeout.finished();
await this.scrollTimeout.finished();
} else {
debuglog("updateHeight getting straight to business, no scrolling going on.");
}
@ -694,14 +720,14 @@ export default class ScrollPanel extends React.Component {
return;
}
const sn = this._getScrollNode();
const itemlist = this._itemlist.current;
const contentHeight = this._getMessagesHeight();
const sn = this.getScrollNode();
const itemlist = this.itemlist.current;
const contentHeight = this.getMessagesHeight();
const minHeight = sn.clientHeight;
const height = Math.max(minHeight, contentHeight);
this._pages = Math.ceil(height / PAGE_SIZE);
this._bottomGrowth = 0;
const newHeight = `${this._getListHeight()}px`;
this.pages = Math.ceil(height / PAGE_SIZE);
this.bottomGrowth = 0;
const newHeight = `${this.getListHeight()}px`;
const scrollState = this.scrollState;
if (scrollState.stuckAtBottom) {
@ -713,7 +739,7 @@ export default class ScrollPanel extends React.Component {
}
debuglog("updateHeight to", newHeight);
} else if (scrollState.trackedScrollToken) {
const trackedNode = this._getTrackedNode();
const trackedNode = this.getTrackedNode();
// if the timeline has been reloaded
// this can be called before scrollToBottom or whatever has been called
// so don't do anything if the node has disappeared from
@ -735,17 +761,17 @@ export default class ScrollPanel extends React.Component {
}
}
_getTrackedNode() {
private getTrackedNode(): HTMLElement {
const scrollState = this.scrollState;
const trackedNode = scrollState.trackedNode;
if (!trackedNode || !trackedNode.parentElement) {
let node;
const messages = this._itemlist.current.children;
const messages = this.itemlist.current.children;
const scrollToken = scrollState.trackedScrollToken;
for (let i = messages.length-1; i >= 0; --i) {
const m = messages[i];
const m = messages[i] as HTMLElement;
// 'data-scroll-tokens' is a DOMString of comma-separated scroll tokens
// There might only be one scroll token
if (m.dataset.scrollTokens &&
@ -768,45 +794,45 @@ export default class ScrollPanel extends React.Component {
return scrollState.trackedNode;
}
_getListHeight() {
return this._bottomGrowth + (this._pages * PAGE_SIZE);
private getListHeight(): number {
return this.bottomGrowth + (this.pages * PAGE_SIZE);
}
_getMessagesHeight() {
const itemlist = this._itemlist.current;
const lastNode = itemlist.lastElementChild;
private getMessagesHeight(): number {
const itemlist = this.itemlist.current;
const lastNode = itemlist.lastElementChild as HTMLElement;
const lastNodeBottom = lastNode ? lastNode.offsetTop + lastNode.clientHeight : 0;
const firstNodeTop = itemlist.firstElementChild ? itemlist.firstElementChild.offsetTop : 0;
const firstNodeTop = itemlist.firstElementChild ? (itemlist.firstElementChild as HTMLElement).offsetTop : 0;
// 18 is itemlist padding
return lastNodeBottom - firstNodeTop + (18 * 2);
}
_topFromBottom(node) {
private topFromBottom(node: HTMLElement): number {
// current capped height - distance from top = distance from bottom of container to top of tracked element
return this._itemlist.current.clientHeight - node.offsetTop;
return this.itemlist.current.clientHeight - node.offsetTop;
}
/* get the DOM node which has the scrollTop property we care about for our
* message panel.
*/
_getScrollNode() {
private getScrollNode(): HTMLDivElement {
if (this.unmounted) {
// this shouldn't happen, but when it does, turn the NPE into
// something more meaningful.
throw new Error("ScrollPanel._getScrollNode called when unmounted");
throw new Error("ScrollPanel.getScrollNode called when unmounted");
}
if (!this._divScroll) {
if (!this.divScroll) {
// Likewise, we should have the ref by this point, but if not
// turn the NPE into something meaningful.
throw new Error("ScrollPanel._getScrollNode called before AutoHideScrollbar ref collected");
throw new Error("ScrollPanel.getScrollNode called before AutoHideScrollbar ref collected");
}
return this._divScroll;
return this.divScroll;
}
_collectScroll = divScroll => {
this._divScroll = divScroll;
private collectScroll = (divScroll: HTMLDivElement) => {
this.divScroll = divScroll;
};
/**
@ -814,15 +840,15 @@ export default class ScrollPanel extends React.Component {
anything below it changes, by calling updatePreventShrinking, to keep
the same minimum bottom offset, effectively preventing the timeline to shrink.
*/
preventShrinking = () => {
const messageList = this._itemlist.current;
public preventShrinking = (): void => {
const messageList = this.itemlist.current;
const tiles = messageList && messageList.children;
if (!messageList) {
return;
}
let lastTileNode;
for (let i = tiles.length - 1; i >= 0; i--) {
const node = tiles[i];
const node = tiles[i] as HTMLElement;
if (node.dataset.scrollTokens) {
lastTileNode = node;
break;
@ -841,8 +867,8 @@ export default class ScrollPanel extends React.Component {
};
/** Clear shrinking prevention. Used internally, and when the timeline is reloaded. */
clearPreventShrinking = () => {
const messageList = this._itemlist.current;
public clearPreventShrinking = (): void => {
const messageList = this.itemlist.current;
const balanceElement = messageList && messageList.parentElement;
if (balanceElement) balanceElement.style.paddingBottom = null;
this.preventShrinkingState = null;
@ -857,11 +883,11 @@ export default class ScrollPanel extends React.Component {
from the bottom of the marked tile grows larger than
what it was when marking.
*/
updatePreventShrinking = () => {
public updatePreventShrinking = (): void => {
if (this.preventShrinkingState) {
const sn = this._getScrollNode();
const sn = this.getScrollNode();
const scrollState = this.scrollState;
const messageList = this._itemlist.current;
const messageList = this.itemlist.current;
const { offsetNode, offsetFromBottom } = this.preventShrinkingState;
// element used to set paddingBottom to balance the typing notifs disappearing
const balanceElement = messageList.parentElement;
@ -898,13 +924,15 @@ export default class ScrollPanel extends React.Component {
// list-style-type: none; is no longer a list
return (
<AutoHideScrollbar
wrappedRef={this._collectScroll}
wrappedRef={this.collectScroll}
onScroll={this.onScroll}
onWheel={this.props.onUserScroll}
className={`mx_ScrollPanel ${this.props.className}`} style={this.props.style}>
className={`mx_ScrollPanel ${this.props.className}`}
style={this.props.style}
>
{ this.props.fixedChildren }
<div className="mx_RoomView_messageListWrapper">
<ol ref={this._itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
<ol ref={this.itemlist} className="mx_RoomView_MessageList" aria-live="polite" role="list">
{ this.props.children }
</ol>
</div>

View file

@ -112,12 +112,12 @@ const Tile: React.FC<ITileProps> = ({
ev.preventDefault();
ev.stopPropagation();
onViewRoomClick(false);
}
};
const onJoinClick = (ev: ButtonEvent) => {
ev.preventDefault();
ev.stopPropagation();
onViewRoomClick(true);
}
};
let button;
if (joinedRoom) {
@ -137,7 +137,7 @@ const Tile: React.FC<ITileProps> = ({
} else {
checkbox = <TextWithTooltip
tooltip={_t("You don't have permission")}
onClick={ev => { ev.stopPropagation() }}
onClick={ev => { ev.stopPropagation(); }}
>
<StyledCheckbox disabled={true} />
</TextWithTooltip>;
@ -340,7 +340,7 @@ export const HierarchyLevel = ({
</Tile>
))
}
</React.Fragment>
</React.Fragment>;
};
// mutate argument refreshToken to force a reload

View file

@ -37,7 +37,7 @@ import * as Email from "../../email";
import defaultDispatcher from "../../dispatcher/dispatcher";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
import ResizeNotifier from "../../utils/ResizeNotifier"
import ResizeNotifier from "../../utils/ResizeNotifier";
import MainSplit from './MainSplit';
import ErrorBoundary from "../views/elements/ErrorBoundary";
import { ActionPayload } from "../../dispatcher/payloads";
@ -162,7 +162,7 @@ const SpaceInfo = ({ space }) => {
</AccessibleButton>
) : null}
</RoomMemberCount> }
</div>
</div>;
};
const onBetaClick = () => {
@ -592,14 +592,14 @@ const SpaceSetupPrivateScope = ({ space, justCreatedOpts, onFinished }) => {
<AccessibleButton
className="mx_SpaceRoomView_privateScope_justMeButton"
onClick={() => { onFinished(false) }}
onClick={() => { onFinished(false); }}
>
<h3>{ _t("Just me") }</h3>
<div>{ _t("A private space to organise your rooms") }</div>
</AccessibleButton>
<AccessibleButton
className="mx_SpaceRoomView_privateScope_meAndMyTeammatesButton"
onClick={() => { onFinished(true) }}
onClick={() => { onFinished(true); }}
>
<h3>{ _t("Me and my teammates") }</h3>
<div>{ _t("A private space for you and your teammates") }</div>
@ -686,7 +686,7 @@ const SpaceSetupPrivateInvite = ({ space, onFinished }) => {
let buttonLabel = _t("Skip for now");
if (emailAddresses.some(name => name.trim())) {
onClick = onNextClick;
buttonLabel = busy ? _t("Inviting...") : _t("Continue")
buttonLabel = busy ? _t("Inviting...") : _t("Continue");
}
return <div className="mx_SpaceRoomView_inviteTeammates">

View file

@ -123,7 +123,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
private onRoom = (room: Room): void => {
this.removePendingJoinRoom(room.roomId);
}
};
private onTagStoreUpdate = () => {
this.forceUpdate(); // we don't have anything useful in state to update
@ -185,7 +185,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
if (this.state.pendingRoomJoin.delete(roomId)) {
this.setState({
pendingRoomJoin: new Set<string>(this.state.pendingRoomJoin),
})
});
}
}
@ -357,7 +357,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
),
})}
</div>
)
);
} else if (hostSignupConfig) {
if (hostSignupConfig && hostSignupConfig.url) {
// If hostSignup.domains is set to a non-empty array, only show
@ -509,7 +509,7 @@ export default class UserMenu extends React.Component<IProps, IState> {
/>
</IconizedContextMenuOptionList>
</React.Fragment>
)
);
} else if (MatrixClientPeg.get().isGuest()) {
primaryOptionList = (
<React.Fragment>

View file

@ -501,9 +501,9 @@ export default class LoginComponent extends React.PureComponent<IProps, IState>
return <React.Fragment>
{ flows.map(flow => {
const stepRenderer = this.stepRendererMap[flow.type];
return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>
return <React.Fragment key={flow.type}>{ stepRenderer() }</React.Fragment>;
}) }
</React.Fragment>
</React.Fragment>;
}
private renderPasswordStep = () => {

View file

@ -267,7 +267,7 @@ export default class Registration extends React.Component<IProps, IState> {
session_id: sessionId,
}),
);
}
};
private onUIAuthFinished = async (success: boolean, response: any) => {
if (!success) {
@ -487,7 +487,13 @@ export default class Registration extends React.Component<IProps, IState> {
fragmentAfterLogin={this.props.fragmentAfterLogin}
/>
<h3 className="mx_AuthBody_centered">
{ _t("%(ssoButtons)s Or %(usernamePassword)s", { ssoButtons: "", usernamePassword: ""}).trim() }
{_t(
"%(ssoButtons)s Or %(usernamePassword)s",
{
ssoButtons: "",
usernamePassword: "",
},
).trim()}
</h3>
</React.Fragment>;
}

View file

@ -354,7 +354,6 @@ export class TermsAuthEntry extends React.Component<ITermsAuthEntryProps, ITerms
CountlyAnalytics.instance.track("onboarding_terms_begin");
}
componentDidMount() {
this.props.onPhaseChange(DEFAULT_PHASE);
}

View file

@ -322,7 +322,7 @@ export default class PasswordLogin extends React.PureComponent<IProps, IState> {
const result = await this.validatePasswordRules(fieldState);
this.markFieldValid(LoginField.Password, result.valid);
return result;
}
};
private renderLoginField(loginType: IState["loginType"], autoFocus: boolean) {
const classes = {

View file

@ -49,7 +49,7 @@ const WidgetAvatar: React.FC<IProps> = ({ app, className, width = 20, height = 2
width={width}
height={height}
/>
)
);
};
export default WidgetAvatar;

View file

@ -42,13 +42,13 @@ export default class CallContextMenu extends React.Component<IProps> {
onHoldClick = () => {
this.props.call.setRemoteOnHold(true);
this.props.onFinished();
}
};
onUnholdClick = () => {
CallHandler.sharedInstance().setActiveCallRoomId(this.props.call.roomId);
this.props.onFinished();
}
};
onTransferClick = () => {
Modal.createTrackedDialog(
@ -56,7 +56,7 @@ export default class CallContextMenu extends React.Component<IProps> {
/*className=*/null, /*isPriority=*/false, /*isStatic=*/true,
);
this.props.onFinished();
}
};
render() {
const holdUnholdCaption = this.props.call.isRemoteOnHold() ? _t("Resume") : _t("Hold");

View file

@ -37,18 +37,17 @@ export default class DialpadContextMenu extends React.Component<IProps, IState>
this.state = {
value: '',
}
};
}
onDigitPress = (digit) => {
this.props.call.sendDtmfDigit(digit);
this.setState({ value: this.state.value + digit });
}
};
onChange = (ev) => {
this.setState({ value: ev.target.value });
}
};
render() {
return <ContextMenu {...this.props}>

View file

@ -23,7 +23,6 @@ import {replaceableComponent} from "../../../utils/replaceableComponent";
* menu.
*/
@replaceableComponent("views.context_menus.GenericElementContextMenu")
export default class GenericElementContextMenu extends React.Component {
static propTypes = {

View file

@ -207,7 +207,7 @@ export default class MessageContextMenu extends React.Component {
this.closeMenu();
};
onPermalinkClick = (e: Event) => {
onPermalinkClick = (e) => {
e.preventDefault();
const ShareDialog = sdk.getComponent("dialogs.ShareDialog");
Modal.createTrackedDialog('share room message dialog', '', ShareDialog, {

View file

@ -68,7 +68,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
private onCancel = (): void => {
this.props.onFinished(false);
}
};
private onSubmit = (): void => {
if ((!this.state.text || !this.state.text.trim()) && (!this.state.issueUrl || !this.state.issueUrl.trim())) {
@ -110,7 +110,7 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
});
}
});
}
};
private onDownload = async (): Promise<void> => {
this.setState({ downloadBusy: true });
@ -139,25 +139,25 @@ export default class BugReportDialog extends React.Component<IProps, IState> {
private onTextChange = (ev: React.FormEvent<HTMLTextAreaElement>): void => {
this.setState({ text: ev.currentTarget.value });
}
};
private onIssueUrlChange = (ev: React.FormEvent<HTMLInputElement>): void => {
this.setState({ issueUrl: ev.currentTarget.value });
}
};
private sendProgressCallback = (progress: string): void => {
if (this.unmounted) {
return;
}
this.setState({ progress });
}
};
private downloadProgressCallback = (downloadProgress: string): void => {
if (this.unmounted) {
return;
}
this.setState({ downloadProgress });
}
};
public render() {
const Loader = sdk.getComponent("elements.Spinner");

View file

@ -93,7 +93,6 @@ export default class ChangelogDialog extends React.Component<IProps> {
</div>
);
return (
<QuestionDialog
title={_t("Changelog")}

View file

@ -175,7 +175,7 @@ export default class CreateCommunityPrototypeDialog extends React.PureComponent<
let preview = <img src={this.state.avatarPreview} className="mx_CreateCommunityPrototypeDialog_avatar" />;
if (!this.state.avatarPreview) {
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />
preview = <div className="mx_CreateCommunityPrototypeDialog_placeholderAvatar" />;
}
return (

View file

@ -62,13 +62,13 @@ abstract class GenericEditor<
} else {
this.props.onBack();
}
}
};
protected onChange = (e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
// @ts-ignore: Unsure how to convince TS this is okay when the state
// type can be extended.
this.setState({ [e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value });
}
};
protected abstract send();
@ -158,7 +158,7 @@ export class SendCustomEvent extends GenericEditor<ISendCustomEventProps, ISendC
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
}
this.setState({ message });
}
};
render() {
if (this.state.message) {
@ -264,7 +264,7 @@ class SendAccountData extends GenericEditor<ISendAccountDataProps, ISendAccountD
message = _t('Failed to send custom event.') + ' (' + e.toString() + ')';
}
this.setState({ message });
}
};
render() {
if (this.state.message) {
@ -442,19 +442,19 @@ class RoomStateExplorer extends React.PureComponent<IExplorerProps, IRoomStateEx
} else {
this.props.onBack();
}
}
};
private editEv = () => {
this.setState({ editing: true });
}
};
private onQueryEventType = (filterEventType: string) => {
this.setState({ queryEventType: filterEventType });
}
};
private onQueryStateKey = (filterStateKey: string) => {
this.setState({ queryStateKey: filterStateKey });
}
};
render() {
if (this.state.event) {
@ -570,19 +570,19 @@ class AccountDataExplorer extends React.PureComponent<IExplorerProps, IAccountDa
} else {
this.props.onBack();
}
}
};
private onChange = (e: ChangeEvent<HTMLInputElement>) => {
this.setState({ [e.target.id]: e.target.type === 'checkbox' ? e.target.checked : e.target.value });
}
};
private editEv = () => {
this.setState({ editing: true });
}
};
private onQueryEventType = (queryEventType: string) => {
this.setState({ queryEventType });
}
};
render() {
if (this.state.event) {
@ -676,7 +676,7 @@ class ServersInRoomList extends React.PureComponent<IExplorerProps, IServersInRo
private onQuery = (query: string) => {
this.setState({ query });
}
};
render() {
return <div>
@ -739,7 +739,7 @@ const VerificationRequestExplorer: React.FC<{
<dd>{JSON.stringify(request.observeOnly)}</dd>
</dl>
</div>);
}
};
class VerificationExplorer extends React.PureComponent<IExplorerProps> {
static getLabel() {
@ -751,7 +751,7 @@ class VerificationExplorer extends React.PureComponent<IExplorerProps> {
private onNewRequest = () => {
this.forceUpdate();
}
};
componentDidMount() {
const cli = this.context;
@ -1221,11 +1221,11 @@ export default class DevtoolsDialog extends React.PureComponent<IProps, IState>
private onBack = () => {
this.setState({ mode: null });
}
};
private onCancel = () => {
this.props.onFinished(false);
}
};
render() {
let body;

View file

@ -122,7 +122,7 @@ export default class EditCommunityPrototypeDialog extends React.PureComponent<IP
const url = mediaFromMxc(this.state.currentAvatarUrl).srcHttp;
preview = <img src={url} className="mx_EditCommunityPrototypeDialog_avatar" />;
} else {
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />
preview = <div className="mx_EditCommunityPrototypeDialog_placeholderAvatar" />;
}
}

View file

@ -30,7 +30,6 @@ const existingIssuesUrl = "https://github.com/vector-im/element-web/issues" +
"?q=is%3Aopen+is%3Aissue+sort%3Areactions-%2B1-desc";
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
export default (props) => {
const [rating, setRating] = useState("");
const [comment, setComment] = useState("");

View file

@ -86,7 +86,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
case PostmessageAction.CloseDialog:
return this.closeDialog();
}
}
};
private maximizeDialog = () => {
this.setState({
@ -96,7 +96,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
this.sendMessage({
action: PostmessageAction.Maximize,
});
}
};
private minimizeDialog = () => {
this.setState({
@ -106,7 +106,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
this.sendMessage({
action: PostmessageAction.Minimize,
});
}
};
private closeDialog = async () => {
window.removeEventListener("message", this.messageHandler);
@ -114,7 +114,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
PersistedElement.destroyElement("host_signup");
// Finally clear the flag in
return HostSignupStore.instance.setHostSignupActive(false);
}
};
private onCloseClick = async () => {
if (this.state.completed) {
@ -137,16 +137,16 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
},
);
}
}
};
private sendMessage = (message: IPostmessageResponseData) => {
this.iframeRef.current.contentWindow.postMessage(message, this.config.url);
}
};
private async sendAccountDetails() {
const openIdToken = await MatrixClientPeg.get().getOpenIdToken();
if (!openIdToken || !openIdToken.access_token) {
console.warn("Failed to connect to homeserver for OpenID token.")
console.warn("Failed to connect to homeserver for OpenID token.");
this.setState({
completed: true,
error: _t("Failed to connect to your homeserver. Please close this dialog and try again."),
@ -171,7 +171,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
return this.sendAccountDetails();
}
return this.closeDialog();
}
};
private onAccountDetailsRequest = () => {
const textComponent = (
@ -215,7 +215,7 @@ export default class HostSignupDialog extends React.PureComponent<IProps, IState
onFinished: this.onAccountDetailsDialogFinished,
},
);
}
};
public componentDidMount() {
window.addEventListener("message", this.messageHandler);

View file

@ -427,7 +427,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
private onConsultFirstChange = (ev) => {
this.setState({ consultFirst: ev.target.checked });
}
};
public static buildRecents(excludedTargetIds: Set<string>): IRecentUser[] {
const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room
@ -696,7 +696,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
return roomOptions;
},
{ invite: [], invite_3pid: [] },
)
);
}
await createRoom(createRoomOptions);
@ -729,7 +729,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
}
try {
const result = await inviteMultipleToRoom(this.props.roomId, targetIds)
const result = await inviteMultipleToRoom(this.props.roomId, targetIds);
CountlyAnalytics.instance.trackSendInvite(startTime, this.props.roomId, targetIds.length);
if (!this.shouldAbortAfterInviteError(result, room)) { // handles setting error message too
this.props.onFinished();
@ -1365,7 +1365,7 @@ export default class InviteDialog extends React.PureComponent<IInviteDialogProps
<div />
</AccessibleTooltipButton>
</div>
</div>
</div>;
} else if (this.props.kind === KIND_INVITE) {
const room = MatrixClientPeg.get()?.getRoom(this.props.roomId);
const isSpace = SettingsStore.getValue("feature_spaces") && room?.isSpaceRoom();

View file

@ -102,7 +102,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
private onWidgetClose = (ev: CustomEvent<IModalWidgetCloseRequest>) => {
this.props.onFinished(true, ev.detail.data);
}
};
private onButtonEnableToggle = (ev: CustomEvent<ISetModalButtonEnabledActionRequest>) => {
ev.preventDefault();
@ -160,7 +160,7 @@ export default class ModalWidgetDialog extends React.PureComponent<IProps, IStat
break;
case ModalButtonKind.Secondary:
kind = "primary_outline";
break
break;
case ModalButtonKind.Danger:
kind = "danger";
break;

View file

@ -40,7 +40,6 @@ interface IState {
nature?: EXTENDED_NATURE;
}
const MODERATED_BY_STATE_EVENT_TYPE = [
"org.matrix.msc3215.room.moderation.moderated_by",
/**
@ -75,7 +74,7 @@ type Moderation = {
moderationRoomId: string;
// The id of the bot in charge of forwarding abuse reports to the moderation room.
moderationBotUserId: string;
}
};
/*
* A dialog for reporting an event.
*

View file

@ -85,7 +85,7 @@ export default class ServerOfflineDialog extends React.PureComponent<IProps> {
{entries}
</div>
</div>
)
);
});
}

View file

@ -24,7 +24,6 @@ import { _t } from '../../../languageHandler';
import Modal from '../../../Modal';
import { replaceableComponent } from "../../../utils/replaceableComponent";
/*
* Prompt the user to set an email address.
*

View file

@ -63,11 +63,11 @@ export default class TabbedIntegrationManagerDialog extends React.Component {
};
}
componentDidMount(): void {
componentDidMount() {
this.openManager(0, true);
}
openManager = async (i: number, force = false) => {
openManager = async (i, force = false) => {
if (i === this.state.currentIndex && !force) return;
const manager = this.state.managers[i];

View file

@ -31,7 +31,7 @@ interface ITermsCheckboxProps {
class TermsCheckbox extends React.PureComponent<ITermsCheckboxProps> {
private onChange = (ev: React.FormEvent<HTMLInputElement>): void => {
this.props.onChange(this.props.url, ev.currentTarget.checked);
}
};
render() {
return <input type="checkbox"
@ -80,11 +80,11 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
private onCancelClick = (): void => {
this.props.onFinished(false);
}
};
private onNextClick = (): void => {
this.props.onFinished(true, Object.keys(this.state.agreedUrls).filter((url) => this.state.agreedUrls[url]));
}
};
private nameForServiceType(serviceType: SERVICE_TYPES, host: string): JSX.Element {
switch (serviceType) {
@ -114,7 +114,7 @@ export default class TermsDialog extends React.PureComponent<ITermsDialogProps,
this.setState({
agreedUrls: Object.assign({}, this.state.agreedUrls, { [url]: checked }),
});
}
};
public render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -36,7 +36,7 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
static defaultProps = {
totalFiles: 1,
}
};
constructor(props) {
super(props);
@ -56,15 +56,15 @@ export default class UploadConfirmDialog extends React.Component<IProps> {
private onCancelClick = () => {
this.props.onFinished(false);
}
};
private onUploadClick = () => {
this.props.onFinished(true);
}
};
private onUploadAllClick = () => {
this.props.onFinished(true, true);
}
};
render() {
const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog');

View file

@ -79,7 +79,7 @@ export default class UserSettingsDialog extends React.Component<IProps, IState>
private mjolnirChanged: CallbackFn = (settingName, roomId, atLevel, newValue) => {
// We can cheat because we know what levels a feature is tracked at, and how it is tracked
this.setState({ mjolnirEnabled: newValue });
}
};
_getTabs() {
const tabs = [];

View file

@ -169,7 +169,7 @@ export default class AccessSecretStorageDialog extends React.PureComponent<IProp
private onRecoveryKeyFileUploadClick = () => {
this.fileUpload.current.click();
}
};
private onPassPhraseNext = async (ev: FormEvent<HTMLFormElement>) => {
ev.preventDefault();

View file

@ -16,6 +16,8 @@ limitations under the License.
*/
import React from 'react';
import { CrossSigningKeys } from 'matrix-js-sdk/src/client';
import { MatrixClientPeg } from '../../../../MatrixClientPeg';
import { _t } from '../../../../languageHandler';
import Modal from '../../../../Modal';
@ -71,7 +73,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent<IProps
private async queryKeyUploadAuth(): Promise<void> {
try {
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {});
await MatrixClientPeg.get().uploadDeviceSigningKeys(null, {} as CrossSigningKeys);
// We should never get here: the server should always require
// UI auth to upload device signing keys. If we do, we upload
// no keys which would be a no-op.
@ -139,7 +141,7 @@ export default class CreateCrossSigningDialog extends React.PureComponent<IProps
throw new Error("Cross-signing key upload auth canceled");
}
}
}
};
private bootstrapCrossSigning = async (): Promise<void> => {
this.setState({
@ -163,11 +165,11 @@ export default class CreateCrossSigningDialog extends React.PureComponent<IProps
this.setState({ error: e });
console.error("Error bootstrapping cross-signing", e);
}
}
};
private onCancel = (): void => {
this.props.onFinished(false);
}
};
render() {
let content;

View file

@ -216,7 +216,9 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
if (removableServers.has(server)) {
const onClick = async () => {
closeMenu();
const {finished} = Modal.createTrackedDialog("Network Dropdown", "Remove server", QuestionDialog, {
const { finished } = Modal.createTrackedDialog(
"Network Dropdown", "Remove server", QuestionDialog,
{
title: _t("Are you sure?"),
description: _t("Are you sure you want to remove <b>%(serverName)s</b>", {
serverName: server,
@ -225,7 +227,9 @@ const NetworkDropdown = ({ onOptionChange, protocols = {}, selectedServerName, s
}),
button: _t("Remove"),
fixedWidth: false,
}, "mx_NetworkDropdown_dialog");
},
"mx_NetworkDropdown_dialog",
);
const [ok] = await finished;
if (!ok) return;

View file

@ -62,8 +62,6 @@ export default class ActionButton extends React.Component {
};
render() {
const TintableSvg = sdk.getComponent("elements.TintableSvg");
let tooltip;
if (this.state.showTooltip) {
const Tooltip = sdk.getComponent("elements.Tooltip");
@ -71,7 +69,7 @@ export default class ActionButton extends React.Component {
}
const icon = this.props.iconPath ?
(<TintableSvg src={this.props.iconPath} width={this.props.size} height={this.props.size} />) :
(<img src={this.props.iconPath} width={this.props.size} height={this.props.size} />) :
undefined;
const classNames = ["mx_RoleButton"];

View file

@ -53,7 +53,6 @@ export default class AddressTile extends React.Component {
}
const BaseAvatar = sdk.getComponent('avatars.BaseAvatar');
const TintableSvg = sdk.getComponent("elements.TintableSvg");
const nameClasses = classNames({
"mx_AddressTile_name": true,
@ -124,7 +123,7 @@ export default class AddressTile extends React.Component {
if (this.props.canDismiss) {
dismiss = (
<div className="mx_AddressTile_dismiss" onClick={this.props.onDismissed} >
<TintableSvg src={require("../../../../res/img/icon-address-delete.svg")} width="9" height="9" />
<img src={require("../../../../res/img/icon-address-delete.svg")} width="9" height="9" />
</div>
);
}

View file

@ -22,7 +22,6 @@ import dis from "../../../dispatcher/dispatcher";
import { Action } from "../../../dispatcher/actions";
import { UserTab } from "../dialogs/UserSettingsDialog";
export enum WarningKind {
Files,
Search,

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import { _t } from '../../../languageHandler';
import BaseDialog from "..//dialogs/BaseDialog"
import BaseDialog from "..//dialogs/BaseDialog";
import AccessibleButton from './AccessibleButton';
import { getDesktopCapturerSources } from "matrix-js-sdk/src/webrtc/call";
import { replaceableComponent } from "../../../utils/replaceableComponent";
@ -44,7 +44,7 @@ export class ExistingSource extends React.Component<DesktopCapturerSourceIProps>
onClick = (ev) => {
this.props.onSelect(this.props.source);
}
};
render() {
return (
@ -108,19 +108,19 @@ export default class DesktopCapturerSourcePicker extends React.Component<
onSelect = (source) => {
this.props.onFinished(source);
}
};
onScreensClick = (ev) => {
this.setState({ selectedTab: Tabs.Screens });
}
};
onWindowsClick = (ev) => {
this.setState({ selectedTab: Tabs.Windows });
}
};
onCloseClick = (ev) => {
this.props.onFinished(null);
}
};
render() {
let sources;

View file

@ -144,7 +144,6 @@ EditableTextContainer.propTypes = {
blurToSubmit: PropTypes.bool,
};
EditableTextContainer.defaultProps = {
initialValue: "",
placeholder: "",

View file

@ -17,7 +17,7 @@
import React, { FunctionComponent, useEffect, useRef } from 'react';
import dis from '../../../dispatcher/dispatcher';
import ICanvasEffect from '../../../effects/ICanvasEffect';
import { CHAT_EFFECTS } from '../../../effects'
import { CHAT_EFFECTS } from '../../../effects';
import UIStore, { UI_EVENTS } from "../../../stores/UIStore";
interface IProps {
@ -32,7 +32,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
if (!name) return null;
let effect: ICanvasEffect | null = effectsRef.current[name] || null;
if (effect === null) {
const options = CHAT_EFFECTS.find((e) => e.command === name)?.options
const options = CHAT_EFFECTS.find((e) => e.command === name)?.options;
try {
const { default: Effect } = await import(`../../../effects/${name}`);
effect = new Effect(options);
@ -56,7 +56,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
const effect = payload.action.substr(actionPrefix.length);
lazyLoadEffectModule(effect).then((module) => module?.start(canvasRef.current));
}
}
};
const dispatcherRef = dis.register(onAction);
const canvas = canvasRef.current;
canvas.height = UIStore.instance.windowHeight;
@ -89,7 +89,7 @@ const EffectsOverlay: FunctionComponent<IProps> = ({ roomWidth }) => {
right: 0,
}}
/>
)
}
);
};
export default EffectsOverlay;

View file

@ -1,5 +1,5 @@
/*
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019 - 2021 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.
@ -14,21 +14,27 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React from 'react';
import * as sdk from '../../../index';
import React, { ErrorInfo } from 'react';
import { _t } from '../../../languageHandler';
import { MatrixClientPeg } from '../../../MatrixClientPeg';
import PlatformPeg from '../../../PlatformPeg';
import Modal from '../../../Modal';
import SdkConfig from "../../../SdkConfig";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import BugReportDialog from '../dialogs/BugReportDialog';
import AccessibleButton from './AccessibleButton';
interface IState {
error: Error;
}
/**
* This error boundary component can be used to wrap large content areas and
* catch exceptions during rendering in the component tree below them.
*/
@replaceableComponent("views.elements.ErrorBoundary")
export default class ErrorBoundary extends React.PureComponent {
export default class ErrorBoundary extends React.PureComponent<{}, IState> {
constructor(props) {
super(props);
@ -37,13 +43,13 @@ export default class ErrorBoundary extends React.PureComponent {
};
}
static getDerivedStateFromError(error) {
static getDerivedStateFromError(error: Error): Partial<IState> {
// Side effects are not permitted here, so we only update the state so
// that the next render shows an error message.
return { error };
}
componentDidCatch(error, { componentStack }) {
componentDidCatch(error: Error, { componentStack }: ErrorInfo): void {
// Browser consoles are better at formatting output when native errors are passed
// in their own `console.error` invocation.
console.error(error);
@ -53,7 +59,7 @@ export default class ErrorBoundary extends React.PureComponent {
);
}
_onClearCacheAndReload = () => {
private onClearCacheAndReload = (): void => {
if (!PlatformPeg.get()) return;
MatrixClientPeg.get().stopClient();
@ -62,11 +68,7 @@ export default class ErrorBoundary extends React.PureComponent {
});
};
_onBugReport = () => {
const BugReportDialog = sdk.getComponent("dialogs.BugReportDialog");
if (!BugReportDialog) {
return;
}
private onBugReport = (): void => {
Modal.createTrackedDialog('Bug Report Dialog', '', BugReportDialog, {
label: 'react-soft-crash',
});
@ -74,7 +76,6 @@ export default class ErrorBoundary extends React.PureComponent {
render() {
if (this.state.error) {
const AccessibleButton = sdk.getComponent('elements.AccessibleButton');
const newIssueUrl = "https://github.com/vector-im/element-web/issues/new";
let bugReportSection;
@ -95,7 +96,7 @@ export default class ErrorBoundary extends React.PureComponent {
"the rooms or groups you have visited and the usernames of " +
"other users. They do not contain messages.",
)}</p>
<AccessibleButton onClick={this._onBugReport} kind='primary'>
<AccessibleButton onClick={this.onBugReport} kind='primary'>
{_t("Submit debug logs")}
</AccessibleButton>
</React.Fragment>;
@ -105,7 +106,7 @@ export default class ErrorBoundary extends React.PureComponent {
<div className="mx_ErrorBoundary_body">
<h1>{_t("Something went wrong!")}</h1>
{ bugReportSection }
<AccessibleButton onClick={this._onClearCacheAndReload} kind='danger'>
<AccessibleButton onClick={this.onClearCacheAndReload} kind='danger'>
{_t("Clear cache and reload")}
</AccessibleButton>
</div>

View file

@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, {ReactChildren, useEffect} from 'react';
import React, { ReactNode, useEffect } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -31,11 +31,11 @@ interface IProps {
// Whether or not to begin with state.expanded=true
startExpanded?: boolean,
// The list of room members for which to show avatars next to the summary
summaryMembers?: RoomMember[],
summaryMembers?: RoomMember[];
// The text to show as the summary of this event list
summaryText?: string,
summaryText?: string;
// An array of EventTiles to render when expanded
children: ReactChildren,
children: ReactNode[];
// Called when the event list expansion is toggled
onToggle?(): void;
}

View file

@ -22,7 +22,6 @@ import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { mediaFromMxc } from "../../../customisations/Media";
class FlairAvatar extends React.Component {
constructor() {
super();

View file

@ -30,7 +30,7 @@ import SettingsStore from "../../../settings/SettingsStore";
import { formatFullDate } from "../../../DateUtils";
import dis from '../../../dispatcher/dispatcher';
import { replaceableComponent } from "../../../utils/replaceableComponent";
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"
import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { normalizeWheelEvent } from "../../../utils/Mouse";
@ -116,7 +116,7 @@ export default class ImageView extends React.Component<IProps, IState> {
private recalculateZoom = () => {
this.setZoomAndRotation();
}
};
private setZoomAndRotation = (inputRotation?: number) => {
const image = this.image.current;
@ -158,7 +158,7 @@ export default class ImageView extends React.Component<IProps, IState> {
rotation: rotation,
zoom: zoom,
});
}
};
private zoom(delta: number) {
const newZoom = this.state.zoom + delta;

View file

@ -29,7 +29,7 @@ export default class InlineSpinner extends React.PureComponent<IProps> {
static defaultProps = {
w: 16,
h: 16,
}
};
render() {
return (

View file

@ -42,7 +42,7 @@ export default class InviteReason extends React.PureComponent<IProps, IState> {
this.setState({
hidden: false,
});
}
};
render() {
const classes = classNames({

View file

@ -16,7 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { ReactChildren } from 'react';
import React, { ComponentProps } from 'react';
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
@ -26,21 +26,11 @@ import { isValid3pidInvite } from "../../../RoomInvite";
import EventListSummary from "./EventListSummary";
import { replaceableComponent } from "../../../utils/replaceableComponent";
interface IProps {
// An array of member events to summarise
events: MatrixEvent[];
interface IProps extends Omit<ComponentProps<typeof EventListSummary>, "summaryText" | "summaryMembers"> {
// The maximum number of names to show in either each summary e.g. 2 would result "A, B and 234 others left"
summaryLength?: number;
// The maximum number of avatars to display in the summary
avatarsMaxLength?: number;
// The minimum number of events needed to trigger summarisation
threshold?: number,
// Whether or not to begin with state.expanded=true
startExpanded?: boolean,
// An array of EventTiles to render when expanded
children: ReactChildren;
// Called when the MELS expansion is toggled
onToggle?(): void,
}
interface IUserEvents {
@ -66,6 +56,7 @@ enum TransitionType {
ChangedName = "changed_name",
ChangedAvatar = "changed_avatar",
NoChange = "no_change",
ServerAcl = "server_acl",
}
const SEP = ",";
@ -298,6 +289,12 @@ export default class MemberEventListSummary extends React.Component<IProps> {
? _t("%(severalUsers)smade no changes %(count)s times", { severalUsers: "", count: repeats })
: _t("%(oneUser)smade no changes %(count)s times", { oneUser: "", count: repeats });
break;
case "server_acl":
res = (userCount > 1)
? _t("%(severalUsers)schanged the server ACLs %(count)s times",
{ severalUsers: "", count: repeats })
: _t("%(oneUser)schanged the server ACLs %(count)s times", { oneUser: "", count: repeats });
break;
}
return res;
@ -324,6 +321,10 @@ export default class MemberEventListSummary extends React.Component<IProps> {
return TransitionType.Invited;
}
if (e.mxEvent.getType() === 'm.room.server_acl') {
return TransitionType.ServerAcl;
}
switch (e.mxEvent.getContent().membership) {
case 'invite': return TransitionType.Invited;
case 'ban': return TransitionType.Banned;
@ -410,19 +411,23 @@ export default class MemberEventListSummary extends React.Component<IProps> {
// Object mapping user IDs to an array of IUserEvents
const userEvents: Record<string, IUserEvents[]> = {};
eventsToRender.forEach((e, index) => {
const userId = e.getStateKey();
const userId = e.getType() === 'm.room.server_acl' ? e.getSender() : e.getStateKey();
// Initialise a user's events
if (!userEvents[userId]) {
userEvents[userId] = [];
}
if (e.target) {
if (e.getType() === 'm.room.server_acl') {
latestUserAvatarMember.set(userId, e.sender);
} else if (e.target) {
latestUserAvatarMember.set(userId, e.target);
}
let displayName = userId;
if (e.getType() === 'm.room.third_party_invite') {
displayName = e.getContent().display_name;
} else if (e.getType() === 'm.room.server_acl') {
displayName = e.sender.name;
} else if (e.target) {
displayName = e.target.name;
}

View file

@ -187,4 +187,4 @@ export default class PersistedElement extends React.Component {
}
}
export const getPersistKey = (appId: string) => 'widget_' + appId;
export const getPersistKey = (appId) => 'widget_' + appId;

View file

@ -48,7 +48,7 @@ const getIcon = (brand: IdentityProviderBrand | string) => {
default:
return null;
}
}
};
const SSOButton: React.FC<ISSOButtonProps> = ({
matrixClient,

View file

@ -87,7 +87,7 @@ const ServerPicker = ({ title, dialogTitle, serverConfig, onServerConfigChange }
<span className="mx_ServerPicker_server">{serverName}</span>
{ editBtn }
{ desc }
</div>
}
</div>;
};
export default ServerPicker;

View file

@ -16,7 +16,7 @@ limitations under the License.
import React from 'react';
import Dropdown from "../../views/elements/Dropdown"
import Dropdown from "../../views/elements/Dropdown";
import * as sdk from '../../../index';
import PlatformPeg from "../../../PlatformPeg";
import SettingsStore from "../../../settings/SettingsStore";
@ -67,8 +67,8 @@ export default class SpellCheckLanguagesDropdown extends React.Component<SpellCh
langs.push({
label: language,
value: language,
})
})
});
});
this.setState({ languages: langs });
}).catch((e) => {
this.setState({ languages: ['en'] });

View file

@ -1,82 +0,0 @@
/*
Copyright 2015 OpenMarket Ltd
Copyright 2019 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 React from 'react';
import PropTypes from 'prop-types';
import Tinter from "../../../Tinter";
import {replaceableComponent} from "../../../utils/replaceableComponent";
@replaceableComponent("views.elements.TintableSvg")
class TintableSvg extends React.Component {
static propTypes = {
src: PropTypes.string.isRequired,
width: PropTypes.string.isRequired,
height: PropTypes.string.isRequired,
className: PropTypes.string,
};
// list of currently mounted TintableSvgs
static mounts = {};
static idSequence = 0;
componentDidMount() {
this.fixups = [];
this.id = TintableSvg.idSequence++;
TintableSvg.mounts[this.id] = this;
}
componentWillUnmount() {
delete TintableSvg.mounts[this.id];
}
tint = () => {
// TODO: only bother running this if the global tint settings have changed
// since we loaded!
Tinter.applySvgFixups(this.fixups);
};
onLoad = event => {
// console.log("TintableSvg.onLoad for " + this.props.src);
this.fixups = Tinter.calcSvgFixups([event.target]);
Tinter.applySvgFixups(this.fixups);
};
render() {
return (
<object className={"mx_TintableSvg " + (this.props.className ? this.props.className : "")}
type="image/svg+xml"
data={this.props.src}
width={this.props.width}
height={this.props.height}
onLoad={this.onLoad}
tabIndex="-1"
/>
);
}
}
// Register with the Tinter so that we will be told if the tint changes
Tinter.registerTintable(function() {
if (TintableSvg.mounts) {
Object.keys(TintableSvg.mounts).forEach((id) => {
TintableSvg.mounts[id].tint();
});
}
});
export default TintableSvg;

View file

@ -17,7 +17,6 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { Component, CSSProperties } from 'react';
import ReactDOM from 'react-dom';
import classNames from 'classnames';

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
/* eslint-disable babel/no-invalid-this */
/* eslint-disable @typescript-eslint/no-invalid-this */
import React from "react";
import classNames from "classnames";

View file

@ -234,7 +234,7 @@ class EmojiPicker extends React.Component<IProps, IState> {
className="mx_EmojiPicker_body"
wrappedRef={ref => {
// @ts-ignore - AutoHideScrollbar should accept a RefObject or fall back to its own instead
this.bodyRef.current = ref
this.bodyRef.current = ref;
}}
onScroll={this.onScroll}
>

View file

@ -32,7 +32,7 @@ import {mediaFromMxc} from "../../../customisations/Media";
// XXX this class copies a lot from RoomTile.js
@replaceableComponent("views.groups.GroupInviteTile")
export default class GroupInviteTile extends React.Component {
static propTypes: {
static propTypes = {
group: PropTypes.object.isRequired,
};

View file

@ -33,4 +33,4 @@ const HostSignupContainer = () => {
</div>;
};
export default HostSignupContainer
export default HostSignupContainer;

View file

@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2018 Michael Telatynski <7t3chguy@gmail.com>
Copyright 2015 - 2021 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.
@ -16,12 +16,12 @@ limitations under the License.
*/
import React from 'react';
import PropTypes from 'prop-types';
import { _t } from '../../../languageHandler';
import { formatFullDateNoTime } from '../../../DateUtils';
import { replaceableComponent } from "../../../utils/replaceableComponent";
function getdaysArray() {
function getDaysArray(): string[] {
return [
_t('Sunday'),
_t('Monday'),
@ -33,17 +33,17 @@ function getdaysArray() {
];
}
@replaceableComponent("views.messages.DateSeparator")
export default class DateSeparator extends React.Component {
static propTypes = {
ts: PropTypes.number.isRequired,
};
interface IProps {
ts: number;
}
getLabel() {
@replaceableComponent("views.messages.DateSeparator")
export default class DateSeparator extends React.Component<IProps> {
private getLabel() {
const date = new Date(this.props.ts);
const today = new Date();
const yesterday = new Date();
const days = getdaysArray();
const days = getDaysArray();
yesterday.setDate(today.getDate() - 1);
if (date.toDateString() === today.toDateString()) {

View file

@ -51,7 +51,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
decryptedThumbnailUrl: null,
decryptedBlob: null,
error: null,
}
};
}
thumbScale(fullWidth: number, fullHeight: number, thumbWidth: number, thumbHeight: number) {
@ -182,7 +182,7 @@ export default class MVideoBody extends React.PureComponent<IProps, IState> {
this.videoRef.current.play();
});
this.props.onHeightChanged();
}
};
render() {
const content = this.props.mxEvent.getContent();

View file

@ -106,6 +106,6 @@ export default class MVoiceMessageBody extends React.PureComponent<IProps, IStat
<RecordingPlayback playback={this.state.playback} />
<MFileBody {...this.props} decryptedBlob={this.state.decryptedBlob} showGenericPlaceholder={false} />
</span>
)
);
}
}

View file

@ -125,7 +125,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
private onDecrypted = () => {
// Decryption changes whether the event is actionable
this.forceUpdate();
}
};
private onReactionsChange = () => {
// TODO: Call `onHeightChanged` as needed
@ -136,7 +136,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
// has changed (this is triggered by events for that purpose only) and
// `PureComponent`s shallow state / props compare would otherwise filter this out.
this.forceUpdate();
}
};
private getMyReactions() {
const reactions = this.props.reactions;
@ -155,7 +155,7 @@ export default class ReactionsRow extends React.PureComponent<IProps, IState> {
this.setState({
showAll: true,
});
}
};
render() {
const { mxEvent, reactions } = this.props;

View file

@ -79,13 +79,13 @@ export default class ReactionsRowButton extends React.PureComponent<IProps, ISta
tooltipRendered: true,
tooltipVisible: true,
});
}
};
onMouseLeave = () => {
this.setState({
tooltipVisible: false,
});
}
};
render() {
const { mxEvent, content, count, reactionEvents, myReactionEvent } = this.props;

Some files were not shown because too many files have changed in this diff Show more