Compare commits

...

27 commits

Author SHA1 Message Date
Eric Eastwood
acaf53de3e More exports 2022-07-21 20:00:16 -05:00
Eric Eastwood
88e24703ff Scope log 2022-07-21 19:59:56 -05:00
Eric Eastwood
b54e884b7e Expose error when we fail to createObjectStore 2022-07-21 17:39:42 -05:00
Eric Eastwood
c824012968 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
2022-07-21 17:23:49 -05:00
Eric Eastwood
871cf1ad80 Revert "Ignore missing events"
This reverts commit 8dc3c13a93.
2022-07-20 02:27:35 -05:00
Eric Eastwood
8dc3c13a93 Ignore missing events 2022-07-20 02:27:18 -05:00
Eric Eastwood
6a6f22047e Merge branch 'master' into madlittlemods/matrix-public-archive-scratch-changes 2022-07-05 06:00:00 -05:00
Eric Eastwood
72300d1b0c Lightbox escape keyboard shortcut also works 2022-06-07 22:35:26 -05:00
Eric Eastwood
5d9dc638ea URL hashes relative to the room of the archive 2022-06-07 19:41:08 -05:00
Eric Eastwood
1a0b1403ef Working lightbox pops up and closes 2022-06-07 17:55:53 -05:00
Eric Eastwood
2d3b78b725 WIP: Make the lightbox open, not working yet 2022-06-07 17:16:58 -05:00
Eric Eastwood
ae673862dc Use correct variable in comment 2022-06-06 17:53:26 -05:00
Eric Eastwood
c24ac43e72 Merge branch 'master' into madlittlemods/matrix-public-archive-scratch-changes
Conflicts:
	scripts/sdk/base-manifest.json
	scripts/sdk/build.sh
	src/domain/session/room/RoomViewModel.js
	src/platform/web/Platform.js
	src/platform/web/ui/general/html.ts
2022-06-06 15:26:52 -05:00
Eric Eastwood
ea2d45cab7 No need to comment this out since linkedom supports it now
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
2022-02-25 01:48:16 -06:00
Eric Eastwood
082d997eed 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
```
2022-02-24 12:15:14 -06:00
Eric Eastwood
fc89bfdd53 Get rid of duplicate export 2022-02-24 02:44:16 -06:00
Eric Eastwood
3e58619935 Add more SVG elements 2022-02-24 02:43:36 -06:00
Eric Eastwood
a4cdde6f53 Use UTC timestamps and add data attribute for easy targeting in tests 2022-02-24 02:41:54 -06:00
Eric Eastwood
6005fcfc55 Add permalink to timestamp 2022-02-24 02:40:37 -06:00
Eric Eastwood
1032f4dbc6 Merge branch 'master' into madlittlemods/matrix-public-archive-scratch-changes
Conflicts:
	scripts/sdk/base-manifest.json
	src/platform/web/parsehtml.js
2022-02-14 15:03:07 -06:00
Eric Eastwood
48825ea30f 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
2022-02-11 19:23:45 -06:00
Eric Eastwood
e75f18c87a Support custom RightPanel content 2022-02-10 02:22:04 -06:00
Eric Eastwood
8d0c4e68b6 Some changes to support RoomView with no composer 2022-02-10 01:45:10 -06:00
Eric Eastwood
4eb24db1de Fix reply tiles not showing the new message 2022-02-09 01:50:05 -06:00
Eric Eastwood
eda179a154 Remove dom side-effect from rendering 2022-02-04 01:26:18 -06:00
Eric Eastwood
5805ce0310 Remove some scratch changes 2022-02-02 01:17:07 -06:00
Eric Eastwood
dcc508c037 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
2022-02-02 01:08:54 -06:00
17 changed files with 145 additions and 50 deletions

View file

@ -28,12 +28,14 @@ import type {Clock} from "../platform/web/dom/Clock";
import type {ILogger} from "../logging/types"; import type {ILogger} from "../logging/types";
import type {Navigation} from "./navigation/Navigation"; import type {Navigation} from "./navigation/Navigation";
import type {URLRouter} from "./navigation/URLRouter"; import type {URLRouter} from "./navigation/URLRouter";
import type {History} from "../platform/web/dom/History";
export type Options = { export type Options = {
platform: Platform platform: Platform
logger: ILogger logger: ILogger
urlCreator: URLRouter urlCreator: URLRouter
navigation: Navigation navigation: Navigation
history: History
emitChange?: (params: any) => void emitChange?: (params: any) => void
} }
@ -142,4 +144,8 @@ export class ViewModel<O extends Options = Options> extends EventEmitter<{change
get navigation(): Navigation { get navigation(): Navigation {
return this._options.navigation; return this._options.navigation;
} }
get history(): History {
return this._options.history;
}
} }

View file

@ -20,7 +20,7 @@ import {RoomViewModel} from "./room/RoomViewModel.js";
import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js"; import {UnknownRoomViewModel} from "./room/UnknownRoomViewModel.js";
import {InviteViewModel} from "./room/InviteViewModel.js"; import {InviteViewModel} from "./room/InviteViewModel.js";
import {RoomBeingCreatedViewModel} from "./room/RoomBeingCreatedViewModel.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 {SessionStatusViewModel} from "./SessionStatusViewModel.js";
import {RoomGridViewModel} from "./RoomGridViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js";
import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {SettingsViewModel} from "./settings/SettingsViewModel.js";
@ -81,12 +81,12 @@ export class SessionViewModel extends ViewModel {
})); }));
this._updateCreateRoom(createRoom.get()); this._updateCreateRoom(createRoom.get());
const lightbox = this.navigation.observe("lightbox"); setupLightboxNavigation(this, 'lightboxViewModel', (eventId) => {
this.track(lightbox.subscribe(eventId => { return {
this._updateLightbox(eventId); room,
})); eventId,
this._updateLightbox(lightbox.get()); };
});
const rightpanel = this.navigation.observe("right-panel"); const rightpanel = this.navigation.observe("right-panel");
this.track(rightpanel.subscribe(() => this._updateRightPanel())); this.track(rightpanel.subscribe(() => this._updateRightPanel()));
@ -267,21 +267,6 @@ export class SessionViewModel extends ViewModel {
this.emitChange("activeMiddleViewModel"); 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() { _roomFromNavigation() {
const roomId = this.navigation.path.get("room")?.value; const roomId = this.navigation.path.get("room")?.value;
const room = this._client.session.rooms.get(roomId); const room = this._client.session.rooms.get(roomId);

View file

@ -19,21 +19,25 @@ import {ViewModel} from "../../ViewModel";
export class LightboxViewModel extends ViewModel { export class LightboxViewModel extends ViewModel {
constructor(options) { constructor(options) {
super(options); super(options);
this._eventId = options.eventId; this._eventEntry = options.eventEntry;
this._eventId = options.eventId || options.eventEntry.id;
this._unencryptedImageUrl = null; this._unencryptedImageUrl = null;
this._decryptedImage = null; this._decryptedImage = null;
this._closeUrl = this.urlCreator.urlUntilSegment("room"); this._closeUrl = this.urlCreator.urlUntilSegment("room");
this._eventEntry = null;
this._date = null; this._date = null;
this._subscribeToEvent(options.room, options.eventId); this._subscribeToEvent(options.room, options.eventId);
} }
_subscribeToEvent(room, eventId) { _subscribeToEvent(room, eventId) {
const eventObservable = room.observeEvent(eventId); let event = this._eventEntry;
this.track(eventObservable.subscribe(eventEntry => { if (!this._eventEntry) {
this._loadEvent(room, eventEntry); const eventObservable = room.observeEvent(eventId);
})); this.track(eventObservable.subscribe(eventEntry => {
this._loadEvent(room, eventObservable.get()); this._loadEvent(room, eventEntry);
}));
event = eventObservable.get();
}
this._loadEvent(room, event);
} }
async _loadEvent(room, eventEntry) { async _loadEvent(room, eventEntry) {
@ -92,6 +96,6 @@ export class LightboxViewModel extends ViewModel {
} }
close() { close() {
this.platform.history.pushUrl(this.closeUrl); this.history.pushUrl(this.closeUrl);
} }
} }

View file

@ -49,6 +49,7 @@ export class RoomViewModel extends ViewModel {
this._room.on("change", this._onRoomChange); this._room.on("change", this._onRoomChange);
try { try {
const timeline = await this._room.openTimeline(); const timeline = await this._room.openTimeline();
console.log('timeline', timeline.entries);
this._tileOptions = this.childOptions({ this._tileOptions = this.childOptions({
roomVM: this, roomVM: this,
timeline, timeline,

View file

@ -0,0 +1,69 @@
/*
Copyright 2022 Bruno Windels <bruno@windels.cloud>
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');
/**
* 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);
}
// 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);
}
}
/**
* 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() {
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, lightboxChildOptionsFunction(eventId));
}));
// Also handle the case where the URL already includes `/lightbox/$eventId` (like
// from page-load)
const initialLightBoxEventId = lightbox.get();
updateLightboxViewModel(vm, fieldName, lightboxChildOptionsFunction(initialLightBoxEventId));
}

View file

@ -49,7 +49,7 @@ export class TimelineViewModel extends ViewModel {
this._showJumpDown = false; 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) { setVisibleTileRange(startTile, endTile) {
// don't clear these once done as they are used to check // don't clear these once done as they are used to check
// for more tiles once loadAtTop finishes // for more tiles once loadAtTop finishes

View file

@ -49,6 +49,10 @@ export class BaseMessageTile extends SimpleTile {
return `https://matrix.to/#/${encodeURIComponent(this.sender)}`; return `https://matrix.to/#/${encodeURIComponent(this.sender)}`;
} }
get eventId() {
return this._entry.id;
}
get displayName() { get displayName() {
return this._entry.displayName || this.sender; return this._entry.displayName || this.sender;
} }
@ -79,15 +83,15 @@ export class BaseMessageTile extends SimpleTile {
} }
get date() { 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() { 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() { get isOwn() {
return this._entry.sender === this._ownMember.userId; return this._entry.sender === this._ownMember?.userId;
} }
get isContinuation() { get isContinuation() {

View file

@ -25,6 +25,18 @@ export {SessionViewModel} from "./domain/session/SessionViewModel.js";
export {SessionView} from "./platform/web/ui/session/SessionView.js"; export {SessionView} from "./platform/web/ui/session/SessionView.js";
export {RoomViewModel} from "./domain/session/room/RoomViewModel.js"; export {RoomViewModel} from "./domain/session/room/RoomViewModel.js";
export {RoomView} from "./platform/web/ui/session/room/RoomView.js"; export {RoomView} from "./platform/web/ui/session/room/RoomView.js";
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";
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 {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js";
export {tileClassForEntry} from "./domain/session/room/timeline/tiles/index"; export {tileClassForEntry} from "./domain/session/room/timeline/tiles/index";
export type {TimelineEntry, TileClassForEntryFn, Options, TileConstructor} from "./domain/session/room/timeline/tiles/index"; export type {TimelineEntry, TileClassForEntryFn, Options, TileConstructor} from "./domain/session/room/timeline/tiles/index";
@ -62,6 +74,7 @@ export {TextMessageView} from "./platform/web/ui/session/room/timeline/TextMessa
export {VideoView} from "./platform/web/ui/session/room/timeline/VideoView.js"; export {VideoView} from "./platform/web/ui/session/room/timeline/VideoView.js";
export {Navigation} from "./domain/navigation/Navigation.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 {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js";
export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js"; export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js";
export {TemplateView} from "./platform/web/ui/general/TemplateView"; export {TemplateView} from "./platform/web/ui/general/TemplateView";

View file

@ -85,6 +85,7 @@ export class Timeline {
const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(20, txn, log)); const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(20, txn, log));
try { try {
const entries = await readerRequest.complete(); const entries = await readerRequest.complete();
console.log('entries', entries)
this._loadContextEntriesWhereNeeded(entries); this._loadContextEntriesWhereNeeded(entries);
this._setupEntries(entries); this._setupEntries(entries);
} finally { } finally {
@ -198,8 +199,10 @@ export class Timeline {
if (!this._localEntries?.hasSubscriptions) { if (!this._localEntries?.hasSubscriptions) {
return; return;
} }
// find any local relations to this new remote event // find any local relations to these new remote events or maybe these
for (const pee of this._localEntries) { // 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 // this will work because we set relatedEventId when removing remote echos
if (pee.relatedEventId) { if (pee.relatedEventId) {
const relationTarget = entries.find(e => e.id === pee.relatedEventId); const relationTarget = entries.find(e => e.id === pee.relatedEventId);

View file

@ -54,6 +54,7 @@ async function readRawTimelineEntriesWithTxn(roomId, eventKey, direction, amount
} else { } else {
eventsWithinFragment = await timelineStore.eventsBefore(roomId, eventKey, amount); eventsWithinFragment = await timelineStore.eventsBefore(roomId, eventKey, amount);
} }
console.log('readRawTimelineEntriesWithTxn eventsWithinFragment', eventsWithinFragment)
let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, fragmentIdComparer)); let eventEntries = eventsWithinFragment.map(e => new EventEntry(e, fragmentIdComparer));
entries = directionalConcat(entries, eventEntries, direction); entries = directionalConcat(entries, eventEntries, direction);
// prepend or append eventsWithinFragment to entries, and wrap them in EventEntry // prepend or append eventsWithinFragment to entries, and wrap them in EventEntry

View file

@ -34,12 +34,11 @@ interface ServiceWorkerHandler {
async function requestPersistedStorage(): Promise<boolean> { async function requestPersistedStorage(): Promise<boolean> {
// don't assume browser so we can run in node with fake-idb // don't assume browser so we can run in node with fake-idb
const glob = this; if (window?.navigator?.storage?.persist) {
if (glob?.navigator?.storage?.persist) { return await window.navigator.storage.persist();
return await glob.navigator.storage.persist(); } else if (window?.document.requestStorageAccess) {
} else if (glob?.document.requestStorageAccess) {
try { try {
await glob.document.requestStorageAccess(); await window.document.requestStorageAccess();
return true; return true;
} catch (err) { } catch (err) {
return false; return false;

View file

@ -40,20 +40,20 @@ interface TimelineEventEntry {
type TimelineEventStorageEntry = TimelineEventEntry & { key: string, eventIdKey: string }; 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)}`; 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("|"); const [roomId, fragmentId, eventIndex] = key.split("|");
return {roomId, eventKey: new EventKey(decodeUint32(fragmentId), decodeUint32(eventIndex))}; 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}`; 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("|"); const [roomId, eventId] = eventIdKey.split("|");
return {roomId, eventId}; return {roomId, eventId};
} }

View file

@ -80,10 +80,15 @@ export function openDatabase(name: string, createObjectStore: CreateObjectStore,
try { try {
await createObjectStore(db, txn, oldVersion, version); await createObjectStore(db, txn, oldVersion, version);
} catch (err) { } catch (err) {
console.error(`openDatabase: Failed to createObjectStore in database=${name}`, err);
// try aborting on error, if that hasn't been done already // try aborting on error, if that hasn't been done already
try { try {
txn.abort(); 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); return reqAsPromise(req);

View file

@ -30,10 +30,10 @@ export class History extends BaseObservableValue {
But for SSO, we need to handle <root>/?loginToken=<TOKEN> But for SSO, we need to handle <root>/?loginToken=<TOKEN>
Handle that as a special case for now. 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.search;
} }
return document.location.hash; return document?.location?.hash;
} }
/** does not emit */ /** does not emit */

View file

@ -39,6 +39,8 @@ export class RightPanelView extends TemplateView {
return new MemberListView(vm); return new MemberListView(vm);
case "member-details": case "member-details":
return new MemberDetailsView(vm); return new MemberDetailsView(vm);
case "custom":
return new vm.customView(vm);
default: default:
return new LoadingView(); return new LoadingView();
} }

View file

@ -15,6 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import { text } from "../../general/html";
import {TemplateView} from "../../general/TemplateView"; import {TemplateView} from "../../general/TemplateView";
import {Popup} from "../../general/Popup.js"; import {Popup} from "../../general/Popup.js";
import {Menu} from "../../general/Menu.js"; import {Menu} from "../../general/Menu.js";
@ -58,7 +59,7 @@ export class RoomView extends TemplateView {
new TimelineView(timelineViewModel, this._viewClassForTile) : new TimelineView(timelineViewModel, this._viewClassForTile) :
new TimelineLoadingView(vm); // vm is just needed for i18n new TimelineLoadingView(vm); // vm is just needed for i18n
}), }),
t.view(bottomView), bottomView ? t.view(bottomView) : text(''),
]) ])
]); ]);
} }

View file

@ -20,7 +20,9 @@ import {ReplyPreviewError, ReplyPreviewView} from "./ReplyPreviewView.js";
export class TextMessageView extends BaseMessageView { export class TextMessageView extends BaseMessageView {
renderMessageBody(t, vm) { 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({ const container = t.div({
className: { className: {
"Timeline_messageBody": true, "Timeline_messageBody": true,