diff options
author | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
---|---|---|
committer | RaindropsSys <contact@minteck.org> | 2023-04-24 14:03:36 +0200 |
commit | 633c92eae865e957121e08de634aeee11a8b3992 (patch) | |
tree | 09d881bee1dae0b6eee49db1dfaf0f500240606c /includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts | |
parent | c4657e4509733699c0f26a3c900bab47e915d5a0 (diff) | |
download | pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.gz pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.tar.bz2 pluralconnect-633c92eae865e957121e08de634aeee11a8b3992.zip |
Updated 18 files, added 1692 files and deleted includes/system/compare.inc (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts')
-rw-r--r-- | includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts | 312 |
1 files changed, 312 insertions, 0 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts new file mode 100644 index 0000000..5858fe5 --- /dev/null +++ b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts @@ -0,0 +1,312 @@ +/* +Copyright 2022 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 { + CachedReceipt, + MAIN_ROOM_TIMELINE, + Receipt, + ReceiptCache, + ReceiptType, + WrappedReceipt, +} from "../@types/read_receipts"; +import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter"; +import * as utils from "../utils"; +import { MatrixEvent } from "./event"; +import { EventType } from "../@types/event"; +import { EventTimelineSet } from "./event-timeline-set"; +import { MapWithDefault } from "../utils"; +import { NotificationCountType } from "./room"; + +export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent { + return new MatrixEvent({ + content: { + [event.getId()!]: { + [receiptType]: { + [userId]: { + ts: event.getTs(), + thread_id: event.threadRootId ?? MAIN_ROOM_TIMELINE, + }, + }, + }, + }, + type: EventType.Receipt, + room_id: event.getRoomId(), + }); +} + +const ReceiptPairRealIndex = 0; +const ReceiptPairSyntheticIndex = 1; + +export abstract class ReadReceipt< + Events extends string, + Arguments extends ListenerMap<Events>, + SuperclassArguments extends ListenerMap<any> = Arguments, +> extends TypedEventEmitter<Events, Arguments, SuperclassArguments> { + // receipts should clobber based on receipt_type and user_id pairs hence + // the form of this structure. This is sub-optimal for the exposed APIs + // which pass in an event ID and get back some receipts, so we also store + // a pre-cached list for this purpose. + // Map: receipt type → user Id → receipt + private receipts = new MapWithDefault<string, Map<string, [WrappedReceipt | null, WrappedReceipt | null]>>( + () => new Map(), + ); + private receiptCacheByEventId: ReceiptCache = new Map(); + + public abstract getUnfilteredTimelineSet(): EventTimelineSet; + public abstract timeline: MatrixEvent[]; + + /** + * Gets the latest receipt for a given user in the room + * @param userId - The id of the user for which we want the receipt + * @param ignoreSynthesized - Whether to ignore synthesized receipts or not + * @param receiptType - Optional. The type of the receipt we want to get + * @returns the latest receipts of the chosen type for the chosen user + */ + public getReadReceiptForUserId( + userId: string, + ignoreSynthesized = false, + receiptType = ReceiptType.Read, + ): WrappedReceipt | null { + const [realReceipt, syntheticReceipt] = this.receipts.get(receiptType)?.get(userId) ?? [null, null]; + if (ignoreSynthesized) { + return realReceipt; + } + + return syntheticReceipt ?? realReceipt; + } + + /** + * Get the ID of the event that a given user has read up to, or null if we + * have received no read receipts from them. + * @param userId - The user ID to get read receipt event ID for + * @param ignoreSynthesized - If true, return only receipts that have been + * sent by the server, not implicit ones generated + * by the JS SDK. + * @returns ID of the latest event that the given user has read, or null. + */ + public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null { + // XXX: This is very very ugly and I hope I won't have to ever add a new + // receipt type here again. IMHO this should be done by the server in + // some more intelligent manner or the client should just use timestamps + + const timelineSet = this.getUnfilteredTimelineSet(); + const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read); + const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate); + + // If we have both, compare them + let comparison: number | null | undefined; + if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) { + comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId); + } + + // If we didn't get a comparison try to compare the ts of the receipts + if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) { + comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts; + } + + // The public receipt is more likely to drift out of date so the private + // one has precedence + if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null; + + // If public read receipt is older, return the private one + return (comparison < 0 ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null; + } + + public addReceiptToStructure( + eventId: string, + receiptType: ReceiptType, + userId: string, + receipt: Receipt, + synthetic: boolean, + ): void { + const receiptTypesMap = this.receipts.getOrCreate(receiptType); + let pair = receiptTypesMap.get(userId); + + if (!pair) { + pair = [null, null]; + receiptTypesMap.set(userId, pair); + } + + let existingReceipt = pair[ReceiptPairRealIndex]; + if (synthetic) { + existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + } + + if (existingReceipt) { + // we only want to add this receipt if we think it is later than the one we already have. + // This is managed server-side, but because we synthesize RRs locally we have to do it here too. + const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId); + if (ordering !== null && ordering >= 0) { + return; + } + } + + const wrappedReceipt: WrappedReceipt = { + eventId, + data: receipt, + }; + + const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt; + const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex]; + + let ordering: number | null = null; + if (realReceipt && syntheticReceipt) { + ordering = this.getUnfilteredTimelineSet().compareEventOrdering( + realReceipt.eventId, + syntheticReceipt.eventId, + ); + } + + const preferSynthetic = ordering === null || ordering < 0; + + // we don't bother caching just real receipts by event ID as there's nothing that would read it. + // Take the current cached receipt before we overwrite the pair elements. + const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + + if (synthetic && preferSynthetic) { + pair[ReceiptPairSyntheticIndex] = wrappedReceipt; + } else if (!synthetic) { + pair[ReceiptPairRealIndex] = wrappedReceipt; + + if (!preferSynthetic) { + pair[ReceiptPairSyntheticIndex] = null; + } + } + + const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex]; + if (cachedReceipt === newCachedReceipt) return; + + // clean up any previous cache entry + if (cachedReceipt && this.receiptCacheByEventId.get(cachedReceipt.eventId)) { + const previousEventId = cachedReceipt.eventId; + // Remove the receipt we're about to clobber out of existence from the cache + this.receiptCacheByEventId.set( + previousEventId, + this.receiptCacheByEventId.get(previousEventId)!.filter((r) => { + return r.type !== receiptType || r.userId !== userId; + }), + ); + + if (this.receiptCacheByEventId.get(previousEventId)!.length < 1) { + this.receiptCacheByEventId.delete(previousEventId); // clean up the cache keys + } + } + + // cache the new one + if (!this.receiptCacheByEventId.get(eventId)) { + this.receiptCacheByEventId.set(eventId, []); + } + this.receiptCacheByEventId.get(eventId)!.push({ + userId: userId, + type: receiptType as ReceiptType, + data: receipt, + }); + } + + /** + * Get a list of receipts for the given event. + * @param event - the event to get receipts for + * @returns A list of receipts with a userId, type and data keys or + * an empty list. + */ + public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] { + return this.receiptCacheByEventId.get(event.getId()!) || []; + } + + public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void; + + public abstract setUnread(type: NotificationCountType, count: number): void; + + /** + * This issue should also be addressed on synapse's side and is tracked as part + * of https://github.com/matrix-org/synapse/issues/14837 + * + * Retrieves the read receipt for the logged in user and checks if it matches + * the last event in the room and whether that event originated from the logged + * in user. + * Under those conditions we can consider the context as read. This is useful + * because we never send read receipts against our own events + * @param userId - the logged in user + */ + public fixupNotifications(userId: string): void { + const receipt = this.getReadReceiptForUserId(userId, false); + + const lastEvent = this.timeline[this.timeline.length - 1]; + if (lastEvent && receipt?.eventId === lastEvent.getId() && userId === lastEvent.getSender()) { + this.setUnread(NotificationCountType.Total, 0); + this.setUnread(NotificationCountType.Highlight, 0); + } + } + + /** + * Add a temporary local-echo receipt to the room to reflect in the + * client the fact that we've sent one. + * @param userId - The user ID if the receipt sender + * @param e - The event that is to be acknowledged + * @param receiptType - The type of receipt + */ + public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void { + this.addReceipt(synthesizeReceipt(userId, e, receiptType), true); + } + + /** + * Get a list of user IDs who have <b>read up to</b> the given event. + * @param event - the event to get read receipts for. + * @returns A list of user IDs. + */ + public getUsersReadUpTo(event: MatrixEvent): string[] { + return this.getReceiptsForEvent(event) + .filter(function (receipt) { + return utils.isSupportedReceiptType(receipt.type); + }) + .map(function (receipt) { + return receipt.userId; + }); + } + + /** + * Determines if the given user has read a particular event ID with the known + * history of the room. This is not a definitive check as it relies only on + * what is available to the room at the time of execution. + * @param userId - The user ID to check the read state of. + * @param eventId - The event ID to check if the user read. + * @returns True if the user has read the event, false otherwise. + */ + public hasUserReadEvent(userId: string, eventId: string): boolean { + const readUpToId = this.getEventReadUpTo(userId, false); + if (readUpToId === eventId) return true; + + if ( + this.timeline?.length && + this.timeline[this.timeline.length - 1].getSender() && + this.timeline[this.timeline.length - 1].getSender() === userId + ) { + // It doesn't matter where the event is in the timeline, the user has read + // it because they've sent the latest event. + return true; + } + + for (let i = this.timeline?.length - 1; i >= 0; --i) { + const ev = this.timeline[i]; + + // If we encounter the target event first, the user hasn't read it + // however if we encounter the readUpToId first then the user has read + // it. These rules apply because we're iterating bottom-up. + if (ev.getId() === eventId) return false; + if (ev.getId() === readUpToId) return true; + } + + // We don't know if the user has read it, so assume not. + return false; + } +} |