From 378536ee2a298c9a07abe6bd888775f76fae5389 Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 14 Oct 2021 17:22:06 +0100 Subject: [PATCH 1/6] Add 'm.thread' relation for replies to a threaded event --- src/components/views/elements/ReplyThread.tsx | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx index 7dd81f469a..3243d5ee03 100644 --- a/src/components/views/elements/ReplyThread.tsx +++ b/src/components/views/elements/ReplyThread.tsx @@ -35,6 +35,8 @@ import Spinner from './Spinner'; import ReplyTile from "../rooms/ReplyTile"; import Pill from './Pill'; import { Room } from 'matrix-js-sdk/src/models/room'; +import { threadId } from 'worker_threads'; +import { RelationType } from 'matrix-js-sdk/src/@types/event'; /** * This number is based on the previous behavior - if we have message of height @@ -226,13 +228,30 @@ export default class ReplyThread extends React.Component { public static makeReplyMixIn(ev: MatrixEvent) { if (!ev) return {}; - return { + + const mixin: any = { 'm.relates_to': { 'm.in_reply_to': { 'event_id': ev.getId(), }, }, }; + + /** + * If the event replied is part of a thread + * Add the `m.thread` relation so that clients + * that know how to handle that relation will + * be able to render them more accurately + */ + if (ev.isThreadRelation) { + mixin['m.relates_to'] = { + ...mixin['m.relates_to'], + rel_type: RelationType.Thread, + event_id: ev.threadRootId, + }; + } + + return mixin; } public static hasThreadReply(event: MatrixEvent) { From 5fccbd4ef72b84c9db0c6a90d6f0e69641103baf Mon Sep 17 00:00:00 2001 From: Germain Souquet Date: Thu, 14 Oct 2021 17:40:00 +0100 Subject: [PATCH 2/6] Remove auto-import --- src/components/views/elements/ReplyThread.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/views/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx index 3243d5ee03..e0cca0c81e 100644 --- a/src/components/views/elements/ReplyThread.tsx +++ b/src/components/views/elements/ReplyThread.tsx @@ -35,7 +35,6 @@ import Spinner from './Spinner'; import ReplyTile from "../rooms/ReplyTile"; import Pill from './Pill'; import { Room } from 'matrix-js-sdk/src/models/room'; -import { threadId } from 'worker_threads'; import { RelationType } from 'matrix-js-sdk/src/@types/event'; /** From 8cdb1c667df0d42901ecb5bb9079af8403cb089e Mon Sep 17 00:00:00 2001 From: Aaron Raimist Date: Fri, 15 Oct 2021 05:23:59 -0500 Subject: [PATCH 3/6] Fix tooltip when downloading unencrypted file --- src/components/views/messages/DownloadActionButton.tsx | 10 ++++++++-- src/i18n/strings/en_EN.json | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/views/messages/DownloadActionButton.tsx b/src/components/views/messages/DownloadActionButton.tsx index 6dc48b0936..35fd5c49be 100644 --- a/src/components/views/messages/DownloadActionButton.tsx +++ b/src/components/views/messages/DownloadActionButton.tsx @@ -20,7 +20,7 @@ import React from "react"; import { RovingAccessibleTooltipButton } from "../../../accessibility/RovingTabIndex"; import Spinner from "../elements/Spinner"; import classNames from "classnames"; -import { _t } from "../../../languageHandler"; +import { _t, _td } from "../../../languageHandler"; import { replaceableComponent } from "../../../utils/replaceableComponent"; import { FileDownloader } from "../../../utils/FileDownloader"; @@ -36,6 +36,7 @@ interface IProps { interface IState { loading: boolean; blob?: Blob; + tooltip: string; } @replaceableComponent("views.messages.DownloadActionButton") @@ -47,12 +48,17 @@ export default class DownloadActionButton extends React.PureComponent { if (this.state.loading) return; + if (this.props.mediaEventHelperGet().media.isEncrypted) { + this.setState({ tooltip: _td("Decrypting") }); + } + this.setState({ loading: true }); if (this.state.blob) { @@ -87,7 +93,7 @@ export default class DownloadActionButton extends React.PureComponent diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3e61146acb..a46d48b655 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1959,6 +1959,7 @@ "Saturday": "Saturday", "Today": "Today", "Yesterday": "Yesterday", + "Downloading": "Downloading", "Decrypting": "Decrypting", "Download": "Download", "View Source": "View Source", From 7edec291acacd687f7a335ee26e5a42b67ba4c43 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 15 Oct 2021 11:49:27 +0100 Subject: [PATCH 4/6] Switch DevicesPanel to use table (for screen readers) --- res/css/views/settings/_DevicesPanel.scss | 18 +++++-------- .../views/settings/DevicesPanel.tsx | 26 +++++++++++-------- .../views/settings/DevicesPanelEntry.tsx | 20 +++++++------- 3 files changed, 32 insertions(+), 32 deletions(-) diff --git a/res/css/views/settings/_DevicesPanel.scss b/res/css/views/settings/_DevicesPanel.scss index 7e836e0d87..1354f6d051 100644 --- a/res/css/views/settings/_DevicesPanel.scss +++ b/res/css/views/settings/_DevicesPanel.scss @@ -15,7 +15,6 @@ limitations under the License. */ .mx_DevicesPanel { - display: table; table-layout: fixed; // Normally the panel is 880px, however this can easily overflow the container. // TODO: Fix the table to not be squishy @@ -25,16 +24,16 @@ limitations under the License. } .mx_DevicesPanel_header { - display: table-header-group; font-weight: bold; } -.mx_DevicesPanel_header > .mx_DevicesPanel_deviceButtons { +.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons { height: 48px; // make this tall so the table doesn't move down when the delete button appears } -.mx_DevicesPanel_header > div { - display: table-cell; +.mx_DevicesPanel_header th { + padding: 0px; + text-align: left; vertical-align: middle; } @@ -50,12 +49,9 @@ limitations under the License. width: 20%; } -.mx_DevicesPanel_device { - display: table-row; -} - -.mx_DevicesPanel_device > div { - display: table-cell; +.mx_DevicesPanel_device td { + vertical-align: baseline; + padding: 0px; } .mx_DevicesPanel_myDevice { diff --git a/src/components/views/settings/DevicesPanel.tsx b/src/components/views/settings/DevicesPanel.tsx index 9a1321619e..d66d0f42eb 100644 --- a/src/components/views/settings/DevicesPanel.tsx +++ b/src/components/views/settings/DevicesPanel.tsx @@ -218,17 +218,21 @@ export default class DevicesPanel extends React.Component { const classes = classNames(this.props.className, "mx_DevicesPanel"); return ( -
-
-
{ _t("ID") }
-
{ _t("Public Name") }
-
{ _t("Last seen") }
-
- { this.state.selectedDevices.length > 0 ? deleteButton : null } -
-
- { devices.map(this.renderDevice) } -
+ + + + + + + + + + + { devices.map(this.renderDevice) } + +
{ _t("ID") }{ _t("Public Name") }{ _t("Last seen") } + { this.state.selectedDevices.length > 0 ? deleteButton : null } +
); } } diff --git a/src/components/views/settings/DevicesPanelEntry.tsx b/src/components/views/settings/DevicesPanelEntry.tsx index d033bc41a9..1131114556 100644 --- a/src/components/views/settings/DevicesPanelEntry.tsx +++ b/src/components/views/settings/DevicesPanelEntry.tsx @@ -66,23 +66,23 @@ export default class DevicesPanelEntry extends React.Component { } return ( -
-
+ + { device.device_id } -
-
+ + -
-
+ + { lastSeen } -
-
+ + -
-
+ + ); } } From 690893ea6b5c2d0d7f51a2b7d578f1a7371cac90 Mon Sep 17 00:00:00 2001 From: Andy Balaam Date: Fri, 15 Oct 2021 13:32:39 +0100 Subject: [PATCH 5/6] Merge duplicate CSS selectors --- res/css/views/settings/_DevicesPanel.scss | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/res/css/views/settings/_DevicesPanel.scss b/res/css/views/settings/_DevicesPanel.scss index 1354f6d051..7d6db7bc96 100644 --- a/res/css/views/settings/_DevicesPanel.scss +++ b/res/css/views/settings/_DevicesPanel.scss @@ -29,6 +29,7 @@ limitations under the License. .mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons { height: 48px; // make this tall so the table doesn't move down when the delete button appears + width: 20%; } .mx_DevicesPanel_header th { @@ -45,10 +46,6 @@ limitations under the License. width: 30%; } -.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons { - width: 20%; -} - .mx_DevicesPanel_device td { vertical-align: baseline; padding: 0px; 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 6/6] 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 });