From 3dcf0d0b7edf6a641d74baa7b4b102f6decb1694 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 15 Oct 2021 10:25:42 +0100 Subject: [PATCH 1/5] Fix incorrect logic to hide thread relations --- src/components/structures/MessagePanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/MessagePanel.tsx b/src/components/structures/MessagePanel.tsx index c1d22ecedf..79aeea8321 100644 --- a/src/components/structures/MessagePanel.tsx +++ b/src/components/structures/MessagePanel.tsx @@ -460,7 +460,7 @@ export default class MessagePanel extends React.Component { // Checking if the message has a "parentEventId" as we do not // want to hide the root event of the thread - if (mxEv.isThreadRoot && this.props.hideThreadedMessages + if (mxEv.isThreadRelation && this.props.hideThreadedMessages && SettingsStore.getValue("feature_thread")) { return false; } From 17c4cc91afaa0b41ceb8634039bc65add5e5e507 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 15 Oct 2021 10:26:04 +0100 Subject: [PATCH 2/5] Reconcile threads root with its children before displaying it --- src/components/views/rooms/EventTile.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 9d608c2833..c6798f1f15 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -531,8 +531,13 @@ export default class EventTile extends React.Component { return null; } - const thread = this.state.thread; const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + const thread = room.threads.get(this.props.mxEvent.getId()); + + if (thread && !thread.ready) { + thread.addEvent(this.props.mxEvent, true); + } + if (!thread || this.props.showThreadInfo === false || thread.length <= 1) { return null; } From 6baa6c8f65035633b86d4e7aec2e571b201c68ef Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 15 Oct 2021 11:08:33 +0100 Subject: [PATCH 3/5] Listen to new thread to render info on timeline --- src/components/structures/ThreadView.tsx | 15 ++++++++++++--- src/components/views/rooms/EventTile.tsx | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/components/structures/ThreadView.tsx b/src/components/structures/ThreadView.tsx index 3462212834..7bd6415cd3 100644 --- a/src/components/structures/ThreadView.tsx +++ b/src/components/structures/ThreadView.tsx @@ -48,10 +48,8 @@ interface IProps { } interface IState { - replyToEvent?: MatrixEvent; thread?: Thread; editState?: EditorStateTransfer; - } @replaceableComponent("structures.ThreadView") @@ -69,11 +67,16 @@ export default class ThreadView extends React.Component { public componentDidMount(): void { this.setupThread(this.props.mxEvent); this.dispatcherRef = dis.register(this.onAction); + + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + room.on(ThreadEvent.New, this.onNewThread); } public componentWillUnmount(): void { this.teardownThread(); dis.unregister(this.dispatcherRef); + const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); + room.on(ThreadEvent.New, this.onNewThread); } public componentDidUpdate(prevProps) { @@ -135,11 +138,17 @@ export default class ThreadView extends React.Component { } }; + private onNewThread = (thread: Thread) => { + if (thread.id === this.props.mxEvent.getId()) { + this.teardownThread(); + this.setupThread(this.props.mxEvent); + } + }; + private updateThread = (thread?: Thread) => { if (thread) { this.setState({ thread, - replyToEvent: thread.replyToEvent, }); } diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index c6798f1f15..9fd9a56730 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -475,6 +475,9 @@ export default class EventTile extends React.Component { this.props.mxEvent.once(ThreadEvent.Ready, this.updateThread); this.props.mxEvent.on(ThreadEvent.Update, this.updateThread); } + + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + room.on(ThreadEvent.New, this.onNewThread); } private updateThread = (thread) => { @@ -516,6 +519,9 @@ export default class EventTile extends React.Component { this.props.mxEvent.off(ThreadEvent.Ready, this.updateThread); this.props.mxEvent.off(ThreadEvent.Update, this.updateThread); } + + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + room.off(ThreadEvent.New, this.onNewThread); } componentDidUpdate(prevProps, prevState, snapshot) { @@ -526,6 +532,14 @@ export default class EventTile extends React.Component { } } + private onNewThread = (thread: Thread) => { + if (thread.id === this.props.mxEvent.getId()) { + this.updateThread(thread); + const room = this.context.getRoom(this.props.mxEvent.getRoomId()); + room.off(ThreadEvent.New, this.onNewThread); + } + }; + private renderThreadInfo(): React.ReactNode { if (!SettingsStore.getValue("feature_thread")) { return null; From f8c516d927ec7dd2b5f82bce5a334839ba7b0227 Mon Sep 17 00:00:00 2001 From: Dariusz Niemczyk <3636685+Palid@users.noreply.github.com> Date: Fri, 15 Oct 2021 15:29:17 +0200 Subject: [PATCH 4/5] Add new thread message preview (#18958) (#6953) Closes https://github.com/vector-im/element-web/issues/18958 --- res/css/views/rooms/_EventTile.scss | 51 ++++++++++++++++++- res/img/element-icons/thread-summary.svg | 1 + src/components/views/rooms/EventTile.tsx | 23 ++++++--- src/i18n/strings/en_EN.json | 2 + src/stores/room-list/MessagePreviewStore.ts | 10 ++++ .../room-list/previews/MessageEventPreview.ts | 4 +- .../previews/ReactionEventPreview.ts | 4 +- .../room-list/previews/StickerEventPreview.ts | 4 +- 8 files changed, 83 insertions(+), 16 deletions(-) create mode 100644 res/img/element-icons/thread-summary.svg diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 470851654b..74fc141b48 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -676,10 +676,57 @@ $hover-select-border: 4px; } } -.mx_ThreadInfo:hover { - cursor: pointer; +.mx_ThreadInfo { + height: 35px; + position: relative; + background-color: $system; + padding-left: 12px; + display: flex; + align-items: center; + border-radius: 8px; + padding-right: 16px; + padding-top: 8px; + padding-bottom: 8px; + font-size: 12px; + color: $secondary-content; + box-sizing: border-box; + justify-content: flex-start; + + &:hover, &-active { + cursor: pointer; + border: 1px solid $quinary-content; + padding-top: 7px; + padding-bottom: 7px; + padding-left: 11px; + padding-right: 15px; + } + + .mx_ThreadInfo_content { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + padding-left: 8px; + } + + .mx_ThreadInfo_thread-icon { + mask-image: url('$(res)/img/element-icons/thread-summary.svg'); + mask-position: center; + height: 16px; + min-width: 16px; + background-color: $secondary-content; + mask-repeat: no-repeat; + mask-size: contain; + } + .mx_ThreadInfo_threads-amount { + font-weight: 600; + position: relative; + padding: 0 8px; + white-space: nowrap; + } } + + .mx_ThreadView { display: flex; flex-direction: column; diff --git a/res/img/element-icons/thread-summary.svg b/res/img/element-icons/thread-summary.svg new file mode 100644 index 0000000000..2c4f0ead0c --- /dev/null +++ b/res/img/element-icons/thread-summary.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 9d608c2833..1aab27ee5d 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -59,6 +59,7 @@ import { getEventDisplayInfo } from '../../../utils/EventUtils'; import SettingsStore from "../../../settings/SettingsStore"; import MKeyVerificationConclusion from "../messages/MKeyVerificationConclusion"; import { dispatchShowThreadEvent } from '../../../dispatcher/dispatch-actions/threads'; +import { MessagePreviewStore } from '../../../stores/room-list/MessagePreviewStore'; const eventTileTypes = { [EventType.RoomMessage]: 'messages.MessageEvent', @@ -532,15 +533,13 @@ export default class EventTile extends React.Component { } const thread = this.state.thread; - const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); if (!thread || this.props.showThreadInfo === false || thread.length <= 1) { return null; } - const avatars = Array.from(thread.participants).map((mxId: string) => { - const member = room.getMember(mxId); - return ; - }); + const threadMessagePreview = MessagePreviewStore.instance.generateThreadPreview(this.state.thread); + + if (!threadMessagePreview) return null; return (
{ dispatchShowThreadEvent(this.props.mxEvent); }} > - - { avatars } + + + { _t("%(count)s reply", { + count: thread.length - 1, + }) } - { thread.length - 1 } { thread.length === 2 ? 'reply' : 'replies' } + +
+ + { threadMessagePreview } + +
); } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index a46d48b655..781642089c 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1551,6 +1551,8 @@ "Send as message": "Send as message", "Edit message": "Edit message", "Mod": "Mod", + "%(count)s reply|other": "%(count)s replies", + "%(count)s reply|one": "%(count)s reply", "This event could not be displayed": "This event could not be displayed", "Your key share request has been sent - please check your other sessions for key share requests.": "Your key share request has been sent - please check your other sessions for key share requests.", "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.": "Key share requests are sent to your other sessions automatically. If you rejected or dismissed the key share request on your other sessions, click here to request the keys for this session again.", diff --git a/src/stores/room-list/MessagePreviewStore.ts b/src/stores/room-list/MessagePreviewStore.ts index 44ec173e08..ab22baf5d1 100644 --- a/src/stores/room-list/MessagePreviewStore.ts +++ b/src/stores/room-list/MessagePreviewStore.ts @@ -27,6 +27,7 @@ import { CallHangupEvent } from "./previews/CallHangupEvent"; import { StickerEventPreview } from "./previews/StickerEventPreview"; import { ReactionEventPreview } from "./previews/ReactionEventPreview"; import { UPDATE_EVENT } from "../AsyncStore"; +import { Thread } from "matrix-js-sdk/src/models/thread"; // Emitted event for when a room's preview has changed. First argument will the room for which // the change happened. @@ -108,6 +109,15 @@ export class MessagePreviewStore extends AsyncStoreWithClient { return previews.get(inTagId); } + public generateThreadPreview(thread: Thread): string { + const lastEvent = thread.replyToEvent; + const previewDef = PREVIEWS[lastEvent.getType()]; + // TODO: Handle case where we don't have + if (!previewDef) return ''; + const previewText = previewDef.previewer.getTextFor(lastEvent, null, true); + return previewText ?? ''; + } + private async generatePreview(room: Room, tagId?: TagID) { const events = room.timeline; if (!events) return; // should only happen in tests diff --git a/src/stores/room-list/previews/MessageEventPreview.ts b/src/stores/room-list/previews/MessageEventPreview.ts index 961f27fda1..e105c27ac2 100644 --- a/src/stores/room-list/previews/MessageEventPreview.ts +++ b/src/stores/room-list/previews/MessageEventPreview.ts @@ -23,7 +23,7 @@ import ReplyThread from "../../../components/views/elements/ReplyThread"; import { getHtmlText } from "../../../HtmlUtils"; export class MessageEventPreview implements IPreview { - public getTextFor(event: MatrixEvent, tagId?: TagID): string { + public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string { let eventContent = event.getContent(); if (event.isRelation("m.replace")) { @@ -64,7 +64,7 @@ export class MessageEventPreview implements IPreview { return _t("* %(senderName)s %(emote)s", { senderName: getSenderName(event), emote: body }); } - if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { return body; } else { return _t("%(senderName)s: %(message)s", { senderName: getSenderName(event), message: body }); diff --git a/src/stores/room-list/previews/ReactionEventPreview.ts b/src/stores/room-list/previews/ReactionEventPreview.ts index 25f8e0b61a..4e2c175055 100644 --- a/src/stores/room-list/previews/ReactionEventPreview.ts +++ b/src/stores/room-list/previews/ReactionEventPreview.ts @@ -23,7 +23,7 @@ import SettingsStore from "../../../settings/SettingsStore"; import DMRoomMap from "../../../utils/DMRoomMap"; export class ReactionEventPreview implements IPreview { - public getTextFor(event: MatrixEvent, tagId?: TagID): string { + public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string { const showDms = SettingsStore.getValue("feature_roomlist_preview_reactions_dms"); const showAll = SettingsStore.getValue("feature_roomlist_preview_reactions_all"); @@ -41,7 +41,7 @@ export class ReactionEventPreview implements IPreview { const reaction = relation.key; if (!reaction) return null; // invalid reaction (unknown format) - if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { return reaction; } else { return _t("%(senderName)s: %(reaction)s", { senderName: getSenderName(event), reaction }); diff --git a/src/stores/room-list/previews/StickerEventPreview.ts b/src/stores/room-list/previews/StickerEventPreview.ts index 56746568af..6ad43ef3e1 100644 --- a/src/stores/room-list/previews/StickerEventPreview.ts +++ b/src/stores/room-list/previews/StickerEventPreview.ts @@ -21,11 +21,11 @@ import { getSenderName, isSelf, shouldPrefixMessagesIn } from "./utils"; import { _t } from "../../../languageHandler"; export class StickerEventPreview implements IPreview { - public getTextFor(event: MatrixEvent, tagId?: TagID): string { + public getTextFor(event: MatrixEvent, tagId?: TagID, isThread?: boolean): string { const stickerName = event.getContent()['body']; if (!stickerName) return null; - if (isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { + if (isThread || isSelf(event) || !shouldPrefixMessagesIn(event.getRoomId(), tagId)) { return stickerName; } else { return _t("%(senderName)s: %(stickerName)s", { senderName: getSenderName(event), stickerName }); From 93fc24c875e15526c10cf29cd60443d78d5798c4 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Fri, 15 Oct 2021 14:47:02 +0100 Subject: [PATCH 5/5] Adding renderThreadInfo comment to explain data accessor --- src/components/views/rooms/EventTile.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 467481c679..44bf500f50 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -546,6 +546,12 @@ export default class EventTile extends React.Component { return null; } + /** + * Accessing the threads value through the room due to a race condition + * that will be solved when there are proper backend support for threads + * We currently have no reliable way to discover than an event is a thread + * when we are at the sync stage + */ const room = MatrixClientPeg.get().getRoom(this.props.mxEvent.getRoomId()); const thread = room.threads.get(this.props.mxEvent.getId());