forked from mystiq/hydrogen-web
WIP 4
This commit is contained in:
parent
cb051ad161
commit
757e08c62c
12 changed files with 374 additions and 178 deletions
|
@ -125,8 +125,7 @@ export class BaseMessageTile extends SimpleTile {
|
|||
|
||||
react(key, log = null) {
|
||||
return this.logger.wrapOrRun(log, "react", async log => {
|
||||
const existingAnnotation = await this._entry.getOwnAnnotationEntry(this._timeline, key);
|
||||
const redaction = existingAnnotation?.pendingRedaction;
|
||||
const redaction = this._entry.getAnnotationPendingRedaction(key);
|
||||
if (redaction && !redaction.pendingEvent.hasStartedSending) {
|
||||
log.set("abort_redaction", true);
|
||||
await redaction.pendingEvent.abort();
|
||||
|
@ -138,9 +137,16 @@ export class BaseMessageTile extends SimpleTile {
|
|||
|
||||
redactReaction(key, log = null) {
|
||||
return this.logger.wrapOrRun(log, "redactReaction", async log => {
|
||||
const redaction = this._entry.getAnnotationPendingRedaction(key);
|
||||
if (redaction) {
|
||||
log.set("already_redacting", true);
|
||||
return;
|
||||
}
|
||||
const entry = await this._entry.getOwnAnnotationEntry(this._timeline, key);
|
||||
if (entry) {
|
||||
await this._room.sendRedaction(entry.id, null, log);
|
||||
} else {
|
||||
log.set("no_reaction", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -452,23 +452,6 @@ export class BaseRoom extends EventEmitter {
|
|||
return observable;
|
||||
}
|
||||
|
||||
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.id, targetId, ANNOTATION_RELATION_TYPE);
|
||||
for (const relation of relations) {
|
||||
const annotation = await txn.timelineEvents.getByEventId(this.id, relation.sourceEventId);
|
||||
if (annotation.event.sender === this._user.id && getRelation(annotation.event).key === key) {
|
||||
const eventEntry = new EventEntry(annotation, this._fragmentIdComparer);
|
||||
// add local relations
|
||||
return eventEntry;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async _readEventById(eventId) {
|
||||
let stores = [this._storage.storeNames.timelineEvents];
|
||||
if (this.isEncrypted) {
|
||||
|
|
|
@ -19,35 +19,33 @@ import {getRelationFromContent} from "./relations.js";
|
|||
export class PendingAnnotations {
|
||||
constructor() {
|
||||
this.aggregatedAnnotations = new Map();
|
||||
// this contains both pending annotation entries, and pending redactions of remote annotation entries
|
||||
this._entries = [];
|
||||
}
|
||||
|
||||
/** adds either a pending annotation entry, or a remote annotation entry with a pending redaction */
|
||||
add(annotationEntry) {
|
||||
const relation = getRelationFromContent(annotationEntry.content);
|
||||
const key = relation.key;
|
||||
add(entry) {
|
||||
const {key} = entry.ownOrRedactedRelation;
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
const count = this.aggregatedAnnotations.get(key) || 0;
|
||||
const addend = annotationEntry.isRedacted ? -1 : 1;
|
||||
console.log("add", count, addend);
|
||||
const addend = entry.isRedaction ? -1 : 1;
|
||||
this.aggregatedAnnotations.set(key, count + addend);
|
||||
this._entries.push(annotationEntry);
|
||||
this._entries.push(entry);
|
||||
}
|
||||
|
||||
/** removes either a pending annotation entry, or a remote annotation entry with a pending redaction */
|
||||
remove(annotationEntry) {
|
||||
const idx = this._entries.indexOf(annotationEntry);
|
||||
remove(entry) {
|
||||
const idx = this._entries.indexOf(entry);
|
||||
if (idx === -1) {
|
||||
return;
|
||||
}
|
||||
this._entries.splice(idx, 1);
|
||||
const relation = getRelationFromContent(annotationEntry.content);
|
||||
const key = relation.key;
|
||||
const {key} = entry.ownOrRedactedRelation;
|
||||
let count = this.aggregatedAnnotations.get(key);
|
||||
if (count !== undefined) {
|
||||
const addend = annotationEntry.isRedacted ? 1 : -1;
|
||||
const addend = entry.isRedaction ? 1 : -1;
|
||||
count += addend;
|
||||
if (count <= 0) {
|
||||
this.aggregatedAnnotations.delete(key);
|
||||
|
@ -60,13 +58,22 @@ export class PendingAnnotations {
|
|||
findForKey(key) {
|
||||
return this._entries.find(e => {
|
||||
const relation = getRelationFromContent(e.content);
|
||||
if (relation.key === key) {
|
||||
if (relation && relation.key === key) {
|
||||
return e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
findRedactionForKey(key) {
|
||||
return this._entries.find(e => {
|
||||
const relation = e.redactingRelation;
|
||||
if (relation && relation.key === key) {
|
||||
return e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get isEmpty() {
|
||||
return this._entries.length;
|
||||
return this._entries.length === 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {SortedArray, MappedList, ConcatList, ObservableArray} from "../../../observable/index.js";
|
||||
import {SortedArray, AsyncMappedList, ConcatList, ObservableArray} from "../../../observable/index.js";
|
||||
import {Disposables} from "../../../utils/Disposables.js";
|
||||
import {Direction} from "./Direction.js";
|
||||
import {TimelineReader} from "./persistence/TimelineReader.js";
|
||||
|
@ -101,85 +101,65 @@ export class Timeline {
|
|||
_setupEntries(timelineEntries) {
|
||||
this._remoteEntries.setManySorted(timelineEntries);
|
||||
if (this._pendingEvents) {
|
||||
this._localEntries = new MappedList(this._pendingEvents, pe => {
|
||||
const pee = new PendingEventEntry({pendingEvent: pe, member: this._ownMember, clock: this._clock});
|
||||
this._onAddPendingEvent(pee);
|
||||
return pee;
|
||||
}, (pee, params) => {
|
||||
// is sending but redacted, who do we detect that here to remove the relation?
|
||||
pee.notifyUpdate(params);
|
||||
}, pee => this._onRemovePendingEvent(pee));
|
||||
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))
|
||||
);
|
||||
} else {
|
||||
this._localEntries = new ObservableArray();
|
||||
}
|
||||
this._allEntries = new ConcatList(this._remoteEntries, this._localEntries);
|
||||
}
|
||||
|
||||
_onAddPendingEvent(pee) {
|
||||
let redactedEntry;
|
||||
this._applyAndEmitLocalRelationChange(pee.pendingEvent, target => {
|
||||
const wasRedacted = target.isRedacted;
|
||||
const params = target.addLocalRelation(pee);
|
||||
if (!wasRedacted && target.isRedacted) {
|
||||
redactedEntry = target;
|
||||
}
|
||||
return params;
|
||||
});
|
||||
if (redactedEntry) {
|
||||
this._addLocallyRedactedRelationToTarget(redactedEntry);
|
||||
async _mapPendingEventToEntry(pe) {
|
||||
// we load the remote redaction target for pending events,
|
||||
// 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.
|
||||
let redactionTarget;
|
||||
if (pe.eventType === REDACTION_TYPE && pe.relatedEventId) {
|
||||
const txn = await this._storage.readWriteTxn([
|
||||
this._storage.storeNames.timelineEvents,
|
||||
]);
|
||||
const redactionTargetEntry = await txn.timelineEvents.getByEventId(this._roomId, pe.relatedEventId);
|
||||
redactionTarget = redactionTargetEntry?.event;
|
||||
}
|
||||
const pee = new PendingEventEntry({pendingEvent: pe, member: this._ownMember, clock: this._clock, redactionTarget});
|
||||
this._applyAndEmitLocalRelationChange(pee, target => target.addLocalRelation(pee));
|
||||
return pee;
|
||||
}
|
||||
|
||||
_addLocallyRedactedRelationToTarget(redactedEntry) {
|
||||
const redactedRelation = getRelationFromContent(redactedEntry.content);
|
||||
if (redactedRelation?.event_id) {
|
||||
const found = this._remoteEntries.findAndUpdate(
|
||||
e => e.id === redactedRelation.event_id,
|
||||
relationTarget => relationTarget.addLocalRelation(redactedEntry) || false
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_onRemovePendingEvent(pee) {
|
||||
let unredactedEntry;
|
||||
this._applyAndEmitLocalRelationChange(pee.pendingEvent, target => {
|
||||
const wasRedacted = target.isRedacted;
|
||||
const params = target.removeLocalRelation(pee);
|
||||
if (wasRedacted && !target.isRedacted) {
|
||||
unredactedEntry = target;
|
||||
}
|
||||
return params;
|
||||
});
|
||||
if (unredactedEntry) {
|
||||
const redactedRelation = getRelationFromContent(unredactedEntry.content);
|
||||
if (redactedRelation?.event_id) {
|
||||
this._remoteEntries.findAndUpdate(
|
||||
e => e.id === redactedRelation.event_id,
|
||||
relationTarget => relationTarget.removeLocalRelation(unredactedEntry) || false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_applyAndEmitLocalRelationChange(pe, updater) {
|
||||
_applyAndEmitLocalRelationChange(pee, updater) {
|
||||
const updateOrFalse = e => {
|
||||
const params = updater(e);
|
||||
return params ? params : false;
|
||||
};
|
||||
let found = false;
|
||||
const {relatedTxnId} = pee.pendingEvent;
|
||||
// first, look in local entries based on txn id
|
||||
if (pe.relatedTxnId) {
|
||||
const found = this._localEntries.findAndUpdate(
|
||||
e => e.id === pe.relatedTxnId,
|
||||
if (relatedTxnId) {
|
||||
found = this._localEntries.findAndUpdate(
|
||||
e => e.id === relatedTxnId,
|
||||
updateOrFalse,
|
||||
);
|
||||
if (found) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// now look in remote entries based on event id
|
||||
if (pe.relatedEventId) {
|
||||
if (!found && pee.relatedEventId) {
|
||||
this._remoteEntries.findAndUpdate(
|
||||
e => e.id === pe.relatedEventId,
|
||||
e => e.id === pee.relatedEventId,
|
||||
updateOrFalse
|
||||
);
|
||||
}
|
||||
// also look for a relation target to update with this redaction
|
||||
if (pee.redactingRelation) {
|
||||
const eventId = pee.redactingRelation.event_id;
|
||||
const found = this._remoteEntries.findAndUpdate(
|
||||
e => e.id === eventId,
|
||||
updateOrFalse
|
||||
);
|
||||
}
|
||||
|
@ -231,32 +211,17 @@ export class Timeline {
|
|||
for (const pee of this._localEntries) {
|
||||
// this will work because we set relatedEventId when removing remote echos
|
||||
if (pee.relatedEventId) {
|
||||
|
||||
|
||||
const relationTarget = entries.find(e => e.id === pee.relatedEventId);
|
||||
if (relationTarget) {
|
||||
const wasRedacted = relationTarget.isRedacted;
|
||||
// no need to emit here as this entry is about to be added
|
||||
relationTarget.addLocalRelation(pee);
|
||||
if (!wasRedacted && relationTarget.isRedacted) {
|
||||
this._addLocallyRedactedRelationToTarget(relationTarget);
|
||||
}
|
||||
} else if (pee.eventType === REDACTION_TYPE) {
|
||||
// if pee is a redaction, we need to lookup the event it is redacting,
|
||||
// and see if that is a relation of one of the entries
|
||||
const redactedEntry = this.getByEventId(pee.relatedEventId);
|
||||
if (redactedEntry) {
|
||||
const relation = getRelation(redactedEntry);
|
||||
if (relation) {
|
||||
const redactedRelationTarget = entries.find(e => e.id === relation.event_id);
|
||||
redactedRelationTarget?.addLocalRelation(redactedEntry);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: errors are swallowed here
|
||||
// console.log(`could not find target for pee ${pee.relatedEventId} ` + entries.filter(e => !["m.reaction", "m.room.redaction"].includes(e.eventType)).map(e => `${e.id}: ${e.content?.body}`).join(","));
|
||||
// console.log(`could not find target for pee ${pee.relatedEventId} ` + entries.filter(e => "m.reaction" === e.eventType).map(e => `${e.id}: ${getRelation(e)?.key}`).join(","));
|
||||
// console.log(`could not find target for pee ${pee.relatedEventId} ` + entries.map(e => `${e.id}: ${e._eventEntry.key.substr(e._eventEntry.key.lastIndexOf("|") + 1)}`).join(","));
|
||||
}
|
||||
}
|
||||
if (pee.redactingRelation) {
|
||||
const eventId = pee.redactingRelation.event_id;
|
||||
const relationTarget = entries.find(e => e.id === eventId);
|
||||
if (relationTarget) {
|
||||
relationTarget.addLocalRelation(pee);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -344,6 +309,7 @@ export class Timeline {
|
|||
}
|
||||
|
||||
import {FragmentIdComparer} from "./FragmentIdComparer.js";
|
||||
import {poll} from "../../../mocks/poll.js";
|
||||
import {Clock as MockClock} from "../../../mocks/Clock.js";
|
||||
import {createMockStorage} from "../../../mocks/Storage.js";
|
||||
import {createEvent, withTextBody, withSender} from "../../../mocks/event.js";
|
||||
|
@ -355,6 +321,14 @@ import {PendingEvent} from "../sending/PendingEvent.js";
|
|||
export function tests() {
|
||||
const fragmentIdComparer = new FragmentIdComparer([]);
|
||||
const roomId = "$abc";
|
||||
const noopHandler = {};
|
||||
noopHandler.onAdd =
|
||||
noopHandler.onUpdate =
|
||||
noopHandler.onRemove =
|
||||
noopHandler.onMove =
|
||||
noopHandler.onReset =
|
||||
function() {};
|
||||
|
||||
return {
|
||||
"adding or replacing entries before subscribing to entries does not loose local relations": async assert => {
|
||||
const pendingEvents = new ObservableArray();
|
||||
|
@ -385,9 +359,10 @@ export function tests() {
|
|||
relatedEventId: event2.event_id
|
||||
}}));
|
||||
// 4. subscribe (it's now safe to iterate timeline.entries)
|
||||
timeline.entries.subscribe({});
|
||||
timeline.entries.subscribe(noopHandler);
|
||||
// 5. check the local relation got correctly aggregated
|
||||
assert.equal(Array.from(timeline.entries)[0].isRedacting, true);
|
||||
const locallyRedacted = await poll(() => Array.from(timeline.entries)[0].isRedacting);
|
||||
assert.equal(locallyRedacted, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,10 @@ export class BaseEventEntry extends BaseEntry {
|
|||
return this.isRedacting;
|
||||
}
|
||||
|
||||
get isRedaction() {
|
||||
return this.eventType === REDACTION_TYPE;
|
||||
}
|
||||
|
||||
get redactionReason() {
|
||||
if (this._pendingRedactions) {
|
||||
return this._pendingRedactions[0].content?.reason;
|
||||
|
@ -46,7 +50,7 @@ export class BaseEventEntry extends BaseEntry {
|
|||
@return [string] returns the name of the field that has changed, if any
|
||||
*/
|
||||
addLocalRelation(entry) {
|
||||
if (entry.eventType === REDACTION_TYPE) {
|
||||
if (entry.eventType === REDACTION_TYPE && entry.relatedEventId === this.id) {
|
||||
if (!this._pendingRedactions) {
|
||||
this._pendingRedactions = [];
|
||||
}
|
||||
|
@ -55,23 +59,25 @@ export class BaseEventEntry extends BaseEntry {
|
|||
return "isRedacted";
|
||||
}
|
||||
} else {
|
||||
const relation = getRelationFromContent(entry.content);
|
||||
if (relation && relation.rel_type === ANNOTATION_RELATION_TYPE) {
|
||||
if (!this._pendingAnnotations) {
|
||||
this._pendingAnnotations = new PendingAnnotations();
|
||||
const relation = entry.ownOrRedactedRelation;
|
||||
if (relation && relation.event_id === this.id) {
|
||||
if (relation.rel_type === ANNOTATION_RELATION_TYPE) {
|
||||
if (!this._pendingAnnotations) {
|
||||
this._pendingAnnotations = new PendingAnnotations();
|
||||
}
|
||||
this._pendingAnnotations.add(entry);
|
||||
return "pendingAnnotations";
|
||||
}
|
||||
this._pendingAnnotations.add(entry);
|
||||
return "pendingAnnotations";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
deaggregates local relation.
|
||||
deaggregates local relation or a local redaction of a remote relation.
|
||||
@return [string] returns the name of the field that has changed, if any
|
||||
*/
|
||||
removeLocalRelation(entry) {
|
||||
if (entry.eventType === REDACTION_TYPE && this._pendingRedactions) {
|
||||
if (entry.eventType === REDACTION_TYPE && entry.relatedEventId === this.id && this._pendingRedactions) {
|
||||
const countBefore = this._pendingRedactions.length;
|
||||
this._pendingRedactions = this._pendingRedactions.filter(e => e !== entry);
|
||||
if (this._pendingRedactions.length === 0) {
|
||||
|
@ -81,13 +87,15 @@ export class BaseEventEntry extends BaseEntry {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
const relation = getRelationFromContent(entry.content);
|
||||
if (relation && relation.rel_type === ANNOTATION_RELATION_TYPE && this._pendingAnnotations) {
|
||||
this._pendingAnnotations.remove(entry);
|
||||
if (this._pendingAnnotations.isEmpty) {
|
||||
this._pendingAnnotations = null;
|
||||
const relation = entry.ownOrRedactedRelation;
|
||||
if (relation && relation.event_id === this.id) {
|
||||
if (relation.rel_type === ANNOTATION_RELATION_TYPE && this._pendingAnnotations) {
|
||||
this._pendingAnnotations.remove(entry);
|
||||
if (this._pendingAnnotations.isEmpty) {
|
||||
this._pendingAnnotations = null;
|
||||
}
|
||||
return "pendingAnnotations";
|
||||
}
|
||||
return "pendingAnnotations";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -120,4 +128,8 @@ export class BaseEventEntry extends BaseEntry {
|
|||
async getOwnAnnotationEntry(timeline, key) {
|
||||
return this._pendingAnnotations?.findForKey(key);
|
||||
}
|
||||
|
||||
getAnnotationPendingRedaction(key) {
|
||||
return this._pendingAnnotations?.findRedactionForKey(key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,14 +16,16 @@ limitations under the License.
|
|||
|
||||
import {PENDING_FRAGMENT_ID} from "./BaseEntry.js";
|
||||
import {BaseEventEntry} from "./BaseEventEntry.js";
|
||||
import {getRelationFromContent} from "../relations.js";
|
||||
|
||||
export class PendingEventEntry extends BaseEventEntry {
|
||||
constructor({pendingEvent, member, clock}) {
|
||||
constructor({pendingEvent, member, clock, redactionTarget}) {
|
||||
super(null);
|
||||
this._pendingEvent = pendingEvent;
|
||||
/** @type {RoomMember} */
|
||||
this._member = member;
|
||||
this._clock = clock;
|
||||
this._redactionTarget = redactionTarget;
|
||||
}
|
||||
|
||||
get fragmentId() {
|
||||
|
@ -86,6 +88,24 @@ export class PendingEventEntry extends BaseEventEntry {
|
|||
return this._pendingEvent.relatedEventId;
|
||||
}
|
||||
|
||||
get redactingRelation() {
|
||||
if (this._redactionTarget) {
|
||||
return getRelationFromContent(this._redactionTarget.content);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* returns either the relationship on this entry,
|
||||
* or the relationship this entry is redacting.
|
||||
*
|
||||
* Useful while aggregating relations for local echo. */
|
||||
get ownOrRedactedRelation() {
|
||||
if (this._redactionTarget) {
|
||||
return getRelationFromContent(this._redactionTarget.content);
|
||||
} else {
|
||||
return getRelationFromContent(this._pendingEvent.content);
|
||||
}
|
||||
}
|
||||
|
||||
getOwnAnnotationId(_, key) {
|
||||
// TODO: implement this once local reactions are implemented
|
||||
return null;
|
||||
|
|
|
@ -23,6 +23,7 @@ import {BaseObservableMap} from "./map/BaseObservableMap.js";
|
|||
export { ObservableArray } from "./list/ObservableArray.js";
|
||||
export { SortedArray } from "./list/SortedArray.js";
|
||||
export { MappedList } from "./list/MappedList.js";
|
||||
export { AsyncMappedList } from "./list/AsyncMappedList.js";
|
||||
export { ConcatList } from "./list/ConcatList.js";
|
||||
export { ObservableMap } from "./map/ObservableMap.js";
|
||||
|
||||
|
|
150
src/observable/list/AsyncMappedList.js
Normal file
150
src/observable/list/AsyncMappedList.js
Normal file
|
@ -0,0 +1,150 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList.js";
|
||||
|
||||
export class AsyncMappedList extends BaseMappedList {
|
||||
constructor(sourceList, mapper, updater, removeCallback) {
|
||||
super(sourceList, mapper, updater, removeCallback);
|
||||
this._eventQueue = null;
|
||||
}
|
||||
|
||||
onSubscribeFirst() {
|
||||
this._sourceUnsubscribe = this._sourceList.subscribe(this);
|
||||
this._eventQueue = [];
|
||||
this._mappedValues = [];
|
||||
let idx = 0;
|
||||
for (const item of this._sourceList) {
|
||||
this._eventQueue.push(new AddEvent(idx, item));
|
||||
idx += 1;
|
||||
}
|
||||
this._flush();
|
||||
}
|
||||
|
||||
async _flush() {
|
||||
if (this._flushing) {
|
||||
return;
|
||||
}
|
||||
this._flushing = true;
|
||||
try {
|
||||
while (this._eventQueue.length) {
|
||||
const event = this._eventQueue.shift();
|
||||
await event.run(this);
|
||||
}
|
||||
} finally {
|
||||
this._flushing = false;
|
||||
}
|
||||
}
|
||||
|
||||
onReset() {
|
||||
if (this._eventQueue) {
|
||||
this._eventQueue.push(new ResetEvent());
|
||||
this._flush();
|
||||
}
|
||||
}
|
||||
|
||||
onAdd(index, value) {
|
||||
if (this._eventQueue) {
|
||||
this._eventQueue.push(new AddEvent(index, value));
|
||||
this._flush();
|
||||
}
|
||||
}
|
||||
|
||||
onUpdate(index, value, params) {
|
||||
if (this._eventQueue) {
|
||||
this._eventQueue.push(new UpdateEvent(index, value, params));
|
||||
this._flush();
|
||||
}
|
||||
}
|
||||
|
||||
onRemove(index) {
|
||||
if (this._eventQueue) {
|
||||
this._eventQueue.push(new RemoveEvent(index));
|
||||
this._flush();
|
||||
}
|
||||
}
|
||||
|
||||
onMove(fromIdx, toIdx) {
|
||||
if (this._eventQueue) {
|
||||
this._eventQueue.push(new MoveEvent(fromIdx, toIdx));
|
||||
this._flush();
|
||||
}
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
this._sourceUnsubscribe();
|
||||
this._eventQueue = null;
|
||||
this._mappedValues = null;
|
||||
}
|
||||
}
|
||||
|
||||
class AddEvent {
|
||||
constructor(index, value) {
|
||||
this.index = index;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
async run(list) {
|
||||
const mappedValue = await list._mapper(this.value);
|
||||
runAdd(list, this.index, mappedValue);
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateEvent {
|
||||
constructor(index, value, params) {
|
||||
this.index = index;
|
||||
this.value = value;
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
async run(list) {
|
||||
runUpdate(list, this.index, this.value, this.params);
|
||||
}
|
||||
}
|
||||
|
||||
class RemoveEvent {
|
||||
constructor(index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
async run(list) {
|
||||
runRemove(list, this.index);
|
||||
}
|
||||
}
|
||||
|
||||
class MoveEvent {
|
||||
constructor(fromIdx, toIdx) {
|
||||
this.fromIdx = fromIdx;
|
||||
this.toIdx = toIdx;
|
||||
}
|
||||
|
||||
async run(list) {
|
||||
runMove(list, this.fromIdx, this.toIdx);
|
||||
}
|
||||
}
|
||||
|
||||
class ResetEvent {
|
||||
async run(list) {
|
||||
runReset(list);
|
||||
}
|
||||
}
|
||||
|
||||
export function tests() {
|
||||
return {
|
||||
|
||||
}
|
||||
}
|
77
src/observable/list/BaseMappedList.js
Normal file
77
src/observable/list/BaseMappedList.js
Normal file
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
Copyright 2020 Bruno Windels <bruno@windels.cloud>
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
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 {BaseObservableList} from "./BaseObservableList.js";
|
||||
import {findAndUpdateInArray} from "./common.js";
|
||||
|
||||
export class BaseMappedList extends BaseObservableList {
|
||||
constructor(sourceList, mapper, updater, removeCallback) {
|
||||
super();
|
||||
this._sourceList = sourceList;
|
||||
this._mapper = mapper;
|
||||
this._updater = updater;
|
||||
this._removeCallback = removeCallback;
|
||||
this._mappedValues = null;
|
||||
this._sourceUnsubscribe = null;
|
||||
}
|
||||
|
||||
findAndUpdate(predicate, updater) {
|
||||
return findAndUpdateInArray(predicate, this._mappedValues, this, updater);
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._mappedValues.length;
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._mappedValues.values();
|
||||
}
|
||||
}
|
||||
|
||||
export function runAdd(list, index, mappedValue) {
|
||||
list._mappedValues.splice(index, 0, mappedValue);
|
||||
list.emitAdd(index, mappedValue);
|
||||
}
|
||||
|
||||
export function runUpdate(list, index, value, params) {
|
||||
const mappedValue = list._mappedValues[index];
|
||||
if (list._updater) {
|
||||
list._updater(mappedValue, params, value);
|
||||
}
|
||||
list.emitUpdate(index, mappedValue, params);
|
||||
}
|
||||
|
||||
export function runRemove(list, index) {
|
||||
const mappedValue = list._mappedValues[index];
|
||||
list._mappedValues.splice(index, 1);
|
||||
if (list._removeCallback) {
|
||||
list._removeCallback(mappedValue);
|
||||
}
|
||||
list.emitRemove(index, mappedValue);
|
||||
}
|
||||
|
||||
export function runMove(list, fromIdx, toIdx) {
|
||||
const mappedValue = list._mappedValues[fromIdx];
|
||||
list._mappedValues.splice(fromIdx, 1);
|
||||
list._mappedValues.splice(toIdx, 0, mappedValue);
|
||||
list.emitMove(fromIdx, toIdx, mappedValue);
|
||||
}
|
||||
|
||||
export function runReset(list) {
|
||||
list._mappedValues = [];
|
||||
list.emitReset();
|
||||
}
|
|
@ -15,20 +15,9 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {BaseObservableList} from "./BaseObservableList.js";
|
||||
import {findAndUpdateInArray} from "./common.js";
|
||||
|
||||
export class MappedList extends BaseObservableList {
|
||||
constructor(sourceList, mapper, updater, removeCallback) {
|
||||
super();
|
||||
this._sourceList = sourceList;
|
||||
this._mapper = mapper;
|
||||
this._updater = updater;
|
||||
this._removeCallback = removeCallback;
|
||||
this._sourceUnsubscribe = null;
|
||||
this._mappedValues = null;
|
||||
}
|
||||
import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList.js";
|
||||
|
||||
export class MappedList extends BaseMappedList {
|
||||
onSubscribeFirst() {
|
||||
this._sourceUnsubscribe = this._sourceList.subscribe(this);
|
||||
this._mappedValues = [];
|
||||
|
@ -38,14 +27,12 @@ export class MappedList extends BaseObservableList {
|
|||
}
|
||||
|
||||
onReset() {
|
||||
this._mappedValues = [];
|
||||
this.emitReset();
|
||||
runReset(this);
|
||||
}
|
||||
|
||||
onAdd(index, value) {
|
||||
const mappedValue = this._mapper(value);
|
||||
this._mappedValues.splice(index, 0, mappedValue);
|
||||
this.emitAdd(index, mappedValue);
|
||||
runAdd(this, index, mappedValue);
|
||||
}
|
||||
|
||||
onUpdate(index, value, params) {
|
||||
|
@ -53,47 +40,24 @@ export class MappedList extends BaseObservableList {
|
|||
if (!this._mappedValues) {
|
||||
return;
|
||||
}
|
||||
const mappedValue = this._mappedValues[index];
|
||||
if (this._updater) {
|
||||
this._updater(mappedValue, params, value);
|
||||
}
|
||||
this.emitUpdate(index, mappedValue, params);
|
||||
runUpdate(this, index, value, params);
|
||||
}
|
||||
|
||||
onRemove(index) {
|
||||
const mappedValue = this._mappedValues[index];
|
||||
this._mappedValues.splice(index, 1);
|
||||
if (this._removeCallback) {
|
||||
this._removeCallback(mappedValue);
|
||||
}
|
||||
this.emitRemove(index, mappedValue);
|
||||
runRemove(this, index);
|
||||
}
|
||||
|
||||
onMove(fromIdx, toIdx) {
|
||||
const mappedValue = this._mappedValues[fromIdx];
|
||||
this._mappedValues.splice(fromIdx, 1);
|
||||
this._mappedValues.splice(toIdx, 0, mappedValue);
|
||||
this.emitMove(fromIdx, toIdx, mappedValue);
|
||||
runMove(this, fromIdx, toIdx);
|
||||
}
|
||||
|
||||
onUnsubscribeLast() {
|
||||
this._sourceUnsubscribe();
|
||||
}
|
||||
|
||||
findAndUpdate(predicate, updater) {
|
||||
return findAndUpdateInArray(predicate, this._mappedValues, this, updater);
|
||||
}
|
||||
|
||||
get length() {
|
||||
return this._mappedValues.length;
|
||||
}
|
||||
|
||||
[Symbol.iterator]() {
|
||||
return this._mappedValues.values();
|
||||
}
|
||||
}
|
||||
|
||||
import {ObservableArray} from "./ObservableArray.js";
|
||||
import {BaseObservableList} from "./BaseObservableList.js";
|
||||
|
||||
export async function tests() {
|
||||
class MockList extends BaseObservableList {
|
||||
|
|
|
@ -243,7 +243,7 @@ only loads when the top comes into view*/
|
|||
|
||||
.Timeline_messageReactions button.haveReacted.isPending {
|
||||
animation-name: glow-reaction-border;
|
||||
animation-duration: 0.8s;
|
||||
animation-duration: 0.5s;
|
||||
animation-direction: alternate;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: linear;
|
||||
|
|
|
@ -74,6 +74,7 @@ export class TimelineList extends ListView {
|
|||
}
|
||||
}
|
||||
catch (err) {
|
||||
console.error(err);
|
||||
//ignore error, as it is handled in the VM
|
||||
}
|
||||
finally {
|
||||
|
|
Loading…
Reference in a new issue