diff --git a/src/platform/web/ui/css/themes/element/theme.css b/src/platform/web/ui/css/themes/element/theme.css index c6c18445..ac197f1d 100644 --- a/src/platform/web/ui/css/themes/element/theme.css +++ b/src/platform/web/ui/css/themes/element/theme.css @@ -16,6 +16,7 @@ limitations under the License. */ @import url('inter.css'); +@import url('timeline.css'); :root { font-size: 10px; @@ -508,168 +509,6 @@ a { background-color: #E3E8F0; } -ul.Timeline > li:not(.continuation) { - margin-top: 7px; -} - -ul.Timeline > li.continuation .profile { - display: none; -} - -ul.Timeline > li.continuation time { - display: none; -} - -ul.Timeline > li.messageStatus .message-container > p { - font-style: italic; - color: #777; -} - -.message-container { - padding: 1px 10px 0px 10px; - margin: 5px 10px 0 10px; - /* so the .media can grow horizontally and its spacer can grow vertically */ - width: 100%; -} - -.message-container .profile { - display: flex; - align-items: center; -} - -.TextMessageView { - width: 100%; -} - -.TextMessageView.continuation .message-container { - margin-top: 0; - margin-bottom: 0; -} - -.message-container .sender { - margin: 6px 0; - margin-left: 6px; - font-weight: bold; - line-height: 1.7rem; -} - -.hydrogen .sender.usercolor1 { color: var(--usercolor1); } -.hydrogen .sender.usercolor2 { color: var(--usercolor2); } -.hydrogen .sender.usercolor3 { color: var(--usercolor3); } -.hydrogen .sender.usercolor4 { color: var(--usercolor4); } -.hydrogen .sender.usercolor5 { color: var(--usercolor5); } -.hydrogen .sender.usercolor6 { color: var(--usercolor6); } -.hydrogen .sender.usercolor7 { color: var(--usercolor7); } -.hydrogen .sender.usercolor8 { color: var(--usercolor8); } - -.message-container time { - padding: 2px 0 0px 10px; - font-size: 0.8em; - line-height: normal; - color: #aaa; -} - - -.message-container .media { - display: grid; - margin-top: 4px; - width: 100%; -} - - -.message-container .media > a { - text-decoration: none; - width: 100%; - display: block; -} - -/* .spacer grows with an inline padding-top to the size of the image, -so the timeline doesn't jump when the image loads */ -.message-container .media > * { - grid-row: 1; - grid-column: 1; -} - -.message-container .media img, .message-container .media video { - width: 100%; - height: auto; - /* for IE11 to still scale even though the spacer is too tall */ - align-self: start; - border-radius: 4px; - display: block; -} -/* stretch the image (to the spacer) on platforms -where we can trust the spacer to always have the correct height, -otherwise the image starts with height 0 and with loading=lazy -only loads when the top comes into view*/ -.hydrogen:not(.legacy) .message-container .media img, -.hydrogen:not(.legacy) .message-container .media video { - align-self: stretch; -} - -.message-container .media > .sendStatus { - align-self: end; - justify-self: start; - font-size: 0.8em; -} - -.message-container .media > progress { - align-self: center; - justify-self: center; - width: 75%; -} - -.message-container .media > time { - align-self: end; - justify-self: end; -} - -.message-container .media > time, -.message-container .media > .sendStatus { - color: #2e2f32; - display: block; - padding: 2px; - margin: 4px; - background-color: rgba(255, 255, 255, 0.75); - border-radius: 4px; -} -.message-container .media > .spacer { - /* TODO: can we implement this with a pseudo element? or perhaps they are not grid items? */ - width: 100%; - /* don't stretch height as it is a spacer, just in case it doesn't match with image height */ - align-self: start; -} - -.TextMessageView.unsent .message-container { - color: #ccc; -} - -.TextMessageView.unverified .message-container { - color: #ff4b55; -} - -.message-container p { - margin: 3px 0; - line-height: 2.2rem; -} - -.AnnouncementView { - margin: 5px 0; - padding: 5px 10%; -} - -.AnnouncementView > div { - margin: 0 auto; - padding: 10px 20px; - background-color: rgba(245, 245, 245, 0.90); - text-align: center; - border-radius: 10px; -} - -.GapView > :not(:first-child) { - margin-left: 12px; -} - .SettingsBody { padding: 0px 16px; } diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css new file mode 100644 index 00000000..666e0ecd --- /dev/null +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -0,0 +1,218 @@ +/* +Copyright 2020 Bruno Windels +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. +*/ + +ul.Timeline > li.messageStatus .Timeline_messageBody > p { + font-style: italic; + color: #777; +} + +.Timeline_message { + display: grid; + grid-template: + "avatar sender" auto + "avatar body" auto + "time body" 1fr / + 30px 1fr; + column-gap: 8px; + padding: 4px; + margin: 0 12px; + /* TODO: check whether this is needed for .media to maintain aspect ratio (on IE11) like the 100% above */ + /* width: 100%; */ + box-sizing: border-box; +} + +.Timeline_message:not(.continuation) { + margin-top: 4px; +} + +@media screen and (max-width: 800px) { + .Timeline_message { + grid-template: + "avatar sender" auto + "body body" 1fr + "time time" auto / + 30px 1fr; + } + + .Timeline_messageSender { + margin-top: 0 !important; + align-self: center; + } +} + +.Timeline_message:hover, .Timeline_message.selected { + background-color: rgba(141, 151, 165, 0.1); + border-radius: 4px; +} + +.Timeline_message:hover > .Timeline_messageOptions { + display: block; +} + +.Timeline_messageAvatar { + grid-area: avatar; +} + +.Timeline_messageSender { + grid-area: sender; + font-weight: bold; + line-height: 1.7rem; +} + +.Timeline_messageSender, .Timeline_messageBody { + /* reset body margin */ + margin: 0; +} + +.Timeline_message:not(.continuation) .Timeline_messageSender, +.Timeline_message:not(.continuation) .Timeline_messageBody { + margin-top: 4px; +} + +.Timeline_messageOptions { + display: none; + grid-area: body; + align-self: start; + justify-self: right; + margin-top: -12px; + margin-right: 4px; +} + +.Timeline_messageTime { + grid-area: time; +} + +.Timeline_messageBody time { + padding: 2px 0 0px 10px; +} + +.Timeline_messageBody time, .Timeline_messageTime { + font-size: 0.8em; + line-height: normal; + color: #aaa; +} + +.Timeline_messageBody { + grid-area: body; + line-height: 2.2rem; + /* so the .media can grow horizontally and its spacer can grow vertically */ + width: 100%; +} + +.hydrogen .Timeline_messageSender.usercolor1 { color: var(--usercolor1); } +.hydrogen .Timeline_messageSender.usercolor2 { color: var(--usercolor2); } +.hydrogen .Timeline_messageSender.usercolor3 { color: var(--usercolor3); } +.hydrogen .Timeline_messageSender.usercolor4 { color: var(--usercolor4); } +.hydrogen .Timeline_messageSender.usercolor5 { color: var(--usercolor5); } +.hydrogen .Timeline_messageSender.usercolor6 { color: var(--usercolor6); } +.hydrogen .Timeline_messageSender.usercolor7 { color: var(--usercolor7); } +.hydrogen .Timeline_messageSender.usercolor8 { color: var(--usercolor8); } + + +.Timeline_messageBody .media { + display: grid; + margin-top: 4px; + width: 100%; +} + +.Timeline_messageBody .media > a { + text-decoration: none; + width: 100%; + display: block; +} + +/* .spacer grows with an inline padding-top to the size of the image, +so the timeline doesn't jump when the image loads */ +.Timeline_messageBody .media > * { + grid-row: 1; + grid-column: 1; +} + +.Timeline_messageBody .media img, .Timeline_messageBody .media video { + width: 100%; + height: auto; + /* for IE11 to still scale even though the spacer is too tall */ + align-self: start; + border-radius: 4px; + display: block; +} +/* stretch the image (to the spacer) on platforms +where we can trust the spacer to always have the correct height, +otherwise the image starts with height 0 and with loading=lazy +only loads when the top comes into view*/ +.hydrogen:not(.legacy) .Timeline_messageBody .media img, +.hydrogen:not(.legacy) .Timeline_messageBody .media video { + align-self: stretch; +} + +.Timeline_messageBody .media > .sendStatus { + align-self: end; + justify-self: start; + font-size: 0.8em; +} + +.Timeline_messageBody .media > progress { + align-self: center; + justify-self: center; + width: 75%; +} + +.Timeline_messageBody .media > time { + align-self: end; + justify-self: end; +} + +.Timeline_messageBody .media > time, +.Timeline_messageBody .media > .sendStatus { + color: #2e2f32; + display: block; + padding: 2px; + margin: 4px; + background-color: rgba(255, 255, 255, 0.75); + border-radius: 4px; +} +.Timeline_messageBody .media > .spacer { + /* TODO: can we implement this with a pseudo element? or perhaps they are not grid items? */ + width: 100%; + /* don't stretch height as it is a spacer, just in case it doesn't match with image height */ + align-self: start; +} + +.Timeline_messageBody.unsent .Timeline_messageBody { + color: #ccc; +} + +.Timeline_messageBody.unverified .Timeline_messageBody { + color: #ff4b55; +} + +.AnnouncementView { + margin: 5px 0; + padding: 5px 10%; +} + +.AnnouncementView > div { + margin: 0 auto; + padding: 10px 20px; + background-color: rgba(245, 245, 245, 0.90); + text-align: center; + border-radius: 10px; +} + +.GapView > :not(:first-child) { + margin-left: 12px; +} diff --git a/src/platform/web/ui/css/timeline.css b/src/platform/web/ui/css/timeline.css index 5d082c08..60af23d9 100644 --- a/src/platform/web/ui/css/timeline.css +++ b/src/platform/web/ui/css/timeline.css @@ -43,11 +43,6 @@ limitations under the License. display: block; } -.TextMessageView { - display: flex; - min-width: 0; -} - .AnnouncementView { display: flex; align-items: center; diff --git a/src/platform/web/ui/session/room/timeline/BaseMediaView.js b/src/platform/web/ui/session/room/timeline/BaseMediaView.js index b5b0ed4b..2b6b07e1 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMediaView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMediaView.js @@ -52,9 +52,9 @@ export class BaseMediaView extends TemplateView { }); children.push(sendStatus, progress); } - return renderMessage(t, vm, [ + return renderMessage(t, vm, t.div({className: "Timeline_messageBody"}, [ t.div({className: "media", style: `max-width: ${vm.width}px`}, children), t.if(vm => vm.error, t => t.p({className: "error"}, vm.error)) - ]); + ])); } } diff --git a/src/platform/web/ui/session/room/timeline/FileView.js b/src/platform/web/ui/session/room/timeline/FileView.js index 62760b3e..aecca858 100644 --- a/src/platform/web/ui/session/room/timeline/FileView.js +++ b/src/platform/web/ui/session/room/timeline/FileView.js @@ -26,7 +26,7 @@ export class FileView extends TemplateView { t.button({className: "link", onClick: () => vm.abortSending()}, vm.i18n`Cancel`), ])); } else { - return renderMessage(t, vm, t.p([ + return renderMessage(t, vm, t.p({className: "Timeline_messageBody"}, [ t.button({className: "link", onClick: () => vm.download()}, vm => vm.label), t.time(vm.date + " " + vm.time) ])); diff --git a/src/platform/web/ui/session/room/timeline/MissingAttachmentView.js b/src/platform/web/ui/session/room/timeline/MissingAttachmentView.js index 8df90131..8afdd6d4 100644 --- a/src/platform/web/ui/session/room/timeline/MissingAttachmentView.js +++ b/src/platform/web/ui/session/room/timeline/MissingAttachmentView.js @@ -20,6 +20,6 @@ import {renderMessage} from "./common.js"; export class MissingAttachmentView extends TemplateView { render(t, vm) { const remove = t.button({className: "link", onClick: () => vm.abortSending()}, vm.i18n`Remove`); - return renderMessage(t, vm, t.p([vm.label, " ", remove])); + return renderMessage(t, vm, t.p({className: "Timeline_messageBody"}, [vm.label, " ", remove])); } } diff --git a/src/platform/web/ui/session/room/timeline/RedactedView.js b/src/platform/web/ui/session/room/timeline/RedactedView.js index 6e9bf105..cdc775b4 100644 --- a/src/platform/web/ui/session/room/timeline/RedactedView.js +++ b/src/platform/web/ui/session/room/timeline/RedactedView.js @@ -21,7 +21,7 @@ export class RedactedView extends TemplateView { render(t, vm) { const cancelButton = t.if(vm => vm.isRedacting, t => t.button({onClick: () => vm.abortPendingRedaction()}, "Cancel")); return renderMessage(t, vm, - [t.p([vm => vm.description, " ", cancelButton, t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time)])] + t.p({className: "Timeline_messageBody"}, [vm => vm.description, " ", cancelButton]) ); } } \ No newline at end of file diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index 6a3b8ed5..b1e7d13d 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -22,9 +22,8 @@ import {renderMessage} from "./common.js"; export class TextMessageView extends TemplateView { render(t, vm) { const bodyView = t.mapView(vm => vm.body, body => new BodyView(body)); - const redactButton = t.button({onClick: () => vm.redact()}, "Redact"); return renderMessage(t, vm, - [t.p([bodyView, redactButton, t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time)])] + t.p({className: "Timeline_messageBody"}, [bodyView, t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time)]) ); } } diff --git a/src/platform/web/ui/session/room/timeline/common.js b/src/platform/web/ui/session/room/timeline/common.js index 3b53245e..e938eb63 100644 --- a/src/platform/web/ui/session/room/timeline/common.js +++ b/src/platform/web/ui/session/room/timeline/common.js @@ -17,23 +17,20 @@ limitations under the License. import {renderStaticAvatar} from "../../../avatar.js"; -export function renderMessage(t, vm, children) { +export function renderMessage(t, vm, body) { const classes = { - "TextMessageView": true, + "Timeline_message": true, own: vm.isOwn, unsent: vm.isUnsent, unverified: vm.isUnverified, continuation: vm => vm.isContinuation, messageStatus: vm => vm.shape === "message-status" || vm.shape === "missing-attachment" || vm.shape === "file" || vm.shape === "redacted", }; - - const profile = t.div({className: "profile"}, [ - renderStaticAvatar(vm, 30), - t.div({className: `sender usercolor${vm.avatarColorNumber}`}, vm.displayName) + return t.li({className: classes}, [ + t.if(vm => !vm.isContinuation, t => renderStaticAvatar(vm, 30, "Timeline_messageAvatar")), + t.if(vm => !vm.isContinuation, t => t.div({className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`}, vm.displayName)), + body, + // should be after body as it is overlayed on top + t.button({className: "Timeline_messageOptions"}, "⋮"), ]); - children = [profile].concat(children); - return t.li( - {className: classes}, - t.div({className: "message-container"}, children) - ); }