draft of timeline tiles support

This commit is contained in:
Bruno Windels 2019-03-08 20:04:56 +01:00
parent 6940e14b18
commit 1f5d488105
5 changed files with 238 additions and 0 deletions

View 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();
}
}

View 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;
}
}

View 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);
}
}

View 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) {
}
}

View 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;
}
}
}
}