From dc6ab455921a3c5a2df948f5029f5bc2854c4da6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 3 Nov 2022 03:06:57 -0500 Subject: [PATCH] Add a way to highlight tiles Part of https://github.com/matrix-org/matrix-public-archive/issues/4 --- doc/SDK.md | 2 +- .../room/timeline/TimelineViewModel.js | 7 +++ .../room/timeline/tiles/BaseMessageTile.js | 4 ++ .../session/room/timeline/tiles/SimpleTile.js | 4 ++ .../room/timeline/entries/EventEntry.js | 9 +++ .../web/ui/css/themes/element/timeline.css | 7 ++- src/platform/web/ui/session/room/RoomView.js | 2 +- .../web/ui/session/room/TimelineView.ts | 62 +++---------------- .../session/room/timeline/AnnouncementView.js | 5 +- .../session/room/timeline/BaseMessageView.js | 1 + 10 files changed, 47 insertions(+), 56 deletions(-) diff --git a/doc/SDK.md b/doc/SDK.md index d7286deb5f..c8f5197fc1 100644 --- a/doc/SDK.md +++ b/doc/SDK.md @@ -89,7 +89,7 @@ async function main() { navigation, }); await vm.load(); - const view = new TimelineView(vm.timelineViewModel, { viewClassForTile }); + const view = new TimelineView(vm.timelineViewModel, viewClassForTile); app.appendChild(view.mount()); } } diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index b5dd627346..c77d990620 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -96,6 +96,13 @@ export class TimelineViewModel extends ViewModel { } } + setEventHighlight(eventId, newHighlightValue) { + const eventEntry = this._timeline.getByEventId(eventId); + if(eventEntry) { + eventEntry.setIsHighlighted(newHighlightValue); + } + } + get tiles() { return this._tiles; } diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 560cdf2f35..9c70768dc4 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -98,6 +98,10 @@ export class BaseMessageTile extends SimpleTile { return this._isContinuation; } + get isHighlighted() { + return this._entry.isHighlighted; + } + get isUnverified() { return this._entry.isUnverified; } diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index 0414157644..e3481d37ed 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -37,6 +37,10 @@ export class SimpleTile extends ViewModel { return false; } + get isHighlighted() { + return false; + } + get hasDateSeparator() { return false; } diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index 3f2a85db21..9baf00a0bb 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -22,6 +22,7 @@ export class EventEntry extends BaseEventEntry { constructor(eventEntry, fragmentIdComparer) { super(fragmentIdComparer); this._eventEntry = eventEntry; + this._isHighlighted = false; this._decryptionError = null; this._decryptionResult = null; } @@ -96,6 +97,14 @@ export class EventEntry extends BaseEventEntry { return this._eventEntry.event.event_id; } + get isHighlighted() { + return this._isHighlighted; + } + + setIsHighlighted(newValue) { + this._isHighlighted = newValue; + } + setDecryptionResult(result) { this._decryptionResult = result; } diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 43c57d191d..b0b62cc1c0 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -43,6 +43,12 @@ limitations under the License. /* TODO: check whether this is needed for .media to maintain aspect ratio (on IE11) like the 100% above */ /* width: 100%; */ box-sizing: border-box; + border-radius: 4px; +} + +.Timeline_message.highlighted, +.AnnouncementView.highlighted { + background-color: #ffff8a; } .Timeline_message:not(.continuation) { @@ -79,7 +85,6 @@ limitations under the License. .Timeline_message:hover:not(.disabled), .Timeline_message.selected, .Timeline_message.menuOpen { /* needs transparency support */ background-color: rgba(141, 151, 165, 0.1); - border-radius: 4px; } .Timeline_message:hover > .Timeline_messageOptions, diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index c58bf002ee..956a0bcb26 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -70,7 +70,7 @@ export class RoomView extends TemplateView { )]), t.mapView(vm => vm.timelineViewModel, timelineViewModel => { return timelineViewModel ? - new TimelineView(timelineViewModel, { viewClassForTile: this._viewClassForTile }) : + new TimelineView(timelineViewModel, this._viewClassForTile) : new TimelineLoadingView(vm); // vm is just needed for i18n }), t.mapView(vm => vm.composerViewModel, diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 8af3868162..5a04991fba 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -42,15 +42,10 @@ export interface TimelineViewModel extends IObservableValue { setVisibleTileRange(start?: SimpleTile, end?: SimpleTile); } -function top(node: HTMLElement): number { - return node.offsetTop; -} - function bottom(node: HTMLElement): number { return node.offsetTop + node.clientHeight; } - function findFirstNodeIndexAtOrBelow(tiles: HTMLElement, top: number, startIndex: number = (tiles.children.length - 1)): number { for (var i = startIndex; i >= 0; i--) { const node = tiles.children[i] as HTMLElement; @@ -62,39 +57,16 @@ function findFirstNodeIndexAtOrBelow(tiles: HTMLElement, top: number, startIndex return 0; } -type AnchorPosition = "bottom" | "top"; - -interface TimelineViewOpts { - viewClassForTile: ViewClassForEntryFn - initialAnchorEventId?: string - stickToBottom?: boolean - anchorPosition?: AnchorPosition -} - export class TimelineView extends TemplateView { - private initialAnchorEventId?: string; private anchoredNode?: HTMLElement; - private anchoredOffset: number = 0; - private anchorPosition: AnchorPosition = 'bottom'; + private anchoredBottom: number = 0; private stickToBottom: boolean = true; private tilesView?: TilesListView; private resizeObserver?: ResizeObserver; - private viewClassForTile: ViewClassForEntryFn; - constructor(vm: TimelineViewModel, { - viewClassForTile, - initialAnchorEventId, - stickToBottom = true, - // TODO: This should default to 'bottom' - anchorPosition= 'top', - }: TimelineViewOpts) { + constructor(vm: TimelineViewModel, private readonly viewClassForTile: ViewClassForEntryFn) { super(vm); - - this.viewClassForTile = viewClassForTile; - this.stickToBottom = stickToBottom; - this.anchorPosition = anchorPosition; - this.initialAnchorEventId = initialAnchorEventId; } render(t: Builder, vm: TimelineViewModel) { @@ -152,19 +124,7 @@ export class TimelineView extends TemplateView { } private restoreScrollPosition() { - console.log('restoreScrollPosition', new Error().stack); - const {scrollNode, tilesNode, initialAnchorEventId} = this; - - // If there is an initial eventId to anchor to, set this once to scroll to first. - // We have to handle this here after render when the HTML nodes are available. - if (initialAnchorEventId) { - const eventTile = tilesNode.querySelector(`[data-event-id="${initialAnchorEventId}"]`); - if (eventTile) { - this.anchoredNode = eventTile as HTMLElement; - } - // Clear this out after we set the `anchoredNode` once - this.initialAnchorEventId = undefined; - } + const {scrollNode, tilesNode} = this; const missingTilesHeight = scrollNode.clientHeight - tilesNode.clientHeight; if (missingTilesHeight > 0) { @@ -177,19 +137,18 @@ export class TimelineView extends TemplateView { if (this.stickToBottom) { scrollNode.scrollTop = scrollNode.scrollHeight; } else if (this.anchoredNode) { - const offsetFunc = this.anchorPosition === 'bottom' ? bottom : top; - const newAnchoredOffset = offsetFunc(this.anchoredNode!); - if (newAnchoredOffset !== this.anchoredOffset) { - const offsetDiff = newAnchoredOffset - this.anchoredOffset; + const newAnchoredBottom = bottom(this.anchoredNode!); + if (newAnchoredBottom !== this.anchoredBottom) { + const bottomDiff = newAnchoredBottom - this.anchoredBottom; // scrollBy tends to create less scroll jumps than reassigning scrollTop as it does // not depend on reading scrollTop, which might be out of date as some platforms // run scrolling off the main thread. if (typeof scrollNode.scrollBy === "function") { - scrollNode.scrollBy(0, offsetDiff); + scrollNode.scrollBy(0, bottomDiff); } else { - scrollNode.scrollTop = scrollNode.scrollTop + offsetDiff; + scrollNode.scrollTop = scrollNode.scrollTop + bottomDiff; } - this.anchoredOffset = newAnchoredOffset; + this.anchoredBottom = newAnchoredBottom; } } // TODO: should we be updating the visible range here as well as the range might have changed even though @@ -210,8 +169,7 @@ export class TimelineView extends TemplateView { const viewportBottom = scrollTop + clientHeight; const anchoredNodeIndex = findFirstNodeIndexAtOrBelow(tilesNode, viewportBottom); this.anchoredNode = tilesNode.childNodes[anchoredNodeIndex] as HTMLElement; - const offsetFunc = this.anchorPosition === 'bottom' ? bottom : top; - this.anchoredOffset = offsetFunc(this.anchoredNode!); + this.anchoredBottom = bottom(this.anchoredNode!); bottomNodeIndex = anchoredNodeIndex; } let topNodeIndex = findFirstNodeIndexAtOrBelow(tilesNode, scrollTop, bottomNodeIndex); diff --git a/src/platform/web/ui/session/room/timeline/AnnouncementView.js b/src/platform/web/ui/session/room/timeline/AnnouncementView.js index 8b68d33bd5..36144f6de6 100644 --- a/src/platform/web/ui/session/room/timeline/AnnouncementView.js +++ b/src/platform/web/ui/session/room/timeline/AnnouncementView.js @@ -24,7 +24,10 @@ export class AnnouncementView extends TemplateView { render(t, vm) { return t.li({ - className: "AnnouncementView", + className: { + "AnnouncementView": true, + "highlighted": vm => vm.isHighlighted, + }, 'data-event-id': vm.eventId }, t.div(vm => vm.announcement)); } diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 74b96ecf75..1db73fddd8 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -49,6 +49,7 @@ export class BaseMessageView extends TemplateView { unverified: vm.isUnverified, disabled: !this._interactive, continuation: vm => vm.isContinuation, + "highlighted": vm => vm.isHighlighted, }, 'data-event-id': vm.eventId }, children);