From 2e9cacdeb1d57537dc08dee931d8e4c87d837c59 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 10:22:43 +0200 Subject: [PATCH 01/35] Migrate JumpToBottomButton to TypeScript --- .../{JumpToBottomButton.js => JumpToBottomButton.tsx} | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) rename src/components/views/rooms/{JumpToBottomButton.js => JumpToBottomButton.tsx} (83%) diff --git a/src/components/views/rooms/JumpToBottomButton.js b/src/components/views/rooms/JumpToBottomButton.tsx similarity index 83% rename from src/components/views/rooms/JumpToBottomButton.js rename to src/components/views/rooms/JumpToBottomButton.tsx index d2e2a391a6..0b680d093d 100644 --- a/src/components/views/rooms/JumpToBottomButton.js +++ b/src/components/views/rooms/JumpToBottomButton.tsx @@ -14,11 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from "react"; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; import classNames from 'classnames'; -export default (props) => { +interface IProps { + numUnreadMessages: number; + highlight: boolean; + onScrollToBottomClick: (e: React.MouseEvent) => void; +} + +const JumpToBottomButton: React.FC = (props) => { const className = classNames({ 'mx_JumpToBottomButton': true, 'mx_JumpToBottomButton_highlight': props.highlight, @@ -36,3 +43,5 @@ export default (props) => { { badge } ); }; + +export default JumpToBottomButton; From e9e6269da7487c7b3456b149d757e08760db6136 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 10:31:18 +0200 Subject: [PATCH 02/35] Migrat ReadReceiptMarker to TypeScript --- src/components/views/rooms/EventTile.tsx | 1 + ...ReceiptMarker.js => ReadReceiptMarker.tsx} | 114 ++++++++++-------- 2 files changed, 64 insertions(+), 51 deletions(-) rename src/components/views/rooms/{ReadReceiptMarker.js => ReadReceiptMarker.tsx} (74%) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 301e33ec42..c5fbb99ede 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -240,6 +240,7 @@ interface IProps { // opaque readreceipt info for each userId; used by ReadReceiptMarker // to manage its animations. Should be an empty object when the room // first loads + // TODO: Proper typing for RR info readReceiptMap?: any; // A function which is used to check if the parent panel is being diff --git a/src/components/views/rooms/ReadReceiptMarker.js b/src/components/views/rooms/ReadReceiptMarker.tsx similarity index 74% rename from src/components/views/rooms/ReadReceiptMarker.js rename to src/components/views/rooms/ReadReceiptMarker.tsx index c9688b4d29..11e7563f11 100644 --- a/src/components/views/rooms/ReadReceiptMarker.js +++ b/src/components/views/rooms/ReadReceiptMarker.tsx @@ -16,7 +16,8 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; +import { RoomMember } from 'matrix-js-sdk/src'; + import { _t } from '../../../languageHandler'; import { formatDate } from '../../../DateUtils'; import NodeAnimator from "../../../NodeAnimator"; @@ -24,53 +25,64 @@ import * as sdk from "../../../index"; import { toPx } from "../../../utils/units"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + // the RoomMember to show the RR for + member?: RoomMember; + // userId to fallback the avatar to + // if the member hasn't been loaded yet + fallbackUserId: string; + + // number of pixels to offset the avatar from the right of its parent; + // typically a negative value. + leftOffset?: number; + + // true to hide the avatar (it will still be animated) + hidden?: boolean; + + // don't animate this RR into position + suppressAnimation?: boolean; + + // an opaque object for storing information about this user's RR in + // this room + // TODO: proper typing for RR info + readReceiptInfo: any; + + // A function which is used to check if the parent panel is being + // unmounted, to avoid unnecessary work. Should return true if we + // are being unmounted. + checkUnmounting?: () => boolean; + + // callback for clicks on this RR + onClick?: (e: React.MouseEvent) => void; + + // Timestamp when the receipt was read + timestamp?: number; + + // True to show twelve hour format, false otherwise + showTwelveHour?: boolean; +} + +interface IState { + suppressDisplay: boolean; + startStyles?: IReadReceiptMarkerStyle[]; +} + +interface IReadReceiptMarkerStyle { + top: number; + left: number; +} + @replaceableComponent("views.rooms.ReadReceiptMarker") -export default class ReadReceiptMarker extends React.PureComponent { - static propTypes = { - // the RoomMember to show the RR for - member: PropTypes.object, - // userId to fallback the avatar to - // if the member hasn't been loaded yet - fallbackUserId: PropTypes.string.isRequired, - - // number of pixels to offset the avatar from the right of its parent; - // typically a negative value. - leftOffset: PropTypes.number, - - // true to hide the avatar (it will still be animated) - hidden: PropTypes.bool, - - // don't animate this RR into position - suppressAnimation: PropTypes.bool, - - // an opaque object for storing information about this user's RR in - // this room - readReceiptInfo: PropTypes.object, - - // A function which is used to check if the parent panel is being - // unmounted, to avoid unnecessary work. Should return true if we - // are being unmounted. - checkUnmounting: PropTypes.func, - - // callback for clicks on this RR - onClick: PropTypes.func, - - // Timestamp when the receipt was read - timestamp: PropTypes.number, - - // True to show twelve hour format, false otherwise - showTwelveHour: PropTypes.bool, - }; +export default class ReadReceiptMarker extends React.PureComponent { + private avatar: React.RefObject = createRef(); static defaultProps = { leftOffset: 0, }; - constructor(props) { + constructor(props: IProps) { super(props); - this._avatar = createRef(); - this.state = { // if we are going to animate the RR, we don't show it on first render, // and instead just add a placeholder to the DOM; once we've been @@ -80,7 +92,7 @@ export default class ReadReceiptMarker extends React.PureComponent { }; } - componentWillUnmount() { + public componentWillUnmount(): void { // before we remove the rr, store its location in the map, so that if // it reappears, it can be animated from the right place. const rrInfo = this.props.readReceiptInfo; @@ -95,29 +107,29 @@ export default class ReadReceiptMarker extends React.PureComponent { return; } - const avatarNode = this._avatar.current; + const avatarNode = this.avatar.current; rrInfo.top = avatarNode.offsetTop; rrInfo.left = avatarNode.offsetLeft; rrInfo.parent = avatarNode.offsetParent; } - componentDidMount() { + public componentDidMount(): void { if (!this.state.suppressDisplay) { // we've already done our display - nothing more to do. return; } - this._animateMarker(); + this.animateMarker(); } - componentDidUpdate(prevProps) { + public componentDidUpdate(prevProps: IProps): void { const differentLeftOffset = prevProps.leftOffset !== this.props.leftOffset; const visibilityChanged = prevProps.hidden !== this.props.hidden; if (differentLeftOffset || visibilityChanged) { - this._animateMarker(); + this.animateMarker(); } } - _animateMarker() { + private animateMarker(): void { // treat new RRs as though they were off the top of the screen let oldTop = -15; @@ -126,7 +138,7 @@ export default class ReadReceiptMarker extends React.PureComponent { oldTop = oldInfo.top + oldInfo.parent.getBoundingClientRect().top; } - const newElement = this._avatar.current; + const newElement = this.avatar.current; let startTopOffset; if (!newElement.offsetParent) { // this seems to happen sometimes for reasons I don't understand @@ -156,10 +168,10 @@ export default class ReadReceiptMarker extends React.PureComponent { }); } - render() { + public render(): JSX.Element { const MemberAvatar = sdk.getComponent('avatars.MemberAvatar'); if (this.state.suppressDisplay) { - return
; + return
; } const style = { @@ -198,7 +210,7 @@ export default class ReadReceiptMarker extends React.PureComponent { style={style} title={title} onClick={this.props.onClick} - inputRef={this._avatar} + inputRef={this.avatar} /> ); From 7290a65924b830868ecb7c0bf712d2a1462514c3 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 10:36:04 +0200 Subject: [PATCH 03/35] Migrate RoomDetailList to TypeScript --- .../{RoomDetailList.js => RoomDetailList.tsx} | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) rename src/components/views/rooms/{RoomDetailList.js => RoomDetailList.tsx} (81%) diff --git a/src/components/views/rooms/RoomDetailList.js b/src/components/views/rooms/RoomDetailList.tsx similarity index 81% rename from src/components/views/rooms/RoomDetailList.js rename to src/components/views/rooms/RoomDetailList.tsx index bf2f5418c9..ee7383d7c7 100644 --- a/src/components/views/rooms/RoomDetailList.js +++ b/src/components/views/rooms/RoomDetailList.tsx @@ -14,24 +14,23 @@ See the License for the specific language governing permissions and limitations under the License. */ +import React from 'react'; +import { Room } from 'matrix-js-sdk/src'; +import classNames from 'classnames'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; -import React from 'react'; import { _t } from '../../../languageHandler'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { roomShape } from './RoomDetailRow'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.rooms.RoomDetailList") -export default class RoomDetailList extends React.Component { - static propTypes = { - rooms: PropTypes.arrayOf(roomShape), - className: PropTypes.string, - }; +interface IProps { + rooms?: Room[]; + className?: string; +} - getRows() { +@replaceableComponent("views.rooms.RoomDetailList") +export default class RoomDetailList extends React.Component { + public getRows(): JSX.Element[] { if (!this.props.rooms) return []; const RoomDetailRow = sdk.getComponent('rooms.RoomDetailRow'); @@ -40,15 +39,15 @@ export default class RoomDetailList extends React.Component { }); } - onDetailsClick = (ev, room) => { + public onDetailsClick = (ev: React.MouseEvent, room: Room): void => { dis.dispatch({ action: 'view_room', room_id: room.roomId, - room_alias: room.canonicalAlias || (room.aliases || [])[0], + room_alias: room.getCanonicalAlias() || (room.getAltAliases() || [])[0], }); }; - render() { + public render(): JSX.Element { const rows = this.getRows(); let rooms; if (rows.length === 0) { From 7e4c88f6ba11d610e4930daf8eab0cc3f1698738 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 10:46:38 +0200 Subject: [PATCH 04/35] Migrate RoomUpgradeWarningBar to TypeScript --- ...arningBar.js => RoomUpgradeWarningBar.tsx} | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) rename src/components/views/rooms/{RoomUpgradeWarningBar.js => RoomUpgradeWarningBar.tsx} (88%) diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.js b/src/components/views/rooms/RoomUpgradeWarningBar.tsx similarity index 88% rename from src/components/views/rooms/RoomUpgradeWarningBar.js rename to src/components/views/rooms/RoomUpgradeWarningBar.tsx index 384845cdf9..6706e248e0 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.js +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2018-2020 New Vector Ltd +Copyright 2018-2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src'; import * as sdk from '../../../index'; import Modal from '../../../Modal'; @@ -23,33 +23,31 @@ import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + room: Room; +} + +interface IState { + upgraded?: boolean; +} + @replaceableComponent("views.rooms.RoomUpgradeWarningBar") -export default class RoomUpgradeWarningBar extends React.PureComponent { - static propTypes = { - room: PropTypes.object.isRequired, - recommendation: PropTypes.object.isRequired, - }; - - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { +export default class RoomUpgradeWarningBar extends React.PureComponent { + public componentDidMount(): void { const tombstone = this.props.room.currentState.getStateEvents("m.room.tombstone", ""); this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room }); - MatrixClientPeg.get().on("RoomState.events", this._onStateEvents); + MatrixClientPeg.get().on("RoomState.events", this.onStateEvents); } - componentWillUnmount() { + public componentWillUnmount(): void { const cli = MatrixClientPeg.get(); if (cli) { - cli.removeListener("RoomState.events", this._onStateEvents); + cli.removeListener("RoomState.events", this.onStateEvents); } } - _onStateEvents = (event, state) => { + private onStateEvents = (event: MatrixEvent, state: RoomState): void => { if (!this.props.room || event.getRoomId() !== this.props.room.roomId) { return; } @@ -60,12 +58,12 @@ export default class RoomUpgradeWarningBar extends React.PureComponent { this.setState({ upgraded: tombstone && tombstone.getContent().replacement_room }); }; - onUpgradeClick = () => { + private onUpgradeClick = (): void => { const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog'); Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room: this.props.room }); }; - render() { + public render(): JSX.Element { const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); let doUpgradeWarnings = ( From eb120901aea7c1fb5b54bb9ceb8cf7516a7498d7 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 10:48:30 +0200 Subject: [PATCH 05/35] Migrate SimpleRoomHeader to TypeScript --- ...mpleRoomHeader.js => SimpleRoomHeader.tsx} | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) rename src/components/views/rooms/{SimpleRoomHeader.js => SimpleRoomHeader.tsx} (84%) diff --git a/src/components/views/rooms/SimpleRoomHeader.js b/src/components/views/rooms/SimpleRoomHeader.tsx similarity index 84% rename from src/components/views/rooms/SimpleRoomHeader.js rename to src/components/views/rooms/SimpleRoomHeader.tsx index a2b5566e39..b81e906559 100644 --- a/src/components/views/rooms/SimpleRoomHeader.js +++ b/src/components/views/rooms/SimpleRoomHeader.tsx @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,23 +16,21 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + title?: string; + // `src` to an image. Optional. + icon?: string; +} + /* * A stripped-down room header used for things like the user settings * and room directory. */ @replaceableComponent("views.rooms.SimpleRoomHeader") -export default class SimpleRoomHeader extends React.Component { - static propTypes = { - title: PropTypes.string, - - // `src` to an image. Optional. - icon: PropTypes.string, - }; - - render() { +export default class SimpleRoomHeader extends React.PureComponent { + public render(): JSX.Element { let icon; if (this.props.icon) { icon = Date: Sat, 14 Aug 2021 10:51:08 +0200 Subject: [PATCH 06/35] Migrate TopUnreadMessagesBar to TypeScript --- ...dMessagesBar.js => TopUnreadMessagesBar.tsx} | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) rename src/components/views/rooms/{TopUnreadMessagesBar.js => TopUnreadMessagesBar.tsx} (84%) diff --git a/src/components/views/rooms/TopUnreadMessagesBar.js b/src/components/views/rooms/TopUnreadMessagesBar.tsx similarity index 84% rename from src/components/views/rooms/TopUnreadMessagesBar.js rename to src/components/views/rooms/TopUnreadMessagesBar.tsx index d2a3e3a303..01797299cf 100644 --- a/src/components/views/rooms/TopUnreadMessagesBar.js +++ b/src/components/views/rooms/TopUnreadMessagesBar.tsx @@ -1,7 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2019 New Vector Ltd +Copyright 2019-2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,19 +17,18 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { _t } from '../../../languageHandler'; import AccessibleButton from '../elements/AccessibleButton'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.rooms.TopUnreadMessagesBar") -export default class TopUnreadMessagesBar extends React.Component { - static propTypes = { - onScrollUpClick: PropTypes.func, - onCloseClick: PropTypes.func, - }; +interface IProps { + onScrollUpClick?: (e: React.MouseEvent) => void; + onCloseClick?: (e: React.MouseEvent) => void; +} - render() { +@replaceableComponent("views.rooms.TopUnreadMessagesBar") +export default class TopUnreadMessagesBar extends React.PureComponent { + public render(): JSX.Element { return (
Date: Sat, 14 Aug 2021 10:53:53 +0200 Subject: [PATCH 07/35] Migrate AvatarSetting to TypeScript --- .../{AvatarSetting.js => AvatarSetting.tsx} | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) rename src/components/views/settings/{AvatarSetting.js => AvatarSetting.tsx} (86%) diff --git a/src/components/views/settings/AvatarSetting.js b/src/components/views/settings/AvatarSetting.tsx similarity index 86% rename from src/components/views/settings/AvatarSetting.js rename to src/components/views/settings/AvatarSetting.tsx index f22c4f1c85..806d0adb73 100644 --- a/src/components/views/settings/AvatarSetting.js +++ b/src/components/views/settings/AvatarSetting.tsx @@ -15,12 +15,19 @@ limitations under the License. */ import React, { useState } from "react"; -import PropTypes from "prop-types"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import classNames from "classnames"; -const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar }) => { +interface IProps { + avatarUrl?: string; + avatarName: string; // name of user/room the avatar belongs to + uploadAvatar?: (e: React.MouseEvent) => void; + removeAvatar?: (e: React.MouseEvent) => void; + avatarAltText: string; +} + +const AvatarSetting: React.FC = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, removeAvatar }) => { const [isHovering, setIsHovering] = useState(false); const hoveringProps = { onMouseEnter: () => setIsHovering(true), @@ -78,12 +85,4 @@ const AvatarSetting = ({ avatarUrl, avatarAltText, avatarName, uploadAvatar, rem
; }; -AvatarSetting.propTypes = { - avatarUrl: PropTypes.string, - avatarName: PropTypes.string.isRequired, // name of user/room the avatar belongs to - uploadAvatar: PropTypes.func, - removeAvatar: PropTypes.func, - avatarAltText: PropTypes.string.isRequired, -}; - export default AvatarSetting; From bedfbedff0ec3ffd8e081eef91951b76702ccf65 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 11:04:36 +0200 Subject: [PATCH 08/35] Migrate ChangeAvatar to TypeScript --- .../{ChangeAvatar.js => ChangeAvatar.tsx} | 96 ++++++++++--------- 1 file changed, 52 insertions(+), 44 deletions(-) rename src/components/views/settings/{ChangeAvatar.js => ChangeAvatar.tsx} (74%) diff --git a/src/components/views/settings/ChangeAvatar.js b/src/components/views/settings/ChangeAvatar.tsx similarity index 74% rename from src/components/views/settings/ChangeAvatar.js rename to src/components/views/settings/ChangeAvatar.tsx index c3a1544cdc..6394bad3de 100644 --- a/src/components/views/settings/ChangeAvatar.js +++ b/src/components/views/settings/ChangeAvatar.tsx @@ -1,5 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd +Copyright 2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,54 +16,62 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Spinner from '../elements/Spinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import { MatrixEvent, Room } from '../../../../../matrix-js-sdk/src'; + +interface IProps { + initialAvatarUrl?: string; + room?: Room; + // if false, you need to call changeAvatar.onFileSelected yourself. + showUploadSection?: boolean; + width?: number; + height?: number; + className?: string; +} + +interface IState { + avatarUrl?: string; + errorText?: string; + phase?: Phases; +} + +enum Phases { + Display = "display", + Uploading = "uploading", + Error = "error", +} @replaceableComponent("views.settings.ChangeAvatar") -export default class ChangeAvatar extends React.Component { - static propTypes = { - initialAvatarUrl: PropTypes.string, - room: PropTypes.object, - // if false, you need to call changeAvatar.onFileSelected yourself. - showUploadSection: PropTypes.bool, - width: PropTypes.number, - height: PropTypes.number, - className: PropTypes.string, - }; - - static Phases = { - Display: "display", - Uploading: "uploading", - Error: "error", - }; - - static defaultProps = { +export default class ChangeAvatar extends React.Component { + public static defaultProps = { showUploadSection: true, className: "", width: 80, height: 80, }; - constructor(props) { + private avatarSet = false; + + constructor(props: IProps) { super(props); this.state = { avatarUrl: this.props.initialAvatarUrl, - phase: ChangeAvatar.Phases.Display, + phase: Phases.Display, }; } - componentDidMount() { + public componentDidMount(): void { MatrixClientPeg.get().on("RoomState.events", this.onRoomStateEvents); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(newProps) { // eslint-disable-line camelcase + public UNSAFE_componentWillReceiveProps(newProps): void { // eslint-disable-line camelcase if (this.avatarSet) { // don't clobber what the user has just set return; @@ -72,13 +81,13 @@ export default class ChangeAvatar extends React.Component { }); } - componentWillUnmount() { + public componentWillUnmount(): void { if (MatrixClientPeg.get()) { MatrixClientPeg.get().removeListener("RoomState.events", this.onRoomStateEvents); } } - onRoomStateEvents = (ev) => { + public onRoomStateEvents = (ev: MatrixEvent) => { if (!this.props.room) { return; } @@ -94,18 +103,17 @@ export default class ChangeAvatar extends React.Component { } }; - setAvatarFromFile(file) { + public setAvatarFromFile(file): Promise<{}> { let newUrl = null; this.setState({ - phase: ChangeAvatar.Phases.Uploading, + phase: Phases.Uploading, }); - const self = this; - const httpPromise = MatrixClientPeg.get().uploadContent(file).then(function(url) { + const httpPromise = MatrixClientPeg.get().uploadContent(file).then((url) => { newUrl = url; - if (self.props.room) { + if (this.props.room) { return MatrixClientPeg.get().sendStateEvent( - self.props.room.roomId, + this.props.room.roomId, 'm.room.avatar', { url: url }, '', @@ -115,33 +123,33 @@ export default class ChangeAvatar extends React.Component { } }); - httpPromise.then(function() { - self.setState({ - phase: ChangeAvatar.Phases.Display, + httpPromise.then(() => { + this.setState({ + phase: Phases.Display, avatarUrl: mediaFromMxc(newUrl).srcHttp, }); - }, function(error) { - self.setState({ - phase: ChangeAvatar.Phases.Error, + }, () => { + this.setState({ + phase: Phases.Error, }); - self.onError(error); + this.onError(); }); return httpPromise; } - onFileSelected = (ev) => { + private onFileSelected = (ev: React.ChangeEvent) => { this.avatarSet = true; return this.setAvatarFromFile(ev.target.files[0]); }; - onError = (error) => { + private onError = (): void => { this.setState({ errorText: _t("Failed to upload profile picture!"), }); }; - render() { + public render(): JSX.Element { let avatarImg; // Having just set an avatar we just display that since it will take a little // time to propagate through to the RoomAvatar. @@ -178,8 +186,8 @@ export default class ChangeAvatar extends React.Component { } switch (this.state.phase) { - case ChangeAvatar.Phases.Display: - case ChangeAvatar.Phases.Error: + case Phases.Display: + case Phases.Error: return (
@@ -188,7 +196,7 @@ export default class ChangeAvatar extends React.Component { { uploadSection }
); - case ChangeAvatar.Phases.Uploading: + case Phases.Uploading: return ( ); From 1e431057ff62f9511181c457652c2ed3b8aa826f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 11:06:34 +0200 Subject: [PATCH 09/35] Migrate ChangeDisplayName to TypeScript --- ...hangeDisplayName.js => ChangeDisplayName.tsx} | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) rename src/components/views/settings/{ChangeDisplayName.js => ChangeDisplayName.tsx} (81%) diff --git a/src/components/views/settings/ChangeDisplayName.js b/src/components/views/settings/ChangeDisplayName.tsx similarity index 81% rename from src/components/views/settings/ChangeDisplayName.js rename to src/components/views/settings/ChangeDisplayName.tsx index 2f336e18c6..09c9ecc23e 100644 --- a/src/components/views/settings/ChangeDisplayName.js +++ b/src/components/views/settings/ChangeDisplayName.tsx @@ -1,6 +1,6 @@ /* Copyright 2015, 2016 OpenMarket Ltd -Copyright 2018 New Vector Ltd +Copyright 2018 - 2021 New Vector Ltd Copyright 2019 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); @@ -24,7 +24,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; @replaceableComponent("views.settings.ChangeDisplayName") export default class ChangeDisplayName extends React.Component { - _getDisplayName = async () => { + private getDisplayName = async (): Promise => { const cli = MatrixClientPeg.get(); try { const res = await cli.getProfileInfo(cli.getUserId()); @@ -34,21 +34,21 @@ export default class ChangeDisplayName extends React.Component { } }; - _changeDisplayName = (newDisplayname) => { + private changeDisplayName = (newDisplayname: string): Promise<{}> => { const cli = MatrixClientPeg.get(); - return cli.setDisplayName(newDisplayname).catch(function(e) { - throw new Error("Failed to set display name", e); + return cli.setDisplayName(newDisplayname).catch(function() { + throw new Error("Failed to set display name"); }); }; - render() { + public render(): JSX.Element { const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer'); return ( + onSubmit={this.changeDisplayName} /> ); } } From fb6a6370e7e37c3c5eef48c3208b3cb9d5915b22 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 11:17:19 +0200 Subject: [PATCH 10/35] Migrate DevicesPanel to TypeScript --- .../{DevicesPanel.js => DevicesPanel.tsx} | 89 +++++++++---------- 1 file changed, 41 insertions(+), 48 deletions(-) rename src/components/views/settings/{DevicesPanel.js => DevicesPanel.tsx} (82%) diff --git a/src/components/views/settings/DevicesPanel.js b/src/components/views/settings/DevicesPanel.tsx similarity index 82% rename from src/components/views/settings/DevicesPanel.js rename to src/components/views/settings/DevicesPanel.tsx index 0f052332ee..6fd7f90c8e 100644 --- a/src/components/views/settings/DevicesPanel.js +++ b/src/components/views/settings/DevicesPanel.tsx @@ -1,6 +1,7 @@ /* Copyright 2016 OpenMarket Ltd Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,8 +17,8 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; +import { IMyDevice } from "matrix-js-sdk/src/client"; import * as sdk from '../../../index'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; @@ -26,42 +27,37 @@ import Modal from '../../../Modal'; import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + className?: string; +} + +interface IState { + devices: IMyDevice[]; + deviceLoadError?: string; + selectedDevices?: string[]; + deleting?: boolean; +} + @replaceableComponent("views.settings.DevicesPanel") -export default class DevicesPanel extends React.Component { - constructor(props) { - super(props); +export default class DevicesPanel extends React.Component { + private unmounted = false; - this.state = { - devices: undefined, - deviceLoadError: undefined, - - selectedDevices: [], - deleting: false, - }; - - this._unmounted = false; - - this._renderDevice = this._renderDevice.bind(this); - this._onDeviceSelectionToggled = this._onDeviceSelectionToggled.bind(this); - this._onDeleteClick = this._onDeleteClick.bind(this); + public componentDidMount(): void { + this.loadDevices(); } - componentDidMount() { - this._loadDevices(); + public componentWillUnmount(): void { + this.unmounted = true; } - componentWillUnmount() { - this._unmounted = true; - } - - _loadDevices() { + private loadDevices(): void { MatrixClientPeg.get().getDevices().then( (resp) => { - if (this._unmounted) { return; } + if (this.unmounted) { return; } this.setState({ devices: resp.devices || [] }); }, (error) => { - if (this._unmounted) { return; } + if (this.unmounted) { return; } let errtxt; if (error.httpStatus == 404) { // 404 probably means the HS doesn't yet support the API. @@ -79,7 +75,7 @@ export default class DevicesPanel extends React.Component { * compare two devices, sorting from most-recently-seen to least-recently-seen * (and then, for stability, by device id) */ - _deviceCompare(a, b) { + private deviceCompare(a: IMyDevice, b: IMyDevice): number { // return < 0 if a comes before b, > 0 if a comes after b. const lastSeenDelta = (b.last_seen_ts || 0) - (a.last_seen_ts || 0); @@ -91,8 +87,8 @@ export default class DevicesPanel extends React.Component { return (idA < idB) ? -1 : (idA > idB) ? 1 : 0; } - _onDeviceSelectionToggled(device) { - if (this._unmounted) { return; } + private onDeviceSelectionToggled = (device: IMyDevice): void => { + if (this.unmounted) { return; } const deviceId = device.device_id; this.setState((state, props) => { @@ -108,15 +104,15 @@ export default class DevicesPanel extends React.Component { return { selectedDevices }; }); - } + }; - _onDeleteClick() { + private onDeleteClick = (): void => { this.setState({ deleting: true, }); - this._makeDeleteRequest(null).catch((error) => { - if (this._unmounted) { return; } + this.makeDeleteRequest(null).catch((error) => { + if (this.unmounted) { return; } if (error.httpStatus !== 401 || !error.data || !error.data.flows) { // doesn't look like an interactive-auth failure throw error; @@ -148,7 +144,7 @@ export default class DevicesPanel extends React.Component { title: _t("Authentication"), matrixClient: MatrixClientPeg.get(), authData: error.data, - makeRequest: this._makeDeleteRequest.bind(this), + makeRequest: this.makeDeleteRequest.bind(this), aestheticsForStagePhases: { [SSOAuthEntry.LOGIN_TYPE]: dialogAesthetics, [SSOAuthEntry.UNSTABLE_LOGIN_TYPE]: dialogAesthetics, @@ -156,15 +152,16 @@ export default class DevicesPanel extends React.Component { }); }).catch((e) => { console.error("Error deleting sessions", e); - if (this._unmounted) { return; } + if (this.unmounted) { return; } }).finally(() => { this.setState({ deleting: false, }); }); - } + }; - _makeDeleteRequest(auth) { + // TODO: proper typing for auth + private makeDeleteRequest(auth?: any): Promise { return MatrixClientPeg.get().deleteMultipleDevices(this.state.selectedDevices, auth).then( () => { // Remove the deleted devices from `devices`, reset selection to [] @@ -178,17 +175,17 @@ export default class DevicesPanel extends React.Component { ); } - _renderDevice(device) { + private renderDevice = (device: IMyDevice): JSX.Element => { const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry'); return ; - } + }; - render() { + public render(): JSX.Element { const Spinner = sdk.getComponent("elements.Spinner"); const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); @@ -208,11 +205,11 @@ export default class DevicesPanel extends React.Component { return ; } - devices.sort(this._deviceCompare); + devices.sort(this.deviceCompare); const deleteButton = this.state.deleting ? : - + { _t("Delete %(count)s sessions", { count: this.state.selectedDevices.length }) } ; @@ -227,12 +224,8 @@ export default class DevicesPanel extends React.Component { { this.state.selectedDevices.length > 0 ? deleteButton : null }
- { devices.map(this._renderDevice) } + { devices.map(this.renderDevice) }
); } } - -DevicesPanel.propTypes = { - className: PropTypes.string, -}; From dfd986751ffdd810e5d54b7fa95d4470ebbca77e Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 11:21:59 +0200 Subject: [PATCH 11/35] Migrate DevicesPanelEntry to TypeScript --- ...cesPanelEntry.js => DevicesPanelEntry.tsx} | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) rename src/components/views/settings/{DevicesPanelEntry.js => DevicesPanelEntry.tsx} (78%) diff --git a/src/components/views/settings/DevicesPanelEntry.js b/src/components/views/settings/DevicesPanelEntry.tsx similarity index 78% rename from src/components/views/settings/DevicesPanelEntry.js rename to src/components/views/settings/DevicesPanelEntry.tsx index a5b674b8f6..d44147f591 100644 --- a/src/components/views/settings/DevicesPanelEntry.js +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -1,5 +1,6 @@ /* Copyright 2016 OpenMarket Ltd +Copyright 2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; +import { IMyDevice } from 'matrix-js-sdk/src'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; @@ -24,21 +25,19 @@ import { formatDate } from '../../../DateUtils'; import StyledCheckbox from '../elements/StyledCheckbox'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + device?: IMyDevice; + onDeviceToggled?: (device: IMyDevice) => void; + selected?: boolean; +} + @replaceableComponent("views.settings.DevicesPanelEntry") -export default class DevicesPanelEntry extends React.Component { - constructor(props) { - super(props); +export default class DevicesPanelEntry extends React.Component { + public static defaultProps = { + onDeviceToggled: () => {}, + }; - this._unmounted = false; - this.onDeviceToggled = this.onDeviceToggled.bind(this); - this._onDisplayNameChanged = this._onDisplayNameChanged.bind(this); - } - - componentWillUnmount() { - this._unmounted = true; - } - - _onDisplayNameChanged(value) { + private onDisplayNameChanged = (value: string): Promise<{}> => { const device = this.props.device; return MatrixClientPeg.get().setDeviceDetails(device.device_id, { display_name: value, @@ -46,13 +45,13 @@ export default class DevicesPanelEntry extends React.Component { console.error("Error setting session display name", e); throw new Error(_t("Failed to set display name")); }); - } + }; - onDeviceToggled() { + private onDeviceToggled = (): void => { this.props.onDeviceToggled(this.props.device); - } + }; - render() { + public render(): JSX.Element { const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer'); const device = this.props.device; @@ -76,7 +75,7 @@ export default class DevicesPanelEntry extends React.Component {
@@ -90,12 +89,3 @@ export default class DevicesPanelEntry extends React.Component { ); } } - -DevicesPanelEntry.propTypes = { - device: PropTypes.object.isRequired, - onDeviceToggled: PropTypes.func, -}; - -DevicesPanelEntry.defaultProps = { - onDeviceToggled: function() {}, -}; From 447beb829458104d2c29adb08751544a1433d4ea Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 11:27:17 +0200 Subject: [PATCH 12/35] Migrate IntegrationManager to TypeScript --- ...ationManager.js => IntegrationManager.tsx} | 61 ++++++++++--------- 1 file changed, 32 insertions(+), 29 deletions(-) rename src/components/views/settings/{IntegrationManager.js => IntegrationManager.tsx} (72%) diff --git a/src/components/views/settings/IntegrationManager.js b/src/components/views/settings/IntegrationManager.tsx similarity index 72% rename from src/components/views/settings/IntegrationManager.js rename to src/components/views/settings/IntegrationManager.tsx index 9f2985df14..f43fb55004 100644 --- a/src/components/views/settings/IntegrationManager.js +++ b/src/components/views/settings/IntegrationManager.tsx @@ -1,6 +1,7 @@ /* Copyright 2015, 2016 OpenMarket Ltd Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,53 +17,55 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { ActionPayload } from '../../../dispatcher/payloads'; + +interface IProps { + // false to display an error saying that we couldn't connect to the integration manager + connected: boolean; + + // true to display a loading spinner + loading: boolean; + + // The source URL to load + url?: string; + + // callback when the manager is dismissed + onFinished: () => void; +} + +interface IState { + errored: boolean; +} @replaceableComponent("views.settings.IntegrationManager") -export default class IntegrationManager extends React.Component { - static propTypes = { - // false to display an error saying that we couldn't connect to the integration manager - connected: PropTypes.bool.isRequired, +export default class IntegrationManager extends React.Component { + private dispatcherRef: string; - // true to display a loading spinner - loading: PropTypes.bool.isRequired, - - // The source URL to load - url: PropTypes.string, - - // callback when the manager is dismissed - onFinished: PropTypes.func.isRequired, - }; - - static defaultProps = { + public static defaultProps = { connected: true, loading: false, }; - constructor(props) { - super(props); + public state = { + errored: false, + }; - this.state = { - errored: false, - }; - } - - componentDidMount() { + public componentDidMount(): void { this.dispatcherRef = dis.register(this.onAction); document.addEventListener("keydown", this.onKeyDown); } - componentWillUnmount() { + public componentWillUnmount(): void { dis.unregister(this.dispatcherRef); document.removeEventListener("keydown", this.onKeyDown); } - onKeyDown = (ev) => { + private onKeyDown = (ev: KeyboardEvent): void => { if (ev.key === Key.ESCAPE) { ev.stopPropagation(); ev.preventDefault(); @@ -70,17 +73,17 @@ export default class IntegrationManager extends React.Component { } }; - onAction = (payload) => { + private onAction = (payload: ActionPayload): void => { if (payload.action === 'close_scalar') { this.props.onFinished(); } }; - onError = () => { + private onError = (): void => { this.setState({ errored: true }); }; - render() { + public render(): JSX.Element { if (this.props.loading) { const Spinner = sdk.getComponent("elements.Spinner"); return ( From 2e1d5aa67bd1688544c26d2180c89a467c87807f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Sat, 14 Aug 2021 11:36:12 +0200 Subject: [PATCH 13/35] Migrate ProfileSettings to TypeScript --- ...ProfileSettings.js => ProfileSettings.tsx} | 61 +++++++++++-------- 1 file changed, 37 insertions(+), 24 deletions(-) rename src/components/views/settings/{ProfileSettings.js => ProfileSettings.tsx} (83%) diff --git a/src/components/views/settings/ProfileSettings.js b/src/components/views/settings/ProfileSettings.tsx similarity index 83% rename from src/components/views/settings/ProfileSettings.js rename to src/components/views/settings/ProfileSettings.tsx index d05fca983c..888ff8967b 100644 --- a/src/components/views/settings/ProfileSettings.js +++ b/src/components/views/settings/ProfileSettings.tsx @@ -1,5 +1,5 @@ /* -Copyright 2019 New Vector Ltd +Copyright 2019-2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,10 +26,25 @@ import ErrorDialog from "../dialogs/ErrorDialog"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +interface IProps { + +} + +interface IState { + userId?: string; + originalDisplayName?: string; + displayName?: string; + originalAvatarUrl?: string; + avatarUrl?: string | ArrayBuffer; + avatarFile?: File; + enableProfileSave?: boolean; +} + @replaceableComponent("views.settings.ProfileSettings") -export default class ProfileSettings extends React.Component { - constructor() { - super(); +export default class ProfileSettings extends React.Component { + private avatarUpload: React.RefObject = createRef(); + constructor(props: IProps) { + super(props); const client = MatrixClientPeg.get(); let avatarUrl = OwnProfileStore.instance.avatarMxc; @@ -43,17 +58,15 @@ export default class ProfileSettings extends React.Component { avatarFile: null, enableProfileSave: false, }; - - this._avatarUpload = createRef(); } - _uploadAvatar = () => { - this._avatarUpload.current.click(); + private uploadAvatar = (): void => { + this.avatarUpload.current.click(); }; - _removeAvatar = () => { + private removeAvatar = (): void => { // clear file upload field so same file can be selected - this._avatarUpload.current.value = ""; + this.avatarUpload.current.value = ""; this.setState({ avatarUrl: null, avatarFile: null, @@ -61,7 +74,7 @@ export default class ProfileSettings extends React.Component { }); }; - _cancelProfileChanges = async (e) => { + private cancelProfileChanges = async (e: React.MouseEvent): Promise => { e.stopPropagation(); e.preventDefault(); @@ -74,7 +87,7 @@ export default class ProfileSettings extends React.Component { }); }; - _saveProfile = async (e) => { + private saveProfile = async (e: React.FormEvent): Promise => { e.stopPropagation(); e.preventDefault(); @@ -82,7 +95,7 @@ export default class ProfileSettings extends React.Component { this.setState({ enableProfileSave: false }); const client = MatrixClientPeg.get(); - const newState = {}; + const newState: IState = {}; const displayName = this.state.displayName.trim(); try { @@ -115,14 +128,14 @@ export default class ProfileSettings extends React.Component { this.setState(newState); }; - _onDisplayNameChanged = (e) => { + private onDisplayNameChanged = (e: React.ChangeEvent): void => { this.setState({ displayName: e.target.value, enableProfileSave: true, }); }; - _onAvatarChanged = (e) => { + private onAvatarChanged = (e: React.ChangeEvent): void => { if (!e.target.files || !e.target.files.length) { this.setState({ avatarUrl: this.state.originalAvatarUrl, @@ -144,7 +157,7 @@ export default class ProfileSettings extends React.Component { reader.readAsDataURL(file); }; - render() { + public render(): JSX.Element { const hostingSignupLink = getHostingLink('user-settings'); let hostingSignup = null; if (hostingSignupLink) { @@ -165,16 +178,16 @@ export default class ProfileSettings extends React.Component { const AvatarSetting = sdk.getComponent('settings.AvatarSetting'); return (
@@ -185,7 +198,7 @@ export default class ProfileSettings extends React.Component { type="text" value={this.state.displayName} autoComplete="off" - onChange={this._onDisplayNameChanged} + onChange={this.onDisplayNameChanged} />

{ this.state.userId } @@ -196,19 +209,19 @@ export default class ProfileSettings extends React.Component { avatarUrl={this.state.avatarUrl} avatarName={this.state.displayName || this.state.userId} avatarAltText={_t("Profile picture")} - uploadAvatar={this._uploadAvatar} - removeAvatar={this._removeAvatar} /> + uploadAvatar={this.uploadAvatar} + removeAvatar={this.removeAvatar} />

{ _t("Cancel") } From 800b3f1424d1aa24e6fa5663968fb6b65dc521ab Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 16 Aug 2021 09:16:02 +0100 Subject: [PATCH 14/35] Fix linter --- src/components/structures/RoomView.tsx | 3 +-- src/components/views/settings/ChangeAvatar.tsx | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 474b99262d..31aa8d50fa 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -1867,7 +1867,7 @@ export default class RoomView extends React.Component { isRoomEncrypted={this.context.isRoomEncrypted(this.state.room.roomId)} />; } else if (showRoomUpgradeBar) { - aux = ; + aux = ; } else if (myMembership !== "join") { // We do have a room object for this room, but we're not currently in it. // We may have a 3rd party invite to it. @@ -2042,7 +2042,6 @@ export default class RoomView extends React.Component { highlight={this.state.room.getUnreadNotificationCount(NotificationCountType.Highlight) > 0} numUnreadMessages={this.state.numUnreadMessages} onScrollToBottomClick={this.jumpToLiveTimeline} - roomId={this.state.roomId} />); } diff --git a/src/components/views/settings/ChangeAvatar.tsx b/src/components/views/settings/ChangeAvatar.tsx index 6394bad3de..92ff34fbcb 100644 --- a/src/components/views/settings/ChangeAvatar.tsx +++ b/src/components/views/settings/ChangeAvatar.tsx @@ -71,7 +71,8 @@ export default class ChangeAvatar extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - public UNSAFE_componentWillReceiveProps(newProps): void { // eslint-disable-line camelcase + // eslint-disable-next-line + public UNSAFE_componentWillReceiveProps(newProps): void { if (this.avatarSet) { // don't clobber what the user has just set return; From 02ece401031092e3e08ae5e410c53a09048aaba2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 16 Aug 2021 09:19:58 +0100 Subject: [PATCH 15/35] Fix import path on ChangeAvatar --- src/components/views/settings/ChangeAvatar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/settings/ChangeAvatar.tsx b/src/components/views/settings/ChangeAvatar.tsx index 92ff34fbcb..7a3e639876 100644 --- a/src/components/views/settings/ChangeAvatar.tsx +++ b/src/components/views/settings/ChangeAvatar.tsx @@ -16,13 +16,13 @@ limitations under the License. */ import React from 'react'; +import { MatrixEvent, Room } from 'matrix-js-sdk/src'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Spinner from '../elements/Spinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; -import { MatrixEvent, Room } from '../../../../../matrix-js-sdk/src'; interface IProps { initialAvatarUrl?: string; From 617e7deff54f21b7e8ad62ad2bd563a50bb22225 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 17 Aug 2021 18:05:10 +0100 Subject: [PATCH 16/35] replace sdk.getComponent with import statements --- src/components/views/rooms/ReadReceiptMarker.tsx | 4 ++-- src/components/views/rooms/RoomDetailList.tsx | 6 ++---- src/components/views/rooms/RoomUpgradeWarningBar.tsx | 6 ++---- src/components/views/settings/ChangeAvatar.tsx | 5 ++--- src/components/views/settings/ChangeDisplayName.tsx | 3 +-- src/components/views/settings/DevicesPanel.tsx | 3 +-- src/components/views/settings/DevicesPanelEntry.tsx | 4 +--- src/components/views/settings/IntegrationManager.tsx | 3 +-- src/components/views/settings/ProfileSettings.tsx | 5 ++--- 9 files changed, 14 insertions(+), 25 deletions(-) diff --git a/src/components/views/rooms/ReadReceiptMarker.tsx b/src/components/views/rooms/ReadReceiptMarker.tsx index 11e7563f11..28e1ec85e9 100644 --- a/src/components/views/rooms/ReadReceiptMarker.tsx +++ b/src/components/views/rooms/ReadReceiptMarker.tsx @@ -21,10 +21,11 @@ import { RoomMember } from 'matrix-js-sdk/src'; import { _t } from '../../../languageHandler'; import { formatDate } from '../../../DateUtils'; import NodeAnimator from "../../../NodeAnimator"; -import * as sdk from "../../../index"; import { toPx } from "../../../utils/units"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import MemberAvatar from '../avatars/MemberAvatar'; + interface IProps { // the RoomMember to show the RR for member?: RoomMember; @@ -169,7 +170,6 @@ export default class ReadReceiptMarker extends React.PureComponent; } diff --git a/src/components/views/rooms/RoomDetailList.tsx b/src/components/views/rooms/RoomDetailList.tsx index ee7383d7c7..cace94ce08 100644 --- a/src/components/views/rooms/RoomDetailList.tsx +++ b/src/components/views/rooms/RoomDetailList.tsx @@ -17,11 +17,11 @@ limitations under the License. import React from 'react'; import { Room } from 'matrix-js-sdk/src'; import classNames from 'classnames'; -import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; import { _t } from '../../../languageHandler'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomDetailRow from "./RoomDetailRow"; interface IProps { rooms?: Room[]; @@ -31,9 +31,7 @@ interface IProps { @replaceableComponent("views.rooms.RoomDetailList") export default class RoomDetailList extends React.Component { public getRows(): JSX.Element[] { - if (!this.props.rooms) return []; - - const RoomDetailRow = sdk.getComponent('rooms.RoomDetailRow'); + if (!this.props.rooms) return []; s; return this.props.rooms.map((room, index) => { return ; }); diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.tsx b/src/components/views/rooms/RoomUpgradeWarningBar.tsx index 6706e248e0..3380bd5392 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.tsx +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -16,12 +16,13 @@ limitations under the License. import React from 'react'; import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src'; -import * as sdk from '../../../index'; import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import RoomUpgradeDialog from '../dialogs/RoomUpgradeDialog'; +import AccessibleButton from '../elements/AccessibleButton'; interface IProps { room: Room; @@ -59,13 +60,10 @@ export default class RoomUpgradeWarningBar extends React.PureComponent { - const RoomUpgradeDialog = sdk.getComponent('dialogs.RoomUpgradeDialog'); Modal.createTrackedDialog('Upgrade Room Version', '', RoomUpgradeDialog, { room: this.props.room }); }; public render(): JSX.Element { - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - let doUpgradeWarnings = (
diff --git a/src/components/views/settings/ChangeAvatar.tsx b/src/components/views/settings/ChangeAvatar.tsx index 7a3e639876..7126fe8cc3 100644 --- a/src/components/views/settings/ChangeAvatar.tsx +++ b/src/components/views/settings/ChangeAvatar.tsx @@ -18,11 +18,12 @@ limitations under the License. import React from 'react'; import { MatrixEvent, Room } from 'matrix-js-sdk/src'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import Spinner from '../elements/Spinner'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import RoomAvatar from '../avatars/RoomAvatar'; +import BaseAvatar from '../avatars/BaseAvatar'; interface IProps { initialAvatarUrl?: string; @@ -155,7 +156,6 @@ export default class ChangeAvatar extends React.Component { // Having just set an avatar we just display that since it will take a little // time to propagate through to the RoomAvatar. if (this.props.room && !this.avatarSet) { - const RoomAvatar = sdk.getComponent('avatars.RoomAvatar'); avatarImg = { resizeMethod='crop' />; } else { - const BaseAvatar = sdk.getComponent("avatars.BaseAvatar"); // XXX: FIXME: once we track in the JS what our own displayname is(!) then use it here rather than ? avatarImg = { } // pop up an interactive auth dialog - const InteractiveAuthDialog = sdk.getComponent("dialogs.InteractiveAuthDialog"); const numDevices = this.state.selectedDevices.length; const dialogAesthetics = { diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index d44147f591..3762f7be83 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -18,12 +18,12 @@ limitations under the License. import React from 'react'; import { IMyDevice } from 'matrix-js-sdk/src'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; import { formatDate } from '../../../DateUtils'; import StyledCheckbox from '../elements/StyledCheckbox'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import EditableTextContainer from "../elements/EditableTextContainer"; interface IProps { device?: IMyDevice; @@ -52,8 +52,6 @@ export default class DevicesPanelEntry extends React.Component { }; public render(): JSX.Element { - const EditableTextContainer = sdk.getComponent('elements.EditableTextContainer'); - const device = this.props.device; let lastSeen = ""; diff --git a/src/components/views/settings/IntegrationManager.tsx b/src/components/views/settings/IntegrationManager.tsx index f43fb55004..7b221aceec 100644 --- a/src/components/views/settings/IntegrationManager.tsx +++ b/src/components/views/settings/IntegrationManager.tsx @@ -17,12 +17,12 @@ limitations under the License. */ import React from 'react'; -import * as sdk from '../../../index'; import { _t } from '../../../languageHandler'; import dis from '../../../dispatcher/dispatcher'; import { Key } from "../../../Keyboard"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { ActionPayload } from '../../../dispatcher/payloads'; +import Spinner from "../elements/Spinner"; interface IProps { // false to display an error saying that we couldn't connect to the integration manager @@ -85,7 +85,6 @@ export default class IntegrationManager extends React.Component public render(): JSX.Element { if (this.props.loading) { - const Spinner = sdk.getComponent("elements.Spinner"); return (

{ _t("Connecting to integration manager...") }

diff --git a/src/components/views/settings/ProfileSettings.tsx b/src/components/views/settings/ProfileSettings.tsx index 888ff8967b..9bd7179f08 100644 --- a/src/components/views/settings/ProfileSettings.tsx +++ b/src/components/views/settings/ProfileSettings.tsx @@ -19,12 +19,13 @@ import { _t } from "../../../languageHandler"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import Field from "../elements/Field"; import { getHostingLink } from '../../../utils/HostingLink'; -import * as sdk from "../../../index"; import { OwnProfileStore } from "../../../stores/OwnProfileStore"; import Modal from "../../../Modal"; import ErrorDialog from "../dialogs/ErrorDialog"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; +import AccessibleButton from '../elements/AccessibleButton'; +import AvatarSetting from './AvatarSetting'; interface IProps { @@ -174,8 +175,6 @@ export default class ProfileSettings extends React.Component { ; } - const AccessibleButton = sdk.getComponent('elements.AccessibleButton'); - const AvatarSetting = sdk.getComponent('settings.AvatarSetting'); return ( Date: Wed, 25 Aug 2021 08:56:21 +0100 Subject: [PATCH 17/35] Relative imports from the js-sdk MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner --- src/components/views/rooms/ReadReceiptMarker.tsx | 2 +- src/components/views/settings/ChangeAvatar.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/rooms/ReadReceiptMarker.tsx b/src/components/views/rooms/ReadReceiptMarker.tsx index 28e1ec85e9..c1bd533d39 100644 --- a/src/components/views/rooms/ReadReceiptMarker.tsx +++ b/src/components/views/rooms/ReadReceiptMarker.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React, { createRef } from 'react'; -import { RoomMember } from 'matrix-js-sdk/src'; +import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { _t } from '../../../languageHandler'; import { formatDate } from '../../../DateUtils'; diff --git a/src/components/views/settings/ChangeAvatar.tsx b/src/components/views/settings/ChangeAvatar.tsx index 7126fe8cc3..e629b9df08 100644 --- a/src/components/views/settings/ChangeAvatar.tsx +++ b/src/components/views/settings/ChangeAvatar.tsx @@ -73,7 +73,7 @@ export default class ChangeAvatar extends React.Component { // TODO: [REACT-WARNING] Replace with appropriate lifecycle event // eslint-disable-next-line - public UNSAFE_componentWillReceiveProps(newProps): void { + public UNSAFE_componentWillReceiveProps(newProps: IProps): void { if (this.avatarSet) { // don't clobber what the user has just set return; From 6945e3f103c017be3824b5a12e9dfe00046ac28a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 25 Aug 2021 09:05:07 +0100 Subject: [PATCH 18/35] Fix ProfileSettings types --- src/components/views/elements/AccessibleButton.tsx | 4 ++-- src/components/views/settings/ProfileSettings.tsx | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/views/elements/AccessibleButton.tsx b/src/components/views/elements/AccessibleButton.tsx index 0ce9a3a030..75b6890112 100644 --- a/src/components/views/elements/AccessibleButton.tsx +++ b/src/components/views/elements/AccessibleButton.tsx @@ -19,7 +19,7 @@ import React, { ReactHTML } from 'react'; import { Key } from '../../../Keyboard'; import classnames from 'classnames'; -export type ButtonEvent = React.MouseEvent | React.KeyboardEvent; +export type ButtonEvent = React.MouseEvent | React.KeyboardEvent | React.FormEvent; /** * children: React's magic prop. Represents all children given to the element. @@ -39,7 +39,7 @@ interface IProps extends React.InputHTMLAttributes { tabIndex?: number; disabled?: boolean; className?: string; - onClick(e?: ButtonEvent): void; + onClick(e?: ButtonEvent): void | Promise; } interface IAccessibleButtonProps extends React.InputHTMLAttributes { diff --git a/src/components/views/settings/ProfileSettings.tsx b/src/components/views/settings/ProfileSettings.tsx index 9bd7179f08..e6e68299cb 100644 --- a/src/components/views/settings/ProfileSettings.tsx +++ b/src/components/views/settings/ProfileSettings.tsx @@ -27,10 +27,6 @@ import { mediaFromMxc } from "../../../customisations/Media"; import AccessibleButton from '../elements/AccessibleButton'; import AvatarSetting from './AvatarSetting'; -interface IProps { - -} - interface IState { userId?: string; originalDisplayName?: string; @@ -42,9 +38,9 @@ interface IState { } @replaceableComponent("views.settings.ProfileSettings") -export default class ProfileSettings extends React.Component { +export default class ProfileSettings extends React.Component<{}, IState> { private avatarUpload: React.RefObject = createRef(); - constructor(props: IProps) { + constructor(props: {}) { super(props); const client = MatrixClientPeg.get(); @@ -205,7 +201,7 @@ export default class ProfileSettings extends React.Component {

Date: Wed, 25 Aug 2021 09:34:16 +0100 Subject: [PATCH 19/35] Fix linting issues --- .eslintrc.js | 5 +++++ src/components/structures/RoomDirectory.tsx | 2 +- src/components/views/avatars/MemberAvatar.tsx | 1 + src/components/views/right_panel/UserInfo.tsx | 2 +- src/components/views/rooms/ReadReceiptMarker.tsx | 8 ++++---- src/components/views/rooms/RoomDetailList.tsx | 2 +- src/components/views/rooms/RoomHeader.tsx | 2 +- src/components/views/settings/DevicesPanel.tsx | 10 ++++------ 8 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 827b373949..9d68942228 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -63,6 +63,11 @@ module.exports = { "@typescript-eslint/ban-ts-comment": "off", }, }], + settings: { + react: { + version: "detect", + } + } }; function buildRestrictedPropertiesOptions(properties, message) { diff --git a/src/components/structures/RoomDirectory.tsx b/src/components/structures/RoomDirectory.tsx index 8d8609d1cf..3c5f99cc7d 100644 --- a/src/components/structures/RoomDirectory.tsx +++ b/src/components/structures/RoomDirectory.tsx @@ -347,7 +347,7 @@ export default class RoomDirectory extends React.Component { }); } - private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: ButtonEvent) => { + private onRoomClicked = (room: IPublicRoomsChunkRoom, ev: React.MouseEvent) => { // If room was shift-clicked, remove it from the room directory if (ev.shiftKey && !this.state.selectedCommunityId) { ev.preventDefault(); diff --git a/src/components/views/avatars/MemberAvatar.tsx b/src/components/views/avatars/MemberAvatar.tsx index 11c24a5981..3c734705b7 100644 --- a/src/components/views/avatars/MemberAvatar.tsx +++ b/src/components/views/avatars/MemberAvatar.tsx @@ -36,6 +36,7 @@ interface IProps extends Omit, "name" | // Whether the onClick of the avatar should be overriden to dispatch `Action.ViewUser` viewUserOnClick?: boolean; title?: string; + style?: any; } interface IState { diff --git a/src/components/views/right_panel/UserInfo.tsx b/src/components/views/right_panel/UserInfo.tsx index 138f5bf9fe..d15f349d62 100644 --- a/src/components/views/right_panel/UserInfo.tsx +++ b/src/components/views/right_panel/UserInfo.tsx @@ -428,7 +428,7 @@ const UserOptionsSection: React.FC<{ let directMessageButton; if (!isMe) { directMessageButton = ( - openDMForUser(cli, member.userId)} className="mx_UserInfo_field"> + { openDMForUser(cli, member.userId); }} className="mx_UserInfo_field"> { _t('Direct message') } ); diff --git a/src/components/views/rooms/ReadReceiptMarker.tsx b/src/components/views/rooms/ReadReceiptMarker.tsx index c1bd533d39..cfc535b23d 100644 --- a/src/components/views/rooms/ReadReceiptMarker.tsx +++ b/src/components/views/rooms/ReadReceiptMarker.tsx @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React, { createRef } from 'react'; +import React, { createRef, RefObject } from 'react'; import { RoomMember } from "matrix-js-sdk/src/models/room-member"; import { _t } from '../../../languageHandler'; @@ -75,7 +75,7 @@ interface IReadReceiptMarkerStyle { @replaceableComponent("views.rooms.ReadReceiptMarker") export default class ReadReceiptMarker extends React.PureComponent { - private avatar: React.RefObject = createRef(); + private avatar: React.RefObject = createRef(); static defaultProps = { leftOffset: 0, @@ -171,7 +171,7 @@ export default class ReadReceiptMarker extends React.PureComponent; + return
} />; } const style = { @@ -210,7 +210,7 @@ export default class ReadReceiptMarker extends React.PureComponent} /> ); diff --git a/src/components/views/rooms/RoomDetailList.tsx b/src/components/views/rooms/RoomDetailList.tsx index cace94ce08..ed2a1fcb44 100644 --- a/src/components/views/rooms/RoomDetailList.tsx +++ b/src/components/views/rooms/RoomDetailList.tsx @@ -31,7 +31,7 @@ interface IProps { @replaceableComponent("views.rooms.RoomDetailList") export default class RoomDetailList extends React.Component { public getRows(): JSX.Element[] { - if (!this.props.rooms) return []; s; + if (!this.props.rooms) return []; return this.props.rooms.map((room, index) => { return ; }); diff --git a/src/components/views/rooms/RoomHeader.tsx b/src/components/views/rooms/RoomHeader.tsx index 15b25ed64b..d0e438bcda 100644 --- a/src/components/views/rooms/RoomHeader.tsx +++ b/src/components/views/rooms/RoomHeader.tsx @@ -195,7 +195,7 @@ export default class RoomHeader extends React.Component { videoCallButton = ev.shiftKey ? + onClick={(ev: React.MouseEvent) => ev.shiftKey ? this.displayInfoDialogAboutScreensharing() : this.props.onCallPlaced(PlaceCallType.Video)} title={_t("Video call")} />; } diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index b10820d6a5..4b1fb280a7 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -26,6 +26,9 @@ import Modal from '../../../Modal'; import { SSOAuthEntry } from "../auth/InteractiveAuthEntryComponents"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import InteractiveAuthDialog from "../dialogs/InteractiveAuthDialog"; +import DevicesPanelEntry from "./DevicesPanelEntry"; +import Spinner from "../elements/Spinner"; +import AccessibleButton from "../elements/AccessibleButton"; interface IProps { className?: string; @@ -175,7 +178,6 @@ export default class DevicesPanel extends React.Component { } private renderDevice = (device: IMyDevice): JSX.Element => { - const DevicesPanelEntry = sdk.getComponent('settings.DevicesPanelEntry'); return { }; public render(): JSX.Element { - const Spinner = sdk.getComponent("elements.Spinner"); - const AccessibleButton = sdk.getComponent("elements.AccessibleButton"); - if (this.state.deviceLoadError !== undefined) { const classes = classNames(this.props.className, "error"); return ( @@ -200,8 +199,7 @@ export default class DevicesPanel extends React.Component { const devices = this.state.devices; if (devices === undefined) { // still loading - const classes = this.props.className; - return ; + return ; } devices.sort(this.deviceCompare); From 7938961d2731d88d4e56ce149ed3bd32d321ee78 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 25 Aug 2021 09:48:29 +0100 Subject: [PATCH 20/35] Update js-sdk imports to target individual files --- src/components/views/rooms/RoomUpgradeWarningBar.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.tsx b/src/components/views/rooms/RoomUpgradeWarningBar.tsx index 3380bd5392..4c1216b620 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.tsx +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -15,7 +15,10 @@ limitations under the License. */ import React from 'react'; -import { MatrixEvent, Room, RoomState } from 'matrix-js-sdk/src'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; +import { RoomState } from 'matrix-js-sdk/src/models/room-state'; + import Modal from '../../../Modal'; import { _t } from '../../../languageHandler'; From e1e0278190faf78b9dcca8ccd5b88441dd307380 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 25 Aug 2021 10:04:54 +0100 Subject: [PATCH 21/35] Update js-sdk imports to target individual files --- src/components/views/settings/ChangeAvatar.tsx | 3 ++- src/components/views/settings/DevicesPanelEntry.tsx | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/views/settings/ChangeAvatar.tsx b/src/components/views/settings/ChangeAvatar.tsx index e629b9df08..08ecd3d065 100644 --- a/src/components/views/settings/ChangeAvatar.tsx +++ b/src/components/views/settings/ChangeAvatar.tsx @@ -16,7 +16,8 @@ limitations under the License. */ import React from 'react'; -import { MatrixEvent, Room } from 'matrix-js-sdk/src'; +import { MatrixEvent } from 'matrix-js-sdk/src/models/event'; +import { Room } from 'matrix-js-sdk/src/models/room'; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from '../../../languageHandler'; import Spinner from '../elements/Spinner'; diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 3762f7be83..940ccf8ba1 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -16,7 +16,7 @@ limitations under the License. */ import React from 'react'; -import { IMyDevice } from 'matrix-js-sdk/src'; +import { IMyDevice } from 'matrix-js-sdk/src/client'; import { _t } from '../../../languageHandler'; import { MatrixClientPeg } from '../../../MatrixClientPeg'; From cb8e62c0b2547b2c1f9745210c1efe27f67d370f Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 26 Aug 2021 08:02:36 +0100 Subject: [PATCH 22/35] human linter and copyright fixer --- src/components/views/rooms/RoomDetailList.tsx | 4 ++-- src/components/views/rooms/RoomUpgradeWarningBar.tsx | 2 +- src/components/views/rooms/SimpleRoomHeader.tsx | 3 +-- src/components/views/rooms/TopUnreadMessagesBar.tsx | 3 +-- src/components/views/settings/ChangeAvatar.tsx | 7 +++---- src/components/views/settings/ChangeDisplayName.tsx | 3 +-- src/components/views/settings/DevicesPanel.tsx | 3 +-- src/components/views/settings/DevicesPanelEntry.tsx | 2 +- src/components/views/settings/IntegrationManager.tsx | 3 +-- src/components/views/settings/ProfileSettings.tsx | 2 ++ 10 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/components/views/rooms/RoomDetailList.tsx b/src/components/views/rooms/RoomDetailList.tsx index ed2a1fcb44..869ab9e8f3 100644 --- a/src/components/views/rooms/RoomDetailList.tsx +++ b/src/components/views/rooms/RoomDetailList.tsx @@ -30,14 +30,14 @@ interface IProps { @replaceableComponent("views.rooms.RoomDetailList") export default class RoomDetailList extends React.Component { - public getRows(): JSX.Element[] { + private getRows(): JSX.Element[] { if (!this.props.rooms) return []; return this.props.rooms.map((room, index) => { return ; }); } - public onDetailsClick = (ev: React.MouseEvent, room: Room): void => { + private onDetailsClick = (ev: React.MouseEvent, room: Room): void => { dis.dispatch({ action: 'view_room', room_id: room.roomId, diff --git a/src/components/views/rooms/RoomUpgradeWarningBar.tsx b/src/components/views/rooms/RoomUpgradeWarningBar.tsx index 4c1216b620..eb334ab825 100644 --- a/src/components/views/rooms/RoomUpgradeWarningBar.tsx +++ b/src/components/views/rooms/RoomUpgradeWarningBar.tsx @@ -1,5 +1,5 @@ /* -Copyright 2018-2021 New Vector Ltd +Copyright 2018-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. diff --git a/src/components/views/rooms/SimpleRoomHeader.tsx b/src/components/views/rooms/SimpleRoomHeader.tsx index b81e906559..d6effaceb4 100644 --- a/src/components/views/rooms/SimpleRoomHeader.tsx +++ b/src/components/views/rooms/SimpleRoomHeader.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd -Copyright 2021 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. diff --git a/src/components/views/rooms/TopUnreadMessagesBar.tsx b/src/components/views/rooms/TopUnreadMessagesBar.tsx index 01797299cf..ec2472f966 100644 --- a/src/components/views/rooms/TopUnreadMessagesBar.tsx +++ b/src/components/views/rooms/TopUnreadMessagesBar.tsx @@ -1,7 +1,6 @@ /* -Copyright 2016 OpenMarket Ltd Copyright 2017 Vector Creations Ltd -Copyright 2019-2021 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. diff --git a/src/components/views/settings/ChangeAvatar.tsx b/src/components/views/settings/ChangeAvatar.tsx index 08ecd3d065..36178540f7 100644 --- a/src/components/views/settings/ChangeAvatar.tsx +++ b/src/components/views/settings/ChangeAvatar.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2021 New Vector Ltd +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -90,7 +89,7 @@ export default class ChangeAvatar extends React.Component { } } - public onRoomStateEvents = (ev: MatrixEvent) => { + private onRoomStateEvents = (ev: MatrixEvent) => { if (!this.props.room) { return; } @@ -106,7 +105,7 @@ export default class ChangeAvatar extends React.Component { } }; - public setAvatarFromFile(file): Promise<{}> { + private setAvatarFromFile(file: File): Promise<{}> { let newUrl = null; this.setState({ diff --git a/src/components/views/settings/ChangeDisplayName.tsx b/src/components/views/settings/ChangeDisplayName.tsx index 5b9703a46b..016f519dd9 100644 --- a/src/components/views/settings/ChangeDisplayName.tsx +++ b/src/components/views/settings/ChangeDisplayName.tsx @@ -1,7 +1,6 @@ /* -Copyright 2015, 2016 OpenMarket Ltd Copyright 2018 - 2021 New Vector Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index 4b1fb280a7..b6797b8ad5 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -1,7 +1,6 @@ /* -Copyright 2016 OpenMarket Ltd Copyright 2019 The Matrix.org Foundation C.I.C. -Copyright 2021 New Vector Ltd +Copyright 2016-2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index 940ccf8ba1..b589ffc7a1 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -1,5 +1,5 @@ /* -Copyright 2016 OpenMarket Ltd +Copyright 2016-2021 The Matrix.org Foundation C.I.C. Copyright 2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/components/views/settings/IntegrationManager.tsx b/src/components/views/settings/IntegrationManager.tsx index 7b221aceec..f9b3f67fad 100644 --- a/src/components/views/settings/IntegrationManager.tsx +++ b/src/components/views/settings/IntegrationManager.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015, 2016 OpenMarket Ltd -Copyright 2019 The Matrix.org Foundation C.I.C. +Copyright 2015-2021 The Matrix.org Foundation C.I.C. Copyright 2021 New Vector Ltd Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/src/components/views/settings/ProfileSettings.tsx b/src/components/views/settings/ProfileSettings.tsx index e6e68299cb..9e1f0444b3 100644 --- a/src/components/views/settings/ProfileSettings.tsx +++ b/src/components/views/settings/ProfileSettings.tsx @@ -1,5 +1,6 @@ /* Copyright 2019-2021 New Vector Ltd +Copyright 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. @@ -40,6 +41,7 @@ interface IState { @replaceableComponent("views.settings.ProfileSettings") export default class ProfileSettings extends React.Component<{}, IState> { private avatarUpload: React.RefObject = createRef(); + constructor(props: {}) { super(props); From e2419dc75b39647c352a5f21aac5dba5b7ced354 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Thu, 26 Aug 2021 12:41:29 +0200 Subject: [PATCH 23/35] Make stronger background blur for light theme Fixes https://github.com/vector-im/element-web/issues/18708 --- res/themes/light/css/_light.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 200d5bb12a..96e5fd7155 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -354,7 +354,7 @@ $appearance-tab-border-color: $input-darker-bg-color; // blur amounts for left left panel (only for element theme) :root { - --lp-background-blur: 30px; + --lp-background-blur: 40px; } $composer-shadow-color: rgba(0, 0, 0, 0.04); From 730af94014ec14541358d725fc188e40267e79fe Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Mon, 30 Aug 2021 14:20:03 -0600 Subject: [PATCH 24/35] Autoplay semantics for voice messages Fixes https://github.com/vector-im/element-web/issues/18804 Fixes https://github.com/vector-im/element-web/issues/18715 Fixes https://github.com/vector-im/element-web/issues/18714 Fixes https://github.com/vector-im/element-web/issues/17961 --- src/audio/ManagedPlayback.ts | 2 +- src/audio/Playback.ts | 7 +- src/audio/PlaybackClock.ts | 6 +- src/audio/PlaybackManager.ts | 12 +- src/audio/PlaybackQueue.ts | 212 ++++++++++++++++++ src/components/views/messages/MAudioBody.tsx | 6 + .../views/rooms/VoiceRecordComposerTile.tsx | 2 +- 7 files changed, 236 insertions(+), 11 deletions(-) create mode 100644 src/audio/PlaybackQueue.ts diff --git a/src/audio/ManagedPlayback.ts b/src/audio/ManagedPlayback.ts index bff6ce7088..5db07671f1 100644 --- a/src/audio/ManagedPlayback.ts +++ b/src/audio/ManagedPlayback.ts @@ -26,7 +26,7 @@ export class ManagedPlayback extends Playback { } public async play(): Promise { - this.manager.playOnly(this); + this.manager.pauseAllExcept(this); return super.play(); } diff --git a/src/audio/Playback.ts b/src/audio/Playback.ts index 9dad828a79..03f3bad760 100644 --- a/src/audio/Playback.ts +++ b/src/audio/Playback.ts @@ -117,6 +117,8 @@ export class Playback extends EventEmitter implements IDestroyable { } public destroy() { + // Dev note: It's critical that we call stop() during cleanup to ensure that downstream callers + // are aware of the final clock position before the user triggered an unload. // noinspection JSIgnoredPromiseFromCall - not concerned about being called async here this.stop(); this.removeAllListeners(); @@ -177,9 +179,12 @@ export class Playback extends EventEmitter implements IDestroyable { this.waveformObservable.update(this.resampledWaveform); - this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore this.clock.flagLoadTime(); // must happen first because setting the duration fires a clock update this.clock.durationSeconds = this.element ? this.element.duration : this.audioBuf.duration; + + // Signal that we're not decoding anymore. This is done last to ensure the clock is updated for + // when the downstream callers try to use it. + this.emit(PlaybackState.Stopped); // signal that we're not decoding anymore } private onPlaybackEnd = async () => { diff --git a/src/audio/PlaybackClock.ts b/src/audio/PlaybackClock.ts index 712d1bfa94..5716d6ac2f 100644 --- a/src/audio/PlaybackClock.ts +++ b/src/audio/PlaybackClock.ts @@ -89,9 +89,9 @@ export class PlaybackClock implements IDestroyable { return this.observable; } - private checkTime = () => { + private checkTime = (force = false) => { const now = this.timeSeconds; // calculated dynamically - if (this.lastCheck !== now) { + if (this.lastCheck !== now || force) { this.observable.update([now, this.durationSeconds]); this.lastCheck = now; } @@ -141,7 +141,7 @@ export class PlaybackClock implements IDestroyable { public syncTo(contextTime: number, clipTime: number) { this.clipStart = contextTime - clipTime; this.stopped = false; // count as a mid-stream pause (if we were stopped) - this.checkTime(); + this.checkTime(true); } public destroy() { diff --git a/src/audio/PlaybackManager.ts b/src/audio/PlaybackManager.ts index 58fa61df56..58c0b9b624 100644 --- a/src/audio/PlaybackManager.ts +++ b/src/audio/PlaybackManager.ts @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { DEFAULT_WAVEFORM, Playback } from "./Playback"; +import { DEFAULT_WAVEFORM, Playback, PlaybackState } from "./Playback"; import { ManagedPlayback } from "./ManagedPlayback"; /** @@ -34,12 +34,14 @@ export class PlaybackManager { } /** - * Stops all other playback instances. If no playback is provided, all instances - * are stopped. + * Pauses all other playback instances. If no playback is provided, all playing + * instances are paused. * @param playback Optional. The playback to leave untouched. */ - public playOnly(playback?: Playback) { - this.instances.filter(p => p !== playback).forEach(p => p.stop()); + public pauseAllExcept(playback?: Playback) { + this.instances + .filter(p => p !== playback && p.currentState === PlaybackState.Playing) + .forEach(p => p.pause()); } public destroyPlaybackInstance(playback: ManagedPlayback) { diff --git a/src/audio/PlaybackQueue.ts b/src/audio/PlaybackQueue.ts new file mode 100644 index 0000000000..6df4c24897 --- /dev/null +++ b/src/audio/PlaybackQueue.ts @@ -0,0 +1,212 @@ +/* +Copyright 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. +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 { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk"; +import { Playback, PlaybackState } from "./Playback"; +import { UPDATE_EVENT } from "../stores/AsyncStore"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import { arrayFastClone } from "../utils/arrays"; +import { PlaybackManager } from "./PlaybackManager"; +import { isVoiceMessage } from "../utils/EventUtils"; +import RoomViewStore from "../stores/RoomViewStore"; + +/** + * Audio playback queue management for a given room. This keeps track of where the user + * was at for each playback, what order the playbacks were played in, and triggers subsequent + * playbacks. + * + * Currently this is only intended to be used by voice messages. + * + * The primary mechanics are: + * * Persisted clock state for each playback instance (tied to Event ID). + * * Limited memory of playback order (see code; not persisted). + * * Autoplay of next eligible playback instance. + */ +export class PlaybackQueue { + private static queues = new Map(); // keyed by room ID + + private playbacks = new Map(); // keyed by event ID + private clockStates = new Map(); // keyed by event ID + private playbackIdOrder: string[] = []; // event IDs, last == current + private currentPlaybackId: string; // event ID, broken out from above for ease of use + private recentFullPlays = new Set(); // event IDs + + constructor(private client: MatrixClient, private room: Room) { + this.loadClocks(); + + RoomViewStore.addListener(() => { + if (RoomViewStore.getRoomId() === this.room.roomId) { + // Reset the state of the playbacks before they start mounting and enqueuing updates. + // We reset the entirety of the queue, including order, to ensure the user isn't left + // confused with what order the messages are playing in. + this.currentPlaybackId = null; // this in particular stops autoplay when the room is switched to + this.recentFullPlays = new Set(); + this.playbackIdOrder = []; + } + }); + } + + public static forRoom(roomId: string): PlaybackQueue { + const cli = MatrixClientPeg.get(); + const room = cli.getRoom(roomId); + if (!room) throw new Error("Unknown room"); + if (PlaybackQueue.queues.has(room.roomId)) { + return PlaybackQueue.queues.get(room.roomId); + } + const queue = new PlaybackQueue(cli, room); + PlaybackQueue.queues.set(room.roomId, queue); + return queue; + } + + private persistClocks() { + localStorage.setItem( + `mx_voice_message_clocks_${this.room.roomId}`, + JSON.stringify(Array.from(this.clockStates.entries())), + ); + } + + private loadClocks() { + const val = localStorage.getItem(`mx_voice_message_clocks_${this.room.roomId}`); + if (!!val) { + this.clockStates = new Map(JSON.parse(val)); + } + } + + public unsortedEnqueue(mxEvent: MatrixEvent, playback: Playback) { + // We don't ever detach our listeners: we expect the Playback to clean up for us + this.playbacks.set(mxEvent.getId(), playback); + playback.on(UPDATE_EVENT, (state) => this.onPlaybackStateChange(playback, mxEvent, state)); + playback.clockInfo.liveData.onUpdate((clock) => this.onPlaybackClock(playback, mxEvent, clock)); + } + + private onPlaybackStateChange(playback: Playback, mxEvent: MatrixEvent, newState: PlaybackState) { + // Remember where the user got to in playback + const wasLastPlaying = this.currentPlaybackId === mxEvent.getId(); + if (newState === PlaybackState.Stopped && this.clockStates.has(mxEvent.getId()) && !wasLastPlaying) { + // noinspection JSIgnoredPromiseFromCall + playback.skipTo(this.clockStates.get(mxEvent.getId())); + } else if (newState === PlaybackState.Stopped) { + // Remove the now-useless clock for some space savings + this.clockStates.delete(mxEvent.getId()); + + if (wasLastPlaying) { + this.recentFullPlays.add(this.currentPlaybackId); + const orderClone = arrayFastClone(this.playbackIdOrder); + const last = orderClone.pop(); + if (last === this.currentPlaybackId) { + const next = orderClone.pop(); + if (next) { + const instance = this.playbacks.get(next); + if (!instance) { + console.warn( + "Voice message queue desync: Missing playback for next message: " + + `Current=${this.currentPlaybackId} Last=${last} Next=${next}`, + ); + } else { + this.playbackIdOrder = orderClone; + PlaybackManager.instance.pauseAllExcept(instance); + + // This should cause a Play event, which will re-populate our playback order + // and update our current playback ID. + // noinspection JSIgnoredPromiseFromCall + instance.play(); + } + } else { + // else no explicit next event, so find an event we haven't played that comes next. The live + // timeline is already most recent last, so we can iterate down that. + const timeline = arrayFastClone(this.room.getLiveTimeline().getEvents()); + let scanForVoiceMessage = false; + let nextEv: MatrixEvent; + for (const event of timeline) { + if (event.getId() === mxEvent.getId()) { + scanForVoiceMessage = true; + continue; + } + if (!scanForVoiceMessage) continue; + + // Dev note: This is where we'd break to cause text/non-voice messages to + // interrupt automatic playback. + + const isRightType = isVoiceMessage(event); + const havePlayback = this.playbacks.has(event.getId()); + const isRecentlyCompleted = this.recentFullPlays.has(event.getId()); + if (isRightType && havePlayback && !isRecentlyCompleted) { + nextEv = event; + break; + } + } + if (!nextEv) { + // if we don't have anywhere to go, reset the recent playback queue so the user + // can start a new chain of playbacks. + this.recentFullPlays = new Set(); + this.playbackIdOrder = []; + } else { + this.playbackIdOrder = orderClone; + + const instance = this.playbacks.get(nextEv.getId()); + PlaybackManager.instance.pauseAllExcept(instance); + + // This should cause a Play event, which will re-populate our playback order + // and update our current playback ID. + // noinspection JSIgnoredPromiseFromCall + instance.play(); + } + } + } else { + console.warn( + "Voice message queue desync: Expected playback stop to be last in order. " + + `Current=${this.currentPlaybackId} Last=${last} EventID=${mxEvent.getId()}`, + ); + } + } + } + + if (newState === PlaybackState.Playing) { + const order = this.playbackIdOrder; + if (this.currentPlaybackId !== mxEvent.getId() && !!this.currentPlaybackId) { + if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) { + const lastInstance = this.playbacks.get(this.currentPlaybackId); + if ( + lastInstance.currentState === PlaybackState.Playing + || lastInstance.currentState === PlaybackState.Paused + ) { + order.push(this.currentPlaybackId); + } + } + } + + this.currentPlaybackId = mxEvent.getId(); + if (order.length === 0 || order[order.length - 1] !== this.currentPlaybackId) { + order.push(this.currentPlaybackId); + } + } + + // Only persist clock information on pause/stop (end) to avoid overwhelming the storage. + // This should get triggered from normal voice message component unmount due to the playback + // stopping itself for cleanup. + if (newState === PlaybackState.Paused || newState === PlaybackState.Stopped) { + this.persistClocks(); + } + } + + private onPlaybackClock(playback: Playback, mxEvent: MatrixEvent, clocks: number[]) { + if (playback.currentState === PlaybackState.Decoding) return; // ignore pre-ready values + + if (playback.currentState !== PlaybackState.Stopped) { + this.clockStates.set(mxEvent.getId(), clocks[0]); // [0] is the current seek position + } + } +} diff --git a/src/components/views/messages/MAudioBody.tsx b/src/components/views/messages/MAudioBody.tsx index 288ad16d88..1975fe8d42 100644 --- a/src/components/views/messages/MAudioBody.tsx +++ b/src/components/views/messages/MAudioBody.tsx @@ -24,6 +24,8 @@ import { IMediaEventContent } from "../../../customisations/models/IMediaEventCo import MFileBody from "./MFileBody"; import { IBodyProps } from "./IBodyProps"; import { PlaybackManager } from "../../../audio/PlaybackManager"; +import { isVoiceMessage } from "../../../utils/EventUtils"; +import { PlaybackQueue } from "../../../audio/PlaybackQueue"; interface IState { error?: Error; @@ -67,6 +69,10 @@ export default class MAudioBody extends React.PureComponent playback.clockInfo.populatePlaceholdersFrom(this.props.mxEvent); this.setState({ playback }); + if (isVoiceMessage(this.props.mxEvent)) { + PlaybackQueue.forRoom(this.props.mxEvent.getRoomId()).unsortedEnqueue(this.props.mxEvent, playback); + } + // Note: the components later on will handle preparing the Playback class for us. } diff --git a/src/components/views/rooms/VoiceRecordComposerTile.tsx b/src/components/views/rooms/VoiceRecordComposerTile.tsx index e8befb90fa..bd573fa474 100644 --- a/src/components/views/rooms/VoiceRecordComposerTile.tsx +++ b/src/components/views/rooms/VoiceRecordComposerTile.tsx @@ -179,7 +179,7 @@ export default class VoiceRecordComposerTile extends React.PureComponent Date: Tue, 31 Aug 2021 16:42:21 +0100 Subject: [PATCH 25/35] Only make the initial space rooms suggested by default --- src/components/structures/SpaceRoomView.tsx | 1 + src/createRoom.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 767e0999c3..5c9662dcf7 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -517,6 +517,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { andView: false, inlineErrors: true, parentSpace: space, + suggested: true, }); })); onFinished(filteredRoomNames.length > 0); diff --git a/src/createRoom.ts b/src/createRoom.ts index 31774bf56f..25e7257289 100644 --- a/src/createRoom.ts +++ b/src/createRoom.ts @@ -62,6 +62,8 @@ export interface IOpts { roomType?: RoomType | string; historyVisibility?: HistoryVisibility; parentSpace?: Room; + // contextually only makes sense if parentSpace is specified, if true then will be added to parentSpace as suggested + suggested?: boolean; joinRule?: JoinRule; } @@ -228,7 +230,7 @@ export default async function createRoom(opts: IOpts): Promise { } }).then(() => { if (opts.parentSpace) { - return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], true); + return SpaceStore.instance.addRoomToSpace(opts.parentSpace, roomId, [client.getDomain()], opts.suggested); } if (opts.associatedWithCommunity) { return GroupStore.addRoomToGroup(opts.associatedWithCommunity, roomId, false); From ae16695713eb47f0034d7b61b16ab9c8050ed334 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 1 Sep 2021 10:19:25 +0100 Subject: [PATCH 26/35] Fix Apache copyright headers --- src/components/views/rooms/TopUnreadMessagesBar.tsx | 3 +-- src/components/views/settings/ChangeDisplayName.tsx | 3 +-- src/components/views/settings/DevicesPanel.tsx | 3 +-- src/components/views/settings/DevicesPanelEntry.tsx | 3 +-- src/components/views/settings/IntegrationManager.tsx | 3 +-- src/components/views/settings/ProfileSettings.tsx | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/components/views/rooms/TopUnreadMessagesBar.tsx b/src/components/views/rooms/TopUnreadMessagesBar.tsx index ec2472f966..14f9a27f2d 100644 --- a/src/components/views/rooms/TopUnreadMessagesBar.tsx +++ b/src/components/views/rooms/TopUnreadMessagesBar.tsx @@ -1,6 +1,5 @@ /* -Copyright 2017 Vector Creations Ltd -Copyright 2016-2021 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. diff --git a/src/components/views/settings/ChangeDisplayName.tsx b/src/components/views/settings/ChangeDisplayName.tsx index 016f519dd9..9f0f813ec6 100644 --- a/src/components/views/settings/ChangeDisplayName.tsx +++ b/src/components/views/settings/ChangeDisplayName.tsx @@ -1,6 +1,5 @@ /* -Copyright 2018 - 2021 New Vector Ltd -Copyright 2015-2021 The Matrix.org Foundation C.I.C. +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index b6797b8ad5..5e297bbea6 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019 The Matrix.org Foundation C.I.C. -Copyright 2016-2021 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. diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index b589ffc7a1..d033bc41a9 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -1,6 +1,5 @@ /* -Copyright 2016-2021 The Matrix.org Foundation C.I.C. -Copyright 2021 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. diff --git a/src/components/views/settings/IntegrationManager.tsx b/src/components/views/settings/IntegrationManager.tsx index f9b3f67fad..0b880c019f 100644 --- a/src/components/views/settings/IntegrationManager.tsx +++ b/src/components/views/settings/IntegrationManager.tsx @@ -1,6 +1,5 @@ /* -Copyright 2015-2021 The Matrix.org Foundation C.I.C. -Copyright 2021 New Vector Ltd +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/components/views/settings/ProfileSettings.tsx b/src/components/views/settings/ProfileSettings.tsx index 9e1f0444b3..9563280550 100644 --- a/src/components/views/settings/ProfileSettings.tsx +++ b/src/components/views/settings/ProfileSettings.tsx @@ -1,6 +1,5 @@ /* -Copyright 2019-2021 New Vector Ltd -Copyright 2021 The Matrix.org Foundation C.I.C. +Copyright 2019 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From dca268e67a0c44ccd8659c8fa2ed40fa1b8efa34 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 1 Sep 2021 10:55:47 +0100 Subject: [PATCH 27/35] Replace eventIsReply util with replyEventId getter --- src/components/views/rooms/EditMessageComposer.tsx | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/views/rooms/EditMessageComposer.tsx b/src/components/views/rooms/EditMessageComposer.tsx index b7e067ee93..7a3767deb7 100644 --- a/src/components/views/rooms/EditMessageComposer.tsx +++ b/src/components/views/rooms/EditMessageComposer.tsx @@ -43,11 +43,6 @@ import QuestionDialog from "../dialogs/QuestionDialog"; import { ActionPayload } from "../../../dispatcher/payloads"; import AccessibleButton from '../elements/AccessibleButton'; -function eventIsReply(mxEvent: MatrixEvent): boolean { - const relatesTo = mxEvent.getContent()["m.relates_to"]; - return !!(relatesTo && relatesTo["m.in_reply_to"]); -} - function getHtmlReplyFallback(mxEvent: MatrixEvent): string { const html = mxEvent.getContent().formatted_body; if (!html) { @@ -72,7 +67,7 @@ function createEditContent(model: EditorModel, editedEvent: MatrixEvent): IConte if (isEmote) { model = stripEmoteCommand(model); } - const isReply = eventIsReply(editedEvent); + const isReply = !!editedEvent.replyEventId; let plainPrefix = ""; let htmlPrefix = ""; From 95d1b06abb4ad612bd00ee5569b4dd85269ddde3 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 1 Sep 2021 12:12:40 +0100 Subject: [PATCH 28/35] Make composer able to reply in thread or in room timeline --- src/components/structures/ThreadView.tsx | 1 + src/components/views/elements/ReplyThread.tsx | 4 ++- .../views/rooms/MessageComposer.tsx | 3 +++ .../views/rooms/SendMessageComposer.tsx | 27 ++++++++++++++----- .../views/rooms/SendMessageComposer-test.js | 8 +++--- 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index a2595debc8..94f3f26261 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -136,6 +136,7 @@ export default class ThreadView extends React.Component { { return { body, html }; } - public static makeReplyMixIn(ev: MatrixEvent) { + public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) { if (!ev) return {}; return { 'm.relates_to': { 'm.in_reply_to': { 'event_id': ev.getId(), + [UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]: replyInThread, }, }, }; diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index fbf3b58570..466675ac64 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -183,6 +183,7 @@ interface IProps { resizeNotifier: ResizeNotifier; permalinkCreator: RoomPermalinkCreator; replyToEvent?: MatrixEvent; + replyInThread?: boolean; showReplyPreview?: boolean; e2eStatus?: E2EStatus; compact?: boolean; @@ -204,6 +205,7 @@ export default class MessageComposer extends React.Component { private voiceRecordingButton: VoiceRecordComposerTile; static defaultProps = { + replyInThread: false, showReplyPreview: true, compact: false, }; @@ -383,6 +385,7 @@ export default class MessageComposer extends React.Component { room={this.props.room} placeholder={this.renderPlaceholderText()} permalinkCreator={this.props.permalinkCreator} + replyInThread={this.props.replyInThread} replyToEvent={this.props.replyToEvent} onChange={this.onChange} disabled={this.state.haveRecording} diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 205320fb68..aca397b6b2 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -57,15 +57,16 @@ import { ActionPayload } from "../../../dispatcher/payloads"; function addReplyToMessageContent( content: IContent, - repliedToEvent: MatrixEvent, + replyToEvent: MatrixEvent, + replyInThread: boolean, permalinkCreator: RoomPermalinkCreator, ): void { - const replyContent = ReplyThread.makeReplyMixIn(repliedToEvent); + const replyContent = ReplyThread.makeReplyMixIn(replyToEvent, replyInThread); Object.assign(content, replyContent); // Part of Replies fallback support - prepend the text we're sending // with the text we're replying to - const nestedReply = ReplyThread.getNestedReplyText(repliedToEvent, permalinkCreator); + const nestedReply = ReplyThread.getNestedReplyText(replyToEvent, permalinkCreator); if (nestedReply) { if (content.formatted_body) { content.formatted_body = nestedReply.html + content.formatted_body; @@ -77,8 +78,9 @@ function addReplyToMessageContent( // exported for tests export function createMessageContent( model: EditorModel, - permalinkCreator: RoomPermalinkCreator, replyToEvent: MatrixEvent, + replyInThread: boolean, + permalinkCreator: RoomPermalinkCreator, ): IContent { const isEmote = containsEmote(model); if (isEmote) { @@ -101,7 +103,7 @@ export function createMessageContent( } if (replyToEvent) { - addReplyToMessageContent(content, replyToEvent, permalinkCreator); + addReplyToMessageContent(content, replyToEvent, replyInThread, permalinkCreator); } return content; @@ -129,6 +131,7 @@ interface IProps { room: Room; placeholder?: string; permalinkCreator: RoomPermalinkCreator; + replyInThread?: boolean; replyToEvent?: MatrixEvent; disabled?: boolean; onChange?(model: EditorModel): void; @@ -357,7 +360,12 @@ export default class SendMessageComposer extends React.Component { if (cmd.category === CommandCategories.messages) { content = await this.runSlashCommand(cmd, args); if (replyToEvent) { - addReplyToMessageContent(content, replyToEvent, this.props.permalinkCreator); + addReplyToMessageContent( + content, + replyToEvent, + this.props.replyInThread, + this.props.permalinkCreator, + ); } } else { this.runSlashCommand(cmd, args); @@ -400,7 +408,12 @@ export default class SendMessageComposer extends React.Component { const startTime = CountlyAnalytics.getTimestamp(); const { roomId } = this.props.room; if (!content) { - content = createMessageContent(this.model, this.props.permalinkCreator, replyToEvent); + content = createMessageContent( + this.model, + replyToEvent, + this.props.replyInThread, + this.props.permalinkCreator, + ); } // don't bother sending an empty message if (!content.body.trim()) return; diff --git a/test/components/views/rooms/SendMessageComposer-test.js b/test/components/views/rooms/SendMessageComposer-test.js index 0c4bde76a8..db5b55df90 100644 --- a/test/components/views/rooms/SendMessageComposer-test.js +++ b/test/components/views/rooms/SendMessageComposer-test.js @@ -46,7 +46,7 @@ describe('', () => { const model = new EditorModel([], createPartCreator(), createRenderer()); model.update("hello world", "insertText", { offset: 11, atNodeEnd: true }); - const content = createMessageContent(model, permalinkCreator); + const content = createMessageContent(model, null, false, permalinkCreator); expect(content).toEqual({ body: "hello world", @@ -58,7 +58,7 @@ describe('', () => { const model = new EditorModel([], createPartCreator(), createRenderer()); model.update("hello *world*", "insertText", { offset: 13, atNodeEnd: true }); - const content = createMessageContent(model, permalinkCreator); + const content = createMessageContent(model, null, false, permalinkCreator); expect(content).toEqual({ body: "hello *world*", @@ -72,7 +72,7 @@ describe('', () => { const model = new EditorModel([], createPartCreator(), createRenderer()); model.update("/me blinks __quickly__", "insertText", { offset: 22, atNodeEnd: true }); - const content = createMessageContent(model, permalinkCreator); + const content = createMessageContent(model, null, false, permalinkCreator); expect(content).toEqual({ body: "blinks __quickly__", @@ -86,7 +86,7 @@ describe('', () => { const model = new EditorModel([], createPartCreator(), createRenderer()); model.update("//dev/null is my favourite place", "insertText", { offset: 32, atNodeEnd: true }); - const content = createMessageContent(model, permalinkCreator); + const content = createMessageContent(model, null, false, permalinkCreator); expect(content).toEqual({ body: "/dev/null is my favourite place", From 9b2c380b642a4a6abc401df1b19a3a912b334332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 1 Sep 2021 17:28:02 +0200 Subject: [PATCH 29/35] Split autoplay gifs and videos in to different settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MImageBody.tsx | 8 ++++---- src/components/views/messages/MVideoBody.tsx | 4 ++-- .../tabs/user/PreferencesUserSettingsTab.tsx | 3 ++- src/settings/Settings.tsx | 9 +++++++-- src/settings/handlers/AccountSettingsHandler.ts | 15 +++++++++++++++ 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 8ead8d9ba2..e36e4c3113 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -128,7 +128,7 @@ export default class MImageBody extends React.Component { private onImageEnter = (e: React.MouseEvent): void => { this.setState({ hover: true }); - if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { + if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifs")) { return; } const imgElement = e.currentTarget; @@ -138,7 +138,7 @@ export default class MImageBody extends React.Component { private onImageLeave = (e: React.MouseEvent): void => { this.setState({ hover: false }); - if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifsAndVideos")) { + if (!this.state.showImage || !this.isGif() || SettingsStore.getValue("autoplayGifs")) { return; } const imgElement = e.currentTarget; @@ -387,7 +387,7 @@ export default class MImageBody extends React.Component { showPlaceholder = false; // because we're hiding the image, so don't show the placeholder. } - if (this.isGif() && !SettingsStore.getValue("autoplayGifsAndVideos") && !this.state.hover) { + if (this.isGif() && !SettingsStore.getValue("autoplayGifs") && !this.state.hover) { gifLabel =

GIF

; } @@ -487,7 +487,7 @@ export default class MImageBody extends React.Component { const contentUrl = this.getContentUrl(); let thumbUrl; - if (this.isGif() && SettingsStore.getValue("autoplayGifsAndVideos")) { + if (this.isGif() && SettingsStore.getValue("autoplayGifs")) { thumbUrl = contentUrl; } else { thumbUrl = this.getThumbUrl(); diff --git a/src/components/views/messages/MVideoBody.tsx b/src/components/views/messages/MVideoBody.tsx index 77c7ebacda..de1915299c 100644 --- a/src/components/views/messages/MVideoBody.tsx +++ b/src/components/views/messages/MVideoBody.tsx @@ -145,7 +145,7 @@ export default class MVideoBody extends React.PureComponent } async componentDidMount() { - const autoplay = SettingsStore.getValue("autoplayGifsAndVideos") as boolean; + const autoplay = SettingsStore.getValue("autoplayVideo") as boolean; this.loadBlurhash(); if (this.props.mediaEventHelper.media.isEncrypted && this.state.decryptedUrl === null) { @@ -209,7 +209,7 @@ export default class MVideoBody extends React.PureComponent render() { const content = this.props.mxEvent.getContent(); - const autoplay = SettingsStore.getValue("autoplayGifsAndVideos"); + const autoplay = SettingsStore.getValue("autoplayVideo"); if (this.state.error !== null) { return ( diff --git a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx index 21c3ab24ec..2209537967 100644 --- a/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/PreferencesUserSettingsTab.tsx @@ -172,7 +172,8 @@ export default class PreferencesUserSettingsTab extends React.Component Date: Wed, 1 Sep 2021 17:28:09 +0200 Subject: [PATCH 30/35] i18n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/i18n/strings/en_EN.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2cb0a546aa..7fc29b02e2 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -832,7 +832,8 @@ "Show read receipts sent by other users": "Show read receipts sent by other users", "Show timestamps in 12 hour format (e.g. 2:30pm)": "Show timestamps in 12 hour format (e.g. 2:30pm)", "Always show message timestamps": "Always show message timestamps", - "Autoplay GIFs and videos": "Autoplay GIFs and videos", + "Autoplay GIFs": "Autoplay GIFs", + "Autoplay videos": "Autoplay videos", "Enable automatic language detection for syntax highlighting": "Enable automatic language detection for syntax highlighting", "Expand code blocks by default": "Expand code blocks by default", "Show line numbers in code blocks": "Show line numbers in code blocks", From 2ce86471206cba5f54985e63d9f9a16c91cc6d59 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 2 Sep 2021 08:36:20 +0100 Subject: [PATCH 31/35] Prevent unstable property to be sent with all events --- src/components/views/elements/ReplyThread.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx index d5b6af17f2..d061d52f46 100644 --- a/src/components/views/elements/ReplyThread.tsx +++ b/src/components/views/elements/ReplyThread.tsx @@ -209,14 +209,26 @@ export default class ReplyThread extends React.Component { public static makeReplyMixIn(ev: MatrixEvent, replyInThread: boolean) { if (!ev) return {}; - return { + + const replyMixin = { 'm.relates_to': { 'm.in_reply_to': { 'event_id': ev.getId(), - [UNSTABLE_ELEMENT_REPLY_IN_THREAD.name]: replyInThread, }, }, }; + + /** + * @experimental + * Rendering hint for threads, only attached if true to make + * sure that Element does not start sending that property for all events + */ + if (replyInThread) { + const inReplyTo = replyMixin['m.relates_to']['m.in_reply_to']; + inReplyTo[UNSTABLE_ELEMENT_REPLY_IN_THREAD.name] = replyInThread; + } + + return replyMixin; } public static makeThread( From c246b027bea6530fc7d686ef6d28c27bc66dec85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Thu, 2 Sep 2021 17:39:13 +0200 Subject: [PATCH 32/35] Add missing space Co-authored-by: Dariusz Niemczyk <3636685+Palid@users.noreply.github.com> --- src/settings/handlers/AccountSettingsHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/settings/handlers/AccountSettingsHandler.ts b/src/settings/handlers/AccountSettingsHandler.ts index 75cef07685..5afe50e4e9 100644 --- a/src/settings/handlers/AccountSettingsHandler.ts +++ b/src/settings/handlers/AccountSettingsHandler.ts @@ -116,7 +116,7 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa const value = settings[settingName]; // Fallback to old combined setting if (value === null || value === undefined) { - const oldCombinedValue= settings["autoplayGifsAndVideos"]; + const oldCombinedValue = settings["autoplayGifsAndVideos"]; // Write, so that we can remove this in the future this.setValue("autoplayGifs", roomId, oldCombinedValue); this.setValue("autoplayVideo", roomId, oldCombinedValue); From 40cb8e8fc6d4b05e7c4bd097a37081416d8179c7 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk Date: Thu, 2 Sep 2021 21:07:08 +0200 Subject: [PATCH 33/35] Fix unnecessary blurhash rendering --- src/components/views/messages/MImageBody.tsx | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index e36e4c3113..cb52155f42 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -55,6 +55,7 @@ export default class MImageBody extends React.Component { static contextType = MatrixClientContext; private unmounted = true; private image = createRef(); + private timeout?: number; constructor(props: IBodyProps) { super(props); @@ -146,12 +147,14 @@ export default class MImageBody extends React.Component { }; private onImageError = (): void => { + this.clearBlurhashTimeout(); this.setState({ imgError: true, }); }; private onImageLoad = (): void => { + this.clearBlurhashTimeout(); this.props.onHeightChanged(); let loadedImageDimensions; @@ -267,6 +270,13 @@ export default class MImageBody extends React.Component { } } + private clearBlurhashTimeout() { + if (this.timeout) { + clearTimeout(this.timeout); + this.timeout = undefined; + } + } + componentDidMount() { this.unmounted = false; this.context.on('sync', this.onClientSync); @@ -281,8 +291,9 @@ export default class MImageBody extends React.Component { } // else don't download anything because we don't want to display anything. // Add a 150ms timer for blurhash to first appear. - if (this.media.isEncrypted) { - setTimeout(() => { + if (this.props.mxEvent.getContent().info?.[BLURHASH_FIELD]) { + this.clearBlurhashTimeout(); + this.timeout = setTimeout(() => { if (!this.state.imgLoaded || !this.state.imgError) { this.setState({ placeholder: 'blurhash', @@ -295,6 +306,7 @@ export default class MImageBody extends React.Component { componentWillUnmount() { this.unmounted = true; this.context.removeListener('sync', this.onClientSync); + this.clearBlurhashTimeout(); } protected messageContent( From 57aa045195d32103d03f81212e46256563e4d9d4 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Sep 2021 14:27:20 -0600 Subject: [PATCH 34/35] Don't rely on rooms having timelines to use when checking widgets --- src/stores/widgets/StopGapWidget.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index d3c886d4b8..0013c77e1c 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -301,7 +301,9 @@ export class StopGapWidget extends EventEmitter { // requests timeline capabilities in other rooms down the road. It's just easier to manage here. for (const room of MatrixClientPeg.get().getRooms()) { // Timelines are most recent last - this.readUpToMap[room.roomId] = arrayFastClone(room.getLiveTimeline().getEvents()).reverse()[0].getId(); + const roomEvent = arrayFastClone(room.getLiveTimeline()?.getEvents() || []).reverse()[0]; + if (!roomEvent) continue; // force later code to think the room is fresh + this.readUpToMap[room.roomId] = roomEvent.getId(); } // Attach listeners for feeding events - the underlying widget classes handle permissions for us From 65905721245f979a656ec69204f3051d605744cf Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 2 Sep 2021 15:24:23 -0600 Subject: [PATCH 35/35] Improve performance of widget startup slightly See https://github.com/matrix-org/matrix-react-sdk/pull/6733#pullrequestreview-745513275 --- src/stores/widgets/StopGapWidget.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index 0013c77e1c..750034c573 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -301,7 +301,8 @@ export class StopGapWidget extends EventEmitter { // requests timeline capabilities in other rooms down the road. It's just easier to manage here. for (const room of MatrixClientPeg.get().getRooms()) { // Timelines are most recent last - const roomEvent = arrayFastClone(room.getLiveTimeline()?.getEvents() || []).reverse()[0]; + const events = room.getLiveTimeline()?.getEvents() || []; + const roomEvent = events[events.length - 1]; if (!roomEvent) continue; // force later code to think the room is fresh this.readUpToMap[room.roomId] = roomEvent.getId(); }