diff --git a/res/css/_components.scss b/res/css/_components.scss index 445ed70ff41..f43f7658d74 100644 --- a/res/css/_components.scss +++ b/res/css/_components.scss @@ -177,6 +177,7 @@ @import "./views/rooms/_EntityTile.scss"; @import "./views/rooms/_EventTile.scss"; @import "./views/rooms/_GroupLayout.scss"; +@import "./views/rooms/_BubbleLayout.scss"; @import "./views/rooms/_IRCLayout.scss"; @import "./views/rooms/_JumpToBottomButton.scss"; @import "./views/rooms/_LinkPreviewWidget.scss"; diff --git a/res/css/views/rooms/_BubbleLayout.scss b/res/css/views/rooms/_BubbleLayout.scss new file mode 100644 index 00000000000..bb6eabe7335 --- /dev/null +++ b/res/css/views/rooms/_BubbleLayout.scss @@ -0,0 +1,361 @@ +/* +Copyright 2015, 2016 OpenMarket Ltd +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +$left-gutter: 56px; + +.sc_BubbleLayout { + // ---- Overrides ---- + + .mx_RoomView_MessageList { + padding-bottom: 0; + } + + .mx_RoomTile { + .mx_RoomTile_nameContainer { + .mx_RoomTile_name, + .mx_RoomTile_messagePreview { + margin: 2px 2px; + } + } + } + + .mx_EventTile { + // SC: no reserved space for read recipts required. + max-width: 100%; + + > .mx_SenderProfile { + line-height: $font-17px; + padding-left: $left-gutter; + max-width: unset !important; + } + + > .mx_EventTile_line { + padding-left: $left-gutter; + margin-right: unset; + } + + > .mx_EventTile_avatar { + position: absolute; + } + + .mx_ReplyThread { + .mx_EventTile_avatar { + top: 14px !important; + } + .mx_MessageTimestamp { + top: 2px !important; + } + } + + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 3px; + padding-bottom: 3px; + line-height: $font-22px; + } + } + + .mx_EventTile_info .mx_EventTile_line { + padding-left: calc($left-gutter + 18px); + } + + .mx_SenderProfile { + // Sender-profile within bubble + max-width: 100%; + } + + .mx_SenderProfile_name { + color: $accent-color !important; + } + + .mx_EventTile_selected > div > a > .mx_MessageTimestamp { + left: -4px; + } + + .mx_EventTile_msgOption { + float: unset; + text-align: unset; + position: unset; + + margin-left: 8px; + margin-right: unset; + + /* Hack to stop the height of this pushing the messages apart. + Replaces margin-top: -6px. This interacts better with a read + marker being in between. Content overflows. */ + // SC: Reserve our space + height: 14px; + margin-top: 6px; + margin-bottom: 6px; + + &.sc_readReceipts_empty { + height: 0; + margin-top: 0; + margin-bottom: 0; + } + } + + .mx_EventTile_readAvatars { + // SC-TODO align left below msg area + //top: 29px; + top: 0; + } + + .mx_EventTile_readAvatarRemainder { + height: 14px; + } + + .mx_EventTile_content { + // Handled by bubble + margin-right: unset; + } + + // Explicit relationships so that it doesn't apply to nested EventTile components (e.g in Replies) + .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line > a > .mx_MessageTimestamp, + .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line > a > .mx_MessageTimestamp, + .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line > a > .mx_MessageTimestamp { + left: 5px; + } + + .mx_MessageActionBar { + right: unset; + } + + .mx_MessageActionBar_left { + left: $left-gutter; + } + + .mx_MessageActionBar_right { + right: 8px; + } + + .mx_MFileBody { + display: flow-root; + } + + .mx_MFileBody_download { + display: inline; + } + + .mx_MImageBody { + margin-right: 34px; + } + + .mx_MessageTimestamp { + position: absolute; + width: 46px; /* 8 + 30 (avatar) + 8 */ + } + + // ---- Bubble specific ---- + + .sc_EventTile_bubbleContainer { + > .mx_EventTile_avatar { + top: 21px; + } + } + + .mx_EventTile_e2eIcon { + display: none !important; + } + + // .sc_EventTile_bubbleLine { + // .mx_EventTile_e2eIcon { + // left: 16px; + + // .sc_EventTile_bubbleTailLeftContainer & { + // top: 35px; + // } + // } + // } + + .sc_EventTile_bubbleArea { + // Max-width 75% for both-side bubbles only + max-width: 75%; + padding: 0px; + margin-bottom: 0; + } + + .sc_EventTile_bubbleArea_left { + margin-left: 0px; + margin-right: auto; + text-align: left; + } + + .sc_EventTile_bubble { + background-color: $dark-panel-bg-color; + padding: 10px; + border-radius: 2px; + //margin: 10px auto; + max-width: max-content; + // Min width: respect/"hide" bubble tail + min-width: 20px; + position: relative; + //box-sizing: content-box; + //display: flex; + //flex-wrap: wrap; + + // Don't inherit bubbleArea alignment + text-align: left; + + > *:not(.mx_ReplyThread_wrapper) { + .sc_LinkedTimestamp { + float: right; + display: flex; + margin-left: 1rem; + + .mx_MessageTimestamp { + visibility: visible !important; + position: unset !important; + width: unset !important; + text-align: right !important; + margin-top: auto; + padding-top: 0.3rem; + margin-bottom: -0.3rem; + font-size: 0.85em; + } + } + + .sc_EventTile_bigContent .sc_LinkedTimestamp { + height: 57px; + } + + .mx_MStickerBody_wrapper + .sc_LinkedTimestamp { + float: unset; + display: block; + } + } + } + + .sc_EventTile_bubble_left { + margin-left: 0px; + margin-right: auto; + background-color: $message-bubble-left; + + &.sc_EventTile_bubble_tail::before { + content: ''; + border: 16px solid transparent; + border-top-color: $message-bubble-left; + border-bottom: 0; + position: absolute; + left: -8px; + top: 0; + } + } + + // Right aligned bubble + + .sc_EventTile_bubbleArea_right { + margin-right: 8px; + margin-left: auto; + text-align: right; + } + + .sc_EventTile_bubble_right { + margin-right: 0px; + margin-left: auto; + background-color: $message-bubble-right; + + &.sc_EventTile_bubble_tail::before { + content: ''; + border: 16px solid transparent; + border-top-color: $message-bubble-right; + border-bottom: 0; + position: absolute; + right: -8px; + top: 0; + } + } +} + +/* Compact layout overrides */ + +.mx_MatrixChat_useCompactLayout { + .mx_EventTile { + padding-top: 4px; + + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0; + padding-bottom: 0; + } + + &.mx_EventTile_info { + // same as the padding for non-compact .mx_EventTile.mx_EventTile_info + padding-top: 0px; + font-size: $font-13px; + .mx_EventTile_line, .mx_EventTile_reply { + line-height: $font-20px; + } + .mx_EventTile_avatar { + top: 4px; + } + } + + .mx_SenderProfile { + font-size: $font-13px; + } + + &.mx_EventTile_emote { + // add a bit more space for emotes so that avatars don't collide + padding-top: 8px; + .mx_EventTile_avatar { + top: 2px; + } + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 1px; + } + } + + &.mx_EventTile_emote.mx_EventTile_continuation { + padding-top: 0; + .mx_EventTile_line, .mx_EventTile_reply { + padding-top: 0px; + padding-bottom: 0px; + } + } + + .mx_EventTile_avatar { + top: 2px; + } + + .mx_EventTile_e2eIcon { + top: 3px; + } + + .mx_EventTile_readAvatars { + top: 27px; + } + + &.mx_EventTile_continuation .mx_EventTile_readAvatars, + &.mx_EventTile_emote .mx_EventTile_readAvatars { + top: 5px; + } + + &.mx_EventTile_info .mx_EventTile_readAvatars { + top: 4px; + } + + .mx_EventTile_content .markdown-body { + p, ul, ol, dl, blockquote, pre, table { + margin-bottom: 4px; // 1/4 of the non-compact margin-bottom + } + } + } + + .mx_RoomView_MessageList h2 { + margin-top: 6px; + } +} diff --git a/res/css/views/rooms/_EventTile.scss b/res/css/views/rooms/_EventTile.scss index 429ac7ed4b6..70cce40737b 100644 --- a/res/css/views/rooms/_EventTile.scss +++ b/res/css/views/rooms/_EventTile.scss @@ -15,7 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -$left-gutter: 64px; +$left-gutter: 56px; .mx_EventTile { max-width: 100%; @@ -177,7 +177,7 @@ $left-gutter: 64px; */ .mx_EventTile_selected > .mx_EventTile_line { border-left: $accent-color 4px solid; - padding-left: 60px; + padding-left: calc($left-gutter - 4px) !important; background-color: $event-selected-color; } @@ -191,7 +191,7 @@ $left-gutter: 64px; } .mx_EventTile_selected.mx_EventTile_info .mx_EventTile_line { - padding-left: 78px; + padding-left: calc($left-gutter + 14px) !important; } .mx_EventTile:hover .mx_EventTile_line, @@ -417,7 +417,7 @@ $left-gutter: 64px; .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unknown .mx_EventTile_line { - padding-left: 60px; + padding-left: calc($left-gutter - 4px); } .mx_EventTile:hover.mx_EventTile_verified .mx_EventTile_line { @@ -435,7 +435,7 @@ $left-gutter: 64px; .mx_EventTile:hover.mx_EventTile_verified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unverified.mx_EventTile_info .mx_EventTile_line, .mx_EventTile:hover.mx_EventTile_unknown.mx_EventTile_info .mx_EventTile_line { - padding-left: 78px; + padding-left: calc($left-gutter + 14px); } /* End to end encryption stuff */ diff --git a/res/themes/dark/css/_dark.scss b/res/themes/dark/css/_dark.scss index 4701169c1dc..5ddad818f3d 100644 --- a/res/themes/dark/css/_dark.scss +++ b/res/themes/dark/css/_dark.scss @@ -177,8 +177,8 @@ $visual-bell-bg-color: #800; $room-warning-bg-color: $header-panel-bg-color; $dark-panel-bg-color: $header-panel-bg-color; -$message-bubble-incoming: #303030; -$message-bubble-outgoing: #424242; +$message-bubble-left: #303030; +$message-bubble-right: #424242; $panel-gradient: rgba(48, 48, 48, 0), rgba(48, 48, 48, 1); $message-action-bar-bg-color: $header-panel-bg-color; diff --git a/res/themes/legacy-dark/css/_legacy-dark.scss b/res/themes/legacy-dark/css/_legacy-dark.scss index 31e1d6702f6..3eecd8dad2c 100644 --- a/res/themes/legacy-dark/css/_legacy-dark.scss +++ b/res/themes/legacy-dark/css/_legacy-dark.scss @@ -172,8 +172,8 @@ $visual-bell-bg-color: #800; $room-warning-bg-color: $header-panel-bg-color; $dark-panel-bg-color: $header-panel-bg-color; -$message-bubble-incoming: $dark-panel-bg-color; -$message-bubble-outgoing: $dark-panel-bg-color; +$message-bubble-left: $dark-panel-bg-color; +$message-bubble-right: $dark-panel-bg-color; $panel-gradient: rgba(48, 48, 48, 0), rgba(48, 48, 48, 1); $message-action-bar-bg-color: $header-panel-bg-color; diff --git a/res/themes/legacy-light/css/_legacy-light.scss b/res/themes/legacy-light/css/_legacy-light.scss index 891c14c6250..b197e7a1e22 100644 --- a/res/themes/legacy-light/css/_legacy-light.scss +++ b/res/themes/legacy-light/css/_legacy-light.scss @@ -295,8 +295,8 @@ $authpage-primary-color: #232f32; $authpage-secondary-color: #616161; $dark-panel-bg-color: $secondary-accent-color; -$message-bubble-incoming: $dark-panel-bg-color; -$message-bubble-outgoing: $dark-panel-bg-color; +$message-bubble-left: $dark-panel-bg-color; +$message-bubble-right: $dark-panel-bg-color; $panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1); $message-action-bar-bg-color: $primary-bg-color; diff --git a/res/themes/light/css/_light.scss b/res/themes/light/css/_light.scss index 537b77ce46a..080b7f00b47 100644 --- a/res/themes/light/css/_light.scss +++ b/res/themes/light/css/_light.scss @@ -294,8 +294,8 @@ $authpage-primary-color: #232f32; $authpage-secondary-color: #616161; $dark-panel-bg-color: $secondary-accent-color; -$message-bubble-incoming: #dedede; -$message-bubble-outgoing: #DCEDC8; +$message-bubble-left: #dedede; +$message-bubble-right: #DCEDC8; $panel-gradient: rgba(242, 245, 248, 0), rgba(242, 245, 248, 1); $message-action-bar-bg-color: $primary-bg-color; diff --git a/src/components/views/rooms/EventTile.js b/src/components/views/rooms/EventTile.js index 11277daa573..c4d07a8cd37 100644 --- a/src/components/views/rooms/EventTile.js +++ b/src/components/views/rooms/EventTile.js @@ -228,6 +228,9 @@ export default class EventTile extends React.Component { // whether to use the irc layout useIRCLayout: PropTypes.bool, + // whether to use the message bubble layout + useBubbleLayout: PropTypes.bool, + // whether or not to show flair at all enableFlair: PropTypes.bool, }; @@ -474,7 +477,11 @@ export default class EventTile extends React.Component { // If hidden, set offset equal to the offset of the final visible avatar or // else set it proportional to index - left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset; + if (this.props.useBubbleLayout) { + left = (hidden ? MAX_READ_AVATARS - 1 : i) * receiptOffset; + } else { + left = (hidden ? MAX_READ_AVATARS - 1 : i) * -receiptOffset; + } const userId = receipt.userId; let readReceiptInfo; @@ -504,10 +511,18 @@ export default class EventTile extends React.Component { let remText; if (!this.state.allReadAvatars) { const remainder = receipts.length - MAX_READ_AVATARS; + + let style; + if (this.props.useBubbleLayout) { + style = { left: "calc(" + toRem(left) + " + " + receiptOffset + "px)" }; + } else { + style = { right: "calc(" + toRem(-left) + " + " + receiptOffset + "px)" }; + } + if (remainder > 0) { remText = { remainder }+ + style={style}>{ remainder }+ ; } } @@ -682,6 +697,14 @@ export default class EventTile extends React.Component { const isRedacted = isMessageEvent(this.props.mxEvent) && this.props.isRedacted; const isEncryptionFailure = this.props.mxEvent.isDecryptionFailure(); + const client = MatrixClientPeg.get(); + const me = client && client.getUserId(); + const scBubbleEnabled = this.props.useBubbleLayout + && !isBubbleMessage && !isInfoMessage + && this.props.tileShape !== 'reply_preview' && this.props.tileShape !== 'reply' + && this.props.tileShape !== 'notif' && this.props.tileShape !== 'file_grid'; + const sentByMe = me === this.props.mxEvent.getSender(); + const isEditing = !!this.props.editState; const classes = classNames({ mx_EventTile_bubbleContainer: isBubbleMessage, @@ -704,6 +727,8 @@ export default class EventTile extends React.Component { mx_EventTile_unknown: !isBubbleMessage && this.state.verified === E2E_STATE.UNKNOWN, mx_EventTile_bad: isEncryptionFailure, mx_EventTile_emote: msgtype === 'm.emote', + sc_EventTile_bubbleContainer: scBubbleEnabled, + sc_EventTile_bubbleTailLeftContainer: scBubbleEnabled && !sentByMe && !this.props.continuation, }); // If the tile is in the Sending state, don't speak the message. @@ -721,7 +746,10 @@ export default class EventTile extends React.Component { let avatarSize; let needsSenderProfile; - if (this.props.tileShape === "notif") { + if (scBubbleEnabled && sentByMe) { + avatarSize = 0; + needsSenderProfile = false; + } else if (this.props.tileShape === "notif") { avatarSize = 24; needsSenderProfile = true; } else if (tileHandler === 'messages.RoomCreate' || isBubbleMessage) { @@ -787,6 +815,7 @@ export default class EventTile extends React.Component { getTile={this.getTile} getReplyThread={this.getReplyThread} onFocusChange={this.onActionBarFocusChange} + showLeft={!sentByMe} /> : undefined; const timestamp = this.props.mxEvent.getTs() ? @@ -835,7 +864,7 @@ export default class EventTile extends React.Component { />; } - const linkedTimestamp = - { ircTimestamp } -
- { readAvatars } -
- { sender } - { ircPadlock } -
- { groupTimestamp } - { groupPadlock } - { thread } - - { keyRequestInfo } - { reactionsRow } - { actionBar } -
- { - // The avatar goes after the event tile as it's absolutely positioned to be over the - // event tile line, so needs to be later in the DOM so it appears on top (this avoids - // the need for further z-indexing chaos) - } - { avatar } + let msgOptionClasses = classNames( + "mx_EventTile_msgOption", + { "sc_readReceipts_empty": !this.props.readReceipts || this.props.readReceipts.length === 0 }, + ); + const msgOption = ( +
+ { readAvatars }
); + + if (scBubbleEnabled) { + const bubbleAreaClasses = classNames( + "sc_EventTile_bubbleArea", + { + "sc_EventTile_bubbleArea_right": sentByMe, + "sc_EventTile_bubbleArea_left": !sentByMe, + }, + ); + const bubbleClasses = classNames( + "sc_EventTile_bubble", + { + "sc_EventTile_bubble_right": sentByMe, + "sc_EventTile_bubble_left": !sentByMe, + "sc_EventTile_bubble_tail": !this.props.continuation, + }, + ); + + + // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers + return ( +
+ { ircTimestamp } + { ircPadlock } +
+ { groupPadlock } +
+
+ { sender } + { thread } + +
+ { keyRequestInfo } + { reactionsRow } + { actionBar } +
+
+ { + // The avatar goes after the event tile as it's absolutely positioned to be over the + // event tile line, so needs to be later in the DOM so it appears on top (this avoids + // the need for further z-indexing chaos) + } + { avatar } + { msgOption } +
+ ); + } else { + // tab-index=-1 to allow it to be focusable but do not add tab stop for it, primarily for screen readers + return ( +
+ { ircTimestamp } + { !this.props.useBubbleLayout ? msgOption : null } + { sender } + { ircPadlock } +
+ { groupTimestamp } + { groupPadlock } + { thread } + + { keyRequestInfo } + { reactionsRow } + { actionBar } +
+ { + // The avatar goes after the event tile as it's absolutely positioned to be over the + // event tile line, so needs to be later in the DOM so it appears on top (this avoids + // the need for further z-indexing chaos) + } + { avatar } + { this.props.useBubbleLayout ? msgOption : null } +
+ ); + } } } }