forked from mystiq/hydrogen-web
draft of timeline tiles support
This commit is contained in:
parent
6940e14b18
commit
1f5d488105
5 changed files with 238 additions and 0 deletions
86
src/domain/session/room/timeline/TilesCollection.js
Normal file
86
src/domain/session/room/timeline/TilesCollection.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import BaseObservableList from "../../../../observable/list/BaseObservableList.js";
|
||||||
|
|
||||||
|
// maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or gap
|
||||||
|
export default class TilesCollection extends BaseObservableList {
|
||||||
|
constructor(entries, tileCreator) {
|
||||||
|
super();
|
||||||
|
this._entries = entries;
|
||||||
|
this._tiles = null;
|
||||||
|
this._entrySubscription = null;
|
||||||
|
this._tileCreator = tileCreator;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSubscribeFirst() {
|
||||||
|
this._entrySubscription = this._entries.subscribe(this);
|
||||||
|
this._populateTiles();
|
||||||
|
}
|
||||||
|
|
||||||
|
_populateTiles() {
|
||||||
|
this._tiles = [];
|
||||||
|
let currentTile = null;
|
||||||
|
for (let entry of this._entries) {
|
||||||
|
if (!currentTile || !currentTile.tryIncludeEntry(entry)) {
|
||||||
|
currentTile = this._tileCreator(entry);
|
||||||
|
this._tiles.push(currentTile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let prevTile = null;
|
||||||
|
for (let tile of this._tiles) {
|
||||||
|
if (prevTile) {
|
||||||
|
prevTile.updateNextSibling(tile);
|
||||||
|
}
|
||||||
|
tile.updatePreviousSibling(prevTile);
|
||||||
|
prevTile = tile;
|
||||||
|
}
|
||||||
|
if (prevTile) {
|
||||||
|
prevTile.updateNextSibling(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_findTileIndex(sortKey) {
|
||||||
|
return sortedIndex(this._tiles, sortKey, (key, tile) => {
|
||||||
|
return tile.compareSortKey(key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUnsubscribeLast() {
|
||||||
|
this._entrySubscription = this._entrySubscription();
|
||||||
|
this._tiles = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
onReset() {
|
||||||
|
// if TileViewModel were disposable, dispose here, or is that for views to do? views I suppose ...
|
||||||
|
this._buildInitialTiles();
|
||||||
|
this.emitReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
onAdd(index, value) {
|
||||||
|
// find position by sort key
|
||||||
|
// ask siblings to be included? both? yes, twice: a (insert c here) b, ask a(c), if yes ask b(a), else ask b(c)? if yes then b(a)?
|
||||||
|
}
|
||||||
|
|
||||||
|
onUpdate(index, value, params) {
|
||||||
|
// outcomes here can be
|
||||||
|
// tiles should be removed (got redacted and we don't want it in the timeline)
|
||||||
|
// tile should be added where there was none before ... ?
|
||||||
|
// entry should get it's own tile now
|
||||||
|
// merge with neighbours? ... hard to imagine for this use case ...
|
||||||
|
|
||||||
|
// also emit update for tile
|
||||||
|
}
|
||||||
|
|
||||||
|
onRemove(index, value) {
|
||||||
|
// find tile, if any
|
||||||
|
// remove entry from tile
|
||||||
|
// emit update or remove (if empty now) on tile
|
||||||
|
}
|
||||||
|
|
||||||
|
onMove(fromIdx, toIdx, value) {
|
||||||
|
// this ... cannot happen in the timeline?
|
||||||
|
// should be sorted by sortKey and sortKey is immutable
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator]() {
|
||||||
|
return this._tiles.values();
|
||||||
|
}
|
||||||
|
}
|
52
src/domain/session/room/timeline/TimelineViewModel.js
Normal file
52
src/domain/session/room/timeline/TimelineViewModel.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
/*
|
||||||
|
need better naming, but
|
||||||
|
entry = event or gap from matrix layer
|
||||||
|
tile = item on visual timeline like event, date separator?, group of joined events
|
||||||
|
|
||||||
|
|
||||||
|
shall we put date separators as marker in EventViewItem or separate item? binary search will be complicated ...
|
||||||
|
|
||||||
|
|
||||||
|
pagination ...
|
||||||
|
|
||||||
|
on the timeline viewmodel (containing the TilesCollection?) we'll have a method to (un)load a tail or head of
|
||||||
|
the timeline (counted in tiles), which results to a range in sortKeys we want on the screen. We pass that range
|
||||||
|
to the room timeline, which unload entries from memory.
|
||||||
|
when loading, it just reads events from a sortkey backwards or forwards...
|
||||||
|
*/
|
||||||
|
import TilesCollection from "./TilesCollection.js";
|
||||||
|
import tilesCreator from "./tilesCreator.js";
|
||||||
|
|
||||||
|
export default class TimelineViewModel {
|
||||||
|
constructor(timeline) {
|
||||||
|
this._timeline = timeline;
|
||||||
|
// once we support sending messages we could do
|
||||||
|
// timeline.entries.concat(timeline.pendingEvents)
|
||||||
|
// for an ObservableList that also contains local echos
|
||||||
|
this._tiles = new TilesCollection(timeline.entries, tilesCreator({timeline}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// doesn't fill gaps, only loads stored entries/tiles
|
||||||
|
loadAtTop() {
|
||||||
|
// load 100 entries, which may result in 0..100 tiles
|
||||||
|
return this._timeline.loadAtTop(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
unloadAtTop(tileAmount) {
|
||||||
|
// get lowerSortKey for tile at index tileAmount - 1
|
||||||
|
// tell timeline to unload till there (included given key)
|
||||||
|
}
|
||||||
|
|
||||||
|
loadAtBottom() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
unloadAtBottom(tileAmount) {
|
||||||
|
// get upperSortKey for tile at index tiles.length - tileAmount
|
||||||
|
// tell timeline to unload till there (included given key)
|
||||||
|
}
|
||||||
|
|
||||||
|
get tiles() {
|
||||||
|
return this._tiles;
|
||||||
|
}
|
||||||
|
}
|
13
src/domain/session/room/timeline/tiles/GapTile.js
Normal file
13
src/domain/session/room/timeline/tiles/GapTile.js
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import SimpleTile from "./SimpleTile";
|
||||||
|
|
||||||
|
export default class GapTile extends SimpleTile {
|
||||||
|
constructor(entry, timeline) {
|
||||||
|
super(entry);
|
||||||
|
this._timeline = timeline;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GapTile specific behaviour
|
||||||
|
fill() {
|
||||||
|
return this._timeline.fillGap(this._entry, 10);
|
||||||
|
}
|
||||||
|
}
|
52
src/domain/session/room/timeline/tiles/SimpleTile.js
Normal file
52
src/domain/session/room/timeline/tiles/SimpleTile.js
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
export default class SimpleTile {
|
||||||
|
constructor(entry) {
|
||||||
|
this._entry = entry;
|
||||||
|
}
|
||||||
|
// view model props for all subclasses
|
||||||
|
// hmmm, could also do instanceof ... ?
|
||||||
|
get shape() {
|
||||||
|
// "gap" | "message" | "image" | ... ?
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't show display name / avatar
|
||||||
|
// probably only for MessageTiles of some sort?
|
||||||
|
get isContinuation() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasDateSeparator() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get upperSortKey() {
|
||||||
|
return this._entry.sortKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
get lowerSortKey() {
|
||||||
|
return this._entry.sortKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TilesCollection contract
|
||||||
|
compareSortKey(key) {
|
||||||
|
return this._entry.sortKey.compare(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// update received for already included (falls within sort keys) entry
|
||||||
|
updateEntry(entry) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// simple entry can only contain 1 entry
|
||||||
|
tryIncludeEntry() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// let item know it has a new sibling
|
||||||
|
updatePreviousSibling(prev) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// let item know it has a new sibling
|
||||||
|
updateNextSibling(next) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
35
src/domain/session/room/timeline/tilesCreator.js
Normal file
35
src/domain/session/room/timeline/tilesCreator.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import GapTile from "./tiles/GapTile.js";
|
||||||
|
import TextTile from "./tiles/TextTile.js";
|
||||||
|
import ImageTile from "./tiles/ImageTile.js";
|
||||||
|
import RoomNameTile from "./tiles/RoomNameTile.js";
|
||||||
|
import RoomMemberTile from "./tiles/RoomMemberTile.js";
|
||||||
|
|
||||||
|
export default function ({timeline}) {
|
||||||
|
return function tilesCreator(entry) {
|
||||||
|
if (entry.gap) {
|
||||||
|
return new GapTile(entry, timeline);
|
||||||
|
} else if (entry.event) {
|
||||||
|
const event = entry.event;
|
||||||
|
switch (event.type) {
|
||||||
|
case "m.room.message": {
|
||||||
|
const content = event.content;
|
||||||
|
const msgtype = content && content.msgtype;
|
||||||
|
switch (msgtype) {
|
||||||
|
case "m.text":
|
||||||
|
return new TextTile(entry);
|
||||||
|
case "m.image":
|
||||||
|
return new ImageTile(entry);
|
||||||
|
default:
|
||||||
|
return null; // unknown tile types are not rendered?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "m.room.name":
|
||||||
|
return new RoomNameTile(entry);
|
||||||
|
case "m.room.member":
|
||||||
|
return new RoomMemberTile(entry);
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue