278 lines
13 KiB
JavaScript
278 lines
13 KiB
JavaScript
"use strict";
|
|
|
|
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
|
|
Object.defineProperty(exports, "__esModule", {
|
|
value: true
|
|
});
|
|
exports.ReadReceipt = void 0;
|
|
exports.synthesizeReceipt = synthesizeReceipt;
|
|
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
|
|
var _read_receipts = require("../@types/read_receipts");
|
|
var _typedEventEmitter = require("./typed-event-emitter");
|
|
var _utils = require("../utils");
|
|
var _event = require("./event");
|
|
var _event2 = require("../@types/event");
|
|
var _room = require("./room");
|
|
/*
|
|
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.
|
|
*/
|
|
|
|
function synthesizeReceipt(userId, event, receiptType) {
|
|
var _event$threadRootId;
|
|
return new _event.MatrixEvent({
|
|
content: {
|
|
[event.getId()]: {
|
|
[receiptType]: {
|
|
[userId]: {
|
|
ts: event.getTs(),
|
|
thread_id: (_event$threadRootId = event.threadRootId) !== null && _event$threadRootId !== void 0 ? _event$threadRootId : _read_receipts.MAIN_ROOM_TIMELINE
|
|
}
|
|
}
|
|
}
|
|
},
|
|
type: _event2.EventType.Receipt,
|
|
room_id: event.getRoomId()
|
|
});
|
|
}
|
|
const ReceiptPairRealIndex = 0;
|
|
const ReceiptPairSyntheticIndex = 1;
|
|
class ReadReceipt extends _typedEventEmitter.TypedEventEmitter {
|
|
constructor(...args) {
|
|
super(...args);
|
|
// 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
|
|
(0, _defineProperty2.default)(this, "receipts", new _utils.MapWithDefault(() => new Map()));
|
|
(0, _defineProperty2.default)(this, "receiptCacheByEventId", new Map());
|
|
(0, _defineProperty2.default)(this, "timeline", void 0);
|
|
}
|
|
/**
|
|
* 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
|
|
*/
|
|
getReadReceiptForUserId(userId, ignoreSynthesized = false, receiptType = _read_receipts.ReceiptType.Read) {
|
|
var _this$receipts$get$ge, _this$receipts$get;
|
|
const [realReceipt, syntheticReceipt] = (_this$receipts$get$ge = (_this$receipts$get = this.receipts.get(receiptType)) === null || _this$receipts$get === void 0 ? void 0 : _this$receipts$get.get(userId)) !== null && _this$receipts$get$ge !== void 0 ? _this$receipts$get$ge : [null, null];
|
|
if (ignoreSynthesized) {
|
|
return realReceipt;
|
|
}
|
|
return syntheticReceipt !== null && syntheticReceipt !== void 0 ? syntheticReceipt : realReceipt;
|
|
}
|
|
compareReceipts(a, b) {
|
|
var _this$getUnfilteredTi;
|
|
// Try compare them in our unfiltered timeline set order, falling back to receipt timestamp which should be
|
|
// relatively sane as receipts are set only by the originating homeserver so as long as its clock doesn't
|
|
// jump around then it should be valid.
|
|
return (_this$getUnfilteredTi = this.getUnfilteredTimelineSet().compareEventOrdering(a.eventId, b.eventId)) !== null && _this$getUnfilteredTi !== void 0 ? _this$getUnfilteredTi : a.data.ts - b.data.ts;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
getEventReadUpTo(userId, ignoreSynthesized = false) {
|
|
var _ref, _privateReadReceipt$e, _ref2;
|
|
// 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 publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.Read);
|
|
const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.ReadPrivate);
|
|
|
|
// If we have both, compare them
|
|
let comparison;
|
|
if (publicReadReceipt !== null && publicReadReceipt !== void 0 && publicReadReceipt.eventId && privateReadReceipt !== null && privateReadReceipt !== void 0 && privateReadReceipt.eventId) {
|
|
comparison = this.compareReceipts(publicReadReceipt, privateReadReceipt);
|
|
}
|
|
|
|
// The public receipt is more likely to drift out of date so the private
|
|
// one has precedence
|
|
if (!comparison) return (_ref = (_privateReadReceipt$e = privateReadReceipt === null || privateReadReceipt === void 0 ? void 0 : privateReadReceipt.eventId) !== null && _privateReadReceipt$e !== void 0 ? _privateReadReceipt$e : publicReadReceipt === null || publicReadReceipt === void 0 ? void 0 : publicReadReceipt.eventId) !== null && _ref !== void 0 ? _ref : null;
|
|
|
|
// If public read receipt is older, return the private one
|
|
return (_ref2 = comparison < 0 ? privateReadReceipt === null || privateReadReceipt === void 0 ? void 0 : privateReadReceipt.eventId : publicReadReceipt === null || publicReadReceipt === void 0 ? void 0 : publicReadReceipt.eventId) !== null && _ref2 !== void 0 ? _ref2 : null;
|
|
}
|
|
addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic) {
|
|
var _pair$ReceiptPairSynt2, _pair$ReceiptPairSynt3;
|
|
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) {
|
|
var _pair$ReceiptPairSynt;
|
|
existingReceipt = (_pair$ReceiptPairSynt = pair[ReceiptPairSyntheticIndex]) !== null && _pair$ReceiptPairSynt !== void 0 ? _pair$ReceiptPairSynt : pair[ReceiptPairRealIndex];
|
|
}
|
|
const wrappedReceipt = {
|
|
eventId,
|
|
data: receipt
|
|
};
|
|
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.compareReceipts(existingReceipt, wrappedReceipt);
|
|
if (ordering >= 0) {
|
|
return;
|
|
}
|
|
}
|
|
const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
|
|
const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
|
|
let ordering = 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$ReceiptPairSynt2 = pair[ReceiptPairSyntheticIndex]) !== null && _pair$ReceiptPairSynt2 !== void 0 ? _pair$ReceiptPairSynt2 : pair[ReceiptPairRealIndex];
|
|
if (synthetic && preferSynthetic) {
|
|
pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
|
|
} else if (!synthetic) {
|
|
pair[ReceiptPairRealIndex] = wrappedReceipt;
|
|
if (!preferSynthetic) {
|
|
pair[ReceiptPairSyntheticIndex] = null;
|
|
}
|
|
}
|
|
const newCachedReceipt = (_pair$ReceiptPairSynt3 = pair[ReceiptPairSyntheticIndex]) !== null && _pair$ReceiptPairSynt3 !== void 0 ? _pair$ReceiptPairSynt3 : 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,
|
|
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.
|
|
*/
|
|
getReceiptsForEvent(event) {
|
|
return this.receiptCacheByEventId.get(event.getId()) || [];
|
|
}
|
|
/**
|
|
* 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
|
|
*/
|
|
fixupNotifications(userId) {
|
|
const receipt = this.getReadReceiptForUserId(userId, false);
|
|
const lastEvent = this.timeline[this.timeline.length - 1];
|
|
if (lastEvent && (receipt === null || receipt === void 0 ? void 0 : receipt.eventId) === lastEvent.getId() && userId === lastEvent.getSender()) {
|
|
this.setUnread(_room.NotificationCountType.Total, 0);
|
|
this.setUnread(_room.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
|
|
*/
|
|
addLocalEchoReceipt(userId, e, receiptType) {
|
|
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.
|
|
*/
|
|
getUsersReadUpTo(event) {
|
|
return this.getReceiptsForEvent(event).filter(function (receipt) {
|
|
return (0, _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.
|
|
*/
|
|
hasUserReadEvent(userId, eventId) {
|
|
var _this$timeline;
|
|
const readUpToId = this.getEventReadUpTo(userId, false);
|
|
if (readUpToId === eventId) return true;
|
|
if ((_this$timeline = this.timeline) !== null && _this$timeline !== void 0 && _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$timeline2 = this.timeline) === null || _this$timeline2 === void 0 ? void 0 : _this$timeline2.length) - 1; i >= 0; --i) {
|
|
var _this$timeline2;
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Returns the most recent unthreaded receipt for a given user
|
|
* @param userId - the MxID of the User
|
|
* @returns an unthreaded Receipt. Can be undefined if receipts have been disabled
|
|
* or a user chooses to use private read receipts (or we have simply not received
|
|
* a receipt from this user yet).
|
|
*/
|
|
}
|
|
exports.ReadReceipt = ReadReceipt;
|
|
//# sourceMappingURL=read-receipt.js.map
|