This commit is contained in:
Bruno Windels 2021-06-10 18:29:10 +02:00
parent cb051ad161
commit 757e08c62c
12 changed files with 374 additions and 178 deletions

View file

@ -125,8 +125,7 @@ export class BaseMessageTile extends SimpleTile {
react(key, log = null) { react(key, log = null) {
return this.logger.wrapOrRun(log, "react", async log => { return this.logger.wrapOrRun(log, "react", async log => {
const existingAnnotation = await this._entry.getOwnAnnotationEntry(this._timeline, key); const redaction = this._entry.getAnnotationPendingRedaction(key);
const redaction = existingAnnotation?.pendingRedaction;
if (redaction && !redaction.pendingEvent.hasStartedSending) { if (redaction && !redaction.pendingEvent.hasStartedSending) {
log.set("abort_redaction", true); log.set("abort_redaction", true);
await redaction.pendingEvent.abort(); await redaction.pendingEvent.abort();
@ -138,9 +137,16 @@ export class BaseMessageTile extends SimpleTile {
redactReaction(key, log = null) { redactReaction(key, log = null) {
return this.logger.wrapOrRun(log, "redactReaction", async log => { 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); const entry = await this._entry.getOwnAnnotationEntry(this._timeline, key);
if (entry) { if (entry) {
await this._room.sendRedaction(entry.id, null, log); await this._room.sendRedaction(entry.id, null, log);
} else {
log.set("no_reaction", true);
} }
}); });
} }

View file

@ -452,23 +452,6 @@ export class BaseRoom extends EventEmitter {
return observable; 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) { async _readEventById(eventId) {
let stores = [this._storage.storeNames.timelineEvents]; let stores = [this._storage.storeNames.timelineEvents];
if (this.isEncrypted) { if (this.isEncrypted) {

View file

@ -19,35 +19,33 @@ import {getRelationFromContent} from "./relations.js";
export class PendingAnnotations { export class PendingAnnotations {
constructor() { constructor() {
this.aggregatedAnnotations = new Map(); this.aggregatedAnnotations = new Map();
// this contains both pending annotation entries, and pending redactions of remote annotation entries
this._entries = []; this._entries = [];
} }
/** adds either a pending annotation entry, or a remote annotation entry with a pending redaction */ /** adds either a pending annotation entry, or a remote annotation entry with a pending redaction */
add(annotationEntry) { add(entry) {
const relation = getRelationFromContent(annotationEntry.content); const {key} = entry.ownOrRedactedRelation;
const key = relation.key;
if (!key) { if (!key) {
return; return;
} }
const count = this.aggregatedAnnotations.get(key) || 0; const count = this.aggregatedAnnotations.get(key) || 0;
const addend = annotationEntry.isRedacted ? -1 : 1; const addend = entry.isRedaction ? -1 : 1;
console.log("add", count, addend);
this.aggregatedAnnotations.set(key, count + addend); 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 */ /** removes either a pending annotation entry, or a remote annotation entry with a pending redaction */
remove(annotationEntry) { remove(entry) {
const idx = this._entries.indexOf(annotationEntry); const idx = this._entries.indexOf(entry);
if (idx === -1) { if (idx === -1) {
return; return;
} }
this._entries.splice(idx, 1); this._entries.splice(idx, 1);
const relation = getRelationFromContent(annotationEntry.content); const {key} = entry.ownOrRedactedRelation;
const key = relation.key;
let count = this.aggregatedAnnotations.get(key); let count = this.aggregatedAnnotations.get(key);
if (count !== undefined) { if (count !== undefined) {
const addend = annotationEntry.isRedacted ? 1 : -1; const addend = entry.isRedaction ? 1 : -1;
count += addend; count += addend;
if (count <= 0) { if (count <= 0) {
this.aggregatedAnnotations.delete(key); this.aggregatedAnnotations.delete(key);
@ -60,13 +58,22 @@ export class PendingAnnotations {
findForKey(key) { findForKey(key) {
return this._entries.find(e => { return this._entries.find(e => {
const relation = getRelationFromContent(e.content); 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; return e;
} }
}); });
} }
get isEmpty() { get isEmpty() {
return this._entries.length; return this._entries.length === 0;
} }
} }

View file

@ -15,7 +15,7 @@ See the License for the specific language governing permissions and
limitations under the License. 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 {Disposables} from "../../../utils/Disposables.js";
import {Direction} from "./Direction.js"; import {Direction} from "./Direction.js";
import {TimelineReader} from "./persistence/TimelineReader.js"; import {TimelineReader} from "./persistence/TimelineReader.js";
@ -101,85 +101,65 @@ export class Timeline {
_setupEntries(timelineEntries) { _setupEntries(timelineEntries) {
this._remoteEntries.setManySorted(timelineEntries); this._remoteEntries.setManySorted(timelineEntries);
if (this._pendingEvents) { if (this._pendingEvents) {
this._localEntries = new MappedList(this._pendingEvents, pe => { this._localEntries = new AsyncMappedList(this._pendingEvents,
const pee = new PendingEventEntry({pendingEvent: pe, member: this._ownMember, clock: this._clock}); pe => this._mapPendingEventToEntry(pe),
this._onAddPendingEvent(pee); (pee, params) => {
return pee;
}, (pee, params) => {
// is sending but redacted, who do we detect that here to remove the relation? // is sending but redacted, who do we detect that here to remove the relation?
pee.notifyUpdate(params); pee.notifyUpdate(params);
}, pee => this._onRemovePendingEvent(pee)); },
pee => this._applyAndEmitLocalRelationChange(pee, target => target.removeLocalRelation(pee))
);
} else { } else {
this._localEntries = new ObservableArray(); this._localEntries = new ObservableArray();
} }
this._allEntries = new ConcatList(this._remoteEntries, this._localEntries); this._allEntries = new ConcatList(this._remoteEntries, this._localEntries);
} }
_onAddPendingEvent(pee) { async _mapPendingEventToEntry(pe) {
let redactedEntry; // we load the remote redaction target for pending events,
this._applyAndEmitLocalRelationChange(pee.pendingEvent, target => { // so if we are redacting a relation, we can pass the redaction
const wasRedacted = target.isRedacted; // to the relation target and the removal of the relation can
const params = target.addLocalRelation(pee); // be taken into account for local echo.
if (!wasRedacted && target.isRedacted) { let redactionTarget;
redactedEntry = target; if (pe.eventType === REDACTION_TYPE && pe.relatedEventId) {
} const txn = await this._storage.readWriteTxn([
return params; this._storage.storeNames.timelineEvents,
}); ]);
if (redactedEntry) { const redactionTargetEntry = await txn.timelineEvents.getByEventId(this._roomId, pe.relatedEventId);
this._addLocallyRedactedRelationToTarget(redactedEntry); 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) { _applyAndEmitLocalRelationChange(pee, updater) {
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) {
const updateOrFalse = e => { const updateOrFalse = e => {
const params = updater(e); const params = updater(e);
return params ? params : false; return params ? params : false;
}; };
let found = false;
const {relatedTxnId} = pee.pendingEvent;
// first, look in local entries based on txn id // first, look in local entries based on txn id
if (pe.relatedTxnId) { if (relatedTxnId) {
const found = this._localEntries.findAndUpdate( found = this._localEntries.findAndUpdate(
e => e.id === pe.relatedTxnId, e => e.id === relatedTxnId,
updateOrFalse, updateOrFalse,
); );
if (found) {
return;
}
} }
// now look in remote entries based on event id // now look in remote entries based on event id
if (pe.relatedEventId) { if (!found && pee.relatedEventId) {
this._remoteEntries.findAndUpdate( 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 updateOrFalse
); );
} }
@ -231,32 +211,17 @@ export class Timeline {
for (const pee of this._localEntries) { for (const pee of this._localEntries) {
// this will work because we set relatedEventId when removing remote echos // this will work because we set relatedEventId when removing remote echos
if (pee.relatedEventId) { if (pee.relatedEventId) {
const relationTarget = entries.find(e => e.id === pee.relatedEventId); const relationTarget = entries.find(e => e.id === pee.relatedEventId);
if (relationTarget) { if (relationTarget) {
const wasRedacted = relationTarget.isRedacted;
// no need to emit here as this entry is about to be added // no need to emit here as this entry is about to be added
relationTarget.addLocalRelation(pee); 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 { if (pee.redactingRelation) {
// TODO: errors are swallowed here const eventId = pee.redactingRelation.event_id;
// 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(",")); const relationTarget = entries.find(e => e.id === eventId);
// 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(",")); if (relationTarget) {
// 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(",")); relationTarget.addLocalRelation(pee);
} }
} }
} }
@ -344,6 +309,7 @@ export class Timeline {
} }
import {FragmentIdComparer} from "./FragmentIdComparer.js"; import {FragmentIdComparer} from "./FragmentIdComparer.js";
import {poll} from "../../../mocks/poll.js";
import {Clock as MockClock} from "../../../mocks/Clock.js"; import {Clock as MockClock} from "../../../mocks/Clock.js";
import {createMockStorage} from "../../../mocks/Storage.js"; import {createMockStorage} from "../../../mocks/Storage.js";
import {createEvent, withTextBody, withSender} from "../../../mocks/event.js"; import {createEvent, withTextBody, withSender} from "../../../mocks/event.js";
@ -355,6 +321,14 @@ import {PendingEvent} from "../sending/PendingEvent.js";
export function tests() { export function tests() {
const fragmentIdComparer = new FragmentIdComparer([]); const fragmentIdComparer = new FragmentIdComparer([]);
const roomId = "$abc"; const roomId = "$abc";
const noopHandler = {};
noopHandler.onAdd =
noopHandler.onUpdate =
noopHandler.onRemove =
noopHandler.onMove =
noopHandler.onReset =
function() {};
return { return {
"adding or replacing entries before subscribing to entries does not loose local relations": async assert => { "adding or replacing entries before subscribing to entries does not loose local relations": async assert => {
const pendingEvents = new ObservableArray(); const pendingEvents = new ObservableArray();
@ -385,9 +359,10 @@ export function tests() {
relatedEventId: event2.event_id relatedEventId: event2.event_id
}})); }}));
// 4. subscribe (it's now safe to iterate timeline.entries) // 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 // 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);
} }
} }
} }

View file

@ -34,6 +34,10 @@ export class BaseEventEntry extends BaseEntry {
return this.isRedacting; return this.isRedacting;
} }
get isRedaction() {
return this.eventType === REDACTION_TYPE;
}
get redactionReason() { get redactionReason() {
if (this._pendingRedactions) { if (this._pendingRedactions) {
return this._pendingRedactions[0].content?.reason; 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 @return [string] returns the name of the field that has changed, if any
*/ */
addLocalRelation(entry) { addLocalRelation(entry) {
if (entry.eventType === REDACTION_TYPE) { if (entry.eventType === REDACTION_TYPE && entry.relatedEventId === this.id) {
if (!this._pendingRedactions) { if (!this._pendingRedactions) {
this._pendingRedactions = []; this._pendingRedactions = [];
} }
@ -55,8 +59,9 @@ export class BaseEventEntry extends BaseEntry {
return "isRedacted"; return "isRedacted";
} }
} else { } else {
const relation = getRelationFromContent(entry.content); const relation = entry.ownOrRedactedRelation;
if (relation && relation.rel_type === ANNOTATION_RELATION_TYPE) { if (relation && relation.event_id === this.id) {
if (relation.rel_type === ANNOTATION_RELATION_TYPE) {
if (!this._pendingAnnotations) { if (!this._pendingAnnotations) {
this._pendingAnnotations = new PendingAnnotations(); this._pendingAnnotations = new PendingAnnotations();
} }
@ -65,13 +70,14 @@ export class BaseEventEntry extends BaseEntry {
} }
} }
} }
}
/** /**
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 @return [string] returns the name of the field that has changed, if any
*/ */
removeLocalRelation(entry) { 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; const countBefore = this._pendingRedactions.length;
this._pendingRedactions = this._pendingRedactions.filter(e => e !== entry); this._pendingRedactions = this._pendingRedactions.filter(e => e !== entry);
if (this._pendingRedactions.length === 0) { if (this._pendingRedactions.length === 0) {
@ -81,8 +87,9 @@ export class BaseEventEntry extends BaseEntry {
} }
} }
} else { } else {
const relation = getRelationFromContent(entry.content); const relation = entry.ownOrRedactedRelation;
if (relation && relation.rel_type === ANNOTATION_RELATION_TYPE && this._pendingAnnotations) { if (relation && relation.event_id === this.id) {
if (relation.rel_type === ANNOTATION_RELATION_TYPE && this._pendingAnnotations) {
this._pendingAnnotations.remove(entry); this._pendingAnnotations.remove(entry);
if (this._pendingAnnotations.isEmpty) { if (this._pendingAnnotations.isEmpty) {
this._pendingAnnotations = null; this._pendingAnnotations = null;
@ -91,6 +98,7 @@ export class BaseEventEntry extends BaseEntry {
} }
} }
} }
}
async abortPendingRedaction() { async abortPendingRedaction() {
if (this._pendingRedactions) { if (this._pendingRedactions) {
@ -120,4 +128,8 @@ export class BaseEventEntry extends BaseEntry {
async getOwnAnnotationEntry(timeline, key) { async getOwnAnnotationEntry(timeline, key) {
return this._pendingAnnotations?.findForKey(key); return this._pendingAnnotations?.findForKey(key);
} }
getAnnotationPendingRedaction(key) {
return this._pendingAnnotations?.findRedactionForKey(key);
}
} }

View file

@ -16,14 +16,16 @@ limitations under the License.
import {PENDING_FRAGMENT_ID} from "./BaseEntry.js"; import {PENDING_FRAGMENT_ID} from "./BaseEntry.js";
import {BaseEventEntry} from "./BaseEventEntry.js"; import {BaseEventEntry} from "./BaseEventEntry.js";
import {getRelationFromContent} from "../relations.js";
export class PendingEventEntry extends BaseEventEntry { export class PendingEventEntry extends BaseEventEntry {
constructor({pendingEvent, member, clock}) { constructor({pendingEvent, member, clock, redactionTarget}) {
super(null); super(null);
this._pendingEvent = pendingEvent; this._pendingEvent = pendingEvent;
/** @type {RoomMember} */ /** @type {RoomMember} */
this._member = member; this._member = member;
this._clock = clock; this._clock = clock;
this._redactionTarget = redactionTarget;
} }
get fragmentId() { get fragmentId() {
@ -86,6 +88,24 @@ export class PendingEventEntry extends BaseEventEntry {
return this._pendingEvent.relatedEventId; 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) { getOwnAnnotationId(_, key) {
// TODO: implement this once local reactions are implemented // TODO: implement this once local reactions are implemented
return null; return null;

View file

@ -23,6 +23,7 @@ import {BaseObservableMap} from "./map/BaseObservableMap.js";
export { ObservableArray } from "./list/ObservableArray.js"; export { ObservableArray } from "./list/ObservableArray.js";
export { SortedArray } from "./list/SortedArray.js"; export { SortedArray } from "./list/SortedArray.js";
export { MappedList } from "./list/MappedList.js"; export { MappedList } from "./list/MappedList.js";
export { AsyncMappedList } from "./list/AsyncMappedList.js";
export { ConcatList } from "./list/ConcatList.js"; export { ConcatList } from "./list/ConcatList.js";
export { ObservableMap } from "./map/ObservableMap.js"; export { ObservableMap } from "./map/ObservableMap.js";

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

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

View file

@ -15,20 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import {BaseObservableList} from "./BaseObservableList.js"; import {BaseMappedList, runAdd, runUpdate, runRemove, runMove, runReset} from "./BaseMappedList.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;
}
export class MappedList extends BaseMappedList {
onSubscribeFirst() { onSubscribeFirst() {
this._sourceUnsubscribe = this._sourceList.subscribe(this); this._sourceUnsubscribe = this._sourceList.subscribe(this);
this._mappedValues = []; this._mappedValues = [];
@ -38,14 +27,12 @@ export class MappedList extends BaseObservableList {
} }
onReset() { onReset() {
this._mappedValues = []; runReset(this);
this.emitReset();
} }
onAdd(index, value) { onAdd(index, value) {
const mappedValue = this._mapper(value); const mappedValue = this._mapper(value);
this._mappedValues.splice(index, 0, mappedValue); runAdd(this, index, mappedValue);
this.emitAdd(index, mappedValue);
} }
onUpdate(index, value, params) { onUpdate(index, value, params) {
@ -53,47 +40,24 @@ export class MappedList extends BaseObservableList {
if (!this._mappedValues) { if (!this._mappedValues) {
return; return;
} }
const mappedValue = this._mappedValues[index]; runUpdate(this, index, value, params);
if (this._updater) {
this._updater(mappedValue, params, value);
}
this.emitUpdate(index, mappedValue, params);
} }
onRemove(index) { onRemove(index) {
const mappedValue = this._mappedValues[index]; runRemove(this, index);
this._mappedValues.splice(index, 1);
if (this._removeCallback) {
this._removeCallback(mappedValue);
}
this.emitRemove(index, mappedValue);
} }
onMove(fromIdx, toIdx) { onMove(fromIdx, toIdx) {
const mappedValue = this._mappedValues[fromIdx]; runMove(this, fromIdx, toIdx);
this._mappedValues.splice(fromIdx, 1);
this._mappedValues.splice(toIdx, 0, mappedValue);
this.emitMove(fromIdx, toIdx, mappedValue);
} }
onUnsubscribeLast() { onUnsubscribeLast() {
this._sourceUnsubscribe(); 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 {ObservableArray} from "./ObservableArray.js";
import {BaseObservableList} from "./BaseObservableList.js";
export async function tests() { export async function tests() {
class MockList extends BaseObservableList { class MockList extends BaseObservableList {

View file

@ -243,7 +243,7 @@ only loads when the top comes into view*/
.Timeline_messageReactions button.haveReacted.isPending { .Timeline_messageReactions button.haveReacted.isPending {
animation-name: glow-reaction-border; animation-name: glow-reaction-border;
animation-duration: 0.8s; animation-duration: 0.5s;
animation-direction: alternate; animation-direction: alternate;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-timing-function: linear; animation-timing-function: linear;

View file

@ -74,6 +74,7 @@ export class TimelineList extends ListView {
} }
} }
catch (err) { catch (err) {
console.error(err);
//ignore error, as it is handled in the VM //ignore error, as it is handled in the VM
} }
finally { finally {