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/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; } 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 9d608c2833..44bf500f50 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', @@ -475,6 +476,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 +520,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,21 +533,39 @@ 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; } - const thread = this.state.thread; + /** + * 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()); + + if (thread && !thread.ready) { + thread.addEvent(this.props.mxEvent, true); + } + 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 });