From 27e48062b6d6f4e42900963b5344ca69e16ace70 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 5 Apr 2022 17:01:34 +0100 Subject: [PATCH] Apply tweaks to Thread list as per design spec (#8149) Co-authored-by: Germain Souquet --- res/css/views/right_panel/_ThreadPanel.scss | 40 ++++++-- res/css/views/rooms/_EventTile.scss | 91 ++++++++++++++----- src/components/structures/ThreadPanel.tsx | 46 +++++++--- .../context_menus/MessageContextMenu.tsx | 8 +- src/components/views/messages/TextualBody.tsx | 15 +-- src/components/views/rooms/EventTile.tsx | 41 ++++++--- src/i18n/strings/en_EN.json | 4 +- 7 files changed, 173 insertions(+), 72 deletions(-) diff --git a/res/css/views/right_panel/_ThreadPanel.scss b/res/css/views/right_panel/_ThreadPanel.scss index cb438c4706..280404a649 100644 --- a/res/css/views/right_panel/_ThreadPanel.scss +++ b/res/css/views/right_panel/_ThreadPanel.scss @@ -20,14 +20,17 @@ limitations under the License. .mx_BaseCard_header { margin-bottom: 12px; + .mx_BaseCard_close, .mx_BaseCard_back { width: 24px; height: 24px; } + .mx_BaseCard_back { left: -4px; } + .mx_BaseCard_close { right: -4px; } @@ -66,6 +69,7 @@ limitations under the License. --size: 24px; width: var(--size); height: var(--size); + &::after { mask-size: var(--size); mask-image: url("$(res)/img/element-icons/message/overflow-large.svg"); @@ -99,11 +103,10 @@ limitations under the License. } .mx_AutoHideScrollbar { - background: #fff; background-color: $background; border-radius: 8px; - width: calc(100% - 16px); - padding-right: 16px; + width: calc(100% - 24px); + padding-right: 18px; } &.mx_ThreadView .mx_ThreadView_timelinePanelWrapper { @@ -125,13 +128,15 @@ limitations under the License. padding-right: 0; } - .mx_EventTile, .mx_GenericEventListSummary { + .mx_EventTile, + .mx_GenericEventListSummary { // Account for scrollbar when hovering padding-top: 0; .mx_ThreadInfo { position: relative; padding-right: 11px; + &::after { content: ''; display: block; @@ -157,6 +162,10 @@ limitations under the License. .mx_EventTile_e2eIcon { left: 8px; } + + &:hover .mx_EventTile_line { + box-shadow: unset !important; // don't show the verification left stroke in the thread list + } } .mx_MessageComposer { @@ -190,10 +199,6 @@ limitations under the License. float: right; } - .mx_ThreadPanel_dropdown[aria-expanded=true]::before { - transform: rotate(180deg); - } - .mx_MessageTimestamp { font-size: $font-12px; color: $secondary-content; @@ -272,19 +277,23 @@ limitations under the License. h2 { color: $primary-content; - font-weight: 600; + font-weight: $font-semi-bold; font-size: $font-18px; + margin-top: 24px; + margin-bottom: 10px; } p { font-size: $font-15px; color: $secondary-content; + margin: 10px 0; } button { border: none; background: none; color: $accent; + font-size: $font-15px; &:hover, &:active { @@ -292,6 +301,15 @@ limitations under the License. cursor: pointer; } } + + .mx_ThreadPanel_empty_tip { + font-size: $font-12px; + line-height: $font-15px; + + >b { + font-weight: $font-semi-bold; + } + } } .mx_ThreadPanel_largeIcon { @@ -317,6 +335,7 @@ limitations under the License. .mx_ContextualMenu_wrapper.mx_ThreadPanel__header { .mx_ContextualMenu { position: initial; + span:first-of-type { font-weight: $font-semi-bold; font-size: inherit; @@ -336,6 +355,7 @@ limitations under the License. left: auto; right: 22px; border-bottom-color: $quinary-content; + &::after { content: ""; border: inherit; @@ -357,10 +377,12 @@ limitations under the License. &:hover { background-color: $event-selected-color; } + &[aria-checked="true"] { :first-child { margin-left: -20px; } + :first-child::before { content: ""; width: 12px; diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 988b79b6ef..ccd6b88d02 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -43,9 +43,11 @@ $left-gutter: 64px; right: 0; } } + .mx_EventTile_receiptSent::before { mask-image: url('$(res)/img/element-icons/circle-sent.svg'); } + .mx_EventTile_receiptSending::before { mask-image: url('$(res)/img/element-icons/circle-sending.svg'); } @@ -61,11 +63,11 @@ $left-gutter: 64px; &[data-shape=ThreadsList][data-notification]::before { content: ""; position: absolute; - width: 8px; - height: 8px; + width: 10px; + height: 10px; border-radius: 50%; - right: -16px; - top: 6px; + right: -25px; // center it in the gutter (16px margin + 4px padding + half 10px width) + top: 4px; left: auto; } @@ -79,7 +81,6 @@ $left-gutter: 64px; .mx_ThreadInfo, .mx_ThreadSummaryIcon { - margin-right: 110px; margin-left: 64px; } @@ -115,7 +116,8 @@ $left-gutter: 64px; .mx_DisambiguatedProfile { color: $primary-content; font-size: $font-14px; - display: inline-block; /* anti-zalgo, with overflow hidden */ + display: inline-block; + /* anti-zalgo, with overflow hidden */ overflow: hidden; padding-bottom: 0px; padding-top: 0px; @@ -142,7 +144,8 @@ $left-gutter: 64px; clear: both; } - .mx_EventTile_line, .mx_EventTile_reply { + .mx_EventTile_line, + .mx_EventTile_reply { position: relative; padding-left: $left-gutter; border-radius: 8px; @@ -308,11 +311,19 @@ $left-gutter: 64px; .mx_RoomView_timeline_rr_enabled { .mx_EventTile[data-layout=group] { + + .mx_ThreadInfo, + .mx_ThreadSummaryIcon, .mx_EventTile_line { /* ideally should be 100px, but 95px gives us a max thumbnail size of 800x600, which is nice */ margin-right: 110px; } + + .mx_ThreadInfo { + max-width: min(calc(100% - $left-gutter - 110px), 600px); // leave space on both left & right gutters + } } + // on ELS we need the margin to allow interaction with the expand/collapse button which is normally in the RR gutter } @@ -408,7 +419,8 @@ $left-gutter: 64px; background-repeat: no-repeat; background-size: contain; - &::before, &::after { + &::before, + &::after { content: ""; display: block; position: absolute; @@ -433,6 +445,7 @@ $left-gutter: 64px; mask-image: url('$(res)/img/e2e/warning.svg'); background-color: $alert; } + opacity: 1; } @@ -441,6 +454,7 @@ $left-gutter: 64px; mask-image: url('$(res)/img/e2e/normal.svg'); background-color: $header-panel-text-primary-color; } + opacity: 1; } @@ -479,7 +493,8 @@ $left-gutter: 64px; color: inherit; // inherit the colour from the dark or light theme by default (but not for code blocks) font-size: $font-14px; - pre, code { + pre, + code { font-family: $monospace-font-family !important; background-color: $codeblock-background-color; } @@ -492,7 +507,7 @@ $left-gutter: 64px; pre code { white-space: pre; // we want code blocks to be scrollable and not wrap - > * { + >* { display: inline; } } @@ -514,6 +529,7 @@ $left-gutter: 64px; float: left; margin: 0 0.5em 0 -1.5em; color: gray; + & span { text-align: right; display: block; @@ -547,18 +563,22 @@ $left-gutter: 64px; height: 19px; background-color: $message-action-bar-fg-color; } + .mx_EventTile_buttonBottom { top: 33px; } + .mx_EventTile_copyButton { mask-image: url($copy-button-url); } + .mx_EventTile_collapseButton { mask-size: 75%; mask-position: center; mask-repeat: no-repeat; mask-image: url("$(res)/img/element-icons/minimise-collapse.svg"); } + .mx_EventTile_expandButton { mask-size: 75%; mask-position: center; @@ -674,10 +694,13 @@ $left-gutter: 64px; } @media only screen and (max-width: 480px) { - .mx_EventTile_line, .mx_EventTile_reply { + + .mx_EventTile_line, + .mx_EventTile_reply { padding-left: 0; margin-right: 0; } + .mx_EventTile_content { margin-top: 10px; margin-right: 0; @@ -692,23 +715,28 @@ $left-gutter: 64px; mask-position: center; height: 18px; min-width: 18px; - background-color: $secondary-content; + background-color: $secondary-content !important; mask-repeat: no-repeat; mask-size: contain; } .mx_ThreadSummaryIcon { + display: inline-block; font-size: $font-12px; - color: $secondary-content; + color: $secondary-content !important; + margin-top: 8px; + margin-bottom: 8px; + &::before { vertical-align: middle; - margin-left: 8px; + margin-right: 8px; + margin-top: -2px; } } .mx_ThreadInfo { min-width: 267px; - max-width: min(calc(100% - $left-gutter - 64px), 600px); // leave space on both left & right gutters + max-width: min(calc(100% - $left-gutter), 600px); // leave space on both left & right gutters width: fit-content; height: 40px; position: relative; @@ -756,7 +784,8 @@ $left-gutter: 64px; } } - &:hover, &:focus { + &:hover, + &:focus { cursor: pointer; border-color: $quinary-content; @@ -782,6 +811,9 @@ $threadInfoLineHeight: calc(2 * $font-12px); .mx_ThreadInfo_sender { font-weight: $font-semi-bold; line-height: $threadInfoLineHeight; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } .mx_ThreadInfo_content { @@ -792,6 +824,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); font-size: $font-12px; line-height: $threadInfoLineHeight; color: $secondary-content; + flex: 1; } .mx_ThreadInfo_avatar { @@ -810,9 +843,10 @@ $threadInfoLineHeight: calc(2 * $font-12px); .mx_EventTile[data-shape=ThreadsList] { --topOffset: 20px; --leftOffset: 46px; + $borderRadius: 8px; margin: var(--topOffset) 16px var(--topOffset) 0; - border-radius: 8px; + border-radius: $borderRadius; display: flex; flex-flow: wrap; @@ -847,6 +881,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); &::after { content: unset; } + margin-bottom: 0; } @@ -857,7 +892,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); padding-top: 0; .mx_EventTile_avatar { - top: -4px; + top: 0; left: 0; } @@ -892,7 +927,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); width: 100%; box-sizing: border-box; padding-left: var(--leftOffset) !important; - padding-bottom: 0; + border-radius: $borderRadius !important; // override 4px } .mx_MessageTimestamp { @@ -918,7 +953,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); .mx_EventTile { display: flex; flex-direction: column; - padding-top: 0; + padding-top: 14px; // due to layout differences, this odd number matches the 18px padding-top of main tl events .mx_EventTile_line { padding-left: 0; @@ -973,7 +1008,7 @@ $threadInfoLineHeight: calc(2 * $font-12px); .mx_UnknownBody, .mx_MPollBody, .mx_ReplyChain_wrapper { - margin-left: 36px; + margin-left: 48px; margin-right: 8px; .mx_EventTile_content, @@ -997,16 +1032,17 @@ $threadInfoLineHeight: calc(2 * $font-12px); .mx_EventTile_senderDetails { display: flex; align-items: center; - gap: calc(6px + $selected-message-border-width); + gap: calc(14px + $selected-message-border-width); a { flex: 1; - min-width: none; + min-width: unset; max-width: 100%; display: flex; align-items: center; .mx_DisambiguatedProfile { + margin-left: 8px; flex: 1; } } @@ -1026,4 +1062,13 @@ $threadInfoLineHeight: calc(2 * $font-12px); .mx_MessageComposer_sendMessage { margin-right: 0; } + + .mx_EditMessageComposer { + margin-left: 30px !important; // align start of first letter with that of the event body + } + + .mx_EditMessageComposer_buttons { + padding-right: 11px; // align with right edge of input + margin-right: 0; // align with right edge of background + } } diff --git a/src/components/structures/ThreadPanel.tsx b/src/components/structures/ThreadPanel.tsx index 3364fdc27c..b9f227780e 100644 --- a/src/components/structures/ThreadPanel.tsx +++ b/src/components/structures/ThreadPanel.tsx @@ -101,7 +101,7 @@ export const ThreadPanelHeader = ({ filterOption, setFilterOption, empty }: { isSelected={opt === value} />); const contextMenu = menuDisplayed ? void; } -const EmptyThread: React.FC = ({ filterOption, showAllThreadsCallback }) => { +const EmptyThread: React.FC = ({ hasThreads, filterOption, showAllThreadsCallback }) => { + let body: JSX.Element; + if (hasThreads) { + body = <> +

+ { _t("Reply to an ongoing thread or use “%(replyInThread)s” " + + "when hovering over a message to start a new one.", { + replyInThread: _t("Reply in thread"), + }) } +

+

+ { /* Always display that paragraph to prevent layout shift when hiding the button */ } + { (filterOption === ThreadFilterType.My) + ? + : <>  + } +

+ ; + } else { + body = <> +

{ _t("Threads help keep your conversations on-topic and easy to track.") }

+

+ { _t('Tip: Use "Reply in thread" when hovering over a message.', {}, { + b: sub => { sub }, + }) } +

+ ; + } + return ; }; @@ -247,6 +266,7 @@ const ThreadPanel: React.FC = ({ timelineSet={timelineSet} showUrlPreview={false} // No URL previews at the threads list level empty={ 0} filterOption={filterOption} showAllThreadsCallback={() => setFilterOption(ThreadFilterType.All)} />} diff --git a/src/components/views/context_menus/MessageContextMenu.tsx b/src/components/views/context_menus/MessageContextMenu.tsx index 8899c13a60..e0b1b7c9a8 100644 --- a/src/components/views/context_menus/MessageContextMenu.tsx +++ b/src/components/views/context_menus/MessageContextMenu.tsx @@ -44,7 +44,6 @@ import { RoomPermalinkCreator } from "../../../utils/permalinks/Permalinks"; import { ChevronFace, IPosition } from '../../structures/ContextMenu'; import RoomContext, { TimelineRenderingType } from '../../../contexts/RoomContext'; import { ComposerInsertPayload } from "../../../dispatcher/payloads/ComposerInsertPayload"; -import { WidgetLayoutStore } from '../../../stores/widgets/WidgetLayoutStore'; import EndPollDialog from '../dialogs/EndPollDialog'; import { isPollEnded } from '../messages/MPollBody'; import { ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; @@ -472,14 +471,11 @@ export default class MessageContextMenu extends React.Component timelineRenderingType === TimelineRenderingType.Thread || timelineRenderingType === TimelineRenderingType.ThreadsList ); - const isThreadRootEvent = isThread && this.props.mxEvent?.getThread()?.rootEvent === this.props.mxEvent; + const isThreadRootEvent = isThread && this.props.mxEvent.isThreadRoot; - const isMainSplitTimelineShown = !WidgetLayoutStore.instance.hasMaximisedWidget( - MatrixClientPeg.get().getRoom(mxEvent.getRoomId()), - ); const commonItemsList = ( - { (isThreadRootEvent && isMainSplitTimelineShown) && { if (this.props.highlightLink) { body = { body }; } else if (content.data && typeof content.data["org.matrix.neb.starter_link"] === "string") { - body = { body }; + body = ( + + { body } + + ); } let widgets; @@ -651,9 +656,7 @@ export default class TextualBody extends React.Component { ); } return ( -
+
{ body } { widgets }
diff --git a/src/components/views/rooms/EventTile.tsx b/src/components/views/rooms/EventTile.tsx index b73ec31e29..e811a37508 100644 --- a/src/components/views/rooms/EventTile.tsx +++ b/src/components/views/rooms/EventTile.tsx @@ -631,12 +631,22 @@ export class UnwrappedEventTile extends React.Component { } private renderThreadInfo(): React.ReactNode { + if (this.state.thread?.id === this.props.mxEvent.getId()) { + return ; + } + if (this.context.timelineRenderingType === TimelineRenderingType.Search && this.props.mxEvent.threadRootId) { + if (this.props.highlightLink) { + return ( + + { _t("From a thread") } + + ); + } + return (

{ _t("From a thread") }

); - } else if (this.state.thread?.id === this.props.mxEvent.getId()) { - return ; } } @@ -1100,6 +1110,7 @@ export class UnwrappedEventTile extends React.Component { let isContinuation = this.props.continuation; if (this.context.timelineRenderingType !== TimelineRenderingType.Room && this.context.timelineRenderingType !== TimelineRenderingType.Search && + this.context.timelineRenderingType !== TimelineRenderingType.Thread && this.props.layout !== Layout.Bubble ) { isContinuation = false; @@ -1146,16 +1157,17 @@ export class UnwrappedEventTile extends React.Component { ? undefined : this.props.mxEvent.getId(); - let avatar; - let sender; - let avatarSize; - let needsSenderProfile; + let avatar: JSX.Element; + let sender: JSX.Element; + let avatarSize: number; + let needsSenderProfile: boolean; - if (this.context.timelineRenderingType === TimelineRenderingType.Notification || - this.context.timelineRenderingType === TimelineRenderingType.ThreadsList - ) { + if (this.context.timelineRenderingType === TimelineRenderingType.Notification) { avatarSize = 24; needsSenderProfile = true; + } else if (this.context.timelineRenderingType === TimelineRenderingType.ThreadsList) { + avatarSize = 36; + needsSenderProfile = true; } else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) { avatarSize = 0; needsSenderProfile = false; @@ -1364,7 +1376,8 @@ export class UnwrappedEventTile extends React.Component {
,
- {
,
{ avatar } - - { sender } - + { sender }
,
{ replyChain } @@ -1417,7 +1428,9 @@ export class UnwrappedEventTile extends React.Component { isSeeingThroughMessageHiddenForModeration={isSeeingThroughMessageHiddenForModeration} /> { actionBar } - { timestamp } + + { timestamp } +
, reactionsRow, ]); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index dea47408e9..795167ac67 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -3099,9 +3099,11 @@ "My threads": "My threads", "Shows all threads you've participated in": "Shows all threads you've participated in", "Show:": "Show:", - "Keep discussions organised with threads": "Keep discussions organised with threads", "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.": "Reply to an ongoing thread or use “%(replyInThread)s” when hovering over a message to start a new one.", "Show all threads": "Show all threads", + "Threads help keep your conversations on-topic and easy to track.": "Threads help keep your conversations on-topic and easy to track.", + "Tip: Use \"Reply in thread\" when hovering over a message.": "Tip: Use \"Reply in thread\" when hovering over a message.", + "Keep discussions organised with threads": "Keep discussions organised with threads", "Thread": "Thread", "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.": "Tried to load a specific point in this room's timeline, but you do not have permission to view the message in question.", "Tried to load a specific point in this room's timeline, but was unable to find it.": "Tried to load a specific point in this room's timeline, but was unable to find it.",