From 3df9557df2f886db13185872641b2f763baf41aa Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Wed, 24 Mar 2021 14:00:09 +0530 Subject: [PATCH 01/54] Dial Pad UI fix --- res/css/views/voip/_DialPad.scss | 1 + res/css/views/voip/_DialPadContextMenu.scss | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index 0c7bff0ce8..fd7c5f56f6 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -30,6 +30,7 @@ limitations under the License. text-align: center; vertical-align: middle; line-height: 40px; + color: #15191e; } .mx_DialPad_deleteButton, .mx_DialPad_dialButton { diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 520f51cf93..c400060b6c 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -27,9 +27,11 @@ limitations under the License. } .mx_DialPadContextMenu_dialled { - height: 1em; + height: 1.5em; font-size: 18px; font-weight: 600; + max-width: 150px; + overflow: auto; } .mx_DialPadContextMenu_dialPad { From 1488457c3322b6d5dd6ef7882d5b14d9bf49e20f Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Thu, 25 Mar 2021 01:31:45 +0530 Subject: [PATCH 02/54] Added the class -button-bg-color for all themes --- res/css/views/voip/_DialPad.scss | 3 +-- res/themes/dark/css/_dark.scss | 2 ++ res/themes/legacy-dark/css/_legacy-dark.scss | 1 + res/themes/legacy-light/css/_legacy-light.scss | 2 ++ res/themes/light/css/_light.scss | 2 ++ 5 files changed, 8 insertions(+), 2 deletions(-) diff --git a/res/css/views/voip/_DialPad.scss b/res/css/views/voip/_DialPad.scss index fd7c5f56f6..483b131bfe 100644 --- a/res/css/views/voip/_DialPad.scss +++ b/res/css/views/voip/_DialPad.scss @@ -23,14 +23,13 @@ limitations under the License. .mx_DialPad_button { width: 40px; height: 40px; - background-color: $theme-button-bg-color; + background-color: $dialpad-button-bg-color; border-radius: 40px; font-size: 18px; font-weight: 600; text-align: center; vertical-align: middle; line-height: 40px; - color: #15191e; } .mx_DialPad_deleteButton, .mx_DialPad_dialButton { diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 7a751ad9c1..42d592c1e1 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -114,6 +114,8 @@ $voipcall-plinth-color: #21262c; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #545454; + $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $bg-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 764b8f302a..ae98141d06 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,6 +111,7 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #545454; $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 9ad154dd93..4313e3c0b6 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -178,6 +178,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + $roomlist-button-bg-color: #fff; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 25fbd0201b..81330d07c9 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -169,6 +169,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; +$dialpad-button-bg-color: #e3e8f0; + $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: #ffffff; From 3201ed2f0fe0e378c741d57d9a147d79b267b842 Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Mon, 26 Apr 2021 01:40:10 +0530 Subject: [PATCH 03/54] Added color scheme for the numbers --- res/css/views/voip/_DialPadContextMenu.scss | 2 +- res/themes/dark/css/_dark.scss | 3 ++- res/themes/legacy-dark/css/_legacy-dark.scss | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index c400060b6c..9879b7da1c 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -30,7 +30,7 @@ limitations under the License. height: 1.5em; font-size: 18px; font-weight: 600; - max-width: 150px; + max-width: 155px; overflow: auto; } diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 42d592c1e1..b83bd52f76 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -114,7 +114,8 @@ $voipcall-plinth-color: #21262c; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #545454; +$dialpad-button-bg-color: #6F7882; +; $roomlist-button-bg-color: rgba(141, 151, 165, 0.2); // Buttons include the filter box, explore button, and sublist buttons diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index ae98141d06..ff85375d35 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -111,7 +111,8 @@ $voipcall-plinth-color: #f2f5f8; // ******************** $theme-button-bg-color: #e3e8f0; -$dialpad-button-bg-color: #545454; +$dialpad-button-bg-color: #6F7882; +; $roomlist-button-bg-color: #1A1D23; // Buttons include the filter box, explore button, and sublist buttons $roomlist-filter-active-bg-color: $roomlist-button-bg-color; From 7509481bb9339e038027efa6564df9746b73518a Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Wed, 28 Apr 2021 02:46:43 +0530 Subject: [PATCH 04/54] Added the LTR support for the dialpad --- res/css/views/voip/_DialPadContextMenu.scss | 14 ++++++++++++-- .../views/context_menus/DialpadContextMenu.tsx | 13 ++++++++++++- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index 9879b7da1c..c01ce0f2d9 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -30,8 +30,18 @@ limitations under the License. height: 1.5em; font-size: 18px; font-weight: 600; - max-width: 155px; - overflow: auto; + max-width: 150px; + border: none; + margin: 0px; + +} +.mx_DialPadContextMenu_dialled input{ + font-size: 18px; + font-weight: 600; + overflow: hidden; + text-align: left; + direction: rtl; + background-color: rgb(0, 0, 0, 0); } .mx_DialPadContextMenu_dialPad { diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 17abce0c61..0a1d8184f2 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import { ContextMenu, IProps as IContextMenuProps } from '../../structures/ContextMenu'; import { MatrixCall } from 'matrix-js-sdk/src/webrtc/call'; +import Field from "../elements/Field"; import Dialpad from '../voip/DialPad'; import {replaceableComponent} from "../../../utils/replaceableComponent"; @@ -44,13 +45,23 @@ export default class DialpadContextMenu extends React.Component this.setState({value: this.state.value + digit}); } + onChange = (ev) => { + this.setState({value: ev.target.value}); + } + + render() { return
{_t("Dial pad")}
-
{this.state.value}
+
+ +
From b42872daa409d27ad4634d2418fee2bfa7dccfea Mon Sep 17 00:00:00 2001 From: Ayush PS Date: Sun, 2 May 2021 22:10:15 +0530 Subject: [PATCH 05/54] Fixed the Dial Pad and finalized the changes --- res/css/views/voip/_DialPadContextMenu.scss | 5 +++-- .../views/context_menus/DialpadContextMenu.tsx | 12 +++++------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/res/css/views/voip/_DialPadContextMenu.scss b/res/css/views/voip/_DialPadContextMenu.scss index c01ce0f2d9..31327113cf 100644 --- a/res/css/views/voip/_DialPadContextMenu.scss +++ b/res/css/views/voip/_DialPadContextMenu.scss @@ -33,14 +33,15 @@ limitations under the License. max-width: 150px; border: none; margin: 0px; - } -.mx_DialPadContextMenu_dialled input{ +.mx_DialPadContextMenu_dialled input { font-size: 18px; font-weight: 600; overflow: hidden; + max-width: 150px; text-align: left; direction: rtl; + padding: 8px 0px; background-color: rgb(0, 0, 0, 0); } diff --git a/src/components/views/context_menus/DialpadContextMenu.tsx b/src/components/views/context_menus/DialpadContextMenu.tsx index 0a1d8184f2..8879629055 100644 --- a/src/components/views/context_menus/DialpadContextMenu.tsx +++ b/src/components/views/context_menus/DialpadContextMenu.tsx @@ -48,7 +48,7 @@ export default class DialpadContextMenu extends React.Component onChange = (ev) => { this.setState({value: ev.target.value}); } - + render() { return @@ -56,12 +56,10 @@ export default class DialpadContextMenu extends React.Component
{_t("Dial pad")}
-
- - +
From bd71bcca5a83380796ee63400c546edd43499fd5 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Fri, 11 Jun 2021 17:38:16 +0200 Subject: [PATCH 06/54] Allow modal widget buttons to be disabled when the modal opens --- src/components/views/dialogs/ModalWidgetDialog.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/views/dialogs/ModalWidgetDialog.tsx b/src/components/views/dialogs/ModalWidgetDialog.tsx index 0c474b160c..df2ed6b335 100644 --- a/src/components/views/dialogs/ModalWidgetDialog.tsx +++ b/src/components/views/dialogs/ModalWidgetDialog.tsx @@ -63,7 +63,8 @@ export default class ModalWidgetDialog extends React.PureComponent = React.createRef(); state: IState = { - disabledButtonIds: [], + disabledButtonIds: (this.props.widgetDefinition.buttons || []).filter(b => b.disabled) + .map(b => b.id), }; constructor(props) { From 27e27b7a871f6eeb4fb37f9ee2d9dc2560442535 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 16 Jun 2021 10:18:32 +0100 Subject: [PATCH 07/54] Convert MultiInviter, RoomInvite and UserAddress to Typescript --- src/{RoomInvite.js => RoomInvite.tsx} | 44 +++--- src/{UserAddress.js => UserAddress.ts} | 35 +++-- .../{MultiInviter.js => MultiInviter.ts} | 143 +++++++++++------- 3 files changed, 127 insertions(+), 95 deletions(-) rename src/{RoomInvite.js => RoomInvite.tsx} (76%) rename src/{UserAddress.js => UserAddress.ts} (69%) rename src/utils/{MultiInviter.js => MultiInviter.ts} (66%) diff --git a/src/RoomInvite.js b/src/RoomInvite.tsx similarity index 76% rename from src/RoomInvite.js rename to src/RoomInvite.tsx index aa758ecbdc..7c75b5d46b 100644 --- a/src/RoomInvite.js +++ b/src/RoomInvite.tsx @@ -1,7 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2016 - 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,15 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import {MatrixClientPeg} from './MatrixClientPeg'; -import MultiInviter from './utils/MultiInviter'; +import React from "react"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { MatrixEvent } from "matrix-js-sdk/src/models/event"; + +import { MatrixClientPeg } from './MatrixClientPeg'; +import MultiInviter, { CompletionStates } from './utils/MultiInviter'; import Modal from './Modal'; import * as sdk from './'; import { _t } from './languageHandler'; -import InviteDialog, {KIND_DM, KIND_INVITE} from "./components/views/dialogs/InviteDialog"; +import InviteDialog, { KIND_DM, KIND_INVITE } from "./components/views/dialogs/InviteDialog"; import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; -import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; +import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; /** * Invites multiple addresses to a room @@ -32,15 +33,18 @@ import {CommunityPrototypeStore} from "./stores/CommunityPrototypeStore"; * no option to cancel. * * @param {string} roomId The ID of the room to invite to - * @param {string[]} addrs Array of strings of addresses to invite. May be matrix IDs or 3pids. + * @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids. * @returns {Promise} Promise */ -export function inviteMultipleToRoom(roomId, addrs) { +export function inviteMultipleToRoom( + roomId: string, + addresses: string[], +): Promise<{ states: CompletionStates, inviter: MultiInviter }> { const inviter = new MultiInviter(roomId); - return inviter.invite(addrs).then(states => Promise.resolve({states, inviter})); + return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter })); } -export function showStartChatInviteDialog(initialText) { +export function showStartChatInviteDialog(initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. const InviteDialog = sdk.getComponent("dialogs.InviteDialog"); Modal.createTrackedDialog( @@ -49,7 +53,7 @@ export function showStartChatInviteDialog(initialText) { ); } -export function showRoomInviteDialog(roomId, initialText = "") { +export function showRoomInviteDialog(roomId: string, initialText = ""): void { // This dialog handles the room creation internally - we don't need to worry about it. Modal.createTrackedDialog( "Invite Users", "", InviteDialog, { @@ -61,14 +65,14 @@ export function showRoomInviteDialog(roomId, initialText = "") { ); } -export function showCommunityRoomInviteDialog(roomId, communityName) { +export function showCommunityRoomInviteDialog(roomId: string, communityName: string): void { Modal.createTrackedDialog( 'Invite Users to Community', '', CommunityPrototypeInviteDialog, {communityName, roomId}, /*className=*/null, /*isPriority=*/false, /*isStatic=*/true, ); } -export function showCommunityInviteDialog(communityId) { +export function showCommunityInviteDialog(communityId: string): void { const chat = CommunityPrototypeStore.instance.getGeneralChat(communityId); if (chat) { const name = CommunityPrototypeStore.instance.getCommunityName(communityId); @@ -83,7 +87,7 @@ export function showCommunityInviteDialog(communityId) { * @param {MatrixEvent} event The event to check * @returns {boolean} True if valid, false otherwise */ -export function isValid3pidInvite(event) { +export function isValid3pidInvite(event: MatrixEvent): boolean { if (!event || event.getType() !== "m.room.third_party_invite") return false; // any events without these keys are not valid 3pid invites, so we ignore them @@ -96,7 +100,7 @@ export function isValid3pidInvite(event) { return true; } -export function inviteUsersToRoom(roomId, userIds) { +export function inviteUsersToRoom(roomId: string, userIds: string[]): Promise { return inviteMultipleToRoom(roomId, userIds).then((result) => { const room = MatrixClientPeg.get().getRoom(roomId); showAnyInviteErrors(result.states, room, result.inviter); @@ -110,9 +114,9 @@ export function inviteUsersToRoom(roomId, userIds) { }); } -export function showAnyInviteErrors(addrs, room, inviter) { +export function showAnyInviteErrors(states: CompletionStates, room: Room, inviter: MultiInviter): boolean { // Show user any errors - const failedUsers = Object.keys(addrs).filter(a => addrs[a] === 'error'); + const failedUsers = Object.keys(states).filter(a => states[a] === 'error'); if (failedUsers.length === 1 && inviter.fatal) { // Just get the first message because there was a fatal problem on the first // user. This usually means that no other users were attempted, making it @@ -126,7 +130,7 @@ export function showAnyInviteErrors(addrs, room, inviter) { } else { const errorList = []; for (const addr of failedUsers) { - if (addrs[addr] === "error") { + if (states[addr] === "error") { const reason = inviter.getErrorText(addr); errorList.push(addr + ": " + reason); } diff --git a/src/UserAddress.js b/src/UserAddress.ts similarity index 69% rename from src/UserAddress.js rename to src/UserAddress.ts index e7501a0d91..a2c546deb7 100644 --- a/src/UserAddress.js +++ b/src/UserAddress.ts @@ -1,5 +1,5 @@ /* -Copyright 2017 New Vector Ltd +Copyright 2017 - 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,15 +14,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -const emailRegex = /^\S+@\S+\.\S+$/; +import PropTypes from "prop-types"; +const emailRegex = /^\S+@\S+\.\S+$/; const mxUserIdRegex = /^@\S+:\S+$/; const mxRoomIdRegex = /^!\S+:\S+$/; -import PropTypes from 'prop-types'; -export const addressTypes = [ - 'mx-user-id', 'mx-room-id', 'email', -]; +export const addressTypes = ['mx-user-id', 'mx-room-id', 'email']; + +export enum AddressType { + Email = "email", + MatrixUserId = "mx-user-id", + MatrixRoomId = "mx-room-id", +} // PropType definition for an object describing // an address that can be invited to a room (which @@ -40,18 +44,13 @@ export const UserAddressType = PropTypes.shape({ isKnown: PropTypes.bool, }); -export function getAddressType(inputText) { - const isEmailAddress = emailRegex.test(inputText); - const isUserId = mxUserIdRegex.test(inputText); - const isRoomId = mxRoomIdRegex.test(inputText); - - // sanity check the input for user IDs - if (isEmailAddress) { - return 'email'; - } else if (isUserId) { - return 'mx-user-id'; - } else if (isRoomId) { - return 'mx-room-id'; +export function getAddressType(inputText: string): AddressType | null { + if (emailRegex.test(inputText)) { + return AddressType.Email; + } else if (mxUserIdRegex.test(inputText)) { + return AddressType.MatrixUserId; + } else if (mxRoomIdRegex.test(inputText)) { + return AddressType.MatrixRoomId; } else { return null; } diff --git a/src/utils/MultiInviter.js b/src/utils/MultiInviter.ts similarity index 66% rename from src/utils/MultiInviter.js rename to src/utils/MultiInviter.ts index 78f956b91b..f6a994484e 100644 --- a/src/utils/MultiInviter.js +++ b/src/utils/MultiInviter.ts @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2017, 2018 New Vector Ltd +Copyright 2016 - 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. @@ -15,23 +14,51 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {MatrixClientPeg} from '../MatrixClientPeg'; -import {getAddressType} from '../UserAddress'; +import { MatrixError } from "matrix-js-sdk/src/http-api"; + +import { MatrixClientPeg } from '../MatrixClientPeg'; +import { AddressType, getAddressType } from '../UserAddress'; import GroupStore from '../stores/GroupStore'; -import {_t} from "../languageHandler"; -import * as sdk from "../index"; +import { _t } from "../languageHandler"; import Modal from "../Modal"; import SettingsStore from "../settings/SettingsStore"; -import {defer} from "./promise"; +import { defer, IDeferred } from "./promise"; +import AskInviteAnywayDialog from "../components/views/dialogs/AskInviteAnywayDialog"; + +export enum InviteState { + Invited = "invited", + Error = "error", +} + +interface IError { + errorText: string; + errcode: string; +} + +const UNKNOWN_PROFILE_ERRORS = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND']; + +export type CompletionStates = Record; /** * Invites multiple addresses to a room or group, handling rate limiting from the server */ export default class MultiInviter { + private readonly roomId?: string; + private readonly groupId?: string; + + private canceled = false; + private addresses: string[] = []; + private busy = false; + private _fatal = false; + private completionStates: CompletionStates = {}; // State of each address (invited or error) + private errors: Record = {}; // { address: {errorText, errcode} } + private deferred: IDeferred = null; + private reason: string = null; + /** * @param {string} targetId The ID of the room or group to invite to */ - constructor(targetId) { + constructor(targetId: string) { if (targetId[0] === '+') { this.roomId = null; this.groupId = targetId; @@ -39,41 +66,38 @@ export default class MultiInviter { this.roomId = targetId; this.groupId = null; } + } - this.canceled = false; - this.addrs = []; - this.busy = false; - this.completionStates = {}; // State of each address (invited or error) - this.errors = {}; // { address: {errorText, errcode} } - this.deferred = null; + public get fatal() { + return this._fatal; } /** * Invite users to this room. This may only be called once per * instance of the class. * - * @param {array} addrs Array of addresses to invite + * @param {array} addresses Array of addresses to invite * @param {string} reason Reason for inviting (optional) * @returns {Promise} Resolved when all invitations in the queue are complete */ - invite(addrs, reason) { - if (this.addrs.length > 0) { + public invite(addresses, reason?: string): Promise { + if (this.addresses.length > 0) { throw new Error("Already inviting/invited"); } - this.addrs.push(...addrs); + this.addresses.push(...addresses); this.reason = reason; - for (const addr of this.addrs) { + for (const addr of this.addresses) { if (getAddressType(addr) === null) { - this.completionStates[addr] = 'error'; + this.completionStates[addr] = InviteState.Error; this.errors[addr] = { errcode: 'M_INVALID', errorText: _t('Unrecognised address'), }; } } - this.deferred = defer(); - this._inviteMore(0); + this.deferred = defer(); + this.inviteMore(0); return this.deferred.promise; } @@ -81,33 +105,36 @@ export default class MultiInviter { /** * Stops inviting. Causes promises returned by invite() to be rejected. */ - cancel() { + public cancel(): void { if (!this.busy) return; - this._canceled = true; + this.canceled = true; this.deferred.reject(new Error('canceled')); } - getCompletionState(addr) { + public getCompletionState(addr: string): InviteState { return this.completionStates[addr]; } - getErrorText(addr) { + public getErrorText(addr: string): string { return this.errors[addr] ? this.errors[addr].errorText : null; } - async _inviteToRoom(roomId, addr, ignoreProfile) { + private async inviteToRoom(roomId: string, addr: string, ignoreProfile = false): Promise<{}> { const addrType = getAddressType(addr); - if (addrType === 'email') { + if (addrType === AddressType.Email) { return MatrixClientPeg.get().inviteByEmail(roomId, addr); - } else if (addrType === 'mx-user-id') { + } else if (addrType === AddressType.MatrixUserId) { const room = MatrixClientPeg.get().getRoom(roomId); if (!room) throw new Error("Room not found"); const member = room.getMember(addr); if (member && ['join', 'invite'].includes(member.membership)) { - throw {errcode: "RIOT.ALREADY_IN_ROOM", error: "Member already invited"}; + throw new new MatrixError({ + errcode: "RIOT.ALREADY_IN_ROOM", + error: "Member already invited", + }); } if (!ignoreProfile && SettingsStore.getValue("promptBeforeInviteUnknownUsers", this.roomId)) { @@ -124,28 +151,28 @@ export default class MultiInviter { } } - _doInvite(address, ignoreProfile) { - return new Promise((resolve, reject) => { + private doInvite(address: string, ignoreProfile = false): Promise { + return new Promise((resolve, reject) => { console.log(`Inviting ${address}`); let doInvite; if (this.groupId !== null) { doInvite = GroupStore.inviteUserToGroup(this.groupId, address); } else { - doInvite = this._inviteToRoom(this.roomId, address, ignoreProfile); + doInvite = this.inviteToRoom(this.roomId, address, ignoreProfile); } doInvite.then(() => { - if (this._canceled) { + if (this.canceled) { return; } - this.completionStates[address] = 'invited'; + this.completionStates[address] = InviteState.Invited; delete this.errors[address]; resolve(); }).catch((err) => { - if (this._canceled) { + if (this.canceled) { return; } @@ -161,7 +188,7 @@ export default class MultiInviter { } else if (err.errcode === 'M_LIMIT_EXCEEDED') { // we're being throttled so wait a bit & try again setTimeout(() => { - this._doInvite(address, ignoreProfile).then(resolve, reject); + this.doInvite(address, ignoreProfile).then(resolve, reject); }, 5000); return; } else if (['M_NOT_FOUND', 'M_USER_NOT_FOUND'].includes(err.errcode)) { @@ -171,7 +198,7 @@ export default class MultiInviter { } else if (err.errcode === 'M_PROFILE_NOT_FOUND' && !ignoreProfile) { // Invite without the profile check console.warn(`User ${address} does not have a profile - inviting anyways automatically`); - this._doInvite(address, true).then(resolve, reject); + this.doInvite(address, true).then(resolve, reject); } else if (err.errcode === "M_BAD_STATE") { errorText = _t("The user must be unbanned before they can be invited."); } else if (err.errcode === "M_UNSUPPORTED_ROOM_VERSION") { @@ -180,14 +207,14 @@ export default class MultiInviter { errorText = _t('Unknown server error'); } - this.completionStates[address] = 'error'; - this.errors[address] = {errorText, errcode: err.errcode}; + this.completionStates[address] = InviteState.Error; + this.errors[address] = { errorText, errcode: err.errcode }; this.busy = !fatal; - this.fatal = fatal; + this._fatal = fatal; if (fatal) { - reject(); + reject(err); } else { resolve(); } @@ -195,22 +222,22 @@ export default class MultiInviter { }); } - _inviteMore(nextIndex, ignoreProfile) { - if (this._canceled) { + private inviteMore(nextIndex: number, ignoreProfile = false): void { + if (this.canceled) { return; } - if (nextIndex === this.addrs.length) { + if (nextIndex === this.addresses.length) { this.busy = false; if (Object.keys(this.errors).length > 0 && !this.groupId) { // There were problems inviting some people - see if we can invite them // without caring if they exist or not. - const unknownProfileErrors = ['M_NOT_FOUND', 'M_USER_NOT_FOUND', 'M_PROFILE_UNDISCLOSED', 'M_PROFILE_NOT_FOUND']; - const unknownProfileUsers = Object.keys(this.errors).filter(a => unknownProfileErrors.includes(this.errors[a].errcode)); + const unknownProfileUsers = Object.keys(this.errors) + .filter(a => UNKNOWN_PROFILE_ERRORS.includes(this.errors[a].errcode)); if (unknownProfileUsers.length > 0) { const inviteUnknowns = () => { - const promises = unknownProfileUsers.map(u => this._doInvite(u, true)); + const promises = unknownProfileUsers.map(u => this.doInvite(u, true)); Promise.all(promises).then(() => this.deferred.resolve(this.completionStates)); }; @@ -219,15 +246,17 @@ export default class MultiInviter { return; } - const AskInviteAnywayDialog = sdk.getComponent("dialogs.AskInviteAnywayDialog"); console.log("Showing failed to invite dialog..."); Modal.createTrackedDialog('Failed to invite', '', AskInviteAnywayDialog, { - unknownProfileUsers: unknownProfileUsers.map(u => {return {userId: u, errorText: this.errors[u].errorText};}), + unknownProfileUsers: unknownProfileUsers.map(u => ({ + userId: u, + errorText: this.errors[u].errorText, + })), onInviteAnyways: () => inviteUnknowns(), onGiveUp: () => { // Fake all the completion states because we already warned the user for (const addr of unknownProfileUsers) { - this.completionStates[addr] = 'invited'; + this.completionStates[addr] = InviteState.Invited; } this.deferred.resolve(this.completionStates); }, @@ -239,25 +268,25 @@ export default class MultiInviter { return; } - const addr = this.addrs[nextIndex]; + const addr = this.addresses[nextIndex]; // don't try to invite it if it's an invalid address // (it will already be marked as an error though, // so no need to do so again) if (getAddressType(addr) === null) { - this._inviteMore(nextIndex + 1); + this.inviteMore(nextIndex + 1); return; } // don't re-invite (there's no way in the UI to do this, but // for sanity's sake) - if (this.completionStates[addr] === 'invited') { - this._inviteMore(nextIndex + 1); + if (this.completionStates[addr] === InviteState.Invited) { + this.inviteMore(nextIndex + 1); return; } - this._doInvite(addr, ignoreProfile).then(() => { - this._inviteMore(nextIndex + 1, ignoreProfile); + this.doInvite(addr, ignoreProfile).then(() => { + this.inviteMore(nextIndex + 1, ignoreProfile); }).catch(() => this.deferred.resolve(this.completionStates)); } } From 47ddd33d2121ed8e74962c2ef02b1271837d6680 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 16 Jun 2021 10:24:51 +0100 Subject: [PATCH 08/54] Remove explicit `.js` imports --- src/components/structures/MatrixChat.tsx | 2 +- src/components/views/dialogs/AddressPickerDialog.js | 2 +- src/components/views/elements/AddressTile.js | 6 +++--- src/components/views/messages/SenderProfile.tsx | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 0af2d3d635..2cb0fbf3f6 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -48,7 +48,7 @@ import createRoom, {IOpts} from "../../createRoom"; import {_t, _td, getCurrentLanguage} from '../../languageHandler'; import SettingsStore from "../../settings/SettingsStore"; import ThemeController from "../../settings/controllers/ThemeController"; -import { startAnyRegistrationFlow } from "../../Registration.js"; +import { startAnyRegistrationFlow } from "../../Registration"; import { messageForSyncError } from '../../utils/ErrorUtils'; import ResizeNotifier from "../../utils/ResizeNotifier"; import AutoDiscoveryUtils, { ValidatedServerConfig } from "../../utils/AutoDiscoveryUtils"; diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.js index 929d688e47..77c69abc4e 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.js @@ -24,7 +24,7 @@ import { _t, _td } from '../../../languageHandler'; import * as sdk from '../../../index'; import {MatrixClientPeg} from '../../../MatrixClientPeg'; import dis from '../../../dispatcher/dispatcher'; -import { addressTypes, getAddressType } from '../../../UserAddress.js'; +import { addressTypes, getAddressType } from '../../../UserAddress'; import GroupStore from '../../../stores/GroupStore'; import * as Email from '../../../email'; import IdentityAuthClient from '../../../IdentityAuthClient'; diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.js index df66d10a71..f8fa294b71 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.js @@ -20,9 +20,9 @@ import PropTypes from 'prop-types'; import classNames from 'classnames'; import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; -import { UserAddressType } from '../../../UserAddress.js'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; -import {mediaFromMxc} from "../../../customisations/Media"; +import { UserAddressType } from '../../../UserAddress'; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { mediaFromMxc } from "../../../customisations/Media"; @replaceableComponent("views.elements.AddressTile") export default class AddressTile extends React.Component { diff --git a/src/components/views/messages/SenderProfile.tsx b/src/components/views/messages/SenderProfile.tsx index 805f842fbc..de1549dffa 100644 --- a/src/components/views/messages/SenderProfile.tsx +++ b/src/components/views/messages/SenderProfile.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import Flair from '../elements/Flair.js'; +import Flair from '../elements/Flair'; import FlairStore from '../../../stores/FlairStore'; import {getUserNameColorClass} from '../../../utils/FormattingUtils'; import MatrixClientContext from "../../../contexts/MatrixClientContext"; From 384bb3af2e6a43fb28f3c17a8cb7d2612121ba4a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 16 Jun 2021 10:37:34 +0100 Subject: [PATCH 09/54] Fix layout regression in the invite dialog for few results --- res/css/views/dialogs/_InviteDialog.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/dialogs/_InviteDialog.scss b/res/css/views/dialogs/_InviteDialog.scss index 2e48b5d8e9..175b1cc556 100644 --- a/res/css/views/dialogs/_InviteDialog.scss +++ b/res/css/views/dialogs/_InviteDialog.scss @@ -294,6 +294,7 @@ limitations under the License. flex-direction: column; .mx_InviteDialog_content { + height: 100%; overflow: hidden; } } From 590ce5674055f8094c69c7ec3d490e40747dcc5d Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 16 Jun 2021 11:48:14 +0100 Subject: [PATCH 10/54] Use MultiInviter error messages in InviteDialog for room invites --- src/RoomInvite.tsx | 10 +++++--- src/components/views/dialogs/InviteDialog.tsx | 25 ++++++++----------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/RoomInvite.tsx b/src/RoomInvite.tsx index 7c75b5d46b..16141a87e8 100644 --- a/src/RoomInvite.tsx +++ b/src/RoomInvite.tsx @@ -27,6 +27,11 @@ import InviteDialog, { KIND_DM, KIND_INVITE } from "./components/views/dialogs/I import CommunityPrototypeInviteDialog from "./components/views/dialogs/CommunityPrototypeInviteDialog"; import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; +export interface IInviteResult { + states: CompletionStates; + inviter: MultiInviter; +} + /** * Invites multiple addresses to a room * Simpler interface to utils/MultiInviter but with @@ -36,10 +41,7 @@ import { CommunityPrototypeStore } from "./stores/CommunityPrototypeStore"; * @param {string[]} addresses Array of strings of addresses to invite. May be matrix IDs or 3pids. * @returns {Promise} Promise */ -export function inviteMultipleToRoom( - roomId: string, - addresses: string[], -): Promise<{ states: CompletionStates, inviter: MultiInviter }> { +export function inviteMultipleToRoom(roomId: string, addresses: string[]): Promise { const inviter = new MultiInviter(roomId); return inviter.invite(addresses).then(states => Promise.resolve({ states, inviter })); } diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 778744b783..f50f2f23d6 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -34,7 +34,12 @@ import {humanizeTime} from "../../../utils/humanize"; import createRoom, { canEncryptToAllUsers, ensureDMExists, findDMForUser, privateShouldBeEncrypted, } from "../../../createRoom"; -import {inviteMultipleToRoom, showCommunityInviteDialog} from "../../../RoomInvite"; +import { + IInviteResult, + inviteMultipleToRoom, + showAnyInviteErrors, + showCommunityInviteDialog, +} from "../../../RoomInvite"; import {Key} from "../../../Keyboard"; import {Action} from "../../../dispatcher/actions"; import {DefaultTagID} from "../../../stores/room-list/models"; @@ -601,19 +606,9 @@ export default class InviteDialog extends React.PureComponent ({userId: m.member.userId, user: m.member})); } - private shouldAbortAfterInviteError(result): boolean { - const failedUsers = Object.keys(result.states).filter(a => result.states[a] === 'error'); - if (failedUsers.length > 0) { - console.log("Failed to invite users: ", result); - this.setState({ - busy: false, - errorText: _t("Failed to invite the following users to chat: %(csvUsers)s", { - csvUsers: failedUsers.join(", "), - }), - }); - return true; // abort - } - return false; + private shouldAbortAfterInviteError(result: IInviteResult, room: Room): boolean { + this.setState({ busy: false }); + return !showAnyInviteErrors(result.states, room, result.inviter); } private convertFilter(): Member[] { @@ -731,7 +726,7 @@ export default class InviteDialog extends React.PureComponent Date: Wed, 16 Jun 2021 20:12:31 +0100 Subject: [PATCH 11/54] i18n --- src/i18n/strings/en_EN.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 17d6f64c46..57bf5fa739 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2241,7 +2241,6 @@ "Confirm to continue": "Confirm to continue", "Click the button below to confirm your identity.": "Click the button below to confirm your identity.", "Invite by email": "Invite by email", - "Failed to invite the following users to chat: %(csvUsers)s": "Failed to invite the following users to chat: %(csvUsers)s", "We couldn't create your DM.": "We couldn't create your DM.", "Something went wrong trying to invite the users.": "Something went wrong trying to invite the users.", "We couldn't invite those users. Please check the users you want to invite and try again.": "We couldn't invite those users. Please check the users you want to invite and try again.", From be10e77704b1635dc514b7b98201b7ee11b24d10 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 15:37:06 +0100 Subject: [PATCH 12/54] Improve typing of Event Index Manager / Seshat --- src/indexing/BaseEventIndexManager.ts | 78 +++++++++++++-------------- src/indexing/EventIndex.ts | 16 +++--- 2 files changed, 47 insertions(+), 47 deletions(-) diff --git a/src/indexing/BaseEventIndexManager.ts b/src/indexing/BaseEventIndexManager.ts index debcb213ca..9478b2987b 100644 --- a/src/indexing/BaseEventIndexManager.ts +++ b/src/indexing/BaseEventIndexManager.ts @@ -17,7 +17,7 @@ limitations under the License. // The following interfaces take their names and member names from seshat and the spec /* eslint-disable camelcase */ -export interface MatrixEvent { +export interface IMatrixEvent { type: string; sender: string; content: {}; @@ -27,37 +27,37 @@ export interface MatrixEvent { roomId: string; } -export interface MatrixProfile { +export interface IMatrixProfile { avatar_url: string; displayname: string; } -export interface CrawlerCheckpoint { +export interface ICrawlerCheckpoint { roomId: string; token: string; fullCrawl?: boolean; direction: string; } -export interface ResultContext { - events_before: [MatrixEvent]; - events_after: [MatrixEvent]; - profile_info: Map; +export interface IResultContext { + events_before: [IMatrixEvent]; + events_after: [IMatrixEvent]; + profile_info: Map; } -export interface ResultsElement { +export interface IResultsElement { rank: number; - result: MatrixEvent; - context: ResultContext; + result: IMatrixEvent; + context: IResultContext; } -export interface SearchResult { +export interface ISearchResult { count: number; - results: [ResultsElement]; + results: [IResultsElement]; highlights: [string]; } -export interface SearchArgs { +export interface ISearchArgs { search_term: string; before_limit: number; after_limit: number; @@ -65,19 +65,19 @@ export interface SearchArgs { room_id?: string; } -export interface EventAndProfile { - event: MatrixEvent; - profile: MatrixProfile; +export interface IEventAndProfile { + event: IMatrixEvent; + profile: IMatrixProfile; } -export interface LoadArgs { +export interface ILoadArgs { roomId: string; limit: number; fromEvent?: string; direction?: string; } -export interface IndexStats { +export interface IIndexStats { size: number; eventCount: number; roomCount: number; @@ -119,13 +119,13 @@ export default abstract class BaseEventIndexManager { * Queue up an event to be added to the index. * * @param {MatrixEvent} ev The event that should be added to the index. - * @param {MatrixProfile} profile The profile of the event sender at the + * @param {IMatrixProfile} profile The profile of the event sender at the * time of the event receival. * * @return {Promise} A promise that will resolve when the was queued up for * addition. */ - async addEventToIndex(ev: MatrixEvent, profile: MatrixProfile): Promise { + async addEventToIndex(ev: IMatrixEvent, profile: IMatrixProfile): Promise { throw new Error("Unimplemented"); } @@ -160,10 +160,10 @@ export default abstract class BaseEventIndexManager { /** * Get statistical information of the index. * - * @return {Promise} A promise that will resolve to the index + * @return {Promise} A promise that will resolve to the index * statistics. */ - async getStats(): Promise { + async getStats(): Promise { throw new Error("Unimplemented"); } @@ -203,13 +203,13 @@ export default abstract class BaseEventIndexManager { /** * Search the event index using the given term for matching events. * - * @param {SearchArgs} searchArgs The search configuration for the search, + * @param {ISearchArgs} searchArgs The search configuration for the search, * sets the search term and determines the search result contents. * - * @return {Promise<[SearchResult]>} A promise that will resolve to an array + * @return {Promise<[ISearchResult]>} A promise that will resolve to an array * of search results once the search is done. */ - async searchEventIndex(searchArgs: SearchArgs): Promise { + async searchEventIndex(searchArgs: ISearchArgs): Promise { throw new Error("Unimplemented"); } @@ -218,12 +218,12 @@ export default abstract class BaseEventIndexManager { * * This is used to add a batch of events to the index. * - * @param {[EventAndProfile]} events The list of events and profiles that + * @param {[IEventAndProfile]} events The list of events and profiles that * should be added to the event index. - * @param {[CrawlerCheckpoint]} checkpoint A new crawler checkpoint that + * @param {[ICrawlerCheckpoint]} checkpoint A new crawler checkpoint that * should be stored in the index which should be used to continue crawling * the room. - * @param {[CrawlerCheckpoint]} oldCheckpoint The checkpoint that was used + * @param {[ICrawlerCheckpoint]} oldCheckpoint The checkpoint that was used * to fetch the current batch of events. This checkpoint will be removed * from the index. * @@ -231,9 +231,9 @@ export default abstract class BaseEventIndexManager { * were already added to the index, false otherwise. */ async addHistoricEvents( - events: [EventAndProfile], - checkpoint: CrawlerCheckpoint | null, - oldCheckpoint: CrawlerCheckpoint | null, + events: IEventAndProfile[], + checkpoint: ICrawlerCheckpoint | null, + oldCheckpoint: ICrawlerCheckpoint | null, ): Promise { throw new Error("Unimplemented"); } @@ -241,36 +241,36 @@ export default abstract class BaseEventIndexManager { /** * Add a new crawler checkpoint to the index. * - * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be added + * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be added * to the index. * * @return {Promise} A promise that will resolve once the checkpoint has * been stored. */ - async addCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise { + async addCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise { throw new Error("Unimplemented"); } /** * Add a new crawler checkpoint to the index. * - * @param {CrawlerCheckpoint} checkpoint The checkpoint that should be + * @param {ICrawlerCheckpoint} checkpoint The checkpoint that should be * removed from the index. * * @return {Promise} A promise that will resolve once the checkpoint has * been removed. */ - async removeCrawlerCheckpoint(checkpoint: CrawlerCheckpoint): Promise { + async removeCrawlerCheckpoint(checkpoint: ICrawlerCheckpoint): Promise { throw new Error("Unimplemented"); } /** * Load the stored checkpoints from the index. * - * @return {Promise<[CrawlerCheckpoint]>} A promise that will resolve to an + * @return {Promise<[ICrawlerCheckpoint]>} A promise that will resolve to an * array of crawler checkpoints once they have been loaded from the index. */ - async loadCheckpoints(): Promise<[CrawlerCheckpoint]> { + async loadCheckpoints(): Promise { throw new Error("Unimplemented"); } @@ -286,11 +286,11 @@ export default abstract class BaseEventIndexManager { * @param {string} args.direction The direction to which we should continue * loading events from. This is used only if fromEvent is used as well. * - * @return {Promise<[EventAndProfile]>} A promise that will resolve to an + * @return {Promise<[IEventAndProfile]>} A promise that will resolve to an * array of Matrix events that contain mxc URLs accompanied with the * historic profile of the sender. */ - async loadFileEvents(args: LoadArgs): Promise<[EventAndProfile]> { + async loadFileEvents(args: ILoadArgs): Promise { throw new Error("Unimplemented"); } diff --git a/src/indexing/EventIndex.ts b/src/indexing/EventIndex.ts index c36f96f368..978a2ac813 100644 --- a/src/indexing/EventIndex.ts +++ b/src/indexing/EventIndex.ts @@ -28,7 +28,7 @@ import { MatrixClientPeg } from "../MatrixClientPeg"; import { sleep } from "../utils/promise"; import SettingsStore from "../settings/SettingsStore"; import { SettingLevel } from "../settings/SettingLevel"; -import {CrawlerCheckpoint, LoadArgs, SearchArgs} from "./BaseEventIndexManager"; +import { ICrawlerCheckpoint, ILoadArgs, ISearchArgs } from "./BaseEventIndexManager"; // The time in ms that the crawler will wait loop iterations if there // have not been any checkpoints to consume in the last iteration. @@ -45,9 +45,9 @@ interface ICrawler { * Event indexing class that wraps the platform specific event indexing. */ export default class EventIndex extends EventEmitter { - private crawlerCheckpoints: CrawlerCheckpoint[] = []; + private crawlerCheckpoints: ICrawlerCheckpoint[] = []; private crawler: ICrawler = null; - private currentCheckpoint: CrawlerCheckpoint = null; + private currentCheckpoint: ICrawlerCheckpoint = null; public async init() { const indexManager = PlatformPeg.get().getEventIndexingManager(); @@ -111,14 +111,14 @@ export default class EventIndex extends EventEmitter { const timeline = room.getLiveTimeline(); const token = timeline.getPaginationToken("b"); - const backCheckpoint: CrawlerCheckpoint = { + const backCheckpoint: ICrawlerCheckpoint = { roomId: room.roomId, token: token, direction: "b", fullCrawl: true, }; - const forwardCheckpoint: CrawlerCheckpoint = { + const forwardCheckpoint: ICrawlerCheckpoint = { roomId: room.roomId, token: token, direction: "f", @@ -668,13 +668,13 @@ export default class EventIndex extends EventEmitter { /** * Search the event index using the given term for matching events. * - * @param {SearchArgs} searchArgs The search configuration for the search, + * @param {ISearchArgs} searchArgs The search configuration for the search, * sets the search term and determines the search result contents. * * @return {Promise<[SearchResult]>} A promise that will resolve to an array * of search results once the search is done. */ - public async search(searchArgs: SearchArgs) { + public async search(searchArgs: ISearchArgs) { const indexManager = PlatformPeg.get().getEventIndexingManager(); return indexManager.searchEventIndex(searchArgs); } @@ -709,7 +709,7 @@ export default class EventIndex extends EventEmitter { const client = MatrixClientPeg.get(); const indexManager = PlatformPeg.get().getEventIndexingManager(); - const loadArgs: LoadArgs = { + const loadArgs: ILoadArgs = { roomId: room.roomId, limit: limit, }; From 8e2a7cc3f6badace1d1cb0dacb90d449689ac8cd Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Sat, 19 Jun 2021 19:41:45 +0100 Subject: [PATCH 13/54] Convert crypto index to TS --- src/rageshake/submit-rageshake.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/rageshake/submit-rageshake.ts b/src/rageshake/submit-rageshake.ts index 08d8ccfd13..64d7405f17 100644 --- a/src/rageshake/submit-rageshake.ts +++ b/src/rageshake/submit-rageshake.ts @@ -86,8 +86,8 @@ async function collectBugReport(opts: IOpts = {}, gzipLogs = true) { body.append('cross_signing_key', client.getCrossSigningId()); // add cross-signing status information - const crossSigning = client.crypto._crossSigningInfo; - const secretStorage = client.crypto._secretStorage; + const crossSigning = client.crypto.crossSigningInfo; + const secretStorage = client.crypto.secretStorage; body.append("cross_signing_ready", String(await client.isCrossSigningReady())); body.append("cross_signing_supported_by_hs", From a8dfc4488ffd51f36d23e16e92d26b2d3750a328 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 14:47:24 +0100 Subject: [PATCH 14/54] Convert more of js-sdk crypto and fix underscored field accesses --- src/SecurityManager.ts | 5 +++-- src/components/views/dialogs/DevtoolsDialog.tsx | 2 +- src/components/views/settings/CrossSigningPanel.js | 4 ++-- src/components/views/settings/SecureBackupPanel.js | 2 +- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/SecurityManager.ts b/src/SecurityManager.ts index 09c8d30614..1ba0d6439b 100644 --- a/src/SecurityManager.ts +++ b/src/SecurityManager.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { ICryptoCallbacks, IDeviceTrustLevel, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; +import { ICryptoCallbacks, ISecretStorageKeyInfo } from 'matrix-js-sdk/src/matrix'; import { MatrixClient } from 'matrix-js-sdk/src/client'; import Modal from './Modal'; import * as sdk from './index'; @@ -28,6 +28,7 @@ import AccessSecretStorageDialog from './components/views/dialogs/security/Acces import RestoreKeyBackupDialog from './components/views/dialogs/security/RestoreKeyBackupDialog'; import SettingsStore from "./settings/SettingsStore"; import SecurityCustomisations from "./customisations/Security"; +import { DeviceTrustLevel } from 'matrix-js-sdk/src/crypto/CrossSigning'; // This stores the secret storage private keys in memory for the JS SDK. This is // only meant to act as a cache to avoid prompting the user multiple times @@ -244,7 +245,7 @@ async function onSecretRequested( deviceId: string, requestId: string, name: string, - deviceTrust: IDeviceTrustLevel, + deviceTrust: DeviceTrustLevel, ): Promise { console.log("onSecretRequested", userId, deviceId, requestId, name, deviceTrust); const client = MatrixClientPeg.get(); diff --git a/src/components/views/dialogs/DevtoolsDialog.tsx b/src/components/views/dialogs/DevtoolsDialog.tsx index 2690eb67d7..b1749b370a 100644 --- a/src/components/views/dialogs/DevtoolsDialog.tsx +++ b/src/components/views/dialogs/DevtoolsDialog.tsx @@ -766,7 +766,7 @@ class VerificationExplorer extends React.PureComponent { render() { const cli = this.context; const room = this.props.room; - const inRoomChannel = cli.crypto._inRoomVerificationRequests; + const inRoomChannel = cli.crypto.inRoomVerificationRequests; const inRoomRequests = (inRoomChannel._requestsByRoomId || new Map()).get(room.roomId) || new Map(); return (
diff --git a/src/components/views/settings/CrossSigningPanel.js b/src/components/views/settings/CrossSigningPanel.js index 0cd1a64ada..43a13a48a7 100644 --- a/src/components/views/settings/CrossSigningPanel.js +++ b/src/components/views/settings/CrossSigningPanel.js @@ -79,8 +79,8 @@ export default class CrossSigningPanel extends React.PureComponent { async _getUpdatedStatus() { const cli = MatrixClientPeg.get(); const pkCache = cli.getCrossSigningCacheCallbacks(); - const crossSigning = cli.crypto._crossSigningInfo; - const secretStorage = cli.crypto._secretStorage; + const crossSigning = cli.crypto.crossSigningInfo; + const secretStorage = cli.crypto.secretStorage; const crossSigningPublicKeysOnDevice = crossSigning.getId(); const crossSigningPrivateKeysInStorage = await crossSigning.isStoredInSecretStorage(secretStorage); const masterPrivateKeyCached = !!(pkCache && await pkCache.getCrossSigningKeyCache("master")); diff --git a/src/components/views/settings/SecureBackupPanel.js b/src/components/views/settings/SecureBackupPanel.js index 4f3eb0bdf6..abfd18f0d3 100644 --- a/src/components/views/settings/SecureBackupPanel.js +++ b/src/components/views/settings/SecureBackupPanel.js @@ -131,7 +131,7 @@ export default class SecureBackupPanel extends React.PureComponent { async _getUpdatedDiagnostics() { const cli = MatrixClientPeg.get(); - const secretStorage = cli.crypto._secretStorage; + const secretStorage = cli.crypto.secretStorage; const backupKeyStored = !!(await cli.isKeyBackupKeyStored()); const backupKeyFromCache = await cli.crypto.getSessionBackupPrivateKey(); From 99c442cea78f2e0ff3666f127d1260be2bf313e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 13:45:55 +0200 Subject: [PATCH 15/54] Convert MemberList to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../rooms/{MemberList.js => MemberList.tsx} | 292 +++++++++++------- 1 file changed, 174 insertions(+), 118 deletions(-) rename src/components/views/rooms/{MemberList.js => MemberList.tsx} (64%) diff --git a/src/components/views/rooms/MemberList.js b/src/components/views/rooms/MemberList.tsx similarity index 64% rename from src/components/views/rooms/MemberList.js rename to src/components/views/rooms/MemberList.tsx index cb50f0fff3..5353eb94ed 100644 --- a/src/components/views/rooms/MemberList.js +++ b/src/components/views/rooms/MemberList.tsx @@ -2,6 +2,7 @@ Copyright 2015, 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd Copyright 2017, 2018 New Vector Ltd +Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,17 +21,28 @@ import React from 'react'; import { _t } from '../../../languageHandler'; import SdkConfig from '../../../SdkConfig'; import dis from '../../../dispatcher/dispatcher'; -import {isValid3pidInvite} from "../../../RoomInvite"; -import rate_limited_func from "../../../ratelimitedfunc"; -import {MatrixClientPeg} from "../../../MatrixClientPeg"; -import * as sdk from "../../../index"; -import {CommunityPrototypeStore} from "../../../stores/CommunityPrototypeStore"; +import { isValid3pidInvite } from "../../../RoomInvite"; +import rateLimitedFunction from "../../../ratelimitedfunc"; +import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { CommunityPrototypeStore } from "../../../stores/CommunityPrototypeStore"; import BaseCard from "../right_panel/BaseCard"; -import {RightPanelPhases} from "../../../stores/RightPanelStorePhases"; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import RoomAvatar from "../avatars/RoomAvatar"; import RoomName from "../elements/RoomName"; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; import SettingsStore from "../../../settings/SettingsStore"; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import { RoomState } from 'matrix-js-sdk/src/models/room-state'; +import { User } from "matrix-js-sdk/src/models/user"; +import TruncatedList from '../elements/TruncatedList'; +import Spinner from "../elements/Spinner"; +import SearchBox from "../../structures/SearchBox"; +import AccessibleButton from '../elements/AccessibleButton'; +import EntityTile from "./EntityTile"; +import MemberTile from "./MemberTile"; +import BaseAvatar from '../avatars/BaseAvatar'; const INITIAL_LOAD_NUM_MEMBERS = 30; const INITIAL_LOAD_NUM_INVITED = 5; @@ -40,41 +52,62 @@ const SHOW_MORE_INCREMENT = 100; // matches all ASCII punctuation: !"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ const SORT_REGEX = /[\x21-\x2F\x3A-\x40\x5B-\x60\x7B-\x7E]+/g; +interface IProps { + roomId: string; + onClose(): void; +} + +interface IState { + loading: boolean; + members: Array; + filteredJoinedMembers: Array; + filteredInvitedMembers: Array; + canInvite: boolean; + truncateAtJoined: number; + truncateAtInvited: number; + searchQuery: string; +} + @replaceableComponent("views.rooms.MemberList") -export default class MemberList extends React.Component { +export default class MemberList extends React.Component { + private showPresence = true; + private mounted = false; + private collator: Intl.Collator; + private sortNames = new Map(); // RoomMember -> sortName + constructor(props) { super(props); const cli = MatrixClientPeg.get(); if (cli.hasLazyLoadMembersEnabled()) { // show an empty list - this.state = this._getMembersState([]); + this.state = this.getMembersState([]); } else { - this.state = this._getMembersState(this.roomMembers()); + this.state = this.getMembersState(this.roomMembers()); } cli.on("Room", this.onRoom); // invites & joining after peek const enablePresenceByHsUrl = SdkConfig.get()["enable_presence_by_hs_url"]; const hsUrl = MatrixClientPeg.get().baseUrl; - this._showPresence = true; + this.showPresence = true; if (enablePresenceByHsUrl && enablePresenceByHsUrl[hsUrl] !== undefined) { - this._showPresence = enablePresenceByHsUrl[hsUrl]; + this.showPresence = enablePresenceByHsUrl[hsUrl]; } } // eslint-disable-next-line camelcase UNSAFE_componentWillMount() { const cli = MatrixClientPeg.get(); - this._mounted = true; + this.mounted = true; if (cli.hasLazyLoadMembersEnabled()) { - this._showMembersAccordingToMembershipWithLL(); + this.showMembersAccordingToMembershipWithLL(); cli.on("Room.myMembership", this.onMyMembership); } else { - this._listenForMembersChanges(); + this.listenForMembersChanges(); } } - _listenForMembersChanges() { + private listenForMembersChanges(): void { const cli = MatrixClientPeg.get(); cli.on("RoomState.members", this.onRoomStateMember); cli.on("RoomMember.name", this.onRoomMemberName); @@ -89,7 +122,7 @@ export default class MemberList extends React.Component { } componentWillUnmount() { - this._mounted = false; + this.mounted = false; const cli = MatrixClientPeg.get(); if (cli) { cli.removeListener("RoomState.members", this.onRoomStateMember); @@ -103,7 +136,7 @@ export default class MemberList extends React.Component { } // cancel any pending calls to the rate_limited_funcs - this._updateList.cancelPendingCall(); + this.updateList.cancelPendingCall(); } /** @@ -111,7 +144,7 @@ export default class MemberList extends React.Component { * show a spinner and load the members if the user is joined, * or show the members available so far if the user is invited */ - async _showMembersAccordingToMembershipWithLL() { + private async showMembersAccordingToMembershipWithLL(): Promise { const cli = MatrixClientPeg.get(); if (cli.hasLazyLoadMembersEnabled()) { const cli = MatrixClientPeg.get(); @@ -122,31 +155,31 @@ export default class MemberList extends React.Component { try { await room.loadMembersIfNeeded(); } catch (ex) {/* already logged in RoomView */} - if (this._mounted) { - this.setState(this._getMembersState(this.roomMembers())); - this._listenForMembersChanges(); + if (this.mounted) { + this.setState(this.getMembersState(this.roomMembers())); + this.listenForMembersChanges(); } } else { // show the members we already have loaded - this.setState(this._getMembersState(this.roomMembers())); + this.setState(this.getMembersState(this.roomMembers())); } } } - get canInvite() { + private get canInvite(): boolean { const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.roomId); return room && room.canInvite(cli.getUserId()); } - _getMembersState(members) { - // set the state after determining _showPresence to make sure it's - // taken into account while rerendering + private getMembersState(members: Array): IState { + // set the state after determining showPresence to make sure it's + // taken into account while rendering return { loading: false, members: members, - filteredJoinedMembers: this._filterMembers(members, 'join'), - filteredInvitedMembers: this._filterMembers(members, 'invite'), + filteredJoinedMembers: this.filterMembers(members, 'join'), + filteredInvitedMembers: this.filterMembers(members, 'invite'), canInvite: this.canInvite, // ideally we'd size this to the page height, but @@ -157,72 +190,72 @@ export default class MemberList extends React.Component { }; } - onUserPresenceChange = (event, user) => { + private onUserPresenceChange = (event: MatrixEvent, user: User): void => { // Attach a SINGLE listener for global presence changes then locate the // member tile and re-render it. This is more efficient than every tile // ever attaching their own listener. const tile = this.refs[user.userId]; // console.log(`Got presence update for ${user.userId}. hasTile=${!!tile}`); if (tile) { - this._updateList(); // reorder the membership list + this.updateList(); // reorder the membership list } }; - onRoom = room => { + private onRoom = (room: Room): void => { if (room.roomId !== this.props.roomId) { return; } // We listen for room events because when we accept an invite // we need to wait till the room is fully populated with state // before refreshing the member list else we get a stale list. - this._showMembersAccordingToMembershipWithLL(); + this.showMembersAccordingToMembershipWithLL(); }; - onMyMembership = (room, membership, oldMembership) => { + private onMyMembership = (room: Room, membership: string, oldMembership: string): void => { if (room.roomId === this.props.roomId && membership === "join") { - this._showMembersAccordingToMembershipWithLL(); + this.showMembersAccordingToMembershipWithLL(); } }; - onRoomStateMember = (ev, state, member) => { + private onRoomStateMember = (ev: MatrixEvent, state: RoomState, member: RoomMember): void => { if (member.roomId !== this.props.roomId) { return; } - this._updateList(); + this.updateList(); }; - onRoomMemberName = (ev, member) => { + private onRoomMemberName = (ev: MatrixEvent, member: RoomMember): void => { if (member.roomId !== this.props.roomId) { return; } - this._updateList(); + this.updateList(); }; - onRoomStateEvent = (event, state) => { + private onRoomStateEvent = (event: MatrixEvent, state: RoomState): void => { if (event.getRoomId() === this.props.roomId && event.getType() === "m.room.third_party_invite") { - this._updateList(); + this.updateList(); } if (this.canInvite !== this.state.canInvite) this.setState({ canInvite: this.canInvite }); }; - _updateList = rate_limited_func(() => { - this._updateListNow(); + private updateList = rateLimitedFunction(() => { + this.updateListNow(); }, 500); - _updateListNow() { - // console.log("Updating memberlist"); - const newState = { + private updateListNow(): void { + const members = this.roomMembers() + + this.setState({ loading: false, - members: this.roomMembers(), - }; - newState.filteredJoinedMembers = this._filterMembers(newState.members, 'join', this.state.searchQuery); - newState.filteredInvitedMembers = this._filterMembers(newState.members, 'invite', this.state.searchQuery); - this.setState(newState); + members: members, + filteredJoinedMembers: this.filterMembers(members, 'join', this.state.searchQuery), + filteredInvitedMembers: this.filterMembers(members, 'invite', this.state.searchQuery), + }); } - getMembersWithUser() { + private getMembersWithUser(): Array { if (!this.props.roomId) return []; const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.roomId); @@ -230,15 +263,18 @@ export default class MemberList extends React.Component { const allMembers = Object.values(room.currentState.members); - allMembers.forEach(function(member) { + allMembers.forEach((member) => { // work around a race where you might have a room member object - // before the user object exists. This may or may not cause + // before the user object exists. This may or may not cause // https://github.com/vector-im/vector-web/issues/186 - if (member.user === null) { + if (!member.user) { member.user = cli.getUser(member.userId); } - member.sortName = (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""); + this.sortNames.set( + member, + (member.name[0] === '@' ? member.name.substr(1) : member.name).replace(SORT_REGEX, ""), + ); // XXX: this user may have no lastPresenceTs value! // the right solution here is to fix the race rather than leave it as 0 @@ -247,7 +283,7 @@ export default class MemberList extends React.Component { return allMembers; } - roomMembers() { + private roomMembers(): Array { const allMembers = this.getMembersWithUser(); const filteredAndSortedMembers = allMembers.filter((m) => { return ( @@ -255,23 +291,21 @@ export default class MemberList extends React.Component { ); }); const language = SettingsStore.getValue("language"); - this.collator = new Intl.Collator(language, { sensitivity: 'base', usePunctuation: true }); + this.collator = new Intl.Collator(language, { sensitivity: 'base', ignorePunctuation: false }); filteredAndSortedMembers.sort(this.memberSort); return filteredAndSortedMembers; } - _createOverflowTileJoined = (overflowCount, totalCount) => { - return this._createOverflowTile(overflowCount, totalCount, this._showMoreJoinedMemberList); + private createOverflowTileJoined = (overflowCount: number, totalCount: number): JSX.Element => { + return this.createOverflowTile(overflowCount, totalCount, this.showMoreJoinedMemberList); }; - _createOverflowTileInvited = (overflowCount, totalCount) => { - return this._createOverflowTile(overflowCount, totalCount, this._showMoreInvitedMemberList); + private createOverflowTileInvited = (overflowCount: number, totalCount: number): JSX.Element => { + return this.createOverflowTile(overflowCount, totalCount, this.showMoreInvitedMemberList); }; - _createOverflowTile = (overflowCount, totalCount, onClick) => { + private createOverflowTile = (overflowCount: number, totalCount: number, onClick: () => void): JSX.Element=> { // For now we'll pretend this is any entity. It should probably be a separate tile. - const EntityTile = sdk.getComponent("rooms.EntityTile"); - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); const text = _t("and %(count)s others...", { count: overflowCount }); return ( { + private showMoreJoinedMemberList = (): void => { this.setState({ truncateAtJoined: this.state.truncateAtJoined + SHOW_MORE_INCREMENT, }); }; - _showMoreInvitedMemberList = () => { + private showMoreInvitedMemberList = (): void => { this.setState({ truncateAtInvited: this.state.truncateAtInvited + SHOW_MORE_INCREMENT, }); }; - memberString(member) { + /** + * SHOULD ONLY BE USED BY TESTS + */ + public memberString(member: RoomMember): string { if (!member) { return "(null)"; } else { const u = member.user; - return "(" + member.name + ", " + member.powerLevel + ", " + (u ? u.lastActiveAgo : "") + ", " + (u ? u.getLastActiveTs() : "") + ", " + (u ? u.currentlyActive : "") + ", " + (u ? u.presence : "") + ")"; + return ( + "(" + + member.name + + ", " + + member.powerLevel + + ", " + + (u ? u.lastActiveAgo : "") + + ", " + + (u ? u.getLastActiveTs() : "") + + ", " + + (u ? u.currentlyActive : "") + + ", " + + (u ? u.presence : "") + + ")" + ); } } // returns negative if a comes before b, // returns 0 if a and b are equivalent in ordering // returns positive if a comes after b. - memberSort = (memberA, memberB) => { + private memberSort = (memberA: RoomMember, memberB: RoomMember): number => { // order by presence, with "active now" first. // ...and then by power level // ...and then by last active @@ -325,7 +376,7 @@ export default class MemberList extends React.Component { if (!userA && userB) return 1; // First by presence - if (this._showPresence) { + if (this.showPresence) { const convertPresence = (p) => p === 'unavailable' ? 'online' : p; const presenceIndex = p => { const order = ['active', 'online', 'offline']; @@ -349,31 +400,31 @@ export default class MemberList extends React.Component { } // Third by last active - if (this._showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { + if (this.showPresence && userA.getLastActiveTs() !== userB.getLastActiveTs()) { // console.log("Comparing on last active timestamp - returning"); return userB.getLastActiveTs() - userA.getLastActiveTs(); } // Fourth by name (alphabetical) - return this.collator.compare(memberA.sortName, memberB.sortName); + return this.collator.compare(this.sortNames.get(memberA), this.sortNames.get(memberB)); }; - onSearchQueryChanged = searchQuery => { + private onSearchQueryChanged = (searchQuery: string): void => { this.setState({ searchQuery, - filteredJoinedMembers: this._filterMembers(this.state.members, 'join', searchQuery), - filteredInvitedMembers: this._filterMembers(this.state.members, 'invite', searchQuery), + filteredJoinedMembers: this.filterMembers(this.state.members, 'join', searchQuery), + filteredInvitedMembers: this.filterMembers(this.state.members, 'invite', searchQuery), }); }; - _onPending3pidInviteClick = inviteEvent => { + private onPending3pidInviteClick = (inviteEvent: MatrixEvent): void => { dis.dispatch({ action: 'view_3pid_invite', event: inviteEvent, }); }; - _filterMembers(members, membership, query) { + private filterMembers(members: Array, membership: string, query?: string): Array { return members.filter((m) => { if (query) { query = query.toLowerCase(); @@ -389,7 +440,7 @@ export default class MemberList extends React.Component { }); } - _getPending3PidInvites() { + private getPending3PidInvites(): Array { // include 3pid invites (m.room.third_party_invite) state events. // The HS may have already converted these into m.room.member invites so // we shouldn't add them if the 3pid invite state key (token) is in the @@ -409,42 +460,40 @@ export default class MemberList extends React.Component { } } - _makeMemberTiles(members) { - const MemberTile = sdk.getComponent("rooms.MemberTile"); - const EntityTile = sdk.getComponent("rooms.EntityTile"); - + private makeMemberTiles(members: Array) { return members.map((m) => { - if (m.userId) { + if (m instanceof RoomMember) { // Is a Matrix invite - return ; + return ; } else { // Is a 3pid invite return this._onPending3pidInviteClick(m)} />; + onClick={() => this.onPending3pidInviteClick(m)} />; } }); } - _getChildrenJoined = (start, end) => this._makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end)); - - _getChildCountJoined = () => this.state.filteredJoinedMembers.length; - - _getChildrenInvited = (start, end) => { - let targets = this.state.filteredInvitedMembers; - if (end > this.state.filteredInvitedMembers.length) { - targets = targets.concat(this._getPending3PidInvites()); - } - - return this._makeMemberTiles(targets.slice(start, end)); + private getChildrenJoined = (start: number, end: number): Array => { + return this.makeMemberTiles(this.state.filteredJoinedMembers.slice(start, end)) }; - _getChildCountInvited = () => { - return this.state.filteredInvitedMembers.length + (this._getPending3PidInvites() || []).length; + private getChildCountJoined = (): number => this.state.filteredJoinedMembers.length; + + private getChildrenInvited = (start: number, end: number): Array => { + let targets = this.state.filteredInvitedMembers; + if (end > this.state.filteredInvitedMembers.length) { + targets = targets.concat(this.getPending3PidInvites()); + } + + return this.makeMemberTiles(targets.slice(start, end)); + }; + + private getChildCountInvited = (): number => { + return this.state.filteredInvitedMembers.length + (this.getPending3PidInvites() || []).length; } render() { if (this.state.loading) { - const Spinner = sdk.getComponent("elements.Spinner"); return ; } - const SearchBox = sdk.getComponent('structures.SearchBox'); - const TruncatedList = sdk.getComponent("elements.TruncatedList"); - const cli = MatrixClientPeg.get(); const room = cli.getRoom(this.props.roomId); let inviteButton; @@ -470,22 +516,30 @@ export default class MemberList extends React.Component { inviteButtonText = _t("Invite to this space"); } - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - inviteButton = - + inviteButton = ( + { inviteButtonText } - ; + + ); } let invitedHeader; let invitedSection; - if (this._getChildCountInvited() > 0) { + if (this.getChildCountInvited() > 0) { invitedHeader =

{ _t("Invited") }

; - invitedSection = ; + invitedSection = ( + + ); } const footer = ( @@ -517,17 +571,19 @@ export default class MemberList extends React.Component { previousPhase={previousPhase} >
- + { invitedHeader } { invitedSection }
; } - onInviteButtonClick = () => { + onInviteButtonClick = (): void => { if (MatrixClientPeg.get().isGuest()) { dis.dispatch({action: 'require_registration'}); return; From 0df6200dd0f5ac87c5897a4d7f014ce2d93afed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 14:32:41 +0200 Subject: [PATCH 16/54] Convert MemberList-test to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...MemberList-test.js => MemberList-test.tsx} | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) rename test/components/views/rooms/{MemberList-test.js => MemberList-test.tsx} (88%) diff --git a/test/components/views/rooms/MemberList-test.js b/test/components/views/rooms/MemberList-test.tsx similarity index 88% rename from test/components/views/rooms/MemberList-test.js rename to test/components/views/rooms/MemberList-test.tsx index 28fead770c..8012c43c4b 100644 --- a/test/components/views/rooms/MemberList-test.js +++ b/test/components/views/rooms/MemberList-test.tsx @@ -1,21 +1,36 @@ +/* +Copyright 2021 Šimon Brandner + +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 ReactTestUtils from 'react-dom/test-utils'; import ReactDOM from 'react-dom'; import * as TestUtils from '../../../test-utils'; - -import {MatrixClientPeg} from '../../../../src/MatrixClientPeg'; import sdk from '../../../skinned-sdk'; - -import {Room, RoomMember, User} from 'matrix-js-sdk'; - +import { MatrixClientPeg } from '../../../../src/MatrixClientPeg'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { RoomMember } from 'matrix-js-sdk/src/models/room-member'; +import { User } from "matrix-js-sdk/src/models/user"; import { compare } from "../../../../src/utils/strings"; +import MemberList from "../../../../src/components/views/rooms/MemberList"; function generateRoomId() { return '!' + Math.random().toString().slice(2, 10) + ':domain'; } - describe('MemberList', () => { function createRoom(opts) { const room = new Room(generateRoomId(), null, client.getUserId()); @@ -97,13 +112,19 @@ describe('MemberList', () => { memberListRoom.currentState.members[member.userId] = member; } - const MemberList = sdk.getComponent('views.rooms.MemberList'); const WrappedMemberList = TestUtils.wrapInMatrixClientContext(MemberList); const gatherWrappedRef = (r) => { memberList = r; }; - root = ReactDOM.render(, parentDiv); + root = ReactDOM.render( + ( + + ), + parentDiv, + ); }); afterEach((done) => { @@ -213,8 +234,8 @@ describe('MemberList', () => { }); // Bypass all the event listeners and skip to the good part - memberList._showPresence = enablePresence; - memberList._updateListNow(); + memberList.showPresence = enablePresence; + memberList.updateListNow(); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); @@ -225,7 +246,7 @@ describe('MemberList', () => { // Bypass all the event listeners and skip to the good part memberList._showPresence = enablePresence; - memberList._updateListNow(); + memberList.updateListNow(); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); @@ -254,8 +275,8 @@ describe('MemberList', () => { }); // Bypass all the event listeners and skip to the good part - memberList._showPresence = enablePresence; - memberList._updateListNow(); + memberList.showPresence = enablePresence; + memberList.updateListNow(); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); @@ -273,8 +294,8 @@ describe('MemberList', () => { }); // Bypass all the event listeners and skip to the good part - memberList._showPresence = enablePresence; - memberList._updateListNow(); + memberList.showPresence = enablePresence; + memberList.updateListNow(); const tiles = ReactTestUtils.scryRenderedComponentsWithType(root, MemberTile); expectOrderedByPresenceAndPowerLevel(tiles, enablePresence); From b7a821a9e4d7bbe99f767b2fb86850936d99ebed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 14:59:36 +0200 Subject: [PATCH 17/54] .tsx can also be tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b369e9c27..f87abf9e44 100644 --- a/package.json +++ b/package.json @@ -172,7 +172,7 @@ "jest": { "testEnvironment": "./__test-utils__/environment.js", "testMatch": [ - "/test/**/*-test.[jt]s" + "/test/**/*-test.[jt]s?(x)" ], "setupFiles": [ "jest-canvas-mock" From 5d93216c94f1f76a99fd30b4bbcd73459446ee2a Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 23 Jun 2021 16:10:47 +0100 Subject: [PATCH 18/54] Decrease e2e shield fill mask size so that it doesn't overlap --- res/css/structures/_ToastContainer.scss | 2 +- res/css/views/messages/_common_CryptoEvent.scss | 2 +- res/css/views/rooms/_E2EIcon.scss | 4 ++-- res/css/views/rooms/_EventTile.scss | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/res/css/structures/_ToastContainer.scss b/res/css/structures/_ToastContainer.scss index 14e4c01389..35d6087a1b 100644 --- a/res/css/structures/_ToastContainer.scss +++ b/res/css/structures/_ToastContainer.scss @@ -71,7 +71,7 @@ limitations under the License. &::before { background-color: #ffffff; mask-image: url('$(res)/img/e2e/normal.svg'); - mask-size: 90%; + mask-size: 80%; } &::after { diff --git a/res/css/views/messages/_common_CryptoEvent.scss b/res/css/views/messages/_common_CryptoEvent.scss index 4faa4b594f..bcc40f1181 100644 --- a/res/css/views/messages/_common_CryptoEvent.scss +++ b/res/css/views/messages/_common_CryptoEvent.scss @@ -21,7 +21,7 @@ limitations under the License. mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } &.mx_cryptoEvent_icon::after { diff --git a/res/css/views/rooms/_E2EIcon.scss b/res/css/views/rooms/_E2EIcon.scss index a3473dedec..68ad44cf6a 100644 --- a/res/css/views/rooms/_E2EIcon.scss +++ b/res/css/views/rooms/_E2EIcon.scss @@ -45,7 +45,7 @@ limitations under the License. mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } // transparent-looking border surrounding the shield for when overlain over avatars @@ -59,7 +59,7 @@ limitations under the License. } // shrink the infill of the badge &::before { - mask-size: 65%; + mask-size: 60%; } } diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 3af266caee..27a83e58f8 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -345,7 +345,7 @@ $hover-select-border: 4px; mask-image: url('$(res)/img/e2e/normal.svg'); mask-repeat: no-repeat; mask-position: center; - mask-size: 90%; + mask-size: 80%; } } From e696a1d5dc4d6848e47caf64e0c0c58bda659c8a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Jun 2021 10:31:08 -0600 Subject: [PATCH 19/54] Update membership reason handling, including leave reason displaying Incorporates ideas from https://github.com/matrix-org/matrix-react-sdk/pull/6198 --- src/TextForEvent.ts | 47 +++++++++++++++++++++++-------------- src/i18n/strings/en_EN.json | 6 ++++- 2 files changed, 35 insertions(+), 18 deletions(-) diff --git a/src/TextForEvent.ts b/src/TextForEvent.ts index 649c53664e..55a7813c6f 100644 --- a/src/TextForEvent.ts +++ b/src/TextForEvent.ts @@ -31,8 +31,8 @@ function textForMemberEvent(ev): () => string | null { const targetName = ev.target ? ev.target.name : ev.getStateKey(); const prevContent = ev.getPrevContent(); const content = ev.getContent(); + const reason = content.reason; - const getReason = () => content.reason ? (_t('Reason') + ': ' + content.reason) : ''; switch (content.membership) { case 'invite': { const threePidContent = content.third_party_invite; @@ -43,14 +43,16 @@ function textForMemberEvent(ev): () => string | null { displayName: threePidContent.display_name, }); } else { - return () => _t('%(targetName)s accepted an invitation.', {targetName}); + return () => _t('%(targetName)s accepted an invitation.', { targetName }); } } else { - return () => _t('%(senderName)s invited %(targetName)s.', {senderName, targetName}); + return () => _t('%(senderName)s invited %(targetName)s.', { senderName, targetName }); } } case 'ban': - return () => _t('%(senderName)s banned %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s banned %(targetName)s. Reason: %(reason)s', { senderName, targetName, reason }) + : _t('%(senderName)s banned %(targetName)s.', { senderName, targetName }); case 'join': if (prevContent && prevContent.membership === 'join') { if (prevContent.displayname && content.displayname && prevContent.displayname !== content.displayname) { @@ -69,38 +71,49 @@ function textForMemberEvent(ev): () => string | null { oldDisplayName: prevContent.displayname, }); } else if (prevContent.avatar_url && !content.avatar_url) { - return () => _t('%(senderName)s removed their profile picture.', {senderName}); + return () => _t('%(senderName)s removed their profile picture.', { senderName }); } else if (prevContent.avatar_url && content.avatar_url && prevContent.avatar_url !== content.avatar_url) { - return () => _t('%(senderName)s changed their profile picture.', {senderName}); + return () => _t('%(senderName)s changed their profile picture.', { senderName }); } else if (!prevContent.avatar_url && content.avatar_url) { - return () => _t('%(senderName)s set a profile picture.', {senderName}); + return () => _t('%(senderName)s set a profile picture.', { senderName }); } else if (SettingsStore.getValue("showHiddenEventsInTimeline")) { // This is a null rejoin, it will only be visible if the Labs option is enabled - return () => _t("%(senderName)s made no change.", {senderName}); + return () => _t("%(senderName)s made no change.", { senderName }); } else { return null; } } else { if (!ev.target) console.warn("Join message has no target! -- " + ev.getContent().state_key); - return () => _t('%(targetName)s joined the room.', {targetName}); + return () => _t('%(targetName)s joined the room.', { targetName }); } case 'leave': if (ev.getSender() === ev.getStateKey()) { if (prevContent.membership === "invite") { - return () => _t('%(targetName)s rejected the invitation.', {targetName}); + return () => _t('%(targetName)s rejected the invitation.', { targetName }); } else { - return () => _t('%(targetName)s left the room.', {targetName}); + return () => reason + ? _t('%(targetName)s left the room. Reason: %(reason)s', { targetName, reason }) + : _t('%(targetName)s left the room.', { targetName }); } } else if (prevContent.membership === "ban") { - return () => _t('%(senderName)s unbanned %(targetName)s.', {senderName, targetName}); + return () => _t('%(senderName)s unbanned %(targetName)s.', { senderName, targetName }); } else if (prevContent.membership === "invite") { - return () => _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { - senderName, - targetName, - }) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s withdrew %(targetName)s\'s invitation. Reason: %(reason)s', { + senderName, + targetName, + reason, + }) + : _t('%(senderName)s withdrew %(targetName)s\'s invitation.', { senderName, targetName }) } else if (prevContent.membership === "join") { - return () => _t('%(senderName)s kicked %(targetName)s.', {senderName, targetName}) + ' ' + getReason(); + return () => reason + ? _t('%(senderName)s kicked %(targetName)s. Reason: %(reason)s', { + senderName, + targetName, + reason, + }) + : _t('%(senderName)s kicked %(targetName)s.', { senderName, targetName }); } else { return null; } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 7751c2eb32..5bee2e2f2c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -489,10 +489,10 @@ "Converts the room to a DM": "Converts the room to a DM", "Converts the DM to a room": "Converts the DM to a room", "Displays action": "Displays action", - "Reason": "Reason", "%(targetName)s accepted the invitation for %(displayName)s.": "%(targetName)s accepted the invitation for %(displayName)s.", "%(targetName)s accepted an invitation.": "%(targetName)s accepted an invitation.", "%(senderName)s invited %(targetName)s.": "%(senderName)s invited %(targetName)s.", + "%(senderName)s banned %(targetName)s. Reason: %(reason)s": "%(senderName)s banned %(targetName)s. Reason: %(reason)s", "%(senderName)s banned %(targetName)s.": "%(senderName)s banned %(targetName)s.", "%(oldDisplayName)s changed their display name to %(displayName)s.": "%(oldDisplayName)s changed their display name to %(displayName)s.", "%(senderName)s set their display name to %(displayName)s.": "%(senderName)s set their display name to %(displayName)s.", @@ -503,9 +503,12 @@ "%(senderName)s made no change.": "%(senderName)s made no change.", "%(targetName)s joined the room.": "%(targetName)s joined the room.", "%(targetName)s rejected the invitation.": "%(targetName)s rejected the invitation.", + "%(targetName)s left the room. Reason: %(reason)s": "%(targetName)s left the room. Reason: %(reason)s", "%(targetName)s left the room.": "%(targetName)s left the room.", "%(senderName)s unbanned %(targetName)s.": "%(senderName)s unbanned %(targetName)s.", + "%(senderName)s withdrew %(targetName)s's invitation. Reason: %(reason)s": "%(senderName)s withdrew %(targetName)s's invitation. Reason: %(reason)s", "%(senderName)s withdrew %(targetName)s's invitation.": "%(senderName)s withdrew %(targetName)s's invitation.", + "%(senderName)s kicked %(targetName)s. Reason: %(reason)s": "%(senderName)s kicked %(targetName)s. Reason: %(reason)s", "%(senderName)s kicked %(targetName)s.": "%(senderName)s kicked %(targetName)s.", "%(senderDisplayName)s changed the topic to \"%(topic)s\".": "%(senderDisplayName)s changed the topic to \"%(topic)s\".", "%(senderDisplayName)s removed the room name.": "%(senderDisplayName)s removed the room name.", @@ -1410,6 +1413,7 @@ "Failed to unban": "Failed to unban", "Unban": "Unban", "Banned by %(displayName)s": "Banned by %(displayName)s", + "Reason": "Reason", "Error changing power level requirement": "Error changing power level requirement", "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.": "An error occurred changing the room's power level requirements. Ensure you have sufficient permissions and try again.", "Error changing power level": "Error changing power level", From e290fdaabcb0259b26412b34b3ec4cd11e76164a Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 23 Jun 2021 11:21:56 -0600 Subject: [PATCH 20/54] Update widget-api for https://github.com/matrix-org/matrix-react-sdk/pull/6178 --- package.json | 2 +- yarn.lock | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 6b369e9c27..bc79b791d4 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.20", "matrix-js-sdk": "12.0.0", - "matrix-widget-api": "^0.1.0-beta.14", + "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index 32ca30a996..3bcb8de404 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1334,6 +1334,7 @@ "@matrix-org/olm@https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz": version "3.2.3" + uid cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4 resolved "https://gitlab.matrix.org/api/v4/projects/27/packages/npm/@matrix-org/olm/-/@matrix-org/olm-3.2.3.tgz#cc332fdd25c08ef0e40f4d33fc3f822a0f98b6f4" "@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents": @@ -5772,10 +5773,10 @@ matrix-react-test-utils@^0.2.3: "@babel/traverse" "^7.13.17" walk "^2.3.14" -matrix-widget-api@^0.1.0-beta.14: - version "0.1.0-beta.14" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.14.tgz#e38beed71c5ebd62c1ac1d79ef262d7150b42c70" - integrity sha512-5tC6LO1vCblKg/Hfzf5U1eHPz1nHUZIobAm3gkEKV5vpYPgRpr8KdkLiGB78VZid0tB17CVtAb4VKI8CQ3lhAQ== +matrix-widget-api@^0.1.0-beta.15: + version "0.1.0-beta.15" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745" + integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From d5acfc6cf453915deed0bf6ac6bca2b5920d73f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 23 Jun 2021 17:27:53 +0200 Subject: [PATCH 21/54] Convert EntityTile to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../rooms/{EntityTile.js => EntityTile.tsx} | 94 ++++++++++--------- 1 file changed, 49 insertions(+), 45 deletions(-) rename src/components/views/rooms/{EntityTile.js => EntityTile.tsx} (75%) diff --git a/src/components/views/rooms/EntityTile.js b/src/components/views/rooms/EntityTile.tsx similarity index 75% rename from src/components/views/rooms/EntityTile.js rename to src/components/views/rooms/EntityTile.tsx index a92d87643c..53c5900fbf 100644 --- a/src/components/views/rooms/EntityTile.js +++ b/src/components/views/rooms/EntityTile.tsx @@ -16,14 +16,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; -import * as sdk from '../../../index'; +import React, { createRef } from 'react'; import AccessibleButton from '../elements/AccessibleButton'; -import { _t } from '../../../languageHandler'; +import { _td } from '../../../languageHandler'; import classNames from "classnames"; import E2EIcon from './E2EIcon'; -import {replaceableComponent} from "../../../utils/replaceableComponent"; +import { replaceableComponent } from "../../../utils/replaceableComponent"; +import BaseAvatar from '../avatars/BaseAvatar'; +import PresenceLabel from "./PresenceLabel"; + +const PowerLabel: Record = { + "admin": _td("Admin"), + "moderator": _td("Mod"), +} const PRESENCE_CLASS = { "offline": "mx_EntityTile_offline", @@ -31,14 +36,14 @@ const PRESENCE_CLASS = { "unavailable": "mx_EntityTile_unavailable", }; -function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { +function presenceClassForMember(presenceState: string, lastActiveAgo: number, showPresence: boolean) { if (showPresence === false) { return 'mx_EntityTile_online_beenactive'; } // offline is split into two categories depending on whether we have // a last_active_ago for them. - if (presenceState == 'offline') { + if (presenceState === 'offline') { if (lastActiveAgo) { return PRESENCE_CLASS['offline'] + '_beenactive'; } else { @@ -51,29 +56,34 @@ function presenceClassForMember(presenceState, lastActiveAgo, showPresence) { } } -@replaceableComponent("views.rooms.EntityTile") -class EntityTile extends React.Component { - static propTypes = { - name: PropTypes.string, - title: PropTypes.string, - avatarJsx: PropTypes.any, // - className: PropTypes.string, - presenceState: PropTypes.string, - presenceLastActiveAgo: PropTypes.number, - presenceLastTs: PropTypes.number, - presenceCurrentlyActive: PropTypes.bool, - showInviteButton: PropTypes.bool, - shouldComponentUpdate: PropTypes.func, - onClick: PropTypes.func, - suppressOnHover: PropTypes.bool, - showPresence: PropTypes.bool, - subtextLabel: PropTypes.string, - e2eStatus: PropTypes.string, - }; +interface IProps { + name?: string, + title?: string, + avatarJsx?: JSX.Element, // + className?: string, + presenceState?: string, + presenceLastActiveAgo?: number, + presenceLastTs?: number, + presenceCurrentlyActive?: boolean, + showInviteButton?: boolean, + shouldComponentUpdate?(nextProps: IProps, nextState: IState): boolean, + onClick?(): void, + suppressOnHover?: boolean, + showPresence?: boolean, + subtextLabel?: string, + e2eStatus?: string, + powerStatus?: string, +} +interface IState { + hover: boolean; +} + +@replaceableComponent("views.rooms.EntityTile") +export default class EntityTile extends React.Component { static defaultProps = { - shouldComponentUpdate: function(nextProps, nextState) { return true; }, - onClick: function() {}, + shouldComponentUpdate: (nextProps: IProps, nextState: IState) => { return true; }, + onClick: () => {}, presenceState: "offline", presenceLastActiveAgo: 0, presenceLastTs: 0, @@ -81,12 +91,17 @@ class EntityTile extends React.Component { suppressOnHover: false, showPresence: true, }; + private container = createRef(); - state = { - hover: false, - }; + constructor(props: IProps) { + super(props); - shouldComponentUpdate(nextProps, nextState) { + this.state = { + hover: false, + }; + } + + shouldComponentUpdate(nextProps: IProps, nextState: IState) { if (this.state.hover !== nextState.hover) return true; return this.props.shouldComponentUpdate(nextProps, nextState); } @@ -110,7 +125,6 @@ class EntityTile extends React.Component { const activeAgo = this.props.presenceLastActiveAgo ? (Date.now() - (this.props.presenceLastTs - this.props.presenceLastActiveAgo)) : -1; - const PresenceLabel = sdk.getComponent("rooms.PresenceLabel"); let presenceLabel = null; if (this.props.showPresence) { presenceLabel = {powerText}
; } @@ -168,14 +179,12 @@ class EntityTile extends React.Component { e2eIcon = ; } - const BaseAvatar = sdk.getComponent('avatars.BaseAvatar'); - const av = this.props.avatarJsx ||