diff --git a/src/domain/session/room/RoomViewModel.js b/src/domain/session/room/RoomViewModel.js index 71060728..66042ae5 100644 --- a/src/domain/session/room/RoomViewModel.js +++ b/src/domain/session/room/RoomViewModel.js @@ -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) { diff --git a/src/domain/session/room/timeline/ReactionsViewModel.js b/src/domain/session/room/timeline/ReactionsViewModel.js index 4f366af0..1977b6f4 100644 --- a/src/domain/session/room/timeline/ReactionsViewModel.js +++ b/src/domain/session/room/timeline/ReactionsViewModel.js @@ -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 () {})); diff --git a/src/domain/session/room/timeline/TilesCollection.js b/src/domain/session/room/timeline/TilesCollection.js index 33ae4472..75af5b09 100644 --- a/src/domain/session/room/timeline/TilesCollection.js +++ b/src/domain/session/room/timeline/TilesCollection.js @@ -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) { diff --git a/src/domain/session/room/timeline/TimelineViewModel.js b/src/domain/session/room/timeline/TimelineViewModel.js index 2408146d..cf36fce4 100644 --- a/src/domain/session/room/timeline/TimelineViewModel.js +++ b/src/domain/session/room/timeline/TimelineViewModel.js @@ -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; diff --git a/src/domain/session/room/timeline/tiles/BaseMediaTile.js b/src/domain/session/room/timeline/tiles/BaseMediaTile.js index a927d766..0ba5b9a9 100644 --- a/src/domain/session/room/timeline/tiles/BaseMediaTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMediaTile.js @@ -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; diff --git a/src/domain/session/room/timeline/tiles/BaseMessageTile.js b/src/domain/session/room/timeline/tiles/BaseMessageTile.js index 3385a587..03cc16ba 100644 --- a/src/domain/session/room/timeline/tiles/BaseMessageTile.js +++ b/src/domain/session/room/timeline/tiles/BaseMessageTile.js @@ -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(); diff --git a/src/domain/session/room/timeline/tiles/BaseTextTile.js b/src/domain/session/room/timeline/tiles/BaseTextTile.js index 164443e3..8e78c95f 100644 --- a/src/domain/session/room/timeline/tiles/BaseTextTile.js +++ b/src/domain/session/room/timeline/tiles/BaseTextTile.js @@ -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 } diff --git a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js index 50f507eb..b96e2d85 100644 --- a/src/domain/session/room/timeline/tiles/EncryptedEventTile.js +++ b/src/domain/session/room/timeline/tiles/EncryptedEventTile.js @@ -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 diff --git a/src/domain/session/room/timeline/tiles/FileTile.js b/src/domain/session/room/timeline/tiles/FileTile.js index 1007d28c..3f7b539b 100644 --- a/src/domain/session/room/timeline/tiles/FileTile.js +++ b/src/domain/session/room/timeline/tiles/FileTile.js @@ -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; } diff --git a/src/domain/session/room/timeline/tiles/GapTile.js b/src/domain/session/room/timeline/tiles/GapTile.js index df0cedd9..6caa4b9b 100644 --- a/src/domain/session/room/timeline/tiles/GapTile.js +++ b/src/domain/session/room/timeline/tiles/GapTile.js @@ -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(); diff --git a/src/domain/session/room/timeline/tiles/ImageTile.js b/src/domain/session/room/timeline/tiles/ImageTile.js index eae2b926..dd959b28 100644 --- a/src/domain/session/room/timeline/tiles/ImageTile.js +++ b/src/domain/session/room/timeline/tiles/ImageTile.js @@ -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), diff --git a/src/domain/session/room/timeline/tiles/RoomMemberTile.js b/src/domain/session/room/timeline/tiles/RoomMemberTile.js index ce41f031..ca9cd9b7 100644 --- a/src/domain/session/room/timeline/tiles/RoomMemberTile.js +++ b/src/domain/session/room/timeline/tiles/RoomMemberTile.js @@ -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"); }, }; diff --git a/src/domain/session/room/timeline/tiles/SimpleTile.js b/src/domain/session/room/timeline/tiles/SimpleTile.js index af2b0e12..b8a7121e 100644 --- a/src/domain/session/room/timeline/tiles/SimpleTile.js +++ b/src/domain/session/room/timeline/tiles/SimpleTile.js @@ -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 ... ? diff --git a/src/domain/session/room/timeline/tiles/index.ts b/src/domain/session/room/timeline/tiles/index.ts new file mode 100644 index 00000000..242bea2f --- /dev/null +++ b/src/domain/session/room/timeline/tiles/index.ts @@ -0,0 +1,94 @@ +/* +Copyright 2020 Bruno Windels + +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; + } + } +} diff --git a/src/domain/session/room/timeline/tilesCreator.js b/src/domain/session/room/timeline/tilesCreator.js deleted file mode 100644 index dc9a850e..00000000 --- a/src/domain/session/room/timeline/tilesCreator.js +++ /dev/null @@ -1,81 +0,0 @@ -/* -Copyright 2020 Bruno Windels - -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; -} diff --git a/src/lib.ts b/src/lib.ts index a0ada84f..7f4e5316 100644 --- a/src/lib.ts +++ b/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"; diff --git a/src/platform/web/ui/session/room/RoomView.js b/src/platform/web/ui/session/room/RoomView.js index c172766a..2190f1f1 100644 --- a/src/platform/web/ui/session/room/RoomView.js +++ b/src/platform/web/ui/session/room/RoomView.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), diff --git a/src/platform/web/ui/session/room/TimelineView.ts b/src/platform/web/ui/session/room/TimelineView.ts index 936b8c7c..0c893847 100644 --- a/src/platform/web/ui/session/room/TimelineView.ts +++ b/src/platform/web/ui/session/room/TimelineView.ts @@ -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 { private tilesView?: TilesListView; private resizeObserver?: ResizeObserver; + constructor(vm: TimelineViewModel, private readonly viewClassForEntry: ViewClassForEntryFn) { + super(vm); + } + render(t: Builder, 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 { private onChanged: () => void; - constructor(tiles: ObservableList, onChanged: () => void) { - const options = { + constructor(tiles: ObservableList, 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 { 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, diff --git a/src/platform/web/ui/session/room/common.ts b/src/platform/web/ui/session/room/common.ts index 5048211a..201f14d0 100644 --- a/src/platform/web/ui/session/room/common.ts +++ b/src/platform/web/ui/session/room/common.ts @@ -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`); } }