From ee8d1f51c2733e1e2550fc06868fc50266f69d0f Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 3 Nov 2020 15:51:23 +0000 Subject: [PATCH 01/67] Fix onPaste handler to work with copying files from Finder --- src/components/views/rooms/SendMessageComposer.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.js b/src/components/views/rooms/SendMessageComposer.js index 9438cceef5..c816c84c9d 100644 --- a/src/components/views/rooms/SendMessageComposer.js +++ b/src/components/views/rooms/SendMessageComposer.js @@ -445,13 +445,11 @@ export default class SendMessageComposer extends React.Component { _onPaste = (event) => { const {clipboardData} = event; - // Prioritize text on the clipboard over files as Office on macOS puts a bitmap - // in the clipboard as well as the content being copied. - if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) { - // This actually not so much for 'files' as such (at time of writing - // neither chrome nor firefox let you paste a plain file copied - // from Finder) but more images copied from a different website - // / word processor etc. + // Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap + // in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore. + // We check text/rtf instead of text/plain as when copy+pasting a file from Finder it puts the filename + // in as text/plain which we want to ignore. + if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) { ContentMessages.sharedInstance().sendContentListToRoom( Array.from(clipboardData.files), this.props.room.roomId, this.context, ); From 73b9ad41da12e1092a850efa32c4e6a296342103 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 5 May 2021 12:38:44 +0530 Subject: [PATCH 02/67] Navigate to room with maximum notifications when clicked on already selected space --- src/stores/SpaceStore.tsx | 15 +++++++++++++-- .../notifications/SpaceNotificationState.ts | 5 +++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 43822007c9..7c0f8cf59b 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -120,8 +120,19 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * should not be done when the space switch is done implicitly due to another event like switching room. */ public async setActiveSpace(space: Room | null, contextSwitch = true) { - if (space === this.activeSpace || (space && !space?.isSpaceRoom())) return; - + if (space && !space?.isSpaceRoom()) return; + if (space === this.activeSpace) { + const notificationState = this.getNotificationState(space.roomId); + if (notificationState.count) { + const roomId = notificationState.getRoomWithMaxNotifications(); + defaultDispatcher.dispatch({ + action: "view_room", + room_id: roomId, + context_switch: true, + }); + } + return; + } this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts index 61a9701a07..fb04648a2a 100644 --- a/src/stores/notifications/SpaceNotificationState.ts +++ b/src/stores/notifications/SpaceNotificationState.ts @@ -53,6 +53,11 @@ export class SpaceNotificationState extends NotificationState { this.calculateTotalState(); } + public getRoomWithMaxNotifications() { + return this.rooms.reduce((prev, curr) => + (prev._notificationCounts.total > curr._notificationCounts.total ? prev : curr)).roomId; + } + public destroy() { super.destroy(); for (const state of Object.values(this.states)) { From bcd1005e3c2ef17e8d6b9212a72d238a639ecbfa Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 5 May 2021 13:01:14 +0530 Subject: [PATCH 03/67] Check truthiness of space --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 7c0f8cf59b..d72ee93956 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -121,7 +121,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { */ public async setActiveSpace(space: Room | null, contextSwitch = true) { if (space && !space?.isSpaceRoom()) return; - if (space === this.activeSpace) { + if (space && space === this.activeSpace) { const notificationState = this.getNotificationState(space.roomId); if (notificationState.count) { const roomId = notificationState.getRoomWithMaxNotifications(); From d3fc047b584836cc2a272c521a6a859eacf89290 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 5 May 2021 13:24:06 +0530 Subject: [PATCH 04/67] Handle home space --- src/stores/SpaceStore.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d72ee93956..5e6d4c8488 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -132,7 +132,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); } return; - } + } else if (space === this.activeSpace) return; + this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); this.emit(SUGGESTED_ROOMS, this._suggestedRooms = []); From 49b61d512f26182e5992b3f2b24193e5e16ff70f Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 5 May 2021 13:46:11 +0530 Subject: [PATCH 05/67] Replicate same behaviour for the home space --- src/stores/SpaceStore.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 5e6d4c8488..d307c56889 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -121,8 +121,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient { */ public async setActiveSpace(space: Room | null, contextSwitch = true) { if (space && !space?.isSpaceRoom()) return; - if (space && space === this.activeSpace) { - const notificationState = this.getNotificationState(space.roomId); + if (space === this.activeSpace) { + const notificationState = this.getNotificationState(space ? space.roomId : HOME_SPACE); if (notificationState.count) { const roomId = notificationState.getRoomWithMaxNotifications(); defaultDispatcher.dispatch({ @@ -132,7 +132,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { }); } return; - } else if (space === this.activeSpace) return; + } this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); From 14f94c388306c64d6ee47aed6e5f2b7ee482720d Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Tue, 11 May 2021 10:41:31 +0530 Subject: [PATCH 06/67] Remove excessive null check Co-authored-by: Travis Ralston --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d307c56889..d906157435 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -120,7 +120,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * should not be done when the space switch is done implicitly due to another event like switching room. */ public async setActiveSpace(space: Room | null, contextSwitch = true) { - if (space && !space?.isSpaceRoom()) return; + if (!space?.isSpaceRoom()) return; if (space === this.activeSpace) { const notificationState = this.getNotificationState(space ? space.roomId : HOME_SPACE); if (notificationState.count) { From 07a952a1bbca35fcd57e130d45ddc5e45d13fde6 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Tue, 11 May 2021 11:01:28 +0530 Subject: [PATCH 07/67] Update src/stores/SpaceStore.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Šimon Brandner --- src/stores/SpaceStore.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index d906157435..2f52061783 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -120,7 +120,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * should not be done when the space switch is done implicitly due to another event like switching room. */ public async setActiveSpace(space: Room | null, contextSwitch = true) { - if (!space?.isSpaceRoom()) return; + if (space && !space.isSpaceRoom()) return; if (space === this.activeSpace) { const notificationState = this.getNotificationState(space ? space.roomId : HOME_SPACE); if (notificationState.count) { From 3e8863fc9af0d5932c3393d0156ab6063baee524 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Tue, 11 May 2021 13:00:42 +0530 Subject: [PATCH 08/67] Adjust behaviour for the home space --- src/stores/SpaceStore.tsx | 5 ++++- .../notifications/SummarizedNotificationState.ts | 12 +++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index edc6bbef77..b1993d9625 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -118,7 +118,10 @@ export class SpaceStoreClass extends AsyncStoreWithClient { public async setActiveSpace(space: Room | null, contextSwitch = true) { if (space && !space.isSpaceRoom()) return; if (space === this.activeSpace) { - const notificationState = this.getNotificationState(space ? space.roomId : HOME_SPACE); + const notificationState = space + ? this.getNotificationState(space.roomId) + : RoomNotificationStateStore.instance.globalState; + if (notificationState.count) { const roomId = notificationState.getRoomWithMaxNotifications(); defaultDispatcher.dispatch({ diff --git a/src/stores/notifications/SummarizedNotificationState.ts b/src/stores/notifications/SummarizedNotificationState.ts index 372da74f36..4a3473792a 100644 --- a/src/stores/notifications/SummarizedNotificationState.ts +++ b/src/stores/notifications/SummarizedNotificationState.ts @@ -16,6 +16,8 @@ limitations under the License. import { NotificationColor } from "./NotificationColor"; import { NotificationState } from "./NotificationState"; +import { Room } from "matrix-js-sdk/src/models/room"; +import { RoomNotificationState } from "./RoomNotificationState"; /** * Summarizes a number of states into a unique snapshot. To populate, call @@ -25,11 +27,13 @@ import { NotificationState } from "./NotificationState"; */ export class SummarizedNotificationState extends NotificationState { private totalStatesWithUnread = 0; + unreadRooms: Room[]; constructor() { super(); this._symbol = null; this._count = 0; + this.unreadRooms = []; this._color = NotificationColor.None; } @@ -37,6 +41,11 @@ export class SummarizedNotificationState extends NotificationState { return this.totalStatesWithUnread; } + public getRoomWithMaxNotifications() { + return this.unreadRooms.reduce((prev, curr) => + (prev._notificationCounts.total > curr._notificationCounts.total ? prev : curr)).roomId; + } + /** * Append a notification state to this snapshot, taking the loudest NotificationColor * of the two. By default this will not adopt the symbol of the other notification @@ -45,7 +54,7 @@ export class SummarizedNotificationState extends NotificationState { * @param includeSymbol If true, the notification state's symbol will be taken if one * is present. */ - public add(other: NotificationState, includeSymbol = false) { + public add(other: RoomNotificationState, includeSymbol = false) { if (other.symbol && includeSymbol) { this._symbol = other.symbol; } @@ -56,6 +65,7 @@ export class SummarizedNotificationState extends NotificationState { this._color = other.color; } if (other.hasUnreadCount) { + this.unreadRooms.push(other.room); this.totalStatesWithUnread++; } } From bf2d26ef21664e427540ff4cb29c89c7f14dcb70 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Thu, 20 May 2021 10:55:22 +0530 Subject: [PATCH 09/67] Modify to navigate only on notification dots click --- src/components/views/spaces/SpacePanel.tsx | 6 +++- .../views/spaces/SpaceTreeLevel.tsx | 6 +++- src/stores/SpaceStore.tsx | 35 ++++++++++--------- .../notifications/SpaceNotificationState.ts | 5 ++- .../SummarizedNotificationState.ts | 12 +++---- 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/components/views/spaces/SpacePanel.tsx b/src/components/views/spaces/SpacePanel.tsx index 411b0f9b5e..74fd01954d 100644 --- a/src/components/views/spaces/SpacePanel.tsx +++ b/src/components/views/spaces/SpacePanel.tsx @@ -74,7 +74,11 @@ const SpaceButton: React.FC = ({ let notifBadge; if (notificationState) { notifBadge =
- + SpaceStore.instance.setActiveRoomInSpace(space)} + forceCount={false} + notification={notificationState} + />
; } diff --git a/src/components/views/spaces/SpaceTreeLevel.tsx b/src/components/views/spaces/SpaceTreeLevel.tsx index e48e1d5dc2..d8569a0387 100644 --- a/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/src/components/views/spaces/SpaceTreeLevel.tsx @@ -326,7 +326,11 @@ export class SpaceItem extends React.PureComponent { let notifBadge; if (notificationState) { notifBadge =
- + SpaceStore.instance.setActiveRoomInSpace(space)} + forceCount={false} + notification={notificationState} + />
; } diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index b1993d9625..e154463408 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -108,6 +108,24 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._suggestedRooms; } + public async setActiveRoomInSpace(space: Room | null) { + if (space && !space.isSpaceRoom()) return; + if (space !== this.activeSpace) await this.setActiveSpace(space); + + const notificationState = space + ? this.getNotificationState(space.roomId) + : RoomNotificationStateStore.instance.globalState; + + if (notificationState.count) { + const roomId = notificationState.getFirstRoomWithNotifications(); + defaultDispatcher.dispatch({ + action: "view_room", + room_id: roomId, + context_switch: true, + }); + } + } + /** * Sets the active space, updates room list filters, * optionally switches the user's room back to where they were when they last viewed that space. @@ -116,22 +134,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * should not be done when the space switch is done implicitly due to another event like switching room. */ public async setActiveSpace(space: Room | null, contextSwitch = true) { - if (space && !space.isSpaceRoom()) return; - if (space === this.activeSpace) { - const notificationState = space - ? this.getNotificationState(space.roomId) - : RoomNotificationStateStore.instance.globalState; - - if (notificationState.count) { - const roomId = notificationState.getRoomWithMaxNotifications(); - defaultDispatcher.dispatch({ - action: "view_room", - room_id: roomId, - context_switch: true, - }); - } - return; - } + if (space === this.activeSpace || (space && !space.isSpaceRoom())) return; this._activeSpace = space; this.emit(UPDATE_SELECTED_SPACE, this.activeSpace); diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts index fb04648a2a..cdb9f2d06a 100644 --- a/src/stores/notifications/SpaceNotificationState.ts +++ b/src/stores/notifications/SpaceNotificationState.ts @@ -53,9 +53,8 @@ export class SpaceNotificationState extends NotificationState { this.calculateTotalState(); } - public getRoomWithMaxNotifications() { - return this.rooms.reduce((prev, curr) => - (prev._notificationCounts.total > curr._notificationCounts.total ? prev : curr)).roomId; + public getFirstRoomWithNotifications() { + return this.rooms.find((room) => room._notificationCounts.total > 0).roomId; } public destroy() { diff --git a/src/stores/notifications/SummarizedNotificationState.ts b/src/stores/notifications/SummarizedNotificationState.ts index 4a3473792a..ec6db1015d 100644 --- a/src/stores/notifications/SummarizedNotificationState.ts +++ b/src/stores/notifications/SummarizedNotificationState.ts @@ -16,7 +16,6 @@ limitations under the License. import { NotificationColor } from "./NotificationColor"; import { NotificationState } from "./NotificationState"; -import { Room } from "matrix-js-sdk/src/models/room"; import { RoomNotificationState } from "./RoomNotificationState"; /** @@ -27,13 +26,13 @@ import { RoomNotificationState } from "./RoomNotificationState"; */ export class SummarizedNotificationState extends NotificationState { private totalStatesWithUnread = 0; - unreadRooms: Room[]; + private unreadRoomId: string; constructor() { super(); this._symbol = null; this._count = 0; - this.unreadRooms = []; + this.unreadRoomId = null; this._color = NotificationColor.None; } @@ -41,9 +40,8 @@ export class SummarizedNotificationState extends NotificationState { return this.totalStatesWithUnread; } - public getRoomWithMaxNotifications() { - return this.unreadRooms.reduce((prev, curr) => - (prev._notificationCounts.total > curr._notificationCounts.total ? prev : curr)).roomId; + public getFirstRoomWithNotifications() { + return this.unreadRoomId; } /** @@ -65,7 +63,7 @@ export class SummarizedNotificationState extends NotificationState { this._color = other.color; } if (other.hasUnreadCount) { - this.unreadRooms.push(other.room); + this.unreadRoomId = !this.unreadRoomId ? other.room.roomId : this.unreadRoomId; this.totalStatesWithUnread++; } } From d466d1a7eef753ba41ee81ddaa4d7f52b423d3ea Mon Sep 17 00:00:00 2001 From: Robin Townsend Date: Thu, 24 Jun 2021 11:28:16 -0400 Subject: [PATCH 10/67] Add alwaysShowTimestamps and others to RoomView setting watchers to allow them to update on the fly. This also modifies the setting watchers to avoid an unnecessary settings lookup. Signed-off-by: Robin Townsend --- src/components/structures/RoomView.tsx | 58 ++++++++++++--------- src/components/structures/TimelinePanel.tsx | 12 +++-- src/contexts/RoomContext.ts | 4 ++ 3 files changed, 46 insertions(+), 28 deletions(-) diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 338da29875..59d2bc3e71 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -177,6 +177,10 @@ export interface IState { canReply: boolean; layout: Layout; lowBandwidth: boolean; + alwaysShowTimestamps: boolean; + showTwelveHourTimestamps: boolean; + readMarkerInViewThresholdMs: number; + readMarkerOutOfViewThresholdMs: number; showReadReceipts: boolean; showRedactions: boolean; showJoinLeaves: boolean; @@ -240,6 +244,10 @@ export default class RoomView extends React.Component { canReply: false, layout: SettingsStore.getValue("layout"), lowBandwidth: SettingsStore.getValue("lowBandwidth"), + alwaysShowTimestamps: SettingsStore.getValue("alwaysShowTimestamps"), + showTwelveHourTimestamps: SettingsStore.getValue("showTwelveHourTimestamps"), + readMarkerInViewThresholdMs: SettingsStore.getValue("readMarkerInViewThresholdMs"), + readMarkerOutOfViewThresholdMs: SettingsStore.getValue("readMarkerOutOfViewThresholdMs"), showReadReceipts: true, showRedactions: true, showJoinLeaves: true, @@ -272,11 +280,23 @@ export default class RoomView extends React.Component { WidgetStore.instance.on(UPDATE_EVENT, this.onWidgetStoreUpdate); this.settingWatchers = [ - SettingsStore.watchSetting("layout", null, () => - this.setState({ layout: SettingsStore.getValue("layout") }), + SettingsStore.watchSetting("layout", null, (...[,,, value]) => + this.setState({ layout: value as Layout }), ), - SettingsStore.watchSetting("lowBandwidth", null, () => - this.setState({ lowBandwidth: SettingsStore.getValue("lowBandwidth") }), + SettingsStore.watchSetting("lowBandwidth", null, (...[,,, value]) => + this.setState({ lowBandwidth: value as boolean }), + ), + SettingsStore.watchSetting("alwaysShowTimestamps", null, (...[,,, value]) => + this.setState({ alwaysShowTimestamps: value as boolean }), + ), + SettingsStore.watchSetting("showTwelveHourTimestamps", null, (...[,,, value]) => + this.setState({ showTwelveHourTimestamps: value as boolean }), + ), + SettingsStore.watchSetting("readMarkerInViewThresholdMs", null, (...[,,, value]) => + this.setState({ readMarkerInViewThresholdMs: value as number }), + ), + SettingsStore.watchSetting("readMarkerOutOfViewThresholdMs", null, (...[,,, value]) => + this.setState({ readMarkerOutOfViewThresholdMs: value as number }), ), ]; } @@ -343,30 +363,20 @@ export default class RoomView extends React.Component { // Add watchers for each of the settings we just looked up this.settingWatchers = this.settingWatchers.concat([ - SettingsStore.watchSetting("showReadReceipts", null, () => - this.setState({ - showReadReceipts: SettingsStore.getValue("showReadReceipts", roomId), - }), + SettingsStore.watchSetting("showReadReceipts", roomId, (...[,,, value]) => + this.setState({ showReadReceipts: value as boolean }), ), - SettingsStore.watchSetting("showRedactions", null, () => - this.setState({ - showRedactions: SettingsStore.getValue("showRedactions", roomId), - }), + SettingsStore.watchSetting("showRedactions", roomId, (...[,,, value]) => + this.setState({ showRedactions: value as boolean }), ), - SettingsStore.watchSetting("showJoinLeaves", null, () => - this.setState({ - showJoinLeaves: SettingsStore.getValue("showJoinLeaves", roomId), - }), + SettingsStore.watchSetting("showJoinLeaves", roomId, (...[,,, value]) => + this.setState({ showJoinLeaves: value as boolean }), ), - SettingsStore.watchSetting("showAvatarChanges", null, () => - this.setState({ - showAvatarChanges: SettingsStore.getValue("showAvatarChanges", roomId), - }), + SettingsStore.watchSetting("showAvatarChanges", roomId, (...[,,, value]) => + this.setState({ showAvatarChanges: value as boolean }), ), - SettingsStore.watchSetting("showDisplaynameChanges", null, () => - this.setState({ - showDisplaynameChanges: SettingsStore.getValue("showDisplaynameChanges", roomId), - }), + SettingsStore.watchSetting("showDisplaynameChanges", roomId, (...[,,, value]) => + this.setState({ showDisplaynameChanges: value as boolean }), ), ]); diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index c2e7a6f346..1a19c2c0ca 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -699,8 +699,8 @@ class TimelinePanel extends React.Component { private readMarkerTimeout(readMarkerPosition: number): number { return readMarkerPosition === 0 ? - this.state.readMarkerInViewThresholdMs : - this.state.readMarkerOutOfViewThresholdMs; + this.context?.readMarkerInViewThresholdMs ?? this.state.readMarkerInViewThresholdMs : + this.context?.readMarkerOutOfViewThresholdMs ?? this.state.readMarkerOutOfViewThresholdMs; } private async updateReadMarkerOnUserActivity(): Promise { @@ -1520,8 +1520,12 @@ class TimelinePanel extends React.Component { onUserScroll={this.props.onUserScroll} onFillRequest={this.onMessageListFillRequest} onUnfillRequest={this.onMessageListUnfillRequest} - isTwelveHour={this.state.isTwelveHour} - alwaysShowTimestamps={this.props.alwaysShowTimestamps || this.state.alwaysShowTimestamps} + isTwelveHour={this.context?.showTwelveHourTimestamps ?? this.state.isTwelveHour} + alwaysShowTimestamps={ + this.props.alwaysShowTimestamps ?? + this.context?.alwaysShowTimestamps ?? + this.state.alwaysShowTimestamps + } className={this.props.className} tileShape={this.props.tileShape} resizeNotifier={this.props.resizeNotifier} diff --git a/src/contexts/RoomContext.ts b/src/contexts/RoomContext.ts index 3464f952a6..495350c7f3 100644 --- a/src/contexts/RoomContext.ts +++ b/src/contexts/RoomContext.ts @@ -41,6 +41,10 @@ const RoomContext = createContext({ canReply: false, layout: Layout.Group, lowBandwidth: false, + alwaysShowTimestamps: false, + showTwelveHourTimestamps: false, + readMarkerInViewThresholdMs: 3000, + readMarkerOutOfViewThresholdMs: 30000, showReadReceipts: true, showRedactions: true, showJoinLeaves: true, From 1b21c8f7328478a56262a9e5dc2dbcbfe1f947c1 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 30 Jun 2021 10:53:46 +0530 Subject: [PATCH 11/67] Remove unreadRoomId from summarized notification state --- src/components/views/rooms/RoomList.tsx | 2 +- src/stores/SpaceStore.tsx | 34 ++++++++++++++----- .../notifications/SpaceNotificationState.ts | 2 +- .../SummarizedNotificationState.ts | 6 ---- 4 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/components/views/rooms/RoomList.tsx b/src/components/views/rooms/RoomList.tsx index eb50224a60..6511c12372 100644 --- a/src/components/views/rooms/RoomList.tsx +++ b/src/components/views/rooms/RoomList.tsx @@ -68,7 +68,7 @@ interface IState { suggestedRooms: ISuggestedRoom[]; } -const TAG_ORDER: TagID[] = [ +export const TAG_ORDER: TagID[] = [ DefaultTagID.Invite, DefaultTagID.Favourite, DefaultTagID.DM, diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index c8144902c9..105d98a8e0 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -38,6 +38,7 @@ import { arrayHasDiff } from "../utils/arrays"; import { objectDiff } from "../utils/objects"; import { arrayHasOrderChange } from "../utils/arrays"; import { reorderLexicographically } from "../utils/stringOrderField"; +import { TAG_ORDER } from "../components/views/rooms/RoomList"; type SpaceKey = string | symbol; @@ -128,16 +129,33 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (space && !space.isSpaceRoom()) return; if (space !== this.activeSpace) await this.setActiveSpace(space); - const notificationState = space - ? this.getNotificationState(space.roomId) - : RoomNotificationStateStore.instance.globalState; - - if (notificationState.count) { + if (space) { + const notificationState = this.getNotificationState(space.roomId) const roomId = notificationState.getFirstRoomWithNotifications(); defaultDispatcher.dispatch({ - action: "view_room", - room_id: roomId, - context_switch: true, + action: "view_room", + room_id: roomId, + context_switch: true, + }); + } else { + const lists = RoomListStore.instance.unfilteredLists; + TAG_ORDER.every(t => { + const listRooms = lists[t]; + const unreadRoom = listRooms.find((r: Room)=> { + if (this.showInHomeSpace(r)) { + const state = RoomNotificationStateStore.instance.getRoomState(r); + return state.isUnread; + } + }); + if (unreadRoom) { + defaultDispatcher.dispatch({ + action: "view_room", + room_id: unreadRoom.roomId, + context_switch: true, + }); + return false; + } + return true; }); } } diff --git a/src/stores/notifications/SpaceNotificationState.ts b/src/stores/notifications/SpaceNotificationState.ts index cdb9f2d06a..4c0a582f3f 100644 --- a/src/stores/notifications/SpaceNotificationState.ts +++ b/src/stores/notifications/SpaceNotificationState.ts @@ -54,7 +54,7 @@ export class SpaceNotificationState extends NotificationState { } public getFirstRoomWithNotifications() { - return this.rooms.find((room) => room._notificationCounts.total > 0).roomId; + return this.rooms.find((room) => room.getUnreadNotificationCount() > 0).roomId; } public destroy() { diff --git a/src/stores/notifications/SummarizedNotificationState.ts b/src/stores/notifications/SummarizedNotificationState.ts index ec6db1015d..6b69e1d470 100644 --- a/src/stores/notifications/SummarizedNotificationState.ts +++ b/src/stores/notifications/SummarizedNotificationState.ts @@ -32,7 +32,6 @@ export class SummarizedNotificationState extends NotificationState { super(); this._symbol = null; this._count = 0; - this.unreadRoomId = null; this._color = NotificationColor.None; } @@ -40,10 +39,6 @@ export class SummarizedNotificationState extends NotificationState { return this.totalStatesWithUnread; } - public getFirstRoomWithNotifications() { - return this.unreadRoomId; - } - /** * Append a notification state to this snapshot, taking the loudest NotificationColor * of the two. By default this will not adopt the symbol of the other notification @@ -63,7 +58,6 @@ export class SummarizedNotificationState extends NotificationState { this._color = other.color; } if (other.hasUnreadCount) { - this.unreadRoomId = !this.unreadRoomId ? other.room.roomId : this.unreadRoomId; this.totalStatesWithUnread++; } } From f50604db784d043b1ba749bf7a7eb2eb9c3b7946 Mon Sep 17 00:00:00 2001 From: Jaiwanth Date: Wed, 30 Jun 2021 12:13:39 +0530 Subject: [PATCH 12/67] missing semicolon --- src/stores/SpaceStore.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index c5227b4f8a..514f8418b8 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -130,7 +130,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { if (space !== this.activeSpace) await this.setActiveSpace(space); if (space) { - const notificationState = this.getNotificationState(space.roomId) + const notificationState = this.getNotificationState(space.roomId); const roomId = notificationState.getFirstRoomWithNotifications(); defaultDispatcher.dispatch({ action: "view_room", @@ -141,7 +141,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { const lists = RoomListStore.instance.unfilteredLists; TAG_ORDER.every(t => { const listRooms = lists[t]; - const unreadRoom = listRooms.find((r: Room)=> { + const unreadRoom = listRooms.find((r: Room) => { if (this.showInHomeSpace(r)) { const state = RoomNotificationStateStore.instance.getRoomState(r); return state.isUnread; From 3921e42e8a753d2f393675b678daa218d403db0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 12:32:30 +0200 Subject: [PATCH 13/67] Make diff colors in codeblocks more pleseant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/themes/dark/css/_dark.scss | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 57cbc7efa9..1f814f08b8 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -288,3 +288,11 @@ $composer-shadow-color: rgba(0, 0, 0, 0.28); .hljs-tag { color: inherit; // Without this they'd be weirdly blue which doesn't match the theme } + +.hljs-addition { + background: #1a4b59; +} + +.hljs-deletion { + background: #53232a; +} From 4ddcb9a484fe0a0b4b4e2afd39640bb3742a76db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 12 Jul 2021 16:59:16 +0200 Subject: [PATCH 14/67] Make diffs look a bit better MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_EventTile.scss | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 55f73c0315..ebd5002843 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -480,6 +480,11 @@ $hover-select-border: 4px; background-color: $header-panel-bg-color; } + pre code > * { + display: inline-block; + width: 100%; + } + pre { // have to use overlay rather than auto otherwise Linux and Windows // Chrome gets very confused about vertical spacing: From 8f6458a79c8ba4fa52e88ca79411ba5ea831e722 Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 16 Jul 2021 01:43:03 -0500 Subject: [PATCH 15/67] Add matrix: to the list of permitted URL schemes Signed-off-by: Aaron Raimist --- src/HtmlUtils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HtmlUtils.tsx b/src/HtmlUtils.tsx index 5e83fdc2a0..dfe5cba3fd 100644 --- a/src/HtmlUtils.tsx +++ b/src/HtmlUtils.tsx @@ -58,7 +58,7 @@ const BIGEMOJI_REGEX = new RegExp(`^(${EMOJIBASE_REGEX.source})+$`, 'i'); const COLOR_REGEX = /^#[0-9a-fA-F]{6}$/; -export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet']; +export const PERMITTED_URL_SCHEMES = ['http', 'https', 'ftp', 'mailto', 'magnet', 'matrix']; const MEDIA_API_MXC_REGEX = /\/_matrix\/media\/r0\/(?:download|thumbnail)\/(.+?)\/(.+?)(?:[?/]|$)/; From e6007874d9c68315cc4b709d4d506ff0cb10ac4c Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 16 Jul 2021 11:43:06 +0100 Subject: [PATCH 16/67] post-merge fixup --- src/components/views/rooms/SendMessageComposer.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/views/rooms/SendMessageComposer.tsx b/src/components/views/rooms/SendMessageComposer.tsx index 0639c20fef..04f74fb2b2 100644 --- a/src/components/views/rooms/SendMessageComposer.tsx +++ b/src/components/views/rooms/SendMessageComposer.tsx @@ -514,13 +514,11 @@ export default class SendMessageComposer extends React.Component { private onPaste = (event: ClipboardEvent): boolean => { const { clipboardData } = event; - // Prioritize text on the clipboard over files as Office on macOS puts a bitmap - // in the clipboard as well as the content being copied. - if (clipboardData.files.length && !clipboardData.types.some(t => t === "text/plain")) { - // This actually not so much for 'files' as such (at time of writing - // neither chrome nor firefox let you paste a plain file copied - // from Finder) but more images copied from a different website - // / word processor etc. + // Prioritize text on the clipboard over files if RTF is present as Office on macOS puts a bitmap + // in the clipboard as well as the content being copied. Modern versions of Office seem to not do this anymore. + // We check text/rtf instead of text/plain as when copy+pasting a file from Finder or Gnome Image Viewer + // it puts the filename in as text/plain which we want to ignore. + if (clipboardData.files.length && !clipboardData.types.includes("text/rtf")) { ContentMessages.sharedInstance().sendContentListToRoom( Array.from(clipboardData.files), this.props.room.roomId, this.context, ); From 28f58278847b49cb385cb532c2fc8aaeaa5ca451 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Jul 2021 18:02:33 +0200 Subject: [PATCH 17/67] Add FileUtils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/FileUtils.ts | 53 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/utils/FileUtils.ts diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts new file mode 100644 index 0000000000..0adcb172e4 --- /dev/null +++ b/src/utils/FileUtils.ts @@ -0,0 +1,53 @@ +/* +Copyright 2021 Šimon Brandner + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import filesize from 'filesize'; +import { IMediaEventContent } from '../customisations/models/IMediaEventContent'; +import { _t } from '../languageHandler'; + +/** + * Extracts a human readable label for the file attachment to use as + * link text. + * + * @param {IMediaEventContent} content The "content" key of the matrix event. + * @param {string} fallbackText The fallback text + * @param {boolean} withSize Whether to include size information. Default true. + * @return {string} the human readable link text for the attachment. + */ +export function presentableTextForFile( + content: IMediaEventContent, + fallbackText = _t("Attachment"), + withSize = true, +): string { + let text = fallbackText; + if (content.body && content.body.length > 0) { + // The content body should be the name of the file including a + // file extension. + text = content.body; + } + + if (content.info && content.info.size && withSize) { + // If we know the size of the file then add it as human readable + // string to the end of the link text so that the user knows how + // big a file they are downloading. + // The content.info also contains a MIME-type but we don't display + // it since it is "ugly", users generally aren't aware what it + // means and the type of the attachment can usually be inferrered + // from the file extension. + text += ' (' + filesize(content.info.size) + ')'; + } + return text; +} From 5d5b9f6022de6e2a049cd1c058e91462a8581894 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Jul 2021 18:04:33 +0200 Subject: [PATCH 18/67] Better labeling of images and stickers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/messages/MFileBody.js | 30 +------------------ src/components/views/messages/MImageBody.tsx | 2 +- .../views/messages/MImageReplyBody.tsx | 9 ++++-- 3 files changed, 8 insertions(+), 33 deletions(-) diff --git a/src/components/views/messages/MFileBody.js b/src/components/views/messages/MFileBody.js index 9236c77e8d..ea0dbe3f1e 100644 --- a/src/components/views/messages/MFileBody.js +++ b/src/components/views/messages/MFileBody.js @@ -25,6 +25,7 @@ import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromContent } from "../../../customisations/Media"; import ErrorDialog from "../dialogs/ErrorDialog"; import { TileShape } from "../rooms/EventTile"; +import { presentableTextForFile } from "../../../utils/FileUtils"; let downloadIconUrl; // cached copy of the download.svg asset for the sandboxed iframe later on @@ -90,35 +91,6 @@ function computedStyle(element) { return cssText; } -/** - * Extracts a human readable label for the file attachment to use as - * link text. - * - * @param {Object} content The "content" key of the matrix event. - * @param {boolean} withSize Whether to include size information. Default true. - * @return {string} the human readable link text for the attachment. - */ -export function presentableTextForFile(content, withSize = true) { - let linkText = _t("Attachment"); - if (content.body && content.body.length > 0) { - // The content body should be the name of the file including a - // file extension. - linkText = content.body; - } - - if (content.info && content.info.size && withSize) { - // If we know the size of the file then add it as human readable - // string to the end of the link text so that the user knows how - // big a file they are downloading. - // The content.info also contains a MIME-type but we don't display - // it since it is "ugly", users generally aren't aware what it - // means and the type of the attachment can usually be inferrered - // from the file extension. - linkText += ' (' + filesize(content.info.size) + ')'; - } - return linkText; -} - @replaceableComponent("views.messages.MFileBody") export default class MFileBody extends React.Component { static propTypes = { diff --git a/src/components/views/messages/MImageBody.tsx b/src/components/views/messages/MImageBody.tsx index 96c8652aee..a2c8663cd7 100644 --- a/src/components/views/messages/MImageBody.tsx +++ b/src/components/views/messages/MImageBody.tsx @@ -451,7 +451,7 @@ export default class MImageBody extends React.Component { } // Overidden by MStickerBody - protected getFileBody(): JSX.Element { + protected getFileBody(): string | JSX.Element { return ; } diff --git a/src/components/views/messages/MImageReplyBody.tsx b/src/components/views/messages/MImageReplyBody.tsx index 44acf18004..8d92920226 100644 --- a/src/components/views/messages/MImageReplyBody.tsx +++ b/src/components/views/messages/MImageReplyBody.tsx @@ -16,9 +16,11 @@ limitations under the License. import React from "react"; import MImageBody from "./MImageBody"; -import { presentableTextForFile } from "./MFileBody"; +import { presentableTextForFile } from "../../../utils/FileUtils"; import { IMediaEventContent } from "../../../customisations/models/IMediaEventContent"; import SenderProfile from "./SenderProfile"; +import { EventType } from "matrix-js-sdk/src/@types/event"; +import { _t } from "../../../languageHandler"; const FORCED_IMAGE_HEIGHT = 44; @@ -32,8 +34,9 @@ export default class MImageReplyBody extends MImageBody { } // Don't show "Download this_file.png ..." - public getFileBody(): JSX.Element { - return presentableTextForFile(this.props.mxEvent.getContent()); + public getFileBody(): string { + const sticker = this.props.mxEvent.getType() === EventType.Sticker; + return presentableTextForFile(this.props.mxEvent.getContent(), sticker ? _t("Sticker") : _t("Image"), !sticker); } render() { From 3250b35151969135fd189a0a02cd172e54ab8057 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Jul 2021 18:10:31 +0200 Subject: [PATCH 19/67] 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 | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 2790e17eed..ae97c0787f 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -671,6 +671,7 @@ "This homeserver has exceeded one of its resource limits.": "This homeserver has exceeded one of its resource limits.", "Please contact your service administrator to continue using the service.": "Please contact your service administrator to continue using the service.", "Unable to connect to Homeserver. Retrying...": "Unable to connect to Homeserver. Retrying...", + "Attachment": "Attachment", "%(items)s and %(count)s others|other": "%(items)s and %(count)s others", "%(items)s and %(count)s others|one": "%(items)s and one other", "%(items)s and %(lastItem)s": "%(items)s and %(lastItem)s", @@ -1873,13 +1874,14 @@ "Retry": "Retry", "Reply": "Reply", "Message Actions": "Message Actions", - "Attachment": "Attachment", "Error decrypting attachment": "Error decrypting attachment", "Decrypt %(text)s": "Decrypt %(text)s", "Download %(text)s": "Download %(text)s", "Invalid file%(extra)s": "Invalid file%(extra)s", "Error decrypting image": "Error decrypting image", "Show image": "Show image", + "Sticker": "Sticker", + "Image": "Image", "Join the conference at the top of this room": "Join the conference at the top of this room", "Join the conference from the room information card on the right": "Join the conference from the room information card on the right", "Video conference ended by %(senderName)s": "Video conference ended by %(senderName)s", From 1b74286cf03dd3efdfa1d8e64a364792f5ee4389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Mon, 19 Jul 2021 18:11:23 +0200 Subject: [PATCH 20/67] /me has managed to steel something by accident MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/utils/FileUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/utils/FileUtils.ts b/src/utils/FileUtils.ts index 0adcb172e4..355fa2135c 100644 --- a/src/utils/FileUtils.ts +++ b/src/utils/FileUtils.ts @@ -1,4 +1,5 @@ /* +Copyright 2015 - 2021 The Matrix.org Foundation C.I.C. Copyright 2021 Šimon Brandner Licensed under the Apache License, Version 2.0 (the "License"); From 8bbd91547d6fbbcb1aba8f30e067ded32ce83315 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Jul 2021 10:59:34 +0200 Subject: [PATCH 21/67] Convert ActionButton to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../{ActionButton.js => ActionButton.tsx} | 51 +++++++++++-------- 1 file changed, 29 insertions(+), 22 deletions(-) rename src/components/views/elements/{ActionButton.js => ActionButton.tsx} (75%) diff --git a/src/components/views/elements/ActionButton.js b/src/components/views/elements/ActionButton.tsx similarity index 75% rename from src/components/views/elements/ActionButton.js rename to src/components/views/elements/ActionButton.tsx index 9c9e9663e7..f776174722 100644 --- a/src/components/views/elements/ActionButton.js +++ b/src/components/views/elements/ActionButton.tsx @@ -15,49 +15,56 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import AccessibleButton from './AccessibleButton'; import dis from '../../../dispatcher/dispatcher'; import * as sdk from '../../../index'; import Analytics from '../../../Analytics'; import { replaceableComponent } from "../../../utils/replaceableComponent"; -@replaceableComponent("views.elements.ActionButton") -export default class ActionButton extends React.Component { - static propTypes = { - size: PropTypes.string, - tooltip: PropTypes.bool, - action: PropTypes.string.isRequired, - mouseOverAction: PropTypes.string, - label: PropTypes.string.isRequired, - iconPath: PropTypes.string, - className: PropTypes.string, - children: PropTypes.node, - }; +interface IProps { + size?: string; + tooltip?: boolean; + action: string; + mouseOverAction?: string; + label: string; + iconPath?: string; + className?: string; + children?: JSX.Element; +} +interface IState { + showTooltip: boolean; +} + +@replaceableComponent("views.elements.ActionButton") +export default class ActionButton extends React.Component { static defaultProps = { size: "25", tooltip: false, }; - state = { - showTooltip: false, - }; + constructor(props: IProps) { + super(props); - _onClick = (ev) => { + this.state = { + showTooltip: false, + }; + } + + private onClick = (ev: React.MouseEvent): void => { ev.stopPropagation(); Analytics.trackEvent('Action Button', 'click', this.props.action); dis.dispatch({ action: this.props.action }); }; - _onMouseEnter = () => { + private onMouseEnter = (): void => { if (this.props.tooltip) this.setState({ showTooltip: true }); if (this.props.mouseOverAction) { dis.dispatch({ action: this.props.mouseOverAction }); } }; - _onMouseLeave = () => { + private onMouseLeave = (): void => { this.setState({ showTooltip: false }); }; @@ -80,9 +87,9 @@ export default class ActionButton extends React.Component { return ( { icon } From 28871ee07d608e26c799a7f723b553dda350b987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Jul 2021 11:04:29 +0200 Subject: [PATCH 22/67] Convert AddressSelector to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...AddressSelector.js => AddressSelector.tsx} | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) rename src/components/views/elements/{AddressSelector.js => AddressSelector.tsx} (70%) diff --git a/src/components/views/elements/AddressSelector.js b/src/components/views/elements/AddressSelector.tsx similarity index 70% rename from src/components/views/elements/AddressSelector.js rename to src/components/views/elements/AddressSelector.tsx index b7c9124438..c55f6f22d2 100644 --- a/src/components/views/elements/AddressSelector.js +++ b/src/components/views/elements/AddressSelector.tsx @@ -15,30 +15,36 @@ See the License for the specific language governing permissions and limitations under the License. */ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { createRef } from 'react'; import * as sdk from '../../../index'; import classNames from 'classnames'; -import { UserAddressType } from '../../../UserAddress'; import { replaceableComponent } from "../../../utils/replaceableComponent"; +interface IProps { + onSelected: (index: number) => void; + + // List of the addresses to display + addressList; // FIXME: UserAddressType should be an interface + // Whether to show the address on the address tiles + showAddress?: boolean; + truncateAt: number; + selected?: number; + + // Element to put as a header on top of the list + header?: JSX.Element; +} + +interface IState { + selected: number; + hover: boolean; +} + @replaceableComponent("views.elements.AddressSelector") -export default class AddressSelector extends React.Component { - static propTypes = { - onSelected: PropTypes.func.isRequired, +export default class AddressSelector extends React.Component { + private scrollElement = createRef(); + private addressListElement = createRef(); - // List of the addresses to display - addressList: PropTypes.arrayOf(UserAddressType).isRequired, - // Whether to show the address on the address tiles - showAddress: PropTypes.bool, - truncateAt: PropTypes.number.isRequired, - selected: PropTypes.number, - - // Element to put as a header on top of the list - header: PropTypes.node, - }; - - constructor(props) { + constructor(props: IProps) { super(props); this.state = { @@ -48,10 +54,10 @@ export default class AddressSelector extends React.Component { } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event - UNSAFE_componentWillReceiveProps(props) { // eslint-disable-line camelcase + UNSAFE_componentWillReceiveProps(props: IProps) { // eslint-disable-line // Make sure the selected item isn't outside the list bounds const selected = this.state.selected; - const maxSelected = this._maxSelected(props.addressList); + const maxSelected = this.maxSelected(props.addressList); if (selected > maxSelected) { this.setState({ selected: maxSelected }); } @@ -60,13 +66,13 @@ export default class AddressSelector extends React.Component { componentDidUpdate() { // As the user scrolls with the arrow keys keep the selected item // at the top of the window. - if (this.scrollElement && this.props.addressList.length > 0 && !this.state.hover) { - const elementHeight = this.addressListElement.getBoundingClientRect().height; - this.scrollElement.scrollTop = (this.state.selected * elementHeight) - elementHeight; + if (this.scrollElement.current && this.props.addressList.length > 0 && !this.state.hover) { + const elementHeight = this.addressListElement.current.getBoundingClientRect().height; + this.scrollElement.current.scrollTop = (this.state.selected * elementHeight) - elementHeight; } } - moveSelectionTop = () => { + public moveSelectionTop = (): void => { if (this.state.selected > 0) { this.setState({ selected: 0, @@ -75,7 +81,7 @@ export default class AddressSelector extends React.Component { } }; - moveSelectionUp = () => { + public moveSelectionUp = (): void => { if (this.state.selected > 0) { this.setState({ selected: this.state.selected - 1, @@ -84,8 +90,8 @@ export default class AddressSelector extends React.Component { } }; - moveSelectionDown = () => { - if (this.state.selected < this._maxSelected(this.props.addressList)) { + public moveSelectionDown = (): void => { + if (this.state.selected < this.maxSelected(this.props.addressList)) { this.setState({ selected: this.state.selected + 1, hover: false, @@ -93,26 +99,26 @@ export default class AddressSelector extends React.Component { } }; - chooseSelection = () => { + public chooseSelection = (): void => { this.selectAddress(this.state.selected); }; - onClick = index => { + private onClick = (index: number): void => { this.selectAddress(index); }; - onMouseEnter = index => { + private onMouseEnter = (index: number): void => { this.setState({ selected: index, hover: true, }); }; - onMouseLeave = () => { + private onMouseLeave = (): void => { this.setState({ hover: false }); }; - selectAddress = index => { + private selectAddress = (index: number): void => { // Only try to select an address if one exists if (this.props.addressList.length !== 0) { this.props.onSelected(index); @@ -120,9 +126,9 @@ export default class AddressSelector extends React.Component { } }; - createAddressListTiles() { + private createAddressListTiles(): JSX.Element[] { const AddressTile = sdk.getComponent("elements.AddressTile"); - const maxSelected = this._maxSelected(this.props.addressList); + const maxSelected = this.maxSelected(this.props.addressList); const addressList = []; // Only create the address elements if there are address @@ -143,7 +149,7 @@ export default class AddressSelector extends React.Component { onMouseEnter={this.onMouseEnter.bind(this, i)} onMouseLeave={this.onMouseLeave} key={this.props.addressList[i].addressType + "/" + this.props.addressList[i].address} - ref={(ref) => { this.addressListElement = ref; }} + ref={this.addressListElement} > (this.props.truncateAt - 1) ? (this.props.truncateAt - 1) : listSize; return maxSelected; @@ -172,7 +178,7 @@ export default class AddressSelector extends React.Component { }); return ( -
{this.scrollElement = ref;}}> +
{ this.props.header } { this.createAddressListTiles() }
From 156901ce62aaf7a836248060919c42c02b7a1d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Jul 2021 11:19:29 +0200 Subject: [PATCH 23/67] Convert AddressTile to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .../{AddressTile.js => AddressTile.tsx} | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) rename src/components/views/elements/{AddressTile.js => AddressTile.tsx} (89%) diff --git a/src/components/views/elements/AddressTile.js b/src/components/views/elements/AddressTile.tsx similarity index 89% rename from src/components/views/elements/AddressTile.js rename to src/components/views/elements/AddressTile.tsx index ca85d73a11..9f147b6bd6 100644 --- a/src/components/views/elements/AddressTile.js +++ b/src/components/views/elements/AddressTile.tsx @@ -16,23 +16,22 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import * as sdk from "../../../index"; import { _t } from '../../../languageHandler'; -import { UserAddressType } from '../../../UserAddress'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { mediaFromMxc } from "../../../customisations/Media"; -@replaceableComponent("views.elements.AddressTile") -export default class AddressTile extends React.Component { - static propTypes = { - address: UserAddressType.isRequired, - canDismiss: PropTypes.bool, - onDismissed: PropTypes.func, - justified: PropTypes.bool, - }; +interface IProps { + address; // FIXME: UserAddressType should be an interface + canDismiss?: boolean; + onDismissed?: () => void; + justified?: boolean; + showAddress?: boolean; +} +@replaceableComponent("views.elements.AddressTile") +export default class AddressTile extends React.Component { static defaultProps = { canDismiss: false, onDismissed: function() {}, // NOP @@ -70,9 +69,10 @@ export default class AddressTile extends React.Component { info = (
{ name }
- { this.props.showAddress ? -
{ address.address }
: -
+ { + this.props.showAddress + ?
{ address.address }
+ :
}
); From a747bbbae7b753fa379935370db6235e05a5608b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Tue, 20 Jul 2021 11:26:33 +0200 Subject: [PATCH 24/67] Convert AddressPickerDialog to TS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- ...ickerDialog.js => AddressPickerDialog.tsx} | 215 +++++++++--------- 1 file changed, 112 insertions(+), 103 deletions(-) rename src/components/views/dialogs/{AddressPickerDialog.js => AddressPickerDialog.tsx} (80%) diff --git a/src/components/views/dialogs/AddressPickerDialog.js b/src/components/views/dialogs/AddressPickerDialog.tsx similarity index 80% rename from src/components/views/dialogs/AddressPickerDialog.js rename to src/components/views/dialogs/AddressPickerDialog.tsx index 09714e24e3..1802f8ced2 100644 --- a/src/components/views/dialogs/AddressPickerDialog.js +++ b/src/components/views/dialogs/AddressPickerDialog.tsx @@ -18,7 +18,6 @@ limitations under the License. */ import React, { createRef } from 'react'; -import PropTypes from 'prop-types'; import { sleep } from "matrix-js-sdk/src/utils"; import { _t, _td } from '../../../languageHandler'; @@ -34,6 +33,7 @@ import { abbreviateUrl } from '../../../utils/UrlUtils'; import { Key } from "../../../Keyboard"; import { Action } from "../../../dispatcher/actions"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import AddressSelector from '../elements/AddressSelector'; const TRUNCATE_QUERY_LIST = 40; const QUERY_USER_DIRECTORY_DEBOUNCE_MS = 200; @@ -44,27 +44,54 @@ const addressTypeName = { 'email': _td("email address"), }; +interface IProps { + title: string; + description?: JSX.Element; + // Extra node inserted after picker input, dropdown and errors + extraNode?: JSX.Element; + value?: string; + placeholder?: ((validAddressTypes: any) => string) | string; + roomId?: string; + button?: string; + focus?: boolean; + validAddressTypes?; // FIXME: UserAddressType should be an interface + onFinished: (success: boolean, list?) => void; // FIXME: UserAddressType should be an interface + groupId?: string; + // The type of entity to search for. Default: 'user'. + pickerType?: 'user' | 'room'; + // Whether the current user should be included in the addresses returned. Only + // applicable when pickerType is `user`. Default: false. + includeSelf?: boolean; +} + +interface IState { + // Whether to show an error message because of an invalid address + invalidAddressError: boolean; + // List of UserAddressType objects representing + // the list of addresses we're going to invite + selectedList; // FIXME: UserAddressType should be an interface + // Whether a search is ongoing + busy: boolean; + // An error message generated during the user directory search + searchError: string; + // Whether the server supports the user_directory API + serverSupportsUserDirectory: boolean; + // The query being searched for + query: string; + // List of UserAddressType objects representing the set of + // auto-completion results for the current search query. + suggestedList; // FIXME: UserAddressType should be an interface + // List of address types initialised from props, but may change while the + // dialog is open and represents the supported list of address types at this time. + validAddressTypes; +} + @replaceableComponent("views.dialogs.AddressPickerDialog") -export default class AddressPickerDialog extends React.Component { - static propTypes = { - title: PropTypes.string.isRequired, - description: PropTypes.node, - // Extra node inserted after picker input, dropdown and errors - extraNode: PropTypes.node, - value: PropTypes.string, - placeholder: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), - roomId: PropTypes.string, - button: PropTypes.string, - focus: PropTypes.bool, - validAddressTypes: PropTypes.arrayOf(PropTypes.oneOf(addressTypes)), - onFinished: PropTypes.func.isRequired, - groupId: PropTypes.string, - // The type of entity to search for. Default: 'user'. - pickerType: PropTypes.oneOf(['user', 'room']), - // Whether the current user should be included in the addresses returned. Only - // applicable when pickerType is `user`. Default: false. - includeSelf: PropTypes.bool, - }; +export default class AddressPickerDialog extends React.Component { + private textinput = createRef(); + private addressSelector = createRef(); + private queryChangedDebouncer: NodeJS.Timeout; + private cancelThreepidLookup: () => void; static defaultProps = { value: "", @@ -74,11 +101,9 @@ export default class AddressPickerDialog extends React.Component { includeSelf: false, }; - constructor(props) { + constructor(props: IProps) { super(props); - this._textinput = createRef(); - let validAddressTypes = this.props.validAddressTypes; // Remove email from validAddressTypes if no IS is configured. It may be added at a later stage by the user if (!MatrixClientPeg.get().getIdentityServerUrl() && validAddressTypes.includes("email")) { @@ -86,24 +111,13 @@ export default class AddressPickerDialog extends React.Component { } this.state = { - // Whether to show an error message because of an invalid address invalidAddressError: false, - // List of UserAddressType objects representing - // the list of addresses we're going to invite selectedList: [], - // Whether a search is ongoing busy: false, - // An error message generated during the user directory search searchError: null, - // Whether the server supports the user_directory API serverSupportsUserDirectory: true, - // The query being searched for query: "", - // List of UserAddressType objects representing the set of - // auto-completion results for the current search query. suggestedList: [], - // List of address types initialised from props, but may change while the - // dialog is open and represents the supported list of address types at this time. validAddressTypes, }; } @@ -111,11 +125,11 @@ export default class AddressPickerDialog extends React.Component { componentDidMount() { if (this.props.focus) { // Set the cursor at the end of the text input - this._textinput.current.value = this.props.value; + this.textinput.current.value = this.props.value; } } - getPlaceholder() { + private getPlaceholder(): string { const { placeholder } = this.props; if (typeof placeholder === "string") { return placeholder; @@ -124,23 +138,23 @@ export default class AddressPickerDialog extends React.Component { return placeholder(this.state.validAddressTypes); } - onButtonClick = () => { + private onButtonClick = (): void => { let selectedList = this.state.selectedList.slice(); // Check the text input field to see if user has an unconverted address // If there is and it's valid add it to the local selectedList - if (this._textinput.current.value !== '') { - selectedList = this._addAddressesToList([this._textinput.current.value]); + if (this.textinput.current.value !== '') { + selectedList = this.addAddressesToList([this.textinput.current.value]); if (selectedList === null) return; } this.props.onFinished(true, selectedList); }; - onCancel = () => { + private onCancel = (): void => { this.props.onFinished(false); }; - onKeyDown = e => { - const textInput = this._textinput.current ? this._textinput.current.value : undefined; + private onKeyDown = (e: React.KeyboardEvent): void => { + const textInput = this.textinput.current ? this.textinput.current.value : undefined; if (e.key === Key.ESCAPE) { e.stopPropagation(); @@ -149,15 +163,15 @@ export default class AddressPickerDialog extends React.Component { } else if (e.key === Key.ARROW_UP) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.moveSelectionUp(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionUp(); } else if (e.key === Key.ARROW_DOWN) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.moveSelectionDown(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionDown(); } else if (this.state.suggestedList.length > 0 && [Key.COMMA, Key.ENTER, Key.TAB].includes(e.key)) { e.stopPropagation(); e.preventDefault(); - if (this.addressSelector) this.addressSelector.chooseSelection(); + if (this.addressSelector.current) this.addressSelector.current.chooseSelection(); } else if (textInput.length === 0 && this.state.selectedList.length && e.key === Key.BACKSPACE) { e.stopPropagation(); e.preventDefault(); @@ -169,17 +183,17 @@ export default class AddressPickerDialog extends React.Component { // if there's nothing in the input box, submit the form this.onButtonClick(); } else { - this._addAddressesToList([textInput]); + this.addAddressesToList([textInput]); } } else if (textInput && (e.key === Key.COMMA || e.key === Key.TAB)) { e.stopPropagation(); e.preventDefault(); - this._addAddressesToList([textInput]); + this.addAddressesToList([textInput]); } }; - onQueryChanged = ev => { - const query = ev.target.value; + private onQueryChanged = (ev: React.ChangeEvent): void => { + const query = (ev.target as HTMLTextAreaElement).value; if (this.queryChangedDebouncer) { clearTimeout(this.queryChangedDebouncer); } @@ -188,17 +202,17 @@ export default class AddressPickerDialog extends React.Component { this.queryChangedDebouncer = setTimeout(() => { if (this.props.pickerType === 'user') { if (this.props.groupId) { - this._doNaiveGroupSearch(query); + this.doNaiveGroupSearch(query); } else if (this.state.serverSupportsUserDirectory) { - this._doUserDirectorySearch(query); + this.doUserDirectorySearch(query); } else { - this._doLocalSearch(query); + this.doLocalSearch(query); } } else if (this.props.pickerType === 'room') { if (this.props.groupId) { - this._doNaiveGroupRoomSearch(query); + this.doNaiveGroupRoomSearch(query); } else { - this._doRoomSearch(query); + this.doRoomSearch(query); } } else { console.error('Unknown pickerType', this.props.pickerType); @@ -213,7 +227,7 @@ export default class AddressPickerDialog extends React.Component { } }; - onDismissed = index => () => { + private onDismissed = (index: number) => () => { const selectedList = this.state.selectedList.slice(); selectedList.splice(index, 1); this.setState({ @@ -221,25 +235,21 @@ export default class AddressPickerDialog extends React.Component { suggestedList: [], query: "", }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); }; - onClick = index => () => { - this.onSelected(index); - }; - - onSelected = index => { + private onSelected = (index: number): void => { const selectedList = this.state.selectedList.slice(); - selectedList.push(this._getFilteredSuggestions()[index]); + selectedList.push(this.getFilteredSuggestions()[index]); this.setState({ selectedList, suggestedList: [], query: "", }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); }; - _doNaiveGroupSearch(query) { + private doNaiveGroupSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); this.setState({ busy: true, @@ -260,7 +270,7 @@ export default class AddressPickerDialog extends React.Component { display_name: u.displayname, }); }); - this._processResults(results, query); + this.processResults(results, query); }).catch((err) => { console.error('Error whilst searching group rooms: ', err); this.setState({ @@ -273,7 +283,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doNaiveGroupRoomSearch(query) { + private doNaiveGroupRoomSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); const results = []; GroupStore.getGroupRooms(this.props.groupId).forEach((r) => { @@ -289,13 +299,13 @@ export default class AddressPickerDialog extends React.Component { name: r.name || r.canonical_alias, }); }); - this._processResults(results, query); + this.processResults(results, query); this.setState({ busy: false, }); } - _doRoomSearch(query) { + private doRoomSearch(query: string): void { const lowerCaseQuery = query.toLowerCase(); const rooms = MatrixClientPeg.get().getRooms(); const results = []; @@ -346,13 +356,13 @@ export default class AddressPickerDialog extends React.Component { return a.rank - b.rank; }); - this._processResults(sortedResults, query); + this.processResults(sortedResults, query); this.setState({ busy: false, }); } - _doUserDirectorySearch(query) { + private doUserDirectorySearch(query: string): void { this.setState({ busy: true, query, @@ -366,7 +376,7 @@ export default class AddressPickerDialog extends React.Component { if (this.state.query !== query) { return; } - this._processResults(resp.results, query); + this.processResults(resp.results, query); }).catch((err) => { console.error('Error whilst searching user directory: ', err); this.setState({ @@ -377,7 +387,7 @@ export default class AddressPickerDialog extends React.Component { serverSupportsUserDirectory: false, }); // Do a local search immediately - this._doLocalSearch(query); + this.doLocalSearch(query); } }).then(() => { this.setState({ @@ -386,7 +396,7 @@ export default class AddressPickerDialog extends React.Component { }); } - _doLocalSearch(query) { + private doLocalSearch(query: string): void { this.setState({ query, searchError: null, @@ -407,10 +417,10 @@ export default class AddressPickerDialog extends React.Component { avatar_url: user.avatarUrl, }); }); - this._processResults(results, query); + this.processResults(results, query); } - _processResults(results, query) { + private processResults(results, query: string): void { const suggestedList = []; results.forEach((result) => { if (result.room_id) { @@ -465,20 +475,20 @@ export default class AddressPickerDialog extends React.Component { address: query, isKnown: false, }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); if (addrType === 'email') { - this._lookupThreepid(addrType, query); + this.lookupThreepid(addrType, query); } } this.setState({ suggestedList, invalidAddressError: false, }, () => { - if (this.addressSelector) this.addressSelector.moveSelectionTop(); + if (this.addressSelector.current) this.addressSelector.current.moveSelectionTop(); }); } - _addAddressesToList(addressTexts) { + private addAddressesToList(addressTexts): void { const selectedList = this.state.selectedList.slice(); let hasError = false; @@ -518,17 +528,17 @@ export default class AddressPickerDialog extends React.Component { query: "", invalidAddressError: hasError ? true : this.state.invalidAddressError, }); - if (this._cancelThreepidLookup) this._cancelThreepidLookup(); + if (this.cancelThreepidLookup) this.cancelThreepidLookup(); return hasError ? null : selectedList; } - async _lookupThreepid(medium, address) { + private async lookupThreepid(medium, address): Promise { let cancelled = false; // Note that we can't safely remove this after we're done // because we don't know that it's the same one, so we just // leave it: it's replacing the old one each time so it's // not like they leak. - this._cancelThreepidLookup = function() { + this.cancelThreepidLookup = function() { cancelled = true; }; @@ -570,7 +580,7 @@ export default class AddressPickerDialog extends React.Component { } } - _getFilteredSuggestions() { + private getFilteredSuggestions() { // FIXME: UserAddressType should be an interface // map addressType => set of addresses to avoid O(n*m) operation const selectedAddresses = {}; this.state.selectedList.forEach(({ address, addressType }) => { @@ -584,15 +594,15 @@ export default class AddressPickerDialog extends React.Component { }); } - _onPaste = e => { + private onPaste = (e: React.ClipboardEvent): void => { // Prevent the text being pasted into the textarea e.preventDefault(); const text = e.clipboardData.getData("text"); // Process it as a list of addresses to add instead - this._addAddressesToList(text.split(/[\s,]+/)); + this.addAddressesToList(text.split(/[\s,]+/)); }; - onUseDefaultIdentityServerClick = e => { + private onUseDefaultIdentityServerClick = (e: React.MouseEvent): void => { e.preventDefault(); // Update the IS in account data. Actually using it may trigger terms. @@ -605,7 +615,7 @@ export default class AddressPickerDialog extends React.Component { this.setState({ validAddressTypes }); }; - onManageSettingsClick = e => { + private onManageSettingsClick = (e: React.MouseEvent): void => { e.preventDefault(); dis.fire(Action.ViewUserSettings); this.onCancel(); @@ -615,12 +625,11 @@ export default class AddressPickerDialog extends React.Component { const BaseDialog = sdk.getComponent('views.dialogs.BaseDialog'); const DialogButtons = sdk.getComponent('views.elements.DialogButtons'); const AddressSelector = sdk.getComponent("elements.AddressSelector"); - this.scrollElement = null; let inputLabel; if (this.props.description) { inputLabel =
- +
; } @@ -644,10 +653,10 @@ export default class AddressPickerDialog extends React.Component { query.push(