2020-08-05 22:08:55 +05:30
|
|
|
/*
|
|
|
|
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
2021-05-21 20:29:29 +05:30
|
|
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
2020-08-05 22:08:55 +05:30
|
|
|
|
|
|
|
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.
|
|
|
|
*/
|
|
|
|
|
2021-06-10 21:59:10 +05:30
|
|
|
import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable/index.js";
|
2021-11-16 14:13:35 +05:30
|
|
|
import {Disposables} from "../../../utils/Disposables";
|
2021-08-18 21:51:43 +05:30
|
|
|
import {Direction} from "./Direction";
|
2020-04-21 00:56:39 +05:30
|
|
|
import {TimelineReader} from "./persistence/TimelineReader.js";
|
|
|
|
import {PendingEventEntry} from "./entries/PendingEventEntry.js";
|
2021-03-03 19:23:22 +05:30
|
|
|
import {RoomMember} from "../members/RoomMember.js";
|
2021-06-16 16:16:44 +05:30
|
|
|
import {getRelation, ANNOTATION_RELATION_TYPE} from "./relations.js";
|
2021-06-09 20:22:30 +05:30
|
|
|
import {REDACTION_TYPE} from "../common.js";
|
2021-12-12 20:57:41 +05:30
|
|
|
import {NonPersistedEventEntry} from "./entries/NonPersistedEventEntry.js";
|
|
|
|
import {DecryptionSource} from "../../e2ee/common.js";
|
2021-12-12 21:01:56 +05:30
|
|
|
import {EVENT_TYPE as MEMBER_EVENT_TYPE} from "../members/RoomMember.js";
|
2019-02-28 03:20:08 +05:30
|
|
|
|
2020-04-21 00:56:39 +05:30
|
|
|
export class Timeline {
|
2021-12-12 20:57:41 +05:30
|
|
|
constructor({roomId, storage, closeCallback, fragmentIdComparer, pendingEvents, clock, powerLevelsObservable, hsApi}) {
|
2019-02-28 03:20:08 +05:30
|
|
|
this._roomId = roomId;
|
|
|
|
this._storage = storage;
|
|
|
|
this._closeCallback = closeCallback;
|
2019-05-12 23:56:03 +05:30
|
|
|
this._fragmentIdComparer = fragmentIdComparer;
|
2020-09-10 20:10:30 +05:30
|
|
|
this._disposables = new Disposables();
|
2021-05-21 20:29:29 +05:30
|
|
|
this._pendingEvents = pendingEvents;
|
|
|
|
this._clock = clock;
|
2021-06-04 19:58:08 +05:30
|
|
|
// constructing this early avoid some problem while sync and openTimeline race
|
|
|
|
this._remoteEntries = new SortedArray((a, b) => a.compare(b));
|
2021-03-03 19:23:22 +05:30
|
|
|
this._ownMember = null;
|
2019-05-20 00:19:46 +05:30
|
|
|
this._timelineReader = new TimelineReader({
|
|
|
|
roomId: this._roomId,
|
|
|
|
storage: this._storage,
|
|
|
|
fragmentIdComparer: this._fragmentIdComparer
|
|
|
|
});
|
2020-09-10 20:10:30 +05:30
|
|
|
this._readerRequest = null;
|
2021-05-21 20:29:29 +05:30
|
|
|
this._allEntries = null;
|
2021-12-13 15:04:55 +05:30
|
|
|
/** Stores event entries that we had to fetch from hs/storage for reply previews (because they were not in timeline) */
|
|
|
|
this._contextEntriesNotInTimeline = new Map();
|
2022-01-06 15:37:58 +05:30
|
|
|
/** Only used to decrypt non-persisted context entries fetched from the homeserver */
|
2021-12-12 20:57:41 +05:30
|
|
|
this._decryptEntries = null;
|
|
|
|
this._hsApi = hsApi;
|
2021-07-14 15:06:39 +05:30
|
|
|
this.initializePowerLevels(powerLevelsObservable);
|
|
|
|
}
|
|
|
|
|
|
|
|
initializePowerLevels(observable) {
|
|
|
|
if (observable) {
|
|
|
|
this._powerLevels = observable.get();
|
|
|
|
this._disposables.track(observable.subscribe(powerLevels => this._powerLevels = powerLevels));
|
|
|
|
}
|
2019-02-28 03:20:08 +05:30
|
|
|
}
|
|
|
|
|
|
|
|
/** @package */
|
2021-04-08 20:00:46 +05:30
|
|
|
async load(user, membership, log) {
|
2021-05-31 17:22:03 +05:30
|
|
|
const txn = await this._storage.readTxn(this._timelineReader.readTxnStores.concat(
|
|
|
|
this._storage.storeNames.roomMembers,
|
|
|
|
this._storage.storeNames.roomState
|
|
|
|
));
|
2021-03-03 19:23:22 +05:30
|
|
|
const memberData = await txn.roomMembers.get(this._roomId, user.id);
|
2021-04-08 20:00:46 +05:30
|
|
|
if (memberData) {
|
|
|
|
this._ownMember = new RoomMember(memberData);
|
|
|
|
} else {
|
|
|
|
// this should never happen, as our own join into the room would have
|
|
|
|
// made us receive our own member event, but just to be on the safe side and not crash,
|
|
|
|
// fall back to bare user id
|
|
|
|
this._ownMember = RoomMember.fromUserId(this._roomId, user.id, membership);
|
|
|
|
}
|
2021-03-03 19:23:22 +05:30
|
|
|
// it should be fine to not update the local entries,
|
|
|
|
// as they should only populate once the view subscribes to it
|
|
|
|
// if they are populated already, the sender profile would be empty
|
|
|
|
|
2021-09-10 18:17:05 +05:30
|
|
|
// choose good amount here between showing messages initially and
|
|
|
|
// not spending too much time decrypting messages before showing the timeline.
|
|
|
|
// more messages should be loaded automatically until the viewport is full by the view if needed.
|
2021-09-16 20:04:13 +05:30
|
|
|
const readerRequest = this._disposables.track(this._timelineReader.readFromEnd(20, txn, log));
|
2020-09-10 20:10:30 +05:30
|
|
|
try {
|
|
|
|
const entries = await readerRequest.complete();
|
2022-01-06 15:14:13 +05:30
|
|
|
this._loadContextEntriesWhereNeeded(entries);
|
2021-05-21 20:29:29 +05:30
|
|
|
this._setupEntries(entries);
|
2020-09-10 20:10:30 +05:30
|
|
|
} finally {
|
|
|
|
this._disposables.disposeTracked(readerRequest);
|
|
|
|
}
|
2021-06-02 18:36:30 +05:30
|
|
|
// txn should be assumed to have finished here, as decryption will close it.
|
2021-05-31 17:22:03 +05:30
|
|
|
}
|
|
|
|
|
2021-05-21 20:29:29 +05:30
|
|
|
_setupEntries(timelineEntries) {
|
|
|
|
this._remoteEntries.setManySorted(timelineEntries);
|
|
|
|
if (this._pendingEvents) {
|
2021-06-10 21:59:10 +05:30
|
|
|
this._localEntries = new AsyncMappedList(this._pendingEvents,
|
|
|
|
pe => this._mapPendingEventToEntry(pe),
|
|
|
|
(pee, params) => {
|
|
|
|
// is sending but redacted, who do we detect that here to remove the relation?
|
|
|
|
pee.notifyUpdate(params);
|
|
|
|
},
|
|
|
|
pee => this._applyAndEmitLocalRelationChange(pee, target => target.removeLocalRelation(pee))
|
|
|
|
);
|
2021-05-21 20:29:29 +05:30
|
|
|
} else {
|
|
|
|
this._localEntries = new ObservableArray();
|
|
|
|
}
|
|
|
|
this._allEntries = new ConcatList(this._remoteEntries, this._localEntries);
|
|
|
|
}
|
|
|
|
|
2021-06-10 21:59:10 +05:30
|
|
|
async _mapPendingEventToEntry(pe) {
|
2021-06-15 22:36:41 +05:30
|
|
|
// we load the redaction target for pending events,
|
2021-06-10 21:59:10 +05:30
|
|
|
// so if we are redacting a relation, we can pass the redaction
|
|
|
|
// to the relation target and the removal of the relation can
|
|
|
|
// be taken into account for local echo.
|
2021-06-16 16:16:44 +05:30
|
|
|
let redactingEntry;
|
2021-06-15 22:36:41 +05:30
|
|
|
if (pe.eventType === REDACTION_TYPE) {
|
2021-06-16 16:16:44 +05:30
|
|
|
redactingEntry = await this._getOrLoadEntry(pe.relatedTxnId, pe.relatedEventId);
|
2021-06-09 20:22:30 +05:30
|
|
|
}
|
2021-06-15 22:36:41 +05:30
|
|
|
const pee = new PendingEventEntry({
|
|
|
|
pendingEvent: pe, member: this._ownMember,
|
2021-06-16 16:16:44 +05:30
|
|
|
clock: this._clock, redactingEntry
|
2021-06-15 22:36:41 +05:30
|
|
|
});
|
2022-01-11 20:58:27 +05:30
|
|
|
this._loadContextEntriesWhereNeeded([pee]);
|
2022-01-12 19:20:32 +05:30
|
|
|
this._applyAndEmitLocalRelationChange(pee, target => target.addLocalRelation(pee));
|
2021-06-10 21:59:10 +05:30
|
|
|
return pee;
|
2021-06-09 20:22:30 +05:30
|
|
|
}
|
|
|
|
|
2021-06-10 21:59:10 +05:30
|
|
|
_applyAndEmitLocalRelationChange(pee, updater) {
|
2021-06-16 16:16:44 +05:30
|
|
|
// this is the contract of findAndUpdate, used in _findAndUpdateRelatedEntry
|
2021-05-31 14:17:32 +05:30
|
|
|
const updateOrFalse = e => {
|
|
|
|
const params = updater(e);
|
|
|
|
return params ? params : false;
|
|
|
|
};
|
2021-12-12 21:12:50 +05:30
|
|
|
this._findAndUpdateEntryById(pee.pendingEvent.relatedTxnId, pee.relatedEventId, updateOrFalse);
|
2021-06-16 16:16:44 +05:30
|
|
|
// also look for a relation target to update with this redaction
|
2021-06-17 13:33:32 +05:30
|
|
|
if (pee.redactingEntry) {
|
2021-06-16 16:16:44 +05:30
|
|
|
// redactingEntry might be a PendingEventEntry or an EventEntry, so don't assume pendingEvent
|
2021-06-17 13:33:32 +05:30
|
|
|
const relatedTxnId = pee.redactingEntry.pendingEvent?.relatedTxnId;
|
2021-12-12 21:12:50 +05:30
|
|
|
this._findAndUpdateEntryById(relatedTxnId, pee.redactingEntry.relatedEventId, updateOrFalse);
|
2022-01-13 19:20:37 +05:30
|
|
|
pee.redactingEntry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e, "contextEntry"));
|
2021-06-16 16:16:44 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-12 21:12:50 +05:30
|
|
|
_findAndUpdateEntryById(txnId, eventId, updateOrFalse) {
|
2021-06-10 21:59:10 +05:30
|
|
|
let found = false;
|
2021-05-31 14:17:32 +05:30
|
|
|
// first, look in local entries based on txn id
|
2021-12-12 21:12:50 +05:30
|
|
|
if (txnId) {
|
2021-06-10 21:59:10 +05:30
|
|
|
found = this._localEntries.findAndUpdate(
|
2021-12-12 21:12:50 +05:30
|
|
|
e => e.id === txnId,
|
2021-06-02 22:13:16 +05:30
|
|
|
updateOrFalse,
|
|
|
|
);
|
|
|
|
}
|
2021-06-16 13:58:17 +05:30
|
|
|
// if not found here, look in remote entries based on event id
|
2021-12-12 21:12:50 +05:30
|
|
|
if (!found && eventId) {
|
2021-06-11 15:00:11 +05:30
|
|
|
this._remoteEntries.findAndUpdate(
|
2021-12-12 21:12:50 +05:30
|
|
|
e => e.id === eventId,
|
2021-05-31 14:17:32 +05:30
|
|
|
updateOrFalse
|
|
|
|
);
|
2021-05-21 20:29:29 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-06-08 20:26:17 +05:30
|
|
|
async getOwnAnnotationEntry(targetId, key) {
|
|
|
|
const txn = await this._storage.readWriteTxn([
|
|
|
|
this._storage.storeNames.timelineEvents,
|
|
|
|
this._storage.storeNames.timelineRelations,
|
|
|
|
]);
|
|
|
|
const relations = await txn.timelineRelations.getForTargetAndType(this._roomId, targetId, ANNOTATION_RELATION_TYPE);
|
|
|
|
for (const relation of relations) {
|
|
|
|
const annotation = await txn.timelineEvents.getByEventId(this._roomId, relation.sourceEventId);
|
2021-06-18 15:21:02 +05:30
|
|
|
if (annotation && annotation.event.sender === this._ownMember.userId && getRelation(annotation.event).key === key) {
|
2021-06-08 20:26:17 +05:30
|
|
|
const eventEntry = new EventEntry(annotation, this._fragmentIdComparer);
|
|
|
|
this._addLocalRelationsToNewRemoteEntries([eventEntry]);
|
|
|
|
return eventEntry;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2021-06-23 21:08:52 +05:30
|
|
|
/** @package */
|
2021-03-03 19:23:22 +05:30
|
|
|
updateOwnMember(member) {
|
|
|
|
this._ownMember = member;
|
|
|
|
}
|
|
|
|
|
2021-05-31 19:54:42 +05:30
|
|
|
_addLocalRelationsToNewRemoteEntries(entries) {
|
2021-06-02 22:12:46 +05:30
|
|
|
// because it is not safe to iterate a derived observable collection
|
|
|
|
// before it has any subscriptions, we bail out if this isn't
|
|
|
|
// the case yet. This can happen when sync adds or replaces entries
|
|
|
|
// before load has finished and the view has subscribed to the timeline.
|
|
|
|
//
|
|
|
|
// Once the subscription is setup, MappedList will set up the local
|
|
|
|
// relations as needed with _applyAndEmitLocalRelationChange,
|
|
|
|
// so we're not missing anything by bailing out.
|
2021-06-04 19:35:28 +05:30
|
|
|
//
|
|
|
|
// _localEntries can also not yet exist
|
|
|
|
if (!this._localEntries?.hasSubscriptions) {
|
2021-06-02 22:12:46 +05:30
|
|
|
return;
|
|
|
|
}
|
2021-05-21 20:29:29 +05:30
|
|
|
// find any local relations to this new remote event
|
|
|
|
for (const pee of this._localEntries) {
|
|
|
|
// this will work because we set relatedEventId when removing remote echos
|
|
|
|
if (pee.relatedEventId) {
|
2021-05-31 19:54:42 +05:30
|
|
|
const relationTarget = entries.find(e => e.id === pee.relatedEventId);
|
2021-06-17 13:42:45 +05:30
|
|
|
// no need to emit here as this entry is about to be added
|
2022-01-12 19:20:32 +05:30
|
|
|
relationTarget?.addLocalRelation(pee);
|
2021-06-10 21:59:10 +05:30
|
|
|
}
|
2021-06-16 16:16:44 +05:30
|
|
|
if (pee.redactingEntry) {
|
|
|
|
const eventId = pee.redactingEntry.relatedEventId;
|
2021-06-10 21:59:10 +05:30
|
|
|
const relationTarget = entries.find(e => e.id === eventId);
|
2022-01-12 19:20:32 +05:30
|
|
|
relationTarget?.addLocalRelation(pee);
|
2021-05-21 20:29:29 +05:30
|
|
|
}
|
|
|
|
}
|
2021-05-31 19:54:42 +05:30
|
|
|
}
|
|
|
|
|
2021-06-23 21:08:52 +05:30
|
|
|
// used in replaceEntries
|
|
|
|
static _entryUpdater(existingEntry, entry) {
|
2021-12-13 14:38:10 +05:30
|
|
|
// ensure other entries for which this existingEntry is a context point to the new entry instead of existingEntry
|
|
|
|
existingEntry.contextForEntries?.forEach(event => event.setContextEntry(entry));
|
2021-06-23 21:08:52 +05:30
|
|
|
entry.updateFrom(existingEntry);
|
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @package */
|
2022-01-06 17:20:32 +05:30
|
|
|
replaceEntries(entries) {
|
2021-06-23 21:08:52 +05:30
|
|
|
this._addLocalRelationsToNewRemoteEntries(entries);
|
|
|
|
for (const entry of entries) {
|
2021-08-04 18:53:03 +05:30
|
|
|
try {
|
|
|
|
this._remoteEntries.getAndUpdate(entry, Timeline._entryUpdater);
|
2022-01-07 17:11:42 +05:30
|
|
|
const oldEntry = this._contextEntriesNotInTimeline.get(entry.id)
|
|
|
|
if (oldEntry) {
|
|
|
|
Timeline._entryUpdater(oldEntry, entry);
|
2022-01-07 17:29:17 +05:30
|
|
|
this._contextEntriesNotInTimeline.set(entry.id, entry);
|
2022-01-07 17:11:42 +05:30
|
|
|
}
|
2021-12-08 17:14:17 +05:30
|
|
|
// Since this entry changed, all dependent entries should be updated
|
2022-01-13 19:20:37 +05:30
|
|
|
entry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e, "contextEntry"));
|
2021-08-04 18:53:03 +05:30
|
|
|
} catch (err) {
|
|
|
|
if (err.name === "CompareError") {
|
|
|
|
// see FragmentIdComparer, if the replacing entry is on a fragment
|
|
|
|
// that is currently not loaded into the FragmentIdComparer, it will
|
|
|
|
// throw a CompareError, and it means that the event is not loaded
|
|
|
|
// in the timeline (like when receiving a relation for an event
|
|
|
|
// that is not loaded in memory) so we can just drop this error as
|
|
|
|
// replacing an event that is not already loaded is a no-op.
|
|
|
|
continue;
|
|
|
|
} else {
|
|
|
|
// don't swallow other errors
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
2021-06-23 21:08:52 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-05-31 19:54:42 +05:30
|
|
|
/** @package */
|
2021-12-07 20:26:54 +05:30
|
|
|
addEntries(newEntries) {
|
2021-05-31 19:54:42 +05:30
|
|
|
this._addLocalRelationsToNewRemoteEntries(newEntries);
|
2022-01-07 19:38:57 +05:30
|
|
|
this._updateEntriesFetchedFromHomeserver(newEntries);
|
2021-12-15 20:42:38 +05:30
|
|
|
this._moveEntryToRemoteEntries(newEntries);
|
2022-01-06 15:14:13 +05:30
|
|
|
this._loadContextEntriesWhereNeeded(newEntries);
|
2022-01-14 22:46:52 +05:30
|
|
|
this._remoteEntries.setManySorted(newEntries);
|
2019-03-09 05:11:06 +05:30
|
|
|
}
|
2021-12-06 11:36:45 +05:30
|
|
|
|
2021-12-13 15:04:55 +05:30
|
|
|
/**
|
2021-12-13 21:27:17 +05:30
|
|
|
* Update entries based on newly received events.
|
2022-01-11 13:28:35 +05:30
|
|
|
* This is specific to events that are not in the timeline but had to be fetched from the homeserver
|
|
|
|
* because they are context-events for other events in the timeline (i.e fetched from hs so that we
|
|
|
|
* can render things like reply previews)
|
2021-12-13 15:04:55 +05:30
|
|
|
*/
|
2022-01-07 19:38:57 +05:30
|
|
|
_updateEntriesFetchedFromHomeserver(entries) {
|
2022-01-10 12:58:45 +05:30
|
|
|
/**
|
|
|
|
* Updates for entries in timeline is handled by remoteEntries observable collection
|
|
|
|
* Updates for entries not in timeline but fetched from storage is handled in this.replaceEntries()
|
|
|
|
* This code is specific to entries fetched from HomeServer i.e NonPersistedEventEntry
|
|
|
|
*/
|
2021-12-09 13:48:55 +05:30
|
|
|
for (const entry of entries) {
|
2021-12-15 17:47:14 +05:30
|
|
|
const relatedEntry = this._contextEntriesNotInTimeline.get(entry.relatedEventId);
|
2022-01-12 19:20:32 +05:30
|
|
|
if (relatedEntry?.isNonPersisted && relatedEntry?.addLocalRelation(entry)) {
|
2021-12-21 13:25:30 +05:30
|
|
|
// update other entries for which this entry is a context entry
|
2022-01-14 16:14:42 +05:30
|
|
|
relatedEntry.contextForEntries?.forEach(e => this._emitUpdateForEntry(e, "contextEntry"));
|
2021-12-15 17:47:14 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-12-13 15:04:55 +05:30
|
|
|
/**
|
|
|
|
* If an event we had to fetch from hs/storage is now in the timeline (for eg, due to gap fill),
|
|
|
|
* remove the event from _contextEntriesNotInTimeline since it is now in remoteEntries
|
|
|
|
*/
|
2021-12-15 20:42:38 +05:30
|
|
|
_moveEntryToRemoteEntries(entries) {
|
2021-12-13 12:45:17 +05:30
|
|
|
for (const entry of entries) {
|
2021-12-13 15:04:55 +05:30
|
|
|
const fetchedEntry = this._contextEntriesNotInTimeline.get(entry.id);
|
2021-12-13 12:45:17 +05:30
|
|
|
if (fetchedEntry) {
|
2022-01-11 13:10:40 +05:30
|
|
|
fetchedEntry.contextForEntries.forEach(e => {
|
|
|
|
e.setContextEntry(entry);
|
2022-01-13 19:20:37 +05:30
|
|
|
this._emitUpdateForEntry(e, "contextEntry");
|
2022-01-11 13:10:40 +05:30
|
|
|
});
|
2021-12-15 11:42:37 +05:30
|
|
|
this._contextEntriesNotInTimeline.delete(entry.id);
|
2021-12-13 12:45:17 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-13 19:20:37 +05:30
|
|
|
_emitUpdateForEntry(entry, param) {
|
2021-12-12 21:12:50 +05:30
|
|
|
const txnId = entry.isPending ? entry.id : null;
|
|
|
|
const eventId = entry.isPending ? null : entry.id;
|
2022-01-13 19:20:37 +05:30
|
|
|
this._findAndUpdateEntryById(txnId, eventId, () => param);
|
2021-12-12 21:12:50 +05:30
|
|
|
}
|
|
|
|
|
2022-01-06 15:14:13 +05:30
|
|
|
/**
|
|
|
|
* For each entry in entries, this method associates a context-entry (if needed) to it.
|
|
|
|
* The context-entry is fetched using the following strategies (in the same order as given):
|
|
|
|
* - timeline
|
|
|
|
* - storage
|
|
|
|
* - homeserver
|
|
|
|
* @param {EventEntry[]} entries
|
|
|
|
*/
|
|
|
|
async _loadContextEntriesWhereNeeded(entries) {
|
2022-01-11 13:14:13 +05:30
|
|
|
for (const entry of entries) {
|
|
|
|
if (!entry.contextEventId) {
|
|
|
|
continue;
|
|
|
|
}
|
2021-12-07 13:00:10 +05:30
|
|
|
const id = entry.contextEventId;
|
2022-01-14 22:46:52 +05:30
|
|
|
// before looking into remoteEntries, check the entries
|
|
|
|
// that about to be added first
|
|
|
|
let contextEvent = entries.find(e => e.id === id);
|
2022-01-14 23:35:30 +05:30
|
|
|
if (!contextEvent) {
|
|
|
|
contextEvent = this._findLoadedEventById(id);
|
|
|
|
}
|
2021-12-07 12:25:56 +05:30
|
|
|
if (contextEvent) {
|
|
|
|
entry.setContextEntry(contextEvent);
|
2022-01-14 22:46:52 +05:30
|
|
|
// we don't emit an update here, as the add or update
|
|
|
|
// that the callee will emit hasn't been emitted yet.
|
|
|
|
} else {
|
|
|
|
// we don't await here, which is not ideal,
|
|
|
|
// but one of our callers, addEntries, is not async
|
|
|
|
// so there is not much point.
|
|
|
|
// Also, we want to run the entry fetching in parallel.
|
|
|
|
this._loadContextEntryNotInTimeline(entry);
|
2021-12-06 11:36:45 +05:30
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2021-12-12 18:52:45 +05:30
|
|
|
|
2022-01-14 22:46:52 +05:30
|
|
|
async _loadContextEntryNotInTimeline(entry) {
|
|
|
|
const id = entry.contextEventId;
|
|
|
|
let contextEvent = await this._getEventFromStorage(id);
|
|
|
|
if (!contextEvent) {
|
|
|
|
contextEvent = await this._getEventFromHomeserver(id);
|
|
|
|
}
|
|
|
|
if (contextEvent) {
|
|
|
|
// this entry was created from storage/hs, so it's not tracked by remoteEntries
|
|
|
|
// we track them here so that we can update reply previews later
|
|
|
|
this._contextEntriesNotInTimeline.set(id, contextEvent);
|
|
|
|
entry.setContextEntry(contextEvent);
|
|
|
|
// here, we awaited something, so from now on we do have to emit
|
|
|
|
// an update if we set the context entry.
|
|
|
|
this._emitUpdateForEntry(entry, "contextEntry");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-06 15:02:44 +05:30
|
|
|
/**
|
2022-01-11 14:57:22 +05:30
|
|
|
* Fetches an entry with the given event-id from localEntries, remoteEntries or contextEntriesNotInTimeline.
|
2022-01-06 15:02:44 +05:30
|
|
|
* @param {string} eventId event-id of the entry
|
|
|
|
* @returns entry if found, undefined otherwise
|
|
|
|
*/
|
2022-01-11 13:20:42 +05:30
|
|
|
_findLoadedEventById(eventId) {
|
2022-01-14 16:14:06 +05:30
|
|
|
return this.getByEventId(eventId) ?? this._contextEntriesNotInTimeline.get(eventId);
|
2021-12-12 18:52:45 +05:30
|
|
|
}
|
2021-12-12 20:57:41 +05:30
|
|
|
|
2021-12-13 12:52:03 +05:30
|
|
|
async _getEventFromStorage(eventId) {
|
|
|
|
const entry = await this._timelineReader.readById(eventId);
|
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
|
2021-12-12 20:57:41 +05:30
|
|
|
async _getEventFromHomeserver(eventId) {
|
|
|
|
const response = await this._hsApi.context(this._roomId, eventId, 0).response();
|
|
|
|
const sender = response.event.sender;
|
2021-12-12 21:01:56 +05:30
|
|
|
const member = response.state.find(e => e.type === MEMBER_EVENT_TYPE && e.user_id === sender);
|
2021-12-12 20:57:41 +05:30
|
|
|
const entry = {
|
|
|
|
event: response.event,
|
|
|
|
displayName: member.content.displayname,
|
|
|
|
avatarUrl: member.content.avatar_url
|
|
|
|
};
|
|
|
|
const eventEntry = new NonPersistedEventEntry(entry, this._fragmentIdComparer);
|
|
|
|
if (this._decryptEntries) {
|
|
|
|
const request = this._decryptEntries(DecryptionSource.Timeline, [eventEntry]);
|
|
|
|
await request.complete();
|
|
|
|
}
|
|
|
|
return eventEntry;
|
|
|
|
}
|
2021-12-06 11:36:45 +05:30
|
|
|
|
2019-06-01 21:09:23 +05:30
|
|
|
// tries to prepend `amount` entries to the `entries` list.
|
2021-03-02 23:59:55 +05:30
|
|
|
/**
|
|
|
|
* [loadAtTop description]
|
|
|
|
* @param {[type]} amount [description]
|
|
|
|
* @return {boolean} true if the top of the timeline has been reached
|
|
|
|
*
|
|
|
|
*/
|
2019-03-09 05:11:06 +05:30
|
|
|
async loadAtTop(amount) {
|
2020-10-13 16:40:35 +05:30
|
|
|
if (this._disposables.isDisposed) {
|
2021-03-02 23:59:55 +05:30
|
|
|
return true;
|
2020-10-13 16:40:35 +05:30
|
|
|
}
|
2019-07-29 23:28:35 +05:30
|
|
|
const firstEventEntry = this._remoteEntries.array.find(e => !!e.eventType);
|
2019-06-16 20:59:33 +05:30
|
|
|
if (!firstEventEntry) {
|
2021-03-02 23:59:55 +05:30
|
|
|
return true;
|
2019-03-09 05:11:06 +05:30
|
|
|
}
|
2020-09-10 20:10:30 +05:30
|
|
|
const readerRequest = this._disposables.track(this._timelineReader.readFrom(
|
2019-06-16 20:59:33 +05:30
|
|
|
firstEventEntry.asEventKey(),
|
2019-06-02 18:45:26 +05:30
|
|
|
Direction.Backward,
|
|
|
|
amount
|
2020-09-10 20:10:30 +05:30
|
|
|
));
|
|
|
|
try {
|
|
|
|
const entries = await readerRequest.complete();
|
2021-06-23 21:08:52 +05:30
|
|
|
this.addEntries(entries);
|
2021-03-02 23:59:55 +05:30
|
|
|
return entries.length < amount;
|
2020-09-10 20:10:30 +05:30
|
|
|
} finally {
|
|
|
|
this._disposables.disposeTracked(readerRequest);
|
|
|
|
}
|
2019-03-09 00:33:18 +05:30
|
|
|
}
|
|
|
|
|
2021-06-16 16:16:44 +05:30
|
|
|
async _getOrLoadEntry(txnId, eventId) {
|
|
|
|
if (txnId) {
|
|
|
|
// also look for redacting relation in pending events, in case the target is already being sent
|
|
|
|
for (const p of this._localEntries) {
|
|
|
|
if (p.id === txnId) {
|
|
|
|
return p;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (eventId) {
|
2021-12-13 12:52:03 +05:30
|
|
|
return this.getByEventId(eventId) ?? await this._getEventFromStorage(eventId);
|
2021-06-16 16:16:44 +05:30
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2020-10-30 19:49:51 +05:30
|
|
|
getByEventId(eventId) {
|
|
|
|
for (let i = 0; i < this._remoteEntries.length; i += 1) {
|
|
|
|
const entry = this._remoteEntries.get(i);
|
|
|
|
if (entry.id === eventId) {
|
|
|
|
return entry;
|
|
|
|
}
|
|
|
|
}
|
2021-05-21 20:29:29 +05:30
|
|
|
return null;
|
2020-10-30 19:49:51 +05:30
|
|
|
}
|
|
|
|
|
2019-02-28 03:20:08 +05:30
|
|
|
/** @public */
|
|
|
|
get entries() {
|
2019-07-29 23:23:58 +05:30
|
|
|
return this._allEntries;
|
2019-02-28 03:20:08 +05:30
|
|
|
}
|
|
|
|
|
2021-03-02 03:00:33 +05:30
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
* @return {Array<EventEntry>} remote event entries, should not be modified
|
|
|
|
*/
|
|
|
|
get remoteEntries() {
|
|
|
|
return this._remoteEntries.array;
|
|
|
|
}
|
|
|
|
|
2019-02-28 03:20:08 +05:30
|
|
|
/** @public */
|
2020-09-10 21:13:01 +05:30
|
|
|
dispose() {
|
2020-05-05 01:51:56 +05:30
|
|
|
if (this._closeCallback) {
|
2020-09-10 21:13:01 +05:30
|
|
|
this._disposables.dispose();
|
2020-05-05 01:51:56 +05:30
|
|
|
this._closeCallback();
|
|
|
|
this._closeCallback = null;
|
|
|
|
}
|
2019-02-28 03:20:08 +05:30
|
|
|
}
|
2020-09-04 18:58:22 +05:30
|
|
|
|
2021-05-31 17:22:03 +05:30
|
|
|
/** @internal */
|
2020-09-04 18:58:22 +05:30
|
|
|
enableEncryption(decryptEntries) {
|
2021-12-12 20:57:41 +05:30
|
|
|
this._decryptEntries = decryptEntries;
|
2020-09-04 18:58:22 +05:30
|
|
|
this._timelineReader.enableEncryption(decryptEntries);
|
|
|
|
}
|
2021-05-31 17:22:03 +05:30
|
|
|
|
|
|
|
get powerLevels() {
|
|
|
|
return this._powerLevels;
|
|
|
|
}
|
2021-05-31 18:48:44 +05:30
|
|
|
|
|
|
|
get me() {
|
|
|
|
return this._ownMember;
|
|
|
|
}
|
2019-02-28 03:20:08 +05:30
|
|
|
}
|
2021-06-02 22:11:03 +05:30
|
|
|
|
|
|
|
import {FragmentIdComparer} from "./FragmentIdComparer.js";
|
2021-06-10 21:59:10 +05:30
|
|
|
import {poll} from "../../../mocks/poll.js";
|
2021-06-02 22:11:03 +05:30
|
|
|
import {Clock as MockClock} from "../../../mocks/Clock.js";
|
2021-09-28 17:49:29 +05:30
|
|
|
import {createMockStorage} from "../../../mocks/Storage";
|
2021-06-18 18:09:54 +05:30
|
|
|
import {ListObserver} from "../../../mocks/ListObserver.js";
|
2022-01-14 18:15:26 +05:30
|
|
|
import {createEvent, withTextBody, withContent, withSender, withRedacts, withReply} from "../../../mocks/event.js";
|
2021-11-15 17:29:08 +05:30
|
|
|
import {NullLogItem} from "../../../logging/NullLogger";
|
2021-06-02 22:11:03 +05:30
|
|
|
import {EventEntry} from "./entries/EventEntry.js";
|
|
|
|
import {User} from "../../User.js";
|
|
|
|
import {PendingEvent} from "../sending/PendingEvent.js";
|
2021-06-18 15:21:02 +05:30
|
|
|
import {createAnnotation} from "./relations.js";
|
2022-01-14 23:35:53 +05:30
|
|
|
import {redactEvent} from "./common.js";
|
2021-06-02 22:11:03 +05:30
|
|
|
|
|
|
|
export function tests() {
|
|
|
|
const fragmentIdComparer = new FragmentIdComparer([]);
|
2021-06-18 15:21:02 +05:30
|
|
|
const roomId = "$abc";
|
|
|
|
const alice = "@alice:hs.tld";
|
|
|
|
const bob = "@bob:hs.tld";
|
2022-01-14 17:28:31 +05:30
|
|
|
const hsApi = {
|
|
|
|
context() {
|
|
|
|
const result = {
|
|
|
|
event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)),
|
|
|
|
state: [{
|
|
|
|
type: MEMBER_EVENT_TYPE,
|
|
|
|
user_id: alice,
|
|
|
|
content: {
|
|
|
|
displayName: "",
|
|
|
|
avatarUrl: ""
|
|
|
|
}
|
|
|
|
}]
|
|
|
|
};
|
|
|
|
return { response: () => result };
|
|
|
|
}
|
|
|
|
};
|
2021-06-18 15:21:02 +05:30
|
|
|
|
|
|
|
function getIndexFromIterable(it, n) {
|
|
|
|
let i = 0;
|
|
|
|
for (const item of it) {
|
|
|
|
if (i === n) {
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
i += 1;
|
|
|
|
}
|
|
|
|
throw new Error("not enough items in iterable");
|
|
|
|
}
|
|
|
|
|
2021-06-02 22:11:03 +05:30
|
|
|
return {
|
2021-06-17 19:37:32 +05:30
|
|
|
"adding or replacing entries before subscribing to entries does not lose local relations": async assert => {
|
2021-06-02 22:11:03 +05:30
|
|
|
const pendingEvents = new ObservableArray();
|
2021-06-18 18:09:54 +05:30
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(),
|
|
|
|
closeCallback: () => {}, fragmentIdComparer, pendingEvents, clock: new MockClock()});
|
2021-06-02 22:11:03 +05:30
|
|
|
// 1. load timeline
|
2021-06-18 15:21:02 +05:30
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2021-06-23 21:08:52 +05:30
|
|
|
// 2. test replaceEntries and addEntries don't fail
|
2021-06-18 15:21:02 +05:30
|
|
|
const event1 = withTextBody("hi!", withSender(bob, createEvent("m.room.message", "!abc")));
|
2021-06-02 22:11:03 +05:30
|
|
|
const entry1 = new EventEntry({event: event1, fragmentId: 1, eventIndex: 1}, fragmentIdComparer);
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.replaceEntries([entry1]);
|
2021-06-18 15:21:02 +05:30
|
|
|
const event2 = withTextBody("hi bob!", withSender(alice, createEvent("m.room.message", "!def")));
|
2021-06-02 22:11:03 +05:30
|
|
|
const entry2 = new EventEntry({event: event2, fragmentId: 1, eventIndex: 2}, fragmentIdComparer);
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([entry2]);
|
2021-06-02 22:11:03 +05:30
|
|
|
// 3. add local relation (redaction)
|
|
|
|
pendingEvents.append(new PendingEvent({data: {
|
|
|
|
roomId,
|
|
|
|
queueIndex: 1,
|
|
|
|
eventType: "m.room.redaction",
|
|
|
|
txnId: "t123",
|
|
|
|
content: {},
|
|
|
|
relatedEventId: event2.event_id
|
|
|
|
}}));
|
2021-06-10 21:59:10 +05:30
|
|
|
// 4. subscribe (it's now safe to iterate timeline.entries)
|
2021-06-18 18:09:54 +05:30
|
|
|
timeline.entries.subscribe(new ListObserver());
|
2021-06-02 22:11:03 +05:30
|
|
|
// 5. check the local relation got correctly aggregated
|
2021-06-10 21:59:10 +05:30
|
|
|
const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting);
|
|
|
|
assert.equal(locallyRedacted, true);
|
2021-06-18 15:21:02 +05:30
|
|
|
},
|
2021-06-18 18:09:54 +05:30
|
|
|
"add and remove local reaction, and cancel again": async assert => {
|
|
|
|
// 1. setup timeline with message
|
2021-06-18 15:21:02 +05:30
|
|
|
const pendingEvents = new ObservableArray();
|
2021-06-18 18:09:54 +05:30
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(),
|
|
|
|
closeCallback: () => {}, fragmentIdComparer, pendingEvents, clock: new MockClock()});
|
2021-06-18 15:21:02 +05:30
|
|
|
await timeline.load(new User(bob), "join", new NullLogItem());
|
2021-06-18 18:09:54 +05:30
|
|
|
timeline.entries.subscribe(new ListObserver());
|
2021-06-18 15:21:02 +05:30
|
|
|
const event = withTextBody("hi bob!", withSender(alice, createEvent("m.room.message", "!abc")));
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([new EventEntry({event, fragmentId: 1, eventIndex: 2}, fragmentIdComparer)]);
|
2021-06-18 15:21:02 +05:30
|
|
|
let entry = getIndexFromIterable(timeline.entries, 0);
|
2021-06-18 18:09:54 +05:30
|
|
|
// 2. add local reaction
|
2021-06-18 15:21:02 +05:30
|
|
|
pendingEvents.append(new PendingEvent({data: {
|
|
|
|
roomId,
|
|
|
|
queueIndex: 1,
|
|
|
|
eventType: "m.reaction",
|
|
|
|
txnId: "t123",
|
|
|
|
content: entry.annotate("👋"),
|
|
|
|
relatedEventId: entry.id
|
|
|
|
}}));
|
2021-06-18 18:09:54 +05:30
|
|
|
await poll(() => timeline.entries.length === 2);
|
2021-06-24 15:56:38 +05:30
|
|
|
assert.equal(entry.pendingAnnotations.get("👋").count, 1);
|
2021-06-18 18:09:54 +05:30
|
|
|
const reactionEntry = getIndexFromIterable(timeline.entries, 1);
|
|
|
|
// 3. add redaction to timeline
|
|
|
|
pendingEvents.append(new PendingEvent({data: {
|
|
|
|
roomId,
|
|
|
|
queueIndex: 2,
|
|
|
|
eventType: "m.room.redaction",
|
|
|
|
txnId: "t456",
|
|
|
|
content: {},
|
|
|
|
relatedTxnId: reactionEntry.id
|
|
|
|
}}));
|
2021-06-18 18:36:49 +05:30
|
|
|
// TODO: await nextUpdate here with ListObserver, to ensure entry emits an update when pendingAnnotations changes
|
2021-06-18 18:09:54 +05:30
|
|
|
await poll(() => timeline.entries.length === 3);
|
2021-06-24 15:56:38 +05:30
|
|
|
assert.equal(entry.pendingAnnotations.get("👋").count, 0);
|
2021-06-18 18:09:54 +05:30
|
|
|
// 4. cancel redaction
|
|
|
|
pendingEvents.remove(1);
|
|
|
|
await poll(() => timeline.entries.length === 2);
|
2021-06-24 15:56:38 +05:30
|
|
|
assert.equal(entry.pendingAnnotations.get("👋").count, 1);
|
2021-06-18 18:09:54 +05:30
|
|
|
// 5. cancel reaction
|
|
|
|
pendingEvents.remove(0);
|
|
|
|
await poll(() => timeline.entries.length === 1);
|
|
|
|
assert(!entry.pendingAnnotations);
|
2021-06-18 15:21:02 +05:30
|
|
|
},
|
2021-06-18 18:09:54 +05:30
|
|
|
"getOwnAnnotationEntry": async assert => {
|
|
|
|
const messageId = "!abc";
|
|
|
|
const reactionId = "!def";
|
2021-06-18 15:21:02 +05:30
|
|
|
// 1. put event and reaction into storage
|
|
|
|
const storage = await createMockStorage();
|
2021-06-18 18:09:54 +05:30
|
|
|
const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]);
|
2021-09-22 00:34:10 +05:30
|
|
|
txn.timelineEvents.tryInsert({
|
2021-06-18 18:09:54 +05:30
|
|
|
event: withContent(createAnnotation(messageId, "👋"), createEvent("m.reaction", reactionId, bob)),
|
|
|
|
fragmentId: 1, eventIndex: 1, roomId
|
2021-09-22 14:03:40 +05:30
|
|
|
}, new NullLogItem());
|
2021-06-18 18:09:54 +05:30
|
|
|
txn.timelineRelations.add(roomId, messageId, ANNOTATION_RELATION_TYPE, reactionId);
|
|
|
|
await txn.complete();
|
|
|
|
// 2. setup the timeline
|
|
|
|
const timeline = new Timeline({roomId, storage, closeCallback: () => {},
|
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()});
|
|
|
|
await timeline.load(new User(bob), "join", new NullLogItem());
|
|
|
|
// 3. get the own annotation out
|
|
|
|
const reactionEntry = await timeline.getOwnAnnotationEntry(messageId, "👋");
|
|
|
|
assert.equal(reactionEntry.id, reactionId);
|
|
|
|
assert.equal(reactionEntry.relation.key, "👋");
|
|
|
|
},
|
|
|
|
"remote reaction": async assert => {
|
|
|
|
const messageEntry = new EventEntry({
|
2021-06-18 15:21:02 +05:30
|
|
|
event: withTextBody("hi bob!", createEvent("m.room.message", "!abc", alice)),
|
|
|
|
fragmentId: 1, eventIndex: 2, roomId,
|
|
|
|
annotations: { // aggregated like RelationWriter would
|
|
|
|
"👋": {count: 1, me: true, firstTimestamp: 0}
|
|
|
|
},
|
2021-06-18 18:09:54 +05:30
|
|
|
}, fragmentIdComparer);
|
2021-06-18 15:21:02 +05:30
|
|
|
// 2. setup timeline
|
|
|
|
const pendingEvents = new ObservableArray();
|
2021-06-18 18:09:54 +05:30
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(),
|
|
|
|
closeCallback: () => {}, fragmentIdComparer, pendingEvents, clock: new MockClock()});
|
2021-06-18 15:21:02 +05:30
|
|
|
await timeline.load(new User(bob), "join", new NullLogItem());
|
2021-06-18 18:09:54 +05:30
|
|
|
timeline.entries.subscribe(new ListObserver());
|
2021-06-18 15:21:02 +05:30
|
|
|
// 3. add message to timeline
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([messageEntry]);
|
2021-06-18 15:21:02 +05:30
|
|
|
const entry = getIndexFromIterable(timeline.entries, 0);
|
|
|
|
assert.equal(entry, messageEntry);
|
|
|
|
assert.equal(entry.annotations["👋"].count, 1);
|
2021-06-18 18:09:54 +05:30
|
|
|
},
|
|
|
|
"remove remote reaction": async assert => {
|
|
|
|
// 1. setup timeline
|
|
|
|
const pendingEvents = new ObservableArray();
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(),
|
2022-01-14 16:54:16 +05:30
|
|
|
closeCallback: () => { }, fragmentIdComparer, pendingEvents, clock: new MockClock()});
|
2021-06-18 18:09:54 +05:30
|
|
|
await timeline.load(new User(bob), "join", new NullLogItem());
|
|
|
|
timeline.entries.subscribe(new ListObserver());
|
|
|
|
// 2. add message and reaction to timeline
|
|
|
|
const messageEntry = new EventEntry({
|
|
|
|
event: withTextBody("hi bob!", createEvent("m.room.message", "!abc", alice)),
|
|
|
|
fragmentId: 1, eventIndex: 2, roomId,
|
|
|
|
}, fragmentIdComparer);
|
|
|
|
const reactionEntry = new EventEntry({
|
|
|
|
event: withContent(createAnnotation(messageEntry.id, "👋"), createEvent("m.reaction", "!def", bob)),
|
|
|
|
fragmentId: 1, eventIndex: 3, roomId
|
|
|
|
}, fragmentIdComparer);
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([messageEntry, reactionEntry]);
|
2021-06-18 18:09:54 +05:30
|
|
|
// 3. redact reaction
|
2021-06-18 15:21:02 +05:30
|
|
|
pendingEvents.append(new PendingEvent({data: {
|
|
|
|
roomId,
|
|
|
|
queueIndex: 1,
|
|
|
|
eventType: "m.room.redaction",
|
|
|
|
txnId: "t123",
|
|
|
|
content: {},
|
|
|
|
relatedEventId: reactionEntry.id
|
|
|
|
}}));
|
2021-06-18 18:09:54 +05:30
|
|
|
await poll(() => timeline.entries.length >= 3);
|
2021-06-24 15:56:38 +05:30
|
|
|
assert.equal(messageEntry.pendingAnnotations.get("👋").count, -1);
|
2021-06-18 15:21:02 +05:30
|
|
|
},
|
2021-06-18 18:39:14 +05:30
|
|
|
"local reaction gets applied after remote echo is added to timeline": async assert => {
|
|
|
|
const messageEntry = new EventEntry({event: withTextBody("hi bob!", withSender(alice, createEvent("m.room.message", "!abc"))),
|
|
|
|
fragmentId: 1, eventIndex: 2}, fragmentIdComparer);
|
|
|
|
// 1. setup timeline
|
|
|
|
const pendingEvents = new ObservableArray();
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(),
|
|
|
|
closeCallback: () => {}, fragmentIdComparer, pendingEvents, clock: new MockClock()});
|
|
|
|
await timeline.load(new User(bob), "join", new NullLogItem());
|
|
|
|
timeline.entries.subscribe(new ListObserver());
|
|
|
|
// 2. add local reaction
|
|
|
|
pendingEvents.append(new PendingEvent({data: {
|
|
|
|
roomId,
|
|
|
|
queueIndex: 1,
|
|
|
|
eventType: "m.reaction",
|
|
|
|
txnId: "t123",
|
|
|
|
content: messageEntry.annotate("👋"),
|
|
|
|
relatedEventId: messageEntry.id
|
|
|
|
}}));
|
|
|
|
await poll(() => timeline.entries.length === 1);
|
|
|
|
// 3. add remote reaction target
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([messageEntry]);
|
2021-06-18 18:39:14 +05:30
|
|
|
await poll(() => timeline.entries.length === 2);
|
|
|
|
const entry = getIndexFromIterable(timeline.entries, 0);
|
|
|
|
assert.equal(entry, messageEntry);
|
2021-06-24 15:56:38 +05:30
|
|
|
assert.equal(entry.pendingAnnotations.get("👋").count, 1);
|
2021-06-18 18:39:14 +05:30
|
|
|
},
|
|
|
|
"local reaction removal gets applied after remote echo is added to timeline with reaction not loaded": async assert => {
|
|
|
|
const messageId = "!abc";
|
|
|
|
const reactionId = "!def";
|
|
|
|
// 1. put reaction in storage
|
|
|
|
const storage = await createMockStorage();
|
|
|
|
const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]);
|
2021-09-22 00:34:10 +05:30
|
|
|
txn.timelineEvents.tryInsert({
|
2021-06-18 18:39:14 +05:30
|
|
|
event: withContent(createAnnotation(messageId, "👋"), createEvent("m.reaction", reactionId, bob)),
|
|
|
|
fragmentId: 1, eventIndex: 3, roomId
|
2021-09-22 14:03:40 +05:30
|
|
|
}, new NullLogItem());
|
2021-06-18 18:39:14 +05:30
|
|
|
await txn.complete();
|
|
|
|
// 2. setup timeline
|
|
|
|
const pendingEvents = new ObservableArray();
|
|
|
|
const timeline = new Timeline({roomId, storage, closeCallback: () => {},
|
|
|
|
fragmentIdComparer, pendingEvents, clock: new MockClock()});
|
|
|
|
await timeline.load(new User(bob), "join", new NullLogItem());
|
|
|
|
timeline.entries.subscribe(new ListObserver());
|
|
|
|
// 3. add local redaction for reaction
|
|
|
|
pendingEvents.append(new PendingEvent({data: {
|
|
|
|
roomId,
|
|
|
|
queueIndex: 1,
|
|
|
|
eventType: "m.room.redaction",
|
|
|
|
txnId: "t123",
|
|
|
|
content: {},
|
|
|
|
relatedEventId: reactionId
|
|
|
|
}}));
|
|
|
|
await poll(() => timeline.entries.length === 1);
|
|
|
|
// 4. add reaction target
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([new EventEntry({
|
2021-06-18 18:39:14 +05:30
|
|
|
event: withTextBody("hi bob!", createEvent("m.room.message", messageId, alice)),
|
|
|
|
fragmentId: 1, eventIndex: 2}, fragmentIdComparer)
|
|
|
|
]);
|
|
|
|
await poll(() => timeline.entries.length === 2);
|
|
|
|
// 5. check that redaction was linked to reaction target
|
|
|
|
const entry = getIndexFromIterable(timeline.entries, 0);
|
2021-06-24 15:56:38 +05:30
|
|
|
assert.equal(entry.pendingAnnotations.get("👋").count, -1);
|
2021-06-18 18:39:14 +05:30
|
|
|
},
|
2021-06-23 21:08:52 +05:30
|
|
|
"decrypted entry preserves content when receiving other update without decryption": async assert => {
|
|
|
|
// 1. create encrypted and decrypted entry
|
|
|
|
const encryptedEntry = new EventEntry({
|
|
|
|
event: withContent({ciphertext: "abc"}, createEvent("m.room.encrypted", "!abc", alice)),
|
|
|
|
fragmentId: 1, eventIndex: 1, roomId
|
|
|
|
}, fragmentIdComparer);
|
|
|
|
const decryptedEntry = encryptedEntry.clone();
|
|
|
|
decryptedEntry.setDecryptionResult({
|
|
|
|
event: withTextBody("hi bob!", createEvent("m.room.message", encryptedEntry.id, encryptedEntry.sender))
|
|
|
|
});
|
|
|
|
// 2. setup the timeline
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {},
|
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()});
|
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([decryptedEntry]);
|
2021-06-23 21:08:52 +05:30
|
|
|
const observer = new ListObserver();
|
|
|
|
timeline.entries.subscribe(observer);
|
|
|
|
// 3. replace the entry with one that is not decrypted
|
|
|
|
// (as would happen when receiving a reaction,
|
|
|
|
// as it does not rerun the decryption)
|
|
|
|
// and check that the decrypted content is preserved
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.replaceEntries([encryptedEntry]);
|
2021-06-23 21:08:52 +05:30
|
|
|
const {value, type} = await observer.next();
|
|
|
|
assert.equal(type, "update");
|
|
|
|
assert.equal(value.eventType, "m.room.message");
|
|
|
|
assert.equal(value.content.body, "hi bob!");
|
2021-12-13 21:27:17 +05:30
|
|
|
},
|
|
|
|
|
|
|
|
"context entry is fetched from remoteEntries": async assert => {
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {},
|
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()});
|
|
|
|
const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)) });
|
2022-01-14 18:15:26 +05:30
|
|
|
const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
|
2021-12-13 21:27:17 +05:30
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2022-01-14 23:35:53 +05:30
|
|
|
timeline.entries.subscribe({
|
|
|
|
onAdd() {},
|
|
|
|
});
|
2022-01-13 19:14:20 +05:30
|
|
|
timeline.addEntries([entryA, entryB]);
|
2021-12-13 21:27:17 +05:30
|
|
|
assert.deepEqual(entryB.contextEntry, entryA);
|
|
|
|
},
|
|
|
|
|
|
|
|
"context entry is fetched from storage": async assert => {
|
2022-01-14 16:51:06 +05:30
|
|
|
const storage = await createMockStorage();
|
|
|
|
const txn = await storage.readWriteTxn([storage.storeNames.timelineEvents, storage.storeNames.timelineRelations]);
|
|
|
|
txn.timelineEvents.tryInsert({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), fragmentId: 1, eventIndex: 1, roomId });
|
|
|
|
await txn.complete();
|
|
|
|
const timeline = new Timeline({roomId, storage, closeCallback: () => {},
|
2021-12-13 21:27:17 +05:30
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()});
|
2022-01-14 18:15:26 +05:30
|
|
|
const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
|
2021-12-13 21:27:17 +05:30
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2022-01-13 19:14:20 +05:30
|
|
|
timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null });
|
|
|
|
timeline.addEntries([entryB]);
|
|
|
|
await poll(() => entryB.contextEntry);
|
2022-01-14 16:51:06 +05:30
|
|
|
assert.strictEqual(entryB.contextEntry.id, "event_id_1");
|
2021-12-13 21:27:17 +05:30
|
|
|
},
|
|
|
|
|
|
|
|
"context entry is fetched from hs": async assert => {
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {},
|
2022-01-14 17:07:06 +05:30
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi});
|
2022-01-14 18:15:26 +05:30
|
|
|
const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
|
2021-12-13 21:27:17 +05:30
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2022-01-13 19:14:20 +05:30
|
|
|
timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null });
|
|
|
|
timeline.addEntries([entryB]);
|
|
|
|
await poll(() => entryB.contextEntry);
|
2022-01-14 17:07:06 +05:30
|
|
|
assert.strictEqual(entryB.contextEntry.id, "event_id_1");
|
2021-12-13 21:27:17 +05:30
|
|
|
},
|
|
|
|
|
|
|
|
"context entry has a list of entries to which it forms the context": async assert => {
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {},
|
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock()});
|
2022-01-13 19:14:20 +05:30
|
|
|
const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 });
|
2022-01-14 18:15:26 +05:30
|
|
|
const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
|
|
|
|
const entryC = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3 });
|
2021-12-13 21:27:17 +05:30
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2022-01-13 19:14:20 +05:30
|
|
|
timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null });
|
|
|
|
timeline.addEntries([entryA, entryB, entryC]);
|
|
|
|
await poll(() => entryA.contextForEntries.length === 2);
|
2021-12-13 21:27:17 +05:30
|
|
|
assert.deepEqual(entryA.contextForEntries, [entryB, entryC]);
|
|
|
|
},
|
|
|
|
|
|
|
|
"context entry in contextEntryNotInTimeline gets updated based on incoming redaction": async assert => {
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {},
|
2022-01-14 17:28:31 +05:30
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi});
|
2022-01-14 18:15:26 +05:30
|
|
|
const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
|
2021-12-13 21:27:17 +05:30
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2022-01-13 19:14:20 +05:30
|
|
|
timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null });
|
|
|
|
timeline.addEntries([entryB]);
|
|
|
|
await poll(() => entryB.contextEntry);
|
2022-01-14 17:28:31 +05:30
|
|
|
const redactingEntry = new EventEntry({ event: withRedacts("event_id_1", "foo", createEvent("m.room.redaction", "event_id_3", alice)), eventIndex: 3 });
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([redactingEntry]);
|
2022-01-14 17:28:31 +05:30
|
|
|
assert.strictEqual(entryB.contextEntry.isRedacted, true);
|
2021-12-13 21:27:17 +05:30
|
|
|
},
|
|
|
|
|
|
|
|
"redaction of context entry triggers updates in other entries": async assert => {
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {},
|
2022-01-14 17:28:31 +05:30
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi});
|
2022-01-14 23:35:53 +05:30
|
|
|
const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1, fragmentId: 1 });
|
|
|
|
const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2, fragmentId: 1 });
|
|
|
|
const entryC = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_3", bob)), eventIndex: 3, fragmentId: 1 });
|
2021-12-13 21:27:17 +05:30
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2022-01-14 17:48:25 +05:30
|
|
|
const bin = [];
|
2021-12-13 21:27:17 +05:30
|
|
|
timeline.entries.subscribe({
|
2022-01-14 23:35:53 +05:30
|
|
|
onUpdate: (index, e) => {
|
2022-01-14 17:48:25 +05:30
|
|
|
bin.push(e.id);
|
2021-12-13 21:27:17 +05:30
|
|
|
},
|
2022-01-13 19:14:20 +05:30
|
|
|
onAdd: () => null,
|
2021-12-13 21:27:17 +05:30
|
|
|
});
|
2022-01-14 23:35:53 +05:30
|
|
|
timeline.addEntries([entryA, entryB, entryC]);
|
|
|
|
const eventAClone = JSON.parse(JSON.stringify(entryA.event));
|
|
|
|
redactEvent(withRedacts("event_id_1", "foo", createEvent("m.room.redaction", "event_id_4", alice)), eventAClone);
|
|
|
|
const redactedEntry = new EventEntry({ event: eventAClone, eventIndex: 1, fragmentId: 1 });
|
|
|
|
timeline.replaceEntries([redactedEntry]);
|
2022-01-14 17:48:25 +05:30
|
|
|
assert.strictEqual(bin.includes("event_id_2"), true);
|
|
|
|
assert.strictEqual(bin.includes("event_id_3"), true);
|
2021-12-21 12:39:52 +05:30
|
|
|
},
|
|
|
|
|
|
|
|
"context entries fetched from storage/hs are moved to remoteEntries": async assert => {
|
|
|
|
const timeline = new Timeline({roomId, storage: await createMockStorage(), closeCallback: () => {},
|
2022-01-14 17:28:31 +05:30
|
|
|
fragmentIdComparer, pendingEvents: new ObservableArray(), clock: new MockClock(), hsApi});
|
2022-01-13 19:14:20 +05:30
|
|
|
const entryA = new EventEntry({ event: withTextBody("foo", createEvent("m.room.message", "event_id_1", alice)), eventIndex: 1 });
|
2022-01-14 18:15:26 +05:30
|
|
|
const entryB = new EventEntry({ event: withReply("event_id_1", createEvent("m.room.message", "event_id_2", bob)), eventIndex: 2 });
|
2021-12-21 12:39:52 +05:30
|
|
|
await timeline.load(new User(alice), "join", new NullLogItem());
|
2022-01-13 19:14:20 +05:30
|
|
|
timeline.entries.subscribe({ onAdd: () => null, onUpdate: () => null });
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([entryB]);
|
2022-01-13 19:14:20 +05:30
|
|
|
await poll(() => entryB.contextEntry);
|
2021-12-21 12:39:52 +05:30
|
|
|
assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), true);
|
2021-12-21 13:41:14 +05:30
|
|
|
timeline.addEntries([entryA]);
|
2021-12-21 12:39:52 +05:30
|
|
|
assert.strictEqual(timeline._contextEntriesNotInTimeline.has(entryA.id), false);
|
|
|
|
const movedEntry = timeline.remoteEntries[0];
|
|
|
|
assert.deepEqual(movedEntry, entryA);
|
2022-01-14 17:28:31 +05:30
|
|
|
assert.deepEqual(movedEntry.contextForEntries[0], entryB);
|
2021-12-21 12:39:52 +05:30
|
|
|
assert.deepEqual(entryB.contextEntry, movedEntry);
|
2021-06-23 21:08:52 +05:30
|
|
|
}
|
2021-06-18 18:09:54 +05:30
|
|
|
};
|
2021-06-04 19:35:28 +05:30
|
|
|
}
|