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/css/views/settings/_DevicesPanel.scss b/res/css/views/settings/_DevicesPanel.scss index 7e836e0d87..7d6db7bc96 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,17 @@ 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 + width: 20%; } -.mx_DevicesPanel_header > div { - display: table-cell; +.mx_DevicesPanel_header th { + padding: 0px; + text-align: left; vertical-align: middle; } @@ -46,16 +46,9 @@ limitations under the License. width: 30%; } -.mx_DevicesPanel_header .mx_DevicesPanel_deviceButtons { - 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/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/elements/ReplyThread.tsx b/src/components/views/elements/ReplyThread.tsx index 7dd81f469a..e0cca0c81e 100644 --- a/src/components/views/elements/ReplyThread.tsx +++ b/src/components/views/elements/ReplyThread.tsx @@ -35,6 +35,7 @@ import Spinner from './Spinner'; import ReplyTile from "../rooms/ReplyTile"; import Pill from './Pill'; import { Room } from 'matrix-js-sdk/src/models/room'; +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 +227,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) { 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/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index 9fd9a56730..467481c679 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', @@ -556,10 +557,9 @@ export default class EventTile extends React.Component { 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/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 } -
-
+ + -
-
+ + ); } } diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 3e61146acb..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.", @@ -1959,6 +1961,7 @@ "Saturday": "Saturday", "Today": "Today", "Yesterday": "Yesterday", + "Downloading": "Downloading", "Decrypting": "Decrypting", "Download": "Download", "View Source": "View Source", 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 });