From dcc508c037ba9996d31328a31771c9f776e17fdd Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 2 Feb 2022 01:08:54 -0600 Subject: [PATCH 01/62] Changes added to work on the Matrix public archive See plan https://docs.google.com/document/d/1wP_TIqmBQjtt862vb2CWWmnmVxTyolcF3J1scuiYMdg/edit# 1. Trying to make it faster/easier to build `hydrogen.es.js` for local linking and dev in `matrix-public-arhive` project 1. Some random changes to accomodate using raw `EventEntry`'s --- scripts/sdk/base-manifest.json | 2 +- scripts/sdk/build.sh | 2 +- src/domain/session/room/RoomViewModel.js | 2 ++ src/domain/session/room/timeline/TimelineViewModel.js | 1 + .../session/room/timeline/tiles/BaseMessageTile.js | 3 ++- src/lib.ts | 7 +++++++ src/matrix/room/timeline/Timeline.js | 10 ++++++++-- src/matrix/room/timeline/persistence/TimelineReader.js | 1 + src/matrix/storage/idb/stores/TimelineEventStore.ts | 8 ++++---- 9 files changed, 27 insertions(+), 9 deletions(-) diff --git a/scripts/sdk/base-manifest.json b/scripts/sdk/base-manifest.json index d3e21d7ba7..f7b9beeb2d 100644 --- a/scripts/sdk/base-manifest.json +++ b/scripts/sdk/base-manifest.json @@ -2,6 +2,6 @@ "name": "hydrogen-view-sdk", "description": "Embeddable matrix client library, including view components", "version": "0.0.4", - "main": "./hydrogen.es.js", + "main": "./lib-build/hydrogen.es.js", "type": "module" } diff --git a/scripts/sdk/build.sh b/scripts/sdk/build.sh index 5534601e7d..149649118f 100755 --- a/scripts/sdk/build.sh +++ b/scripts/sdk/build.sh @@ -1,5 +1,5 @@ #!/bin/bash -rm -rf target +rm -rf target/* yarn run vite build -c vite.sdk-assets-config.js yarn run vite build -c vite.sdk-lib-config.js yarn tsc -p tsconfig-declaration.json diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index ace933fb7e..354eba194a 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -21,6 +21,7 @@ import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../ import {tilesCreator} from "./timeline/tilesCreator.js"; import {ViewModel} from "../../ViewModel.js"; +console.log('RoomViewModel asdfwafeawfefewfewaeafwafewefw'); export class RoomViewModel extends ViewModel { constructor(options) { super(options); @@ -45,6 +46,7 @@ export class RoomViewModel extends ViewModel { this._room.on("change", this._onRoomChange); try { const timeline = await this._room.openTimeline(); + console.log('timeline', timeline.entries); this._tilesCreator = tilesCreator(this.childOptions({ roomVM: this, timeline, diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 9c936218d5..c259168f02 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -36,6 +36,7 @@ import {ViewModel} from "../../../ViewModel.js"; export class TimelineViewModel extends ViewModel { constructor(options) { + console.log('TimelineViewModel asdf', options) super(options); const {timeline, tilesCreator} = options; this._timeline = this.track(timeline); diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 6b0b43563e..a98a3a6bb2 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -87,7 +87,7 @@ export class BaseMessageTile extends SimpleTile { } get isOwn() { - return this._entry.sender === this._ownMember.userId; + return this._ownMember && this._entry.sender === this._ownMember.userId; } get isContinuation() { @@ -124,6 +124,7 @@ export class BaseMessageTile extends SimpleTile { updateEntry(entry, param, tilesCreator) { const action = super.updateEntry(entry, param, tilesCreator); + console.log('updateEntry', entry); if (action.shouldUpdate) { this._updateReactions(); } diff --git a/src/lib.ts b/src/lib.ts index 0aa1bb4400..c9de5516d3 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -24,5 +24,12 @@ export {SessionViewModel} from "./domain/session/SessionViewModel.js"; export {SessionView} from "./platform/web/ui/session/SessionView.js"; export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; +export {MediaRepository} from "./matrix/net/MediaRepository"; +export {TilesCollection} from "./domain/session/room/timeline/TilesCollection.js"; +export {tilesCreator} from "./domain/session/room/timeline/tilesCreator.js"; +export {FragmentIdComparer} from "./matrix/room/timeline/FragmentIdComparer.js"; +export {EventEntry} from "./matrix/room/timeline/entries/EventEntry.js"; +export {encodeKey, decodeKey, encodeEventIdKey, decodeEventIdKey} from "./matrix/storage/idb/stores/TimelineEventStore"; +export {Timeline} from "./matrix/room/timeline/Timeline.js"; export {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js"; export {TimelineView} from "./platform/web/ui/session/room/TimelineView"; diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 90ec29eb5e..66ded2a931 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -86,6 +86,7 @@ export class Timeline { const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(20, txn, log)); try { const entries = await readerRequest.complete(); + console.log('entries', entries) this._loadContextEntriesWhereNeeded(entries); this._setupEntries(entries); } finally { @@ -186,6 +187,8 @@ export class Timeline { } _addLocalRelationsToNewRemoteEntries(entries) { + console.log('_addLocalRelationsToNewRemoteEntries entries', entries) + console.log('this._localEntries?.hasSubscriptions entries', this._localEntries?.hasSubscriptions) // because it is not safe to iterate a derived observable collection // before it has any subscriptions, we bail out if this isn't // the case yet. This can happen when sync adds or replaces entries @@ -199,8 +202,11 @@ export class Timeline { if (!this._localEntries?.hasSubscriptions) { return; } - // find any local relations to this new remote event - for (const pee of this._localEntries) { + console.log('after') + // find any local relations to these new remote events or maybe these + // new remote events reference one of the other new remote events we have. + const entryList = new ConcatList(entries, this._localEntries); + for (const pee of entryList) { // this will work because we set relatedEventId when removing remote echos if (pee.relatedEventId) { const relationTarget = entries.find(e => e.id === pee.relatedEventId); diff --git a/src/matrix/room/timeline/persistence/TimelineReader.js b/src/matrix/room/timeline/persistence/TimelineReader.js index 23ec4ed862..db274e282a 100644 --- a/src/matrix/room/timeline/persistence/TimelineReader.js +++ b/src/matrix/room/timeline/persistence/TimelineReader.js @@ -54,6 +54,7 @@ async function readRawTimelineEntriesWithTxn(roomId, eventKey, direction, amount } else { eventsWithinFragment = await timelineStore.eventsBefore(roomId, eventKey, amount); } + console.log('readRawTimelineEntriesWithTxn eventsWithinFragment', eventsWithinFragment) let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, fragmentIdComparer)); entries = directionalConcat(entries, eventEntries, direction); // prepend or append eventsWithinFragment to entries, and wrap them in EventEntry diff --git a/src/matrix/storage/idb/stores/TimelineEventStore.ts b/src/matrix/storage/idb/stores/TimelineEventStore.ts index bb6f652ff6..91cda0710c 100644 --- a/src/matrix/storage/idb/stores/TimelineEventStore.ts +++ b/src/matrix/storage/idb/stores/TimelineEventStore.ts @@ -40,20 +40,20 @@ interface TimelineEventEntry { type TimelineEventStorageEntry = TimelineEventEntry & { key: string, eventIdKey: string }; -function encodeKey(roomId: string, fragmentId: number, eventIndex: number): string { +export function encodeKey(roomId: string, fragmentId: number, eventIndex: number): string { return `${roomId}|${encodeUint32(fragmentId)}|${encodeUint32(eventIndex)}`; } -function decodeKey(key: string): { roomId: string, eventKey: EventKey } { +export function decodeKey(key: string): { roomId: string, eventKey: EventKey } { const [roomId, fragmentId, eventIndex] = key.split("|"); return {roomId, eventKey: new EventKey(decodeUint32(fragmentId), decodeUint32(eventIndex))}; } -function encodeEventIdKey(roomId: string, eventId: string): string { +export function encodeEventIdKey(roomId: string, eventId: string): string { return `${roomId}|${eventId}`; } -function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string } { +export function decodeEventIdKey(eventIdKey: string): { roomId: string, eventId: string } { const [roomId, eventId] = eventIdKey.split("|"); return {roomId, eventId}; } From 5805ce0310253ef55b8088df4aaeb12b4231cb2d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 2 Feb 2022 01:17:07 -0600 Subject: [PATCH 02/62] Remove some scratch changes --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 3 +-- src/matrix/room/timeline/Timeline.js | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index a98a3a6bb2..f7db1e723e 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -87,7 +87,7 @@ export class BaseMessageTile extends SimpleTile { } get isOwn() { - return this._ownMember && this._entry.sender === this._ownMember.userId; + return this._entry.sender === this._ownMember?.userId; } get isContinuation() { @@ -124,7 +124,6 @@ export class BaseMessageTile extends SimpleTile { updateEntry(entry, param, tilesCreator) { const action = super.updateEntry(entry, param, tilesCreator); - console.log('updateEntry', entry); if (action.shouldUpdate) { this._updateReactions(); } diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 66ded2a931..12997a213a 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -187,8 +187,6 @@ export class Timeline { } _addLocalRelationsToNewRemoteEntries(entries) { - console.log('_addLocalRelationsToNewRemoteEntries entries', entries) - console.log('this._localEntries?.hasSubscriptions entries', this._localEntries?.hasSubscriptions) // because it is not safe to iterate a derived observable collection // before it has any subscriptions, we bail out if this isn't // the case yet. This can happen when sync adds or replaces entries @@ -202,7 +200,6 @@ export class Timeline { if (!this._localEntries?.hasSubscriptions) { return; } - console.log('after') // find any local relations to these new remote events or maybe these // new remote events reference one of the other new remote events we have. const entryList = new ConcatList(entries, this._localEntries); From eda179a154b37fbeb3bbb57d49df38e35448a5ff Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 4 Feb 2022 01:26:18 -0600 Subject: [PATCH 03/62] Remove dom side-effect from rendering --- src/platform/web/ui/session/room/TimelineView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 936b8c7c97..b13b089d97 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -59,7 +59,7 @@ export class TimelineView extends TemplateView { // assume this view will be mounted in the parent DOM straight away requestAnimationFrame(() => { // do initial scroll positioning - this.restoreScrollPosition(); + //this.restoreScrollPosition(); }); this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition()); const root = t.div({className: "Timeline"}, [ From 4eb24db1de36607481618a1dda1bd2e45cc90039 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 9 Feb 2022 01:50:05 -0600 Subject: [PATCH 04/62] Fix reply tiles not showing the new message --- src/platform/web/parsehtml.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/web/parsehtml.js b/src/platform/web/parsehtml.js index 21c8f39a24..6689c5189e 100644 --- a/src/platform/web/parsehtml.js +++ b/src/platform/web/parsehtml.js @@ -64,6 +64,9 @@ export function parseHTML(html) { // If DOMPurify uses DOMParser, can't we just get the built tree from it // instead of re-parsing? const sanitized = DOMPurify.sanitize(html, sanitizeConfig); - const bodyNode = new DOMParser().parseFromString(sanitized, "text/html").body; + // FIXME: DOMParser from linkedom does not behave the same as the browser, + // see https://github.com/WebReflection/linkedom/issues/106 + const bodyNode = new DOMParser().parseFromString(`${sanitized}`, "text/html").documentElement; + return new HTMLParseResult(bodyNode); } From 8d0c4e68b6bb4bd6ba908dc97b4ad73f163ef653 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Feb 2022 01:45:10 -0600 Subject: [PATCH 05/62] Some changes to support RoomView with no composer --- src/domain/session/room/RoomViewModel.js | 1 - src/lib.ts | 2 ++ src/platform/web/ui/session/room/RoomView.js | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 354eba194a..45799f391f 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -21,7 +21,6 @@ import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../ import {tilesCreator} from "./timeline/tilesCreator.js"; import {ViewModel} from "../../ViewModel.js"; -console.log('RoomViewModel asdfwafeawfefewfewaeafwafewefw'); export class RoomViewModel extends ViewModel { constructor(options) { super(options); diff --git a/src/lib.ts b/src/lib.ts index c9de5516d3..b776fce858 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -19,11 +19,13 @@ export {Client, LoadStatus} from "./matrix/Client.js"; // export main view & view models export {createNavigation, createRouter} from "./domain/navigation/index.js"; export {RootViewModel} from "./domain/RootViewModel.js"; +export {TemplateView} from "./platform/web/ui/general/TemplateView"; export {RootView} from "./platform/web/ui/RootView.js"; export {SessionViewModel} from "./domain/session/SessionViewModel.js"; export {SessionView} from "./platform/web/ui/session/SessionView.js"; export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; +export {RightPanelView} from "./platform/web/ui/session/rightpanel/RightPanelView.js"; export {MediaRepository} from "./matrix/net/MediaRepository"; export {TilesCollection} from "./domain/session/room/timeline/TilesCollection.js"; export {tilesCreator} from "./domain/session/room/timeline/tilesCreator.js"; diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index c172766a78..55fbd4f4fd 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -15,6 +15,7 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { text } from "../../general/html"; import {TemplateView} from "../../general/TemplateView"; import {Popup} from "../../general/Popup.js"; import {Menu} from "../../general/Menu.js"; @@ -57,7 +58,7 @@ export class RoomView extends TemplateView { new TimelineView(timelineViewModel) : new TimelineLoadingView(vm); // vm is just needed for i18n }), - t.view(bottomView), + bottomView ? t.view(bottomView) : text(''), ]) ]); } From e75f18c87aaf5a84be9f8f6799af18528c807610 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Feb 2022 02:22:04 -0600 Subject: [PATCH 06/62] Support custom RightPanel content --- src/platform/web/ui/session/rightpanel/RightPanelView.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/platform/web/ui/session/rightpanel/RightPanelView.js b/src/platform/web/ui/session/rightpanel/RightPanelView.js index 5297d2ef73..1588768866 100644 --- a/src/platform/web/ui/session/rightpanel/RightPanelView.js +++ b/src/platform/web/ui/session/rightpanel/RightPanelView.js @@ -39,6 +39,8 @@ export class RightPanelView extends TemplateView { return new MemberListView(vm); case "member-details": return new MemberDetailsView(vm); + case "custom": + return new vm.customView(vm); default: return new LoadingView(); } From 48825ea30ff024bffe15c21af72242c9d43bfa1e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 11 Feb 2022 19:23:45 -0600 Subject: [PATCH 07/62] Use explicit HTML document boilerplate to get consistent results in browser and linkedom (for SSR) Context: - https://github.com/WebReflection/linkedom/issues/106 - https://github.com/WebReflection/linkedom/pull/108 --- src/platform/web/parsehtml.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/platform/web/parsehtml.js b/src/platform/web/parsehtml.js index 6689c5189e..1e646d109b 100644 --- a/src/platform/web/parsehtml.js +++ b/src/platform/web/parsehtml.js @@ -64,9 +64,7 @@ export function parseHTML(html) { // If DOMPurify uses DOMParser, can't we just get the built tree from it // instead of re-parsing? const sanitized = DOMPurify.sanitize(html, sanitizeConfig); - // FIXME: DOMParser from linkedom does not behave the same as the browser, - // see https://github.com/WebReflection/linkedom/issues/106 - const bodyNode = new DOMParser().parseFromString(`${sanitized}`, "text/html").documentElement; + const bodyNode = new DOMParser().parseFromString(`${sanitized}`, "text/html").body; return new HTMLParseResult(bodyNode); } From 6005fcfc5555a85a2705cff637fbc1f8143bc65c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 24 Feb 2022 02:40:37 -0600 Subject: [PATCH 08/62] Add permalink to timestamp --- src/platform/web/ui/session/room/timeline/TextMessageView.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index c0c0cfb089..a83307dd70 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -20,7 +20,9 @@ import {ReplyPreviewError, ReplyPreviewView} from "./ReplyPreviewView.js"; export class TextMessageView extends BaseMessageView { renderMessageBody(t, vm) { - const time = t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time); + const time = t.a({ href: vm.permaLink, target: "_blank" }, [ + t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time) + ]); const container = t.div({ className: { "Timeline_messageBody": true, From a4cdde6f539c6745803aaa9adce7b975123538af Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 24 Feb 2022 02:41:54 -0600 Subject: [PATCH 09/62] Use UTC timestamps and add data attribute for easy targeting in tests --- .../room/timeline/tiles/BaseMessageTile.js | 8 ++++++-- .../session/room/timeline/BaseMessageView.js | 19 +++++++++++-------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index f7db1e723e..10ad7b7cba 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -49,6 +49,10 @@ export class BaseMessageTile extends SimpleTile { return `https://matrix.to/#/${encodeURIComponent(this.sender)}`; } + get eventId() { + return this._entry.id; + } + get displayName() { return this._entry.displayName || this.sender; } @@ -79,11 +83,11 @@ export class BaseMessageTile extends SimpleTile { } get date() { - return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"}); + return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric", timeZone: 'UTC'}); } get time() { - return this._date && this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit"}); + return this._date && this._date.toLocaleTimeString({}, {hour: "numeric", minute: "2-digit", timeZone: 'UTC'}); } get isOwn() { diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index a6fbb9be20..083e0e5e12 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -40,14 +40,17 @@ export class BaseMessageView extends TemplateView { if (this._interactive) { children.push(t.button({className: "Timeline_messageOptions"}, "⋯")); } - const li = t.el(this._tagName, {className: { - "Timeline_message": true, - own: vm.isOwn, - unsent: vm.isUnsent, - unverified: vm.isUnverified, - disabled: !this._interactive, - continuation: vm => vm.isContinuation, - }}, children); + const li = t.el(this._tagName, { + className: { + "Timeline_message": true, + own: vm.isOwn, + unsent: vm.isUnsent, + unverified: vm.isUnverified, + disabled: !this._interactive, + continuation: vm => vm.isContinuation, + }, + 'data-event-id': vm.eventId + }, children); // given that there can be many tiles, we don't add // unneeded DOM nodes in case of a continuation, and we add it // with a side-effect binding to not have to create sub views, From 3e5861993560013173015c04490bd452a4a6435c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 24 Feb 2022 02:43:36 -0600 Subject: [PATCH 10/62] Add more SVG elements --- src/platform/web/ui/general/html.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/general/html.ts b/src/platform/web/ui/general/html.ts index 9ebcfaafcc..b429f2afaa 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -104,8 +104,8 @@ export const TAG_NAMES = { "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote", "table", "thead", "tbody", "tr", "th", "td", "hr", - "pre", "code", "button", "time", "input", "textarea", "label", "form", "progress", "output", "video"], - [SVG_NS]: ["svg", "circle"] + "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", "progress", "output", "video"], + [SVG_NS]: ["svg", "circle", "path"] } as const; export const tag: { [tagName in typeof TAG_NAMES[string][number]]: (attributes?: BasicAttributes | Child | Child[], children?: Child | Child[]) => Element } = {} as any; From fc89bfdd53c704091dba4868b5ca896d4b4b984e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 24 Feb 2022 02:44:16 -0600 Subject: [PATCH 11/62] Get rid of duplicate export --- src/lib.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.ts b/src/lib.ts index 6c113eeb3e..33e1851bd8 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -19,7 +19,6 @@ export {Client, LoadStatus} from "./matrix/Client.js"; // export main view & view models export {createNavigation, createRouter} from "./domain/navigation/index.js"; export {RootViewModel} from "./domain/RootViewModel.js"; -export {TemplateView} from "./platform/web/ui/general/TemplateView"; export {RootView} from "./platform/web/ui/RootView.js"; export {SessionViewModel} from "./domain/session/SessionViewModel.js"; export {SessionView} from "./platform/web/ui/session/SessionView.js"; From 082d997eed2b9d848d2c497468fab9e504ba123d Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 24 Feb 2022 12:13:05 -0600 Subject: [PATCH 12/62] Only try to use window.crypto.subtle in secure contexts to avoid it throwing and stopping all JavaScript Related to https://github.com/vector-im/hydrogen-web/issues/579 ``` Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'deriveBits') at new Crypto at new Platform at mountHydrogen ``` --- src/platform/web/Platform.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/platform/web/Platform.js b/src/platform/web/Platform.js index 9de3d4ce89..b6fdfae550 100644 --- a/src/platform/web/Platform.js +++ b/src/platform/web/Platform.js @@ -143,7 +143,10 @@ export class Platform { this._serviceWorkerHandler.registerAndStart(assetPaths.serviceWorker); } this.notificationService = new NotificationService(this._serviceWorkerHandler, config.push); - this.crypto = new Crypto(cryptoExtras); + // `window.crypto.subtle` is only available in a secure context + if(window.isSecureContext) { + this.crypto = new Crypto(cryptoExtras); + } this.storageFactory = new StorageFactory(this._serviceWorkerHandler); this.sessionInfoStorage = new SessionInfoStorage("hydrogen_sessions_v1"); this.estimateStorageUsage = estimateStorageUsage; From ea2d45cab7c4a7131441c62cd632fb19c137cd34 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 25 Feb 2022 01:48:16 -0600 Subject: [PATCH 13/62] No need to comment this out since linkedom supports it now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit See https://github.com/vector-im/hydrogen-web/pull/653#discussion_r805103800 We can allow this to run now since I added support for `setProperty` in `linkedom` ⏩ https://github.com/WebReflection/linkedom/pull/114 --- src/platform/web/ui/session/room/TimelineView.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index b13b089d97..936b8c7c97 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -59,7 +59,7 @@ export class TimelineView extends TemplateView { // assume this view will be mounted in the parent DOM straight away requestAnimationFrame(() => { // do initial scroll positioning - //this.restoreScrollPosition(); + this.restoreScrollPosition(); }); this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition()); const root = t.div({className: "Timeline"}, [ From ae673862dce4024c361ade3bfef4ad5ec8df3ccf Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 6 Jun 2022 17:53:26 -0500 Subject: [PATCH 14/62] Use correct variable in comment --- src/domain/session/room/timeline/TimelineViewModel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index fc16774f4f..3e12bd2fbd 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -50,7 +50,7 @@ export class TimelineViewModel extends ViewModel { this._showJumpDown = false; } - /** if this.tiles is empty, call this with undefined for both startTile and endTile */ + /** if this._tiles is empty, call this with undefined for both startTile and endTile */ setVisibleTileRange(startTile, endTile) { // don't clear these once done as they are used to check // for more tiles once loadAtTop finishes From 2d3b78b725e66fe261ee01a985ffceaaf6311335 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jun 2022 17:16:58 -0500 Subject: [PATCH 15/62] WIP: Make the lightbox open, not working yet --- src/domain/navigation/URLRouter.js | 2 ++ src/domain/session/SessionViewModel.js | 24 ++------------ src/domain/session/room/LightboxViewModel.js | 16 ++++++---- .../session/room/lightbox-navigation.js | 31 +++++++++++++++++++ src/lib.ts | 2 ++ 5 files changed, 47 insertions(+), 28 deletions(-) create mode 100644 src/domain/session/room/lightbox-navigation.js diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.js index 586eec8a91..81fc311372 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.js @@ -36,6 +36,7 @@ export class URLRouter { } attach() { + console.log('attach'); this._subscription = this._history.subscribe(url => this._applyUrl(url)); // subscribe to path before applying initial url // so redirects in _applyNavPathToHistory are reflected in url bar @@ -75,6 +76,7 @@ export class URLRouter { } _applyUrl(url) { + console.log('URLRouter _applyUrl', url); const navPath = this._urlAsNavPath(url); this._applyNavPathToNavigation(navPath); } diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index a67df3a732..b3a63c9d1c 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -20,7 +20,7 @@ import {RoomViewModel} from "./room/RoomViewModel.js"; import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js"; import {InviteViewModel} from "./room/InviteViewModel.js"; import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.js"; -import {LightboxViewModel} from "./room/LightboxViewModel.js"; +import {setupLightboxNavigation} from "./room/lightbox-navigation.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js"; @@ -81,12 +81,7 @@ export class SessionViewModel extends ViewModel { })); this._updateCreateRoom(createRoom.get()); - const lightbox = this.navigation.observe("lightbox"); - this.track(lightbox.subscribe(eventId => { - this._updateLightbox(eventId); - })); - this._updateLightbox(lightbox.get()); - + setupLightboxNavigation(this, 'lightboxViewModel'); const rightpanel = this.navigation.observe("right-panel"); this.track(rightpanel.subscribe(() => this._updateRightPanel())); @@ -267,21 +262,6 @@ export class SessionViewModel extends ViewModel { this.emitChange("activeMiddleViewModel"); } - _updateLightbox(eventId) { - if (this._lightboxViewModel) { - this._lightboxViewModel = this.disposeTracked(this._lightboxViewModel); - } - if (eventId) { - const room = this._roomFromNavigation(); - this._lightboxViewModel = this.track(new LightboxViewModel(this.childOptions({eventId, room}))); - } - this.emitChange("lightboxViewModel"); - } - - get lightboxViewModel() { - return this._lightboxViewModel; - } - _roomFromNavigation() { const roomId = this.navigation.path.get("room")?.value; const room = this._client.session.rooms.get(roomId); diff --git a/src/domain/session/room/LightboxViewModel.js b/src/domain/session/room/LightboxViewModel.js index 8ce8757a4a..527dfa5de6 100644 --- a/src/domain/session/room/LightboxViewModel.js +++ b/src/domain/session/room/LightboxViewModel.js @@ -23,17 +23,21 @@ export class LightboxViewModel extends ViewModel { this._unencryptedImageUrl = null; this._decryptedImage = null; this._closeUrl = this.urlCreator.urlUntilSegment("room"); - this._eventEntry = null; + this._eventEntry = options.eventEntry; this._date = null; this._subscribeToEvent(options.room, options.eventId); } _subscribeToEvent(room, eventId) { - const eventObservable = room.observeEvent(eventId); - this.track(eventObservable.subscribe(eventEntry => { - this._loadEvent(room, eventEntry); - })); - this._loadEvent(room, eventObservable.get()); + let event = this._eventEntry; + if (!this._eventEntry) { + const eventObservable = room.observeEvent(eventId); + this.track(eventObservable.subscribe(eventEntry => { + this._loadEvent(room, eventEntry); + })); + event = eventObservable.get(); + } + this._loadEvent(room, event); } async _loadEvent(room, eventEntry) { diff --git a/src/domain/session/room/lightbox-navigation.js b/src/domain/session/room/lightbox-navigation.js new file mode 100644 index 0000000000..c56a04f489 --- /dev/null +++ b/src/domain/session/room/lightbox-navigation.js @@ -0,0 +1,31 @@ +import {LightboxViewModel} from "./LightboxViewModel.js"; + +let lightboxViewModelSymbol = Symbol('lightboxViewModel'); + +export function updateLightboxViewModel(vm, fieldName, eventId) { + if (vm[lightboxViewModelSymbol]) { + vm[lightboxViewModelSymbol] = vm.disposeTracked(vm[lightboxViewModelSymbol]); + vm.emitChange(fieldName); + } + if (eventId) { + const room = vm._roomFromNavigation(); + vm[lightboxViewModelSymbol] = vm.track(new LightboxViewModel(vm.childOptions({eventId, room}))); + vm.emitChange(fieldName); + } +} + +// Whenever the page navigates somewhere, keep the lightboxViewModel up to date +export function setupLightboxNavigation(vm, fieldName = 'lightboxViewModel') { + Object.defineProperty(vm, fieldName, { + get: function() { + vm[lightboxViewModelSymbol]; + } + }); + + const lightbox = vm.navigation.observe("lightbox"); + vm.track(lightbox.subscribe(eventId => { + updateLightboxViewModel(vm, fieldName, eventId); + })); + const initialLightBoxEventId = lightbox.get(); + updateLightboxViewModel(vm, fieldName, initialLightBoxEventId); +} diff --git a/src/lib.ts b/src/lib.ts index aa464d972a..c15a89813c 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -25,6 +25,8 @@ export {SessionViewModel} from "./domain/session/SessionViewModel.js"; export {SessionView} from "./platform/web/ui/session/SessionView.js"; export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; +export {LightboxView} from "./platform/web/ui/session/room/LightboxView.js"; +export {setupLightboxNavigation, updateLightboxViewModel} from "./domain/session/room/lightbox-navigation.js"; export {RightPanelView} from "./platform/web/ui/session/rightpanel/RightPanelView.js"; export {MediaRepository} from "./matrix/net/MediaRepository"; export {TilesCollection} from "./domain/session/room/timeline/TilesCollection.js"; From 1a0b1403ef03463905975891b59e289f93bd0d1e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jun 2022 17:55:53 -0500 Subject: [PATCH 16/62] Working lightbox pops up and closes --- src/domain/session/SessionViewModel.js | 7 ++- src/domain/session/room/LightboxViewModel.js | 4 +- .../session/room/lightbox-navigation.js | 56 ++++++++++++++++--- src/lib.ts | 2 +- 4 files changed, 56 insertions(+), 13 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index b3a63c9d1c..3b0c63d124 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -81,7 +81,12 @@ export class SessionViewModel extends ViewModel { })); this._updateCreateRoom(createRoom.get()); - setupLightboxNavigation(this, 'lightboxViewModel'); + setupLightboxNavigation(this, 'lightboxViewModel', (eventId) => { + return { + room, + eventId, + }; + }); const rightpanel = this.navigation.observe("right-panel"); this.track(rightpanel.subscribe(() => this._updateRightPanel())); diff --git a/src/domain/session/room/LightboxViewModel.js b/src/domain/session/room/LightboxViewModel.js index 527dfa5de6..52e518c779 100644 --- a/src/domain/session/room/LightboxViewModel.js +++ b/src/domain/session/room/LightboxViewModel.js @@ -19,11 +19,11 @@ import {ViewModel} from "../../ViewModel"; export class LightboxViewModel extends ViewModel { constructor(options) { super(options); - this._eventId = options.eventId; + this._eventEntry = options.eventEntry; + this._eventId = options.eventId || options.eventEntry.id; this._unencryptedImageUrl = null; this._decryptedImage = null; this._closeUrl = this.urlCreator.urlUntilSegment("room"); - this._eventEntry = options.eventEntry; this._date = null; this._subscribeToEvent(options.room, options.eventId); } diff --git a/src/domain/session/room/lightbox-navigation.js b/src/domain/session/room/lightbox-navigation.js index c56a04f489..1cc2886c40 100644 --- a/src/domain/session/room/lightbox-navigation.js +++ b/src/domain/session/room/lightbox-navigation.js @@ -1,31 +1,69 @@ +/* +Copyright 2022 Bruno Windels +Copyright 2022 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. +*/ + import {LightboxViewModel} from "./LightboxViewModel.js"; +// Store the `LightboxViewModel` under a symbol so no one else can tamper with +// it. This acts like a private field on the class since no one else has the +// symbol to look it up. let lightboxViewModelSymbol = Symbol('lightboxViewModel'); -export function updateLightboxViewModel(vm, fieldName, eventId) { +/** + * Destroys and creates a new the `LightboxViewModel` depending if + * `lightboxChildOptions.eventEntry` or `lightboxChildOptions.eventId` are + * provided. + */ +function updateLightboxViewModel(vm, fieldName, lightboxChildOptions) { + // Remove any existing `LightboxViewModel` before we assemble the new one below if (vm[lightboxViewModelSymbol]) { vm[lightboxViewModelSymbol] = vm.disposeTracked(vm[lightboxViewModelSymbol]); + // Let the `LightboxView` know that the `LightboxViewModel` has changed vm.emitChange(fieldName); } - if (eventId) { - const room = vm._roomFromNavigation(); - vm[lightboxViewModelSymbol] = vm.track(new LightboxViewModel(vm.childOptions({eventId, room}))); + // Create the new `LightboxViewModel` if the `eventEntry` exists directly or + // `eventId` which we can load from the store + if (lightboxChildOptions.eventId || lightboxChildOptions.eventEntry) { + vm[lightboxViewModelSymbol] = vm.track(new LightboxViewModel(vm.childOptions(lightboxChildOptions))); + // Let the `LightboxView` know that the `LightboxViewModel` has changed vm.emitChange(fieldName); } } -// Whenever the page navigates somewhere, keep the lightboxViewModel up to date -export function setupLightboxNavigation(vm, fieldName = 'lightboxViewModel') { +/** + * Handles updating the `LightboxViewModel` whenever the page URL changes and + * emits changes which the `LightboxView` will use to re-render. This is a + * composable piece of logic to call in an existing `ViewModel`'s constructor. + */ +export function setupLightboxNavigation(vm, fieldName = 'lightboxViewModel', lightboxChildOptionsFunction) { + // On the given `vm`, create a getter at `fieldName` that the + // `LightboxViewModel` is exposed at for usage in the view. Object.defineProperty(vm, fieldName, { get: function() { - vm[lightboxViewModelSymbol]; + return vm[lightboxViewModelSymbol]; } }); + // Whenever the page navigates somewhere, keep the `lightboxViewModel` up to date const lightbox = vm.navigation.observe("lightbox"); vm.track(lightbox.subscribe(eventId => { - updateLightboxViewModel(vm, fieldName, eventId); + updateLightboxViewModel(vm, fieldName, lightboxChildOptionsFunction(eventId)); })); + // Also handle the case where the URL already includes `/lightbox/$eventId` (like + // from page-load) const initialLightBoxEventId = lightbox.get(); - updateLightboxViewModel(vm, fieldName, initialLightBoxEventId); + updateLightboxViewModel(vm, fieldName, lightboxChildOptionsFunction(initialLightBoxEventId)); } diff --git a/src/lib.ts b/src/lib.ts index c15a89813c..9d0ca367b1 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -26,7 +26,7 @@ export {SessionView} from "./platform/web/ui/session/SessionView.js"; export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; export {LightboxView} from "./platform/web/ui/session/room/LightboxView.js"; -export {setupLightboxNavigation, updateLightboxViewModel} from "./domain/session/room/lightbox-navigation.js"; +export {setupLightboxNavigation} from "./domain/session/room/lightbox-navigation.js"; export {RightPanelView} from "./platform/web/ui/session/rightpanel/RightPanelView.js"; export {MediaRepository} from "./matrix/net/MediaRepository"; export {TilesCollection} from "./domain/session/room/timeline/TilesCollection.js"; From 5d9dc638ea100a9737e4d714154a5a7111806075 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jun 2022 19:41:08 -0500 Subject: [PATCH 17/62] URL hashes relative to the room of the archive --- src/domain/navigation/URLRouter.js | 2 -- src/lib.ts | 1 + src/platform/web/dom/History.js | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/domain/navigation/URLRouter.js b/src/domain/navigation/URLRouter.js index 81fc311372..586eec8a91 100644 --- a/src/domain/navigation/URLRouter.js +++ b/src/domain/navigation/URLRouter.js @@ -36,7 +36,6 @@ export class URLRouter { } attach() { - console.log('attach'); this._subscription = this._history.subscribe(url => this._applyUrl(url)); // subscribe to path before applying initial url // so redirects in _applyNavPathToHistory are reflected in url bar @@ -76,7 +75,6 @@ export class URLRouter { } _applyUrl(url) { - console.log('URLRouter _applyUrl', url); const navPath = this._urlAsNavPath(url); this._applyNavPathToNavigation(navPath); } diff --git a/src/lib.ts b/src/lib.ts index 9d0ca367b1..6796484ab2 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -71,6 +71,7 @@ export {TextMessageView} from "./platform/web/ui/session/room/timeline/TextMessa export {VideoView} from "./platform/web/ui/session/room/timeline/VideoView.js"; export {Navigation} from "./domain/navigation/Navigation.js"; +export {History} from "./platform/web/dom/History.js"; export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; export {TemplateView} from "./platform/web/ui/general/TemplateView"; diff --git a/src/platform/web/dom/History.js b/src/platform/web/dom/History.js index d51974bb50..7a98987985 100644 --- a/src/platform/web/dom/History.js +++ b/src/platform/web/dom/History.js @@ -30,10 +30,10 @@ export class History extends BaseObservableValue { But for SSO, we need to handle /?loginToken= Handle that as a special case for now. */ - if (document.location.search.includes("loginToken")) { + if (document?.location?.search.includes("loginToken")) { return document.location.search; } - return document.location.hash; + return document?.location?.hash; } /** does not emit */ From 72300d1b0cf63cff6ee8338710f29408604bb7a8 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 7 Jun 2022 22:35:26 -0500 Subject: [PATCH 18/62] Lightbox escape keyboard shortcut also works --- src/domain/ViewModel.ts | 6 ++++++ src/domain/session/room/LightboxViewModel.js | 2 +- src/domain/session/room/timeline/TimelineViewModel.js | 1 - 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 0bc52f6eaa..6e3bfd7790 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -28,12 +28,14 @@ import type {Clock} from "../platform/web/dom/Clock"; import type {ILogger} from "../logging/types"; import type {Navigation} from "./navigation/Navigation"; import type {URLRouter} from "./navigation/URLRouter"; +import type {History} from "../platform/web/dom/History"; export type Options = { platform: Platform logger: ILogger urlCreator: URLRouter navigation: Navigation + history: History emitChange?: (params: any) => void } @@ -142,4 +144,8 @@ export class ViewModel extends EventEmitter<{change get navigation(): Navigation { return this._options.navigation; } + + get history(): History { + return this._options.history; + } } diff --git a/src/domain/session/room/LightboxViewModel.js b/src/domain/session/room/LightboxViewModel.js index 52e518c779..b22ba1c46e 100644 --- a/src/domain/session/room/LightboxViewModel.js +++ b/src/domain/session/room/LightboxViewModel.js @@ -96,6 +96,6 @@ export class LightboxViewModel extends ViewModel { } close() { - this.platform.history.pushUrl(this.closeUrl); + this.history.pushUrl(this.closeUrl); } } diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 3e12bd2fbd..b5dd627346 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -36,7 +36,6 @@ import {ViewModel} from "../../../ViewModel"; export class TimelineViewModel extends ViewModel { constructor(options) { - console.log('TimelineViewModel asdf', options) super(options); const {timeline, tileOptions} = options; this._timeline = this.track(timeline); From 8dc3c13a93c48a33633d7a17a920f88d4dcb2576 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 20 Jul 2022 02:27:18 -0500 Subject: [PATCH 19/62] Ignore missing events --- src/matrix/room/timeline/Timeline.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index d76ad1478c..3700906bad 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -27,7 +27,7 @@ import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js"; import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js"; export class Timeline { - constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, hsApi}) { + constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, hsApi, ignoreMissingEvents}) { this._roomId = roomId; this._storage = storage; this._closeCallback = closeCallback; @@ -50,6 +50,7 @@ export class Timeline { /** Only used to decrypt non-persisted context entries fetched from the homeserver */ this._decryptEntries = null; this._hsApi = hsApi; + this._ignoreMissingEvents = ignoreMissingEvents || false; this.initializePowerLevels(powerLevelsObservable); } @@ -321,18 +322,18 @@ export class Timeline { if (!entry.contextEventId) { continue; } - const id = entry.contextEventId; + const contextEventId = entry.contextEventId; // before looking into remoteEntries, check the entries // that about to be added first - let contextEvent = entries.find(e => e.id === id); + let contextEvent = entries.find(e => e.id === contextEventId); if (!contextEvent) { - contextEvent = this._findLoadedEventById(id); + contextEvent = this._findLoadedEventById(contextEventId); } if (contextEvent) { entry.setContextEntry(contextEvent); // we don't emit an update here, as the add or update // that the callee will emit hasn't been emitted yet. - } else { + } else if(!this._ignoreMissingEvents) { // we don't await here, which is not ideal, // but one of our callers, addEntries, is not async // so there is not much point. From 871cf1ad80417ecd5b8f6da29a13ad553199a624 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 20 Jul 2022 02:27:35 -0500 Subject: [PATCH 20/62] Revert "Ignore missing events" This reverts commit 8dc3c13a93c48a33633d7a17a920f88d4dcb2576. --- src/matrix/room/timeline/Timeline.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/matrix/room/timeline/Timeline.js b/src/matrix/room/timeline/Timeline.js index 3700906bad..d76ad1478c 100644 --- a/src/matrix/room/timeline/Timeline.js +++ b/src/matrix/room/timeline/Timeline.js @@ -27,7 +27,7 @@ import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js"; import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js"; export class Timeline { - constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, hsApi, ignoreMissingEvents}) { + constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, hsApi}) { this._roomId = roomId; this._storage = storage; this._closeCallback = closeCallback; @@ -50,7 +50,6 @@ export class Timeline { /** Only used to decrypt non-persisted context entries fetched from the homeserver */ this._decryptEntries = null; this._hsApi = hsApi; - this._ignoreMissingEvents = ignoreMissingEvents || false; this.initializePowerLevels(powerLevelsObservable); } @@ -322,18 +321,18 @@ export class Timeline { if (!entry.contextEventId) { continue; } - const contextEventId = entry.contextEventId; + const id = entry.contextEventId; // before looking into remoteEntries, check the entries // that about to be added first - let contextEvent = entries.find(e => e.id === contextEventId); + let contextEvent = entries.find(e => e.id === id); if (!contextEvent) { - contextEvent = this._findLoadedEventById(contextEventId); + contextEvent = this._findLoadedEventById(id); } if (contextEvent) { entry.setContextEntry(contextEvent); // we don't emit an update here, as the add or update // that the callee will emit hasn't been emitted yet. - } else if(!this._ignoreMissingEvents) { + } else { // we don't await here, which is not ideal, // but one of our callers, addEntries, is not async // so there is not much point. From c824012968a1fc6257cee93de97caa3bfbb4bbf8 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 21 Jul 2022 17:23:49 -0500 Subject: [PATCH 21/62] `this` doesn't work in strict mode which the SDK is exported as See https://github.com/vector-im/hydrogen-web/pull/373/files#r927145321 --- src/matrix/storage/idb/StorageFactory.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/matrix/storage/idb/StorageFactory.ts b/src/matrix/storage/idb/StorageFactory.ts index 5cb1b6e57c..9f7245580a 100644 --- a/src/matrix/storage/idb/StorageFactory.ts +++ b/src/matrix/storage/idb/StorageFactory.ts @@ -34,12 +34,11 @@ interface ServiceWorkerHandler { async function requestPersistedStorage(): Promise { // don't assume browser so we can run in node with fake-idb - const glob = this; - if (glob?.navigator?.storage?.persist) { - return await glob.navigator.storage.persist(); - } else if (glob?.document.requestStorageAccess) { + if (window?.navigator?.storage?.persist) { + return await window.navigator.storage.persist(); + } else if (window?.document.requestStorageAccess) { try { - await glob.document.requestStorageAccess(); + await window.document.requestStorageAccess(); return true; } catch (err) { return false; From b54e884b7e9a5912020ae0ef5c08b76a47901ca7 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 21 Jul 2022 17:39:42 -0500 Subject: [PATCH 22/62] Expose error when we fail to createObjectStore --- src/matrix/storage/idb/utils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index 4ac373d286..b685f365d2 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -80,10 +80,15 @@ export function openDatabase(name: string, createObjectStore: CreateObjectStore, try { await createObjectStore(db, txn, oldVersion, version); } catch (err) { + console.error(`Failed to createObjectStore in database=${name}`, err); // try aborting on error, if that hasn't been done already try { txn.abort(); - } catch (err) {} + } catch (err) { + // No-op: `InvalidStateError` is only thrown if the transaction has + // already been committed or aborted. Since we wanted the txn to + // be aborted anyway, it doesn't matter if this fails. + } } }; return reqAsPromise(req); From 88e24703fff22705e09fa8f4283845b4b03d7c08 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 21 Jul 2022 19:59:56 -0500 Subject: [PATCH 23/62] Scope log --- src/matrix/storage/idb/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/matrix/storage/idb/utils.ts b/src/matrix/storage/idb/utils.ts index b685f365d2..948bdfafd6 100644 --- a/src/matrix/storage/idb/utils.ts +++ b/src/matrix/storage/idb/utils.ts @@ -80,7 +80,7 @@ export function openDatabase(name: string, createObjectStore: CreateObjectStore, try { await createObjectStore(db, txn, oldVersion, version); } catch (err) { - console.error(`Failed to createObjectStore in database=${name}`, err); + console.error(`openDatabase: Failed to createObjectStore in database=${name}`, err); // try aborting on error, if that hasn't been done already try { txn.abort(); From acaf53de3e9a584af3c1f8c30198f0ec6ebdfcf6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 21 Jul 2022 20:00:16 -0500 Subject: [PATCH 24/62] More exports --- src/lib.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib.ts b/src/lib.ts index 6796484ab2..b81389c430 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -29,6 +29,9 @@ export {LightboxView} from "./platform/web/ui/session/room/LightboxView.js"; export {setupLightboxNavigation} from "./domain/session/room/lightbox-navigation.js"; export {RightPanelView} from "./platform/web/ui/session/rightpanel/RightPanelView.js"; export {MediaRepository} from "./matrix/net/MediaRepository"; +export {HomeServerApi} from "./matrix/net/HomeServerApi"; +export {Storage} from "./matrix/storage/idb/Storage"; +export {StorageFactory} from "./matrix/storage/idb/StorageFactory"; export {TilesCollection} from "./domain/session/room/timeline/TilesCollection.js"; export {FragmentIdComparer} from "./matrix/room/timeline/FragmentIdComparer.js"; export {EventEntry} from "./matrix/room/timeline/entries/EventEntry.js"; From 7c87e0651ec4fb42f1770448563e52dfc240e74f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 29 Aug 2022 22:55:51 -0500 Subject: [PATCH 25/62] WIP: Custom room header --- src/platform/web/ui/session/room/RoomView.js | 30 ++++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 5d342a95d9..e0e7f5efae 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -25,6 +25,23 @@ import {MessageComposer} from "./MessageComposer.js"; import {RoomArchivedView} from "./RoomArchivedView.js"; import {AvatarView} from "../../AvatarView.js"; +class RoomHeaderView extends TemplateView { + render(t, vm) { + return t.div({className: "RoomHeader middle-header"}, [ + t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close room`}), + t.view(new AvatarView(vm, 32)), + t.div({className: "room-description"}, [ + t.h2(vm => vm.name), + ]), + t.button({ + className: "button-utility room-options", + "aria-label":vm.i18n`Room options`, + onClick: evt => this._toggleOptionsMenu(evt) + }) + ]) + } + } + export class RoomView extends TemplateView { constructor(vm, viewClassForTile) { super(vm); @@ -40,18 +57,7 @@ export class RoomView extends TemplateView { bottomView = new RoomArchivedView(vm.composerViewModel); } return t.main({className: "RoomView middle"}, [ - t.div({className: "RoomHeader middle-header"}, [ - t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close room`}), - t.view(new AvatarView(vm, 32)), - t.div({className: "room-description"}, [ - t.h2(vm => vm.name), - ]), - t.button({ - className: "button-utility room-options", - "aria-label":vm.i18n`Room options`, - onClick: evt => this._toggleOptionsMenu(evt) - }) - ]), + //t.view(new RoomHeaderView(vm)), t.div({className: "RoomView_body"}, [ t.div({className: "RoomView_error"}, vm => vm.error), t.mapView(vm => vm.timelineViewModel, timelineViewModel => { From 36f26a048c6670602f3fba2d35df35f512029d51 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Mon, 29 Aug 2022 23:06:14 -0500 Subject: [PATCH 26/62] Try fix no composer --- src/domain/session/SessionViewModel.js | 1 + src/platform/web/ui/session/room/RoomView.js | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index d74a016add..3dd7ea2665 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -85,6 +85,7 @@ export class SessionViewModel extends ViewModel { setupLightboxNavigation(this, 'lightboxViewModel', (eventId) => { return { + // TODO: room is not defined room, eventId, }; diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 0de80c1d90..6c4d5dfa4a 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -73,7 +73,7 @@ export class RoomView extends TemplateView { case "disabled": return new DisabledComposerView(vm.composerViewModel); case "none": - text(''), + text(''); } }), ]) From bdf820ac07fbbbf33093b4602d6706fd2a0a05f3 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 30 Aug 2022 00:03:42 -0500 Subject: [PATCH 27/62] Include SDK asset build fix from https://github.com/vector-im/hydrogen-web/pull/859 --- scripts/build-plugins/rollup-plugin-build-themes.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scripts/build-plugins/rollup-plugin-build-themes.js b/scripts/build-plugins/rollup-plugin-build-themes.js index c8c7322063..d159f1db62 100644 --- a/scripts/build-plugins/rollup-plugin-build-themes.js +++ b/scripts/build-plugins/rollup-plugin-build-themes.js @@ -13,7 +13,13 @@ 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. */ -const path = require('path').posix; +// Use the path implementation native to the platform so paths from disk play +// well with resolving against the relative location (think Windows `C:\` and +// backslashes). +const path = require('path'); +// Use the posix (forward slash) implementation when working with `import` paths +// to reference resources +const posixPath = require('path').posix; const {optimize} = require('svgo'); async function readCSSSource(location) { @@ -238,7 +244,7 @@ module.exports = function buildThemes(options) { switch (file) { case "index.js": { const isDark = variants[variant].dark; - return `import "${path.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` + + return `import "${posixPath.resolve(`${location}/theme.css`)}${isDark? "?dark=true": ""}";` + `import "@theme/${theme}/${variant}/variables.css"`; } case "variables.css": { From eb470faf36f47304d0f6416bf940f47d46c10fb2 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 30 Aug 2022 01:49:47 -0500 Subject: [PATCH 28/62] Scratch changes for responsive and toggling right-panel via room header See https://github.com/matrix-org/matrix-public-archive/pull/53 --- src/domain/navigation/index.ts | 5 +++-- src/platform/web/ui/session/room/RoomView.js | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index 849d870aee..dfe0a07798 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -34,6 +34,7 @@ export type SegmentType = { "details": true; "members": true; "member": string; + "change-dates": true; }; export function createNavigation(): Navigation { @@ -58,7 +59,7 @@ function allowsChild(parent: Segment | undefined, child: Segment, if (sessionSegment) { segments.push(sessionSegment); } - } else if (type === "details" || type === "members") { + } else if (type === "details" || type === "members" || type === "change-dates") { pushRightPanelSegment(segments, type); } else if (type === "member") { const userId = iterator.next().value; diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 6c4d5dfa4a..52d8d25177 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -40,18 +40,27 @@ class RoomHeaderView extends TemplateView { }) ]) } - } +} export class RoomView extends TemplateView { - constructor(vm, viewClassForTile) { + constructor(vm, viewClassForTile, slots) { super(vm); this._viewClassForTile = viewClassForTile; this._optionsPopup = null; + this._slots = slots || {}; } render(t, vm) { + let headerView; + if (this._slots.RoomHeaderView) { + headerView = new this._slots.RoomHeaderView(vm) + } else { + headerView = new RoomHeaderView(vm) + } + + return t.main({className: "RoomView middle"}, [ - //t.view(new RoomHeaderView(vm)), + t.view(headerView), t.div({className: "RoomView_body"}, [ t.div({className: "RoomView_error"}, [ t.if(vm => vm.error, t => t.div( From f2baf3d8338f15e07b62b091b29c3624f496b1e3 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 30 Aug 2022 16:34:05 -0500 Subject: [PATCH 29/62] Seems like an unused style that collides with styles in the archive to hide the change dates calendar button at desktop widths --- src/platform/web/ui/css/room.css | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/platform/web/ui/css/room.css b/src/platform/web/ui/css/room.css index 746349c8ac..7dd4fd8190 100644 --- a/src/platform/web/ui/css/room.css +++ b/src/platform/web/ui/css/room.css @@ -23,10 +23,6 @@ limitations under the License. flex: 1; } -.middle-header button { - display: block; -} - .middle-header .room-description { flex: 1; min-width: 0; From fa0e4875d8106f0838929987b479279811232c88 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 30 Aug 2022 18:08:48 -0500 Subject: [PATCH 30/62] Remove custom `none` composer in favor of useful HTML message in the `DisabledComposerView` Changes for https://github.com/matrix-org/matrix-public-archive/pull/54 --- src/domain/session/SessionViewModel.js | 3 +-- src/lib.ts | 1 + src/platform/web/ui/session/room/DisabledComposerView.js | 4 ++-- src/platform/web/ui/session/room/RoomView.js | 3 --- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index 3dd7ea2665..96593dd056 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -85,8 +85,7 @@ export class SessionViewModel extends ViewModel { setupLightboxNavigation(this, 'lightboxViewModel', (eventId) => { return { - // TODO: room is not defined - room, + room: this._roomFromNavigation(), eventId, }; }); diff --git a/src/lib.ts b/src/lib.ts index 0399f22752..4fcce06ab2 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -77,6 +77,7 @@ export {Navigation} from "./domain/navigation/Navigation.js"; export {History} from "./platform/web/dom/History.js"; export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; +export {text, tag} from "./platform/web/ui/general/html"; export {TemplateView} from "./platform/web/ui/general/TemplateView"; export {ViewModel} from "./domain/ViewModel"; export {LoadingView} from "./platform/web/ui/general/LoadingView.js"; diff --git a/src/platform/web/ui/session/room/DisabledComposerView.js b/src/platform/web/ui/session/room/DisabledComposerView.js index caa8eeb9a8..929786df5f 100644 --- a/src/platform/web/ui/session/room/DisabledComposerView.js +++ b/src/platform/web/ui/session/room/DisabledComposerView.js @@ -17,7 +17,7 @@ limitations under the License. import {TemplateView} from "../../general/TemplateView"; export class DisabledComposerView extends TemplateView { - render(t) { - return t.div({className: "DisabledComposerView"}, t.h3(vm => vm.description)); + render(t, vm) { + return t.div({className: "DisabledComposerView"}, t.h3(vm.description)); } } diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 52d8d25177..956a0bcb26 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.js @@ -15,7 +15,6 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { text } from "../../general/html"; import {TemplateView} from "../../general/TemplateView"; import {Popup} from "../../general/Popup.js"; import {Menu} from "../../general/Menu.js"; @@ -81,8 +80,6 @@ export class RoomView extends TemplateView { return new MessageComposer(vm.composerViewModel, this._viewClassForTile); case "disabled": return new DisabledComposerView(vm.composerViewModel); - case "none": - text(''); } }), ]) From 98e0dd9207548e37a43b0a1ab2818d14cc5e8520 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 30 Aug 2022 20:28:28 -0500 Subject: [PATCH 31/62] Make changes for stubbed PowerLevels Changes for https://github.com/matrix-org/matrix-public-archive/pull/57 --- src/lib.ts | 1 + src/matrix/room/PowerLevels.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib.ts b/src/lib.ts index 4fcce06ab2..45ec394103 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -17,6 +17,7 @@ limitations under the License. export {Platform} from "./platform/web/Platform.js"; export {Client, LoadStatus} from "./matrix/Client.js"; export {RoomStatus} from "./matrix/room/common"; +export {PowerLevels} from "./matrix/room/PowerLevels.js"; // export main view & view models export {createNavigation, createRouter} from "./domain/navigation/index"; export {RootViewModel} from "./domain/RootViewModel.js"; diff --git a/src/matrix/room/PowerLevels.js b/src/matrix/room/PowerLevels.js index bb723c3409..76e062ef37 100644 --- a/src/matrix/room/PowerLevels.js +++ b/src/matrix/room/PowerLevels.js @@ -75,11 +75,11 @@ export class PowerLevels { } _getEventTypeLevel(eventType) { - const level = this._plEvent?.content.events?.[eventType]; + const level = this._plEvent?.content?.events?.[eventType]; if (typeof level === "number") { return level; } else { - const level = this._plEvent?.content.events_default; + const level = this._plEvent?.content?.events_default; if (typeof level === "number") { return level; } else { From 921544a5d6ed6295062b087bb4887dde06bb16f0 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 2 Sep 2022 20:52:59 -0500 Subject: [PATCH 32/62] Add header/footer --- src/platform/web/ui/general/html.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/html.ts b/src/platform/web/ui/general/html.ts index 44f7476ae4..c1f26b2dc8 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -102,7 +102,8 @@ export const SVG_NS: string = "http://www.w3.org/2000/svg"; export const TAG_NAMES = { [HTML_NS]: [ "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", - "p", "strong", "em", "span", "img", "section", "main", "article", "aside", "del", "blockquote", + "p", "strong", "em", "span", "img", "section", "header", "main", "footer", + "article", "aside", "del", "blockquote", "table", "thead", "tbody", "tr", "th", "td", "hr", "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", "progress", "output", "video"], From d5ba256f8f65884bbde99615bb3a929432f2c74c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 8 Sep 2022 00:35:33 -0500 Subject: [PATCH 33/62] More necessary exports --- src/lib.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib.ts b/src/lib.ts index 45ec394103..e332340107 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -80,9 +80,11 @@ export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; export {text, tag} from "./platform/web/ui/general/html"; export {TemplateView} from "./platform/web/ui/general/TemplateView"; +export {ListView} from "./platform/web/ui/general/ListView"; export {ViewModel} from "./domain/ViewModel"; export {LoadingView} from "./platform/web/ui/general/LoadingView.js"; export {AvatarView} from "./platform/web/ui/AvatarView.js"; +export {avatarInitials, getIdentifierColorNumber} from "./domain/avatar" export {RoomType} from "./matrix/room/common"; export {EventEmitter} from "./utils/EventEmitter"; export {Disposables} from "./utils/Disposables"; From 77105806574e8aa308de765f39b6e3c67d89984f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 15 Sep 2022 02:02:12 -0500 Subject: [PATCH 34/62] Add style tag for adding dynamic debugging styles --- src/platform/web/ui/general/html.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/html.ts b/src/platform/web/ui/general/html.ts index c1f26b2dc8..4e1fe78deb 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -106,7 +106,7 @@ export const TAG_NAMES = { "article", "aside", "del", "blockquote", "table", "thead", "tbody", "tr", "th", "td", "hr", "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", - "progress", "output", "video"], + "progress", "output", "video", "style"], [SVG_NS]: ["svg", "g", "path", "circle", "ellipse", "rect", "use"] } as const; From 89d044cbd19867ee880e1687d54c0ea3d4783791 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 16 Sep 2022 00:00:28 -0500 Subject: [PATCH 35/62] Add data-event-id to the AnnouncementView --- .../web/ui/session/room/timeline/AnnouncementView.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/session/room/timeline/AnnouncementView.js b/src/platform/web/ui/session/room/timeline/AnnouncementView.js index 5ae92daa31..8b68d33bd5 100644 --- a/src/platform/web/ui/session/room/timeline/AnnouncementView.js +++ b/src/platform/web/ui/session/room/timeline/AnnouncementView.js @@ -22,8 +22,11 @@ export class AnnouncementView extends TemplateView { super(vm); } - render(t) { - return t.li({className: "AnnouncementView"}, t.div(vm => vm.announcement)); + render(t, vm) { + return t.li({ + className: "AnnouncementView", + 'data-event-id': vm.eventId + }, t.div(vm => vm.announcement)); } /* This is called by the parent ListView, which just has 1 listener for the whole list */ From 127f39a96909131708e26b5aa1e3f740577c0a1c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 16 Sep 2022 13:49:28 -0500 Subject: [PATCH 36/62] Log error caught in boundary to the console --- src/platform/web/ui/general/utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/platform/web/ui/general/utils.ts b/src/platform/web/ui/general/utils.ts index b310571fe8..3f470048ab 100644 --- a/src/platform/web/ui/general/utils.ts +++ b/src/platform/web/ui/general/utils.ts @@ -22,6 +22,9 @@ export function mountView(view: IView, mountArgs?: IMountArgs): ViewNode { try { node = view.mount(mountArgs); } catch (err) { + // Log it to the console so it's easy to reference + console.error(err); + // Then render our error boundary to the DOM node = errorToDOM(err); } return node; From fa524d9e61d362c813a6842dfd1ea5066d0313d3 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Sat, 17 Sep 2022 00:45:23 -0500 Subject: [PATCH 37/62] Add support for developer-options navigation --- src/domain/navigation/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/domain/navigation/index.ts b/src/domain/navigation/index.ts index dfe0a07798..4f042419da 100644 --- a/src/domain/navigation/index.ts +++ b/src/domain/navigation/index.ts @@ -35,6 +35,7 @@ export type SegmentType = { "members": true; "member": string; "change-dates": true; + "developer-options": true; }; export function createNavigation(): Navigation { @@ -57,7 +58,7 @@ function allowsChild(parent: Segment | undefined, child: Segment Date: Tue, 20 Sep 2022 00:36:38 -0500 Subject: [PATCH 38/62] Extra style for reference by children --- src/platform/web/ui/css/timeline.css | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/platform/web/ui/css/timeline.css b/src/platform/web/ui/css/timeline.css index b6d62c9850..31c18e2c93 100644 --- a/src/platform/web/ui/css/timeline.css +++ b/src/platform/web/ui/css/timeline.css @@ -40,6 +40,11 @@ limitations under the License. .Timeline_scroller > ul { list-style: none; + /* This is just so that children can also use `height: 100%;` take up the + same height as the scroll viewport. This is used in the Matrix public + archive so that even if there aren't enough events from a given day to fill + up the scroll, you can still have the last event within scroll range. */ + height: 100%; /* use small horizontal padding so first/last children margin isn't collapsed at the edge and a scrollbar shows up when setting margin-top to bottom-align content when there are not yet enough tiles to fill the viewport */ From de261aac91a6690df37e138385c74f0d1325eab4 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 20 Sep 2022 12:37:59 -0500 Subject: [PATCH 39/62] Be able to access the event room_id --- src/matrix/room/timeline/entries/EventEntry.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/matrix/room/timeline/entries/EventEntry.js b/src/matrix/room/timeline/entries/EventEntry.js index d218a598e6..3f2a85db21 100644 --- a/src/matrix/room/timeline/entries/EventEntry.js +++ b/src/matrix/room/timeline/entries/EventEntry.js @@ -76,6 +76,10 @@ export class EventEntry extends BaseEventEntry { return this._eventEntry.event.sender; } + get roomId() { + return this._eventEntry.event.room_id; + } + get displayName() { return this._eventEntry.displayName; } From e0e486878ebbd7c85116f1c08eade14fb578de8c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 20 Sep 2022 15:19:49 -0500 Subject: [PATCH 40/62] Make the summary stick to the bottom --- src/platform/web/ui/css/timeline.css | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ui/css/timeline.css b/src/platform/web/ui/css/timeline.css index 31c18e2c93..0f144ffdf4 100644 --- a/src/platform/web/ui/css/timeline.css +++ b/src/platform/web/ui/css/timeline.css @@ -40,11 +40,10 @@ limitations under the License. .Timeline_scroller > ul { list-style: none; - /* This is just so that children can also use `height: 100%;` take up the - same height as the scroll viewport. This is used in the Matrix public - archive so that even if there aren't enough events from a given day to fill - up the scroll, you can still have the last event within scroll range. */ - height: 100%; + /* If there are only a few events, align them to the bottom */ + display: flex; + flex-direction: column; + justify-content: flex-end; /* use small horizontal padding so first/last children margin isn't collapsed at the edge and a scrollbar shows up when setting margin-top to bottom-align content when there are not yet enough tiles to fill the viewport */ From c94765ebdb48e5e74cb965019a4b5f893e49552b Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 19 Oct 2022 01:05:28 -0500 Subject: [PATCH 41/62] Add new elements --- src/platform/web/ui/general/html.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/platform/web/ui/general/html.ts b/src/platform/web/ui/general/html.ts index 4e1fe78deb..75ea88ab1b 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -102,10 +102,10 @@ export const SVG_NS: string = "http://www.w3.org/2000/svg"; export const TAG_NAMES = { [HTML_NS]: [ "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", - "p", "strong", "em", "span", "img", "section", "header", "main", "footer", + "p", "strong", "em", "span", "img", "section", "header", "main", "footer", "dialog", "article", "aside", "del", "blockquote", "table", "thead", "tbody", "tr", "th", "td", "hr", - "pre", "code", "button", "time", "input", "textarea", "select", "option", "label", "form", + "pre", "code", "button", "time", "input", "textarea", "select", "option", "optgroup", "label", "form", "progress", "output", "video", "style"], [SVG_NS]: ["svg", "g", "path", "circle", "ellipse", "rect", "use"] } as const; From 26cb86e72a2bfb8b070910e9958fccc5013103f9 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 20 Oct 2022 01:39:12 -0500 Subject: [PATCH 42/62] Add export --- src/lib.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.ts b/src/lib.ts index 8fc582f510..c4a8b232ff 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -75,6 +75,7 @@ export {TextMessageView} from "./platform/web/ui/session/room/timeline/TextMessa export {VideoView} from "./platform/web/ui/session/room/timeline/VideoView.js"; export {Navigation} from "./domain/navigation/Navigation.js"; +export {URLRouter} from "./domain/navigation/URLRouter"; export {History} from "./platform/web/dom/History.js"; export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; From 9f5e66e9fe58b8ecb1e722a14d0d27d02de4d850 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 21 Oct 2022 02:03:41 -0500 Subject: [PATCH 43/62] Add details element --- src/platform/web/ui/general/html.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/general/html.ts b/src/platform/web/ui/general/html.ts index 75ea88ab1b..93512897a6 100644 --- a/src/platform/web/ui/general/html.ts +++ b/src/platform/web/ui/general/html.ts @@ -103,7 +103,7 @@ export const TAG_NAMES = { [HTML_NS]: [ "br", "a", "ol", "ul", "li", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "strong", "em", "span", "img", "section", "header", "main", "footer", "dialog", - "article", "aside", "del", "blockquote", + "article", "aside", "del", "blockquote", "details", "summary", "table", "thead", "tbody", "tr", "th", "td", "hr", "pre", "code", "button", "time", "input", "textarea", "select", "option", "optgroup", "label", "form", "progress", "output", "video", "style"], From 1d9c1bfafb6ef0311c7709a55f7ea7bca174496e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 1 Nov 2022 09:11:29 -0500 Subject: [PATCH 44/62] Add a way to change the anchor position to the top of a node This would be helpful for https://github.com/matrix-org/matrix-public-archive/issues/73 --- doc/SDK.md | 2 +- src/platform/web/ui/session/room/RoomView.js | 2 +- .../web/ui/session/room/TimelineView.ts | 42 +++++++++++++++---- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/doc/SDK.md b/doc/SDK.md index c8f5197fc1..d7286deb5f 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/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 956a0bcb26..c58bf002ee 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, this._viewClassForTile) : + new TimelineView(timelineViewModel, { viewClassForTile: 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 5a04991fba..90c05f638d 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -42,10 +42,15 @@ 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; @@ -57,16 +62,32 @@ function findFirstNodeIndexAtOrBelow(tiles: HTMLElement, top: number, startIndex return 0; } +type AnchorPosition = "bottom" | "top"; + +interface TimelineViewOpts { + viewClassForTile: ViewClassForEntryFn + anchorPosition: AnchorPosition +} + export class TimelineView extends TemplateView { private anchoredNode?: HTMLElement; - private anchoredBottom: number = 0; + private anchoredOffset: number = 0; + private anchorPosition: AnchorPosition = 'bottom'; private stickToBottom: boolean = true; private tilesView?: TilesListView; private resizeObserver?: ResizeObserver; + private viewClassForTile: ViewClassForEntryFn; - constructor(vm: TimelineViewModel, private readonly viewClassForTile: ViewClassForEntryFn) { + constructor(vm: TimelineViewModel, { + viewClassForTile, + // TODO: This should default to 'bottom' + anchorPosition= 'top' + }: TimelineViewOpts) { super(vm); + + this.viewClassForTile = viewClassForTile; + this.anchorPosition = anchorPosition; } render(t: Builder, vm: TimelineViewModel) { @@ -124,6 +145,7 @@ export class TimelineView extends TemplateView { } private restoreScrollPosition() { + console.log('restoreScrollPosition', new Error().stack); const {scrollNode, tilesNode} = this; const missingTilesHeight = scrollNode.clientHeight - tilesNode.clientHeight; @@ -137,18 +159,19 @@ export class TimelineView extends TemplateView { if (this.stickToBottom) { scrollNode.scrollTop = scrollNode.scrollHeight; } else if (this.anchoredNode) { - const newAnchoredBottom = bottom(this.anchoredNode!); - if (newAnchoredBottom !== this.anchoredBottom) { - const bottomDiff = newAnchoredBottom - this.anchoredBottom; + const offsetFunc = this.anchorPosition === 'bottom' ? bottom : top; + const newAnchoredOffset = offsetFunc(this.anchoredNode!); + if (newAnchoredOffset !== this.anchoredOffset) { + const offsetDiff = newAnchoredOffset - this.anchoredOffset; // 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, bottomDiff); + scrollNode.scrollBy(0, offsetDiff); } else { - scrollNode.scrollTop = scrollNode.scrollTop + bottomDiff; + scrollNode.scrollTop = scrollNode.scrollTop + offsetDiff; } - this.anchoredBottom = newAnchoredBottom; + this.anchoredOffset = newAnchoredOffset; } } // TODO: should we be updating the visible range here as well as the range might have changed even though @@ -169,7 +192,8 @@ export class TimelineView extends TemplateView { const viewportBottom = scrollTop + clientHeight; const anchoredNodeIndex = findFirstNodeIndexAtOrBelow(tilesNode, viewportBottom); this.anchoredNode = tilesNode.childNodes[anchoredNodeIndex] as HTMLElement; - this.anchoredBottom = bottom(this.anchoredNode!); + const offsetFunc = this.anchorPosition === 'bottom' ? bottom : top; + this.anchoredOffset = offsetFunc(this.anchoredNode!); bottomNodeIndex = anchoredNodeIndex; } let topNodeIndex = findFirstNodeIndexAtOrBelow(tilesNode, scrollTop, bottomNodeIndex); From 4e3f3975e1cdcc780adf543caf054ce976b3d17c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 1 Nov 2022 09:14:16 -0500 Subject: [PATCH 45/62] Revert "Add a way to change the anchor position to the top of a node" This reverts commit 1d9c1bfafb6ef0311c7709a55f7ea7bca174496e. --- doc/SDK.md | 2 +- src/platform/web/ui/session/room/RoomView.js | 2 +- .../web/ui/session/room/TimelineView.ts | 42 ++++--------------- 3 files changed, 11 insertions(+), 35 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/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 90c05f638d..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,32 +57,16 @@ function findFirstNodeIndexAtOrBelow(tiles: HTMLElement, top: number, startIndex return 0; } -type AnchorPosition = "bottom" | "top"; - -interface TimelineViewOpts { - viewClassForTile: ViewClassForEntryFn - anchorPosition: AnchorPosition -} - export class TimelineView extends TemplateView { 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, - // TODO: This should default to 'bottom' - anchorPosition= 'top' - }: TimelineViewOpts) { + constructor(vm: TimelineViewModel, private readonly viewClassForTile: ViewClassForEntryFn) { super(vm); - - this.viewClassForTile = viewClassForTile; - this.anchorPosition = anchorPosition; } render(t: Builder, vm: TimelineViewModel) { @@ -145,7 +124,6 @@ export class TimelineView extends TemplateView { } private restoreScrollPosition() { - console.log('restoreScrollPosition', new Error().stack); const {scrollNode, tilesNode} = this; const missingTilesHeight = scrollNode.clientHeight - tilesNode.clientHeight; @@ -159,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 @@ -192,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); From 5fdbe612e8fb8f0d6965954552bbf6061853c524 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 1 Nov 2022 23:25:57 -0500 Subject: [PATCH 46/62] Add a way to toggle scrollToBottom so you can start scroll from the top as well and continue reading See `?continue=top` in https://github.com/matrix-org/matrix-public-archive/pull/114 for how this is used --- doc/SDK.md | 2 +- src/platform/web/ui/session/room/RoomView.js | 2 +- src/platform/web/ui/session/room/TimelineView.ts | 13 ++++++++++++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/doc/SDK.md b/doc/SDK.md index c8f5197fc1..d7286deb5f 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/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index 956a0bcb26..c58bf002ee 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, this._viewClassForTile) : + new TimelineView(timelineViewModel, { viewClassForTile: 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 5a04991fba..50e665d5cf 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -57,6 +57,10 @@ function findFirstNodeIndexAtOrBelow(tiles: HTMLElement, top: number, startIndex return 0; } +interface TimelineViewOpts { + viewClassForTile: ViewClassForEntryFn + stickToBottom: boolean +} export class TimelineView extends TemplateView { private anchoredNode?: HTMLElement; @@ -64,9 +68,16 @@ export class TimelineView extends TemplateView { private stickToBottom: boolean = true; private tilesView?: TilesListView; private resizeObserver?: ResizeObserver; + private viewClassForTile: ViewClassForEntryFn; - constructor(vm: TimelineViewModel, private readonly viewClassForTile: ViewClassForEntryFn) { + constructor(vm: TimelineViewModel, { + viewClassForTile, + stickToBottom = true + }: TimelineViewOpts) { super(vm); + + this.viewClassForTile = viewClassForTile; + this.stickToBottom = stickToBottom; } render(t: Builder, vm: TimelineViewModel) { From c9859ff268b9411dd4c75b8399b684a8c738eb41 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 1 Nov 2022 09:11:29 -0500 Subject: [PATCH 47/62] Add a way to change the anchor position to the top of a node This would be helpful for https://github.com/matrix-org/matrix-public-archive/issues/73 --- .../web/ui/session/room/TimelineView.ts | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 50e665d5cf..67ff6da29d 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -42,10 +42,15 @@ 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; @@ -57,14 +62,19 @@ function findFirstNodeIndexAtOrBelow(tiles: HTMLElement, top: number, startIndex return 0; } +type AnchorPosition = "bottom" | "top"; + interface TimelineViewOpts { viewClassForTile: ViewClassForEntryFn stickToBottom: boolean + anchorPosition: AnchorPosition } + export class TimelineView extends TemplateView { private anchoredNode?: HTMLElement; - private anchoredBottom: number = 0; + private anchoredOffset: number = 0; + private anchorPosition: AnchorPosition = 'bottom'; private stickToBottom: boolean = true; private tilesView?: TilesListView; private resizeObserver?: ResizeObserver; @@ -72,12 +82,15 @@ export class TimelineView extends TemplateView { constructor(vm: TimelineViewModel, { viewClassForTile, - stickToBottom = true + stickToBottom = true, + // TODO: This should default to 'bottom' + anchorPosition= 'top' }: TimelineViewOpts) { super(vm); this.viewClassForTile = viewClassForTile; this.stickToBottom = stickToBottom; + this.anchorPosition = anchorPosition; } render(t: Builder, vm: TimelineViewModel) { @@ -135,6 +148,7 @@ export class TimelineView extends TemplateView { } private restoreScrollPosition() { + console.log('restoreScrollPosition', new Error().stack); const {scrollNode, tilesNode} = this; const missingTilesHeight = scrollNode.clientHeight - tilesNode.clientHeight; @@ -148,18 +162,19 @@ export class TimelineView extends TemplateView { if (this.stickToBottom) { scrollNode.scrollTop = scrollNode.scrollHeight; } else if (this.anchoredNode) { - const newAnchoredBottom = bottom(this.anchoredNode!); - if (newAnchoredBottom !== this.anchoredBottom) { - const bottomDiff = newAnchoredBottom - this.anchoredBottom; + const offsetFunc = this.anchorPosition === 'bottom' ? bottom : top; + const newAnchoredOffset = offsetFunc(this.anchoredNode!); + if (newAnchoredOffset !== this.anchoredOffset) { + const offsetDiff = newAnchoredOffset - this.anchoredOffset; // 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, bottomDiff); + scrollNode.scrollBy(0, offsetDiff); } else { - scrollNode.scrollTop = scrollNode.scrollTop + bottomDiff; + scrollNode.scrollTop = scrollNode.scrollTop + offsetDiff; } - this.anchoredBottom = newAnchoredBottom; + this.anchoredOffset = newAnchoredOffset; } } // TODO: should we be updating the visible range here as well as the range might have changed even though @@ -180,7 +195,8 @@ export class TimelineView extends TemplateView { const viewportBottom = scrollTop + clientHeight; const anchoredNodeIndex = findFirstNodeIndexAtOrBelow(tilesNode, viewportBottom); this.anchoredNode = tilesNode.childNodes[anchoredNodeIndex] as HTMLElement; - this.anchoredBottom = bottom(this.anchoredNode!); + const offsetFunc = this.anchorPosition === 'bottom' ? bottom : top; + this.anchoredOffset = offsetFunc(this.anchoredNode!); bottomNodeIndex = anchoredNodeIndex; } let topNodeIndex = findFirstNodeIndexAtOrBelow(tilesNode, scrollTop, bottomNodeIndex); From 76028257647510c84c6a11540c9e5bde10dda5bd Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 3 Nov 2022 01:55:11 -0500 Subject: [PATCH 48/62] Re-add way to align scroll top to event --- .../web/ui/session/room/TimelineView.ts | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 67ff6da29d..8af3868162 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -66,12 +66,14 @@ type AnchorPosition = "bottom" | "top"; interface TimelineViewOpts { viewClassForTile: ViewClassForEntryFn - stickToBottom: boolean - anchorPosition: AnchorPosition + 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'; @@ -82,15 +84,17 @@ export class TimelineView extends TemplateView { constructor(vm: TimelineViewModel, { viewClassForTile, + initialAnchorEventId, stickToBottom = true, // TODO: This should default to 'bottom' - anchorPosition= 'top' + anchorPosition= 'top', }: TimelineViewOpts) { super(vm); this.viewClassForTile = viewClassForTile; this.stickToBottom = stickToBottom; this.anchorPosition = anchorPosition; + this.initialAnchorEventId = initialAnchorEventId; } render(t: Builder, vm: TimelineViewModel) { @@ -149,7 +153,18 @@ export class TimelineView extends TemplateView { private restoreScrollPosition() { console.log('restoreScrollPosition', new Error().stack); - const {scrollNode, tilesNode} = this; + 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 missingTilesHeight = scrollNode.clientHeight - tilesNode.clientHeight; if (missingTilesHeight > 0) { From dc6ab455921a3c5a2df948f5029f5bc2854c4da6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 3 Nov 2022 03:06:57 -0500 Subject: [PATCH 49/62] 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); From ceca3d1a169b7f236990e5b907449b99140cd798 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 3 Nov 2022 03:59:00 -0500 Subject: [PATCH 50/62] Add a way to scroll to the given highlighted event --- .../room/timeline/TimelineViewModel.js | 13 +++++++++- .../web/ui/session/room/TimelineView.ts | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index c77d990620..60d080c6b9 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -47,6 +47,7 @@ export class TimelineViewModel extends ViewModel { this._requestedEndTile = null; this._requestScheduled = false; this._showJumpDown = false; + this._eventIdHighlighted = null; } /** if this._tiles is empty, call this with undefined for both startTile and endTile */ @@ -98,11 +99,21 @@ export class TimelineViewModel extends ViewModel { setEventHighlight(eventId, newHighlightValue) { const eventEntry = this._timeline.getByEventId(eventId); - if(eventEntry) { + if (eventEntry) { eventEntry.setIsHighlighted(newHighlightValue); + + // If a new highlight, emit a change so we can scroll to this new highlight + if (newHighlightValue) { + this._eventIdHighlighted = eventId; + this.emitChange('eventIdHighlighted'); + } } } + get eventIdHighlighted() { + return this._eventIdHighlighted; + } + get tiles() { return this._tiles; } diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 5a04991fba..16e306db27 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -91,6 +91,17 @@ export class TimelineView extends TemplateView { }) ]); + t.mapSideEffect(vm => vm.eventIdHighlighted, (eventIdHighlighted) => { + if (eventIdHighlighted) { + // assume this view will be mounted in the parent DOM straight away + requestAnimationFrame(() => { + this.scrollToEventId(eventIdHighlighted, { + block: 'center' + }); + }); + } + }); + if (typeof ResizeObserver === "function") { this.resizeObserver = new ResizeObserver(() => { this.restoreScrollPosition(); @@ -176,6 +187,20 @@ export class TimelineView extends TemplateView { this.updateVisibleRange(topNodeIndex, bottomNodeIndex); } + private scrollToEventId(eventId: string, scrollIntoViewOptions?: object) { + const {tilesNode} = this; + const eventEl = [...tilesNode.childNodes].find((node: HTMLElement) => { + return node.matches(`[data-event-id="${eventId}"]`); + }) as HTMLElement; + + if (eventEl) { + eventEl.scrollIntoView(scrollIntoViewOptions); + this.stickToBottom = false; + } + + return !!eventEl; + } + private updateVisibleRange(startIndex: number, endIndex: number) { // can be undefined, meaning the tiles collection is still empty const firstVisibleChild = this.tilesView!.getChildInstanceByIndex(startIndex); From b84dd3b42c5e4f95c29e3648bccff7bd56adc4fd Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 3 Nov 2022 04:42:36 -0500 Subject: [PATCH 51/62] Fix tsc error --- src/platform/web/ui/session/room/TimelineView.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 16e306db27..2c1d79f764 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -39,6 +39,7 @@ export type ViewClassForEntryFn = (tile: SimpleTile) => TileViewConstructor; export interface TimelineViewModel extends IObservableValue { showJumpDown: boolean; tiles: ObservableList; + eventIdHighlighted: string; setVisibleTileRange(start?: SimpleTile, end?: SimpleTile); } From 65ec71b403fbd8494899aa638f4c82d1dd7fa79f Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 3 Nov 2022 04:42:46 -0500 Subject: [PATCH 52/62] Prevent messages nested in a reply from being highlighted --- .../web/ui/css/themes/element/timeline.css | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index b0b62cc1c0..d916991eb5 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -46,11 +46,6 @@ limitations under the License. border-radius: 4px; } -.Timeline_message.highlighted, -.AnnouncementView.highlighted { - background-color: #ffff8a; -} - .Timeline_message:not(.continuation) { margin-top: 4px; } @@ -82,6 +77,17 @@ limitations under the License. } } +.Timeline_message.highlighted, +.AnnouncementView.highlighted { + background-color: #ffff8a; +} + +/* Prevent messages nested in a reply from being highlighted */ +.ReplyPreviewView .Timeline_message.highlighted, +.ReplyPreviewView .AnnouncementView.highlighted { + background-color: transparent; +} + .Timeline_message:hover:not(.disabled), .Timeline_message.selected, .Timeline_message.menuOpen { /* needs transparency support */ background-color: rgba(141, 151, 165, 0.1); From 168788d405ea42794983709c50f79fa5497057a6 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 8 Nov 2022 22:06:19 -0600 Subject: [PATCH 53/62] Add MXID as title tooltip --- .../web/ui/session/room/timeline/BaseMessageView.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 1db73fddd8..546ed07a7a 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -64,7 +64,13 @@ export class BaseMessageView extends TemplateView { li.removeChild(li.querySelector(".Timeline_messageSender")); } else if (!isContinuation && !this._isReplyPreview) { const avatar = tag.a({href: vm.memberPanelLink, className: "Timeline_messageAvatar"}, [renderStaticAvatar(vm, 30)]); - const sender = tag.div({className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`}, vm.displayName); + const sender = tag.div( + { + className: `Timeline_messageSender usercolor${vm.avatarColorNumber}`, + title: vm.sender, + }, + vm.displayName, + ); li.insertBefore(avatar, li.firstChild); li.insertBefore(sender, li.firstChild); } From 847482fb3ded20b88495116042bebf02107a6b03 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 8 Nov 2022 22:11:38 -0600 Subject: [PATCH 54/62] Add full MXID to message avatar/sender --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 2 +- src/platform/web/ui/avatar.js | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 9c70768dc4..cc89122a8b 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -79,7 +79,7 @@ export class BaseMessageTile extends SimpleTile { } get avatarTitle() { - return this.displayName; + return this.sender; } get date() { diff --git a/src/platform/web/ui/avatar.js b/src/platform/web/ui/avatar.js index 547d4b51ec..ce2dfdabd8 100644 --- a/src/platform/web/ui/avatar.js +++ b/src/platform/web/ui/avatar.js @@ -31,7 +31,11 @@ export function renderStaticAvatar(vm, size, extraClasses = undefined) { avatarClasses += ` ${extraClasses}`; } const avatarContent = hasAvatar ? renderImg(vm, size) : text(vm.avatarLetter); - const avatar = tag.div({className: avatarClasses, "data-testid": "avatar"}, [avatarContent]); + const avatar = tag.div({ + className: avatarClasses, + title: vm.avatarTitle, + "data-testid": "avatar", + }, [avatarContent]); if (hasAvatar) { setAttribute(avatar, "data-avatar-letter", vm.avatarLetter); setAttribute(avatar, "data-avatar-color", vm.avatarColorNumber); From bb51174094f4801254143510bfe495b93c4c38d2 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Tue, 8 Nov 2022 23:59:45 -0600 Subject: [PATCH 55/62] Fix AnnouncementView member events not highlighting --- src/domain/session/room/timeline/tiles/BaseMessageTile.js | 4 ---- src/domain/session/room/timeline/tiles/SimpleTile.js | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index cc89122a8b..0eb99868e6 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -98,10 +98,6 @@ 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 e3481d37ed..09e05e28aa 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -38,7 +38,7 @@ export class SimpleTile extends ViewModel { } get isHighlighted() { - return false; + return this._entry.isHighlighted; } get hasDateSeparator() { From 07cc95c2a0aa10ab2899ccb1339ccefa188b0138 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 9 Nov 2022 00:14:37 -0600 Subject: [PATCH 56/62] Add different hover styles so it's more obvious if you page-load with your cursor on that event --- .../web/ui/css/themes/element/timeline.css | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index d916991eb5..1cc5145faf 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -77,17 +77,6 @@ limitations under the License. } } -.Timeline_message.highlighted, -.AnnouncementView.highlighted { - background-color: #ffff8a; -} - -/* Prevent messages nested in a reply from being highlighted */ -.ReplyPreviewView .Timeline_message.highlighted, -.ReplyPreviewView .AnnouncementView.highlighted { - background-color: transparent; -} - .Timeline_message:hover:not(.disabled), .Timeline_message.selected, .Timeline_message.menuOpen { /* needs transparency support */ background-color: rgba(141, 151, 165, 0.1); @@ -99,6 +88,24 @@ limitations under the License. user-select: none; } +.Timeline_message.highlighted, +.AnnouncementView.highlighted { + background-color: #ffff8a; +} + +.Timeline_message.highlighted:hover:not(.disabled), +.AnnouncementView.highlighted:hover, +.Timeline_message.highlighted.selected:hover +.Timeline_message.highlighted.menuOpen:hover { + background-color: #e0f4ff; +} + +/* Prevent messages nested in a reply from being highlighted */ +.ReplyPreviewView .Timeline_message.highlighted, +.ReplyPreviewView .AnnouncementView.highlighted { + background-color: transparent; +} + .Timeline_messageAvatar { grid-area: avatar; text-decoration: none; From 64b6ba143db1b464abbe04877ef9aaa3abce3b40 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 10 Nov 2022 20:52:55 -0600 Subject: [PATCH 57/62] Add copy permalink action --- .../room/timeline/tiles/BaseMessageTile.js | 6 ++ src/matrix/room/PowerLevels.js | 3 +- src/platform/web/dom/ImageHandle.js | 2 +- src/platform/web/dom/utils.js | 35 -------- src/platform/web/dom/utils.ts | 79 +++++++++++++++++++ .../session/room/timeline/BaseMessageView.js | 2 + .../web/ui/session/room/timeline/VideoView.js | 2 +- 7 files changed, 91 insertions(+), 38 deletions(-) delete mode 100644 src/platform/web/dom/utils.js create mode 100644 src/platform/web/dom/utils.ts diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 1d9b14184f..76d2b03fa8 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -17,6 +17,8 @@ limitations under the License. import {SimpleTile} from "./SimpleTile.js"; import {ReactionsViewModel} from "../ReactionsViewModel.js"; import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar"; +import {copyPlaintext} from "../../../../../platform/web/dom/utils"; + export class BaseMessageTile extends SimpleTile { constructor(entry, options) { @@ -45,6 +47,10 @@ export class BaseMessageTile extends SimpleTile { return `https://matrix.to/#/${encodeURIComponent(this._room.id)}/${encodeURIComponent(this._entry.id)}`; } + copyPermalink() { + copyPlaintext(this.permaLink); + } + get senderProfileLink() { return `https://matrix.to/#/${encodeURIComponent(this.sender)}`; } diff --git a/src/matrix/room/PowerLevels.js b/src/matrix/room/PowerLevels.js index 76e062ef37..63a5b0b0a1 100644 --- a/src/matrix/room/PowerLevels.js +++ b/src/matrix/room/PowerLevels.js @@ -66,10 +66,11 @@ export class PowerLevels { /** @param {string} action either "invite", "kick", "ban" or "redact". */ _getActionLevel(action) { - const level = this._plEvent?.content[action]; + const level = this._plEvent?.content?.[action]; if (typeof level === "number") { return level; } else { + // TODO: Why does this default to 50? return 50; } } diff --git a/src/platform/web/dom/ImageHandle.js b/src/platform/web/dom/ImageHandle.js index 4ac3a6cd2e..e41486f983 100644 --- a/src/platform/web/dom/ImageHandle.js +++ b/src/platform/web/dom/ImageHandle.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BlobHandle} from "./BlobHandle.js"; -import {domEventAsPromise} from "./utils.js"; +import {domEventAsPromise} from "./utils"; export class ImageHandle { static async fromBlob(blob) { diff --git a/src/platform/web/dom/utils.js b/src/platform/web/dom/utils.js deleted file mode 100644 index 43a2664033..0000000000 --- a/src/platform/web/dom/utils.js +++ /dev/null @@ -1,35 +0,0 @@ -/* -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. -*/ - -export function domEventAsPromise(element, successEvent) { - return new Promise((resolve, reject) => { - let detach; - const handleError = evt => { - detach(); - reject(evt.target.error); - }; - const handleSuccess = () => { - detach(); - resolve(); - }; - detach = () => { - element.removeEventListener(successEvent, handleSuccess); - element.removeEventListener("error", handleError); - }; - element.addEventListener(successEvent, handleSuccess); - element.addEventListener("error", handleError); - }); -} diff --git a/src/platform/web/dom/utils.ts b/src/platform/web/dom/utils.ts new file mode 100644 index 0000000000..4c17f1af95 --- /dev/null +++ b/src/platform/web/dom/utils.ts @@ -0,0 +1,79 @@ +/* +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. +*/ + +export function domEventAsPromise(element, successEvent): Promise { + return new Promise((resolve, reject) => { + let detach; + const handleError = evt => { + detach(); + reject(evt.target.error); + }; + const handleSuccess = () => { + detach(); + resolve(); + }; + detach = () => { + element.removeEventListener(successEvent, handleSuccess); + element.removeEventListener("error", handleError); + }; + element.addEventListener(successEvent, handleSuccess); + element.addEventListener("error", handleError); + }); +} + +// Copies the given text to clipboard and returns a boolean of whether the action was +// successful +export async function copyPlaintext(text: string): Promise { + try { + if (navigator?.clipboard?.writeText) { + await navigator.clipboard.writeText(text); + return true; + } else { + const textArea = document.createElement("textarea"); + textArea.value = text; + + // Avoid scrolling to bottom + textArea.style.top = "0"; + textArea.style.left = "0"; + textArea.style.position = "fixed"; + + document.body.appendChild(textArea); + + const selection = document.getSelection(); + if (!selection) { + console.error('copyPlaintext: Unable to copy text to clipboard in fallback mode because `selection` was null/undefined'); + return false; + } + + const range = document.createRange(); + // range.selectNodeContents(textArea); + range.selectNode(textArea); + selection.removeAllRanges(); + selection.addRange(range); + + const successful = document.execCommand("copy"); + selection.removeAllRanges(); + document.body.removeChild(textArea); + if(!successful) { + console.error('copyPlaintext: Unable to copy text to clipboard in fallback mode because the `copy` command is unsupported or disabled'); + } + return successful; + } + } catch (err) { + console.error("copyPlaintext: Ran into an error", err); + } + return false; +} diff --git a/src/platform/web/ui/session/room/timeline/BaseMessageView.js b/src/platform/web/ui/session/room/timeline/BaseMessageView.js index 5b745a92ec..6300d869e8 100644 --- a/src/platform/web/ui/session/room/timeline/BaseMessageView.js +++ b/src/platform/web/ui/session/room/timeline/BaseMessageView.js @@ -127,6 +127,8 @@ export class BaseMessageView extends TemplateView { } else if (vm.canRedact) { options.push(Menu.option(vm.i18n`Delete`, () => vm.redact()).setDestructive()); } + + options.push(Menu.option(vm.i18n`Copy permalink`, () => vm.copyPermalink())); return options; } diff --git a/src/platform/web/ui/session/room/timeline/VideoView.js b/src/platform/web/ui/session/room/timeline/VideoView.js index 340cae6d24..9b092ed091 100644 --- a/src/platform/web/ui/session/room/timeline/VideoView.js +++ b/src/platform/web/ui/session/room/timeline/VideoView.js @@ -15,7 +15,7 @@ limitations under the License. */ import {BaseMediaView} from "./BaseMediaView.js"; -import {domEventAsPromise} from "../../../../dom/utils.js"; +import {domEventAsPromise} from "../../../../dom/utils"; export class VideoView extends BaseMediaView { renderMedia(t) { From 72837d812869141a5efa229966a0bf8ab37fa036 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 11 Nov 2022 21:59:37 -0600 Subject: [PATCH 58/62] Remove matrix.to link from message timestamp since we have an context menu option for it now --- src/platform/web/ui/session/room/timeline/TextMessageView.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/platform/web/ui/session/room/timeline/TextMessageView.js b/src/platform/web/ui/session/room/timeline/TextMessageView.js index c66a9d8089..8d6cb4dcd0 100644 --- a/src/platform/web/ui/session/room/timeline/TextMessageView.js +++ b/src/platform/web/ui/session/room/timeline/TextMessageView.js @@ -20,9 +20,7 @@ import {ReplyPreviewError, ReplyPreviewView} from "./ReplyPreviewView.js"; export class TextMessageView extends BaseMessageView { renderMessageBody(t, vm) { - const time = t.a({ href: vm.permaLink, target: "_blank" }, [ - t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time) - ]); + const time = t.time({className: {hidden: !vm.date}}, vm.date + " " + vm.time); const container = t.div({ className: { "Timeline_messageBody": true, From 987d27aa9527fbce7c9caaecc65da2b26e391d1e Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 26 Apr 2023 16:09:26 -0500 Subject: [PATCH 59/62] Increase width to accomodate long dates with year like 'Wednesday, November 16, 2022' --- src/platform/web/ui/css/themes/element/timeline.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform/web/ui/css/themes/element/timeline.css b/src/platform/web/ui/css/themes/element/timeline.css index 41fb56a78c..71fd0809f6 100644 --- a/src/platform/web/ui/css/themes/element/timeline.css +++ b/src/platform/web/ui/css/themes/element/timeline.css @@ -451,7 +451,7 @@ only loads when the top comes into view*/ .DateHeader time { margin: 0 auto; padding: 12px 4px; - width: 250px; + max-width: 350px; padding: 12px; display: block; color: var(--light-text-color); From ba0eb55b065cd0fad98d307fa2c712c0dbe67d79 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Wed, 3 May 2023 00:41:54 -0500 Subject: [PATCH 60/62] Add ApplyMap to exports --- src/lib.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.ts b/src/lib.ts index 5a2ab076e4..9f560b6a0a 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -104,6 +104,7 @@ export {Disposables} from "./utils/Disposables"; export {LocalMedia} from "./matrix/calls/LocalMedia"; // these should eventually be moved to another library export { + ApplyMap, ObservableArray, SortedArray, MappedList, From f1ec4d4a91a12a5868eddac668651616a8d49204 Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Thu, 11 May 2023 01:55:02 -0500 Subject: [PATCH 61/62] Add MXC URL's as data-attribute for easy reference Useful to quickly grab the MXC URL's to quarantine media in moderation scenarios --- .../session/room/timeline/tiles/BaseMediaTile.js | 10 +++++++++- src/platform/web/ui/session/room/timeline/ImageView.js | 2 ++ src/platform/web/ui/session/room/timeline/VideoView.js | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/domain/session/room/timeline/tiles/BaseMediaTile.js b/src/domain/session/room/timeline/tiles/BaseMediaTile.js index aa53661c53..af2feb44d7 100644 --- a/src/domain/session/room/timeline/tiles/BaseMediaTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMediaTile.js @@ -86,6 +86,14 @@ export class BaseMediaTile extends BaseMessageTile { } } + get mxcUrl() { + return this._getContent().url; + } + + get thumbnailMxcUrl() { + return this._getContent().info?.thumbnail_url; + } + get thumbnailUrl() { if (!this._isVisible) { return ""; @@ -93,7 +101,7 @@ export class BaseMediaTile extends BaseMessageTile { if (this._decryptedThumbnail) { return this._decryptedThumbnail.url; } else { - const thumbnailMxc = this._getContent().info?.thumbnail_url; + const thumbnailMxc = this.thumbnailMxcUrl; if (thumbnailMxc) { return this._mediaRepository.mxcUrlThumbnail(thumbnailMxc, this.width, this.height, "scale"); } diff --git a/src/platform/web/ui/session/room/timeline/ImageView.js b/src/platform/web/ui/session/room/timeline/ImageView.js index 19591606e6..5b850eebf8 100644 --- a/src/platform/web/ui/session/room/timeline/ImageView.js +++ b/src/platform/web/ui/session/room/timeline/ImageView.js @@ -22,6 +22,8 @@ export class ImageView extends BaseMediaView { src: vm => vm.thumbnailUrl, alt: vm => vm.label, title: vm => vm.label, + 'data-mxc-url': vm => vm.mxcUrl, + 'data-thumbnail-mxc-url': vm => vm.thumbnailMxcUrl, style: `max-width: ${vm.width}px; max-height: ${vm.height}px;` }); return vm.isPending || !vm.lightboxUrl ? img : t.a({href: vm.lightboxUrl}, img); diff --git a/src/platform/web/ui/session/room/timeline/VideoView.js b/src/platform/web/ui/session/room/timeline/VideoView.js index 9b092ed091..b5d19ffe35 100644 --- a/src/platform/web/ui/session/room/timeline/VideoView.js +++ b/src/platform/web/ui/session/room/timeline/VideoView.js @@ -24,6 +24,7 @@ export class VideoView extends BaseMediaView { // Chrome/Electron need this to enable the play button. src: vm => vm.videoUrl || `data:${vm.mimeType},`, title: vm => vm.label, + 'data-mxc-url': vm => vm.mxcUrl, controls: true, preload: "none", poster: vm => vm.thumbnailUrl, From 8034c53a2ed440a732ce387fdb8a02ac7b0ed96c Mon Sep 17 00:00:00 2001 From: Eric Eastwood Date: Fri, 26 May 2023 18:23:21 -0500 Subject: [PATCH 62/62] Backport fix to make SDK depedencies available when npm linking via https://github.com/vector-im/hydrogen-web/pull/1100 --- scripts/sdk/build.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/sdk/build.sh b/scripts/sdk/build.sh index 5e1632d3ee..e72d7dbfa8 100755 --- a/scripts/sdk/build.sh +++ b/scripts/sdk/build.sh @@ -12,6 +12,11 @@ yarn run vite build -c vite.sdk-assets-config.js yarn run vite build -c vite.sdk-lib-config.js yarn tsc -p tsconfig-declaration.json ./scripts/sdk/create-manifest.js ./target/package.json +pushd target/ +# Make sure the dependencies are available for any consuming project that uses +# `npm link hydrogen-view-sdk` +yarn install --no-lockfile +popd mkdir target/paths # this doesn't work, the ?url imports need to be in the consuming project, so disable for now # ./scripts/sdk/transform-paths.js ./src/platform/web/sdk/paths/vite.js ./target/paths/vite.js