From 6868478044e8eb4eccfa15b279d9193499907891 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 8 Jul 2021 11:30:56 +0200 Subject: [PATCH 01/50] Add threaded messaging feature flag --- src/i18n/strings/en_EN.json | 1 + src/settings/Settings.tsx | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index bbf6954435..eb82c1b533 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -808,6 +808,7 @@ "Render LaTeX maths in messages": "Render LaTeX maths in messages", "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.": "Communities v2 prototypes. Requires compatible homeserver. Highly experimental - use with caution.", "Message Pinning": "Message Pinning", + "Threaded messaging": "Threaded messaging", "Custom user status messages": "Custom user status messages", "Group & filter rooms by custom tags (refresh to apply changes)": "Group & filter rooms by custom tags (refresh to apply changes)", "Render simple counters in room header": "Render simple counters in room header", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 1751eddb2c..2224d1bbd5 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -239,6 +239,12 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, + "feature_threading": { + isFeature: true, + displayName: _td("Threaded messaging"), + supportedLevels: LEVELS_FEATURE, + default: false, + }, "feature_custom_status": { isFeature: true, displayName: _td("Custom user status messages"), From cf3117bd57ee298fa84cc0b0dea6eba7804abc4a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 8 Jul 2021 11:55:31 +0200 Subject: [PATCH 02/50] Migrate ViewSourceEvent to TypeScript --- ...ViewSourceEvent.js => ViewSourceEvent.tsx} | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) rename src/components/views/messages/{ViewSourceEvent.js => ViewSourceEvent.tsx} (85%) diff --git a/src/components/views/messages/ViewSourceEvent.js b/src/components/views/messages/ViewSourceEvent.tsx similarity index 85% rename from src/components/views/messages/ViewSourceEvent.js rename to src/components/views/messages/ViewSourceEvent.tsx index 62454fef1a..d8b558c4a1 100644 --- a/src/components/views/messages/ViewSourceEvent.js +++ b/src/components/views/messages/ViewSourceEvent.tsx @@ -15,27 +15,29 @@ limitations under the License. */ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; +import { MatrixEvent } from '../../../../../matrix-js-sdk/src'; + +interface IProps { + mxEvent: MatrixEvent; +} + +interface IState { + expanded: boolean; +} @replaceableComponent("views.messages.ViewSourceEvent") -export default class ViewSourceEvent extends React.PureComponent { - static propTypes = { - /* the MatrixEvent to show */ - mxEvent: PropTypes.object.isRequired, - }; - - constructor(props) { +export default class ViewSourceEvent extends React.PureComponent { + constructor(props: IProps) { super(props); - this.state = { expanded: false, }; } - componentDidMount() { + public componentDidMount(): void { const { mxEvent } = this.props; const client = MatrixClientPeg.get(); @@ -46,15 +48,15 @@ export default class ViewSourceEvent extends React.PureComponent { } } - onToggle = (ev) => { + private onToggle = (ev: React.MouseEvent): void => { ev.preventDefault(); const { expanded } = this.state; this.setState({ expanded: !expanded, }); - } + }; - render() { + public render(): React.ReactNode { const { mxEvent } = this.props; const { expanded } = this.state; From d9718027892e0932ebf84ab10652c5d616e79f7b Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 10 Aug 2021 14:30:12 +0200 Subject: [PATCH 03/50] Create ThreadView phase in RightPanel --- res/css/views/messages/_MessageActionBar.scss | 4 + src/components/structures/RightPanel.tsx | 9 ++ src/components/structures/ThreadView.tsx | 90 +++++++++++++++++++ .../views/messages/MessageActionBar.js | 33 +++++-- src/i18n/strings/en_EN.json | 1 + src/stores/RightPanelStorePhases.ts | 3 + 6 files changed, 134 insertions(+), 6 deletions(-) create mode 100644 src/components/structures/ThreadView.tsx diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 69f3c672b7..509ded8ee8 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -92,6 +92,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg'); } +.mx_MessageActionBar_threadButton::after { + mask-image: url('$(res)/img/element-icons/room/files.svg'); +} + .mx_MessageActionBar_editButton::after { mask-image: url('$(res)/img/element-icons/room/message-bar/edit.svg'); } diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 95d70e913a..f2bd7db0f3 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -45,6 +45,7 @@ import GroupRoomInfo from "../views/groups/GroupRoomInfo"; import UserInfo from "../views/right_panel/UserInfo"; import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; import FilePanel from "./FilePanel"; +import ThreadView from "./ThreadView"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; @@ -309,6 +310,14 @@ export default class RightPanel extends React.Component { panel = ; break; + case RightPanelPhases.ThreadView: + panel = ; + break; + case RightPanelPhases.RoomSummary: panel = ; break; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx new file mode 100644 index 0000000000..8b196cb4f0 --- /dev/null +++ b/src/components/structures/ThreadView.tsx @@ -0,0 +1,90 @@ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { MatrixEvent } from 'matrix-js-sdk/src'; + +import BaseCard from "../views/right_panel/BaseCard"; +import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import { MatrixClientPeg } from '../../MatrixClientPeg'; + +import ResizeNotifier from '../../utils/ResizeNotifier'; +import EventTile from '../views/rooms/EventTile'; +import MessageComposer from '../views/rooms/MessageComposer'; + +interface IProps { + roomId: string; + onClose: () => void; + resizeNotifier: ResizeNotifier; + mxEvent: MatrixEvent; +} + +interface IState { +} + +/* + * Component which shows the filtered file using a TimelinePanel + */ +@replaceableComponent("structures.ThreadView") +class ThreadView extends React.Component { + state = {}; + + public componentDidMount(): void {} + + public componentWillUnmount(): void {} + + public renderEventTile(event: MatrixEvent): JSX.Element { + return ; + } + + public render() { + const client = MatrixClientPeg.get(); + const room = client.getRoom(this.props.roomId); + const thread = room.getThread(this.props.mxEvent.getId()); + return ( + + { this.renderEventTile(this.props.mxEvent) } + + { thread && ( + thread.eventTimeline.map((event: MatrixEvent) => { + return this.renderEventTile(event); + }) + ) } + + + + ); + } +} + +export default ThreadView; diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 7fe0eca697..25f3dc740f 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -23,6 +23,8 @@ import { EventStatus } from 'matrix-js-sdk/src/models/event'; import { _t } from '../../../languageHandler'; import * as sdk from '../../../index'; import dis from '../../../dispatcher/dispatcher'; +import { Action } from '../../../dispatcher/actions'; +import { RightPanelPhases } from '../../../stores/RightPanelStorePhases'; import { aboveLeftOf, ContextMenu, ContextMenuTooltipButton, useContextMenu } from '../../structures/ContextMenu'; import { isContentActionable, canEditContent } from '../../../utils/EventUtils'; import RoomContext from "../../../contexts/RoomContext"; @@ -170,6 +172,17 @@ export default class MessageActionBar extends React.PureComponent { }); }; + onThreadClick = () => { + dis.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.ThreadView, + allowClose: false, + refireParams: { + event: this.props.mxEvent, + } + }); + } + onEditClick = (ev) => { dis.dispatch({ action: 'edit_event', @@ -254,12 +267,20 @@ export default class MessageActionBar extends React.PureComponent { // The only catch is we do the reply button first so that we can make sure the react // button is the very first button without having to do length checks for `splice()`. if (this.context.canReply) { - toolbarOpts.splice(0, 0, ); + toolbarOpts.splice(0, 0, <> + + + ); } if (this.context.canReact) { toolbarOpts.splice(0, 0, Date: Tue, 17 Aug 2021 10:38:09 +0100 Subject: [PATCH 04/50] Adapt threading UI to new backend --- src/components/structures/RightPanel.tsx | 13 ++- src/components/structures/RoomView.tsx | 5 +- src/components/structures/ThreadPanel.tsx | 106 ++++++++++++++++++ src/components/structures/ThreadView.tsx | 22 +++- .../views/right_panel/RoomSummaryCard.tsx | 10 ++ src/i18n/strings/en_EN.json | 1 + src/stores/RightPanelStorePhases.ts | 1 + 7 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 src/components/structures/ThreadPanel.tsx diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index f2bd7db0f3..85d3013a58 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -46,17 +46,20 @@ import UserInfo from "../views/right_panel/UserInfo"; import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; import FilePanel from "./FilePanel"; import ThreadView from "./ThreadView"; +import ThreadPanel from "./ThreadPanel"; import NotificationPanel from "./NotificationPanel"; import ResizeNotifier from "../../utils/ResizeNotifier"; import PinnedMessagesCard from "../views/right_panel/PinnedMessagesCard"; import { throttle } from 'lodash'; import SpaceStore from "../../stores/SpaceStore"; +import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; interface IProps { room?: Room; // if showing panels for a given room, this is set groupId?: string; // if showing panels for a given group, this is set user?: User; // used if we know the user ahead of opening the panel resizeNotifier: ResizeNotifier; + permalinkCreator?: RoomPermalinkCreator; } interface IState { @@ -315,7 +318,15 @@ export default class RightPanel extends React.Component { roomId={roomId} resizeNotifier={this.props.resizeNotifier} onClose={this.onClose} - mxEvent={this.state.event} />; + mxEvent={this.state.event} + permalinkCreator={this.props.permalinkCreator} />; + break; + + case RightPanelPhases.ThreadPanel: + panel = ; break; case RightPanelPhases.RoomSummary: diff --git a/src/components/structures/RoomView.tsx b/src/components/structures/RoomView.tsx index 474b99262d..baf557ee18 100644 --- a/src/components/structures/RoomView.tsx +++ b/src/components/structures/RoomView.tsx @@ -2052,7 +2052,10 @@ export default class RoomView extends React.Component { const showRightPanel = this.state.room && this.state.showRightPanel; const rightPanel = showRightPanel - ? + ? : null; const timelineClasses = classNames("mx_RoomView_timeline", { diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx new file mode 100644 index 0000000000..c99246ccda --- /dev/null +++ b/src/components/structures/ThreadPanel.tsx @@ -0,0 +1,106 @@ +/* +Copyright 2016 OpenMarket Ltd +Copyright 2019 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import React from 'react'; +import { MatrixEvent, Room } from 'matrix-js-sdk/src'; + +import BaseCard from "../views/right_panel/BaseCard"; +import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; +import { replaceableComponent } from "../../utils/replaceableComponent"; +import { MatrixClientPeg } from '../../MatrixClientPeg'; + +import ResizeNotifier from '../../utils/ResizeNotifier'; +import EventTile from '../views/rooms/EventTile'; +import { Thread } from '../../../../matrix-js-sdk/src/models/thread'; + +interface IProps { + roomId: string; + onClose: () => void; + resizeNotifier: ResizeNotifier; +} + +interface IState { + threads?: Thread[]; +} + +/* + * Component which shows the filtered file using a TimelinePanel + */ +@replaceableComponent("structures.ThreadView") +class ThreadView extends React.Component { + private room: Room; + + constructor(props: IProps) { + super(props); + this.room = MatrixClientPeg.get().getRoom(this.props.roomId); + } + + public componentDidMount(): void { + this.room.on("Thread.update", this.onThreadEventReceived); + this.room.on("Thread.ready", this.onThreadEventReceived); + this.updateThreads(() => { + this.state.threads.forEach(thread => { + if (!thread.ready) { + thread.fetchReplyChain(); + } + }); + }); + } + + public componentWillUnmount(): void { + this.room.removeListener("Thread.update", this.onThreadEventReceived); + this.room.removeListener("Thread.ready", this.onThreadEventReceived); + } + + public onThreadEventReceived = () => this.updateThreads(); + + public updateThreads = (callback?: () => void): void => { + this.setState({ + threads: this.room.getThreads(), + }, callback); + }; + + public renderEventTile(event: MatrixEvent): JSX.Element { + return ; + } + + public render() { + return ( + + { + this.state?.threads.map((thread: Thread) => { + if (thread.ready) { + return this.renderEventTile(thread.rootEvent); + } + }) + } + + ); + } +} + +export default ThreadView; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 8b196cb4f0..0f9d499884 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -26,12 +26,14 @@ import { MatrixClientPeg } from '../../MatrixClientPeg'; import ResizeNotifier from '../../utils/ResizeNotifier'; import EventTile from '../views/rooms/EventTile'; import MessageComposer from '../views/rooms/MessageComposer'; +import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; interface IProps { roomId: string; onClose: () => void; resizeNotifier: ResizeNotifier; mxEvent: MatrixEvent; + permalinkCreator?: RoomPermalinkCreator; } interface IState { @@ -44,9 +46,18 @@ interface IState { class ThreadView extends React.Component { state = {}; - public componentDidMount(): void {} + public componentDidMount(): void { + // this.props.mxEvent.getThread().on("Thread.update", this.updateThread); + this.props.mxEvent.getThread().once("Thread.ready", this.updateThread); + } - public componentWillUnmount(): void {} + public componentWillUnmount(): void { + this.props.mxEvent.getThread().removeListener("Thread.update", this.updateThread); + } + + updateThread = () => { + this.forceUpdate(); + }; public renderEventTile(event: MatrixEvent): JSX.Element { return { } public render() { - const client = MatrixClientPeg.get(); - const room = client.getRoom(this.props.roomId); - const thread = room.getThread(this.props.mxEvent.getId()); + const thread = this.props.mxEvent.getThread(); + const room = MatrixClientPeg.get().getRoom(this.props.roomId); return ( { room={room} resizeNotifier={this.props.resizeNotifier} replyToEvent={this.props.mxEvent} - permalinkCreator={null} + permalinkCreator={this.props.permalinkCreator} /> ); diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index 047448d925..eb3d7499f4 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -220,6 +220,13 @@ const onRoomFilesClick = () => { }); }; +const onRoomThreadsClick = () => { + defaultDispatcher.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.ThreadPanel, + }); +}; + const onRoomSettingsClick = () => { defaultDispatcher.dispatch({ action: "open_room_settings" }); }; @@ -273,6 +280,9 @@ const RoomSummaryCard: React.FC = ({ room, onClose }) => { + diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index ca55f45619..ec28be664b 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1790,6 +1790,7 @@ "%(count)s people|other": "%(count)s people", "%(count)s people|one": "%(count)s person", "Show files": "Show files", + "Show threads": "Show threads", "Share room": "Share room", "Room settings": "Room settings", "Trusted": "Trusted", diff --git a/src/stores/RightPanelStorePhases.ts b/src/stores/RightPanelStorePhases.ts index 8f41b17e47..96a585b676 100644 --- a/src/stores/RightPanelStorePhases.ts +++ b/src/stores/RightPanelStorePhases.ts @@ -40,6 +40,7 @@ export enum RightPanelPhases { // Thread stuff ThreadView = "ThreadView", + ThreadPanel = "ThreadPanel", } // These are the phases that are safe to persist (the ones that don't require additional From d1dbfbd014094554a6d3e25e37a5ff33708708f1 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 17 Aug 2021 11:10:02 +0100 Subject: [PATCH 05/50] hide thread events from the timeline --- res/css/views/rooms/_EventTile.scss | 30 +++++++++++++++ res/img/element-icons/message/thread.svg | 4 ++ src/components/structures/MessagePanel.tsx | 7 ++++ src/components/structures/ThreadView.tsx | 29 +++++++++----- src/components/structures/TimelinePanel.tsx | 4 ++ src/components/views/rooms/EventTile.tsx | 38 +++++++++++++++++++ .../views/rooms/MessageComposer.tsx | 9 ++++- 7 files changed, 110 insertions(+), 11 deletions(-) create mode 100644 res/img/element-icons/message/thread.svg diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 1c9d8e87d9..1adfc0cde3 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -670,3 +670,33 @@ $hover-select-border: 4px; margin-right: 0; } } + + +.mx_ThreadView { + + display: flex; + flex-direction: column; + + .mx_ThreadView_List { + flex: 1; + overflow: scroll; + } + + .mx_EventTile_roomName { + display: none; + } + + .mx_EventTile_line { + padding-left: 0 !important; + order: 10 !important; + } + .mx_EventTile { + width: 100%; + display: flex; + flex-direction: column; + border-bottom: 1px solid #888; + margin-top: 0; + padding-bottom: 5px; + margin-bottom: 5px; + } +} diff --git a/res/img/element-icons/message/thread.svg b/res/img/element-icons/message/thread.svg new file mode 100644 index 0000000000..b4a7cc0066 --- /dev/null +++ b/res/img/element-icons/message/thread.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 1691d90651..15275328a6 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -173,6 +173,8 @@ interface IProps { onUnfillRequest?(backwards: boolean, scrollToken: string): void; getRelationsForEvent?(eventId: string, relationType: string, eventType: string): Relations; + + hideThreadedMessages?: boolean; } interface IState { @@ -443,6 +445,11 @@ export default class MessagePanel extends React.Component { // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; + const threadingEnabled = SettingsStore.getValue("feature_threading"); + if (threadingEnabled && mxEv.replyEventId && this.props.hideThreadedMessages === true) { + return false; + } + return !shouldHideEvent(mxEv, this.context); } diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 0f9d499884..f34be87c95 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -24,9 +24,11 @@ import { replaceableComponent } from "../../utils/replaceableComponent"; import { MatrixClientPeg } from '../../MatrixClientPeg'; import ResizeNotifier from '../../utils/ResizeNotifier'; -import EventTile from '../views/rooms/EventTile'; +import EventTile, { TileShape } from '../views/rooms/EventTile'; import MessageComposer from '../views/rooms/MessageComposer'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; +import { Layout } from '../../settings/Layout'; +import TimelinePanel from './TimelinePanel'; interface IProps { roomId: string; @@ -37,6 +39,7 @@ interface IProps { } interface IState { + replyToEvent?: MatrixEvent; } /* @@ -65,6 +68,7 @@ class ThreadView extends React.Component { mxEvent={event} enableFlair={false} showReadReceipts={false} + tileShape={TileShape.FileGrid} as="div" />; } @@ -77,19 +81,24 @@ class ThreadView extends React.Component { className="mx_ThreadView" onClose={this.props.onClose} previousPhase={RightPanelPhases.RoomSummary} + withoutScrollContainer={true} > - { this.renderEventTile(this.props.mxEvent) } - - { thread && ( - thread.eventTimeline.map((event: MatrixEvent) => { - return this.renderEventTile(event); - }) - ) } - + empty} + alwaysShowTimestamps={true} + layout={Layout.Group} + hideThreadedMessages={false} + /> diff --git a/src/components/structures/TimelinePanel.tsx b/src/components/structures/TimelinePanel.tsx index f62676a4fc..e5fa6967dc 100644 --- a/src/components/structures/TimelinePanel.tsx +++ b/src/components/structures/TimelinePanel.tsx @@ -126,6 +126,8 @@ interface IProps { // callback which is called when we wish to paginate the timeline window. onPaginationRequest?(timelineWindow: TimelineWindow, direction: string, size: number): Promise; + + hideThreadedMessages?: boolean; } interface IState { @@ -214,6 +216,7 @@ class TimelinePanel extends React.Component { timelineCap: Number.MAX_VALUE, className: 'mx_RoomView_messagePanel', sendReadReceiptOnLoad: true, + hideThreadedMessages: true, }; private lastRRSentEventId: string = undefined; @@ -1511,6 +1514,7 @@ class TimelinePanel extends React.Component { showReactions={this.props.showReactions} layout={this.props.layout} enableFlair={SettingsStore.getValue(UIFeature.Flair)} + hideThreadedMessages={this.props.hideThreadedMessages} /> ); } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 884d004551..30355e55bf 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -55,6 +55,7 @@ import ReadReceiptMarker from "./ReadReceiptMarker"; import MessageActionBar from "../messages/MessageActionBar"; import ReactionsRow from '../messages/ReactionsRow'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; +import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -299,6 +300,9 @@ interface IProps { // whether or not to display the sender hideSender?: boolean; + + // whether or not to display thread info + showThreadInfo?: boolean; } interface IState { @@ -451,6 +455,7 @@ export default class EventTile extends React.Component { client.on("Room.receipt", this.onRoomReceipt); this.isListeningForReceipts = true; } + this.props.mxEvent.on("Thread.update", this.forceUpdate); } // TODO: [REACT-WARNING] Replace with appropriate lifecycle event @@ -491,6 +496,38 @@ export default class EventTile extends React.Component { } } + private renderThreadInfo(): React.ReactNode { + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const thread = room.getThread(this.props.mxEvent.getId()); + if (!thread || this.props.showThreadInfo === false) { + return null; + } + + const avatars = Array.from(thread.participants).map((mxId: string) => { + const member = room.getMember(mxId); + return ; + }); + + return ( +
{ + dis.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.ThreadView, + refireParams: { + event: this.props.mxEvent, + }, + }); + }} + > + + { avatars } + + { thread.length } { thread.length === 1 ? 'reply' : 'replies' } +
+ ); + } + private onRoomReceipt = (ev, room) => { // ignore events for other rooms const tileRoom = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); @@ -1167,6 +1204,7 @@ export default class EventTile extends React.Component { { keyRequestInfo } { actionBar } { this.props.layout === Layout.IRC && (reactionsRow) } + { this.renderThreadInfo() } { this.props.layout !== Layout.IRC && (reactionsRow) } { msgOption } diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index 8455e9aa11..bd53c9566a 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; + showReplyPreview?: boolean; e2eStatus?: E2EStatus; } @@ -201,6 +202,10 @@ export default class MessageComposer extends React.Component { private messageComposerInput: SendMessageComposer; private voiceRecordingButton: VoiceRecordComposerTile; + static defaultProps = { + showReplyPreview: true, + }; + constructor(props) { super(props); VoiceRecordingStore.instance.on(UPDATE_EVENT, this.onVoiceStoreUpdate); @@ -454,7 +459,9 @@ export default class MessageComposer extends React.Component {
{ recordingTooltip }
- + { this.props.showReplyPreview && ( + + ) }
{ controls }
From 95f4513bd21506665646dd5bfca230add77398f6 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Tue, 17 Aug 2021 17:42:47 +0100 Subject: [PATCH 06/50] Make UI respond to thread events --- res/css/views/messages/_MessageActionBar.scss | 4 ++-- .../views/right_panel/_RoomSummaryCard.scss | 4 ++++ src/components/structures/MessagePanel.tsx | 1 + src/components/structures/ThreadPanel.tsx | 7 ------ src/components/structures/ThreadView.tsx | 23 +++++++++++++++---- .../views/right_panel/RoomSummaryCard.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 20 +++++++++++++--- 7 files changed, 44 insertions(+), 17 deletions(-) diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 509ded8ee8..3e5ab87ca9 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -89,11 +89,11 @@ limitations under the License. } .mx_MessageActionBar_replyButton::after { - mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg'); + mask-image: url('$(res)/img/element-icons/room/files.svg'); } .mx_MessageActionBar_threadButton::after { - mask-image: url('$(res)/img/element-icons/room/files.svg'); + mask-image: url('$(res)/img/element-icons/message/thread.svg'); } .mx_MessageActionBar_editButton::after { diff --git a/res/css/views/right_panel/_RoomSummaryCard.scss b/res/css/views/right_panel/_RoomSummaryCard.scss index dc7804d072..d93e593679 100644 --- a/res/css/views/right_panel/_RoomSummaryCard.scss +++ b/res/css/views/right_panel/_RoomSummaryCard.scss @@ -232,6 +232,10 @@ limitations under the License. mask-image: url('$(res)/img/element-icons/room/files.svg'); } +.mx_RoomSummaryCard_icon_threads::before { + mask-image: url('$(res)/img/element-icons/message/thread.svg'); +} + .mx_RoomSummaryCard_icon_share::before { mask-image: url('$(res)/img/element-icons/room/share.svg'); } diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 15275328a6..fd716041c2 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -267,6 +267,7 @@ export default class MessagePanel extends React.Component { componentDidMount() { this.calculateRoomMembersCount(); this.props.room?.on("RoomState.members", this.calculateRoomMembersCount); + this.props.room?.getThreads().forEach(thread => thread.fetchReplyChain()); this.isMounted = true; } diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index c99246ccda..f0b85e9723 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -52,13 +52,6 @@ class ThreadView extends React.Component { public componentDidMount(): void { this.room.on("Thread.update", this.onThreadEventReceived); this.room.on("Thread.ready", this.onThreadEventReceived); - this.updateThreads(() => { - this.state.threads.forEach(thread => { - if (!thread.ready) { - thread.fetchReplyChain(); - } - }); - }); } public componentWillUnmount(): void { diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index f34be87c95..7767e5929e 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -29,6 +29,7 @@ import MessageComposer from '../views/rooms/MessageComposer'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { Layout } from '../../settings/Layout'; import TimelinePanel from './TimelinePanel'; +import { Thread } from '../../../../matrix-js-sdk/src/models/thread'; interface IProps { roomId: string; @@ -40,6 +41,7 @@ interface IProps { interface IState { replyToEvent?: MatrixEvent; + thread?: Thread; } /* @@ -50,16 +52,29 @@ class ThreadView extends React.Component { state = {}; public componentDidMount(): void { - // this.props.mxEvent.getThread().on("Thread.update", this.updateThread); - this.props.mxEvent.getThread().once("Thread.ready", this.updateThread); + this.setupThread(); } public componentWillUnmount(): void { - this.props.mxEvent.getThread().removeListener("Thread.update", this.updateThread); + if (this.state.thread) { + this.state.thread.removeListener("Thread.update", this.updateThread); + this.state.thread.removeListener("Thread.ready", this.updateThread); + } } + setupThread = () => { + const thread = this.props.mxEvent.getThread(); + if (thread) { + thread.on("Thread.update", this.updateThread); + thread.once("Thread.ready", this.updateThread); + this.updateThread(); + } + }; + updateThread = () => { - this.forceUpdate(); + this.setState({ + thread: this.props.mxEvent.getThread(), + }); }; public renderEventTile(event: MatrixEvent): JSX.Element { diff --git a/src/components/views/right_panel/RoomSummaryCard.tsx b/src/components/views/right_panel/RoomSummaryCard.tsx index eb3d7499f4..a09fabc06d 100644 --- a/src/components/views/right_panel/RoomSummaryCard.tsx +++ b/src/components/views/right_panel/RoomSummaryCard.tsx @@ -280,7 +280,7 @@ const RoomSummaryCard: React.FC = ({ room, onClose }) => { - - + { SettingsStore.getValue("experimentalThreadSupport") && ( + + ) } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 6f807f66d6..74d9fc5ca6 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -57,6 +57,7 @@ import ReactionsRow from '../messages/ReactionsRow'; import { getEventDisplayInfo } from '../../../utils/EventUtils'; import { RightPanelPhases } from "../../../stores/RightPanelStorePhases"; import { Thread } from '../../../../../matrix-js-sdk/src/models/thread'; +import SettingsStore from "../../../settings/SettingsStore"; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -461,8 +462,10 @@ export default class EventTile extends React.Component { this.isListeningForReceipts = true; } - this.props.mxEvent.once("Thread.ready", this.updateThread); - this.props.mxEvent.on("Thread.update", this.updateThread); + if (SettingsStore.getValue("experimentalThreadSupport")) { + this.props.mxEvent.once("Thread.ready", this.updateThread); + this.props.mxEvent.on("Thread.update", this.updateThread); + } } private updateThread = (thread) => { @@ -511,6 +514,10 @@ export default class EventTile extends React.Component { } private renderThreadInfo(): React.ReactNode { + if (!SettingsStore.getValue("experimentalThreadSupport")) { + return null; + } + const thread = this.state.thread; const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); if (!thread || this.props.showThreadInfo === false) { From 30a762944af85a7134e3a4829d2f95ab53001d6c Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 20 Aug 2021 12:11:04 +0100 Subject: [PATCH 09/50] Implement a very low fidelity UI for threads --- res/css/views/rooms/_EventTile.scss | 25 ++++++++++++++++++- res/css/views/rooms/_MessageComposer.scss | 14 +++++++++++ src/components/structures/ThreadView.tsx | 1 + .../views/rooms/MessageComposer.tsx | 12 +++++++-- 4 files changed, 49 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 4dd91eb7f2..4b48ec971b 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -681,6 +681,30 @@ $hover-select-border: 4px; display: flex; flex-direction: column; + .mx_ScrollPanel { + margin-top: 20px; + .mx_RoomView_MessageList { + padding: 0; + } + } + + .mx_EventTile_senderDetails { + display: flex; + align-items: center; + gap: 6px; + margin-bottom: 6px; + a { + flex: 1; + min-width: none; + max-width: 100%; + display: flex; + align-items: center; + .mx_SenderProfile { + flex: 1; + } + } + } + .mx_ThreadView_List { flex: 1; overflow: scroll; @@ -698,7 +722,6 @@ $hover-select-border: 4px; width: 100%; display: flex; flex-direction: column; - border-bottom: 1px solid #888; margin-top: 0; padding-bottom: 5px; margin-bottom: 5px; diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index 5e2eff4047..b2747f7a9b 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -340,3 +340,17 @@ limitations under the License. height: 50px; } } + +/** + * Unstable compact mode + */ + +.mx_MessageComposer.mx_MessageComposer--compact { + margin-right: 0; + .mx_MessageComposer_wrapper { + padding: 0; + } + .mx_MessageComposer_button:last-child { + margin-right: 0; + } +} diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 7767e5929e..5d07b7feaa 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -115,6 +115,7 @@ class ThreadView extends React.Component { replyToEvent={thread?.replyToEvent} showReplyPreview={false} permalinkCreator={this.props.permalinkCreator} + compact={true} /> ); diff --git a/src/components/views/rooms/MessageComposer.tsx b/src/components/views/rooms/MessageComposer.tsx index bd53c9566a..fbf3b58570 100644 --- a/src/components/views/rooms/MessageComposer.tsx +++ b/src/components/views/rooms/MessageComposer.tsx @@ -185,6 +185,7 @@ interface IProps { replyToEvent?: MatrixEvent; showReplyPreview?: boolean; e2eStatus?: E2EStatus; + compact?: boolean; } interface IState { @@ -204,6 +205,7 @@ export default class MessageComposer extends React.Component { static defaultProps = { showReplyPreview: true, + compact: false, }; constructor(props) { @@ -367,7 +369,7 @@ export default class MessageComposer extends React.Component { render() { const controls = [ - this.state.me ? : null, + this.state.me && !this.props.compact ? : null, this.props.e2eStatus ? : null, @@ -455,8 +457,14 @@ export default class MessageComposer extends React.Component { />; } + const classes = classNames({ + "mx_MessageComposer": true, + "mx_GroupLayout": true, + "mx_MessageComposer--compact": this.props.compact, + }); + return ( -
+
{ recordingTooltip }
{ this.props.showReplyPreview && ( From 9facb0d963afa4932f5b48ff3f20aa7128bef30a Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 23 Aug 2021 14:44:44 +0100 Subject: [PATCH 10/50] Polish UI --- res/css/views/messages/_MessageActionBar.scss | 2 +- res/css/views/rooms/_EventTile.scss | 4 + src/components/structures/ThreadView.tsx | 88 +++++++++++++------ src/components/views/rooms/EventTile.tsx | 3 +- 4 files changed, 67 insertions(+), 30 deletions(-) diff --git a/res/css/views/messages/_MessageActionBar.scss b/res/css/views/messages/_MessageActionBar.scss index 3e5ab87ca9..c2f3c2ebca 100644 --- a/res/css/views/messages/_MessageActionBar.scss +++ b/res/css/views/messages/_MessageActionBar.scss @@ -89,7 +89,7 @@ limitations under the License. } .mx_MessageActionBar_replyButton::after { - mask-image: url('$(res)/img/element-icons/room/files.svg'); + mask-image: url('$(res)/img/element-icons/room/message-bar/reply.svg'); } .mx_MessageActionBar_threadButton::after { diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 4b48ec971b..1bf62d3fb1 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -676,6 +676,10 @@ $hover-select-border: 4px; } +.mx_ThreadInfo:hover { + cursor: pointer; +} + .mx_ThreadView { display: flex; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 5d07b7feaa..cac4054dfb 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -30,6 +30,8 @@ import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { Layout } from '../../settings/Layout'; import TimelinePanel from './TimelinePanel'; import { Thread } from '../../../../matrix-js-sdk/src/models/thread'; +import dis from "../../dispatcher/dispatcher"; +import { ActionPayload } from '../../dispatcher/payloads'; interface IProps { roomId: string; @@ -49,32 +51,61 @@ interface IState { */ @replaceableComponent("structures.ThreadView") class ThreadView extends React.Component { - state = {}; + private dispatcherRef: string; + + constructor(props: IProps) { + super(props); + this.state = {}; + } public componentDidMount(): void { - this.setupThread(); + this.setupThread(this.props.mxEvent); + this.dispatcherRef = dis.register(this.onAction); } public componentWillUnmount(): void { + this.teardownThread(); + dis.unregister(this.dispatcherRef); + } + + public componentDidUpdate(prevProps) { + if (prevProps.mxEvent !== this.props.mxEvent) { + this.teardownThread(); + this.setupThread(this.props.mxEvent); + } + } + + private onAction = (payload: ActionPayload): void => { + if (payload.phase == RightPanelPhases.ThreadView && payload.event) { + if (payload.event !== this.props.mxEvent) { + this.teardownThread(); + this.setupThread(payload.event); + } + } + }; + + setupThread = (mxEv: MatrixEvent) => { + const thread = mxEv.getThread(); + if (thread) { + thread.on("Thread.update", this.updateThread); + thread.once("Thread.ready", this.updateThread); + this.updateThread(thread); + } + }; + + teardownThread = () => { if (this.state.thread) { this.state.thread.removeListener("Thread.update", this.updateThread); this.state.thread.removeListener("Thread.ready", this.updateThread); } - } - - setupThread = () => { - const thread = this.props.mxEvent.getThread(); - if (thread) { - thread.on("Thread.update", this.updateThread); - thread.once("Thread.ready", this.updateThread); - this.updateThread(); - } }; - updateThread = () => { - this.setState({ - thread: this.props.mxEvent.getThread(), - }); + updateThread = (thread?: Thread) => { + if (thread) { + this.setState({ thread }); + } else { + this.forceUpdate(); + } }; public renderEventTile(event: MatrixEvent): JSX.Element { @@ -89,7 +120,6 @@ class ThreadView extends React.Component { } public render() { - const thread = this.props.mxEvent.getThread(); const room = MatrixClientPeg.get().getRoom(this.props.roomId); return ( { previousPhase={RightPanelPhases.RoomSummary} withoutScrollContainer={true} > - empty
} - alwaysShowTimestamps={true} - layout={Layout.Group} - hideThreadedMessages={false} - /> + { this.state.thread && ( + empty
} + alwaysShowTimestamps={true} + layout={Layout.Group} + hideThreadedMessages={false} + /> + ) } { return (
{ dis.dispatch({ action: Action.SetRightPanelPhase, @@ -544,7 +545,7 @@ export default class EventTile extends React.Component { { avatars } - { thread.length } { thread.length === 1 ? 'reply' : 'replies' } + { thread.length - 1 } { thread.length === 2 ? 'reply' : 'replies' }
); } From ef51a46d247b1ed673532632ac1cfaf5445c46ec Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 23 Aug 2021 14:55:14 +0100 Subject: [PATCH 11/50] Fix linting --- src/components/views/messages/MessageActionBar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 29212449d1..83e063ab8c 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -180,7 +180,7 @@ export default class MessageActionBar extends React.PureComponent { allowClose: false, refireParams: { event: this.props.mxEvent, - } + }, }); } @@ -282,7 +282,7 @@ export default class MessageActionBar extends React.PureComponent { onClick={this.onThreadClick} key="thread" /> - )} + ) } ); } if (this.context.canReact) { From 34da07f1f9f314b21441e5cf4f2568a00d38f283 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Mon, 23 Aug 2021 17:31:23 +0100 Subject: [PATCH 12/50] Pass room to ThreadView over roomId --- src/components/structures/RightPanel.tsx | 2 +- src/components/structures/ThreadPanel.tsx | 2 +- src/components/structures/ThreadView.tsx | 18 +++++++++++++----- src/components/views/rooms/EventTile.tsx | 2 +- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 85d3013a58..67634c63d2 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -315,7 +315,7 @@ export default class RightPanel extends React.Component { case RightPanelPhases.ThreadView: panel = void; resizeNotifier: ResizeNotifier; mxEvent: MatrixEvent; @@ -73,6 +75,13 @@ class ThreadView extends React.Component { this.teardownThread(); this.setupThread(this.props.mxEvent); } + + if (prevProps.room !== this.props.room) { + dis.dispatch({ + action: Action.SetRightPanelPhase, + phase: RightPanelPhases.RoomSummary, + }); + } } private onAction = (payload: ActionPayload): void => { @@ -120,7 +129,6 @@ class ThreadView extends React.Component { } public render() { - const room = MatrixClientPeg.get().getRoom(this.props.roomId); return ( { /> ) } Date: Tue, 24 Aug 2021 09:09:28 +0100 Subject: [PATCH 13/50] PR feedback --- .stylelintrc.js | 1 + res/css/views/rooms/_EventTile.scss | 6 ++++-- res/css/views/rooms/_MessageComposer.scss | 2 ++ src/MatrixClientPeg.ts | 2 +- src/components/structures/MessagePanel.tsx | 7 ++++--- src/components/structures/ThreadPanel.tsx | 18 ++++++------------ src/components/structures/ThreadView.tsx | 19 ++++++------------- .../views/messages/MessageActionBar.js | 2 +- .../views/right_panel/RoomSummaryCard.tsx | 2 +- src/components/views/rooms/EventTile.tsx | 4 ++-- src/settings/Settings.tsx | 2 +- 11 files changed, 29 insertions(+), 36 deletions(-) diff --git a/.stylelintrc.js b/.stylelintrc.js index 0e6de7000f..c044b19a63 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -17,6 +17,7 @@ module.exports = { "selector-list-comma-newline-after": null, "at-rule-no-unknown": null, "no-descending-specificity": null, + "no-empty-first-line": true, "scss/at-rule-no-unknown": [true, { // https://github.com/vector-im/element-web/issues/10544 "ignoreAtRules": ["define-mixin"], diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 1bf62d3fb1..0e77f20c49 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -643,6 +643,7 @@ $hover-select-border: 4px; // Remove some of the default tile padding so that the error is centered margin-right: 0; + .mx_EventTile_line { padding-left: 0; margin-right: 0; @@ -675,18 +676,17 @@ $hover-select-border: 4px; } } - .mx_ThreadInfo:hover { cursor: pointer; } .mx_ThreadView { - display: flex; flex-direction: column; .mx_ScrollPanel { margin-top: 20px; + .mx_RoomView_MessageList { padding: 0; } @@ -703,6 +703,7 @@ $hover-select-border: 4px; max-width: 100%; display: flex; align-items: center; + .mx_SenderProfile { flex: 1; } @@ -722,6 +723,7 @@ $hover-select-border: 4px; padding-left: 0 !important; order: 10 !important; } + .mx_EventTile { width: 100%; display: flex; diff --git a/res/css/views/rooms/_MessageComposer.scss b/res/css/views/rooms/_MessageComposer.scss index b2747f7a9b..54c250fc2e 100644 --- a/res/css/views/rooms/_MessageComposer.scss +++ b/res/css/views/rooms/_MessageComposer.scss @@ -347,9 +347,11 @@ limitations under the License. .mx_MessageComposer.mx_MessageComposer--compact { margin-right: 0; + .mx_MessageComposer_wrapper { padding: 0; } + .mx_MessageComposer_button:last-child { margin-right: 0; } diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index d3382a2b5e..7d0ff560b7 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -213,7 +213,7 @@ class MatrixClientPegClass implements IMatrixClientPeg { opts.pendingEventOrdering = PendingEventOrdering.Detached; opts.lazyLoadMembers = true; opts.clientWellKnownPollPeriod = 2 * 60 * 60; // 2 hours - opts.experimentalThreadSupport = SettingsStore.getValue("experimentalThreadSupport"); + opts.experimentalThreadSupport = SettingsStore.getValue("feature_thread"); // Connect the matrix client to the dispatcher and setting handlers MatrixActionCreators.start(this.matrixClient); diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index 49fb50814c..8bf1f5bd5f 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -267,7 +267,7 @@ export default class MessagePanel extends React.Component { componentDidMount() { this.calculateRoomMembersCount(); this.props.room?.on("RoomState.members", this.calculateRoomMembersCount); - if (SettingsStore.getValue("experimentalThreadSupport")) { + if (SettingsStore.getValue("feature_thread")) { this.props.room?.getThreads().forEach(thread => thread.fetchReplyChain()); } this.isMounted = true; @@ -448,8 +448,9 @@ export default class MessagePanel extends React.Component { // Always show highlighted event if (this.props.highlightedEventId === mxEv.getId()) return true; - const threadingEnabled = SettingsStore.getValue("experimentalThreadSupport"); - if (threadingEnabled && mxEv.replyEventId && this.props.hideThreadedMessages === true) { + if (mxEv.replyEventId + && this.props.hideThreadedMessages + && SettingsStore.getValue("feature_thread")) { return false; } diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 9259dbe13b..047e527f34 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -1,6 +1,5 @@ /* -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. @@ -37,11 +36,8 @@ interface IState { threads?: Thread[]; } -/* - * Component which shows the filtered file using a TimelinePanel - */ @replaceableComponent("structures.ThreadView") -class ThreadView extends React.Component { +export default class ThreadPanel extends React.Component { private room: Room; constructor(props: IProps) { @@ -59,15 +55,15 @@ class ThreadView extends React.Component { this.room.removeListener("Thread.ready", this.onThreadEventReceived); } - public onThreadEventReceived = () => this.updateThreads(); + private onThreadEventReceived = () => this.updateThreads(); - public updateThreads = (callback?: () => void): void => { + private updateThreads = (callback?: () => void): void => { this.setState({ threads: this.room.getThreads(), }, callback); }; - public renderEventTile(event: MatrixEvent): JSX.Element { + private renderEventTile(event: MatrixEvent): JSX.Element { return { />; } - public render() { + public render(): JSX.Element { return ( { ); } } - -export default ThreadView; diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 38b1c8dc08..03609c66d0 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -1,6 +1,5 @@ /* -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. @@ -22,7 +21,6 @@ import { Thread } from 'matrix-js-sdk/src/models/thread'; import BaseCard from "../views/right_panel/BaseCard"; import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { replaceableComponent } from "../../utils/replaceableComponent"; -import { MatrixClientPeg } from '../../MatrixClientPeg'; import ResizeNotifier from '../../utils/ResizeNotifier'; import EventTile, { TileShape } from '../views/rooms/EventTile'; @@ -48,11 +46,8 @@ interface IState { thread?: Thread; } -/* - * Component which shows the filtered file using a TimelinePanel - */ @replaceableComponent("structures.ThreadView") -class ThreadView extends React.Component { +export default class ThreadView extends React.Component { private dispatcherRef: string; constructor(props: IProps) { @@ -93,7 +88,7 @@ class ThreadView extends React.Component { } }; - setupThread = (mxEv: MatrixEvent) => { + private setupThread = (mxEv: MatrixEvent) => { const thread = mxEv.getThread(); if (thread) { thread.on("Thread.update", this.updateThread); @@ -102,14 +97,14 @@ class ThreadView extends React.Component { } }; - teardownThread = () => { + private teardownThread = () => { if (this.state.thread) { this.state.thread.removeListener("Thread.update", this.updateThread); this.state.thread.removeListener("Thread.ready", this.updateThread); } }; - updateThread = (thread?: Thread) => { + private updateThread = (thread?: Thread) => { if (thread) { this.setState({ thread }); } else { @@ -128,7 +123,7 @@ class ThreadView extends React.Component { />; } - public render() { + public render(): JSX.Element { return ( { ); } } - -export default ThreadView; diff --git a/src/components/views/messages/MessageActionBar.js b/src/components/views/messages/MessageActionBar.js index 83e063ab8c..cb8ea7a50d 100644 --- a/src/components/views/messages/MessageActionBar.js +++ b/src/components/views/messages/MessageActionBar.js @@ -275,7 +275,7 @@ export default class MessageActionBar extends React.PureComponent { onClick={this.onReplyClick} key="reply" /> - { SettingsStore.getValue("experimentalThreadSupport") && ( + { SettingsStore.getValue("feature_thread") && ( = ({ room, onClose }) => { - { SettingsStore.getValue("experimentalThreadSupport") && ( + { SettingsStore.getValue("feature_thread") && ( diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 638bef5061..935a349b10 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -462,7 +462,7 @@ export default class EventTile extends React.Component { this.isListeningForReceipts = true; } - if (SettingsStore.getValue("experimentalThreadSupport")) { + if (SettingsStore.getValue("feature_thread")) { this.props.mxEvent.once("Thread.ready", this.updateThread); this.props.mxEvent.on("Thread.update", this.updateThread); } @@ -514,7 +514,7 @@ export default class EventTile extends React.Component { } private renderThreadInfo(): React.ReactNode { - if (!SettingsStore.getValue("experimentalThreadSupport")) { + if (!SettingsStore.getValue("feature_thread")) { return null; } diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 6496231679..2dba4cf55d 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -211,7 +211,7 @@ export const SETTINGS: {[setting: string]: ISetting} = { supportedLevels: LEVELS_FEATURE, default: false, }, - "experimentalThreadSupport": { + "feature_thread": { isFeature: true, // Requires a reload as we change an option flag on the `js-sdk` // And the entire sync history needs to be parsed again From 289ac347645c9b3e5e370279f671e1af22b1ca1f Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Aug 2021 18:16:40 -0600 Subject: [PATCH 14/50] Add support for MSC2762's timeline functionality See https://github.com/matrix-org/matrix-widget-api/pull/41 --- .../WidgetCapabilitiesPromptDialog.tsx | 19 +++- src/i18n/strings/en_EN.json | 2 + src/stores/widgets/StopGapWidget.ts | 6 +- src/stores/widgets/StopGapWidgetDriver.ts | 98 ++++++++++++------- src/widgets/CapabilityText.tsx | 42 +++++++- 5 files changed, 119 insertions(+), 48 deletions(-) diff --git a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx index ebeab191b1..556dc057f9 100644 --- a/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx +++ b/src/components/views/dialogs/WidgetCapabilitiesPromptDialog.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 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. @@ -20,6 +20,7 @@ import { _t } from "../../../languageHandler"; import { IDialogProps } from "./IDialogProps"; import { Capability, + isTimelineCapability, Widget, WidgetEventCapability, WidgetKind, @@ -30,6 +31,7 @@ import DialogButtons from "../elements/DialogButtons"; import LabelledToggleSwitch from "../elements/LabelledToggleSwitch"; import { CapabilityText } from "../../../widgets/CapabilityText"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import { lexicographicCompare } from "matrix-js-sdk/src/utils"; export function getRememberedCapabilitiesForWidget(widget: Widget): Capability[] { return JSON.parse(localStorage.getItem(`widget_${widget.id}_approved_caps`) || "[]"); @@ -102,7 +104,20 @@ export default class WidgetCapabilitiesPromptDialog extends React.PureComponent< } public render() { - const checkboxRows = Object.entries(this.state.booleanStates).map(([cap, isChecked], i) => { + // We specifically order the timeline capabilities down to the bottom. The capability text + // generation cares strongly about this. + const orderedCapabilities = Object.entries(this.state.booleanStates).sort(([capA], [capB]) => { + const isTimelineA = isTimelineCapability(capA); + const isTimelineB = isTimelineCapability(capB); + + if (!isTimelineA && !isTimelineB) return lexicographicCompare(capA, capB); + if (isTimelineA && !isTimelineB) return 1; + if (!isTimelineA && isTimelineB) return -1; + if (isTimelineA && isTimelineB) return lexicographicCompare(capA, capB); + + return 0; + }); + const checkboxRows = orderedCapabilities.map(([cap, isChecked], i) => { const text = CapabilityText.for(cap, this.props.widgetKind); const byline = text.byline ? { text.byline } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21859fb1aa..3b67db374c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -604,6 +604,8 @@ "See when anyone posts a sticker to your active room": "See when anyone posts a sticker to your active room", "with an empty state key": "with an empty state key", "with state key %(stateKey)s": "with state key %(stateKey)s", + "The above, but in any room you are joined or invited to as well": "The above, but in any room you are joined or invited to as well", + "The above, but in as well": "The above, but in as well", "Send %(eventType)s events as you in this room": "Send %(eventType)s events as you in this room", "See %(eventType)s events posted to this room": "See %(eventType)s events posted to this room", "Send %(eventType)s events as you in your active room": "Send %(eventType)s events as you in your active room", diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index daa1e0e787..49653626c1 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020, 2021 The Matrix.org Foundation C.I.C. + * Copyright 2020 - 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. @@ -408,13 +408,11 @@ export class StopGapWidget extends EventEmitter { private onEvent = (ev: MatrixEvent) => { MatrixClientPeg.get().decryptEventIfNeeded(ev); if (ev.isBeingDecrypted() || ev.isDecryptionFailure()) return; - if (ev.getRoomId() !== this.eventListenerRoomId) return; this.feedEvent(ev); }; private onEventDecrypted = (ev: MatrixEvent) => { if (ev.isDecryptionFailure()) return; - if (ev.getRoomId() !== this.eventListenerRoomId) return; this.feedEvent(ev); }; @@ -422,7 +420,7 @@ export class StopGapWidget extends EventEmitter { if (!this.messaging) return; const raw = ev.getEffectiveEvent(); - this.messaging.feedEvent(raw).catch(e => { + this.messaging.feedEvent(raw, this.eventListenerRoomId).catch(e => { console.error("Error sending event to widget: ", e); }); } diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 13cd260ef0..78d7c9ede0 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * Copyright 2020 - 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. @@ -23,6 +23,7 @@ import { MatrixCapabilities, OpenIDRequestState, SimpleObservable, + Symbols, Widget, WidgetDriver, WidgetEventCapability, @@ -44,7 +45,8 @@ import { CHAT_EFFECTS } from "../../effects"; import { containsEmoji } from "../../effects/utils"; import dis from "../../dispatcher/dispatcher"; import { tryTransformPermalinkToLocalHref } from "../../utils/permalinks/Permalinks"; -import { MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { IEvent, MatrixEvent } from "matrix-js-sdk/src/models/event"; +import { Room } from "matrix-js-sdk"; // TODO: Purge this from the universe @@ -119,9 +121,9 @@ export class StopGapWidgetDriver extends WidgetDriver { return new Set(iterableUnion(allowedSoFar, requested)); } - public async sendEvent(eventType: string, content: any, stateKey: string = null): Promise { + public async sendEvent(eventType: string, content: any, stateKey: string = null, targetRoomId: string = null): Promise { const client = MatrixClientPeg.get(); - const roomId = ActiveRoomObserver.activeRoomId; + const roomId = targetRoomId || ActiveRoomObserver.activeRoomId; if (!client || !roomId) throw new Error("Not in a room or not attached to a client"); @@ -145,48 +147,68 @@ export class StopGapWidgetDriver extends WidgetDriver { return { roomId, eventId: r.event_id }; } - public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { - limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice - + private pickRooms(roomIds: (string | Symbols.AnyRoom)[] = null): Room[] { const client = MatrixClientPeg.get(); - const roomId = ActiveRoomObserver.activeRoomId; - const room = client.getRoom(roomId); - if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); + if (!client) throw new Error("Not attached to a client"); - const results: MatrixEvent[] = []; - const events = room.getLiveTimeline().getEvents(); // timelines are most recent last - for (let i = events.length - 1; i > 0; i--) { - if (results.length >= limit) break; - - const ev = events[i]; - if (ev.getType() !== eventType || ev.isState()) continue; - if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue; - results.push(ev); - } - - return results.map(e => e.getEffectiveEvent()); + const targetRooms = roomIds + ? (roomIds.includes(Symbols.AnyRoom) ? client.getVisibleRooms() : roomIds.map(r => client.getRoom(r))) + : [client.getRoom(ActiveRoomObserver.activeRoomId)]; + return targetRooms.filter(r => !!r); } - public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise { - limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice + public async readRoomEvents( + eventType: string, + msgtype: string | undefined, + limitPerRoom: number, + roomIds: (string | Symbols.AnyRoom)[] = null, + ): Promise { + limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, 25) : 25; // arbitrary choice - const client = MatrixClientPeg.get(); - const roomId = ActiveRoomObserver.activeRoomId; - const room = client.getRoom(roomId); - if (!client || !roomId || !room) throw new Error("Not in a room or not attached to a client"); + const rooms = this.pickRooms(roomIds); + const allResults: IEvent[] = []; + for (const room of rooms) { + const results: MatrixEvent[] = []; + const events = room.getLiveTimeline().getEvents(); // timelines are most recent last + for (let i = events.length - 1; i > 0; i--) { + if (results.length >= limitPerRoom) break; - const results: MatrixEvent[] = []; - const state: Map = room.currentState.events.get(eventType); - if (state) { - if (stateKey === "" || !!stateKey) { - const forKey = state.get(stateKey); - if (forKey) results.push(forKey); - } else { - results.push(...Array.from(state.values())); + const ev = events[i]; + if (ev.getType() !== eventType || ev.isState()) continue; + if (eventType === EventType.RoomMessage && msgtype && msgtype !== ev.getContent()['msgtype']) continue; + results.push(ev); } - } - return results.slice(0, limit).map(e => e.event); + results.forEach(e => allResults.push(e.getEffectiveEvent())); + } + return allResults; + } + + public async readStateEvents( + eventType: string, + stateKey: string | undefined, + limitPerRoom: number, + roomIds: (string | Symbols.AnyRoom)[] = null, + ): Promise { + limitPerRoom = limitPerRoom > 0 ? Math.min(limitPerRoom, 100) : 100; // arbitrary choice + + const rooms = this.pickRooms(roomIds); + const allResults: IEvent[] = []; + for (const room of rooms) { + const results: MatrixEvent[] = []; + const state: Map = room.currentState.events.get(eventType); + if (state) { + if (stateKey === "" || !!stateKey) { + const forKey = state.get(stateKey); + if (forKey) results.push(forKey); + } else { + results.push(...Array.from(state.values())); + } + } + + results.slice(0, limitPerRoom).forEach(e => allResults.push(e.getEffectiveEvent())); + } + return allResults; } public async askOpenID(observer: SimpleObservable) { diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index 63e34eea7a..30349fe0f6 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -1,5 +1,5 @@ /* -Copyright 2020 The Matrix.org Foundation C.I.C. +Copyright 2020 - 2021 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,11 +14,22 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { Capability, EventDirection, MatrixCapabilities, WidgetEventCapability, WidgetKind } from "matrix-widget-api"; +import { + Capability, + EventDirection, + getTimelineRoomIDFromCapability, + isTimelineCapability, + isTimelineCapabilityFor, + MatrixCapabilities, Symbols, + WidgetEventCapability, + WidgetKind +} from "matrix-widget-api"; import { _t, _td, TranslatedString } from "../languageHandler"; import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; import { ElementWidgetCapabilities } from "../stores/widgets/ElementWidgetCapabilities"; import React from "react"; +import { MatrixClientPeg } from "../MatrixClientPeg"; +import TextWithTooltip from "../components/views/elements/TextWithTooltip"; type GENERIC_WIDGET_KIND = "generic"; // eslint-disable-line @typescript-eslint/naming-convention const GENERIC_WIDGET_KIND: GENERIC_WIDGET_KIND = "generic"; @@ -138,8 +149,31 @@ export class CapabilityText { if (textForKind[GENERIC_WIDGET_KIND]) return { primary: _t(textForKind[GENERIC_WIDGET_KIND]) }; // ... we'll fall through to the generic capability processing at the end of this - // function if we fail to locate a simple string and the capability isn't for an - // event. + // function if we fail to generate a string for the capability. + } + + // Try to handle timeline capabilities. The text here implies that the caller has sorted + // the timeline caps to the end for UI purposes. + if (isTimelineCapability(capability)) { + if (isTimelineCapabilityFor(capability, Symbols.AnyRoom)) { + return { primary: _t("The above, but in any room you are joined or invited to as well") }; + } else { + const roomId = getTimelineRoomIDFromCapability(capability); + const room = MatrixClientPeg.get().getRoom(roomId); + return { + primary: _t("The above, but in as well", {}, { + Room: () => { + if (room) { + return + { room.name } + ; + } else { + return { roomId }; + } + }, + }), + }; + } } // We didn't have a super simple line of text, so try processing the capability as the From f912d9d1b40308f55e0db008abc0f1288f0a255e Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 25 Aug 2021 18:25:20 -0600 Subject: [PATCH 15/50] Appease the linter --- src/stores/widgets/StopGapWidgetDriver.ts | 7 ++++++- src/widgets/CapabilityText.tsx | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index 78d7c9ede0..45c7d6bd2e 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -121,7 +121,12 @@ export class StopGapWidgetDriver extends WidgetDriver { return new Set(iterableUnion(allowedSoFar, requested)); } - public async sendEvent(eventType: string, content: any, stateKey: string = null, targetRoomId: string = null): Promise { + public async sendEvent( + eventType: string, + content: any, + stateKey: string = null, + targetRoomId: string = null, + ): Promise { const client = MatrixClientPeg.get(); const roomId = targetRoomId || ActiveRoomObserver.activeRoomId; diff --git a/src/widgets/CapabilityText.tsx b/src/widgets/CapabilityText.tsx index 30349fe0f6..8c13a4b2fc 100644 --- a/src/widgets/CapabilityText.tsx +++ b/src/widgets/CapabilityText.tsx @@ -22,7 +22,7 @@ import { isTimelineCapabilityFor, MatrixCapabilities, Symbols, WidgetEventCapability, - WidgetKind + WidgetKind, } from "matrix-widget-api"; import { _t, _td, TranslatedString } from "../languageHandler"; import { EventType, MsgType } from "matrix-js-sdk/src/@types/event"; From bd1aa01b67b489ba831841577d7afadcd0804dad Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 26 Aug 2021 08:19:44 +0100 Subject: [PATCH 16/50] Update copyright and method accessors --- res/css/views/rooms/_EventTile.scss | 1 + src/components/structures/ThreadPanel.tsx | 2 +- src/components/structures/ThreadView.tsx | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 5648a164e4..494f14c9bd 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -697,6 +697,7 @@ $hover-select-border: 4px; align-items: center; gap: 6px; margin-bottom: 6px; + a { flex: 1; min-width: none; diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 047e527f34..a0bccfdce9 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -1,5 +1,5 @@ /* -Copyright 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. diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 03609c66d0..2e69d7b0f4 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -1,5 +1,5 @@ /* -Copyright 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. @@ -112,7 +112,7 @@ export default class ThreadView extends React.Component { } }; - public renderEventTile(event: MatrixEvent): JSX.Element { + private renderEventTile(event: MatrixEvent): JSX.Element { return Date: Thu, 26 Aug 2021 08:45:00 +0100 Subject: [PATCH 17/50] Remove unused renderEventTile method --- src/components/structures/ThreadView.tsx | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 2e69d7b0f4..951c821d5c 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -112,17 +112,6 @@ export default class ThreadView extends React.Component { } }; - private renderEventTile(event: MatrixEvent): JSX.Element { - return ; - } - public render(): JSX.Element { return ( Date: Thu, 26 Aug 2021 13:28:48 -0600 Subject: [PATCH 18/50] Don't send prehistorical events to widgets during decryption at startup Fixes https://github.com/vector-im/element-web/issues/18060 Tracking a localized read receipt of sorts appears to be the fastest and least complex approach, though not the greatest. --- src/stores/widgets/StopGapWidget.ts | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/stores/widgets/StopGapWidget.ts b/src/stores/widgets/StopGapWidget.ts index daa1e0e787..f0c64b1d4a 100644 --- a/src/stores/widgets/StopGapWidget.ts +++ b/src/stores/widgets/StopGapWidget.ts @@ -55,6 +55,7 @@ import { MatrixEvent } from "matrix-js-sdk/src/models/event"; import { ELEMENT_CLIENT_ID } from "../../identifiers"; import { getUserLanguage } from "../../languageHandler"; import { WidgetVariableCustomisations } from "../../customisations/WidgetVariables"; +import { arrayFastClone } from "../../utils/arrays"; // TODO: Destroy all of this code @@ -146,6 +147,7 @@ export class StopGapWidget extends EventEmitter { private scalarToken: string; private roomId?: string; private kind: WidgetKind; + private readUpToMap: {[roomId: string]: string} = {}; // room ID to event ID constructor(private appTileProps: IAppTileProps) { super(); @@ -294,6 +296,14 @@ export class StopGapWidget extends EventEmitter { this.messaging.transport.reply(ev.detail, {}); }); + // Populate the map of "read up to" events for this widget with the current event in every room. + // This is a bit inefficient, but should be okay. We do this for all rooms in case the widget + // 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(); + } + // Attach listeners for feeding events - the underlying widget classes handle permissions for us MatrixClientPeg.get().on('event', this.onEvent); MatrixClientPeg.get().on('Event.decrypted', this.onEventDecrypted); @@ -421,6 +431,43 @@ export class StopGapWidget extends EventEmitter { private feedEvent(ev: MatrixEvent) { if (!this.messaging) return; + // Check to see if this event would be before or after our "read up to" marker. If it's + // before, or we can't decide, then we assume the widget will have already seen the event. + // If the event is after, or we don't have a marker for the room, then we'll send it through. + // + // This approach of "read up to" prevents widgets receiving decryption spam from startup or + // receiving out-of-order events from backfill and such. + const upToEventId = this.readUpToMap[ev.getRoomId()]; + if (upToEventId) { + // Small optimization for exact match (prevent search) + if (upToEventId === ev.getId()) { + return; + } + + let isBeforeMark = true; + + // Timelines are most recent last, so reverse the order and limit ourselves to 100 events + // to avoid overusing the CPU. + const timeline = MatrixClientPeg.get().getRoom(ev.getRoomId()).getLiveTimeline(); + const events = arrayFastClone(timeline.getEvents()).reverse().slice(0, 100); + + for (const timelineEvent of events) { + if (timelineEvent.getId() === upToEventId) { + break; + } else if (timelineEvent.getId() === ev.getId()) { + isBeforeMark = false; + break; + } + } + + if (isBeforeMark) { + // Ignore the event: it is before our interest. + return; + } + } + + this.readUpToMap[ev.getRoomId()] = ev.getId(); + const raw = ev.getEffectiveEvent(); this.messaging.feedEvent(raw).catch(e => { console.error("Error sending event to widget: ", e); From 816f0f5e9027b91442b844f28a32cdcb0346f9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Fri, 27 Aug 2021 09:56:53 +0200 Subject: [PATCH 19/50] Avoid stacked dialogs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/spaces/SpacePublicShare.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/spaces/SpacePublicShare.tsx b/src/components/views/spaces/SpacePublicShare.tsx index 39e5115e55..cb282d8d1f 100644 --- a/src/components/views/spaces/SpacePublicShare.tsx +++ b/src/components/views/spaces/SpacePublicShare.tsx @@ -54,8 +54,8 @@ const SpacePublicShare = ({ space, onFinished }: IProps) => { { space.canInvite(MatrixClientPeg.get()?.getUserId()) ? { - showRoomInviteDialog(space.roomId); if (onFinished) onFinished(); + showRoomInviteDialog(space.roomId); }} >

{ _t("Invite people") }

From bf3c8e56646f0c394327984339e72cd4e016f6a2 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 27 Aug 2021 10:38:01 +0100 Subject: [PATCH 20/50] Fix imports --- src/components/structures/ThreadView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 951c821d5c..a2595debc8 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -23,7 +23,7 @@ import { RightPanelPhases } from "../../stores/RightPanelStorePhases"; import { replaceableComponent } from "../../utils/replaceableComponent"; import ResizeNotifier from '../../utils/ResizeNotifier'; -import EventTile, { TileShape } from '../views/rooms/EventTile'; +import { TileShape } from '../views/rooms/EventTile'; import MessageComposer from '../views/rooms/MessageComposer'; import { RoomPermalinkCreator } from '../../utils/permalinks/Permalinks'; import { Layout } from '../../settings/Layout'; From 6e30673164b41ecd2597e7d288930b2e17801364 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Fri, 27 Aug 2021 19:23:26 +0200 Subject: [PATCH 21/50] Show spinner if user id is missing in event tile preview --- src/components/views/elements/EventTilePreview.tsx | 3 +++ .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 68a70133e6..d1386b63f4 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -25,6 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { Layout } from "../../../settings/Layout"; import { UIFeature } from "../../../settings/UIFeature"; import { replaceableComponent } from "../../../utils/replaceableComponent"; +import InlineSpinner from './InlineSpinner'; interface IProps { /** @@ -118,6 +119,8 @@ export default class EventTilePreview extends React.Component { } public render() { + if (!this.props.userId) return
; + const event = this.fakeEvent(this.state); const className = classnames(this.props.className, { diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index cbf0b7916c..47c0f712bd 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -92,8 +92,8 @@ export default class AppearanceUserSettingsTab extends React.Component Date: Fri, 27 Aug 2021 19:32:38 +0200 Subject: [PATCH 22/50] Update props and state interfaces --- src/components/views/elements/EventTilePreview.tsx | 2 +- src/components/views/settings/LayoutSwitcher.tsx | 2 +- .../views/settings/tabs/user/AppearanceUserSettingsTab.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index d1386b63f4..4e03508305 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -46,7 +46,7 @@ interface IProps { /** * The ID of the displayed user */ - userId: string; + userId?: string; /** * The display name of the displayed user diff --git a/src/components/views/settings/LayoutSwitcher.tsx b/src/components/views/settings/LayoutSwitcher.tsx index dd7accf9a8..ad8abd0033 100644 --- a/src/components/views/settings/LayoutSwitcher.tsx +++ b/src/components/views/settings/LayoutSwitcher.tsx @@ -26,7 +26,7 @@ import { Layout } from "../../../settings/Layout"; import { SettingLevel } from "../../../settings/SettingLevel"; interface IProps { - userId: string; + userId?: string; displayName: string; avatarUrl: string; messagePreviewText: string; diff --git a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx index 47c0f712bd..bc54a8155c 100644 --- a/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx +++ b/src/components/views/settings/tabs/user/AppearanceUserSettingsTab.tsx @@ -67,7 +67,7 @@ interface IState extends IThemeState { showAdvanced: boolean; layout: Layout; // User profile data for the message preview - userId: string; + userId?: string; displayName: string; avatarUrl: string; } From 9b495eeec32b22f26129bcc29ab411804150bf39 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Fri, 27 Aug 2021 20:10:01 +0200 Subject: [PATCH 23/50] Optimized style --- src/components/views/elements/EventTilePreview.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 4e03508305..3b4351f5e8 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -119,15 +119,15 @@ export default class EventTilePreview extends React.Component { } public render() { - if (!this.props.userId) return
; - - const event = this.fakeEvent(this.state); - const className = classnames(this.props.className, { "mx_IRCLayout": this.props.layout == Layout.IRC, "mx_GroupLayout": this.props.layout == Layout.Group, }); + if (!this.props.userId) return
; + + const event = this.fakeEvent(this.state); + return
Date: Fri, 27 Aug 2021 20:11:34 +0200 Subject: [PATCH 24/50] Fixed padding --- src/components/views/elements/EventTilePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 3b4351f5e8..a433bd4b53 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -124,7 +124,7 @@ export default class EventTilePreview extends React.Component { "mx_GroupLayout": this.props.layout == Layout.Group, }); - if (!this.props.userId) return
; + if (!this.props.userId) return
; const event = this.fakeEvent(this.state); From f65eff103e34966012c13dbe61a214c36628a64a Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Fri, 27 Aug 2021 20:48:29 +0200 Subject: [PATCH 25/50] Switch from inline-spinner to spinner --- src/components/views/elements/EventTilePreview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index a433bd4b53..5bea8a0706 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -25,7 +25,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import { Layout } from "../../../settings/Layout"; import { UIFeature } from "../../../settings/UIFeature"; import { replaceableComponent } from "../../../utils/replaceableComponent"; -import InlineSpinner from './InlineSpinner'; +import Spinner from './Spinner'; interface IProps { /** @@ -124,7 +124,7 @@ export default class EventTilePreview extends React.Component { "mx_GroupLayout": this.props.layout == Layout.Group, }); - if (!this.props.userId) return
; + if (!this.props.userId) return
; const event = this.fakeEvent(this.state); From 6da11f375dfb4c057a87ca50ca9acb04e6c60a59 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Fri, 27 Aug 2021 21:44:34 +0200 Subject: [PATCH 26/50] Allow to use basic html formatting in invite reasons --- src/components/views/elements/InviteReason.tsx | 4 +++- src/components/views/rooms/RoomPreviewBar.js | 7 ++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/views/elements/InviteReason.tsx b/src/components/views/elements/InviteReason.tsx index dff5c7d6bd..865a5be747 100644 --- a/src/components/views/elements/InviteReason.tsx +++ b/src/components/views/elements/InviteReason.tsx @@ -16,11 +16,13 @@ limitations under the License. import classNames from "classnames"; import React from "react"; +import { sanitizedHtmlNode } from "../../../HtmlUtils"; import { _t } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; interface IProps { reason: string; + htmlReason?: string; } interface IState { @@ -51,7 +53,7 @@ export default class InviteReason extends React.PureComponent { }); return
-
{ this.props.reason }
+
{ this.props.htmlReason ? sanitizedHtmlNode(this.props.htmlReason) : this.props.reason }
diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index b8a4315e2d..4358a2351f 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -492,9 +492,10 @@ export default class RoomPreviewBar extends React.Component { } const myUserId = MatrixClientPeg.get().getUserId(); - const reason = this.props.room.currentState.getMember(myUserId).events.member.event.content.reason; - if (reason) { - reasonElement = ; + const memberEventContent = this.props.room.currentState.getMember(myUserId).events.member.event.content; + + if (memberEventContent.reason) { + reasonElement = ; } primaryActionHandler = this.props.onJoinClick; From 70b4308bab3173a3ed3ee0b164c158dbfd0beba6 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Fri, 27 Aug 2021 21:59:22 +0200 Subject: [PATCH 27/50] Fix eslint error --- src/components/views/rooms/RoomPreviewBar.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index 4358a2351f..c73c066338 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -495,7 +495,10 @@ export default class RoomPreviewBar extends React.Component { const memberEventContent = this.props.room.currentState.getMember(myUserId).events.member.event.content; if (memberEventContent.reason) { - reasonElement = ; + reasonElement = ; } primaryActionHandler = this.props.onJoinClick; From d76adde1aba4fc6709dec5bb37a8db135a6d482e Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Sat, 28 Aug 2021 13:41:24 +0200 Subject: [PATCH 28/50] Use scss instead of inline styles --- res/css/_components.scss | 1 + res/css/views/elements/_EventTilePreview.scss | 22 +++++++++++++++++++ .../views/elements/EventTilePreview.tsx | 2 +- 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 res/css/views/elements/_EventTilePreview.scss diff --git a/res/css/_components.scss b/res/css/_components.scss index 566b84a7c8..ffaec43b68 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -132,6 +132,7 @@ @import "./views/elements/_EditableItemList.scss"; @import "./views/elements/_ErrorBoundary.scss"; @import "./views/elements/_EventListSummary.scss"; +@import "./views/elements/_EventTilePreview.scss"; @import "./views/elements/_FacePile.scss"; @import "./views/elements/_Field.scss"; @import "./views/elements/_ImageView.scss"; diff --git a/res/css/views/elements/_EventTilePreview.scss b/res/css/views/elements/_EventTilePreview.scss new file mode 100644 index 0000000000..a8c0deabbe --- /dev/null +++ b/res/css/views/elements/_EventTilePreview.scss @@ -0,0 +1,22 @@ +/* +Copyright 2016 OpenMarket Ltd + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.mx_EventTilePreview_loader { + &.mx_IRCLayout, + &.mx_GroupLayout { + padding: 9px 0; + } +} diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 5bea8a0706..183ac09268 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -124,7 +124,7 @@ export default class EventTilePreview extends React.Component { "mx_GroupLayout": this.props.layout == Layout.Group, }); - if (!this.props.userId) return
; + if (!this.props.userId) return
; const event = this.fakeEvent(this.state); From e661c017e3e5498af5f70119db70d0fa00c57f21 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Sat, 28 Aug 2021 13:48:33 +0200 Subject: [PATCH 29/50] Moved custom field name to a const --- src/components/views/rooms/RoomPreviewBar.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index c73c066338..f3201d2a3d 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -28,6 +28,8 @@ import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import InviteReason from "../elements/InviteReason"; +const MemberEventHtmlReasonField = "io.element.html_reason"; + const MessageCase = Object.freeze({ NotLoggedIn: "NotLoggedIn", Joining: "Joining", @@ -497,7 +499,7 @@ export default class RoomPreviewBar extends React.Component { if (memberEventContent.reason) { reasonElement = ; } From 9fdc5659b7df4bbb8703f8144cc08aeb043d1c09 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Sat, 28 Aug 2021 13:56:08 +0200 Subject: [PATCH 30/50] Fixed eslint error and copyright header --- res/css/views/elements/_EventTilePreview.scss | 2 +- src/components/views/elements/EventTilePreview.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/res/css/views/elements/_EventTilePreview.scss b/res/css/views/elements/_EventTilePreview.scss index a8c0deabbe..6bb726168f 100644 --- a/res/css/views/elements/_EventTilePreview.scss +++ b/res/css/views/elements/_EventTilePreview.scss @@ -1,5 +1,5 @@ /* -Copyright 2016 OpenMarket 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. diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 183ac09268..8928c45fc8 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -124,7 +124,11 @@ export default class EventTilePreview extends React.Component { "mx_GroupLayout": this.props.layout == Layout.Group, }); - if (!this.props.userId) return
; + if (!this.props.userId) { + return
+ +
; + } const event = this.fakeEvent(this.state); From 3ef9584f688d0df87b40810b626213a4a2b1078e Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Sat, 28 Aug 2021 14:00:18 +0200 Subject: [PATCH 31/50] Simplify things --- src/components/views/elements/EventTilePreview.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index 8928c45fc8..bea060fcca 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -122,13 +122,10 @@ export default class EventTilePreview extends React.Component { const className = classnames(this.props.className, { "mx_IRCLayout": this.props.layout == Layout.IRC, "mx_GroupLayout": this.props.layout == Layout.Group, + "mx_EventTilePreview_loader": !this.props.userId }); - if (!this.props.userId) { - return
- -
; - } + if (!this.props.userId) return
; const event = this.fakeEvent(this.state); From 6a550f2f9c01035fc01e2e0fe2e191285d986285 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Sat, 28 Aug 2021 14:04:00 +0200 Subject: [PATCH 32/50] Added trailing comma to make eslint happy --- src/components/views/elements/EventTilePreview.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/elements/EventTilePreview.tsx b/src/components/views/elements/EventTilePreview.tsx index bea060fcca..a7ebf40c3a 100644 --- a/src/components/views/elements/EventTilePreview.tsx +++ b/src/components/views/elements/EventTilePreview.tsx @@ -122,7 +122,7 @@ export default class EventTilePreview extends React.Component { const className = classnames(this.props.className, { "mx_IRCLayout": this.props.layout == Layout.IRC, "mx_GroupLayout": this.props.layout == Layout.Group, - "mx_EventTilePreview_loader": !this.props.userId + "mx_EventTilePreview_loader": !this.props.userId, }); if (!this.props.userId) return
; From 5b65528038b97c6c70b06e1a7a1474fd54f2075d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Sun, 29 Aug 2021 13:42:12 +0200 Subject: [PATCH 33/50] Make GH actions clone from the correct fork and cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- .github/workflows/develop.yml | 2 ++ scripts/fetchdep.sh | 60 ++++++++++++++++------------------- 2 files changed, 29 insertions(+), 33 deletions(-) diff --git a/.github/workflows/develop.yml b/.github/workflows/develop.yml index 0ae59da09a..4f9826391a 100644 --- a/.github/workflows/develop.yml +++ b/.github/workflows/develop.yml @@ -10,6 +10,8 @@ on: jobs: end-to-end: runs-on: ubuntu-latest + env: + PR_NUMBER: ${{github.event.number}} container: vectorim/element-web-ci-e2etests-env:latest steps: - name: Checkout code diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 0990af70ce..97be9c2414 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -10,6 +10,7 @@ defbranch="$3" rm -r "$defrepo" || true +# A fnction that clones a branch of a repo based on the org, repo and branch clone() { org=$1 repo=$2 @@ -21,45 +22,38 @@ clone() { fi } -# Try the PR author's branch in case it exists on the deps as well. -# First we check if GITHUB_HEAD_REF is defined, -# Then we check if BUILDKITE_BRANCH is defined, -# if they aren't we can assume this is a Netlify build -if [ -n "$GITHUB_HEAD_REF" ]; then - head=$GITHUB_HEAD_REF -elif [ -n "$BUILDKITE_BRANCH" ]; then - head=$BUILDKITE_BRANCH -else - # Netlify doesn't give us info about the fork so we have to get it from GitHub API - apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" - apiEndpoint+=$REVIEW_ID - head=$(curl $apiEndpoint | jq -r '.head.label') -fi +# A function that gets info about a PR from the GitHub API based on its number +getPRInfo() { + number=$1 + if [ -n "$number" ]; then + echo "Getting info about a PR with number $number" -# If head is set, it will contain on Buildkite either: -# * "branch" when the author's branch and target branch are in the same repo -# * "fork:branch" when the author's branch is in their fork or if this is a Netlify build -# We can split on `:` into an array to check. -# For GitHub Actions we need to inspect GITHUB_REPOSITORY and GITHUB_ACTOR -# to determine whether the branch is from a fork or not -BRANCH_ARRAY=(${head//:/ }) -if [[ "${#BRANCH_ARRAY[@]}" == "1" ]]; then + apiEndpoint="https://api.github.com/repos/matrix-org/matrix-react-sdk/pulls/" + apiEndpoint+=$number - if [ -n "$GITHUB_HEAD_REF" ]; then - if [[ "$GITHUB_REPOSITORY" == "$deforg"* ]]; then - clone $deforg $defrepo $GITHUB_HEAD_REF - else - REPO_ARRAY=(${GITHUB_REPOSITORY//\// }) - clone $REPO_ARRAY[0] $defrepo $GITHUB_HEAD_REF - fi - else - clone $deforg $defrepo $BUILDKITE_BRANCH + head=$(curl $apiEndpoint | jq -r '.head.label') fi +} -elif [[ "${#BRANCH_ARRAY[@]}" == "2" ]]; then - clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} +# Some CIs don't give us enough info, so we just get the PR number and ask the +# GH API for more info - "fork:branch". Some give us this directly. +if [ -n "$BUILDKITE_BRANCH" ]; then + # BuildKite + head=$BUILDKITE_BRANCH +elif [ -n "$PR_NUMBER" ]; then + # GitHub + getPRInfo $PR_NUMBER +elif [ -n "$REVIEW_ID" ]; then + # Netlify + getPRInfo $REVIEW_ID fi +# $head will always be in the format "fork:branch", so we split it by ":" into +# an array. The first element will then be the fork and the second the branch. +# Based on that we clone +BRANCH_ARRAY=(${head//:/ }) +clone ${BRANCH_ARRAY[0]} $defrepo ${BRANCH_ARRAY[1]} + # Try the target branch of the push or PR. if [ -n $GITHUB_BASE_REF ]; then clone $deforg $defrepo $GITHUB_BASE_REF From b814100b98c342af64c7aef3f153388e813ec0cc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Aug 2021 16:31:10 +0100 Subject: [PATCH 34/50] Fix membership updates to Spaces not applying in real-time --- src/stores/SpaceStore.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index c08c66714b..c1e86504f4 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -678,12 +678,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } this.emit(room.roomId); break; + } + }; - case EventType.RoomMember: - if (room.isSpaceRoom()) { - this.onSpaceMembersChange(ev); - } - break; + // listening for m.room.member events in onRoomState above doesn't work as the Member object isn't updated by then + private onRoomStateMembers = (ev: MatrixEvent) => { + const room = this.matrixClient.getRoom(ev.getRoomId()); + if (room?.isSpaceRoom()) { + this.onSpaceMembersChange(ev); } }; @@ -743,6 +745,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.removeListener("Room.myMembership", this.onRoom); this.matrixClient.removeListener("Room.accountData", this.onRoomAccountData); this.matrixClient.removeListener("RoomState.events", this.onRoomState); + this.matrixClient.removeListener("RoomState.members", this.onRoomStateMembers); this.matrixClient.removeListener("accountData", this.onAccountData); } await this.reset(); @@ -754,6 +757,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.on("Room.myMembership", this.onRoom); this.matrixClient.on("Room.accountData", this.onRoomAccountData); this.matrixClient.on("RoomState.events", this.onRoomState); + this.matrixClient.on("RoomState.members", this.onRoomStateMembers); this.matrixClient.on("accountData", this.onAccountData); this.matrixClient.getCapabilities().then(capabilities => { From bf4ffa965c5cf2bb720c6a8001b30333f3cd829b Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Aug 2021 16:56:47 +0100 Subject: [PATCH 35/50] Reload suggested rooms if we see the state change down /sync --- src/stores/SpaceStore.tsx | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index c08c66714b..471e10d9d6 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -190,7 +190,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { * @param contextSwitch whether to switch the user's context, * 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) { + public setActiveSpace(space: Room | null, contextSwitch = true) { if (space === this.activeSpace || (space && !space.isSpaceRoom())) return; this._activeSpace = space; @@ -293,11 +293,15 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } if (space) { - const suggestedRooms = await this.fetchSuggestedRooms(space); - if (this._activeSpace === space) { - this._suggestedRooms = suggestedRooms; - this.emit(SUGGESTED_ROOMS, this._suggestedRooms); - } + this.loadSuggestedRooms(space); + } + } + + private async loadSuggestedRooms(space) { + const suggestedRooms = await this.fetchSuggestedRooms(space); + if (this._activeSpace === space) { + this._suggestedRooms = suggestedRooms; + this.emit(SUGGESTED_ROOMS, this._suggestedRooms); } } @@ -666,6 +670,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.onSpaceUpdate(); this.emit(room.roomId); } + + if (room === this.activeSpace && // current space + this.matrixClient.getRoom(ev.getStateKey())?.getMyMembership() !== "join" && // target not joined + ev.getPrevContent().suggested !== ev.getContent().suggested // suggested flag changed + ) { + this.loadSuggestedRooms(room); + } + break; case EventType.SpaceParent: From bbd420096b8807cdf2119c908f51b3f27dea10df Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Aug 2021 17:03:43 +0100 Subject: [PATCH 36/50] don't bother awaiting SpaceStore::setActiveSpace as it is no longer async --- src/stores/SpaceStore.tsx | 4 +- test/stores/SpaceStore-test.ts | 58 +++++++++++----------- test/stores/room-list/SpaceWatcher-test.ts | 22 ++++---- 3 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/stores/SpaceStore.tsx b/src/stores/SpaceStore.tsx index 471e10d9d6..5c921d6ad7 100644 --- a/src/stores/SpaceStore.tsx +++ b/src/stores/SpaceStore.tsx @@ -145,9 +145,9 @@ export class SpaceStoreClass extends AsyncStoreWithClient { return this._allRoomsInHome; } - public async setActiveRoomInSpace(space: Room | null): Promise { + public setActiveRoomInSpace(space: Room | null): void { if (space && !space.isSpaceRoom()) return; - if (space !== this.activeSpace) await this.setActiveSpace(space); + if (space !== this.activeSpace) this.setActiveSpace(space); if (space) { const roomId = this.getNotificationState(space.roomId).getFirstRoomWithNotifications(); diff --git a/test/stores/SpaceStore-test.ts b/test/stores/SpaceStore-test.ts index 2e823aa72b..7cfd97b234 100644 --- a/test/stores/SpaceStore-test.ts +++ b/test/stores/SpaceStore-test.ts @@ -562,7 +562,7 @@ describe("SpaceStore", () => { ]); mkSpace(space3).getMyMembership.mockReturnValue("invite"); await run(); - await store.setActiveSpace(null); + store.setActiveSpace(null); expect(store.activeSpace).toBe(null); }); afterEach(() => { @@ -570,31 +570,31 @@ describe("SpaceStore", () => { }); it("switch to home space", async () => { - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); fn.mockClear(); - await store.setActiveSpace(null); + store.setActiveSpace(null); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, null); expect(store.activeSpace).toBe(null); }); it("switch to invited space", async () => { const space = client.getRoom(space3); - await store.setActiveSpace(space); + store.setActiveSpace(space); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); expect(store.activeSpace).toBe(space); }); it("switch to top level space", async () => { const space = client.getRoom(space1); - await store.setActiveSpace(space); + store.setActiveSpace(space); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); expect(store.activeSpace).toBe(space); }); it("switch to subspace", async () => { const space = client.getRoom(space2); - await store.setActiveSpace(space); + store.setActiveSpace(space); expect(fn).toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); expect(store.activeSpace).toBe(space); }); @@ -602,7 +602,7 @@ describe("SpaceStore", () => { it("switch to unknown space is a nop", async () => { expect(store.activeSpace).toBe(null); const space = client.getRoom(room1); // not a space - await store.setActiveSpace(space); + store.setActiveSpace(space); expect(fn).not.toHaveBeenCalledWith(UPDATE_SELECTED_SPACE, space); expect(store.activeSpace).toBe(null); }); @@ -635,59 +635,59 @@ describe("SpaceStore", () => { }; it("last viewed room in target space is the current viewed and in both spaces", async () => { - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); viewRoom(room2); - await store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(client.getRoom(space2)); viewRoom(room2); - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); expect(getCurrentRoom()).toBe(room2); }); it("last viewed room in target space is in the current space", async () => { - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); viewRoom(room2); - await store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(client.getRoom(space2)); expect(getCurrentRoom()).toBe(space2); - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); expect(getCurrentRoom()).toBe(room2); }); it("last viewed room in target space is not in the current space", async () => { - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); - await store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(client.getRoom(space2)); viewRoom(room2); - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); expect(getCurrentRoom()).toBe(room1); }); it("last viewed room is target space is not known", async () => { - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); localStorage.setItem(`mx_space_context_${space2}`, orphan2); - await store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(client.getRoom(space2)); expect(getCurrentRoom()).toBe(space2); }); it("last viewed room is target space is no longer in that space", async () => { - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); localStorage.setItem(`mx_space_context_${space2}`, room1); - await store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(client.getRoom(space2)); expect(getCurrentRoom()).toBe(space2); // Space home instead of room1 }); it("no last viewed room in target space", async () => { - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); - await store.setActiveSpace(client.getRoom(space2)); + store.setActiveSpace(client.getRoom(space2)); expect(getCurrentRoom()).toBe(space2); }); it("no last viewed room in home space", async () => { - await store.setActiveSpace(client.getRoom(space1)); + store.setActiveSpace(client.getRoom(space1)); viewRoom(room1); - await store.setActiveSpace(null); + store.setActiveSpace(null); expect(getCurrentRoom()).toBeNull(); // Home }); }); @@ -715,28 +715,28 @@ describe("SpaceStore", () => { it("no switch required, room is in current space", async () => { viewRoom(room1); - await store.setActiveSpace(client.getRoom(space1), false); + store.setActiveSpace(client.getRoom(space1), false); viewRoom(room2); expect(store.activeSpace).toBe(client.getRoom(space1)); }); it("switch to canonical parent space for room", async () => { viewRoom(room1); - await store.setActiveSpace(client.getRoom(space2), false); + store.setActiveSpace(client.getRoom(space2), false); viewRoom(room2); expect(store.activeSpace).toBe(client.getRoom(space2)); }); it("switch to first containing space for room", async () => { viewRoom(room2); - await store.setActiveSpace(client.getRoom(space2), false); + store.setActiveSpace(client.getRoom(space2), false); viewRoom(room3); expect(store.activeSpace).toBe(client.getRoom(space1)); }); it("switch to home for orphaned room", async () => { viewRoom(room1); - await store.setActiveSpace(client.getRoom(space1), false); + store.setActiveSpace(client.getRoom(space1), false); viewRoom(orphan1); expect(store.activeSpace).toBeNull(); }); @@ -744,7 +744,7 @@ describe("SpaceStore", () => { it("when switching rooms in the all rooms home space don't switch to related space", async () => { await setShowAllRooms(true); viewRoom(room2); - await store.setActiveSpace(null, false); + store.setActiveSpace(null, false); viewRoom(room1); expect(store.activeSpace).toBeNull(); }); diff --git a/test/stores/room-list/SpaceWatcher-test.ts b/test/stores/room-list/SpaceWatcher-test.ts index 85f79c75b6..474c279fdd 100644 --- a/test/stores/room-list/SpaceWatcher-test.ts +++ b/test/stores/room-list/SpaceWatcher-test.ts @@ -57,7 +57,7 @@ describe("SpaceWatcher", () => { beforeEach(async () => { filter = null; store.removeAllListeners(); - await store.setActiveSpace(null); + store.setActiveSpace(null); client.getVisibleRooms.mockReturnValue(rooms = []); space1 = mkSpace(space1Id); @@ -95,7 +95,7 @@ describe("SpaceWatcher", () => { await setShowAllRooms(true); new SpaceWatcher(mockRoomListStore); - await SpaceStore.instance.setActiveSpace(space1); + SpaceStore.instance.setActiveSpace(space1); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(space1); @@ -114,7 +114,7 @@ describe("SpaceWatcher", () => { await setShowAllRooms(false); new SpaceWatcher(mockRoomListStore); - await SpaceStore.instance.setActiveSpace(space1); + SpaceStore.instance.setActiveSpace(space1); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(space1); @@ -124,22 +124,22 @@ describe("SpaceWatcher", () => { await setShowAllRooms(true); new SpaceWatcher(mockRoomListStore); - await SpaceStore.instance.setActiveSpace(space1); + SpaceStore.instance.setActiveSpace(space1); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(space1); - await SpaceStore.instance.setActiveSpace(null); + SpaceStore.instance.setActiveSpace(null); expect(filter).toBeNull(); }); it("updates filter correctly for space -> home transition", async () => { await setShowAllRooms(false); - await SpaceStore.instance.setActiveSpace(space1); + SpaceStore.instance.setActiveSpace(space1); new SpaceWatcher(mockRoomListStore); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(space1); - await SpaceStore.instance.setActiveSpace(null); + SpaceStore.instance.setActiveSpace(null); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(null); @@ -147,12 +147,12 @@ describe("SpaceWatcher", () => { it("updates filter correctly for space -> space transition", async () => { await setShowAllRooms(false); - await SpaceStore.instance.setActiveSpace(space1); + SpaceStore.instance.setActiveSpace(space1); new SpaceWatcher(mockRoomListStore); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(space1); - await SpaceStore.instance.setActiveSpace(space2); + SpaceStore.instance.setActiveSpace(space2); expect(filter).toBeInstanceOf(SpaceFilterCondition); expect(filter["space"]).toBe(space2); @@ -160,7 +160,7 @@ describe("SpaceWatcher", () => { it("doesn't change filter when changing showAllRooms mode to true", async () => { await setShowAllRooms(false); - await SpaceStore.instance.setActiveSpace(space1); + SpaceStore.instance.setActiveSpace(space1); new SpaceWatcher(mockRoomListStore); expect(filter).toBeInstanceOf(SpaceFilterCondition); @@ -173,7 +173,7 @@ describe("SpaceWatcher", () => { it("doesn't change filter when changing showAllRooms mode to false", async () => { await setShowAllRooms(true); - await SpaceStore.instance.setActiveSpace(space1); + SpaceStore.instance.setActiveSpace(space1); new SpaceWatcher(mockRoomListStore); expect(filter).toBeInstanceOf(SpaceFilterCondition); From 4ce79148cf8708708972ec6ee9830038b99d9d47 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Aug 2021 17:14:50 +0100 Subject: [PATCH 37/50] Clear currentRoomId when viewing home page, fixing document title --- src/components/structures/MatrixChat.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 902d2a0921..531dc9fbe9 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -1016,6 +1016,7 @@ export default class MatrixChat extends React.PureComponent { this.setStateForNewView({ view: Views.LOGGED_IN, justRegistered, + currentRoomId: null, }); this.setPage(PageTypes.HomePage); this.notifyNewScreen('home'); From e089c34db72f129e04281dcae132cf62bbbc3baa Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 31 Aug 2021 17:36:10 +0100 Subject: [PATCH 38/50] Fix EmojiPicker filtering to lower case emojibase data strings --- src/components/views/emojipicker/EmojiPicker.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/components/views/emojipicker/EmojiPicker.tsx b/src/components/views/emojipicker/EmojiPicker.tsx index 0884db7101..9cc995a140 100644 --- a/src/components/views/emojipicker/EmojiPicker.tsx +++ b/src/components/views/emojipicker/EmojiPicker.tsx @@ -173,16 +173,16 @@ class EmojiPicker extends React.Component { }; private onChangeFilter = (filter: string) => { - filter = filter.toLowerCase(); // filter is case insensitive stored lower-case + const lcFilter = filter.toLowerCase().trim(); // filter is case insensitive for (const cat of this.categories) { let emojis; // If the new filter string includes the old filter string, we don't have to re-filter the whole dataset. - if (filter.includes(this.state.filter)) { + if (lcFilter.includes(this.state.filter)) { emojis = this.memoizedDataByCategory[cat.id]; } else { emojis = cat.id === "recent" ? this.recentlyUsed : DATA_BY_CATEGORY[cat.id]; } - emojis = emojis.filter(emoji => this.emojiMatchesFilter(emoji, filter)); + emojis = emojis.filter(emoji => this.emojiMatchesFilter(emoji, lcFilter)); this.memoizedDataByCategory[cat.id] = emojis; cat.enabled = emojis.length > 0; // The setState below doesn't re-render the header and we already have the refs for updateVisibility, so... @@ -194,9 +194,12 @@ class EmojiPicker extends React.Component { setTimeout(this.updateVisibility, 0); }; - private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean => - [emoji.annotation, ...emoji.shortcodes, emoji.emoticon, ...emoji.unicode.split(ZERO_WIDTH_JOINER)] - .some(x => x?.includes(filter)); + private emojiMatchesFilter = (emoji: IEmoji, filter: string): boolean => { + return emoji.annotation.toLowerCase().includes(filter) || + emoji.emoticon?.toLowerCase().includes(filter) || + emoji.shortcodes.some(x => x.toLowerCase().includes(filter)) || + emoji.unicode.split(ZERO_WIDTH_JOINER).includes(filter); + }; private onEnterFilter = () => { const btn = this.bodyRef.current.querySelector(".mx_EmojiPicker_item"); From 18b7a649104656799554cd50a472dd78f9d6b2ad Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Tue, 31 Aug 2021 13:17:49 -0600 Subject: [PATCH 39/50] Remove arbitrary limits from send/receive events for widgets Fixes https://github.com/vector-im/element-web/issues/17994 As per MSC change: https://github.com/matrix-org/matrix-doc/pull/2762/commits/aeadae81e2d68f76523eb61ff0ebbbd5c3202deb --- src/stores/widgets/StopGapWidgetDriver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/stores/widgets/StopGapWidgetDriver.ts b/src/stores/widgets/StopGapWidgetDriver.ts index d473ecf3b1..c0f627b9c0 100644 --- a/src/stores/widgets/StopGapWidgetDriver.ts +++ b/src/stores/widgets/StopGapWidgetDriver.ts @@ -163,7 +163,7 @@ export class StopGapWidgetDriver extends WidgetDriver { } public async readRoomEvents(eventType: string, msgtype: string | undefined, limit: number): Promise { - limit = limit > 0 ? Math.min(limit, 25) : 25; // arbitrary choice + limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary const client = MatrixClientPeg.get(); const roomId = ActiveRoomObserver.activeRoomId; @@ -185,7 +185,7 @@ export class StopGapWidgetDriver extends WidgetDriver { } public async readStateEvents(eventType: string, stateKey: string | undefined, limit: number): Promise { - limit = limit > 0 ? Math.min(limit, 100) : 100; // arbitrary choice + limit = limit > 0 ? Math.min(limit, Number.MAX_SAFE_INTEGER) : Number.MAX_SAFE_INTEGER; // relatively arbitrary const client = MatrixClientPeg.get(); const roomId = ActiveRoomObserver.activeRoomId; From 855c3819c1a24f83901398b21e676b364940d74b Mon Sep 17 00:00:00 2001 From: David Baker Date: Wed, 1 Sep 2021 09:52:32 +0100 Subject: [PATCH 40/50] Reset matrix-js-sdk back to develop branch --- package.json | 2 +- yarn.lock | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 2f59ba4504..5c62100587 100644 --- a/package.json +++ b/package.json @@ -82,7 +82,7 @@ "katex": "^0.12.0", "linkifyjs": "^2.1.9", "lodash": "^4.17.20", - "matrix-js-sdk": "12.4.0", + "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", "matrix-widget-api": "^0.1.0-beta.15", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", diff --git a/yarn.lock b/yarn.lock index 6dd1d269c3..4e58909563 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5791,10 +5791,9 @@ mathml-tag-names@^2.1.3: resolved "https://registry.yarnpkg.com/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz#4ddadd67308e780cf16a47685878ee27b736a0a3" integrity sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg== -matrix-js-sdk@12.4.0: +"matrix-js-sdk@github:matrix-org/matrix-js-sdk#develop": version "12.4.0" - resolved "https://registry.yarnpkg.com/matrix-js-sdk/-/matrix-js-sdk-12.4.0.tgz#ff60306f9a9e39fd1ae6c7e501001f80eb779dd7" - integrity sha512-KamHmvNle4mkdErmNgVsGIL3n8/zgPe60DLVaEA2t4aSNwQLEmRS+oVpIgsO3ZrUivBvn4oc9sBqxP0OIl6kUg== + resolved "https://codeload.github.com/matrix-org/matrix-js-sdk/tar.gz/2783d162b77d6629c574f35e88bea9ae29765c34" dependencies: "@babel/runtime" "^7.12.5" another-json "^0.2.0" From dca268e67a0c44ccd8659c8fa2ed40fa1b8efa34 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Wed, 1 Sep 2021 10:55:47 +0100 Subject: [PATCH 41/50] 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 42/50] 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 030fa17a6679d44bb0c6ea086ee20e6612e70903 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Wed, 1 Sep 2021 12:47:43 +0100 Subject: [PATCH 43/50] When creating private spaces, make the initial rooms restricted if supported --- src/components/structures/SpaceRoomView.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/structures/SpaceRoomView.tsx b/src/components/structures/SpaceRoomView.tsx index 767e0999c3..1ce5a53c3e 100644 --- a/src/components/structures/SpaceRoomView.tsx +++ b/src/components/structures/SpaceRoomView.tsx @@ -16,7 +16,7 @@ limitations under the License. import React, { RefObject, useContext, useRef, useState } from "react"; import { EventType } from "matrix-js-sdk/src/@types/event"; -import { Preset, JoinRule } from "matrix-js-sdk/src/@types/partials"; +import { JoinRule, Preset } from "matrix-js-sdk/src/@types/partials"; import { Room } from "matrix-js-sdk/src/models/room"; import { EventSubscription } from "fbemitter"; @@ -505,11 +505,12 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { setError(""); setBusy(true); try { + const isPublic = space.getJoinRule() === JoinRule.Public; const filteredRoomNames = roomNames.map(name => name.trim()).filter(Boolean); await Promise.all(filteredRoomNames.map(name => { return createRoom({ createOpts: { - preset: space.getJoinRule() === "public" ? Preset.PublicChat : Preset.PrivateChat, + preset: isPublic ? Preset.PublicChat : Preset.PrivateChat, name, }, spinner: false, @@ -517,6 +518,7 @@ const SpaceSetupFirstRooms = ({ space, title, description, onFinished }) => { andView: false, inlineErrors: true, parentSpace: space, + joinRule: !isPublic ? JoinRule.Restricted : undefined, }); })); onFinished(filteredRoomNames.length > 0); From 4328ee18f523404016ee9a89530a9d9c61954d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 1 Sep 2021 15:37:59 +0200 Subject: [PATCH 44/50] Show autocomplete sections vertically MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- res/css/views/rooms/_Autocomplete.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/res/css/views/rooms/_Autocomplete.scss b/res/css/views/rooms/_Autocomplete.scss index bebe0b47c9..8d2b338d9d 100644 --- a/res/css/views/rooms/_Autocomplete.scss +++ b/res/css/views/rooms/_Autocomplete.scss @@ -10,6 +10,7 @@ max-height: 35vh; overflow: clip; display: flex; + flex-direction: column; box-shadow: 0px -16px 32px $composer-shadow-color; } From 387239864d0685cb18495db682247b594d213498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=A0imon=20Brandner?= Date: Wed, 1 Sep 2021 16:50:13 +0200 Subject: [PATCH 45/50] Add fallbackUserId MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Šimon Brandner --- src/components/views/elements/ImageView.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/views/elements/ImageView.tsx b/src/components/views/elements/ImageView.tsx index 87f3dab718..7a1efb7a62 100644 --- a/src/components/views/elements/ImageView.tsx +++ b/src/components/views/elements/ImageView.tsx @@ -419,6 +419,7 @@ export default class ImageView extends React.Component { const avatar = ( Date: Wed, 1 Sep 2021 16:12:39 +0100 Subject: [PATCH 46/50] When creating subspaces properly set restricted join rule --- src/components/views/dialogs/CreateSubspaceDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/dialogs/CreateSubspaceDialog.tsx b/src/components/views/dialogs/CreateSubspaceDialog.tsx index 03927c7d62..d80245918f 100644 --- a/src/components/views/dialogs/CreateSubspaceDialog.tsx +++ b/src/components/views/dialogs/CreateSubspaceDialog.tsx @@ -79,7 +79,7 @@ const CreateSubspaceDialog: React.FC = ({ space, onAddExistingSpaceClick } try { - await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace }); + await createSpace(name, joinRule === JoinRule.Public, alias, topic, avatar, {}, { parentSpace, joinRule }); onFinished(true); } catch (e) { From 4b557fe0adcd9f72f144af6a5aa6a5f377a10cc7 Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Wed, 1 Sep 2021 13:22:09 -0600 Subject: [PATCH 47/50] Update widget-api --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 5c62100587..6245b2c34e 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "linkifyjs": "^2.1.9", "lodash": "^4.17.20", "matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop", - "matrix-widget-api": "^0.1.0-beta.15", + "matrix-widget-api": "^0.1.0-beta.16", "minimist": "^1.2.5", "opus-recorder": "^8.0.3", "pako": "^2.0.3", diff --git a/yarn.lock b/yarn.lock index 4e58909563..f70f0e75c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5827,10 +5827,10 @@ matrix-react-test-utils@^0.2.3: "@babel/traverse" "^7.13.17" walk "^2.3.14" -matrix-widget-api@^0.1.0-beta.15: - version "0.1.0-beta.15" - resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.15.tgz#b02511f93fe1a3634868b6e246d736107f182745" - integrity sha512-sWmtb8ZarSbHVbk5ni7IHBR9jOh7m1+5R4soky0fEO9VKl+MN7skT0+qNux3J9WuUAu2D80dZW9xPUT9cxfxbg== +matrix-widget-api@^0.1.0-beta.16: + version "0.1.0-beta.16" + resolved "https://registry.yarnpkg.com/matrix-widget-api/-/matrix-widget-api-0.1.0-beta.16.tgz#32655f05cab48239b97fe4111a1d0858f2aad61a" + integrity sha512-9zqaNLaM14YDHfFb7WGSUOivGOjYw+w5Su84ZfOl6A4IUy1xT9QPp0nsSA8wNfz0LpxOIPn3nuoF8Tn/40F5tg== dependencies: "@types/events" "^3.0.0" events "^3.2.0" From c32a77c3b2c8cff17b54f166b41ac78c46106f39 Mon Sep 17 00:00:00 2001 From: Steffen Kolmer Date: Wed, 1 Sep 2021 22:40:42 +0200 Subject: [PATCH 48/50] Use helper function to get event content --- src/components/views/rooms/RoomPreviewBar.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/views/rooms/RoomPreviewBar.js b/src/components/views/rooms/RoomPreviewBar.js index f3201d2a3d..89b493595f 100644 --- a/src/components/views/rooms/RoomPreviewBar.js +++ b/src/components/views/rooms/RoomPreviewBar.js @@ -494,7 +494,7 @@ export default class RoomPreviewBar extends React.Component { } const myUserId = MatrixClientPeg.get().getUserId(); - const memberEventContent = this.props.room.currentState.getMember(myUserId).events.member.event.content; + const memberEventContent = this.props.room.currentState.getMember(myUserId).events.member.getContent(); if (memberEventContent.reason) { reasonElement = Date: Wed, 1 Sep 2021 14:43:37 -0600 Subject: [PATCH 49/50] Update scripts/fetchdep.sh --- scripts/fetchdep.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/fetchdep.sh b/scripts/fetchdep.sh index 97be9c2414..ec021236d9 100755 --- a/scripts/fetchdep.sh +++ b/scripts/fetchdep.sh @@ -10,7 +10,7 @@ defbranch="$3" rm -r "$defrepo" || true -# A fnction that clones a branch of a repo based on the org, repo and branch +# A function that clones a branch of a repo based on the org, repo and branch clone() { org=$1 repo=$2 From 2ce86471206cba5f54985e63d9f9a16c91cc6d59 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 2 Sep 2021 08:36:20 +0100 Subject: [PATCH 50/50] 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(