mirror of
https://github.com/vector-im/hydrogen-web.git
synced 2025-02-02 07:31:38 +01:00
WIP
This commit is contained in:
parent
12e378eb62
commit
3f7c1577e0
@ -16,6 +16,7 @@ limitations under the License.
|
||||
|
||||
import {BaseObservableList} from "../../../../observable/list/BaseObservableList";
|
||||
import {sortedIndex} from "../../../../utils/sortedIndex";
|
||||
import {TileShape} from "./tiles/ITile";
|
||||
|
||||
// maps 1..n entries to 0..1 tile. Entries are what is stored in the timeline, either an event or fragmentboundary
|
||||
// for now, tileClassForEntry should be stable in whether it returns a tile or not.
|
||||
@ -51,12 +52,14 @@ export class TilesCollection extends BaseObservableList {
|
||||
}
|
||||
|
||||
_populateTiles() {
|
||||
this._silent = true;
|
||||
this._tiles = [];
|
||||
let currentTile = null;
|
||||
for (let entry of this._entries) {
|
||||
if (!currentTile || !currentTile.tryIncludeEntry(entry)) {
|
||||
currentTile = this._createTile(entry);
|
||||
if (currentTile) {
|
||||
console.log("adding initial tile", currentTile.shape, currentTile.eventId, "at", this._tiles.length);
|
||||
this._tiles.push(currentTile);
|
||||
}
|
||||
}
|
||||
@ -72,11 +75,20 @@ export class TilesCollection extends BaseObservableList {
|
||||
if (prevTile) {
|
||||
prevTile.updateNextSibling(null);
|
||||
}
|
||||
// add date headers here
|
||||
for (let idx = 0; idx < this._tiles.length; idx += 1) {
|
||||
const tile = this._tiles[idx];
|
||||
if (tile.needsDateSeparator) {
|
||||
this._addTileAt(idx, tile.createDateSeparator(), true);
|
||||
idx += 1; // tile's index moved one up, don't process it again
|
||||
}
|
||||
}
|
||||
// now everything is wired up,
|
||||
// allow tiles to emit updates
|
||||
for (const tile of this._tiles) {
|
||||
tile.setUpdateEmit(this._emitSpontanousUpdate);
|
||||
}
|
||||
this._silent = false;
|
||||
}
|
||||
|
||||
_findTileIdx(entry) {
|
||||
@ -130,25 +142,61 @@ export class TilesCollection extends BaseObservableList {
|
||||
|
||||
const newTile = this._createTile(entry);
|
||||
if (newTile) {
|
||||
if (prevTile) {
|
||||
prevTile.updateNextSibling(newTile);
|
||||
// this emits an update while the add hasn't been emitted yet
|
||||
newTile.updatePreviousSibling(prevTile);
|
||||
}
|
||||
if (nextTile) {
|
||||
newTile.updateNextSibling(nextTile);
|
||||
nextTile.updatePreviousSibling(newTile);
|
||||
}
|
||||
this._tiles.splice(tileIdx, 0, newTile);
|
||||
this.emitAdd(tileIdx, newTile);
|
||||
// add event is emitted, now the tile
|
||||
// can emit updates
|
||||
newTile.setUpdateEmit(this._emitSpontanousUpdate);
|
||||
console.log("adding tile", newTile.shape, newTile.eventId, "at", tileIdx);
|
||||
this._addTileAt(tileIdx, newTile);
|
||||
this._evaluateDateHeaderAtIdx(tileIdx);
|
||||
}
|
||||
// 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)?
|
||||
}
|
||||
|
||||
_evaluateDateHeaderAtIdx(tileIdx) {
|
||||
//console.log("_evaluateDateHeaderAtIdx", tileIdx);
|
||||
// consider the two adjacent tiles where the previous sibling changed:
|
||||
// the new tile and the next tile
|
||||
for (let i = 0; i < 5; i += 1) {
|
||||
const idx = Math.max(tileIdx + i - 2, 0);
|
||||
const tile = this._tiles[idx];
|
||||
const prevTile = idx > 0 ? this._tiles[idx - 1] : undefined;
|
||||
const hasDateSeparator = prevTile?.shape === TileShape.DateHeader;
|
||||
if (tile.needsDateSeparator) {
|
||||
if (hasDateSeparator) {
|
||||
// TODO: replace this by return UpdateAction from updateNextSibling
|
||||
// and do this in onAdd
|
||||
//console.log(" update", idx - 1, prevTile?.shape, prevTile?.eventId);
|
||||
this.emitUpdate(idx - 1, prevTile, "date");
|
||||
} else {
|
||||
//console.log(" add", idx, tile.shape, tile.eventId);
|
||||
tileIdx += 1;
|
||||
this._addTileAt(idx, tile.createDateSeparator());
|
||||
}
|
||||
// TODO must be looking at the wrong index to find the old date separator??
|
||||
} else if (hasDateSeparator) {
|
||||
// this is never triggered because needsDateSeparator is not cleared
|
||||
// when loading more items because we don't do anything once the
|
||||
// direct sibling is a DateTile
|
||||
//console.log(" remove", idx -1, prevTile?.shape, prevTile?.eventId);
|
||||
this._removeTile(idx - 1, prevTile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addTileAt(idx, newTile, silent = false) {
|
||||
const prevTile = idx > 0 ? this._tiles[idx - 1] : undefined;
|
||||
const nextTile = this._tiles[idx];
|
||||
prevTile?.updateNextSibling(newTile);
|
||||
newTile.updatePreviousSibling(prevTile);
|
||||
newTile.updateNextSibling(nextTile);
|
||||
nextTile?.updatePreviousSibling(newTile);
|
||||
this._tiles.splice(idx, 0, newTile);
|
||||
if (!silent) {
|
||||
this.emitAdd(idx, newTile);
|
||||
}
|
||||
// add event is emitted, now the tile
|
||||
// can emit updates
|
||||
newTile.setUpdateEmit(this._emitSpontanousUpdate);
|
||||
}
|
||||
|
||||
onUpdate(index, entry, params) {
|
||||
// if an update is emitted while calling source.subscribe() from onSubscribeFirst, ignore it
|
||||
if (!this._tiles) {
|
||||
|
@ -15,13 +15,13 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {SimpleTile} from "./SimpleTile.js";
|
||||
import {TileShape} from "./ITile";
|
||||
import {ReactionsViewModel} from "../ReactionsViewModel.js";
|
||||
import {getIdentifierColorNumber, avatarInitials, getAvatarHttpUrl} from "../../../../avatar";
|
||||
|
||||
export class BaseMessageTile extends SimpleTile {
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : null;
|
||||
this._isContinuation = false;
|
||||
this._reactions = null;
|
||||
this._replyTile = null;
|
||||
@ -78,6 +78,7 @@ export class BaseMessageTile extends SimpleTile {
|
||||
return this.sender;
|
||||
}
|
||||
|
||||
// TODO: remove?
|
||||
get date() {
|
||||
return this._date && this._date.toLocaleDateString({}, {month: "numeric", day: "numeric"});
|
||||
}
|
||||
|
141
src/domain/session/room/timeline/tiles/DateTile.ts
Normal file
141
src/domain/session/room/timeline/tiles/DateTile.ts
Normal file
@ -0,0 +1,141 @@
|
||||
import {ITile, TileShape, EmitUpdateFn} from "./ITile";
|
||||
import {UpdateAction} from "../UpdateAction";
|
||||
import {BaseEntry} from "../../../../../matrix/room/timeline/entries/BaseEntry";
|
||||
import {BaseEventEntry} from "../../../../../matrix/room/timeline/entries/BaseEventEntry";
|
||||
import {ViewModel} from "../../../../ViewModel";
|
||||
import type {Options} from "../../../../ViewModel";
|
||||
|
||||
/**
|
||||
* edge cases:
|
||||
* - be able to remove the tile in response to the sibling changing,
|
||||
* probably by letting updateNextSibling/updatePreviousSibling
|
||||
* return an UpdateAction and change TilesCollection accordingly.
|
||||
* this is relevant when next becomes undefined there when
|
||||
* a pending event is removed on remote echo.
|
||||
* */
|
||||
|
||||
export class DateTile extends ViewModel implements ITile<BaseEventEntry> {
|
||||
private _emitUpdate?: EmitUpdateFn;
|
||||
private _dateString?: string;
|
||||
|
||||
constructor(private _firstTileInDay: ITile<BaseEventEntry>, options: Options) {
|
||||
super(options);
|
||||
}
|
||||
|
||||
setUpdateEmit(emitUpdate: EmitUpdateFn): void {
|
||||
this._emitUpdate = emitUpdate;
|
||||
}
|
||||
|
||||
get upperEntry(): BaseEventEntry {
|
||||
return this.refEntry;
|
||||
}
|
||||
|
||||
get lowerEntry(): BaseEventEntry {
|
||||
return this.refEntry;
|
||||
}
|
||||
|
||||
/** the entry reference by this datetile, e.g. the entry of the first tile for this day */
|
||||
private get refEntry(): BaseEventEntry {
|
||||
// lowerEntry is the first entry... i think?
|
||||
// so given the date header always comes before,
|
||||
// this is our closest entry.
|
||||
return this._firstTileInDay.lowerEntry;
|
||||
}
|
||||
|
||||
compare(tile: ITile<BaseEntry>): number {
|
||||
return this.compareEntry(tile.upperEntry);
|
||||
}
|
||||
|
||||
get date(): string {
|
||||
if (!this._dateString) {
|
||||
const date = new Date(this.refEntry.timestamp);
|
||||
this._dateString = date.toLocaleDateString({}, {
|
||||
weekday: 'long',
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
});
|
||||
}
|
||||
return this._dateString;
|
||||
}
|
||||
|
||||
get shape(): TileShape {
|
||||
return TileShape.DateHeader;
|
||||
}
|
||||
|
||||
get needsDateSeparator(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
createDateSeparator(): undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* _findTileIdx in TilesCollection should never return
|
||||
* the index of a DateTile as that is mainly used
|
||||
* for mapping incoming event indices coming from the Timeline
|
||||
* to the tile index to propage the event.
|
||||
* This is not a path that is relevant to date headers as they
|
||||
* are added as a side-effect of adding other tiles and are generally
|
||||
* not updated (only removed in some cases). _findTileIdx is also
|
||||
* used for emitting spontanous updates, but that should also not be
|
||||
* needed for a DateTile.
|
||||
* The problem is basically that _findTileIdx maps an entry to
|
||||
* a tile, and DateTile adopts the entry of it's sibling tile (_firstTileInDay)
|
||||
* so now we have the entry pointing to two tiles. So we should avoid
|
||||
* returning the DateTile itself from the compare method.
|
||||
* We will always return -1 or 1 from here to signal an entry comes before or after us,
|
||||
* never 0
|
||||
* */
|
||||
compareEntry(entry: BaseEntry): number {
|
||||
const result = this.refEntry.compare(entry);
|
||||
if (result === 0) {
|
||||
// if it's a match for the reference entry (e.g. _firstTileInDay),
|
||||
// say it comes after us as the date tile always comes at the top
|
||||
// of the day.
|
||||
return -1;
|
||||
}
|
||||
// otherwise, assume the given entry is never for ourselves
|
||||
// as we don't have our own entry, we only borrow one from _firstTileInDay
|
||||
return result;
|
||||
}
|
||||
|
||||
// update received for already included (falls within sort keys) entry
|
||||
updateEntry(entry, param): UpdateAction {
|
||||
return UpdateAction.Nothing();
|
||||
}
|
||||
|
||||
// return whether the tile should be removed
|
||||
// as SimpleTile only has one entry, the tile should be removed
|
||||
removeEntry(entry: BaseEntry): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// SimpleTile can only contain 1 entry
|
||||
tryIncludeEntry(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// let item know it has a new sibling
|
||||
updatePreviousSibling(prev: ITile<BaseEntry> | undefined): void {
|
||||
this._firstTileInDay.updatePreviousSibling(prev);
|
||||
}
|
||||
|
||||
// let item know it has a new sibling
|
||||
updateNextSibling(next: ITile<BaseEntry> | undefined): UpdateAction {
|
||||
// TODO: next can be undefined when a pending event is removed
|
||||
// TODO: we need a way to remove this date header
|
||||
this._firstTileInDay = next!;
|
||||
this._dateString = undefined;
|
||||
// TODO: do we need to reevaluate our date here and emit an update?
|
||||
}
|
||||
|
||||
notifyVisible(): void {
|
||||
// trigger sticky logic here?
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
}
|
||||
}
|
@ -19,6 +19,7 @@ import {UpdateAction} from "../UpdateAction.js";
|
||||
import {ConnectionError} from "../../../../../matrix/error.js";
|
||||
import {ConnectionStatus} from "../../../../../matrix/net/Reconnector";
|
||||
|
||||
// TODO: should this become an ITile and SimpleTile become EventTile?
|
||||
export class GapTile extends SimpleTile {
|
||||
constructor(entry, options) {
|
||||
super(entry, options);
|
||||
@ -29,6 +30,10 @@ export class GapTile extends SimpleTile {
|
||||
this._showSpinner = false;
|
||||
}
|
||||
|
||||
get needsDateSeparator() {
|
||||
return false;
|
||||
}
|
||||
|
||||
async fill() {
|
||||
if (!this._loading && !this._entry.edgeReached) {
|
||||
this._loading = true;
|
||||
|
44
src/domain/session/room/timeline/tiles/ITile.ts
Normal file
44
src/domain/session/room/timeline/tiles/ITile.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import {UpdateAction} from "../UpdateAction.js";
|
||||
import {BaseEntry} from "../../../../../matrix/room/timeline/entries/BaseEntry";
|
||||
import {BaseEventEntry} from "../../../../../matrix/room/timeline/entries/BaseEventEntry";
|
||||
import {IDisposable} from "../../../../../utils/Disposables";
|
||||
|
||||
export type EmitUpdateFn = (tile: ITile<BaseEntry>, props: any) => void
|
||||
|
||||
export enum TileShape {
|
||||
Message = "message",
|
||||
MessageStatus = "message-status",
|
||||
Announcement = "announcement",
|
||||
File = "file",
|
||||
Gap = "gap",
|
||||
Image = "image",
|
||||
Location = "location",
|
||||
MissingAttachment = "missing-attachment",
|
||||
Redacted = "redacted",
|
||||
Video = "video",
|
||||
DateHeader = "date-header"
|
||||
}
|
||||
|
||||
// TODO: should we imply inheriting from view model here?
|
||||
export interface ITile<E extends BaseEntry = BaseEntry> extends IDisposable {
|
||||
setUpdateEmit(emitUpdate: EmitUpdateFn): void;
|
||||
get upperEntry(): E;
|
||||
get lowerEntry(): E;
|
||||
compare(tile: ITile<BaseEntry>): number;
|
||||
compareEntry(entry: BaseEntry): number;
|
||||
// update received for already included (falls within sort keys) entry
|
||||
updateEntry(entry: BaseEntry, param: any): UpdateAction;
|
||||
// return whether the tile should be removed
|
||||
// as SimpleTile only has one entry, the tile should be removed
|
||||
removeEntry(entry: BaseEntry): boolean
|
||||
// SimpleTile can only contain 1 entry
|
||||
tryIncludeEntry(): boolean;
|
||||
// let item know it has a new sibling
|
||||
updatePreviousSibling(prev: ITile<BaseEntry> | undefined): void;
|
||||
// let item know it has a new sibling
|
||||
updateNextSibling(next: ITile<BaseEntry> | undefined): void;
|
||||
notifyVisible(): void;
|
||||
get needsDateSeparator(): boolean;
|
||||
createDateSeparator(): ITile<BaseEntry> | undefined;
|
||||
get shape(): TileShape;
|
||||
}
|
@ -15,13 +15,17 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import {UpdateAction} from "../UpdateAction.js";
|
||||
import {TileShape} from "./ITile";
|
||||
import {ViewModel} from "../../../../ViewModel";
|
||||
import {SendStatus} from "../../../../../matrix/room/sending/PendingEvent.js";
|
||||
import {DateTile} from "./DateTile";
|
||||
|
||||
export class SimpleTile extends ViewModel {
|
||||
constructor(entry, options) {
|
||||
super(options);
|
||||
this._entry = entry;
|
||||
this._date = this._entry.timestamp ? new Date(this._entry.timestamp) : undefined;
|
||||
this._needsDateSeparator = false;
|
||||
this._emitUpdate = undefined;
|
||||
}
|
||||
// view model props for all subclasses
|
||||
@ -37,8 +41,26 @@ export class SimpleTile extends ViewModel {
|
||||
return false;
|
||||
}
|
||||
|
||||
get hasDateSeparator() {
|
||||
return false;
|
||||
get needsDateSeparator() {
|
||||
return this._needsDateSeparator;
|
||||
}
|
||||
|
||||
createDateSeparator() {
|
||||
return new DateTile(this, this.childOptions({}));
|
||||
}
|
||||
|
||||
_updateDateSeparator(prev) {
|
||||
if (prev && prev._date && this._date) {
|
||||
const neededDateSeparator = this._needsDateSeparator;
|
||||
this._needsDateSeparator = prev._date.getFullYear() !== this._date.getFullYear() ||
|
||||
prev._date.getMonth() !== this._date.getMonth() ||
|
||||
prev._date.getDate() !== this._date.getDate();
|
||||
if (neededDateSeparator && !this._needsDateSeparator) {
|
||||
console.log("clearing needsDateSeparator", {this: this._entry.content, prev: prev.content});
|
||||
}
|
||||
} else {
|
||||
this._needsDateSeparator = !!this._date;
|
||||
}
|
||||
}
|
||||
|
||||
get id() {
|
||||
@ -123,8 +145,10 @@ export class SimpleTile extends ViewModel {
|
||||
return false;
|
||||
}
|
||||
// let item know it has a new sibling
|
||||
updatePreviousSibling(/*prev*/) {
|
||||
|
||||
updatePreviousSibling(prev) {
|
||||
if (prev?.shape !== TileShape.DateHeader) {
|
||||
this._updateDateSeparator(prev);
|
||||
}
|
||||
}
|
||||
|
||||
// let item know it has a new sibling
|
||||
|
@ -27,7 +27,7 @@ import {EncryptedEventTile} from "./EncryptedEventTile.js";
|
||||
import {EncryptionEnabledTile} from "./EncryptionEnabledTile.js";
|
||||
import {MissingAttachmentTile} from "./MissingAttachmentTile.js";
|
||||
|
||||
import type {SimpleTile} from "./SimpleTile.js";
|
||||
import type {ITile, TileShape} from "./ITile";
|
||||
import type {Room} from "../../../../../matrix/room/Room";
|
||||
import type {Timeline} from "../../../../../matrix/room/timeline/Timeline";
|
||||
import type {FragmentBoundaryEntry} from "../../../../../matrix/room/timeline/entries/FragmentBoundaryEntry";
|
||||
@ -42,7 +42,7 @@ export type Options = ViewModelOptions & {
|
||||
timeline: Timeline
|
||||
tileClassForEntry: TileClassForEntryFn;
|
||||
};
|
||||
export type TileConstructor = new (entry: TimelineEntry, options: Options) => SimpleTile;
|
||||
export type TileConstructor = new (entry: TimelineEntry, options: Options) => ITile;
|
||||
|
||||
export function tileClassForEntry(entry: TimelineEntry): TileConstructor | undefined {
|
||||
if (entry.isGap) {
|
||||
|
@ -22,31 +22,34 @@ import {LocationView} from "./timeline/LocationView.js";
|
||||
import {MissingAttachmentView} from "./timeline/MissingAttachmentView.js";
|
||||
import {AnnouncementView} from "./timeline/AnnouncementView.js";
|
||||
import {RedactedView} from "./timeline/RedactedView.js";
|
||||
import {SimpleTile} from "../../../../../domain/session/room/timeline/tiles/SimpleTile.js";
|
||||
import {ITile, TileShape} from "../../../../../domain/session/room/timeline/tiles/ITile.js";
|
||||
import {GapView} from "./timeline/GapView.js";
|
||||
import {DateHeaderView} from "./timeline/DateHeaderView";
|
||||
import type {TileViewConstructor, ViewClassForEntryFn} from "./TimelineView";
|
||||
|
||||
export function viewClassForTile(vm: SimpleTile): TileViewConstructor {
|
||||
export function viewClassForTile(vm: ITile): TileViewConstructor {
|
||||
switch (vm.shape) {
|
||||
case "gap":
|
||||
case TileShape.Gap:
|
||||
return GapView;
|
||||
case "announcement":
|
||||
case TileShape.Announcement:
|
||||
return AnnouncementView;
|
||||
case "message":
|
||||
case "message-status":
|
||||
case TileShape.Message:
|
||||
case TileShape.MessageStatus:
|
||||
return TextMessageView;
|
||||
case "image":
|
||||
case TileShape.Image:
|
||||
return ImageView;
|
||||
case "video":
|
||||
case TileShape.Video:
|
||||
return VideoView;
|
||||
case "file":
|
||||
case TileShape.File:
|
||||
return FileView;
|
||||
case "location":
|
||||
case TileShape.Location:
|
||||
return LocationView;
|
||||
case "missing-attachment":
|
||||
case TileShape.MissingAttachment:
|
||||
return MissingAttachmentView;
|
||||
case "redacted":
|
||||
case TileShape.Redacted:
|
||||
return RedactedView;
|
||||
case TileShape.DateHeader:
|
||||
return DateHeaderView;
|
||||
default:
|
||||
throw new Error(`Tiles of shape "${vm.shape}" are not supported, check the tileClassForEntry function in the view model`);
|
||||
}
|
||||
|
33
src/platform/web/ui/session/room/timeline/DateHeaderView.ts
Normal file
33
src/platform/web/ui/session/room/timeline/DateHeaderView.ts
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
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 {TemplateView} from "../../../general/TemplateView";
|
||||
import {spinner} from "../../../common.js";
|
||||
import type {DateTile} from "../../../../../../domain/session/room/timeline/tiles/DateTile";
|
||||
|
||||
export class DateHeaderView extends TemplateView<DateTile> {
|
||||
// ignore other argument
|
||||
constructor(vm) {
|
||||
super(vm);
|
||||
}
|
||||
|
||||
render(t, vm) {
|
||||
return t.div({className: "DateHeader"}, t.div(vm.date));
|
||||
}
|
||||
|
||||
/* This is called by the parent ListView, which just has 1 listener for the whole list */
|
||||
onClick() {}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user