forked from mystiq/hydrogen-web
Merge pull request #714 from vector-im/bwindels/custom-tiles
Allow custom timeline tiles for SDK usage
This commit is contained in:
commit
e977a6829b
19 changed files with 232 additions and 151 deletions
|
@ -18,17 +18,20 @@ limitations under the License.
|
|||
import {TimelineViewModel} from "./timeline/TimelineViewModel.js";
|
||||
import {ComposerViewModel} from "./ComposerViewModel.js"
|
||||
import {avatarInitials, getIdentifierColorNumber, getAvatarHttpUrl} from "../../avatar";
|
||||
import {tilesCreator} from "./timeline/tilesCreator.js";
|
||||
import {ViewModel} from "../../ViewModel";
|
||||
import {imageToInfo} from "../common.js";
|
||||
// TODO: remove fallback so default isn't included in bundle for SDK users that have their custom tileClassForEntry
|
||||
// this is a breaking SDK change though to make this option mandatory
|
||||
import {tileClassForEntry as defaultTileClassForEntry} from "./timeline/tiles/index";
|
||||
|
||||
export class RoomViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const {room} = options;
|
||||
const {room, tileClassForEntry} = options;
|
||||
this._room = room;
|
||||
this._timelineVM = null;
|
||||
this._tilesCreator = null;
|
||||
this._tileClassForEntry = tileClassForEntry ?? defaultTileClassForEntry;
|
||||
this._tileOptions = undefined;
|
||||
this._onRoomChange = this._onRoomChange.bind(this);
|
||||
this._timelineError = null;
|
||||
this._sendError = null;
|
||||
|
@ -46,12 +49,13 @@ export class RoomViewModel extends ViewModel {
|
|||
this._room.on("change", this._onRoomChange);
|
||||
try {
|
||||
const timeline = await this._room.openTimeline();
|
||||
this._tilesCreator = tilesCreator(this.childOptions({
|
||||
this._tileOptions = this.childOptions({
|
||||
roomVM: this,
|
||||
timeline,
|
||||
}));
|
||||
tileClassForEntry: this._tileClassForEntry,
|
||||
});
|
||||
this._timelineVM = this.track(new TimelineViewModel(this.childOptions({
|
||||
tilesCreator: this._tilesCreator,
|
||||
tileOptions: this._tileOptions,
|
||||
timeline,
|
||||
})));
|
||||
this.emitChange("timelineViewModel");
|
||||
|
@ -161,7 +165,12 @@ export class RoomViewModel extends ViewModel {
|
|||
}
|
||||
|
||||
_createTile(entry) {
|
||||
return this._tilesCreator(entry);
|
||||
if (this._tileOptions) {
|
||||
const Tile = this._tileOptions.tileClassForEntry(entry);
|
||||
if (Tile) {
|
||||
return new Tile(entry, this._tileOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _sendMessage(message, replyingTo) {
|
||||
|
|
|
@ -222,7 +222,7 @@ export function tests() {
|
|||
};
|
||||
const tiles = new MappedList(timeline.entries, entry => {
|
||||
if (entry.eventType === "m.room.message") {
|
||||
return new BaseMessageTile({entry, roomVM: {room}, timeline, platform: {logger}});
|
||||
return new BaseMessageTile(entry, {roomVM: {room}, timeline, platform: {logger}});
|
||||
}
|
||||
return null;
|
||||
}, (tile, params, entry) => tile?.updateEntry(entry, params, function () {}));
|
||||
|
|
|
@ -18,20 +18,27 @@ import {BaseObservableList} from "../../../../observable/list/BaseObservableList
|
|||
import {sortedIndex} from "../../../../utils/sortedIndex";
|
||||
|
||||
// maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary
|
||||
// for now, tileCreator should be stable in whether it returns a tile or not.
|
||||
// for now, tileClassForEntry should be stable in whether it returns a tile or not.
|
||||
// e.g. the decision to create a tile or not should be based on properties
|
||||
// not updated later on (e.g. event type)
|
||||
// also see big comment in onUpdate
|
||||
export class TilesCollection extends BaseObservableList {
|
||||
constructor(entries, tileCreator) {
|
||||
constructor(entries, tileOptions) {
|
||||
super();
|
||||
this._entries = entries;
|
||||
this._tiles = null;
|
||||
this._entrySubscription = null;
|
||||
this._tileCreator = tileCreator;
|
||||
this._tileOptions = tileOptions;
|
||||
this._emitSpontanousUpdate = this._emitSpontanousUpdate.bind(this);
|
||||
}
|
||||
|
||||
_createTile(entry) {
|
||||
const Tile = this._tileOptions.tileClassForEntry(entry);
|
||||
if (Tile) {
|
||||
return new Tile(entry, this._tileOptions);
|
||||
}
|
||||
}
|
||||
|
||||
_emitSpontanousUpdate(tile, params) {
|
||||
const entry = tile.lowerEntry;
|
||||
const tileIdx = this._findTileIdx(entry);
|
||||
|
@ -48,7 +55,7 @@ export class TilesCollection extends BaseObservableList {
|
|||
let currentTile = null;
|
||||
for (let entry of this._entries) {
|
||||
if (!currentTile || !currentTile.tryIncludeEntry(entry)) {
|
||||
currentTile = this._tileCreator(entry);
|
||||
currentTile = this._createTile(entry);
|
||||
if (currentTile) {
|
||||
this._tiles.push(currentTile);
|
||||
}
|
||||
|
@ -121,7 +128,7 @@ export class TilesCollection extends BaseObservableList {
|
|||
return;
|
||||
}
|
||||
|
||||
const newTile = this._tileCreator(entry);
|
||||
const newTile = this._createTile(entry);
|
||||
if (newTile) {
|
||||
if (prevTile) {
|
||||
prevTile.updateNextSibling(newTile);
|
||||
|
@ -150,9 +157,9 @@ export class TilesCollection extends BaseObservableList {
|
|||
const tileIdx = this._findTileIdx(entry);
|
||||
const tile = this._findTileAtIdx(entry, tileIdx);
|
||||
if (tile) {
|
||||
const action = tile.updateEntry(entry, params, this._tileCreator);
|
||||
const action = tile.updateEntry(entry, params);
|
||||
if (action.shouldReplace) {
|
||||
const newTile = this._tileCreator(entry);
|
||||
const newTile = this._createTile(entry);
|
||||
if (newTile) {
|
||||
this._replaceTile(tileIdx, tile, newTile, action.updateParams);
|
||||
newTile.setUpdateEmit(this._emitSpontanousUpdate);
|
||||
|
@ -303,7 +310,10 @@ export function tests() {
|
|||
}
|
||||
}
|
||||
const entries = new ObservableArray([{n: 5}, {n: 10}]);
|
||||
const tiles = new TilesCollection(entries, entry => new UpdateOnSiblingTile(entry));
|
||||
const tileOptions = {
|
||||
tileClassForEntry: entry => UpdateOnSiblingTile,
|
||||
};
|
||||
const tiles = new TilesCollection(entries, tileOptions);
|
||||
let receivedAdd = false;
|
||||
tiles.subscribe({
|
||||
onAdd(idx, tile) {
|
||||
|
@ -326,7 +336,10 @@ export function tests() {
|
|||
}
|
||||
}
|
||||
const entries = new ObservableArray([{n: 5}, {n: 10}, {n: 15}]);
|
||||
const tiles = new TilesCollection(entries, entry => new UpdateOnSiblingTile(entry));
|
||||
const tileOptions = {
|
||||
tileClassForEntry: entry => UpdateOnSiblingTile,
|
||||
};
|
||||
const tiles = new TilesCollection(entries, tileOptions);
|
||||
const events = [];
|
||||
tiles.subscribe({
|
||||
onUpdate(idx, tile) {
|
||||
|
|
|
@ -37,9 +37,9 @@ import {ViewModel} from "../../../ViewModel";
|
|||
export class TimelineViewModel extends ViewModel {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
const {timeline, tilesCreator} = options;
|
||||
const {timeline, tileOptions} = options;
|
||||
this._timeline = this.track(timeline);
|
||||
this._tiles = new TilesCollection(timeline.entries, tilesCreator);
|
||||
this._tiles = new TilesCollection(timeline.entries, tileOptions);
|
||||
this._startTile = null;
|
||||
this._endTile = null;
|
||||
this._topLoadingPromise = null;
|
||||
|
|
|
@ -21,8 +21,8 @@ const MAX_HEIGHT = 300;
|
|||
const MAX_WIDTH = 400;
|
||||
|
||||
export class BaseMediaTile extends BaseMessageTile {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
this._decryptedThumbnail = null;
|
||||
this._decryptedFile = null;
|
||||
this._isVisible = false;
|
||||
|
|
|
@ -19,8 +19,8 @@ import {ReactionsViewModel} from "../ReactionsViewModel.js";
|
|||
import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar";
|
||||
|
||||
export class BaseMessageTile extends SimpleTile {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
||||
this._isContinuation = false;
|
||||
this._reactions = null;
|
||||
|
@ -28,7 +28,7 @@ export class BaseMessageTile extends SimpleTile {
|
|||
if (this._entry.annotations || this._entry.pendingAnnotations) {
|
||||
this._updateReactions();
|
||||
}
|
||||
this._updateReplyTileIfNeeded(options.tilesCreator, undefined);
|
||||
this._updateReplyTileIfNeeded(undefined);
|
||||
}
|
||||
|
||||
notifyVisible() {
|
||||
|
@ -122,23 +122,27 @@ export class BaseMessageTile extends SimpleTile {
|
|||
}
|
||||
}
|
||||
|
||||
updateEntry(entry, param, tilesCreator) {
|
||||
const action = super.updateEntry(entry, param, tilesCreator);
|
||||
updateEntry(entry, param) {
|
||||
const action = super.updateEntry(entry, param);
|
||||
if (action.shouldUpdate) {
|
||||
this._updateReactions();
|
||||
}
|
||||
this._updateReplyTileIfNeeded(tilesCreator, param);
|
||||
this._updateReplyTileIfNeeded(param);
|
||||
return action;
|
||||
}
|
||||
|
||||
_updateReplyTileIfNeeded(tilesCreator, param) {
|
||||
_updateReplyTileIfNeeded(param) {
|
||||
const replyEntry = this._entry.contextEntry;
|
||||
if (replyEntry) {
|
||||
// this is an update to contextEntry used for replyPreview
|
||||
const action = this._replyTile?.updateEntry(replyEntry, param, tilesCreator);
|
||||
const action = this._replyTile?.updateEntry(replyEntry, param);
|
||||
if (action?.shouldReplace || !this._replyTile) {
|
||||
this.disposeTracked(this._replyTile);
|
||||
this._replyTile = tilesCreator(replyEntry);
|
||||
const tileClassForEntry = this._options.tileClassForEntry;
|
||||
const ReplyTile = tileClassForEntry(replyEntry);
|
||||
if (ReplyTile) {
|
||||
this._replyTile = new ReplyTile(replyEntry, this._options);
|
||||
}
|
||||
}
|
||||
if(action?.shouldUpdate) {
|
||||
this._replyTile?.emitChange();
|
||||
|
|
|
@ -21,8 +21,8 @@ import {createEnum} from "../../../../../utils/enum";
|
|||
export const BodyFormat = createEnum("Plain", "Html");
|
||||
|
||||
export class BaseTextTile extends BaseMessageTile {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
this._messageBody = null;
|
||||
this._format = null
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ import {BaseTextTile} from "./BaseTextTile.js";
|
|||
import {UpdateAction} from "../UpdateAction.js";
|
||||
|
||||
export class EncryptedEventTile extends BaseTextTile {
|
||||
updateEntry(entry, params, tilesCreator) {
|
||||
const parentResult = super.updateEntry(entry, params, tilesCreator);
|
||||
updateEntry(entry, params) {
|
||||
const parentResult = super.updateEntry(entry, params);
|
||||
// event got decrypted, recreate the tile and replace this one with it
|
||||
if (entry.eventType !== "m.room.encrypted") {
|
||||
// the "shape" parameter trigger tile recreation in TimelineView
|
||||
|
|
|
@ -20,8 +20,8 @@ import {formatSize} from "../../../../../utils/formatSize";
|
|||
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
|
||||
|
||||
export class FileTile extends BaseMessageTile {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
this._downloadError = null;
|
||||
this._downloading = false;
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ import {SimpleTile} from "./SimpleTile.js";
|
|||
import {UpdateAction} from "../UpdateAction.js";
|
||||
|
||||
export class GapTile extends SimpleTile {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
this._loading = false;
|
||||
this._error = null;
|
||||
this._isAtTop = true;
|
||||
|
@ -81,8 +81,8 @@ export class GapTile extends SimpleTile {
|
|||
this._siblingChanged = true;
|
||||
}
|
||||
|
||||
updateEntry(entry, params, tilesCreator) {
|
||||
super.updateEntry(entry, params, tilesCreator);
|
||||
updateEntry(entry, params) {
|
||||
super.updateEntry(entry, params);
|
||||
if (!entry.isGap) {
|
||||
return UpdateAction.Remove();
|
||||
} else {
|
||||
|
@ -125,7 +125,7 @@ export function tests() {
|
|||
tile.updateEntry(newEntry);
|
||||
}
|
||||
};
|
||||
const tile = new GapTile({entry: new FragmentBoundaryEntry(fragment, true), roomVM: {room}});
|
||||
const tile = new GapTile(new FragmentBoundaryEntry(fragment, true), {roomVM: {room}});
|
||||
await tile.fill();
|
||||
await tile.fill();
|
||||
await tile.fill();
|
||||
|
|
|
@ -18,8 +18,8 @@ limitations under the License.
|
|||
import {BaseMediaTile} from "./BaseMediaTile.js";
|
||||
|
||||
export class ImageTile extends BaseMediaTile {
|
||||
constructor(options) {
|
||||
super(options);
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
this._lightboxUrl = this.urlCreator.urlForSegments([
|
||||
// ensure the right room is active if in grid view
|
||||
this.navigation.segment("room", this._room.id),
|
||||
|
|
|
@ -66,23 +66,25 @@ export class RoomMemberTile extends SimpleTile {
|
|||
export function tests() {
|
||||
return {
|
||||
"user removes display name": (assert) => {
|
||||
const tile = new RoomMemberTile({
|
||||
entry: {
|
||||
const tile = new RoomMemberTile(
|
||||
{
|
||||
prevContent: {displayname: "foo", membership: "join"},
|
||||
content: {membership: "join"},
|
||||
stateKey: "foo@bar.com",
|
||||
},
|
||||
});
|
||||
{}
|
||||
);
|
||||
assert.strictEqual(tile.announcement, "foo@bar.com removed their name (foo)");
|
||||
},
|
||||
"user without display name sets a new display name": (assert) => {
|
||||
const tile = new RoomMemberTile({
|
||||
entry: {
|
||||
const tile = new RoomMemberTile(
|
||||
{
|
||||
prevContent: {membership: "join"},
|
||||
content: {displayname: "foo", membership: "join" },
|
||||
stateKey: "foo@bar.com",
|
||||
},
|
||||
});
|
||||
{}
|
||||
);
|
||||
assert.strictEqual(tile.announcement, "foo@bar.com changed their name to foo");
|
||||
},
|
||||
};
|
||||
|
|
|
@ -19,9 +19,9 @@ import {ViewModel} from "../../../../ViewModel";
|
|||
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
|
||||
|
||||
export class SimpleTile extends ViewModel {
|
||||
constructor(options) {
|
||||
constructor(entry, options) {
|
||||
super(options);
|
||||
this._entry = options.entry;
|
||||
this._entry = entry;
|
||||
}
|
||||
// view model props for all subclasses
|
||||
// hmmm, could also do instanceof ... ?
|
||||
|
|
94
src/domain/session/room/timeline/tiles/index.ts
Normal file
94
src/domain/session/room/timeline/tiles/index.ts
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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 {GapTile} from "./GapTile.js";
|
||||
import {TextTile} from "./TextTile.js";
|
||||
import {RedactedTile} from "./RedactedTile.js";
|
||||
import {ImageTile} from "./ImageTile.js";
|
||||
import {VideoTile} from "./VideoTile.js";
|
||||
import {FileTile} from "./FileTile.js";
|
||||
import {LocationTile} from "./LocationTile.js";
|
||||
import {RoomNameTile} from "./RoomNameTile.js";
|
||||
import {RoomMemberTile} from "./RoomMemberTile.js";
|
||||
import {EncryptedEventTile} from "./EncryptedEventTile.js";
|
||||
import {EncryptionEnabledTile} from "./EncryptionEnabledTile.js";
|
||||
import {MissingAttachmentTile} from "./MissingAttachmentTile.js";
|
||||
|
||||
import type {SimpleTile} from "./SimpleTile.js";
|
||||
import type {Room} from "../../../../../matrix/room/Room";
|
||||
import type {Timeline} from "../../../../../matrix/room/timeline/Timeline";
|
||||
import type {FragmentBoundaryEntry} from "../../../../../matrix/room/timeline/entries/FragmentBoundaryEntry";
|
||||
import type {EventEntry} from "../../../../../matrix/room/timeline/entries/EventEntry";
|
||||
import type {PendingEventEntry} from "../../../../../matrix/room/timeline/entries/PendingEventEntry";
|
||||
import type {Options as ViewModelOptions} from "../../../../ViewModel";
|
||||
|
||||
export type TimelineEntry = FragmentBoundaryEntry | EventEntry | PendingEventEntry;
|
||||
export type TileClassForEntryFn = (entry: TimelineEntry) => TileConstructor | undefined;
|
||||
export type Options = ViewModelOptions & {
|
||||
room: Room,
|
||||
timeline: Timeline
|
||||
tileClassForEntry: TileClassForEntryFn;
|
||||
};
|
||||
export type TileConstructor = new (entry: TimelineEntry, options: Options) => SimpleTile;
|
||||
|
||||
export function tileClassForEntry(entry: TimelineEntry): TileConstructor | undefined {
|
||||
if (entry.isGap) {
|
||||
return GapTile;
|
||||
} else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
|
||||
return MissingAttachmentTile;
|
||||
} else if (entry.eventType) {
|
||||
switch (entry.eventType) {
|
||||
case "m.room.message": {
|
||||
if (entry.isRedacted) {
|
||||
return RedactedTile;
|
||||
}
|
||||
const content = entry.content;
|
||||
const msgtype = content && content.msgtype;
|
||||
switch (msgtype) {
|
||||
case "m.text":
|
||||
case "m.notice":
|
||||
case "m.emote":
|
||||
return TextTile;
|
||||
case "m.image":
|
||||
return ImageTile;
|
||||
case "m.video":
|
||||
return VideoTile;
|
||||
case "m.file":
|
||||
return FileTile;
|
||||
case "m.location":
|
||||
return LocationTile;
|
||||
default:
|
||||
// unknown msgtype not rendered
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
case "m.room.name":
|
||||
return RoomNameTile;
|
||||
case "m.room.member":
|
||||
return RoomMemberTile;
|
||||
case "m.room.encrypted":
|
||||
if (entry.isRedacted) {
|
||||
return RedactedTile;
|
||||
}
|
||||
return EncryptedEventTile;
|
||||
case "m.room.encryption":
|
||||
return EncryptionEnabledTile;
|
||||
default:
|
||||
// unknown type not rendered
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
|
||||
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 {GapTile} from "./tiles/GapTile.js";
|
||||
import {TextTile} from "./tiles/TextTile.js";
|
||||
import {RedactedTile} from "./tiles/RedactedTile.js";
|
||||
import {ImageTile} from "./tiles/ImageTile.js";
|
||||
import {VideoTile} from "./tiles/VideoTile.js";
|
||||
import {FileTile} from "./tiles/FileTile.js";
|
||||
import {LocationTile} from "./tiles/LocationTile.js";
|
||||
import {RoomNameTile} from "./tiles/RoomNameTile.js";
|
||||
import {RoomMemberTile} from "./tiles/RoomMemberTile.js";
|
||||
import {EncryptedEventTile} from "./tiles/EncryptedEventTile.js";
|
||||
import {EncryptionEnabledTile} from "./tiles/EncryptionEnabledTile.js";
|
||||
import {MissingAttachmentTile} from "./tiles/MissingAttachmentTile.js";
|
||||
|
||||
export function tilesCreator(baseOptions) {
|
||||
const tilesCreator = function tilesCreator(entry, emitUpdate) {
|
||||
const options = Object.assign({entry, emitUpdate, tilesCreator}, baseOptions);
|
||||
if (entry.isGap) {
|
||||
return new GapTile(options);
|
||||
} else if (entry.isPending && entry.pendingEvent.isMissingAttachments) {
|
||||
return new MissingAttachmentTile(options);
|
||||
} else if (entry.eventType) {
|
||||
switch (entry.eventType) {
|
||||
case "m.room.message": {
|
||||
if (entry.isRedacted) {
|
||||
return new RedactedTile(options);
|
||||
}
|
||||
const content = entry.content;
|
||||
const msgtype = content && content.msgtype;
|
||||
switch (msgtype) {
|
||||
case "m.text":
|
||||
case "m.notice":
|
||||
case "m.emote":
|
||||
return new TextTile(options);
|
||||
case "m.image":
|
||||
return new ImageTile(options);
|
||||
case "m.video":
|
||||
return new VideoTile(options);
|
||||
case "m.file":
|
||||
return new FileTile(options);
|
||||
case "m.location":
|
||||
return new LocationTile(options);
|
||||
default:
|
||||
// unknown msgtype not rendered
|
||||
return null;
|
||||
}
|
||||
}
|
||||
case "m.room.name":
|
||||
return new RoomNameTile(options);
|
||||
case "m.room.member":
|
||||
return new RoomMemberTile(options);
|
||||
case "m.room.encrypted":
|
||||
if (entry.isRedacted) {
|
||||
return new RedactedTile(options);
|
||||
}
|
||||
return new EncryptedEventTile(options);
|
||||
case "m.room.encryption":
|
||||
return new EncryptionEnabledTile(options);
|
||||
default:
|
||||
// unknown type not rendered
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
return tilesCreator;
|
||||
}
|
34
src/lib.ts
34
src/lib.ts
|
@ -26,7 +26,41 @@ 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 {TimelineViewModel} from "./domain/session/room/timeline/TimelineViewModel.js";
|
||||
export {tileClassForEntry} from "./domain/session/room/timeline/tiles/index";
|
||||
export type {TimelineEntry, TileClassForEntryFn, Options, TileConstructor} from "./domain/session/room/timeline/tiles/index";
|
||||
// export timeline tile view models
|
||||
export {GapTile} from "./domain/session/room/timeline/tiles/GapTile.js";
|
||||
export {TextTile} from "./domain/session/room/timeline/tiles/TextTile.js";
|
||||
export {RedactedTile} from "./domain/session/room/timeline/tiles/RedactedTile.js";
|
||||
export {ImageTile} from "./domain/session/room/timeline/tiles/ImageTile.js";
|
||||
export {VideoTile} from "./domain/session/room/timeline/tiles/VideoTile.js";
|
||||
export {FileTile} from "./domain/session/room/timeline/tiles/FileTile.js";
|
||||
export {LocationTile} from "./domain/session/room/timeline/tiles/LocationTile.js";
|
||||
export {RoomNameTile} from "./domain/session/room/timeline/tiles/RoomNameTile.js";
|
||||
export {RoomMemberTile} from "./domain/session/room/timeline/tiles/RoomMemberTile.js";
|
||||
export {EncryptedEventTile} from "./domain/session/room/timeline/tiles/EncryptedEventTile.js";
|
||||
export {EncryptionEnabledTile} from "./domain/session/room/timeline/tiles/EncryptionEnabledTile.js";
|
||||
export {MissingAttachmentTile} from "./domain/session/room/timeline/tiles/MissingAttachmentTile.js";
|
||||
export {SimpleTile} from "./domain/session/room/timeline/tiles/SimpleTile.js";
|
||||
|
||||
export {TimelineView} from "./platform/web/ui/session/room/TimelineView";
|
||||
export {viewClassForEntry} from "./platform/web/ui/session/room/common";
|
||||
export type {TileViewConstructor, ViewClassForEntryFn} from "./platform/web/ui/session/room/TimelineView";
|
||||
// export timeline tile views
|
||||
export {AnnouncementView} from "./platform/web/ui/session/room/timeline/AnnouncementView.js";
|
||||
export {BaseMediaView} from "./platform/web/ui/session/room/timeline/BaseMediaView.js";
|
||||
export {BaseMessageView} from "./platform/web/ui/session/room/timeline/BaseMessageView.js";
|
||||
export {FileView} from "./platform/web/ui/session/room/timeline/FileView.js";
|
||||
export {GapView} from "./platform/web/ui/session/room/timeline/GapView.js";
|
||||
export {ImageView} from "./platform/web/ui/session/room/timeline/ImageView.js";
|
||||
export {LocationView} from "./platform/web/ui/session/room/timeline/LocationView.js";
|
||||
export {MissingAttachmentView} from "./platform/web/ui/session/room/timeline/MissingAttachmentView.js";
|
||||
export {ReactionsView} from "./platform/web/ui/session/room/timeline/ReactionsView.js";
|
||||
export {RedactedView} from "./platform/web/ui/session/room/timeline/RedactedView.js";
|
||||
export {ReplyPreviewView} from "./platform/web/ui/session/room/timeline/ReplyPreviewView.js";
|
||||
export {TextMessageView} from "./platform/web/ui/session/room/timeline/TextMessageView.js";
|
||||
export {VideoView} from "./platform/web/ui/session/room/timeline/VideoView.js";
|
||||
|
||||
export {Navigation} from "./domain/navigation/Navigation.js";
|
||||
export {ComposerViewModel} from "./domain/session/room/ComposerViewModel.js";
|
||||
export {MessageComposer} from "./platform/web/ui/session/room/MessageComposer.js";
|
||||
|
|
|
@ -23,6 +23,7 @@ import {TimelineLoadingView} from "./TimelineLoadingView.js";
|
|||
import {MessageComposer} from "./MessageComposer.js";
|
||||
import {RoomArchivedView} from "./RoomArchivedView.js";
|
||||
import {AvatarView} from "../../AvatarView.js";
|
||||
import {viewClassForEntry} from "./common";
|
||||
|
||||
export class RoomView extends TemplateView {
|
||||
constructor(options) {
|
||||
|
@ -54,7 +55,7 @@ export class RoomView extends TemplateView {
|
|||
t.div({className: "RoomView_error"}, vm => vm.error),
|
||||
t.mapView(vm => vm.timelineViewModel, timelineViewModel => {
|
||||
return timelineViewModel ?
|
||||
new TimelineView(timelineViewModel) :
|
||||
new TimelineView(timelineViewModel, viewClassForEntry) :
|
||||
new TimelineLoadingView(vm); // vm is just needed for i18n
|
||||
}),
|
||||
t.view(bottomView),
|
||||
|
|
|
@ -14,9 +14,8 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import type {TileView} from "./common";
|
||||
import {viewClassForEntry} from "./common";
|
||||
import {ListView} from "../../general/ListView";
|
||||
import type {IView} from "../../general/types";
|
||||
import {TemplateView, Builder} from "../../general/TemplateView";
|
||||
import {IObservableValue} from "../../general/BaseUpdateView";
|
||||
import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
|
||||
|
@ -25,6 +24,13 @@ import {RedactedView} from "./timeline/RedactedView.js";
|
|||
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
|
||||
import {BaseObservableList as ObservableList} from "../../../../../observable/list/BaseObservableList";
|
||||
|
||||
export interface TileView extends IView {
|
||||
readonly value: SimpleTile;
|
||||
onClick(event: UIEvent);
|
||||
}
|
||||
export type TileViewConstructor = new (tile: SimpleTile) => TileView;
|
||||
export type ViewClassForEntryFn = (tile: SimpleTile) => TileViewConstructor;
|
||||
|
||||
//import {TimelineViewModel} from "../../../../../domain/session/room/timeline/TimelineViewModel.js";
|
||||
export interface TimelineViewModel extends IObservableValue {
|
||||
showJumpDown: boolean;
|
||||
|
@ -55,13 +61,17 @@ export class TimelineView extends TemplateView<TimelineViewModel> {
|
|||
private tilesView?: TilesListView;
|
||||
private resizeObserver?: ResizeObserver;
|
||||
|
||||
constructor(vm: TimelineViewModel, private readonly viewClassForEntry: ViewClassForEntryFn) {
|
||||
super(vm);
|
||||
}
|
||||
|
||||
render(t: Builder<TimelineViewModel>, vm: TimelineViewModel) {
|
||||
// assume this view will be mounted in the parent DOM straight away
|
||||
requestAnimationFrame(() => {
|
||||
// do initial scroll positioning
|
||||
this.restoreScrollPosition();
|
||||
});
|
||||
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition());
|
||||
this.tilesView = new TilesListView(vm.tiles, () => this.restoreScrollPosition(), this.viewClassForEntry);
|
||||
const root = t.div({className: "Timeline"}, [
|
||||
t.div({
|
||||
className: "Timeline_scroller bottom-aligned-scroll",
|
||||
|
@ -174,16 +184,13 @@ class TilesListView extends ListView<SimpleTile, TileView> {
|
|||
|
||||
private onChanged: () => void;
|
||||
|
||||
constructor(tiles: ObservableList<SimpleTile>, onChanged: () => void) {
|
||||
const options = {
|
||||
constructor(tiles: ObservableList<SimpleTile>, onChanged: () => void, private readonly viewClassForEntry: ViewClassForEntryFn) {
|
||||
super({
|
||||
list: tiles,
|
||||
onItemClick: (tileView, evt) => tileView.onClick(evt),
|
||||
};
|
||||
super(options, entry => {
|
||||
}, entry => {
|
||||
const View = viewClassForEntry(entry);
|
||||
if (View) {
|
||||
return new View(entry);
|
||||
}
|
||||
return new View(entry);
|
||||
});
|
||||
this.onChanged = onChanged;
|
||||
}
|
||||
|
@ -195,7 +202,7 @@ class TilesListView extends ListView<SimpleTile, TileView> {
|
|||
|
||||
onUpdate(index: number, value: SimpleTile, param: any) {
|
||||
if (param === "shape") {
|
||||
const ExpectedClass = viewClassForEntry(value);
|
||||
const ExpectedClass = this.viewClassForEntry(value);
|
||||
const child = this.getChildInstanceByIndex(index);
|
||||
if (!ExpectedClass || !(child instanceof ExpectedClass)) {
|
||||
// shape was updated, so we need to recreate the tile view,
|
||||
|
|
|
@ -24,14 +24,10 @@ import {AnnouncementView} from "./timeline/AnnouncementView.js";
|
|||
import {RedactedView} from "./timeline/RedactedView.js";
|
||||
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
|
||||
import {GapView} from "./timeline/GapView.js";
|
||||
import type {TileViewConstructor, ViewClassForEntryFn} from "./TimelineView";
|
||||
|
||||
export type TileView = GapView | AnnouncementView | TextMessageView |
|
||||
ImageView | VideoView | FileView | LocationView | MissingAttachmentView | RedactedView;
|
||||
|
||||
// TODO: this is what works for a ctor but doesn't actually check we constrain the returned ctors to the types above
|
||||
type TileViewConstructor = (this: TileView, SimpleTile) => void;
|
||||
export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | undefined {
|
||||
switch (entry.shape) {
|
||||
export function viewClassForEntry(vm: SimpleTile): TileViewConstructor {
|
||||
switch (vm.shape) {
|
||||
case "gap":
|
||||
return GapView;
|
||||
case "announcement":
|
||||
|
@ -51,5 +47,7 @@ export function viewClassForEntry(entry: SimpleTile): TileViewConstructor | unde
|
|||
return MissingAttachmentView;
|
||||
case "redacted":
|
||||
return RedactedView;
|
||||
default:
|
||||
throw new Error(`Tiles of shape "${vm.shape}" are not supported, check the tileClassForEntry function in the view model`);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue