diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index c90d6546..82f1c3eb 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -49,12 +49,13 @@ limitations under the License. } } -.Timeline_message:hover, .Timeline_message.selected { +.Timeline_message:hover, .Timeline_message.selected, .Timeline_message.menuOpen { background-color: rgba(141, 151, 165, 0.1); border-radius: 4px; } -.Timeline_message:hover > .Timeline_messageOptions { +.Timeline_message:hover > .Timeline_messageOptions, +.Timeline_message.menuOpen > .Timeline_messageOptions { display: block; } diff --git a/src/platform/web/ui/general/Popup.js b/src/platform/web/ui/general/Popup.js index b927b44b..b5df3a23 100644 --- a/src/platform/web/ui/general/Popup.js +++ b/src/platform/web/ui/general/Popup.js @@ -30,13 +30,14 @@ const VerticalAxis = { }; export class Popup { - constructor(view) { + constructor(view, closeCallback = null) { this._view = view; this._target = null; this._arrangement = null; this._scroller = null; this._fakeRoot = null; this._trackingTemplateView = null; + this._closeCallback = closeCallback; } trackInTemplateView(templateView) { @@ -82,6 +83,9 @@ export class Popup { document.body.removeEventListener("click", this, false); this._popup.remove(); this._view = null; + if (this._closeCallback) { + this._closeCallback(); + } } } diff --git a/src/platform/web/ui/session/room/TimelineList.js b/src/platform/web/ui/session/room/TimelineList.js index 81763c05..a0ad1c83 100644 --- a/src/platform/web/ui/session/room/TimelineList.js +++ b/src/platform/web/ui/session/room/TimelineList.js @@ -45,6 +45,7 @@ export class TimelineList extends ListView { const options = { className: "Timeline bottom-aligned-scroll", list: viewModel.tiles, + onItemClick: (tileView, evt) => tileView.onClick(evt), } super(options, entry => { const View = viewClassForEntry(entry); diff --git a/src/platform/web/ui/session/room/timeline/AnnouncementView.js b/src/platform/web/ui/session/room/timeline/AnnouncementView.js index 43eb91ea..2dd58b32 100644 --- a/src/platform/web/ui/session/room/timeline/AnnouncementView.js +++ b/src/platform/web/ui/session/room/timeline/AnnouncementView.js @@ -20,4 +20,7 @@ export class AnnouncementView extends TemplateView { render(t) { return t.li({className: "AnnouncementView"}, t.div(vm => vm.announcement)); } + + /* This is called by the parent ListView, which just has 1 listener for the whole list */ + onClick() {} } diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index d95ebd90..b110a9aa 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -17,8 +17,15 @@ limitations under the License. import {renderStaticAvatar} from "../../../avatar.js"; import {TemplateView} from "../../../general/TemplateView.js"; +import {Popup} from "../../../general/Popup.js"; +import {Menu} from "../../../general/Menu.js"; export class BaseMessageView extends TemplateView { + constructor(value) { + super(value); + this._menuPopup = null; + } + render(t, vm) { const classes = { "Timeline_message": true, @@ -36,7 +43,46 @@ export class BaseMessageView extends TemplateView { ]); } - renderMessageBody() { - + /* This is called by the parent ListView, which just has 1 listener for the whole list */ + onClick(evt) { + if (evt.target.className === "Timeline_messageOptions") { + this._toggleMenu(evt.target); + } } + + _toggleMenu(button) { + if (this._menuPopup && this._menuPopup.isOpen) { + this._menuPopup.close(); + } else { + const options = this.createMenuOptions(this.value); + if (!options.length) { + return; + } + this.root().classList.add("menuOpen"); + this._menuPopup = new Popup(new Menu(options), () => this.root().classList.remove("menuOpen")); + this._menuPopup.trackInTemplateView(this); + this._menuPopup.showRelativeTo(button, { + horizontal: { + relativeTo: "end", + align: "start", + after: 0 + }, + vertical: { + relativeTo: "start", + align: "end", + before: -24 + } + }); + } + } + + createMenuOptions(vm) { + const options = []; + if (vm.shape !== "redacted") { + options.push(Menu.option(vm.i18n`Delete`, () => vm.redact())); + } + return options; + } + + renderMessageBody() {} } diff --git a/src/platform/web/ui/session/room/timeline/RedactedView.js b/src/platform/web/ui/session/room/timeline/RedactedView.js index bd485b58..14d27d24 100644 --- a/src/platform/web/ui/session/room/timeline/RedactedView.js +++ b/src/platform/web/ui/session/room/timeline/RedactedView.js @@ -15,10 +15,18 @@ limitations under the License. */ import {BaseMessageView} from "./BaseMessageView.js"; +import {Menu} from "../../../general/Menu.js"; export class RedactedView extends BaseMessageView { renderMessageBody(t, vm) { - const cancelButton = t.if(vm => vm.isRedacting, t => t.button({onClick: () => vm.abortPendingRedaction()}, "Cancel")); - return t.p({className: "Timeline_messageBody statusMessage"}, [vm => vm.description, " ", cancelButton]); + return t.p({className: "Timeline_messageBody statusMessage"}, vm => vm.description); + } + + createMenuOptions(vm) { + const options = super.createMenuOptions(vm); + if (vm.isRedacting) { + options.push(Menu.option(vm.i18n`Cancel`, () => vm.abortPendingRedaction())); + } + return options; } } \ No newline at end of file