Compare commits
27 commits
master
...
madlittlem
Author | SHA1 | Date | |
---|---|---|---|
|
acaf53de3e | ||
|
88e24703ff | ||
|
b54e884b7e | ||
|
c824012968 | ||
|
871cf1ad80 | ||
|
8dc3c13a93 | ||
|
6a6f22047e | ||
|
72300d1b0c | ||
|
5d9dc638ea | ||
|
1a0b1403ef | ||
|
2d3b78b725 | ||
|
ae673862dc | ||
|
c24ac43e72 | ||
|
ea2d45cab7 | ||
|
082d997eed | ||
|
fc89bfdd53 | ||
|
3e58619935 | ||
|
a4cdde6f53 | ||
|
6005fcfc55 | ||
|
1032f4dbc6 | ||
|
48825ea30f | ||
|
e75f18c87a | ||
|
8d0c4e68b6 | ||
|
4eb24db1de | ||
|
eda179a154 | ||
|
5805ce0310 | ||
|
dcc508c037 |
17 changed files with 145 additions and 50 deletions
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
69
src/domain/session/room/lightbox-navigation.js
Normal file
69
src/domain/session/room/lightbox-navigation.js
Normal 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));
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
13
src/lib.ts
13
src/lib.ts
|
@ -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";
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 */
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(''),
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Reference in a new issue