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) {
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
// is sending but redacted, who do we detect that here to remove the relation?
|
||||||
}, (pee, params) => {
|
pee.notifyUpdate(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))
|
||||||
}, pee => this._onRemovePendingEvent(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);
|
}
|
||||||
}
|
if (pee.redactingRelation) {
|
||||||
} else if (pee.eventType === REDACTION_TYPE) {
|
const eventId = pee.redactingRelation.event_id;
|
||||||
// if pee is a redaction, we need to lookup the event it is redacting,
|
const relationTarget = entries.find(e => e.id === eventId);
|
||||||
// and see if that is a relation of one of the entries
|
if (relationTarget) {
|
||||||
const redactedEntry = this.getByEventId(pee.relatedEventId);
|
relationTarget.addLocalRelation(pee);
|
||||||
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(","));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -384,10 +358,11 @@ export function tests() {
|
||||||
content: {},
|
content: {},
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,23 +59,25 @@ 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 (!this._pendingAnnotations) {
|
if (relation.rel_type === ANNOTATION_RELATION_TYPE) {
|
||||||
this._pendingAnnotations = new PendingAnnotations();
|
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
|
@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,13 +87,15 @@ 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) {
|
||||||
this._pendingAnnotations.remove(entry);
|
if (relation.rel_type === ANNOTATION_RELATION_TYPE && this._pendingAnnotations) {
|
||||||
if (this._pendingAnnotations.isEmpty) {
|
this._pendingAnnotations.remove(entry);
|
||||||
this._pendingAnnotations = null;
|
if (this._pendingAnnotations.isEmpty) {
|
||||||
|
this._pendingAnnotations = null;
|
||||||
|
}
|
||||||
|
return "pendingAnnotations";
|
||||||
}
|
}
|
||||||
return "pendingAnnotations";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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";
|
||||||
|
|
||||||
|
|
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.
|
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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Reference in a new issue