summaryrefslogtreecommitdiffstats
path: root/comm/chat/protocols/matrix/lib/matrix-sdk/models
diff options
context:
space:
mode:
Diffstat (limited to 'comm/chat/protocols/matrix/lib/matrix-sdk/models')
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089Branch.js227
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089TreeSpace.js508
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/ToDeviceMessage.js5
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/beacon.js181
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/device.js80
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/event-context.js116
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/event-status.js35
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js809
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js469
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/event.js1442
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/invites-ignorer.js358
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/poll.js237
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/read-receipt.js260
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/related-relations.js41
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/relations-container.js135
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/relations.js336
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/room-member.js363
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/room-state.js931
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js34
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/room.js3079
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/search-result.js58
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/thread.js649
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/typed-event-emitter.js200
-rw-r--r--comm/chat/protocols/matrix/lib/matrix-sdk/models/user.js211
24 files changed, 10764 insertions, 0 deletions
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089Branch.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089Branch.js
new file mode 100644
index 0000000000..61560cde27
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089Branch.js
@@ -0,0 +1,227 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.MSC3089Branch = void 0;
+var _event = require("../@types/event");
+var _eventTimeline = require("./event-timeline");
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089) branch - a reference
+ * to a file (leaf) in the tree. Note that this is UNSTABLE and subject to breaking changes
+ * without notice.
+ */
+class MSC3089Branch {
+ constructor(client, indexEvent, directory) {
+ this.client = client;
+ this.indexEvent = indexEvent;
+ this.directory = directory;
+ } // Nothing to do
+
+ /**
+ * The file ID.
+ */
+ get id() {
+ const stateKey = this.indexEvent.getStateKey();
+ if (!stateKey) {
+ throw new Error("State key not found for branch");
+ }
+ return stateKey;
+ }
+
+ /**
+ * Whether this branch is active/valid.
+ */
+ get isActive() {
+ return this.indexEvent.getContent()["active"] === true;
+ }
+
+ /**
+ * Version for the file, one-indexed.
+ */
+ get version() {
+ return this.indexEvent.getContent()["version"] ?? 1;
+ }
+ get roomId() {
+ return this.indexEvent.getRoomId();
+ }
+
+ /**
+ * Deletes the file from the tree, including all prior edits/versions.
+ * @returns Promise which resolves when complete.
+ */
+ async delete() {
+ await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, {}, this.id);
+ await this.client.redactEvent(this.roomId, this.id);
+ const nextVersion = (await this.getVersionHistory())[1]; // [0] will be us
+ if (nextVersion) await nextVersion.delete(); // implicit recursion
+ }
+
+ /**
+ * Gets the name for this file.
+ * @returns The name, or "Unnamed File" if unknown.
+ */
+ getName() {
+ return this.indexEvent.getContent()["name"] || "Unnamed File";
+ }
+
+ /**
+ * Sets the name for this file.
+ * @param name - The new name for this file.
+ * @returns Promise which resolves when complete.
+ */
+ async setName(name) {
+ await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, _objectSpread(_objectSpread({}, this.indexEvent.getContent()), {}, {
+ name: name
+ }), this.id);
+ }
+
+ /**
+ * Gets whether or not a file is locked.
+ * @returns True if locked, false otherwise.
+ */
+ isLocked() {
+ return this.indexEvent.getContent()["locked"] || false;
+ }
+
+ /**
+ * Sets a file as locked or unlocked.
+ * @param locked - True to lock the file, false otherwise.
+ * @returns Promise which resolves when complete.
+ */
+ async setLocked(locked) {
+ await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, _objectSpread(_objectSpread({}, this.indexEvent.getContent()), {}, {
+ locked: locked
+ }), this.id);
+ }
+
+ /**
+ * Gets information about the file needed to download it.
+ * @returns Information about the file.
+ */
+ async getFileInfo() {
+ const event = await this.getFileEvent();
+ const file = event.getOriginalContent()["file"];
+ const httpUrl = this.client.mxcUrlToHttp(file["url"]);
+ if (!httpUrl) {
+ throw new Error(`No HTTP URL available for ${file["url"]}`);
+ }
+ return {
+ info: file,
+ httpUrl: httpUrl
+ };
+ }
+
+ /**
+ * Gets the event the file points to.
+ * @returns Promise which resolves to the file's event.
+ */
+ async getFileEvent() {
+ const room = this.client.getRoom(this.roomId);
+ if (!room) throw new Error("Unknown room");
+ let event = room.getUnfilteredTimelineSet().findEventById(this.id);
+
+ // keep scrolling back if needed until we find the event or reach the start of the room:
+ while (!event && room.getLiveTimeline().getState(_eventTimeline.EventTimeline.BACKWARDS).paginationToken) {
+ await this.client.scrollback(room, 100);
+ event = room.getUnfilteredTimelineSet().findEventById(this.id);
+ }
+ if (!event) throw new Error("Failed to find event");
+
+ // Sometimes the event isn't decrypted for us, so do that. We specifically set `emit: true`
+ // to ensure that the relations system in the sdk will function.
+ await this.client.decryptEventIfNeeded(event, {
+ emit: true,
+ isRetry: true
+ });
+ return event;
+ }
+
+ /**
+ * Creates a new version of this file with contents in a type that is compatible with MatrixClient.uploadContent().
+ * @param name - The name of the file.
+ * @param encryptedContents - The encrypted contents.
+ * @param info - The encrypted file information.
+ * @param additionalContent - Optional event content fields to include in the message.
+ * @returns Promise which resolves to the file event's sent response.
+ */
+ async createNewVersion(name, encryptedContents, info, additionalContent) {
+ const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, _objectSpread(_objectSpread({}, additionalContent ?? {}), {}, {
+ "m.new_content": true,
+ "m.relates_to": {
+ rel_type: _event.RelationType.Replace,
+ event_id: this.id
+ }
+ }));
+
+ // Update the version of the new event
+ await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, {
+ active: true,
+ name: name,
+ version: this.version + 1
+ }, fileEventResponse["event_id"]);
+
+ // Deprecate ourselves
+ await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, _objectSpread(_objectSpread({}, this.indexEvent.getContent()), {}, {
+ active: false
+ }), this.id);
+ return fileEventResponse;
+ }
+
+ /**
+ * Gets the file's version history, starting at this file.
+ * @returns Promise which resolves to the file's version history, with the
+ * first element being the current version and the last element being the first version.
+ */
+ async getVersionHistory() {
+ const fileHistory = [];
+ fileHistory.push(this); // start with ourselves
+
+ const room = this.client.getRoom(this.roomId);
+ if (!room) throw new Error("Invalid or unknown room");
+
+ // Clone the timeline to reverse it, getting most-recent-first ordering, hopefully
+ // shortening the awful loop below. Without the clone, we can unintentionally mutate
+ // the timeline.
+ const timelineEvents = [...room.getLiveTimeline().getEvents()].reverse();
+
+ // XXX: This is a very inefficient search, but it's the best we can do with the
+ // relations structure we have in the SDK. As of writing, it is not worth the
+ // investment in improving the structure.
+ let childEvent;
+ let parentEvent = await this.getFileEvent();
+ do {
+ childEvent = timelineEvents.find(e => e.replacingEventId() === parentEvent.getId());
+ if (childEvent) {
+ const branch = this.directory.getFile(childEvent.getId());
+ if (branch) {
+ fileHistory.push(branch);
+ parentEvent = childEvent;
+ } else {
+ break; // prevent infinite loop
+ }
+ }
+ } while (childEvent);
+ return fileHistory;
+ }
+}
+exports.MSC3089Branch = MSC3089Branch; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089TreeSpace.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089TreeSpace.js
new file mode 100644
index 0000000000..e73ef3f12a
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/MSC3089TreeSpace.js
@@ -0,0 +1,508 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.TreePermissions = exports.MSC3089TreeSpace = exports.DEFAULT_TREE_POWER_LEVELS_TEMPLATE = void 0;
+var _pRetry = _interopRequireDefault(require("p-retry"));
+var _event = require("../@types/event");
+var _logger = require("../logger");
+var _utils = require("../utils");
+var _MSC3089Branch = require("./MSC3089Branch");
+var _megolm = require("../crypto/algorithms/megolm");
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+/**
+ * The recommended defaults for a tree space's power levels. Note that this
+ * is UNSTABLE and subject to breaking changes without notice.
+ */
+const DEFAULT_TREE_POWER_LEVELS_TEMPLATE = {
+ // Owner
+ invite: 100,
+ kick: 100,
+ ban: 100,
+ // Editor
+ redact: 50,
+ state_default: 50,
+ events_default: 50,
+ // Viewer
+ users_default: 0,
+ // Mixed
+ events: {
+ [_event.EventType.RoomPowerLevels]: 100,
+ [_event.EventType.RoomHistoryVisibility]: 100,
+ [_event.EventType.RoomTombstone]: 100,
+ [_event.EventType.RoomEncryption]: 100,
+ [_event.EventType.RoomName]: 50,
+ [_event.EventType.RoomMessage]: 50,
+ [_event.EventType.RoomMessageEncrypted]: 50,
+ [_event.EventType.Sticker]: 50
+ },
+ users: {} // defined by calling code
+};
+
+/**
+ * Ease-of-use representation for power levels represented as simple roles.
+ * Note that this is UNSTABLE and subject to breaking changes without notice.
+ */
+exports.DEFAULT_TREE_POWER_LEVELS_TEMPLATE = DEFAULT_TREE_POWER_LEVELS_TEMPLATE;
+let TreePermissions = /*#__PURE__*/function (TreePermissions) {
+ TreePermissions["Viewer"] = "viewer";
+ TreePermissions["Editor"] = "editor";
+ TreePermissions["Owner"] = "owner";
+ return TreePermissions;
+}({}); // "Admin" or PL100
+/**
+ * Represents a [MSC3089](https://github.com/matrix-org/matrix-doc/pull/3089)
+ * file tree Space. Note that this is UNSTABLE and subject to breaking changes
+ * without notice.
+ */
+exports.TreePermissions = TreePermissions;
+class MSC3089TreeSpace {
+ constructor(client, roomId) {
+ this.client = client;
+ this.roomId = roomId;
+ _defineProperty(this, "room", void 0);
+ this.room = this.client.getRoom(this.roomId);
+ if (!this.room) throw new Error("Unknown room");
+ }
+
+ /**
+ * Syntactic sugar for room ID of the Space.
+ */
+ get id() {
+ return this.roomId;
+ }
+
+ /**
+ * Whether or not this is a top level space.
+ */
+ get isTopLevel() {
+ // XXX: This is absolutely not how you find out if the space is top level
+ // but is safe for a managed usecase like we offer in the SDK.
+ const parentEvents = this.room.currentState.getStateEvents(_event.EventType.SpaceParent);
+ if (!parentEvents?.length) return true;
+ return parentEvents.every(e => !e.getContent()?.["via"]);
+ }
+
+ /**
+ * Sets the name of the tree space.
+ * @param name - The new name for the space.
+ * @returns Promise which resolves when complete.
+ */
+ async setName(name) {
+ await this.client.sendStateEvent(this.roomId, _event.EventType.RoomName, {
+ name
+ }, "");
+ }
+
+ /**
+ * Invites a user to the tree space. They will be given the default Viewer
+ * permission level unless specified elsewhere.
+ * @param userId - The user ID to invite.
+ * @param andSubspaces - True (default) to invite the user to all
+ * directories/subspaces too, recursively.
+ * @param shareHistoryKeys - True (default) to share encryption keys
+ * with the invited user. This will allow them to decrypt the events (files)
+ * in the tree. Keys will not be shared if the room is lacking appropriate
+ * history visibility (by default, history visibility is "shared" in trees,
+ * which is an appropriate visibility for these purposes).
+ * @returns Promise which resolves when complete.
+ */
+ async invite(userId, andSubspaces = true, shareHistoryKeys = true) {
+ const promises = [this.retryInvite(userId)];
+ if (andSubspaces) {
+ promises.push(...this.getDirectories().map(d => d.invite(userId, andSubspaces, shareHistoryKeys)));
+ }
+ return Promise.all(promises).then(() => {
+ // Note: key sharing is default on because for file trees it is relatively important that the invite
+ // target can actually decrypt the files. The implied use case is that by inviting a user to the tree
+ // it means the sender would like the receiver to view/download the files contained within, much like
+ // sharing a folder in other circles.
+ if (shareHistoryKeys && (0, _megolm.isRoomSharedHistory)(this.room)) {
+ // noinspection JSIgnoredPromiseFromCall - we aren't concerned as much if this fails.
+ this.client.sendSharedHistoryKeys(this.roomId, [userId]);
+ }
+ });
+ }
+ retryInvite(userId) {
+ return (0, _utils.simpleRetryOperation)(async () => {
+ await this.client.invite(this.roomId, userId).catch(e => {
+ // We don't want to retry permission errors forever...
+ if (e?.errcode === "M_FORBIDDEN") {
+ throw new _pRetry.default.AbortError(e);
+ }
+ throw e;
+ });
+ });
+ }
+
+ /**
+ * Sets the permissions of a user to the given role. Note that if setting a user
+ * to Owner then they will NOT be able to be demoted. If the user does not have
+ * permission to change the power level of the target, an error will be thrown.
+ * @param userId - The user ID to change the role of.
+ * @param role - The role to assign.
+ * @returns Promise which resolves when complete.
+ */
+ async setPermissions(userId, role) {
+ const currentPls = this.room.currentState.getStateEvents(_event.EventType.RoomPowerLevels, "");
+ if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
+ const pls = currentPls?.getContent() || {};
+ const viewLevel = pls["users_default"] || 0;
+ const editLevel = pls["events_default"] || 50;
+ const adminLevel = pls["events"]?.[_event.EventType.RoomPowerLevels] || 100;
+ const users = pls["users"] || {};
+ switch (role) {
+ case TreePermissions.Viewer:
+ users[userId] = viewLevel;
+ break;
+ case TreePermissions.Editor:
+ users[userId] = editLevel;
+ break;
+ case TreePermissions.Owner:
+ users[userId] = adminLevel;
+ break;
+ default:
+ throw new Error("Invalid role: " + role);
+ }
+ pls["users"] = users;
+ await this.client.sendStateEvent(this.roomId, _event.EventType.RoomPowerLevels, pls, "");
+ }
+
+ /**
+ * Gets the current permissions of a user. Note that any users missing explicit permissions (or not
+ * in the space) will be considered Viewers. Appropriate membership checks need to be performed
+ * elsewhere.
+ * @param userId - The user ID to check permissions of.
+ * @returns The permissions for the user, defaulting to Viewer.
+ */
+ getPermissions(userId) {
+ const currentPls = this.room.currentState.getStateEvents(_event.EventType.RoomPowerLevels, "");
+ if (Array.isArray(currentPls)) throw new Error("Unexpected return type for power levels");
+ const pls = currentPls?.getContent() || {};
+ const viewLevel = pls["users_default"] || 0;
+ const editLevel = pls["events_default"] || 50;
+ const adminLevel = pls["events"]?.[_event.EventType.RoomPowerLevels] || 100;
+ const userLevel = pls["users"]?.[userId] || viewLevel;
+ if (userLevel >= adminLevel) return TreePermissions.Owner;
+ if (userLevel >= editLevel) return TreePermissions.Editor;
+ return TreePermissions.Viewer;
+ }
+
+ /**
+ * Creates a directory under this tree space, represented as another tree space.
+ * @param name - The name for the directory.
+ * @returns Promise which resolves to the created directory.
+ */
+ async createDirectory(name) {
+ const directory = await this.client.unstableCreateFileTree(name);
+ await this.client.sendStateEvent(this.roomId, _event.EventType.SpaceChild, {
+ via: [this.client.getDomain()]
+ }, directory.roomId);
+ await this.client.sendStateEvent(directory.roomId, _event.EventType.SpaceParent, {
+ via: [this.client.getDomain()]
+ }, this.roomId);
+ return directory;
+ }
+
+ /**
+ * Gets a list of all known immediate subdirectories to this tree space.
+ * @returns The tree spaces (directories). May be empty, but not null.
+ */
+ getDirectories() {
+ const trees = [];
+ const children = this.room.currentState.getStateEvents(_event.EventType.SpaceChild);
+ for (const child of children) {
+ try {
+ const stateKey = child.getStateKey();
+ if (stateKey) {
+ const tree = this.client.unstableGetFileTreeSpace(stateKey);
+ if (tree) trees.push(tree);
+ }
+ } catch (e) {
+ _logger.logger.warn("Unable to create tree space instance for listing. Are we joined?", e);
+ }
+ }
+ return trees;
+ }
+
+ /**
+ * Gets a subdirectory of a given ID under this tree space. Note that this will not recurse
+ * into children and instead only look one level deep.
+ * @param roomId - The room ID (directory ID) to find.
+ * @returns The directory, or undefined if not found.
+ */
+ getDirectory(roomId) {
+ return this.getDirectories().find(r => r.roomId === roomId);
+ }
+
+ /**
+ * Deletes the tree, kicking all members and deleting **all subdirectories**.
+ * @returns Promise which resolves when complete.
+ */
+ async delete() {
+ const subdirectories = this.getDirectories();
+ for (const dir of subdirectories) {
+ await dir.delete();
+ }
+ const kickMemberships = ["invite", "knock", "join"];
+ const members = this.room.currentState.getStateEvents(_event.EventType.RoomMember);
+ for (const member of members) {
+ const isNotUs = member.getStateKey() !== this.client.getUserId();
+ if (isNotUs && kickMemberships.includes(member.getContent().membership)) {
+ const stateKey = member.getStateKey();
+ if (!stateKey) {
+ throw new Error("State key not found for branch");
+ }
+ await this.client.kick(this.roomId, stateKey, "Room deleted");
+ }
+ }
+ await this.client.leave(this.roomId);
+ }
+ getOrderedChildren(children) {
+ const ordered = children.map(c => ({
+ roomId: c.getStateKey(),
+ order: c.getContent()["order"]
+ })).filter(c => c.roomId);
+ ordered.sort((a, b) => {
+ if (a.order && !b.order) {
+ return -1;
+ } else if (!a.order && b.order) {
+ return 1;
+ } else if (!a.order && !b.order) {
+ const roomA = this.client.getRoom(a.roomId);
+ const roomB = this.client.getRoom(b.roomId);
+ if (!roomA || !roomB) {
+ // just don't bother trying to do more partial sorting
+ return (0, _utils.lexicographicCompare)(a.roomId, b.roomId);
+ }
+ const createTsA = roomA.currentState.getStateEvents(_event.EventType.RoomCreate, "")?.getTs() ?? 0;
+ const createTsB = roomB.currentState.getStateEvents(_event.EventType.RoomCreate, "")?.getTs() ?? 0;
+ if (createTsA === createTsB) {
+ return (0, _utils.lexicographicCompare)(a.roomId, b.roomId);
+ }
+ return createTsA - createTsB;
+ } else {
+ // both not-null orders
+ return (0, _utils.lexicographicCompare)(a.order, b.order);
+ }
+ });
+ return ordered;
+ }
+ getParentRoom() {
+ const parents = this.room.currentState.getStateEvents(_event.EventType.SpaceParent);
+ const parent = parents[0]; // XXX: Wild assumption
+ if (!parent) throw new Error("Expected to have a parent in a non-top level space");
+
+ // XXX: We are assuming the parent is a valid tree space.
+ // We probably don't need to validate the parent room state for this usecase though.
+ const stateKey = parent.getStateKey();
+ if (!stateKey) throw new Error("No state key found for parent");
+ const parentRoom = this.client.getRoom(stateKey);
+ if (!parentRoom) throw new Error("Unable to locate room for parent");
+ return parentRoom;
+ }
+
+ /**
+ * Gets the current order index for this directory. Note that if this is the top level space
+ * then -1 will be returned.
+ * @returns The order index of this space.
+ */
+ getOrder() {
+ if (this.isTopLevel) return -1;
+ const parentRoom = this.getParentRoom();
+ const children = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild);
+ const ordered = this.getOrderedChildren(children);
+ return ordered.findIndex(c => c.roomId === this.roomId);
+ }
+
+ /**
+ * Sets the order index for this directory within its parent. Note that if this is a top level
+ * space then an error will be thrown. -1 can be used to move the child to the start, and numbers
+ * larger than the number of children can be used to move the child to the end.
+ * @param index - The new order index for this space.
+ * @returns Promise which resolves when complete.
+ * @throws Throws if this is a top level space.
+ */
+ async setOrder(index) {
+ if (this.isTopLevel) throw new Error("Cannot set order of top level spaces currently");
+ const parentRoom = this.getParentRoom();
+ const children = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild);
+ const ordered = this.getOrderedChildren(children);
+ index = Math.max(Math.min(index, ordered.length - 1), 0);
+ const currentIndex = this.getOrder();
+ const movingUp = currentIndex < index;
+ if (movingUp && index === ordered.length - 1) {
+ index--;
+ } else if (!movingUp && index === 0) {
+ index++;
+ }
+ const prev = ordered[movingUp ? index : index - 1];
+ const next = ordered[movingUp ? index + 1 : index];
+ let newOrder = _utils.DEFAULT_ALPHABET[0];
+ let ensureBeforeIsSane = false;
+ if (!prev) {
+ // Move to front
+ if (next?.order) {
+ newOrder = (0, _utils.prevString)(next.order);
+ }
+ } else if (index === ordered.length - 1) {
+ // Move to back
+ if (next?.order) {
+ newOrder = (0, _utils.nextString)(next.order);
+ }
+ } else {
+ // Move somewhere in the middle
+ const startOrder = prev?.order;
+ const endOrder = next?.order;
+ if (startOrder && endOrder) {
+ if (startOrder === endOrder) {
+ // Error case: just move +1 to break out of awful math
+ newOrder = (0, _utils.nextString)(startOrder);
+ } else {
+ newOrder = (0, _utils.averageBetweenStrings)(startOrder, endOrder);
+ }
+ } else {
+ if (startOrder) {
+ // We're at the end (endOrder is null, so no explicit order)
+ newOrder = (0, _utils.nextString)(startOrder);
+ } else if (endOrder) {
+ // We're at the start (startOrder is null, so nothing before us)
+ newOrder = (0, _utils.prevString)(endOrder);
+ } else {
+ // Both points are unknown. We're likely in a range where all the children
+ // don't have particular order values, so we may need to update them too.
+ // The other possibility is there's only us as a child, but we should have
+ // shown up in the other states.
+ ensureBeforeIsSane = true;
+ }
+ }
+ }
+ if (ensureBeforeIsSane) {
+ // We were asked by the order algorithm to prepare the moving space for a landing
+ // in the undefined order part of the order array, which means we need to update the
+ // spaces that come before it with a stable order value.
+ let lastOrder;
+ for (let i = 0; i <= index; i++) {
+ const target = ordered[i];
+ if (i === 0) {
+ lastOrder = target.order;
+ }
+ if (!target.order) {
+ // XXX: We should be creating gaps to avoid conflicts
+ lastOrder = lastOrder ? (0, _utils.nextString)(lastOrder) : _utils.DEFAULT_ALPHABET[0];
+ const currentChild = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild, target.roomId);
+ const content = currentChild?.getContent() ?? {
+ via: [this.client.getDomain()]
+ };
+ await this.client.sendStateEvent(parentRoom.roomId, _event.EventType.SpaceChild, _objectSpread(_objectSpread({}, content), {}, {
+ order: lastOrder
+ }), target.roomId);
+ } else {
+ lastOrder = target.order;
+ }
+ }
+ if (lastOrder) {
+ newOrder = (0, _utils.nextString)(lastOrder);
+ }
+ }
+
+ // TODO: Deal with order conflicts by reordering
+
+ // Now we can finally update our own order state
+ const currentChild = parentRoom.currentState.getStateEvents(_event.EventType.SpaceChild, this.roomId);
+ const content = currentChild?.getContent() ?? {
+ via: [this.client.getDomain()]
+ };
+ await this.client.sendStateEvent(parentRoom.roomId, _event.EventType.SpaceChild, _objectSpread(_objectSpread({}, content), {}, {
+ // TODO: Safely constrain to 50 character limit required by spaces.
+ order: newOrder
+ }), this.roomId);
+ }
+
+ /**
+ * Creates (uploads) a new file to this tree. The file must have already been encrypted for the room.
+ * The file contents are in a type that is compatible with MatrixClient.uploadContent().
+ * @param name - The name of the file.
+ * @param encryptedContents - The encrypted contents.
+ * @param info - The encrypted file information.
+ * @param additionalContent - Optional event content fields to include in the message.
+ * @returns Promise which resolves to the file event's sent response.
+ */
+ async createFile(name, encryptedContents, info, additionalContent) {
+ const {
+ content_uri: mxc
+ } = await this.client.uploadContent(encryptedContents, {
+ includeFilename: false
+ });
+ info.url = mxc;
+ const fileContent = {
+ msgtype: _event.MsgType.File,
+ body: name,
+ url: mxc,
+ file: info
+ };
+ additionalContent = additionalContent ?? {};
+ if (additionalContent["m.new_content"]) {
+ // We do the right thing according to the spec, but due to how relations are
+ // handled we also end up duplicating this information to the regular `content`
+ // as well.
+ additionalContent["m.new_content"] = fileContent;
+ }
+ const res = await this.client.sendMessage(this.roomId, _objectSpread(_objectSpread(_objectSpread({}, additionalContent), fileContent), {}, {
+ [_event.UNSTABLE_MSC3089_LEAF.name]: {}
+ }));
+ await this.client.sendStateEvent(this.roomId, _event.UNSTABLE_MSC3089_BRANCH.name, {
+ active: true,
+ name: name
+ }, res["event_id"]);
+ return res;
+ }
+
+ /**
+ * Retrieves a file from the tree.
+ * @param fileEventId - The event ID of the file.
+ * @returns The file, or null if not found.
+ */
+ getFile(fileEventId) {
+ const branch = this.room.currentState.getStateEvents(_event.UNSTABLE_MSC3089_BRANCH.name, fileEventId);
+ return branch ? new _MSC3089Branch.MSC3089Branch(this.client, branch, this) : null;
+ }
+
+ /**
+ * Gets an array of all known files for the tree.
+ * @returns The known files. May be empty, but not null.
+ */
+ listFiles() {
+ return this.listAllFiles().filter(b => b.isActive);
+ }
+
+ /**
+ * Gets an array of all known files for the tree, including inactive/invalid ones.
+ * @returns The known files. May be empty, but not null.
+ */
+ listAllFiles() {
+ const branches = this.room.currentState.getStateEvents(_event.UNSTABLE_MSC3089_BRANCH.name) ?? [];
+ return branches.map(e => new _MSC3089Branch.MSC3089Branch(this.client, e, this));
+ }
+}
+exports.MSC3089TreeSpace = MSC3089TreeSpace; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/ToDeviceMessage.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/ToDeviceMessage.js
new file mode 100644
index 0000000000..430afc16cd
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/ToDeviceMessage.js
@@ -0,0 +1,5 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+}); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/beacon.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/beacon.js
new file mode 100644
index 0000000000..6b3cf3c509
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/beacon.js
@@ -0,0 +1,181 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.isTimestampInDuration = exports.getBeaconInfoIdentifier = exports.BeaconEvent = exports.Beacon = void 0;
+var _contentHelpers = require("../content-helpers");
+var _utils = require("../utils");
+var _typedEventEmitter = require("./typed-event-emitter");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+let BeaconEvent = /*#__PURE__*/function (BeaconEvent) {
+ BeaconEvent["New"] = "Beacon.new";
+ BeaconEvent["Update"] = "Beacon.update";
+ BeaconEvent["LivenessChange"] = "Beacon.LivenessChange";
+ BeaconEvent["Destroy"] = "Beacon.Destroy";
+ BeaconEvent["LocationUpdate"] = "Beacon.LocationUpdate";
+ return BeaconEvent;
+}({});
+exports.BeaconEvent = BeaconEvent;
+const isTimestampInDuration = (startTimestamp, durationMs, timestamp) => timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
+
+// beacon info events are uniquely identified by
+// `<roomId>_<state_key>`
+exports.isTimestampInDuration = isTimestampInDuration;
+const getBeaconInfoIdentifier = event => `${event.getRoomId()}_${event.getStateKey()}`;
+
+// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
+exports.getBeaconInfoIdentifier = getBeaconInfoIdentifier;
+class Beacon extends _typedEventEmitter.TypedEventEmitter {
+ constructor(rootEvent) {
+ super();
+ this.rootEvent = rootEvent;
+ _defineProperty(this, "roomId", void 0);
+ // beaconInfo is assigned by setBeaconInfo in the constructor
+ // ! to make tsc believe it is definitely assigned
+ _defineProperty(this, "_beaconInfo", void 0);
+ _defineProperty(this, "_isLive", void 0);
+ _defineProperty(this, "livenessWatchTimeout", void 0);
+ _defineProperty(this, "_latestLocationEvent", void 0);
+ _defineProperty(this, "clearLatestLocation", () => {
+ this._latestLocationEvent = undefined;
+ this.emit(BeaconEvent.LocationUpdate, this.latestLocationState);
+ });
+ this.roomId = this.rootEvent.getRoomId();
+ this.setBeaconInfo(this.rootEvent);
+ }
+ get isLive() {
+ return !!this._isLive;
+ }
+ get identifier() {
+ return getBeaconInfoIdentifier(this.rootEvent);
+ }
+ get beaconInfoId() {
+ return this.rootEvent.getId();
+ }
+ get beaconInfoOwner() {
+ return this.rootEvent.getStateKey();
+ }
+ get beaconInfoEventType() {
+ return this.rootEvent.getType();
+ }
+ get beaconInfo() {
+ return this._beaconInfo;
+ }
+ get latestLocationState() {
+ return this._latestLocationEvent && (0, _contentHelpers.parseBeaconContent)(this._latestLocationEvent.getContent());
+ }
+ get latestLocationEvent() {
+ return this._latestLocationEvent;
+ }
+ update(beaconInfoEvent) {
+ if (getBeaconInfoIdentifier(beaconInfoEvent) !== this.identifier) {
+ throw new Error("Invalid updating event");
+ }
+ // don't update beacon with an older event
+ if (beaconInfoEvent.getTs() < this.rootEvent.getTs()) {
+ return;
+ }
+ this.rootEvent = beaconInfoEvent;
+ this.setBeaconInfo(this.rootEvent);
+ this.emit(BeaconEvent.Update, beaconInfoEvent, this);
+ this.clearLatestLocation();
+ }
+ destroy() {
+ if (this.livenessWatchTimeout) {
+ clearTimeout(this.livenessWatchTimeout);
+ }
+ this._isLive = false;
+ this.emit(BeaconEvent.Destroy, this.identifier);
+ }
+
+ /**
+ * Monitor liveness of a beacon
+ * Emits BeaconEvent.LivenessChange when beacon expires
+ */
+ monitorLiveness() {
+ if (this.livenessWatchTimeout) {
+ clearTimeout(this.livenessWatchTimeout);
+ }
+ this.checkLiveness();
+ if (!this.beaconInfo) return;
+ if (this.isLive) {
+ const expiryInMs = this.beaconInfo.timestamp + this.beaconInfo.timeout - Date.now();
+ if (expiryInMs > 1) {
+ this.livenessWatchTimeout = setTimeout(() => {
+ this.monitorLiveness();
+ }, expiryInMs);
+ }
+ } else if (this.beaconInfo.timestamp > Date.now()) {
+ // beacon start timestamp is in the future
+ // check liveness again then
+ this.livenessWatchTimeout = setTimeout(() => {
+ this.monitorLiveness();
+ }, this.beaconInfo.timestamp - Date.now());
+ }
+ }
+
+ /**
+ * Process Beacon locations
+ * Emits BeaconEvent.LocationUpdate
+ */
+ addLocations(beaconLocationEvents) {
+ // discard locations for beacons that are not live
+ if (!this.isLive) {
+ return;
+ }
+ const validLocationEvents = beaconLocationEvents.filter(event => {
+ const content = event.getContent();
+ const parsed = (0, _contentHelpers.parseBeaconContent)(content);
+ if (!parsed.uri || !parsed.timestamp) return false; // we won't be able to process these
+ const {
+ timestamp
+ } = parsed;
+ return this._beaconInfo.timestamp &&
+ // only include positions that were taken inside the beacon's live period
+ isTimestampInDuration(this._beaconInfo.timestamp, this._beaconInfo.timeout, timestamp) && (
+ // ignore positions older than our current latest location
+ !this.latestLocationState || timestamp > this.latestLocationState.timestamp);
+ });
+ const latestLocationEvent = validLocationEvents.sort(_utils.sortEventsByLatestContentTimestamp)?.[0];
+ if (latestLocationEvent) {
+ this._latestLocationEvent = latestLocationEvent;
+ this.emit(BeaconEvent.LocationUpdate, this.latestLocationState);
+ }
+ }
+ setBeaconInfo(event) {
+ this._beaconInfo = (0, _contentHelpers.parseBeaconInfoContent)(event.getContent());
+ this.checkLiveness();
+ }
+ checkLiveness() {
+ const prevLiveness = this.isLive;
+
+ // element web sets a beacon's start timestamp to the senders local current time
+ // when Alice's system clock deviates slightly from Bob's a beacon Alice intended to be live
+ // may have a start timestamp in the future from Bob's POV
+ // handle this by adding 6min of leniency to the start timestamp when it is in the future
+ if (!this.beaconInfo) return;
+ const startTimestamp = this.beaconInfo.timestamp > Date.now() ? this.beaconInfo.timestamp - 360000 /* 6min */ : this.beaconInfo.timestamp;
+ this._isLive = !!this._beaconInfo.live && !!startTimestamp && isTimestampInDuration(startTimestamp, this._beaconInfo.timeout, Date.now());
+ if (prevLiveness !== this.isLive) {
+ this.emit(BeaconEvent.LivenessChange, this.isLive, this);
+ }
+ }
+}
+exports.Beacon = Beacon; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/device.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/device.js
new file mode 100644
index 0000000000..fb91d0865a
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/device.js
@@ -0,0 +1,80 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.DeviceVerification = exports.Device = void 0;
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
+/*
+Copyright 2023 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.
+*/
+/** State of the verification of the device. */
+let DeviceVerification = /*#__PURE__*/function (DeviceVerification) {
+ DeviceVerification[DeviceVerification["Blocked"] = -1] = "Blocked";
+ DeviceVerification[DeviceVerification["Unverified"] = 0] = "Unverified";
+ DeviceVerification[DeviceVerification["Verified"] = 1] = "Verified";
+ return DeviceVerification;
+}({});
+/** A map from user ID to device ID to Device */
+exports.DeviceVerification = DeviceVerification;
+/**
+ * Information on a user's device, as returned by {@link Crypto.CryptoApi.getUserDeviceInfo}.
+ */
+class Device {
+ constructor(opts) {
+ /** id of the device */
+ _defineProperty(this, "deviceId", void 0);
+ /** id of the user that owns the device */
+ _defineProperty(this, "userId", void 0);
+ /** list of algorithms supported by this device */
+ _defineProperty(this, "algorithms", void 0);
+ /** a map from `<key type>:<id> -> <base64-encoded key>` */
+ _defineProperty(this, "keys", void 0);
+ /** whether the device has been verified/blocked by the user */
+ _defineProperty(this, "verified", void 0);
+ /** a map `<userId, map<algorithm:device_id, signature>>` */
+ _defineProperty(this, "signatures", void 0);
+ /** display name of the device */
+ _defineProperty(this, "displayName", void 0);
+ this.deviceId = opts.deviceId;
+ this.userId = opts.userId;
+ this.algorithms = opts.algorithms;
+ this.keys = opts.keys;
+ this.verified = opts.verified || DeviceVerification.Unverified;
+ this.signatures = opts.signatures || new Map();
+ this.displayName = opts.displayName;
+ }
+
+ /**
+ * Get the fingerprint for this device (ie, the Ed25519 key)
+ *
+ * @returns base64-encoded fingerprint of this device
+ */
+ getFingerprint() {
+ return this.keys.get(`ed25519:${this.deviceId}`);
+ }
+
+ /**
+ * Get the identity key for this device (ie, the Curve25519 key)
+ *
+ * @returns base64-encoded identity key of this device
+ */
+ getIdentityKey() {
+ return this.keys.get(`curve25519:${this.deviceId}`);
+ }
+}
+exports.Device = Device; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-context.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-context.js
new file mode 100644
index 0000000000..29f9223674
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-context.js
@@ -0,0 +1,116 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.EventContext = void 0;
+var _eventTimeline = require("./event-timeline");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2015 - 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.
+ */
+class EventContext {
+ /**
+ * Construct a new EventContext
+ *
+ * An eventcontext is used for circumstances such as search results, when we
+ * have a particular event of interest, and a bunch of events before and after
+ * it.
+ *
+ * It also stores pagination tokens for going backwards and forwards in the
+ * timeline.
+ *
+ * @param ourEvent - the event at the centre of this context
+ */
+ constructor(ourEvent) {
+ this.ourEvent = ourEvent;
+ _defineProperty(this, "timeline", void 0);
+ _defineProperty(this, "ourEventIndex", 0);
+ _defineProperty(this, "paginateTokens", {
+ [_eventTimeline.Direction.Backward]: null,
+ [_eventTimeline.Direction.Forward]: null
+ });
+ this.timeline = [ourEvent];
+ }
+
+ /**
+ * Get the main event of interest
+ *
+ * This is a convenience function for getTimeline()[getOurEventIndex()].
+ *
+ * @returns The event at the centre of this context.
+ */
+ getEvent() {
+ return this.timeline[this.ourEventIndex];
+ }
+
+ /**
+ * Get the list of events in this context
+ *
+ * @returns An array of MatrixEvents
+ */
+ getTimeline() {
+ return this.timeline;
+ }
+
+ /**
+ * Get the index in the timeline of our event
+ */
+ getOurEventIndex() {
+ return this.ourEventIndex;
+ }
+
+ /**
+ * Get a pagination token.
+ *
+ * @param backwards - true to get the pagination token for going
+ */
+ getPaginateToken(backwards = false) {
+ return this.paginateTokens[backwards ? _eventTimeline.Direction.Backward : _eventTimeline.Direction.Forward];
+ }
+
+ /**
+ * Set a pagination token.
+ *
+ * Generally this will be used only by the matrix js sdk.
+ *
+ * @param token - pagination token
+ * @param backwards - true to set the pagination token for going
+ * backwards in time
+ */
+ setPaginateToken(token, backwards = false) {
+ this.paginateTokens[backwards ? _eventTimeline.Direction.Backward : _eventTimeline.Direction.Forward] = token ?? null;
+ }
+
+ /**
+ * Add more events to the timeline
+ *
+ * @param events - new events, in timeline order
+ * @param atStart - true to insert new events at the start
+ */
+ addEvents(events, atStart = false) {
+ // TODO: should we share logic with Room.addEventsToTimeline?
+ // Should Room even use EventContext?
+
+ if (atStart) {
+ this.timeline = events.concat(this.timeline);
+ this.ourEventIndex += events.length;
+ } else {
+ this.timeline = this.timeline.concat(events);
+ }
+ }
+}
+exports.EventContext = EventContext; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-status.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-status.js
new file mode 100644
index 0000000000..5618c09aca
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-status.js
@@ -0,0 +1,35 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.EventStatus = void 0;
+/*
+Copyright 2015 - 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.
+*/
+/**
+ * Enum for event statuses.
+ * @readonly
+ */
+let EventStatus = /*#__PURE__*/function (EventStatus) {
+ EventStatus["NOT_SENT"] = "not_sent";
+ EventStatus["ENCRYPTING"] = "encrypting";
+ EventStatus["SENDING"] = "sending";
+ EventStatus["QUEUED"] = "queued";
+ EventStatus["SENT"] = "sent";
+ EventStatus["CANCELLED"] = "cancelled";
+ return EventStatus;
+}({});
+exports.EventStatus = EventStatus; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js
new file mode 100644
index 0000000000..3452bbfaee
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline-set.js
@@ -0,0 +1,809 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.EventTimelineSet = exports.DuplicateStrategy = void 0;
+var _eventTimeline = require("./event-timeline");
+var _logger = require("../logger");
+var _room = require("./room");
+var _typedEventEmitter = require("./typed-event-emitter");
+var _relationsContainer = require("./relations-container");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2016 - 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.
+ */
+const DEBUG = true;
+
+/* istanbul ignore next */
+let debuglog;
+if (DEBUG) {
+ // using bind means that we get to keep useful line numbers in the console
+ debuglog = _logger.logger.log.bind(_logger.logger);
+} else {
+ /* istanbul ignore next */
+ debuglog = function () {};
+}
+let DuplicateStrategy = /*#__PURE__*/function (DuplicateStrategy) {
+ DuplicateStrategy["Ignore"] = "ignore";
+ DuplicateStrategy["Replace"] = "replace";
+ return DuplicateStrategy;
+}({});
+exports.DuplicateStrategy = DuplicateStrategy;
+class EventTimelineSet extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * Construct a set of EventTimeline objects, typically on behalf of a given
+ * room. A room may have multiple EventTimelineSets for different levels
+ * of filtering. The global notification list is also an EventTimelineSet, but
+ * lacks a room.
+ *
+ * <p>This is an ordered sequence of timelines, which may or may not
+ * be continuous. Each timeline lists a series of events, as well as tracking
+ * the room state at the start and the end of the timeline (if appropriate).
+ * It also tracks forward and backward pagination tokens, as well as containing
+ * links to the next timeline in the sequence.
+ *
+ * <p>There is one special timeline - the 'live' timeline, which represents the
+ * timeline to which events are being added in real-time as they are received
+ * from the /sync API. Note that you should not retain references to this
+ * timeline - even if it is the current timeline right now, it may not remain
+ * so if the server gives us a timeline gap in /sync.
+ *
+ * <p>In order that we can find events from their ids later, we also maintain a
+ * map from event_id to timeline and index.
+ *
+ * @param room - Room for this timelineSet. May be null for non-room cases, such as the
+ * notification timeline.
+ * @param opts - Options inherited from Room.
+ * @param client - the Matrix client which owns this EventTimelineSet,
+ * can be omitted if room is specified.
+ * @param thread - the thread to which this timeline set relates.
+ * @param isThreadTimeline - Whether this timeline set relates to a thread list timeline
+ * (e.g., All threads or My threads)
+ */
+ constructor(room, opts = {}, client, thread, threadListType = null) {
+ super();
+ this.room = room;
+ this.thread = thread;
+ this.threadListType = threadListType;
+ _defineProperty(this, "relations", void 0);
+ _defineProperty(this, "timelineSupport", void 0);
+ _defineProperty(this, "displayPendingEvents", void 0);
+ _defineProperty(this, "liveTimeline", void 0);
+ _defineProperty(this, "timelines", void 0);
+ _defineProperty(this, "_eventIdToTimeline", new Map());
+ _defineProperty(this, "filter", void 0);
+ this.timelineSupport = Boolean(opts.timelineSupport);
+ this.liveTimeline = new _eventTimeline.EventTimeline(this);
+ this.displayPendingEvents = opts.pendingEvents !== false;
+
+ // just a list - *not* ordered.
+ this.timelines = [this.liveTimeline];
+ this._eventIdToTimeline = new Map();
+ this.filter = opts.filter;
+ this.relations = this.room?.relations ?? new _relationsContainer.RelationsContainer(room?.client ?? client);
+ }
+
+ /**
+ * Get all the timelines in this set
+ * @returns the timelines in this set
+ */
+ getTimelines() {
+ return this.timelines;
+ }
+
+ /**
+ * Get the filter object this timeline set is filtered on, if any
+ * @returns the optional filter for this timelineSet
+ */
+ getFilter() {
+ return this.filter;
+ }
+
+ /**
+ * Set the filter object this timeline set is filtered on
+ * (passed to the server when paginating via /messages).
+ * @param filter - the filter for this timelineSet
+ */
+ setFilter(filter) {
+ this.filter = filter;
+ }
+
+ /**
+ * Get the list of pending sent events for this timelineSet's room, filtered
+ * by the timelineSet's filter if appropriate.
+ *
+ * @returns A list of the sent events
+ * waiting for remote echo.
+ *
+ * @throws If `opts.pendingEventOrdering` was not 'detached'
+ */
+ getPendingEvents() {
+ if (!this.room || !this.displayPendingEvents) {
+ return [];
+ }
+ return this.room.getPendingEvents();
+ }
+ /**
+ * Get the live timeline for this room.
+ *
+ * @returns live timeline
+ */
+ getLiveTimeline() {
+ return this.liveTimeline;
+ }
+
+ /**
+ * Set the live timeline for this room.
+ *
+ * @returns live timeline
+ */
+ setLiveTimeline(timeline) {
+ this.liveTimeline = timeline;
+ }
+
+ /**
+ * Return the timeline (if any) this event is in.
+ * @param eventId - the eventId being sought
+ * @returns timeline
+ */
+ eventIdToTimeline(eventId) {
+ return this._eventIdToTimeline.get(eventId);
+ }
+
+ /**
+ * Track a new event as if it were in the same timeline as an old event,
+ * replacing it.
+ * @param oldEventId - event ID of the original event
+ * @param newEventId - event ID of the replacement event
+ */
+ replaceEventId(oldEventId, newEventId) {
+ const existingTimeline = this._eventIdToTimeline.get(oldEventId);
+ if (existingTimeline) {
+ this._eventIdToTimeline.delete(oldEventId);
+ this._eventIdToTimeline.set(newEventId, existingTimeline);
+ }
+ }
+
+ /**
+ * Reset the live timeline, and start a new one.
+ *
+ * <p>This is used when /sync returns a 'limited' timeline.
+ *
+ * @param backPaginationToken - token for back-paginating the new timeline
+ * @param forwardPaginationToken - token for forward-paginating the old live timeline,
+ * if absent or null, all timelines are reset.
+ *
+ * @remarks
+ * Fires {@link RoomEvent.TimelineReset}
+ */
+ resetLiveTimeline(backPaginationToken, forwardPaginationToken) {
+ // Each EventTimeline has RoomState objects tracking the state at the start
+ // and end of that timeline. The copies at the end of the live timeline are
+ // special because they will have listeners attached to monitor changes to
+ // the current room state, so we move this RoomState from the end of the
+ // current live timeline to the end of the new one and, if necessary,
+ // replace it with a newly created one. We also make a copy for the start
+ // of the new timeline.
+
+ // if timeline support is disabled, forget about the old timelines
+ const resetAllTimelines = !this.timelineSupport || !forwardPaginationToken;
+ const oldTimeline = this.liveTimeline;
+ const newTimeline = resetAllTimelines ? oldTimeline.forkLive(_eventTimeline.EventTimeline.FORWARDS) : oldTimeline.fork(_eventTimeline.EventTimeline.FORWARDS);
+ if (resetAllTimelines) {
+ this.timelines = [newTimeline];
+ this._eventIdToTimeline = new Map();
+ } else {
+ this.timelines.push(newTimeline);
+ }
+ if (forwardPaginationToken) {
+ // Now set the forward pagination token on the old live timeline
+ // so it can be forward-paginated.
+ oldTimeline.setPaginationToken(forwardPaginationToken, _eventTimeline.EventTimeline.FORWARDS);
+ }
+
+ // make sure we set the pagination token before firing timelineReset,
+ // otherwise clients which start back-paginating will fail, and then get
+ // stuck without realising that they *can* back-paginate.
+ newTimeline.setPaginationToken(backPaginationToken ?? null, _eventTimeline.EventTimeline.BACKWARDS);
+
+ // Now we can swap the live timeline to the new one.
+ this.liveTimeline = newTimeline;
+ this.emit(_room.RoomEvent.TimelineReset, this.room, this, resetAllTimelines);
+ }
+
+ /**
+ * Get the timeline which contains the given event, if any
+ *
+ * @param eventId - event ID to look for
+ * @returns timeline containing
+ * the given event, or null if unknown
+ */
+ getTimelineForEvent(eventId) {
+ if (eventId === null || eventId === undefined) {
+ return null;
+ }
+ const res = this._eventIdToTimeline.get(eventId);
+ return res === undefined ? null : res;
+ }
+
+ /**
+ * Get an event which is stored in our timelines
+ *
+ * @param eventId - event ID to look for
+ * @returns the given event, or undefined if unknown
+ */
+ findEventById(eventId) {
+ const tl = this.getTimelineForEvent(eventId);
+ if (!tl) {
+ return undefined;
+ }
+ return tl.getEvents().find(function (ev) {
+ return ev.getId() == eventId;
+ });
+ }
+
+ /**
+ * Add a new timeline to this timeline list
+ *
+ * @returns newly-created timeline
+ */
+ addTimeline() {
+ if (!this.timelineSupport) {
+ throw new Error("timeline support is disabled. Set the 'timelineSupport'" + " parameter to true when creating MatrixClient to enable" + " it.");
+ }
+ const timeline = new _eventTimeline.EventTimeline(this);
+ this.timelines.push(timeline);
+ return timeline;
+ }
+
+ /**
+ * Add events to a timeline
+ *
+ * <p>Will fire "Room.timeline" for each event added.
+ *
+ * @param events - A list of events to add.
+ *
+ * @param toStartOfTimeline - True to add these events to the start
+ * (oldest) instead of the end (newest) of the timeline. If true, the oldest
+ * event will be the <b>last</b> element of 'events'.
+ *
+ * @param timeline - timeline to
+ * add events to.
+ *
+ * @param paginationToken - token for the next batch of events
+ *
+ * @remarks
+ * Fires {@link RoomEvent.Timeline}
+ *
+ */
+ addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken) {
+ if (!timeline) {
+ throw new Error("'timeline' not specified for EventTimelineSet.addEventsToTimeline");
+ }
+ if (!toStartOfTimeline && timeline == this.liveTimeline) {
+ throw new Error("EventTimelineSet.addEventsToTimeline cannot be used for adding events to " + "the live timeline - use Room.addLiveEvents instead");
+ }
+ if (this.filter) {
+ events = this.filter.filterRoomTimeline(events);
+ if (!events.length) {
+ return;
+ }
+ }
+ const direction = toStartOfTimeline ? _eventTimeline.EventTimeline.BACKWARDS : _eventTimeline.EventTimeline.FORWARDS;
+ const inverseDirection = toStartOfTimeline ? _eventTimeline.EventTimeline.FORWARDS : _eventTimeline.EventTimeline.BACKWARDS;
+
+ // Adding events to timelines can be quite complicated. The following
+ // illustrates some of the corner-cases.
+ //
+ // Let's say we start by knowing about four timelines. timeline3 and
+ // timeline4 are neighbours:
+ //
+ // timeline1 timeline2 timeline3 timeline4
+ // [M] [P] [S] <------> [T]
+ //
+ // Now we paginate timeline1, and get the following events from the server:
+ // [M, N, P, R, S, T, U].
+ //
+ // 1. First, we ignore event M, since we already know about it.
+ //
+ // 2. Next, we append N to timeline 1.
+ //
+ // 3. Next, we don't add event P, since we already know about it,
+ // but we do link together the timelines. We now have:
+ //
+ // timeline1 timeline2 timeline3 timeline4
+ // [M, N] <---> [P] [S] <------> [T]
+ //
+ // 4. Now we add event R to timeline2:
+ //
+ // timeline1 timeline2 timeline3 timeline4
+ // [M, N] <---> [P, R] [S] <------> [T]
+ //
+ // Note that we have switched the timeline we are working on from
+ // timeline1 to timeline2.
+ //
+ // 5. We ignore event S, but again join the timelines:
+ //
+ // timeline1 timeline2 timeline3 timeline4
+ // [M, N] <---> [P, R] <---> [S] <------> [T]
+ //
+ // 6. We ignore event T, and the timelines are already joined, so there
+ // is nothing to do.
+ //
+ // 7. Finally, we add event U to timeline4:
+ //
+ // timeline1 timeline2 timeline3 timeline4
+ // [M, N] <---> [P, R] <---> [S] <------> [T, U]
+ //
+ // The important thing to note in the above is what happened when we
+ // already knew about a given event:
+ //
+ // - if it was appropriate, we joined up the timelines (steps 3, 5).
+ // - in any case, we started adding further events to the timeline which
+ // contained the event we knew about (steps 3, 5, 6).
+ //
+ //
+ // So much for adding events to the timeline. But what do we want to do
+ // with the pagination token?
+ //
+ // In the case above, we will be given a pagination token which tells us how to
+ // get events beyond 'U' - in this case, it makes sense to store this
+ // against timeline4. But what if timeline4 already had 'U' and beyond? in
+ // that case, our best bet is to throw away the pagination token we were
+ // given and stick with whatever token timeline4 had previously. In short,
+ // we want to only store the pagination token if the last event we receive
+ // is one we didn't previously know about.
+ //
+ // We make an exception for this if it turns out that we already knew about
+ // *all* of the events, and we weren't able to join up any timelines. When
+ // that happens, it means our existing pagination token is faulty, since it
+ // is only telling us what we already know. Rather than repeatedly
+ // paginating with the same token, we might as well use the new pagination
+ // token in the hope that we eventually work our way out of the mess.
+
+ let didUpdate = false;
+ let lastEventWasNew = false;
+ for (const event of events) {
+ const eventId = event.getId();
+ const existingTimeline = this._eventIdToTimeline.get(eventId);
+ if (!existingTimeline) {
+ // we don't know about this event yet. Just add it to the timeline.
+ this.addEventToTimeline(event, timeline, {
+ toStartOfTimeline
+ });
+ lastEventWasNew = true;
+ didUpdate = true;
+ continue;
+ }
+ lastEventWasNew = false;
+ if (existingTimeline == timeline) {
+ debuglog("Event " + eventId + " already in timeline " + timeline);
+ continue;
+ }
+ const neighbour = timeline.getNeighbouringTimeline(direction);
+ if (neighbour) {
+ // this timeline already has a neighbour in the relevant direction;
+ // let's assume the timelines are already correctly linked up, and
+ // skip over to it.
+ //
+ // there's probably some edge-case here where we end up with an
+ // event which is in a timeline a way down the chain, and there is
+ // a break in the chain somewhere. But I can't really imagine how
+ // that would happen, so I'm going to ignore it for now.
+ //
+ if (existingTimeline == neighbour) {
+ debuglog("Event " + eventId + " in neighbouring timeline - " + "switching to " + existingTimeline);
+ } else {
+ debuglog("Event " + eventId + " already in a different " + "timeline " + existingTimeline);
+ }
+ timeline = existingTimeline;
+ continue;
+ }
+
+ // time to join the timelines.
+ _logger.logger.info("Already have timeline for " + eventId + " - joining timeline " + timeline + " to " + existingTimeline);
+
+ // Variables to keep the line length limited below.
+ const existingIsLive = existingTimeline === this.liveTimeline;
+ const timelineIsLive = timeline === this.liveTimeline;
+ const backwardsIsLive = direction === _eventTimeline.EventTimeline.BACKWARDS && existingIsLive;
+ const forwardsIsLive = direction === _eventTimeline.EventTimeline.FORWARDS && timelineIsLive;
+ if (backwardsIsLive || forwardsIsLive) {
+ // The live timeline should never be spliced into a non-live position.
+ // We use independent logging to better discover the problem at a glance.
+ if (backwardsIsLive) {
+ _logger.logger.warn("Refusing to set a preceding existingTimeLine on our " + "timeline as the existingTimeLine is live (" + existingTimeline + ")");
+ }
+ if (forwardsIsLive) {
+ _logger.logger.warn("Refusing to set our preceding timeline on a existingTimeLine " + "as our timeline is live (" + timeline + ")");
+ }
+ continue; // abort splicing - try next event
+ }
+
+ timeline.setNeighbouringTimeline(existingTimeline, direction);
+ existingTimeline.setNeighbouringTimeline(timeline, inverseDirection);
+ timeline = existingTimeline;
+ didUpdate = true;
+ }
+
+ // see above - if the last event was new to us, or if we didn't find any
+ // new information, we update the pagination token for whatever
+ // timeline we ended up on.
+ if (lastEventWasNew || !didUpdate) {
+ if (direction === _eventTimeline.EventTimeline.FORWARDS && timeline === this.liveTimeline) {
+ _logger.logger.warn({
+ lastEventWasNew,
+ didUpdate
+ }); // for debugging
+ _logger.logger.warn(`Refusing to set forwards pagination token of live timeline ` + `${timeline} to ${paginationToken}`);
+ return;
+ }
+ timeline.setPaginationToken(paginationToken ?? null, direction);
+ }
+ }
+
+ /**
+ * Add an event to the end of this live timeline.
+ *
+ * @param event - Event to be added
+ * @param options - addLiveEvent options
+ */
+
+ /**
+ * @deprecated In favor of the overload with `IAddLiveEventOptions`
+ */
+
+ addLiveEvent(event, duplicateStrategyOrOpts, fromCache = false, roomState) {
+ let duplicateStrategy = duplicateStrategyOrOpts || DuplicateStrategy.Ignore;
+ let timelineWasEmpty;
+ if (typeof duplicateStrategyOrOpts === "object") {
+ ({
+ duplicateStrategy = DuplicateStrategy.Ignore,
+ fromCache = false,
+ roomState,
+ timelineWasEmpty
+ } = duplicateStrategyOrOpts);
+ } else if (duplicateStrategyOrOpts !== undefined) {
+ // Deprecation warning
+ // FIXME: Remove after 2023-06-01 (technical debt)
+ _logger.logger.warn("Overload deprecated: " + "`EventTimelineSet.addLiveEvent(event, duplicateStrategy?, fromCache?, roomState?)` " + "is deprecated in favor of the overload with " + "`EventTimelineSet.addLiveEvent(event, IAddLiveEventOptions)`");
+ }
+ if (this.filter) {
+ const events = this.filter.filterRoomTimeline([event]);
+ if (!events.length) {
+ return;
+ }
+ }
+ const timeline = this._eventIdToTimeline.get(event.getId());
+ if (timeline) {
+ if (duplicateStrategy === DuplicateStrategy.Replace) {
+ debuglog("EventTimelineSet.addLiveEvent: replacing duplicate event " + event.getId());
+ const tlEvents = timeline.getEvents();
+ for (let j = 0; j < tlEvents.length; j++) {
+ if (tlEvents[j].getId() === event.getId()) {
+ // still need to set the right metadata on this event
+ if (!roomState) {
+ roomState = timeline.getState(_eventTimeline.EventTimeline.FORWARDS);
+ }
+ _eventTimeline.EventTimeline.setEventMetadata(event, roomState, false);
+ tlEvents[j] = event;
+
+ // XXX: we need to fire an event when this happens.
+ break;
+ }
+ }
+ } else {
+ debuglog("EventTimelineSet.addLiveEvent: ignoring duplicate event " + event.getId());
+ }
+ return;
+ }
+ this.addEventToTimeline(event, this.liveTimeline, {
+ toStartOfTimeline: false,
+ fromCache,
+ roomState,
+ timelineWasEmpty
+ });
+ }
+
+ /**
+ * Add event to the given timeline, and emit Room.timeline. Assumes
+ * we have already checked we don't know about this event.
+ *
+ * Will fire "Room.timeline" for each event added.
+ *
+ * @param options - addEventToTimeline options
+ *
+ * @remarks
+ * Fires {@link RoomEvent.Timeline}
+ */
+
+ /**
+ * @deprecated In favor of the overload with `IAddEventToTimelineOptions`
+ */
+
+ addEventToTimeline(event, timeline, toStartOfTimelineOrOpts, fromCache = false, roomState) {
+ let toStartOfTimeline = !!toStartOfTimelineOrOpts;
+ let timelineWasEmpty;
+ if (typeof toStartOfTimelineOrOpts === "object") {
+ ({
+ toStartOfTimeline,
+ fromCache = false,
+ roomState,
+ timelineWasEmpty
+ } = toStartOfTimelineOrOpts);
+ } else if (toStartOfTimelineOrOpts !== undefined) {
+ // Deprecation warning
+ // FIXME: Remove after 2023-06-01 (technical debt)
+ _logger.logger.warn("Overload deprecated: " + "`EventTimelineSet.addEventToTimeline(event, timeline, toStartOfTimeline, fromCache?, roomState?)` " + "is deprecated in favor of the overload with " + "`EventTimelineSet.addEventToTimeline(event, timeline, IAddEventToTimelineOptions)`");
+ }
+ if (timeline.getTimelineSet() !== this) {
+ throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
+ "in timelineSet(threadId=${this.thread?.id})`);
+ }
+
+ // Make sure events don't get mixed in timelines they shouldn't be in (e.g. a
+ // threaded message should not be in the main timeline).
+ //
+ // We can only run this check for timelines with a `room` because `canContain`
+ // requires it
+ if (this.room && !this.canContain(event)) {
+ let eventDebugString = `event=${event.getId()}`;
+ if (event.threadRootId) {
+ eventDebugString += `(belongs to thread=${event.threadRootId})`;
+ }
+ _logger.logger.warn(`EventTimelineSet.addEventToTimeline: Ignoring ${eventDebugString} that does not belong ` + `in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`);
+ return;
+ }
+ const eventId = event.getId();
+ timeline.addEvent(event, {
+ toStartOfTimeline,
+ roomState,
+ timelineWasEmpty
+ });
+ this._eventIdToTimeline.set(eventId, timeline);
+ this.relations.aggregateParentEvent(event);
+ this.relations.aggregateChildEvent(event, this);
+ const data = {
+ timeline: timeline,
+ liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache
+ };
+ this.emit(_room.RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), false, data);
+ }
+
+ /**
+ * Insert event to the given timeline, and emit Room.timeline. Assumes
+ * we have already checked we don't know about this event.
+ *
+ * TEMPORARY: until we have recursive relations, we need this function
+ * to exist to allow us to insert events in timeline order, which is our
+ * best guess for Sync Order.
+ * This is a copy of addEventToTimeline above, modified to insert the event
+ * after the event it relates to, and before any event with a later
+ * timestamp. This is our best guess at Sync Order.
+ *
+ * Will fire "Room.timeline" for each event added.
+ *
+ * @internal
+ *
+ * @param options - addEventToTimeline options
+ *
+ * @remarks
+ * Fires {@link RoomEvent.Timeline}
+ */
+ insertEventIntoTimeline(event, timeline, roomState) {
+ if (timeline.getTimelineSet() !== this) {
+ throw new Error(`EventTimelineSet.addEventToTimeline: Timeline=${timeline.toString()} does not belong " +
+ "in timelineSet(threadId=${this.thread?.id})`);
+ }
+
+ // Make sure events don't get mixed in timelines they shouldn't be in (e.g. a
+ // threaded message should not be in the main timeline).
+ //
+ // We can only run this check for timelines with a `room` because `canContain`
+ // requires it
+ if (this.room && !this.canContain(event)) {
+ let eventDebugString = `event=${event.getId()}`;
+ if (event.threadRootId) {
+ eventDebugString += `(belongs to thread=${event.threadRootId})`;
+ }
+ _logger.logger.warn(`EventTimelineSet.addEventToTimeline: Ignoring ${eventDebugString} that does not belong ` + `in timeline=${timeline.toString()} timelineSet(threadId=${this.thread?.id})`);
+ return;
+ }
+
+ // Find the event that this event is related to - the "parent"
+ const parentEventId = event.relationEventId;
+ if (!parentEventId) {
+ // Not related to anything - we just append
+ this.addEventToTimeline(event, timeline, {
+ toStartOfTimeline: false,
+ fromCache: false,
+ timelineWasEmpty: false,
+ roomState
+ });
+ return;
+ }
+ const parentEvent = this.findEventById(parentEventId);
+ const timelineEvents = timeline.getEvents();
+
+ // Start searching from the parent event, or if it's not loaded, start
+ // at the beginning and insert purely using timestamp order.
+ const parentIndex = parentEvent !== undefined ? timelineEvents.indexOf(parentEvent) : 0;
+ let insertIndex = parentIndex;
+ for (; insertIndex < timelineEvents.length; insertIndex++) {
+ const nextEvent = timelineEvents[insertIndex];
+ if (nextEvent.getTs() > event.getTs()) {
+ // We found an event later than ours, so insert before that.
+ break;
+ }
+ }
+ // If we got to the end of the loop, insertIndex points at the end of
+ // the list.
+
+ const eventId = event.getId();
+ timeline.insertEvent(event, insertIndex, roomState);
+ this._eventIdToTimeline.set(eventId, timeline);
+ this.relations.aggregateParentEvent(event);
+ this.relations.aggregateChildEvent(event, this);
+ const data = {
+ timeline: timeline,
+ liveEvent: timeline == this.liveTimeline
+ };
+ this.emit(_room.RoomEvent.Timeline, event, this.room, false, false, data);
+ }
+
+ /**
+ * Replaces event with ID oldEventId with one with newEventId, if oldEventId is
+ * recognised. Otherwise, add to the live timeline. Used to handle remote echos.
+ *
+ * @param localEvent - the new event to be added to the timeline
+ * @param oldEventId - the ID of the original event
+ * @param newEventId - the ID of the replacement event
+ *
+ * @remarks
+ * Fires {@link RoomEvent.Timeline}
+ */
+ handleRemoteEcho(localEvent, oldEventId, newEventId) {
+ // XXX: why don't we infer newEventId from localEvent?
+ const existingTimeline = this._eventIdToTimeline.get(oldEventId);
+ if (existingTimeline) {
+ this._eventIdToTimeline.delete(oldEventId);
+ this._eventIdToTimeline.set(newEventId, existingTimeline);
+ } else if (!this.filter || this.filter.filterRoomTimeline([localEvent]).length) {
+ this.addEventToTimeline(localEvent, this.liveTimeline, {
+ toStartOfTimeline: false
+ });
+ }
+ }
+
+ /**
+ * Removes a single event from this room.
+ *
+ * @param eventId - The id of the event to remove
+ *
+ * @returns the removed event, or null if the event was not found
+ * in this room.
+ */
+ removeEvent(eventId) {
+ const timeline = this._eventIdToTimeline.get(eventId);
+ if (!timeline) {
+ return null;
+ }
+ const removed = timeline.removeEvent(eventId);
+ if (removed) {
+ this._eventIdToTimeline.delete(eventId);
+ const data = {
+ timeline: timeline
+ };
+ this.emit(_room.RoomEvent.Timeline, removed, this.room, undefined, true, data);
+ }
+ return removed;
+ }
+
+ /**
+ * Determine where two events appear in the timeline relative to one another
+ *
+ * @param eventId1 - The id of the first event
+ * @param eventId2 - The id of the second event
+ * @returns a number less than zero if eventId1 precedes eventId2, and
+ * greater than zero if eventId1 succeeds eventId2. zero if they are the
+ * same event; null if we can't tell (either because we don't know about one
+ * of the events, or because they are in separate timelines which don't join
+ * up).
+ */
+ compareEventOrdering(eventId1, eventId2) {
+ if (eventId1 == eventId2) {
+ // optimise this case
+ return 0;
+ }
+ const timeline1 = this._eventIdToTimeline.get(eventId1);
+ const timeline2 = this._eventIdToTimeline.get(eventId2);
+ if (timeline1 === undefined) {
+ return null;
+ }
+ if (timeline2 === undefined) {
+ return null;
+ }
+ if (timeline1 === timeline2) {
+ // both events are in the same timeline - figure out their relative indices
+ let idx1 = undefined;
+ let idx2 = undefined;
+ const events = timeline1.getEvents();
+ for (let idx = 0; idx < events.length && (idx1 === undefined || idx2 === undefined); idx++) {
+ const evId = events[idx].getId();
+ if (evId == eventId1) {
+ idx1 = idx;
+ }
+ if (evId == eventId2) {
+ idx2 = idx;
+ }
+ }
+ return idx1 - idx2;
+ }
+
+ // the events are in different timelines. Iterate through the
+ // linkedlist to see which comes first.
+
+ // first work forwards from timeline1
+ let tl = timeline1;
+ while (tl) {
+ if (tl === timeline2) {
+ // timeline1 is before timeline2
+ return -1;
+ }
+ tl = tl.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS);
+ }
+
+ // now try backwards from timeline1
+ tl = timeline1;
+ while (tl) {
+ if (tl === timeline2) {
+ // timeline2 is before timeline1
+ return 1;
+ }
+ tl = tl.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS);
+ }
+
+ // the timelines are not contiguous.
+ return null;
+ }
+
+ /**
+ * Determine whether a given event can sanely be added to this event timeline set,
+ * for timeline sets relating to a thread, only return true for events in the same
+ * thread timeline, for timeline sets not relating to a thread only return true
+ * for events which should be shown in the main room timeline.
+ * Requires the `room` property to have been set at EventTimelineSet construction time.
+ *
+ * @param event - the event to check whether it belongs to this timeline set.
+ * @throws Error if `room` was not set when constructing this timeline set.
+ * @returns whether the event belongs to this timeline set.
+ */
+ canContain(event) {
+ if (!this.room) {
+ throw new Error("Cannot call `EventTimelineSet::canContain without a `room` set. " + "Set the room when creating the EventTimelineSet to call this method.");
+ }
+ const {
+ threadId,
+ shouldLiveInRoom
+ } = this.room.eventShouldLiveIn(event);
+ if (this.thread) {
+ return this.thread.id === threadId;
+ }
+ return shouldLiveInRoom;
+ }
+}
+exports.EventTimelineSet = EventTimelineSet; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js
new file mode 100644
index 0000000000..d5e9b4ac40
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event-timeline.js
@@ -0,0 +1,469 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.EventTimeline = exports.Direction = void 0;
+var _logger = require("../logger");
+var _roomState = require("./room-state");
+var _event = require("../@types/event");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2016 - 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.
+ */
+let Direction = /*#__PURE__*/function (Direction) {
+ Direction["Backward"] = "b";
+ Direction["Forward"] = "f";
+ return Direction;
+}({});
+exports.Direction = Direction;
+class EventTimeline {
+ /**
+ * Static helper method to set sender and target properties
+ *
+ * @param event - the event whose metadata is to be set
+ * @param stateContext - the room state to be queried
+ * @param toStartOfTimeline - if true the event's forwardLooking flag is set false
+ */
+ static setEventMetadata(event, stateContext, toStartOfTimeline) {
+ // When we try to generate a sentinel member before we have that member
+ // in the members object, we still generate a sentinel but it doesn't
+ // have a membership event, so test to see if events.member is set. We
+ // check this to avoid overriding non-sentinel members by sentinel ones
+ // when adding the event to a filtered timeline
+ if (!event.sender?.events?.member) {
+ event.sender = stateContext.getSentinelMember(event.getSender());
+ }
+ if (!event.target?.events?.member && event.getType() === _event.EventType.RoomMember) {
+ event.target = stateContext.getSentinelMember(event.getStateKey());
+ }
+ if (event.isState()) {
+ // room state has no concept of 'old' or 'current', but we want the
+ // room state to regress back to previous values if toStartOfTimeline
+ // is set, which means inspecting prev_content if it exists. This
+ // is done by toggling the forwardLooking flag.
+ if (toStartOfTimeline) {
+ event.forwardLooking = false;
+ }
+ }
+ }
+ /**
+ * Construct a new EventTimeline
+ *
+ * <p>An EventTimeline represents a contiguous sequence of events in a room.
+ *
+ * <p>As well as keeping track of the events themselves, it stores the state of
+ * the room at the beginning and end of the timeline, and pagination tokens for
+ * going backwards and forwards in the timeline.
+ *
+ * <p>In order that clients can meaningfully maintain an index into a timeline,
+ * the EventTimeline object tracks a 'baseIndex'. This starts at zero, but is
+ * incremented when events are prepended to the timeline. The index of an event
+ * relative to baseIndex therefore remains constant.
+ *
+ * <p>Once a timeline joins up with its neighbour, they are linked together into a
+ * doubly-linked list.
+ *
+ * @param eventTimelineSet - the set of timelines this is part of
+ */
+ constructor(eventTimelineSet) {
+ this.eventTimelineSet = eventTimelineSet;
+ _defineProperty(this, "roomId", void 0);
+ _defineProperty(this, "name", void 0);
+ _defineProperty(this, "events", []);
+ _defineProperty(this, "baseIndex", 0);
+ _defineProperty(this, "startState", void 0);
+ _defineProperty(this, "endState", void 0);
+ // If we have a roomId then we delegate pagination token storage to the room state objects `startState` and
+ // `endState`, but for things like the notification timeline which mix multiple rooms we store the tokens ourselves.
+ _defineProperty(this, "startToken", null);
+ _defineProperty(this, "endToken", null);
+ _defineProperty(this, "prevTimeline", null);
+ _defineProperty(this, "nextTimeline", null);
+ _defineProperty(this, "paginationRequests", {
+ [Direction.Backward]: null,
+ [Direction.Forward]: null
+ });
+ this.roomId = eventTimelineSet.room?.roomId ?? null;
+ if (this.roomId) {
+ this.startState = new _roomState.RoomState(this.roomId);
+ this.endState = new _roomState.RoomState(this.roomId);
+ }
+
+ // this is used by client.js
+ this.paginationRequests = {
+ b: null,
+ f: null
+ };
+ this.name = this.roomId + ":" + new Date().toISOString();
+ }
+
+ /**
+ * Initialise the start and end state with the given events
+ *
+ * <p>This can only be called before any events are added.
+ *
+ * @param stateEvents - list of state events to initialise the
+ * state with.
+ * @throws Error if an attempt is made to call this after addEvent is called.
+ */
+ initialiseState(stateEvents, {
+ timelineWasEmpty
+ } = {}) {
+ if (this.events.length > 0) {
+ throw new Error("Cannot initialise state after events are added");
+ }
+ this.startState?.setStateEvents(stateEvents, {
+ timelineWasEmpty
+ });
+ this.endState?.setStateEvents(stateEvents, {
+ timelineWasEmpty
+ });
+ }
+
+ /**
+ * Forks the (live) timeline, taking ownership of the existing directional state of this timeline.
+ * All attached listeners will keep receiving state updates from the new live timeline state.
+ * The end state of this timeline gets replaced with an independent copy of the current RoomState,
+ * and will need a new pagination token if it ever needs to paginate forwards.
+ * @param direction - EventTimeline.BACKWARDS to get the state at the
+ * start of the timeline; EventTimeline.FORWARDS to get the state at the end
+ * of the timeline.
+ *
+ * @returns the new timeline
+ */
+ forkLive(direction) {
+ const forkState = this.getState(direction);
+ const timeline = new EventTimeline(this.eventTimelineSet);
+ timeline.startState = forkState?.clone();
+ // Now clobber the end state of the new live timeline with that from the
+ // previous live timeline. It will be identical except that we'll keep
+ // using the same RoomMember objects for the 'live' set of members with any
+ // listeners still attached
+ timeline.endState = forkState;
+ // Firstly, we just stole the current timeline's end state, so it needs a new one.
+ // Make an immutable copy of the state so back pagination will get the correct sentinels.
+ this.endState = forkState?.clone();
+ return timeline;
+ }
+
+ /**
+ * Creates an independent timeline, inheriting the directional state from this timeline.
+ *
+ * @param direction - EventTimeline.BACKWARDS to get the state at the
+ * start of the timeline; EventTimeline.FORWARDS to get the state at the end
+ * of the timeline.
+ *
+ * @returns the new timeline
+ */
+ fork(direction) {
+ const forkState = this.getState(direction);
+ const timeline = new EventTimeline(this.eventTimelineSet);
+ timeline.startState = forkState?.clone();
+ timeline.endState = forkState?.clone();
+ return timeline;
+ }
+
+ /**
+ * Get the ID of the room for this timeline
+ * @returns room ID
+ */
+ getRoomId() {
+ return this.roomId;
+ }
+
+ /**
+ * Get the filter for this timeline's timelineSet (if any)
+ * @returns filter
+ */
+ getFilter() {
+ return this.eventTimelineSet.getFilter();
+ }
+
+ /**
+ * Get the timelineSet for this timeline
+ * @returns timelineSet
+ */
+ getTimelineSet() {
+ return this.eventTimelineSet;
+ }
+
+ /**
+ * Get the base index.
+ *
+ * <p>This is an index which is incremented when events are prepended to the
+ * timeline. An individual event therefore stays at the same index in the array
+ * relative to the base index (although note that a given event's index may
+ * well be less than the base index, thus giving that event a negative relative
+ * index).
+ */
+ getBaseIndex() {
+ return this.baseIndex;
+ }
+
+ /**
+ * Get the list of events in this context
+ *
+ * @returns An array of MatrixEvents
+ */
+ getEvents() {
+ return this.events;
+ }
+
+ /**
+ * Get the room state at the start/end of the timeline
+ *
+ * @param direction - EventTimeline.BACKWARDS to get the state at the
+ * start of the timeline; EventTimeline.FORWARDS to get the state at the end
+ * of the timeline.
+ *
+ * @returns state at the start/end of the timeline
+ */
+ getState(direction) {
+ if (direction == EventTimeline.BACKWARDS) {
+ return this.startState;
+ } else if (direction == EventTimeline.FORWARDS) {
+ return this.endState;
+ } else {
+ throw new Error("Invalid direction '" + direction + "'");
+ }
+ }
+
+ /**
+ * Get a pagination token
+ *
+ * @param direction - EventTimeline.BACKWARDS to get the pagination
+ * token for going backwards in time; EventTimeline.FORWARDS to get the
+ * pagination token for going forwards in time.
+ *
+ * @returns pagination token
+ */
+ getPaginationToken(direction) {
+ if (this.roomId) {
+ return this.getState(direction).paginationToken;
+ } else if (direction === Direction.Backward) {
+ return this.startToken;
+ } else {
+ return this.endToken;
+ }
+ }
+
+ /**
+ * Set a pagination token
+ *
+ * @param token - pagination token
+ *
+ * @param direction - EventTimeline.BACKWARDS to set the pagination
+ * token for going backwards in time; EventTimeline.FORWARDS to set the
+ * pagination token for going forwards in time.
+ */
+ setPaginationToken(token, direction) {
+ if (this.roomId) {
+ this.getState(direction).paginationToken = token;
+ } else if (direction === Direction.Backward) {
+ this.startToken = token;
+ } else {
+ this.endToken = token;
+ }
+ }
+
+ /**
+ * Get the next timeline in the series
+ *
+ * @param direction - EventTimeline.BACKWARDS to get the previous
+ * timeline; EventTimeline.FORWARDS to get the next timeline.
+ *
+ * @returns previous or following timeline, if they have been
+ * joined up.
+ */
+ getNeighbouringTimeline(direction) {
+ if (direction == EventTimeline.BACKWARDS) {
+ return this.prevTimeline;
+ } else if (direction == EventTimeline.FORWARDS) {
+ return this.nextTimeline;
+ } else {
+ throw new Error("Invalid direction '" + direction + "'");
+ }
+ }
+
+ /**
+ * Set the next timeline in the series
+ *
+ * @param neighbour - previous/following timeline
+ *
+ * @param direction - EventTimeline.BACKWARDS to set the previous
+ * timeline; EventTimeline.FORWARDS to set the next timeline.
+ *
+ * @throws Error if an attempt is made to set the neighbouring timeline when
+ * it is already set.
+ */
+ setNeighbouringTimeline(neighbour, direction) {
+ if (this.getNeighbouringTimeline(direction)) {
+ throw new Error("timeline already has a neighbouring timeline - " + "cannot reset neighbour (direction: " + direction + ")");
+ }
+ if (direction == EventTimeline.BACKWARDS) {
+ this.prevTimeline = neighbour;
+ } else if (direction == EventTimeline.FORWARDS) {
+ this.nextTimeline = neighbour;
+ } else {
+ throw new Error("Invalid direction '" + direction + "'");
+ }
+
+ // make sure we don't try to paginate this timeline
+ this.setPaginationToken(null, direction);
+ }
+
+ /**
+ * Add a new event to the timeline, and update the state
+ *
+ * @param event - new event
+ * @param options - addEvent options
+ */
+
+ /**
+ * @deprecated In favor of the overload with `IAddEventOptions`
+ */
+
+ addEvent(event, toStartOfTimelineOrOpts, roomState) {
+ let toStartOfTimeline = !!toStartOfTimelineOrOpts;
+ let timelineWasEmpty;
+ if (typeof toStartOfTimelineOrOpts === "object") {
+ ({
+ toStartOfTimeline,
+ roomState,
+ timelineWasEmpty
+ } = toStartOfTimelineOrOpts);
+ } else if (toStartOfTimelineOrOpts !== undefined) {
+ // Deprecation warning
+ // FIXME: Remove after 2023-06-01 (technical debt)
+ _logger.logger.warn("Overload deprecated: " + "`EventTimeline.addEvent(event, toStartOfTimeline, roomState?)` " + "is deprecated in favor of the overload with `EventTimeline.addEvent(event, IAddEventOptions)`");
+ }
+ if (!roomState) {
+ roomState = toStartOfTimeline ? this.startState : this.endState;
+ }
+ const timelineSet = this.getTimelineSet();
+ if (timelineSet.room) {
+ EventTimeline.setEventMetadata(event, roomState, toStartOfTimeline);
+
+ // modify state but only on unfiltered timelineSets
+ if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
+ roomState?.setStateEvents([event], {
+ timelineWasEmpty
+ });
+ // it is possible that the act of setting the state event means we
+ // can set more metadata (specifically sender/target props), so try
+ // it again if the prop wasn't previously set. It may also mean that
+ // the sender/target is updated (if the event set was a room member event)
+ // so we want to use the *updated* member (new avatar/name) instead.
+ //
+ // However, we do NOT want to do this on member events if we're going
+ // back in time, else we'll set the .sender value for BEFORE the given
+ // member event, whereas we want to set the .sender value for the ACTUAL
+ // member event itself.
+ if (!event.sender || event.getType() === _event.EventType.RoomMember && !toStartOfTimeline) {
+ EventTimeline.setEventMetadata(event, roomState, toStartOfTimeline);
+ }
+ }
+ }
+ let insertIndex;
+ if (toStartOfTimeline) {
+ insertIndex = 0;
+ } else {
+ insertIndex = this.events.length;
+ }
+ this.events.splice(insertIndex, 0, event); // insert element
+ if (toStartOfTimeline) {
+ this.baseIndex++;
+ }
+ }
+
+ /**
+ * Insert a new event into the timeline, and update the state.
+ *
+ * TEMPORARY: until we have recursive relations, we need this function
+ * to exist to allow us to insert events in timeline order, which is our
+ * best guess for Sync Order.
+ * This is a copy of addEvent above, modified to allow inserting an event at
+ * a specific index.
+ *
+ * @internal
+ */
+ insertEvent(event, insertIndex, roomState) {
+ const timelineSet = this.getTimelineSet();
+ if (timelineSet.room) {
+ EventTimeline.setEventMetadata(event, roomState, false);
+
+ // modify state but only on unfiltered timelineSets
+ if (event.isState() && timelineSet.room.getUnfilteredTimelineSet() === timelineSet) {
+ roomState.setStateEvents([event], {});
+ // it is possible that the act of setting the state event means we
+ // can set more metadata (specifically sender/target props), so try
+ // it again if the prop wasn't previously set. It may also mean that
+ // the sender/target is updated (if the event set was a room member event)
+ // so we want to use the *updated* member (new avatar/name) instead.
+ //
+ // However, we do NOT want to do this on member events if we're going
+ // back in time, else we'll set the .sender value for BEFORE the given
+ // member event, whereas we want to set the .sender value for the ACTUAL
+ // member event itself.
+ if (!event.sender || event.getType() === _event.EventType.RoomMember) {
+ EventTimeline.setEventMetadata(event, roomState, false);
+ }
+ }
+ }
+ this.events.splice(insertIndex, 0, event); // insert element
+ }
+
+ /**
+ * Remove an event from the timeline
+ *
+ * @param eventId - ID of event to be removed
+ * @returns removed event, or null if not found
+ */
+ removeEvent(eventId) {
+ for (let i = this.events.length - 1; i >= 0; i--) {
+ const ev = this.events[i];
+ if (ev.getId() == eventId) {
+ this.events.splice(i, 1);
+ if (i < this.baseIndex) {
+ this.baseIndex--;
+ }
+ return ev;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Return a string to identify this timeline, for debugging
+ *
+ * @returns name for this timeline
+ */
+ toString() {
+ return this.name;
+ }
+}
+exports.EventTimeline = EventTimeline;
+/**
+ * Symbolic constant for methods which take a 'direction' argument:
+ * refers to the start of the timeline, or backwards in time.
+ */
+_defineProperty(EventTimeline, "BACKWARDS", Direction.Backward);
+/**
+ * Symbolic constant for methods which take a 'direction' argument:
+ * refers to the end of the timeline, or forwards in time.
+ */
+_defineProperty(EventTimeline, "FORWARDS", Direction.Forward); \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/event.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event.js
new file mode 100644
index 0000000000..eb89ee148d
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/event.js
@@ -0,0 +1,1442 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+Object.defineProperty(exports, "EventStatus", {
+ enumerable: true,
+ get: function () {
+ return _eventStatus.EventStatus;
+ }
+});
+exports.MatrixEventEvent = exports.MatrixEvent = void 0;
+var _matrixEventsSdk = require("matrix-events-sdk");
+var _logger = require("../logger");
+var _event = require("../@types/event");
+var _utils = require("../utils");
+var _thread = require("./thread");
+var _ReEmitter = require("../ReEmitter");
+var _typedEventEmitter = require("./typed-event-emitter");
+var _algorithms = require("../crypto/algorithms");
+var _OlmDevice = require("../crypto/OlmDevice");
+var _eventStatus = require("./event-status");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2015 - 2023 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.
+ */ /**
+ * This is an internal module. See {@link MatrixEvent} and {@link RoomEvent} for
+ * the public classes.
+ */
+/* eslint-disable camelcase */
+
+/**
+ * When an event is a visibility change event, as per MSC3531,
+ * the visibility change implied by the event.
+ */
+
+/* eslint-enable camelcase */
+
+/**
+ * Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
+ */
+
+/**
+ * Variant of `MessageVisibility` for the case in which the message should be displayed.
+ */
+
+/**
+ * Variant of `MessageVisibility` for the case in which the message should be hidden.
+ */
+
+// A singleton implementing `IMessageVisibilityVisible`.
+const MESSAGE_VISIBLE = Object.freeze({
+ visible: true
+});
+let MatrixEventEvent = /*#__PURE__*/function (MatrixEventEvent) {
+ MatrixEventEvent["Decrypted"] = "Event.decrypted";
+ MatrixEventEvent["BeforeRedaction"] = "Event.beforeRedaction";
+ MatrixEventEvent["VisibilityChange"] = "Event.visibilityChange";
+ MatrixEventEvent["LocalEventIdReplaced"] = "Event.localEventIdReplaced";
+ MatrixEventEvent["Status"] = "Event.status";
+ MatrixEventEvent["Replaced"] = "Event.replaced";
+ MatrixEventEvent["RelationsCreated"] = "Event.relationsCreated";
+ return MatrixEventEvent;
+}({});
+exports.MatrixEventEvent = MatrixEventEvent;
+class MatrixEvent extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * Construct a Matrix Event object
+ *
+ * @param event - The raw (possibly encrypted) event. <b>Do not access
+ * this property</b> directly unless you absolutely have to. Prefer the getter
+ * methods defined on this class. Using the getter methods shields your app
+ * from changes to event JSON between Matrix versions.
+ */
+ constructor(event = {}) {
+ super();
+
+ // intern the values of matrix events to force share strings and reduce the
+ // amount of needless string duplication. This can save moderate amounts of
+ // memory (~10% on a 350MB heap).
+ // 'membership' at the event level (rather than the content level) is a legacy
+ // field that Element never otherwise looks at, but it will still take up a lot
+ // of space if we don't intern it.
+ this.event = event;
+ // applied push rule and action for this event
+ _defineProperty(this, "pushDetails", {});
+ _defineProperty(this, "_replacingEvent", null);
+ _defineProperty(this, "_localRedactionEvent", null);
+ _defineProperty(this, "_isCancelled", false);
+ _defineProperty(this, "clearEvent", void 0);
+ /* Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
+ Note: We're returning this object, so any value stored here MUST be frozen.
+ */
+ _defineProperty(this, "visibility", MESSAGE_VISIBLE);
+ // Not all events will be extensible-event compatible, so cache a flag in
+ // addition to a falsy cached event value. We check the flag later on in
+ // a public getter to decide if the cache is valid.
+ _defineProperty(this, "_hasCachedExtEv", false);
+ _defineProperty(this, "_cachedExtEv", undefined);
+ /* curve25519 key which we believe belongs to the sender of the event. See
+ * getSenderKey()
+ */
+ _defineProperty(this, "senderCurve25519Key", null);
+ /* ed25519 key which the sender of this event (for olm) or the creator of
+ * the megolm session (for megolm) claims to own. See getClaimedEd25519Key()
+ */
+ _defineProperty(this, "claimedEd25519Key", null);
+ /* curve25519 keys of devices involved in telling us about the
+ * senderCurve25519Key and claimedEd25519Key.
+ * See getForwardingCurve25519KeyChain().
+ */
+ _defineProperty(this, "forwardingCurve25519KeyChain", []);
+ /* where the decryption key is untrusted
+ */
+ _defineProperty(this, "untrusted", null);
+ /* if we have a process decrypting this event, a Promise which resolves
+ * when it is finished. Normally null.
+ */
+ _defineProperty(this, "decryptionPromise", null);
+ /* flag to indicate if we should retry decrypting this event after the
+ * first attempt (eg, we have received new data which means that a second
+ * attempt may succeed)
+ */
+ _defineProperty(this, "retryDecryption", false);
+ /* The txnId with which this event was sent if it was during this session,
+ * allows for a unique ID which does not change when the event comes back down sync.
+ */
+ _defineProperty(this, "txnId", void 0);
+ /**
+ * A reference to the thread this event belongs to
+ */
+ _defineProperty(this, "thread", void 0);
+ _defineProperty(this, "threadId", void 0);
+ /*
+ * True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
+ * the sender has disabled encrypting to unverified devices.
+ */
+ _defineProperty(this, "encryptedDisabledForUnverifiedDevices", false);
+ /* Set an approximate timestamp for the event relative the local clock.
+ * This will inherently be approximate because it doesn't take into account
+ * the time between the server putting the 'age' field on the event as it sent
+ * it to us and the time we're now constructing this event, but that's better
+ * than assuming the local clock is in sync with the origin HS's clock.
+ */
+ _defineProperty(this, "localTimestamp", void 0);
+ /**
+ * The room member who sent this event, or null e.g.
+ * this is a presence event. This is only guaranteed to be set for events that
+ * appear in a timeline, ie. do not guarantee that it will be set on state
+ * events.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "sender", null);
+ /**
+ * The room member who is the target of this event, e.g.
+ * the invitee, the person being banned, etc.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "target", null);
+ /**
+ * The sending status of the event.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "status", null);
+ /**
+ * most recent error associated with sending the event, if any
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "error", null);
+ /**
+ * True if this event is 'forward looking', meaning
+ * that getDirectionalContent() will return event.content and not event.prev_content.
+ * Only state events may be backwards looking
+ * Default: true. <strong>This property is experimental and may change.</strong>
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "forwardLooking", true);
+ /* If the event is a `m.key.verification.request` (or to_device `m.key.verification.start`) event,
+ * `Crypto` will set this the `VerificationRequest` for the event
+ * so it can be easily accessed from the timeline.
+ */
+ _defineProperty(this, "verificationRequest", void 0);
+ _defineProperty(this, "reEmitter", void 0);
+ ["state_key", "type", "sender", "room_id", "membership"].forEach(prop => {
+ if (typeof event[prop] !== "string") return;
+ event[prop] = (0, _utils.internaliseString)(event[prop]);
+ });
+ ["membership", "avatar_url", "displayname"].forEach(prop => {
+ if (typeof event.content?.[prop] !== "string") return;
+ event.content[prop] = (0, _utils.internaliseString)(event.content[prop]);
+ });
+ ["rel_type"].forEach(prop => {
+ if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return;
+ event.content["m.relates_to"][prop] = (0, _utils.internaliseString)(event.content["m.relates_to"][prop]);
+ });
+ this.txnId = event.txn_id;
+ this.localTimestamp = Date.now() - (this.getAge() ?? 0);
+ this.reEmitter = new _ReEmitter.TypedReEmitter(this);
+ }
+
+ /**
+ * Unstable getter to try and get an extensible event. Note that this might
+ * return a falsy value if the event could not be parsed as an extensible
+ * event.
+ *
+ * @deprecated Use stable functions where possible.
+ */
+ get unstableExtensibleEvent() {
+ if (!this._hasCachedExtEv) {
+ this._cachedExtEv = _matrixEventsSdk.ExtensibleEvents.parse(this.getEffectiveEvent());
+ }
+ return this._cachedExtEv;
+ }
+ invalidateExtensibleEvent() {
+ // just reset the flag - that'll trick the getter into parsing a new event
+ this._hasCachedExtEv = false;
+ }
+
+ /**
+ * Gets the event as though it would appear unencrypted. If the event is already not
+ * encrypted, it is simply returned as-is.
+ * @returns The event in wire format.
+ */
+ getEffectiveEvent() {
+ const content = Object.assign({}, this.getContent()); // clone for mutation
+
+ if (this.getWireType() === _event.EventType.RoomMessageEncrypted) {
+ // Encrypted events sometimes aren't symmetrical on the `content` so we'll copy
+ // that over too, but only for missing properties. We don't copy over mismatches
+ // between the plain and decrypted copies of `content` because we assume that the
+ // app is relying on the decrypted version, so we want to expose that as a source
+ // of truth here too.
+ for (const [key, value] of Object.entries(this.getWireContent())) {
+ // Skip fields from the encrypted event schema though - we don't want to leak
+ // these.
+ if (["algorithm", "ciphertext", "device_id", "sender_key", "session_id"].includes(key)) {
+ continue;
+ }
+ if (content[key] === undefined) content[key] = value;
+ }
+ }
+
+ // clearEvent doesn't have all the fields, so we'll copy what we can from this.event.
+ // We also copy over our "fixed" content key.
+ return Object.assign({}, this.event, this.clearEvent, {
+ content
+ });
+ }
+
+ /**
+ * Get the event_id for this event.
+ * @returns The event ID, e.g. <code>$143350589368169JsLZx:localhost
+ * </code>
+ */
+ getId() {
+ return this.event.event_id;
+ }
+
+ /**
+ * Get the user_id for this event.
+ * @returns The user ID, e.g. `@alice:matrix.org`
+ */
+ getSender() {
+ return this.event.sender || this.event.user_id; // v2 / v1
+ }
+
+ /**
+ * Get the (decrypted, if necessary) type of event.
+ *
+ * @returns The event type, e.g. `m.room.message`
+ */
+ getType() {
+ if (this.clearEvent) {
+ return this.clearEvent.type;
+ }
+ return this.event.type;
+ }
+
+ /**
+ * Get the (possibly encrypted) type of the event that will be sent to the
+ * homeserver.
+ *
+ * @returns The event type.
+ */
+ getWireType() {
+ return this.event.type;
+ }
+
+ /**
+ * Get the room_id for this event. This will return `undefined`
+ * for `m.presence` events.
+ * @returns The room ID, e.g. <code>!cURbafjkfsMDVwdRDQ:matrix.org
+ * </code>
+ */
+ getRoomId() {
+ return this.event.room_id;
+ }
+
+ /**
+ * Get the timestamp of this event.
+ * @returns The event timestamp, e.g. `1433502692297`
+ */
+ getTs() {
+ return this.event.origin_server_ts;
+ }
+
+ /**
+ * Get the timestamp of this event, as a Date object.
+ * @returns The event date, e.g. `new Date(1433502692297)`
+ */
+ getDate() {
+ return this.event.origin_server_ts ? new Date(this.event.origin_server_ts) : null;
+ }
+
+ /**
+ * Get a string containing details of this event
+ *
+ * This is intended for logging, to help trace errors. Example output:
+ *
+ * @example
+ * ```
+ * id=$HjnOHV646n0SjLDAqFrgIjim7RCpB7cdMXFrekWYAn type=m.room.encrypted
+ * sender=@user:example.com room=!room:example.com ts=2022-10-25T17:30:28.404Z
+ * ```
+ */
+ getDetails() {
+ let details = `id=${this.getId()} type=${this.getWireType()} sender=${this.getSender()}`;
+ const room = this.getRoomId();
+ if (room) {
+ details += ` room=${room}`;
+ }
+ const date = this.getDate();
+ if (date) {
+ details += ` ts=${date.toISOString()}`;
+ }
+ return details;
+ }
+
+ /**
+ * Get the (decrypted, if necessary) event content JSON, even if the event
+ * was replaced by another event.
+ *
+ * @returns The event content JSON, or an empty object.
+ */
+ getOriginalContent() {
+ if (this._localRedactionEvent) {
+ return {};
+ }
+ if (this.clearEvent) {
+ return this.clearEvent.content || {};
+ }
+ return this.event.content || {};
+ }
+
+ /**
+ * Get the (decrypted, if necessary) event content JSON,
+ * or the content from the replacing event, if any.
+ * See `makeReplaced`.
+ *
+ * @returns The event content JSON, or an empty object.
+ */
+ getContent() {
+ if (this._localRedactionEvent) {
+ return {};
+ } else if (this._replacingEvent) {
+ return this._replacingEvent.getContent()["m.new_content"] || {};
+ } else {
+ return this.getOriginalContent();
+ }
+ }
+
+ /**
+ * Get the (possibly encrypted) event content JSON that will be sent to the
+ * homeserver.
+ *
+ * @returns The event content JSON, or an empty object.
+ */
+ getWireContent() {
+ return this.event.content || {};
+ }
+
+ /**
+ * Get the event ID of the thread head
+ */
+ get threadRootId() {
+ const relatesTo = this.getWireContent()?.["m.relates_to"];
+ if (relatesTo?.rel_type === _thread.THREAD_RELATION_TYPE.name) {
+ return relatesTo.event_id;
+ } else {
+ return this.getThread()?.id || this.threadId;
+ }
+ }
+
+ /**
+ * A helper to check if an event is a thread's head or not
+ */
+ get isThreadRoot() {
+ const threadDetails = this.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name);
+
+ // Bundled relationships only returned when the sync response is limited
+ // hence us having to check both bundled relation and inspect the thread
+ // model
+ return !!threadDetails || this.getThread()?.id === this.getId();
+ }
+ get replyEventId() {
+ return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id;
+ }
+ get relationEventId() {
+ return this.getWireContent()?.["m.relates_to"]?.event_id;
+ }
+
+ /**
+ * Get the previous event content JSON. This will only return something for
+ * state events which exist in the timeline.
+ * @returns The previous event content JSON, or an empty object.
+ */
+ getPrevContent() {
+ // v2 then v1 then default
+ return this.getUnsigned().prev_content || this.event.prev_content || {};
+ }
+
+ /**
+ * Get either 'content' or 'prev_content' depending on if this event is
+ * 'forward-looking' or not. This can be modified via event.forwardLooking.
+ * In practice, this means we get the chronologically earlier content value
+ * for this event (this method should surely be called getEarlierContent)
+ * <strong>This method is experimental and may change.</strong>
+ * @returns event.content if this event is forward-looking, else
+ * event.prev_content.
+ */
+ getDirectionalContent() {
+ return this.forwardLooking ? this.getContent() : this.getPrevContent();
+ }
+
+ /**
+ * Get the age of this event. This represents the age of the event when the
+ * event arrived at the device, and not the age of the event when this
+ * function was called.
+ * Can only be returned once the server has echo'ed back
+ * @returns The age of this event in milliseconds.
+ */
+ getAge() {
+ return this.getUnsigned().age || this.event.age; // v2 / v1
+ }
+
+ /**
+ * Get the age of the event when this function was called.
+ * This is the 'age' field adjusted according to how long this client has
+ * had the event.
+ * @returns The age of this event in milliseconds.
+ */
+ getLocalAge() {
+ return Date.now() - this.localTimestamp;
+ }
+
+ /**
+ * Get the event state_key if it has one. This will return <code>undefined
+ * </code> for message events.
+ * @returns The event's `state_key`.
+ */
+ getStateKey() {
+ return this.event.state_key;
+ }
+
+ /**
+ * Check if this event is a state event.
+ * @returns True if this is a state event.
+ */
+ isState() {
+ return this.event.state_key !== undefined;
+ }
+
+ /**
+ * Replace the content of this event with encrypted versions.
+ * (This is used when sending an event; it should not be used by applications).
+ *
+ * @internal
+ *
+ * @param cryptoType - type of the encrypted event - typically
+ * <tt>"m.room.encrypted"</tt>
+ *
+ * @param cryptoContent - raw 'content' for the encrypted event.
+ *
+ * @param senderCurve25519Key - curve25519 key to record for the
+ * sender of this event.
+ * See {@link MatrixEvent#getSenderKey}.
+ *
+ * @param claimedEd25519Key - claimed ed25519 key to record for the
+ * sender if this event.
+ * See {@link MatrixEvent#getClaimedEd25519Key}
+ */
+ makeEncrypted(cryptoType, cryptoContent, senderCurve25519Key, claimedEd25519Key) {
+ // keep the plain-text data for 'view source'
+ this.clearEvent = {
+ type: this.event.type,
+ content: this.event.content
+ };
+ this.event.type = cryptoType;
+ this.event.content = cryptoContent;
+ this.senderCurve25519Key = senderCurve25519Key;
+ this.claimedEd25519Key = claimedEd25519Key;
+ }
+
+ /**
+ * Check if this event is currently being decrypted.
+ *
+ * @returns True if this event is currently being decrypted, else false.
+ */
+ isBeingDecrypted() {
+ return this.decryptionPromise != null;
+ }
+ getDecryptionPromise() {
+ return this.decryptionPromise;
+ }
+
+ /**
+ * Check if this event is an encrypted event which we failed to decrypt
+ *
+ * (This implies that we might retry decryption at some point in the future)
+ *
+ * @returns True if this event is an encrypted event which we
+ * couldn't decrypt.
+ */
+ isDecryptionFailure() {
+ return this.clearEvent?.content?.msgtype === "m.bad.encrypted";
+ }
+
+ /*
+ * True if this event is an encrypted event which we failed to decrypt, the receiver's device is unverified and
+ * the sender has disabled encrypting to unverified devices.
+ */
+ get isEncryptedDisabledForUnverifiedDevices() {
+ return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices;
+ }
+ shouldAttemptDecryption() {
+ if (this.isRedacted()) return false;
+ if (this.isBeingDecrypted()) return false;
+ if (this.clearEvent) return false;
+ if (!this.isEncrypted()) return false;
+ return true;
+ }
+
+ /**
+ * Start the process of trying to decrypt this event.
+ *
+ * (This is used within the SDK: it isn't intended for use by applications)
+ *
+ * @internal
+ *
+ * @param crypto - crypto module
+ *
+ * @returns promise which resolves (to undefined) when the decryption
+ * attempt is completed.
+ */
+ async attemptDecryption(crypto, options = {}) {
+ // start with a couple of sanity checks.
+ if (!this.isEncrypted()) {
+ throw new Error("Attempt to decrypt event which isn't encrypted");
+ }
+ const alreadyDecrypted = this.clearEvent && !this.isDecryptionFailure();
+ const forceRedecrypt = options.forceRedecryptIfUntrusted && this.isKeySourceUntrusted();
+ if (alreadyDecrypted && !forceRedecrypt) {
+ // we may want to just ignore this? let's start with rejecting it.
+ throw new Error("Attempt to decrypt event which has already been decrypted");
+ }
+
+ // if we already have a decryption attempt in progress, then it may
+ // fail because it was using outdated info. We now have reason to
+ // succeed where it failed before, but we don't want to have multiple
+ // attempts going at the same time, so just set a flag that says we have
+ // new info.
+ //
+ if (this.decryptionPromise) {
+ _logger.logger.log(`Event ${this.getId()} already being decrypted; queueing a retry`);
+ this.retryDecryption = true;
+ return this.decryptionPromise;
+ }
+ this.decryptionPromise = this.decryptionLoop(crypto, options);
+ return this.decryptionPromise;
+ }
+
+ /**
+ * Cancel any room key request for this event and resend another.
+ *
+ * @param crypto - crypto module
+ * @param userId - the user who received this event
+ *
+ * @returns a promise that resolves when the request is queued
+ */
+ cancelAndResendKeyRequest(crypto, userId) {
+ const wireContent = this.getWireContent();
+ return crypto.requestRoomKey({
+ algorithm: wireContent.algorithm,
+ room_id: this.getRoomId(),
+ session_id: wireContent.session_id,
+ sender_key: wireContent.sender_key
+ }, this.getKeyRequestRecipients(userId), true);
+ }
+
+ /**
+ * Calculate the recipients for keyshare requests.
+ *
+ * @param userId - the user who received this event.
+ *
+ * @returns array of recipients
+ */
+ getKeyRequestRecipients(userId) {
+ // send the request to all of our own devices
+ const recipients = [{
+ userId,
+ deviceId: "*"
+ }];
+ return recipients;
+ }
+ async decryptionLoop(crypto, options = {}) {
+ // make sure that this method never runs completely synchronously.
+ // (doing so would mean that we would clear decryptionPromise *before*
+ // it is set in attemptDecryption - and hence end up with a stuck
+ // `decryptionPromise`).
+ await Promise.resolve();
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ this.retryDecryption = false;
+ let res;
+ let err = undefined;
+ try {
+ if (!crypto) {
+ res = this.badEncryptedMessage("Encryption not enabled");
+ } else {
+ res = await crypto.decryptEvent(this);
+ if (options.isRetry === true) {
+ _logger.logger.info(`Decrypted event on retry (${this.getDetails()})`);
+ }
+ }
+ } catch (e) {
+ const detailedError = e instanceof _algorithms.DecryptionError ? e.detailedString : String(e);
+ err = e;
+
+ // see if we have a retry queued.
+ //
+ // NB: make sure to keep this check in the same tick of the
+ // event loop as `decryptionPromise = null` below - otherwise we
+ // risk a race:
+ //
+ // * A: we check retryDecryption here and see that it is
+ // false
+ // * B: we get a second call to attemptDecryption, which sees
+ // that decryptionPromise is set so sets
+ // retryDecryption
+ // * A: we continue below, clear decryptionPromise, and
+ // never do the retry.
+ //
+ if (this.retryDecryption) {
+ // decryption error, but we have a retry queued.
+ _logger.logger.log(`Error decrypting event (${this.getDetails()}), but retrying: ${detailedError}`);
+ continue;
+ }
+
+ // decryption error, no retries queued. Warn about the error and
+ // set it to m.bad.encrypted.
+ //
+ // the detailedString already includes the name and message of the error, and the stack isn't much use,
+ // so we don't bother to log `e` separately.
+ _logger.logger.warn(`Error decrypting event (${this.getDetails()}): ${detailedError}`);
+ res = this.badEncryptedMessage(String(e));
+ }
+
+ // at this point, we've either successfully decrypted the event, or have given up
+ // (and set res to a 'badEncryptedMessage'). Either way, we can now set the
+ // cleartext of the event and raise Event.decrypted.
+ //
+ // make sure we clear 'decryptionPromise' before sending the 'Event.decrypted' event,
+ // otherwise the app will be confused to see `isBeingDecrypted` still set when
+ // there isn't an `Event.decrypted` on the way.
+ //
+ // see also notes on retryDecryption above.
+ //
+ this.decryptionPromise = null;
+ this.retryDecryption = false;
+ this.setClearData(res);
+
+ // Before we emit the event, clear the push actions so that they can be recalculated
+ // by relevant code. We do this because the clear event has now changed, making it
+ // so that existing rules can be re-run over the applicable properties. Stuff like
+ // highlighting when the user's name is mentioned rely on this happening. We also want
+ // to set the push actions before emitting so that any notification listeners don't
+ // pick up the wrong contents.
+ this.setPushDetails();
+ if (options.emit !== false) {
+ this.emit(MatrixEventEvent.Decrypted, this, err);
+ }
+ return;
+ }
+ }
+ badEncryptedMessage(reason) {
+ return {
+ clearEvent: {
+ type: _event.EventType.RoomMessage,
+ content: {
+ msgtype: "m.bad.encrypted",
+ body: "** Unable to decrypt: " + reason + " **"
+ }
+ },
+ encryptedDisabledForUnverifiedDevices: reason === `DecryptionError: ${_OlmDevice.WITHHELD_MESSAGES["m.unverified"]}`
+ };
+ }
+
+ /**
+ * Update the cleartext data on this event.
+ *
+ * (This is used after decrypting an event; it should not be used by applications).
+ *
+ * @internal
+ *
+ * @param decryptionResult - the decryption result, including the plaintext and some key info
+ *
+ * @remarks
+ * Fires {@link MatrixEventEvent.Decrypted}
+ */
+ setClearData(decryptionResult) {
+ this.clearEvent = decryptionResult.clearEvent;
+ this.senderCurve25519Key = decryptionResult.senderCurve25519Key ?? null;
+ this.claimedEd25519Key = decryptionResult.claimedEd25519Key ?? null;
+ this.forwardingCurve25519KeyChain = decryptionResult.forwardingCurve25519KeyChain || [];
+ this.untrusted = decryptionResult.untrusted || false;
+ this.encryptedDisabledForUnverifiedDevices = decryptionResult.encryptedDisabledForUnverifiedDevices || false;
+ this.invalidateExtensibleEvent();
+ }
+
+ /**
+ * Gets the cleartext content for this event. If the event is not encrypted,
+ * or encryption has not been completed, this will return null.
+ *
+ * @returns The cleartext (decrypted) content for the event
+ */
+ getClearContent() {
+ return this.clearEvent ? this.clearEvent.content : null;
+ }
+
+ /**
+ * Check if the event is encrypted.
+ * @returns True if this event is encrypted.
+ */
+ isEncrypted() {
+ return !this.isState() && this.event.type === _event.EventType.RoomMessageEncrypted;
+ }
+
+ /**
+ * The curve25519 key for the device that we think sent this event
+ *
+ * For an Olm-encrypted event, this is inferred directly from the DH
+ * exchange at the start of the session: the curve25519 key is involved in
+ * the DH exchange, so only a device which holds the private part of that
+ * key can establish such a session.
+ *
+ * For a megolm-encrypted event, it is inferred from the Olm message which
+ * established the megolm session
+ */
+ getSenderKey() {
+ return this.senderCurve25519Key;
+ }
+
+ /**
+ * The additional keys the sender of this encrypted event claims to possess.
+ *
+ * Just a wrapper for #getClaimedEd25519Key (q.v.)
+ */
+ getKeysClaimed() {
+ if (!this.claimedEd25519Key) return {};
+ return {
+ ed25519: this.claimedEd25519Key
+ };
+ }
+
+ /**
+ * Get the ed25519 the sender of this event claims to own.
+ *
+ * For Olm messages, this claim is encoded directly in the plaintext of the
+ * event itself. For megolm messages, it is implied by the m.room_key event
+ * which established the megolm session.
+ *
+ * Until we download the device list of the sender, it's just a claim: the
+ * device list gives a proof that the owner of the curve25519 key used for
+ * this event (and returned by #getSenderKey) also owns the ed25519 key by
+ * signing the public curve25519 key with the ed25519 key.
+ *
+ * In general, applications should not use this method directly, but should
+ * instead use MatrixClient.getEventSenderDeviceInfo.
+ */
+ getClaimedEd25519Key() {
+ return this.claimedEd25519Key;
+ }
+
+ /**
+ * Get the curve25519 keys of the devices which were involved in telling us
+ * about the claimedEd25519Key and sender curve25519 key.
+ *
+ * Normally this will be empty, but in the case of a forwarded megolm
+ * session, the sender keys are sent to us by another device (the forwarding
+ * device), which we need to trust to do this. In that case, the result will
+ * be a list consisting of one entry.
+ *
+ * If the device that sent us the key (A) got it from another device which
+ * it wasn't prepared to vouch for (B), the result will be [A, B]. And so on.
+ *
+ * @returns base64-encoded curve25519 keys, from oldest to newest.
+ */
+ getForwardingCurve25519KeyChain() {
+ return this.forwardingCurve25519KeyChain;
+ }
+
+ /**
+ * Whether the decryption key was obtained from an untrusted source. If so,
+ * we cannot verify the authenticity of the message.
+ */
+ isKeySourceUntrusted() {
+ return !!this.untrusted;
+ }
+ getUnsigned() {
+ return this.event.unsigned || {};
+ }
+ setUnsigned(unsigned) {
+ this.event.unsigned = unsigned;
+ }
+ unmarkLocallyRedacted() {
+ const value = this._localRedactionEvent;
+ this._localRedactionEvent = null;
+ if (this.event.unsigned) {
+ this.event.unsigned.redacted_because = undefined;
+ }
+ return !!value;
+ }
+ markLocallyRedacted(redactionEvent) {
+ if (this._localRedactionEvent) return;
+ this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
+ this._localRedactionEvent = redactionEvent;
+ if (!this.event.unsigned) {
+ this.event.unsigned = {};
+ }
+ this.event.unsigned.redacted_because = redactionEvent.event;
+ }
+
+ /**
+ * Change the visibility of an event, as per https://github.com/matrix-org/matrix-doc/pull/3531 .
+ *
+ * @param visibilityChange - event holding a hide/unhide payload, or nothing
+ * if the event is being reset to its original visibility (presumably
+ * by a visibility event being redacted).
+ *
+ * @remarks
+ * Fires {@link MatrixEventEvent.VisibilityChange} if `visibilityEvent`
+ * caused a change in the actual visibility of this event, either by making it
+ * visible (if it was hidden), by making it hidden (if it was visible) or by
+ * changing the reason (if it was hidden).
+ */
+ applyVisibilityEvent(visibilityChange) {
+ const visible = visibilityChange?.visible ?? true;
+ const reason = visibilityChange?.reason ?? null;
+ let change = false;
+ if (this.visibility.visible !== visible) {
+ change = true;
+ } else if (!this.visibility.visible && this.visibility["reason"] !== reason) {
+ change = true;
+ }
+ if (change) {
+ if (visible) {
+ this.visibility = MESSAGE_VISIBLE;
+ } else {
+ this.visibility = Object.freeze({
+ visible: false,
+ reason
+ });
+ }
+ this.emit(MatrixEventEvent.VisibilityChange, this, visible);
+ }
+ }
+
+ /**
+ * Return instructions to display or hide the message.
+ *
+ * @returns Instructions determining whether the message
+ * should be displayed.
+ */
+ messageVisibility() {
+ // Note: We may return `this.visibility` without fear, as
+ // this is a shallow frozen object.
+ return this.visibility;
+ }
+
+ /**
+ * Update the content of an event in the same way it would be by the server
+ * if it were redacted before it was sent to us
+ *
+ * @param redactionEvent - event causing the redaction
+ */
+ makeRedacted(redactionEvent) {
+ // quick sanity-check
+ if (!redactionEvent.event) {
+ throw new Error("invalid redactionEvent in makeRedacted");
+ }
+ this._localRedactionEvent = null;
+ this.emit(MatrixEventEvent.BeforeRedaction, this, redactionEvent);
+ this._replacingEvent = null;
+ // we attempt to replicate what we would see from the server if
+ // the event had been redacted before we saw it.
+ //
+ // The server removes (most of) the content of the event, and adds a
+ // "redacted_because" key to the unsigned section containing the
+ // redacted event.
+ if (!this.event.unsigned) {
+ this.event.unsigned = {};
+ }
+ this.event.unsigned.redacted_because = redactionEvent.event;
+ for (const key in this.event) {
+ if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(key)) {
+ delete this.event[key];
+ }
+ }
+
+ // If the event is encrypted prune the decrypted bits
+ if (this.isEncrypted()) {
+ this.clearEvent = undefined;
+ }
+ const keeps = this.getType() in REDACT_KEEP_CONTENT_MAP ? REDACT_KEEP_CONTENT_MAP[this.getType()] : {};
+ const content = this.getContent();
+ for (const key in content) {
+ if (content.hasOwnProperty(key) && !keeps[key]) {
+ delete content[key];
+ }
+ }
+ this.invalidateExtensibleEvent();
+ }
+
+ /**
+ * Check if this event has been redacted
+ *
+ * @returns True if this event has been redacted
+ */
+ isRedacted() {
+ return Boolean(this.getUnsigned().redacted_because);
+ }
+
+ /**
+ * Check if this event is a redaction of another event
+ *
+ * @returns True if this event is a redaction
+ */
+ isRedaction() {
+ return this.getType() === _event.EventType.RoomRedaction;
+ }
+
+ /**
+ * Return the visibility change caused by this event,
+ * as per https://github.com/matrix-org/matrix-doc/pull/3531.
+ *
+ * @returns If the event is a well-formed visibility change event,
+ * an instance of `IVisibilityChange`, otherwise `null`.
+ */
+ asVisibilityChange() {
+ if (!_event.EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType())) {
+ // Not a visibility change event.
+ return null;
+ }
+ const relation = this.getRelation();
+ if (!relation || relation.rel_type != "m.reference") {
+ // Ill-formed, ignore this event.
+ return null;
+ }
+ const eventId = relation.event_id;
+ if (!eventId) {
+ // Ill-formed, ignore this event.
+ return null;
+ }
+ const content = this.getWireContent();
+ const visible = !!content.visible;
+ const reason = content.reason;
+ if (reason && typeof reason != "string") {
+ // Ill-formed, ignore this event.
+ return null;
+ }
+ // Well-formed visibility change event.
+ return {
+ visible,
+ reason,
+ eventId
+ };
+ }
+
+ /**
+ * Check if this event alters the visibility of another event,
+ * as per https://github.com/matrix-org/matrix-doc/pull/3531.
+ *
+ * @returns True if this event alters the visibility
+ * of another event.
+ */
+ isVisibilityEvent() {
+ return _event.EVENT_VISIBILITY_CHANGE_TYPE.matches(this.getType());
+ }
+
+ /**
+ * Get the (decrypted, if necessary) redaction event JSON
+ * if event was redacted
+ *
+ * @returns The redaction event JSON, or an empty object
+ */
+ getRedactionEvent() {
+ if (!this.isRedacted()) return null;
+ if (this.clearEvent?.unsigned) {
+ return this.clearEvent?.unsigned.redacted_because ?? null;
+ } else if (this.event.unsigned?.redacted_because) {
+ return this.event.unsigned.redacted_because;
+ } else {
+ return {};
+ }
+ }
+
+ /**
+ * Get the push actions, if known, for this event
+ *
+ * @returns push actions
+ */
+ getPushActions() {
+ return this.pushDetails.actions || null;
+ }
+
+ /**
+ * Get the push details, if known, for this event
+ *
+ * @returns push actions
+ */
+ getPushDetails() {
+ return this.pushDetails;
+ }
+
+ /**
+ * Set the push actions for this event.
+ * Clears rule from push details if present
+ * @deprecated use `setPushDetails`
+ *
+ * @param pushActions - push actions
+ */
+ setPushActions(pushActions) {
+ this.pushDetails = {
+ actions: pushActions || undefined
+ };
+ }
+
+ /**
+ * Set the push details for this event.
+ *
+ * @param pushActions - push actions
+ * @param rule - the executed push rule
+ */
+ setPushDetails(pushActions, rule) {
+ this.pushDetails = {
+ actions: pushActions,
+ rule
+ };
+ }
+
+ /**
+ * Replace the `event` property and recalculate any properties based on it.
+ * @param event - the object to assign to the `event` property
+ */
+ handleRemoteEcho(event) {
+ const oldUnsigned = this.getUnsigned();
+ const oldId = this.getId();
+ this.event = event;
+ // if this event was redacted before it was sent, it's locally marked as redacted.
+ // At this point, we've received the remote echo for the event, but not yet for
+ // the redaction that we are sending ourselves. Preserve the locally redacted
+ // state by copying over redacted_because so we don't get a flash of
+ // redacted, not-redacted, redacted as remote echos come in
+ if (oldUnsigned.redacted_because) {
+ if (!this.event.unsigned) {
+ this.event.unsigned = {};
+ }
+ this.event.unsigned.redacted_because = oldUnsigned.redacted_because;
+ }
+ // successfully sent.
+ this.setStatus(null);
+ if (this.getId() !== oldId) {
+ // emit the event if it changed
+ this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
+ }
+ this.localTimestamp = Date.now() - this.getAge();
+ }
+
+ /**
+ * Whether the event is in any phase of sending, send failure, waiting for
+ * remote echo, etc.
+ */
+ isSending() {
+ return !!this.status;
+ }
+
+ /**
+ * Update the event's sending status and emit an event as well.
+ *
+ * @param status - The new status
+ */
+ setStatus(status) {
+ this.status = status;
+ this.emit(MatrixEventEvent.Status, this, status);
+ }
+ replaceLocalEventId(eventId) {
+ this.event.event_id = eventId;
+ this.emit(MatrixEventEvent.LocalEventIdReplaced, this);
+ }
+
+ /**
+ * Get whether the event is a relation event, and of a given type if
+ * `relType` is passed in. State events cannot be relation events
+ *
+ * @param relType - if given, checks that the relation is of the
+ * given type
+ */
+ isRelation(relType) {
+ // Relation info is lifted out of the encrypted content when sent to
+ // encrypted rooms, so we have to check `getWireContent` for this.
+ const relation = this.getWireContent()?.["m.relates_to"];
+ if (this.isState() && relation?.rel_type === _event.RelationType.Replace) {
+ // State events cannot be m.replace relations
+ return false;
+ }
+ return !!(relation?.rel_type && relation.event_id && (relType ? relation.rel_type === relType : true));
+ }
+
+ /**
+ * Get relation info for the event, if any.
+ */
+ getRelation() {
+ if (!this.isRelation()) {
+ return null;
+ }
+ return this.getWireContent()["m.relates_to"] ?? null;
+ }
+
+ /**
+ * Set an event that replaces the content of this event, through an m.replace relation.
+ *
+ * @param newEvent - the event with the replacing content, if any.
+ *
+ * @remarks
+ * Fires {@link MatrixEventEvent.Replaced}
+ */
+ makeReplaced(newEvent) {
+ // don't allow redacted events to be replaced.
+ // if newEvent is null we allow to go through though,
+ // as with local redaction, the replacing event might get
+ // cancelled, which should be reflected on the target event.
+ if (this.isRedacted() && newEvent) {
+ return;
+ }
+ // don't allow state events to be replaced using this mechanism as per MSC2676
+ if (this.isState()) {
+ return;
+ }
+ if (this._replacingEvent !== newEvent) {
+ this._replacingEvent = newEvent ?? null;
+ this.emit(MatrixEventEvent.Replaced, this);
+ this.invalidateExtensibleEvent();
+ }
+ }
+
+ /**
+ * Returns the status of any associated edit or redaction
+ * (not for reactions/annotations as their local echo doesn't affect the original event),
+ * or else the status of the event.
+ */
+ getAssociatedStatus() {
+ if (this._replacingEvent) {
+ return this._replacingEvent.status;
+ } else if (this._localRedactionEvent) {
+ return this._localRedactionEvent.status;
+ }
+ return this.status;
+ }
+ getServerAggregatedRelation(relType) {
+ return this.getUnsigned()["m.relations"]?.[relType];
+ }
+
+ /**
+ * Returns the event ID of the event replacing the content of this event, if any.
+ */
+ replacingEventId() {
+ const replaceRelation = this.getServerAggregatedRelation(_event.RelationType.Replace);
+ if (replaceRelation) {
+ return replaceRelation.event_id;
+ } else if (this._replacingEvent) {
+ return this._replacingEvent.getId();
+ }
+ }
+
+ /**
+ * Returns the event replacing the content of this event, if any.
+ * Replacements are aggregated on the server, so this would only
+ * return an event in case it came down the sync, or for local echo of edits.
+ */
+ replacingEvent() {
+ return this._replacingEvent;
+ }
+
+ /**
+ * Returns the origin_server_ts of the event replacing the content of this event, if any.
+ */
+ replacingEventDate() {
+ const replaceRelation = this.getServerAggregatedRelation(_event.RelationType.Replace);
+ if (replaceRelation) {
+ const ts = replaceRelation.origin_server_ts;
+ if (Number.isFinite(ts)) {
+ return new Date(ts);
+ }
+ } else if (this._replacingEvent) {
+ return this._replacingEvent.getDate() ?? undefined;
+ }
+ }
+
+ /**
+ * Returns the event that wants to redact this event, but hasn't been sent yet.
+ * @returns the event
+ */
+ localRedactionEvent() {
+ return this._localRedactionEvent;
+ }
+
+ /**
+ * For relations and redactions, returns the event_id this event is referring to.
+ */
+ getAssociatedId() {
+ const relation = this.getRelation();
+ if (this.replyEventId) {
+ return this.replyEventId;
+ } else if (relation) {
+ return relation.event_id;
+ } else if (this.isRedaction()) {
+ return this.event.redacts;
+ }
+ }
+
+ /**
+ * Checks if this event is associated with another event. See `getAssociatedId`.
+ * @deprecated use hasAssociation instead.
+ */
+ hasAssocation() {
+ return !!this.getAssociatedId();
+ }
+
+ /**
+ * Checks if this event is associated with another event. See `getAssociatedId`.
+ */
+ hasAssociation() {
+ return !!this.getAssociatedId();
+ }
+
+ /**
+ * Update the related id with a new one.
+ *
+ * Used to replace a local id with remote one before sending
+ * an event with a related id.
+ *
+ * @param eventId - the new event id
+ */
+ updateAssociatedId(eventId) {
+ const relation = this.getRelation();
+ if (relation) {
+ relation.event_id = eventId;
+ } else if (this.isRedaction()) {
+ this.event.redacts = eventId;
+ }
+ }
+
+ /**
+ * Flags an event as cancelled due to future conditions. For example, a verification
+ * request event in the same sync transaction may be flagged as cancelled to warn
+ * listeners that a cancellation event is coming down the same pipe shortly.
+ * @param cancelled - Whether the event is to be cancelled or not.
+ */
+ flagCancelled(cancelled = true) {
+ this._isCancelled = cancelled;
+ }
+
+ /**
+ * Gets whether or not the event is flagged as cancelled. See flagCancelled() for
+ * more information.
+ * @returns True if the event is cancelled, false otherwise.
+ */
+ isCancelled() {
+ return this._isCancelled;
+ }
+
+ /**
+ * Get a copy/snapshot of this event. The returned copy will be loosely linked
+ * back to this instance, though will have "frozen" event information. Other
+ * properties of this MatrixEvent instance will be copied verbatim, which can
+ * mean they are in reference to this instance despite being on the copy too.
+ * The reference the snapshot uses does not change, however members aside from
+ * the underlying event will not be deeply cloned, thus may be mutated internally.
+ * For example, the sender profile will be copied over at snapshot time, and
+ * the sender profile internally may mutate without notice to the consumer.
+ *
+ * This is meant to be used to snapshot the event details themselves, not the
+ * features (such as sender) surrounding the event.
+ * @returns A snapshot of this event.
+ */
+ toSnapshot() {
+ const ev = new MatrixEvent(JSON.parse(JSON.stringify(this.event)));
+ for (const [p, v] of Object.entries(this)) {
+ if (p !== "event") {
+ // exclude the thing we just cloned
+ // @ts-ignore - XXX: this is just nasty
+ ev[p] = v;
+ }
+ }
+ return ev;
+ }
+
+ /**
+ * Determines if this event is equivalent to the given event. This only checks
+ * the event object itself, not the other properties of the event. Intended for
+ * use with toSnapshot() to identify events changing.
+ * @param otherEvent - The other event to check against.
+ * @returns True if the events are the same, false otherwise.
+ */
+ isEquivalentTo(otherEvent) {
+ if (!otherEvent) return false;
+ if (otherEvent === this) return true;
+ const myProps = (0, _utils.deepSortedObjectEntries)(this.event);
+ const theirProps = (0, _utils.deepSortedObjectEntries)(otherEvent.event);
+ return JSON.stringify(myProps) === JSON.stringify(theirProps);
+ }
+
+ /**
+ * Summarise the event as JSON. This is currently used by React SDK's view
+ * event source feature and Seshat's event indexing, so take care when
+ * adjusting the output here.
+ *
+ * If encrypted, include both the decrypted and encrypted view of the event.
+ *
+ * This is named `toJSON` for use with `JSON.stringify` which checks objects
+ * for functions named `toJSON` and will call them to customise the output
+ * if they are defined.
+ */
+ toJSON() {
+ const event = this.getEffectiveEvent();
+ if (!this.isEncrypted()) {
+ return event;
+ }
+ return {
+ decrypted: event,
+ encrypted: this.event
+ };
+ }
+ setVerificationRequest(request) {
+ this.verificationRequest = request;
+ }
+ setTxnId(txnId) {
+ this.txnId = txnId;
+ }
+ getTxnId() {
+ return this.txnId;
+ }
+
+ /**
+ * Set the instance of a thread associated with the current event
+ * @param thread - the thread
+ */
+ setThread(thread) {
+ if (this.thread) {
+ this.reEmitter.stopReEmitting(this.thread, [_thread.ThreadEvent.Update]);
+ }
+ this.thread = thread;
+ this.setThreadId(thread?.id);
+ if (thread) {
+ this.reEmitter.reEmit(thread, [_thread.ThreadEvent.Update]);
+ }
+ }
+
+ /**
+ * Get the instance of the thread associated with the current event
+ */
+ getThread() {
+ return this.thread;
+ }
+ setThreadId(threadId) {
+ this.threadId = threadId;
+ }
+}
+
+/* REDACT_KEEP_KEYS gives the keys we keep when an event is redacted
+ *
+ * This is specified here:
+ * http://matrix.org/speculator/spec/HEAD/client_server/latest.html#redactions
+ *
+ * Also:
+ * - We keep 'unsigned' since that is created by the local server
+ * - We keep user_id for backwards-compat with v1
+ */
+exports.MatrixEvent = MatrixEvent;
+const REDACT_KEEP_KEYS = new Set(["event_id", "type", "room_id", "user_id", "sender", "state_key", "prev_state", "content", "unsigned", "origin_server_ts"]);
+
+// a map from state event type to the .content keys we keep when an event is redacted
+const REDACT_KEEP_CONTENT_MAP = {
+ [_event.EventType.RoomMember]: {
+ membership: 1
+ },
+ [_event.EventType.RoomCreate]: {
+ creator: 1
+ },
+ [_event.EventType.RoomJoinRules]: {
+ join_rule: 1
+ },
+ [_event.EventType.RoomPowerLevels]: {
+ ban: 1,
+ events: 1,
+ events_default: 1,
+ kick: 1,
+ redact: 1,
+ state_default: 1,
+ users: 1,
+ users_default: 1
+ }
+}; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/invites-ignorer.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/invites-ignorer.js
new file mode 100644
index 0000000000..b49d37c372
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/invites-ignorer.js
@@ -0,0 +1,358 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.PolicyScope = exports.POLICIES_ACCOUNT_EVENT_TYPE = exports.IgnoredInvites = exports.IGNORE_INVITES_ACCOUNT_EVENT_KEY = void 0;
+var _matrixEventsSdk = require("matrix-events-sdk");
+var _eventTimeline = require("./event-timeline");
+var _partials = require("../@types/partials");
+var _utils = require("../utils");
+/*
+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.
+*/
+
+/// The event type storing the user's individual policies.
+///
+/// Exported for testing purposes.
+const POLICIES_ACCOUNT_EVENT_TYPE = new _matrixEventsSdk.UnstableValue("m.policies", "org.matrix.msc3847.policies");
+
+/// The key within the user's individual policies storing the user's ignored invites.
+///
+/// Exported for testing purposes.
+exports.POLICIES_ACCOUNT_EVENT_TYPE = POLICIES_ACCOUNT_EVENT_TYPE;
+const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new _matrixEventsSdk.UnstableValue("m.ignore.invites", "org.matrix.msc3847.ignore.invites");
+
+/// The types of recommendations understood.
+exports.IGNORE_INVITES_ACCOUNT_EVENT_KEY = IGNORE_INVITES_ACCOUNT_EVENT_KEY;
+var PolicyRecommendation = /*#__PURE__*/function (PolicyRecommendation) {
+ PolicyRecommendation["Ban"] = "m.ban";
+ return PolicyRecommendation;
+}(PolicyRecommendation || {});
+/**
+ * The various scopes for policies.
+ */
+let PolicyScope = /*#__PURE__*/function (PolicyScope) {
+ PolicyScope["User"] = "m.policy.user";
+ PolicyScope["Room"] = "m.policy.room";
+ PolicyScope["Server"] = "m.policy.server";
+ return PolicyScope;
+}({});
+/**
+ * A container for ignored invites.
+ *
+ * # Performance
+ *
+ * This implementation is extremely naive. It expects that we are dealing
+ * with a very short list of sources (e.g. only one). If real-world
+ * applications turn out to require longer lists, we may need to rework
+ * our data structures.
+ */
+exports.PolicyScope = PolicyScope;
+class IgnoredInvites {
+ constructor(client) {
+ this.client = client;
+ }
+
+ /**
+ * Add a new rule.
+ *
+ * @param scope - The scope for this rule.
+ * @param entity - The entity covered by this rule. Globs are supported.
+ * @param reason - A human-readable reason for introducing this new rule.
+ * @returns The event id for the new rule.
+ */
+ async addRule(scope, entity, reason) {
+ const target = await this.getOrCreateTargetRoom();
+ const response = await this.client.sendStateEvent(target.roomId, scope, {
+ entity,
+ reason,
+ recommendation: PolicyRecommendation.Ban
+ });
+ return response.event_id;
+ }
+
+ /**
+ * Remove a rule.
+ */
+ async removeRule(event) {
+ await this.client.redactEvent(event.getRoomId(), event.getId());
+ }
+
+ /**
+ * Add a new room to the list of sources. If the user isn't a member of the
+ * room, attempt to join it.
+ *
+ * @param roomId - A valid room id. If this room is already in the list
+ * of sources, it will not be duplicated.
+ * @returns `true` if the source was added, `false` if it was already present.
+ * @throws If `roomId` isn't the id of a room that the current user is already
+ * member of or can join.
+ *
+ * # Safety
+ *
+ * This method will rewrite the `Policies` object in the user's account data.
+ * This rewrite is inherently racy and could overwrite or be overwritten by
+ * other concurrent rewrites of the same object.
+ */
+ async addSource(roomId) {
+ // We attempt to join the room *before* calling
+ // `await this.getOrCreateSourceRooms()` to decrease the duration
+ // of the racy section.
+ await this.client.joinRoom(roomId);
+ // Race starts.
+ const sources = (await this.getOrCreateSourceRooms()).map(room => room.roomId);
+ if (sources.includes(roomId)) {
+ return false;
+ }
+ sources.push(roomId);
+ await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
+ ignoreInvitesPolicies.sources = sources;
+ });
+
+ // Race ends.
+ return true;
+ }
+
+ /**
+ * Find out whether an invite should be ignored.
+ *
+ * @param sender - The user id for the user who issued the invite.
+ * @param roomId - The room to which the user is invited.
+ * @returns A rule matching the entity, if any was found, `null` otherwise.
+ */
+ async getRuleForInvite({
+ sender,
+ roomId
+ }) {
+ // In this implementation, we perform a very naive lookup:
+ // - search in each policy room;
+ // - turn each (potentially glob) rule entity into a regexp.
+ //
+ // Real-world testing will tell us whether this is performant enough.
+ // In the (unfortunately likely) case it isn't, there are several manners
+ // in which we could optimize this:
+ // - match several entities per go;
+ // - pre-compile each rule entity into a regexp;
+ // - pre-compile entire rooms into a single regexp.
+ const policyRooms = await this.getOrCreateSourceRooms();
+ const senderServer = sender.split(":")[1];
+ const roomServer = roomId.split(":")[1];
+ for (const room of policyRooms) {
+ const state = room.getUnfilteredTimelineSet().getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
+ for (const {
+ scope,
+ entities
+ } of [{
+ scope: PolicyScope.Room,
+ entities: [roomId]
+ }, {
+ scope: PolicyScope.User,
+ entities: [sender]
+ }, {
+ scope: PolicyScope.Server,
+ entities: [senderServer, roomServer]
+ }]) {
+ const events = state.getStateEvents(scope);
+ for (const event of events) {
+ const content = event.getContent();
+ if (content?.recommendation != PolicyRecommendation.Ban) {
+ // Ignoring invites only looks at `m.ban` recommendations.
+ continue;
+ }
+ const glob = content?.entity;
+ if (!glob) {
+ // Invalid event.
+ continue;
+ }
+ let regexp;
+ try {
+ regexp = new RegExp((0, _utils.globToRegexp)(glob));
+ } catch (ex) {
+ // Assume invalid event.
+ continue;
+ }
+ for (const entity of entities) {
+ if (entity && regexp.test(entity)) {
+ return event;
+ }
+ }
+ // No match.
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Get the target room, i.e. the room in which any new rule should be written.
+ *
+ * If there is no target room setup, a target room is created.
+ *
+ * Note: This method is public for testing reasons. Most clients should not need
+ * to call it directly.
+ *
+ * # Safety
+ *
+ * This method will rewrite the `Policies` object in the user's account data.
+ * This rewrite is inherently racy and could overwrite or be overwritten by
+ * other concurrent rewrites of the same object.
+ */
+ async getOrCreateTargetRoom() {
+ const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
+ let target = ignoreInvitesPolicies.target;
+ // Validate `target`. If it is invalid, trash out the current `target`
+ // and create a new room.
+ if (typeof target !== "string") {
+ target = null;
+ }
+ if (target) {
+ // Check that the room exists and is valid.
+ const room = this.client.getRoom(target);
+ if (room) {
+ return room;
+ } else {
+ target = null;
+ }
+ }
+ // We need to create our own policy room for ignoring invites.
+ target = (await this.client.createRoom({
+ name: "Individual Policy Room",
+ preset: _partials.Preset.PrivateChat
+ })).room_id;
+ await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
+ ignoreInvitesPolicies.target = target;
+ });
+
+ // Since we have just called `createRoom`, `getRoom` should not be `null`.
+ return this.client.getRoom(target);
+ }
+
+ /**
+ * Get the list of source rooms, i.e. the rooms from which rules need to be read.
+ *
+ * If no source rooms are setup, the target room is used as sole source room.
+ *
+ * Note: This method is public for testing reasons. Most clients should not need
+ * to call it directly.
+ *
+ * # Safety
+ *
+ * This method will rewrite the `Policies` object in the user's account data.
+ * This rewrite is inherently racy and could overwrite or be overwritten by
+ * other concurrent rewrites of the same object.
+ */
+ async getOrCreateSourceRooms() {
+ const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
+ let sources = ignoreInvitesPolicies.sources;
+
+ // Validate `sources`. If it is invalid, trash out the current `sources`
+ // and create a new list of sources from `target`.
+ let hasChanges = false;
+ if (!Array.isArray(sources)) {
+ // `sources` could not be an array.
+ hasChanges = true;
+ sources = [];
+ }
+ let sourceRooms = sources
+ // `sources` could contain non-string / invalid room ids
+ .filter(roomId => typeof roomId === "string").map(roomId => this.client.getRoom(roomId)).filter(room => !!room);
+ if (sourceRooms.length != sources.length) {
+ hasChanges = true;
+ }
+ if (sourceRooms.length == 0) {
+ // `sources` could be empty (possibly because we've removed
+ // invalid content)
+ const target = await this.getOrCreateTargetRoom();
+ hasChanges = true;
+ sourceRooms = [target];
+ }
+ if (hasChanges) {
+ // Reload `policies`/`ignoreInvitesPolicies` in case it has been changed
+ // during or by our call to `this.getTargetRoom()`.
+ await this.withIgnoreInvitesPolicies(ignoreInvitesPolicies => {
+ ignoreInvitesPolicies.sources = sources;
+ });
+ }
+ return sourceRooms;
+ }
+
+ /**
+ * Fetch the `IGNORE_INVITES_POLICIES` object from account data.
+ *
+ * If both an unstable prefix version and a stable prefix version are available,
+ * it will return the stable prefix version preferentially.
+ *
+ * The result is *not* validated but is guaranteed to be a non-null object.
+ *
+ * @returns A non-null object.
+ */
+ getIgnoreInvitesPolicies() {
+ return this.getPoliciesAndIgnoreInvitesPolicies().ignoreInvitesPolicies;
+ }
+
+ /**
+ * Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
+ */
+ async withIgnoreInvitesPolicies(cb) {
+ const {
+ policies,
+ ignoreInvitesPolicies
+ } = this.getPoliciesAndIgnoreInvitesPolicies();
+ cb(ignoreInvitesPolicies);
+ policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
+ await this.client.setAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name, policies);
+ }
+
+ /**
+ * As `getIgnoreInvitesPolicies` but also return the `POLICIES_ACCOUNT_EVENT_TYPE`
+ * object.
+ */
+ getPoliciesAndIgnoreInvitesPolicies() {
+ let policies = {};
+ for (const key of [POLICIES_ACCOUNT_EVENT_TYPE.name, POLICIES_ACCOUNT_EVENT_TYPE.altName]) {
+ if (!key) {
+ continue;
+ }
+ const value = this.client.getAccountData(key)?.getContent();
+ if (value) {
+ policies = value;
+ break;
+ }
+ }
+ let ignoreInvitesPolicies = {};
+ let hasIgnoreInvitesPolicies = false;
+ for (const key of [IGNORE_INVITES_ACCOUNT_EVENT_KEY.name, IGNORE_INVITES_ACCOUNT_EVENT_KEY.altName]) {
+ if (!key) {
+ continue;
+ }
+ const value = policies[key];
+ if (value && typeof value == "object") {
+ ignoreInvitesPolicies = value;
+ hasIgnoreInvitesPolicies = true;
+ break;
+ }
+ }
+ if (!hasIgnoreInvitesPolicies) {
+ policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name] = ignoreInvitesPolicies;
+ }
+ return {
+ policies,
+ ignoreInvitesPolicies
+ };
+ }
+}
+exports.IgnoredInvites = IgnoredInvites; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/poll.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/poll.js
new file mode 100644
index 0000000000..25818398f5
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/poll.js
@@ -0,0 +1,237 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.isPollEvent = exports.PollEvent = exports.Poll = void 0;
+var _matrixEventsSdk = require("matrix-events-sdk");
+var _polls = require("../@types/polls");
+var _relations = require("./relations");
+var _typedEventEmitter = require("./typed-event-emitter");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2023 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.
+ */
+let PollEvent = /*#__PURE__*/function (PollEvent) {
+ PollEvent["New"] = "Poll.new";
+ PollEvent["End"] = "Poll.end";
+ PollEvent["Update"] = "Poll.update";
+ PollEvent["Responses"] = "Poll.Responses";
+ PollEvent["Destroy"] = "Poll.Destroy";
+ PollEvent["UndecryptableRelations"] = "Poll.UndecryptableRelations";
+ return PollEvent;
+}({});
+exports.PollEvent = PollEvent;
+const filterResponseRelations = (relationEvents, pollEndTimestamp) => {
+ const responseEvents = relationEvents.filter(event => {
+ if (event.isDecryptionFailure()) {
+ return;
+ }
+ return _polls.M_POLL_RESPONSE.matches(event.getType()) &&
+ // From MSC3381:
+ // "Votes sent on or before the end event's timestamp are valid votes"
+ event.getTs() <= pollEndTimestamp;
+ });
+ return {
+ responseEvents
+ };
+};
+class Poll extends _typedEventEmitter.TypedEventEmitter {
+ constructor(rootEvent, matrixClient, room) {
+ super();
+ this.rootEvent = rootEvent;
+ this.matrixClient = matrixClient;
+ this.room = room;
+ _defineProperty(this, "roomId", void 0);
+ _defineProperty(this, "pollEvent", void 0);
+ _defineProperty(this, "_isFetchingResponses", false);
+ _defineProperty(this, "relationsNextBatch", void 0);
+ _defineProperty(this, "responses", null);
+ _defineProperty(this, "endEvent", void 0);
+ /**
+ * Keep track of undecryptable relations
+ * As incomplete result sets affect poll results
+ */
+ _defineProperty(this, "undecryptableRelationEventIds", new Set());
+ _defineProperty(this, "countUndecryptableEvents", events => {
+ const undecryptableEventIds = events.filter(event => event.isDecryptionFailure()).map(event => event.getId());
+ const previousCount = this.undecryptableRelationsCount;
+ this.undecryptableRelationEventIds = new Set([...this.undecryptableRelationEventIds, ...undecryptableEventIds]);
+ if (this.undecryptableRelationsCount !== previousCount) {
+ this.emit(PollEvent.UndecryptableRelations, this.undecryptableRelationsCount);
+ }
+ });
+ if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) {
+ throw new Error("Invalid poll start event.");
+ }
+ this.roomId = this.rootEvent.getRoomId();
+ this.pollEvent = this.rootEvent.unstableExtensibleEvent;
+ }
+ get pollId() {
+ return this.rootEvent.getId();
+ }
+ get endEventId() {
+ return this.endEvent?.getId();
+ }
+ get isEnded() {
+ return !!this.endEvent;
+ }
+ get isFetchingResponses() {
+ return this._isFetchingResponses;
+ }
+ get undecryptableRelationsCount() {
+ return this.undecryptableRelationEventIds.size;
+ }
+ async getResponses() {
+ // if we have already fetched some responses
+ // just return them
+ if (this.responses) {
+ return this.responses;
+ }
+
+ // if there is no fetching in progress
+ // start fetching
+ if (!this.isFetchingResponses) {
+ await this.fetchResponses();
+ }
+ // return whatever responses we got from the first page
+ return this.responses;
+ }
+
+ /**
+ *
+ * @param event - event with a relation to the rootEvent
+ * @returns void
+ */
+ onNewRelation(event) {
+ if (_polls.M_POLL_END.matches(event.getType()) && this.validateEndEvent(event)) {
+ this.endEvent = event;
+ this.refilterResponsesOnEnd();
+ this.emit(PollEvent.End);
+ }
+
+ // wait for poll responses to be initialised
+ if (!this.responses) {
+ return;
+ }
+ const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
+ const {
+ responseEvents
+ } = filterResponseRelations([event], pollEndTimestamp);
+ this.countUndecryptableEvents([event]);
+ if (responseEvents.length) {
+ responseEvents.forEach(event => {
+ this.responses.addEvent(event);
+ });
+ this.emit(PollEvent.Responses, this.responses);
+ }
+ }
+ async fetchResponses() {
+ this._isFetchingResponses = true;
+
+ // we want:
+ // - stable and unstable M_POLL_RESPONSE
+ // - stable and unstable M_POLL_END
+ // so make one api call and filter by event type client side
+ const allRelations = await this.matrixClient.relations(this.roomId, this.rootEvent.getId(), "m.reference", undefined, {
+ from: this.relationsNextBatch || undefined
+ });
+ await Promise.all(allRelations.events.map(event => this.matrixClient.decryptEventIfNeeded(event)));
+ const responses = this.responses || new _relations.Relations("m.reference", _polls.M_POLL_RESPONSE.name, this.matrixClient, [_polls.M_POLL_RESPONSE.altName]);
+ const pollEndEvent = allRelations.events.find(event => _polls.M_POLL_END.matches(event.getType()));
+ if (this.validateEndEvent(pollEndEvent)) {
+ this.endEvent = pollEndEvent;
+ this.refilterResponsesOnEnd();
+ this.emit(PollEvent.End);
+ }
+ const pollCloseTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
+ const {
+ responseEvents
+ } = filterResponseRelations(allRelations.events, pollCloseTimestamp);
+ responseEvents.forEach(event => {
+ responses.addEvent(event);
+ });
+ this.relationsNextBatch = allRelations.nextBatch ?? undefined;
+ this.responses = responses;
+ this.countUndecryptableEvents(allRelations.events);
+
+ // while there are more pages of relations
+ // fetch them
+ if (this.relationsNextBatch) {
+ // don't await
+ // we want to return the first page as soon as possible
+ this.fetchResponses();
+ } else {
+ // no more pages
+ this._isFetchingResponses = false;
+ }
+
+ // emit after updating _isFetchingResponses state
+ this.emit(PollEvent.Responses, this.responses);
+ }
+
+ /**
+ * Only responses made before the poll ended are valid
+ * Refilter after an end event is recieved
+ * To ensure responses are valid
+ */
+ refilterResponsesOnEnd() {
+ if (!this.responses) {
+ return;
+ }
+ const pollEndTimestamp = this.endEvent?.getTs() || Number.MAX_SAFE_INTEGER;
+ this.responses.getRelations().forEach(event => {
+ if (event.getTs() > pollEndTimestamp) {
+ this.responses?.removeEvent(event);
+ }
+ });
+ this.emit(PollEvent.Responses, this.responses);
+ }
+ validateEndEvent(endEvent) {
+ if (!endEvent) {
+ return false;
+ }
+ /**
+ * Repeated end events are ignored -
+ * only the first (valid) closure event by origin_server_ts is counted.
+ */
+ if (this.endEvent && this.endEvent.getTs() < endEvent.getTs()) {
+ return false;
+ }
+
+ /**
+ * MSC3381
+ * If a m.poll.end event is received from someone other than the poll creator or user with permission to redact
+ * others' messages in the room, the event must be ignored by clients due to being invalid.
+ */
+ const roomCurrentState = this.room.currentState;
+ const endEventSender = endEvent.getSender();
+ return !!endEventSender && (endEventSender === this.rootEvent.getSender() || roomCurrentState.maySendRedactionForEvent(this.rootEvent, endEventSender));
+ }
+}
+
+/**
+ * Tests whether the event is a start, response or end poll event.
+ *
+ * @param event - Event to test
+ * @returns true if the event is a poll event, else false
+ */
+exports.Poll = Poll;
+const isPollEvent = event => {
+ const eventType = event.getType();
+ return _matrixEventsSdk.M_POLL_START.matches(eventType) || _polls.M_POLL_RESPONSE.matches(eventType) || _polls.M_POLL_END.matches(eventType);
+};
+exports.isPollEvent = isPollEvent; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/read-receipt.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/read-receipt.js
new file mode 100644
index 0000000000..8bef646ab4
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/read-receipt.js
@@ -0,0 +1,260 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ReadReceipt = void 0;
+exports.synthesizeReceipt = synthesizeReceipt;
+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");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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) {
+ return new _event.MatrixEvent({
+ content: {
+ [event.getId()]: {
+ [receiptType]: {
+ [userId]: {
+ ts: event.getTs(),
+ thread_id: 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
+ _defineProperty(this, "receipts", new _utils.MapWithDefault(() => new Map()));
+ _defineProperty(this, "receiptCacheByEventId", new Map());
+ _defineProperty(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) {
+ 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.
+ */
+ getEventReadUpTo(userId, ignoreSynthesized = false) {
+ // 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, _read_receipts.ReceiptType.Read);
+ const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, _read_receipts.ReceiptType.ReadPrivate);
+
+ // If we have both, compare them
+ let comparison;
+ 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;
+ }
+ addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic) {
+ 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 = {
+ eventId,
+ data: receipt
+ };
+ 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[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,
+ 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?.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) {
+ 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;
+ }
+}
+exports.ReadReceipt = ReadReceipt; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/related-relations.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/related-relations.js
new file mode 100644
index 0000000000..9db67bf544
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/related-relations.js
@@ -0,0 +1,41 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RelatedRelations = void 0;
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); }
+/*
+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.
+*/
+
+class RelatedRelations {
+ constructor(relations) {
+ _defineProperty(this, "relations", void 0);
+ this.relations = relations.filter(r => !!r);
+ }
+ getRelations() {
+ return this.relations.reduce((c, p) => [...c, ...p.getRelations()], []);
+ }
+ on(ev, fn) {
+ this.relations.forEach(r => r.on(ev, fn));
+ }
+ off(ev, fn) {
+ this.relations.forEach(r => r.off(ev, fn));
+ }
+}
+exports.RelatedRelations = RelatedRelations; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations-container.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations-container.js
new file mode 100644
index 0000000000..489ab267eb
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations-container.js
@@ -0,0 +1,135 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RelationsContainer = void 0;
+var _relations = require("./relations");
+var _event = require("./event");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ 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.
+ */
+class RelationsContainer {
+ constructor(client, room) {
+ this.client = client;
+ this.room = room;
+ // A tree of objects to access a set of related children for an event, as in:
+ // this.relations.get(parentEventId).get(relationType).get(relationEventType)
+ _defineProperty(this, "relations", new Map());
+ }
+
+ /**
+ * Get a collection of child events to a given event in this timeline set.
+ *
+ * @param eventId - The ID of the event that you'd like to access child events for.
+ * For example, with annotations, this would be the ID of the event being annotated.
+ * @param relationType - The type of relationship involved, such as "m.annotation", "m.reference", "m.replace", etc.
+ * @param eventType - The relation event's type, such as "m.reaction", etc.
+ * @throws If `eventId</code>, <code>relationType</code> or <code>eventType`
+ * are not valid.
+ *
+ * @returns
+ * A container for relation events or undefined if there are no relation events for
+ * the relationType.
+ */
+ getChildEventsForEvent(eventId, relationType, eventType) {
+ return this.relations.get(eventId)?.get(relationType)?.get(eventType);
+ }
+ getAllChildEventsForEvent(parentEventId) {
+ const relationsForEvent = this.relations.get(parentEventId) ?? new Map();
+ const events = [];
+ for (const relationsRecord of relationsForEvent.values()) {
+ for (const relations of relationsRecord.values()) {
+ events.push(...relations.getRelations());
+ }
+ }
+ return events;
+ }
+
+ /**
+ * Set an event as the target event if any Relations exist for it already.
+ * Child events can point to other child events as their parent, so this method may be
+ * called for events which are also logically child events.
+ *
+ * @param event - The event to check as relation target.
+ */
+ aggregateParentEvent(event) {
+ const relationsForEvent = this.relations.get(event.getId());
+ if (!relationsForEvent) return;
+ for (const relationsWithRelType of relationsForEvent.values()) {
+ for (const relationsWithEventType of relationsWithRelType.values()) {
+ relationsWithEventType.setTargetEvent(event);
+ }
+ }
+ }
+
+ /**
+ * Add relation events to the relevant relation collection.
+ *
+ * @param event - The new child event to be aggregated.
+ * @param timelineSet - The event timeline set within which to search for the related event if any.
+ */
+ aggregateChildEvent(event, timelineSet) {
+ if (event.isRedacted() || event.status === _event.EventStatus.CANCELLED) {
+ return;
+ }
+ const relation = event.getRelation();
+ if (!relation) return;
+ const onEventDecrypted = () => {
+ if (event.isDecryptionFailure()) {
+ // This could for example happen if the encryption keys are not yet available.
+ // The event may still be decrypted later. Register the listener again.
+ event.once(_event.MatrixEventEvent.Decrypted, onEventDecrypted);
+ return;
+ }
+ this.aggregateChildEvent(event, timelineSet);
+ };
+
+ // If the event is currently encrypted, wait until it has been decrypted.
+ if (event.isBeingDecrypted() || event.shouldAttemptDecryption()) {
+ event.once(_event.MatrixEventEvent.Decrypted, onEventDecrypted);
+ return;
+ }
+ const {
+ event_id: relatesToEventId,
+ rel_type: relationType
+ } = relation;
+ const eventType = event.getType();
+ let relationsForEvent = this.relations.get(relatesToEventId);
+ if (!relationsForEvent) {
+ relationsForEvent = new Map();
+ this.relations.set(relatesToEventId, relationsForEvent);
+ }
+ let relationsWithRelType = relationsForEvent.get(relationType);
+ if (!relationsWithRelType) {
+ relationsWithRelType = new Map();
+ relationsForEvent.set(relationType, relationsWithRelType);
+ }
+ let relationsWithEventType = relationsWithRelType.get(eventType);
+ if (!relationsWithEventType) {
+ relationsWithEventType = new _relations.Relations(relationType, eventType, this.client);
+ relationsWithRelType.set(eventType, relationsWithEventType);
+ const room = this.room ?? timelineSet?.room;
+ const relatesToEvent = timelineSet?.findEventById(relatesToEventId) ?? room?.findEventById(relatesToEventId) ?? room?.getPendingEvent(relatesToEventId);
+ if (relatesToEvent) {
+ relationsWithEventType.setTargetEvent(relatesToEvent);
+ }
+ }
+ relationsWithEventType.addEvent(event);
+ }
+}
+exports.RelationsContainer = RelationsContainer; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations.js
new file mode 100644
index 0000000000..1b83615e8b
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/relations.js
@@ -0,0 +1,336 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RelationsEvent = exports.Relations = void 0;
+var _event = require("./event");
+var _logger = require("../logger");
+var _event2 = require("../@types/event");
+var _typedEventEmitter = require("./typed-event-emitter");
+var _room = require("./room");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2019, 2021, 2023 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.
+ */
+let RelationsEvent = /*#__PURE__*/function (RelationsEvent) {
+ RelationsEvent["Add"] = "Relations.add";
+ RelationsEvent["Remove"] = "Relations.remove";
+ RelationsEvent["Redaction"] = "Relations.redaction";
+ return RelationsEvent;
+}({});
+exports.RelationsEvent = RelationsEvent;
+const matchesEventType = (eventType, targetEventType, altTargetEventTypes = []) => [targetEventType, ...altTargetEventTypes].includes(eventType);
+
+/**
+ * A container for relation events that supports easy access to common ways of
+ * aggregating such events. Each instance holds events that of a single relation
+ * type and event type. All of the events also relate to the same original event.
+ *
+ * The typical way to get one of these containers is via
+ * EventTimelineSet#getRelationsForEvent.
+ */
+class Relations extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * @param relationType - The type of relation involved, such as "m.annotation", "m.reference", "m.replace", etc.
+ * @param eventType - The relation event's type, such as "m.reaction", etc.
+ * @param client - The client which created this instance. For backwards compatibility also accepts a Room.
+ * @param altEventTypes - alt event types for relation events, for example to support unstable prefixed event types
+ */
+ constructor(relationType, eventType, client, altEventTypes) {
+ super();
+ this.relationType = relationType;
+ this.eventType = eventType;
+ this.altEventTypes = altEventTypes;
+ _defineProperty(this, "relationEventIds", new Set());
+ _defineProperty(this, "relations", new Set());
+ _defineProperty(this, "annotationsByKey", {});
+ _defineProperty(this, "annotationsBySender", {});
+ _defineProperty(this, "sortedAnnotationsByKey", []);
+ _defineProperty(this, "targetEvent", null);
+ _defineProperty(this, "creationEmitted", false);
+ _defineProperty(this, "client", void 0);
+ /**
+ * Listens for event status changes to remove cancelled events.
+ *
+ * @param event - The event whose status has changed
+ * @param status - The new status
+ */
+ _defineProperty(this, "onEventStatus", (event, status) => {
+ if (!event.isSending()) {
+ // Sending is done, so we don't need to listen anymore
+ event.removeListener(_event.MatrixEventEvent.Status, this.onEventStatus);
+ return;
+ }
+ if (status !== _event.EventStatus.CANCELLED) {
+ return;
+ }
+ // Event was cancelled, remove from the collection
+ event.removeListener(_event.MatrixEventEvent.Status, this.onEventStatus);
+ this.removeEvent(event);
+ });
+ /**
+ * For relations that have been redacted, we want to remove them from
+ * aggregation data sets and emit an update event.
+ *
+ * To do so, we listen for `Event.beforeRedaction`, which happens:
+ * - after the server accepted the redaction and remote echoed back to us
+ * - before the original event has been marked redacted in the client
+ *
+ * @param redactedEvent - The original relation event that is about to be redacted.
+ */
+ _defineProperty(this, "onBeforeRedaction", async redactedEvent => {
+ if (!this.relations.has(redactedEvent)) {
+ return;
+ }
+ this.relations.delete(redactedEvent);
+ if (this.relationType === _event2.RelationType.Annotation) {
+ // Remove the redacted annotation from aggregation by key
+ this.removeAnnotationFromAggregation(redactedEvent);
+ } else if (this.relationType === _event2.RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
+ const lastReplacement = await this.getLastReplacement();
+ this.targetEvent.makeReplaced(lastReplacement);
+ }
+ redactedEvent.removeListener(_event.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
+ this.emit(RelationsEvent.Redaction, redactedEvent);
+ });
+ this.client = client instanceof _room.Room ? client.client : client;
+ }
+
+ /**
+ * Add relation events to this collection.
+ *
+ * @param event - The new relation event to be added.
+ */
+ async addEvent(event) {
+ if (this.relationEventIds.has(event.getId())) {
+ return;
+ }
+ const relation = event.getRelation();
+ if (!relation) {
+ _logger.logger.error("Event must have relation info");
+ return;
+ }
+ const relationType = relation.rel_type;
+ const eventType = event.getType();
+ if (this.relationType !== relationType || !matchesEventType(eventType, this.eventType, this.altEventTypes)) {
+ _logger.logger.error("Event relation info doesn't match this container");
+ return;
+ }
+
+ // If the event is in the process of being sent, listen for cancellation
+ // so we can remove the event from the collection.
+ if (event.isSending()) {
+ event.on(_event.MatrixEventEvent.Status, this.onEventStatus);
+ }
+ this.relations.add(event);
+ this.relationEventIds.add(event.getId());
+ if (this.relationType === _event2.RelationType.Annotation) {
+ this.addAnnotationToAggregation(event);
+ } else if (this.relationType === _event2.RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
+ const lastReplacement = await this.getLastReplacement();
+ this.targetEvent.makeReplaced(lastReplacement);
+ }
+ event.on(_event.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
+ this.emit(RelationsEvent.Add, event);
+ this.maybeEmitCreated();
+ }
+
+ /**
+ * Remove relation event from this collection.
+ *
+ * @param event - The relation event to remove.
+ */
+ async removeEvent(event) {
+ if (!this.relations.has(event)) {
+ return;
+ }
+ this.relations.delete(event);
+ if (this.relationType === _event2.RelationType.Annotation) {
+ this.removeAnnotationFromAggregation(event);
+ } else if (this.relationType === _event2.RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
+ const lastReplacement = await this.getLastReplacement();
+ this.targetEvent.makeReplaced(lastReplacement);
+ }
+ this.emit(RelationsEvent.Remove, event);
+ }
+ /**
+ * Get all relation events in this collection.
+ *
+ * These are currently in the order of insertion to this collection, which
+ * won't match timeline order in the case of scrollback.
+ * TODO: Tweak `addEvent` to insert correctly for scrollback.
+ *
+ * Relation events in insertion order.
+ */
+ getRelations() {
+ return [...this.relations];
+ }
+ addAnnotationToAggregation(event) {
+ const {
+ key
+ } = event.getRelation() ?? {};
+ if (!key) return;
+ let eventsForKey = this.annotationsByKey[key];
+ if (!eventsForKey) {
+ eventsForKey = this.annotationsByKey[key] = new Set();
+ this.sortedAnnotationsByKey.push([key, eventsForKey]);
+ }
+ // Add the new event to the set for this key
+ eventsForKey.add(event);
+ // Re-sort the [key, events] pairs in descending order of event count
+ this.sortedAnnotationsByKey.sort((a, b) => {
+ const aEvents = a[1];
+ const bEvents = b[1];
+ return bEvents.size - aEvents.size;
+ });
+ const sender = event.getSender();
+ let eventsFromSender = this.annotationsBySender[sender];
+ if (!eventsFromSender) {
+ eventsFromSender = this.annotationsBySender[sender] = new Set();
+ }
+ // Add the new event to the set for this sender
+ eventsFromSender.add(event);
+ }
+ removeAnnotationFromAggregation(event) {
+ const {
+ key
+ } = event.getRelation() ?? {};
+ if (!key) return;
+ const eventsForKey = this.annotationsByKey[key];
+ if (eventsForKey) {
+ eventsForKey.delete(event);
+
+ // Re-sort the [key, events] pairs in descending order of event count
+ this.sortedAnnotationsByKey.sort((a, b) => {
+ const aEvents = a[1];
+ const bEvents = b[1];
+ return bEvents.size - aEvents.size;
+ });
+ }
+ const sender = event.getSender();
+ const eventsFromSender = this.annotationsBySender[sender];
+ if (eventsFromSender) {
+ eventsFromSender.delete(event);
+ }
+ }
+ /**
+ * Get all events in this collection grouped by key and sorted by descending
+ * event count in each group.
+ *
+ * This is currently only supported for the annotation relation type.
+ *
+ * An array of [key, events] pairs sorted by descending event count.
+ * The events are stored in a Set (which preserves insertion order).
+ */
+ getSortedAnnotationsByKey() {
+ if (this.relationType !== _event2.RelationType.Annotation) {
+ // Other relation types are not grouped currently.
+ return null;
+ }
+ return this.sortedAnnotationsByKey;
+ }
+
+ /**
+ * Get all events in this collection grouped by sender.
+ *
+ * This is currently only supported for the annotation relation type.
+ *
+ * An object with each relation sender as a key and the matching Set of
+ * events for that sender as a value.
+ */
+ getAnnotationsBySender() {
+ if (this.relationType !== _event2.RelationType.Annotation) {
+ // Other relation types are not grouped currently.
+ return null;
+ }
+ return this.annotationsBySender;
+ }
+
+ /**
+ * Returns the most recent (and allowed) m.replace relation, if any.
+ *
+ * This is currently only supported for the m.replace relation type,
+ * once the target event is known, see `addEvent`.
+ */
+ async getLastReplacement() {
+ if (this.relationType !== _event2.RelationType.Replace) {
+ // Aggregating on last only makes sense for this relation type
+ return null;
+ }
+ if (!this.targetEvent) {
+ // Don't know which replacements to accept yet.
+ // This method shouldn't be called before the original
+ // event is known anyway.
+ return null;
+ }
+
+ // the all-knowning server tells us that the event at some point had
+ // this timestamp for its replacement, so any following replacement should definitely not be less
+ const replaceRelation = this.targetEvent.getServerAggregatedRelation(_event2.RelationType.Replace);
+ const minTs = replaceRelation?.origin_server_ts;
+ const lastReplacement = this.getRelations().reduce((last, event) => {
+ if (event.getSender() !== this.targetEvent.getSender()) {
+ return last;
+ }
+ if (minTs && minTs > event.getTs()) {
+ return last;
+ }
+ if (last && last.getTs() > event.getTs()) {
+ return last;
+ }
+ return event;
+ }, null);
+ if (lastReplacement?.shouldAttemptDecryption() && this.client.isCryptoEnabled()) {
+ await lastReplacement.attemptDecryption(this.client.crypto);
+ } else if (lastReplacement?.isBeingDecrypted()) {
+ await lastReplacement.getDecryptionPromise();
+ }
+ return lastReplacement;
+ }
+
+ /*
+ * @param targetEvent - the event the relations are related to.
+ */
+ async setTargetEvent(event) {
+ if (this.targetEvent) {
+ return;
+ }
+ this.targetEvent = event;
+ if (this.relationType === _event2.RelationType.Replace && !this.targetEvent.isState()) {
+ const replacement = await this.getLastReplacement();
+ // this is the initial update, so only call it if we already have something
+ // to not emit Event.replaced needlessly
+ if (replacement) {
+ this.targetEvent.makeReplaced(replacement);
+ }
+ }
+ this.maybeEmitCreated();
+ }
+ maybeEmitCreated() {
+ if (this.creationEmitted) {
+ return;
+ }
+ // Only emit we're "created" once we have a target event instance _and_
+ // at least one related event.
+ if (!this.targetEvent || !this.relations.size) {
+ return;
+ }
+ this.creationEmitted = true;
+ this.targetEvent.emit(_event.MatrixEventEvent.RelationsCreated, this.relationType, this.eventType);
+ }
+}
+exports.Relations = Relations; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-member.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-member.js
new file mode 100644
index 0000000000..53c01065de
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-member.js
@@ -0,0 +1,363 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RoomMemberEvent = exports.RoomMember = void 0;
+var _contentRepo = require("../content-repo");
+var _utils = require("../utils");
+var _logger = require("../logger");
+var _typedEventEmitter = require("./typed-event-emitter");
+var _event = require("../@types/event");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2015 - 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.
+ */
+let RoomMemberEvent = /*#__PURE__*/function (RoomMemberEvent) {
+ RoomMemberEvent["Membership"] = "RoomMember.membership";
+ RoomMemberEvent["Name"] = "RoomMember.name";
+ RoomMemberEvent["PowerLevel"] = "RoomMember.powerLevel";
+ RoomMemberEvent["Typing"] = "RoomMember.typing";
+ return RoomMemberEvent;
+}({});
+exports.RoomMemberEvent = RoomMemberEvent;
+class RoomMember extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * Construct a new room member.
+ *
+ * @param roomId - The room ID of the member.
+ * @param userId - The user ID of the member.
+ */
+ constructor(roomId, userId) {
+ super();
+ this.roomId = roomId;
+ this.userId = userId;
+ _defineProperty(this, "_isOutOfBand", false);
+ _defineProperty(this, "modified", -1);
+ _defineProperty(this, "requestedProfileInfo", false);
+ // used by sync.ts
+ // XXX these should be read-only
+ /**
+ * True if the room member is currently typing.
+ */
+ _defineProperty(this, "typing", false);
+ /**
+ * The human-readable name for this room member. This will be
+ * disambiguated with a suffix of " (\@user_id:matrix.org)" if another member shares the
+ * same displayname.
+ */
+ _defineProperty(this, "name", void 0);
+ /**
+ * The ambiguous displayname of this room member.
+ */
+ _defineProperty(this, "rawDisplayName", void 0);
+ /**
+ * The power level for this room member.
+ */
+ _defineProperty(this, "powerLevel", 0);
+ /**
+ * The normalised power level (0-100) for this room member.
+ */
+ _defineProperty(this, "powerLevelNorm", 0);
+ /**
+ * The User object for this room member, if one exists.
+ */
+ _defineProperty(this, "user", void 0);
+ /**
+ * The membership state for this room member e.g. 'join'.
+ */
+ _defineProperty(this, "membership", void 0);
+ /**
+ * True if the member's name is disambiguated.
+ */
+ _defineProperty(this, "disambiguate", false);
+ /**
+ * The events describing this RoomMember.
+ */
+ _defineProperty(this, "events", {});
+ this.name = userId;
+ this.rawDisplayName = userId;
+ this.updateModifiedTime();
+ }
+
+ /**
+ * Mark the member as coming from a channel that is not sync
+ */
+ markOutOfBand() {
+ this._isOutOfBand = true;
+ }
+
+ /**
+ * @returns does the member come from a channel that is not sync?
+ * This is used to store the member seperately
+ * from the sync state so it available across browser sessions.
+ */
+ isOutOfBand() {
+ return this._isOutOfBand;
+ }
+
+ /**
+ * Update this room member's membership event. May fire "RoomMember.name" if
+ * this event updates this member's name.
+ * @param event - The `m.room.member` event
+ * @param roomState - Optional. The room state to take into account
+ * when calculating (e.g. for disambiguating users with the same name).
+ *
+ * @remarks
+ * Fires {@link RoomMemberEvent.Name}
+ * Fires {@link RoomMemberEvent.Membership}
+ */
+ setMembershipEvent(event, roomState) {
+ const displayName = event.getDirectionalContent().displayname ?? "";
+ if (event.getType() !== _event.EventType.RoomMember) {
+ return;
+ }
+ this._isOutOfBand = false;
+ this.events.member = event;
+ const oldMembership = this.membership;
+ this.membership = event.getDirectionalContent().membership;
+ if (this.membership === undefined) {
+ // logging to diagnose https://github.com/vector-im/element-web/issues/20962
+ // (logs event content, although only of membership events)
+ _logger.logger.trace(`membership event with membership undefined (forwardLooking: ${event.forwardLooking})!`, event.getContent(), `prevcontent is `, event.getPrevContent());
+ }
+ this.disambiguate = shouldDisambiguate(this.userId, displayName, roomState);
+ const oldName = this.name;
+ this.name = calculateDisplayName(this.userId, displayName, this.disambiguate);
+
+ // not quite raw: we strip direction override chars so it can safely be inserted into
+ // blocks of text without breaking the text direction
+ this.rawDisplayName = (0, _utils.removeDirectionOverrideChars)(event.getDirectionalContent().displayname ?? "");
+ if (!this.rawDisplayName || !(0, _utils.removeHiddenChars)(this.rawDisplayName)) {
+ this.rawDisplayName = this.userId;
+ }
+ if (oldMembership !== this.membership) {
+ this.updateModifiedTime();
+ this.emit(RoomMemberEvent.Membership, event, this, oldMembership);
+ }
+ if (oldName !== this.name) {
+ this.updateModifiedTime();
+ this.emit(RoomMemberEvent.Name, event, this, oldName);
+ }
+ }
+
+ /**
+ * Update this room member's power level event. May fire
+ * "RoomMember.powerLevel" if this event updates this member's power levels.
+ * @param powerLevelEvent - The `m.room.power_levels` event
+ *
+ * @remarks
+ * Fires {@link RoomMemberEvent.PowerLevel}
+ */
+ setPowerLevelEvent(powerLevelEvent) {
+ if (powerLevelEvent.getType() !== _event.EventType.RoomPowerLevels || powerLevelEvent.getStateKey() !== "") {
+ return;
+ }
+ const evContent = powerLevelEvent.getDirectionalContent();
+ let maxLevel = evContent.users_default || 0;
+ const users = evContent.users || {};
+ Object.values(users).forEach(lvl => {
+ maxLevel = Math.max(maxLevel, lvl);
+ });
+ const oldPowerLevel = this.powerLevel;
+ const oldPowerLevelNorm = this.powerLevelNorm;
+ if (users[this.userId] !== undefined && Number.isInteger(users[this.userId])) {
+ this.powerLevel = users[this.userId];
+ } else if (evContent.users_default !== undefined) {
+ this.powerLevel = evContent.users_default;
+ } else {
+ this.powerLevel = 0;
+ }
+ this.powerLevelNorm = 0;
+ if (maxLevel > 0) {
+ this.powerLevelNorm = this.powerLevel * 100 / maxLevel;
+ }
+
+ // emit for changes in powerLevelNorm as well (since the app will need to
+ // redraw everyone's level if the max has changed)
+ if (oldPowerLevel !== this.powerLevel || oldPowerLevelNorm !== this.powerLevelNorm) {
+ this.updateModifiedTime();
+ this.emit(RoomMemberEvent.PowerLevel, powerLevelEvent, this);
+ }
+ }
+
+ /**
+ * Update this room member's typing event. May fire "RoomMember.typing" if
+ * this event changes this member's typing state.
+ * @param event - The typing event
+ *
+ * @remarks
+ * Fires {@link RoomMemberEvent.Typing}
+ */
+ setTypingEvent(event) {
+ if (event.getType() !== "m.typing") {
+ return;
+ }
+ const oldTyping = this.typing;
+ this.typing = false;
+ const typingList = event.getContent().user_ids;
+ if (!Array.isArray(typingList)) {
+ // malformed event :/ bail early. TODO: whine?
+ return;
+ }
+ if (typingList.indexOf(this.userId) !== -1) {
+ this.typing = true;
+ }
+ if (oldTyping !== this.typing) {
+ this.updateModifiedTime();
+ this.emit(RoomMemberEvent.Typing, event, this);
+ }
+ }
+
+ /**
+ * Update the last modified time to the current time.
+ */
+ updateModifiedTime() {
+ this.modified = Date.now();
+ }
+
+ /**
+ * Get the timestamp when this RoomMember was last updated. This timestamp is
+ * updated when properties on this RoomMember are updated.
+ * It is updated <i>before</i> firing events.
+ * @returns The timestamp
+ */
+ getLastModifiedTime() {
+ return this.modified;
+ }
+ isKicked() {
+ return this.membership === "leave" && this.events.member !== undefined && this.events.member.getSender() !== this.events.member.getStateKey();
+ }
+
+ /**
+ * If this member was invited with the is_direct flag set, return
+ * the user that invited this member
+ * @returns user id of the inviter
+ */
+ getDMInviter() {
+ // when not available because that room state hasn't been loaded in,
+ // we don't really know, but more likely to not be a direct chat
+ if (this.events.member) {
+ // TODO: persist the is_direct flag on the member as more member events
+ // come in caused by displayName changes.
+
+ // the is_direct flag is set on the invite member event.
+ // This is copied on the prev_content section of the join member event
+ // when the invite is accepted.
+
+ const memberEvent = this.events.member;
+ let memberContent = memberEvent.getContent();
+ let inviteSender = memberEvent.getSender();
+ if (memberContent.membership === "join") {
+ memberContent = memberEvent.getPrevContent();
+ inviteSender = memberEvent.getUnsigned().prev_sender;
+ }
+ if (memberContent.membership === "invite" && memberContent.is_direct) {
+ return inviteSender;
+ }
+ }
+ }
+
+ /**
+ * Get the avatar URL for a room member.
+ * @param baseUrl - The base homeserver URL See
+ * {@link MatrixClient#getHomeserverUrl}.
+ * @param width - The desired width of the thumbnail.
+ * @param height - The desired height of the thumbnail.
+ * @param resizeMethod - The thumbnail resize method to use, either
+ * "crop" or "scale".
+ * @param allowDefault - (optional) Passing false causes this method to
+ * return null if the user has no avatar image. Otherwise, a default image URL
+ * will be returned. Default: true. (Deprecated)
+ * @param allowDirectLinks - (optional) If true, the avatar URL will be
+ * returned even if it is a direct hyperlink rather than a matrix content URL.
+ * If false, any non-matrix content URLs will be ignored. Setting this option to
+ * true will expose URLs that, if fetched, will leak information about the user
+ * to anyone who they share a room with.
+ * @returns the avatar URL or null.
+ */
+ getAvatarUrl(baseUrl, width, height, resizeMethod, allowDefault = true, allowDirectLinks) {
+ const rawUrl = this.getMxcAvatarUrl();
+ if (!rawUrl && !allowDefault) {
+ return null;
+ }
+ const httpUrl = (0, _contentRepo.getHttpUriForMxc)(baseUrl, rawUrl, width, height, resizeMethod, allowDirectLinks);
+ if (httpUrl) {
+ return httpUrl;
+ }
+ return null;
+ }
+
+ /**
+ * get the mxc avatar url, either from a state event, or from a lazily loaded member
+ * @returns the mxc avatar url
+ */
+ getMxcAvatarUrl() {
+ if (this.events.member) {
+ return this.events.member.getDirectionalContent().avatar_url;
+ } else if (this.user) {
+ return this.user.avatarUrl;
+ }
+ }
+}
+exports.RoomMember = RoomMember;
+const MXID_PATTERN = /@.+:.+/;
+const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
+function shouldDisambiguate(selfUserId, displayName, roomState) {
+ if (!displayName || displayName === selfUserId) return false;
+
+ // First check if the displayname is something we consider truthy
+ // after stripping it of zero width characters and padding spaces
+ if (!(0, _utils.removeHiddenChars)(displayName)) return false;
+ if (!roomState) return false;
+
+ // Next check if the name contains something that look like a mxid
+ // If it does, it may be someone trying to impersonate someone else
+ // Show full mxid in this case
+ if (MXID_PATTERN.test(displayName)) return true;
+
+ // Also show mxid if the display name contains any LTR/RTL characters as these
+ // make it very difficult for us to find similar *looking* display names
+ // E.g "Mark" could be cloned by writing "kraM" but in RTL.
+ if (LTR_RTL_PATTERN.test(displayName)) return true;
+
+ // Also show mxid if there are other people with the same or similar
+ // displayname, after hidden character removal.
+ const userIds = roomState.getUserIdsWithDisplayName(displayName);
+ if (userIds.some(u => u !== selfUserId)) return true;
+ return false;
+}
+function calculateDisplayName(selfUserId, displayName, disambiguate) {
+ if (!displayName || displayName === selfUserId) return selfUserId;
+ if (disambiguate) return (0, _utils.removeDirectionOverrideChars)(displayName) + " (" + selfUserId + ")";
+
+ // First check if the displayname is something we consider truthy
+ // after stripping it of zero width characters and padding spaces
+ if (!(0, _utils.removeHiddenChars)(displayName)) return selfUserId;
+
+ // We always strip the direction override characters (LRO and RLO).
+ // These override the text direction for all subsequent characters
+ // in the paragraph so if display names contained these, they'd
+ // need to be wrapped in something to prevent this from leaking out
+ // (which we can do in HTML but not text) or we'd need to add
+ // control characters to the string to reset any overrides (eg.
+ // adding PDF characters at the end). As far as we can see,
+ // there should be no reason these would be necessary - rtl display
+ // names should flip into the correct direction automatically based on
+ // the characters, and you can still embed rtl in ltr or vice versa
+ // with the embed chars or marker chars.
+ return (0, _utils.removeDirectionOverrideChars)(displayName);
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-state.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-state.js
new file mode 100644
index 0000000000..69d37911cf
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-state.js
@@ -0,0 +1,931 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RoomStateEvent = exports.RoomState = void 0;
+var _roomMember = require("./room-member");
+var _logger = require("../logger");
+var _utils = require("../utils");
+var _event = require("../@types/event");
+var _event2 = require("./event");
+var _partials = require("../@types/partials");
+var _typedEventEmitter = require("./typed-event-emitter");
+var _beacon = require("./beacon");
+var _ReEmitter = require("../ReEmitter");
+var _beacon2 = require("../@types/beacon");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2015 - 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.
+ */
+// possible statuses for out-of-band member loading
+var OobStatus = /*#__PURE__*/function (OobStatus) {
+ OobStatus[OobStatus["NotStarted"] = 0] = "NotStarted";
+ OobStatus[OobStatus["InProgress"] = 1] = "InProgress";
+ OobStatus[OobStatus["Finished"] = 2] = "Finished";
+ return OobStatus;
+}(OobStatus || {});
+let RoomStateEvent = /*#__PURE__*/function (RoomStateEvent) {
+ RoomStateEvent["Events"] = "RoomState.events";
+ RoomStateEvent["Members"] = "RoomState.members";
+ RoomStateEvent["NewMember"] = "RoomState.newMember";
+ RoomStateEvent["Update"] = "RoomState.update";
+ RoomStateEvent["BeaconLiveness"] = "RoomState.BeaconLiveness";
+ RoomStateEvent["Marker"] = "RoomState.Marker";
+ return RoomStateEvent;
+}({});
+exports.RoomStateEvent = RoomStateEvent;
+class RoomState extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * Construct room state.
+ *
+ * Room State represents the state of the room at a given point.
+ * It can be mutated by adding state events to it.
+ * There are two types of room member associated with a state event:
+ * normal member objects (accessed via getMember/getMembers) which mutate
+ * with the state to represent the current state of that room/user, e.g.
+ * the object returned by `getMember('@bob:example.com')` will mutate to
+ * get a different display name if Bob later changes his display name
+ * in the room.
+ * There are also 'sentinel' members (accessed via getSentinelMember).
+ * These also represent the state of room members at the point in time
+ * represented by the RoomState object, but unlike objects from getMember,
+ * sentinel objects will always represent the room state as at the time
+ * getSentinelMember was called, so if Bob subsequently changes his display
+ * name, a room member object previously acquired with getSentinelMember
+ * will still have his old display name. Calling getSentinelMember again
+ * after the display name change will return a new RoomMember object
+ * with Bob's new display name.
+ *
+ * @param roomId - Optional. The ID of the room which has this state.
+ * If none is specified it just tracks paginationTokens, useful for notifTimelineSet
+ * @param oobMemberFlags - Optional. The state of loading out of bound members.
+ * As the timeline might get reset while they are loading, this state needs to be inherited
+ * and shared when the room state is cloned for the new timeline.
+ * This should only be passed from clone.
+ */
+ constructor(roomId, oobMemberFlags = {
+ status: OobStatus.NotStarted
+ }) {
+ super();
+ this.roomId = roomId;
+ this.oobMemberFlags = oobMemberFlags;
+ _defineProperty(this, "reEmitter", new _ReEmitter.TypedReEmitter(this));
+ _defineProperty(this, "sentinels", {});
+ // userId: RoomMember
+ // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
+ _defineProperty(this, "displayNameToUserIds", new Map());
+ _defineProperty(this, "userIdsToDisplayNames", {});
+ _defineProperty(this, "tokenToInvite", {});
+ // 3pid invite state_key to m.room.member invite
+ _defineProperty(this, "joinedMemberCount", null);
+ // cache of the number of joined members
+ // joined members count from summary api
+ // once set, we know the server supports the summary api
+ // and we should only trust that
+ // we could also only trust that before OOB members
+ // are loaded but doesn't seem worth the hassle atm
+ _defineProperty(this, "summaryJoinedMemberCount", null);
+ // same for invited member count
+ _defineProperty(this, "invitedMemberCount", null);
+ _defineProperty(this, "summaryInvitedMemberCount", null);
+ _defineProperty(this, "modified", -1);
+ // XXX: Should be read-only
+ // The room member dictionary, keyed on the user's ID.
+ _defineProperty(this, "members", {});
+ // userId: RoomMember
+ // The state events dictionary, keyed on the event type and then the state_key value.
+ _defineProperty(this, "events", new Map());
+ // Map<eventType, Map<stateKey, MatrixEvent>>
+ // The pagination token for this state.
+ _defineProperty(this, "paginationToken", null);
+ _defineProperty(this, "beacons", new Map());
+ _defineProperty(this, "_liveBeaconIds", []);
+ this.updateModifiedTime();
+ }
+
+ /**
+ * Returns the number of joined members in this room
+ * This method caches the result.
+ * @returns The number of members in this room whose membership is 'join'
+ */
+ getJoinedMemberCount() {
+ if (this.summaryJoinedMemberCount !== null) {
+ return this.summaryJoinedMemberCount;
+ }
+ if (this.joinedMemberCount === null) {
+ this.joinedMemberCount = this.getMembers().reduce((count, m) => {
+ return m.membership === "join" ? count + 1 : count;
+ }, 0);
+ }
+ return this.joinedMemberCount;
+ }
+
+ /**
+ * Set the joined member count explicitly (like from summary part of the sync response)
+ * @param count - the amount of joined members
+ */
+ setJoinedMemberCount(count) {
+ this.summaryJoinedMemberCount = count;
+ }
+
+ /**
+ * Returns the number of invited members in this room
+ * @returns The number of members in this room whose membership is 'invite'
+ */
+ getInvitedMemberCount() {
+ if (this.summaryInvitedMemberCount !== null) {
+ return this.summaryInvitedMemberCount;
+ }
+ if (this.invitedMemberCount === null) {
+ this.invitedMemberCount = this.getMembers().reduce((count, m) => {
+ return m.membership === "invite" ? count + 1 : count;
+ }, 0);
+ }
+ return this.invitedMemberCount;
+ }
+
+ /**
+ * Set the amount of invited members in this room
+ * @param count - the amount of invited members
+ */
+ setInvitedMemberCount(count) {
+ this.summaryInvitedMemberCount = count;
+ }
+
+ /**
+ * Get all RoomMembers in this room.
+ * @returns A list of RoomMembers.
+ */
+ getMembers() {
+ return Object.values(this.members);
+ }
+
+ /**
+ * Get all RoomMembers in this room, excluding the user IDs provided.
+ * @param excludedIds - The user IDs to exclude.
+ * @returns A list of RoomMembers.
+ */
+ getMembersExcept(excludedIds) {
+ return this.getMembers().filter(m => !excludedIds.includes(m.userId));
+ }
+
+ /**
+ * Get a room member by their user ID.
+ * @param userId - The room member's user ID.
+ * @returns The member or null if they do not exist.
+ */
+ getMember(userId) {
+ return this.members[userId] || null;
+ }
+
+ /**
+ * Get a room member whose properties will not change with this room state. You
+ * typically want this if you want to attach a RoomMember to a MatrixEvent which
+ * may no longer be represented correctly by Room.currentState or Room.oldState.
+ * The term 'sentinel' refers to the fact that this RoomMember is an unchanging
+ * guardian for state at this particular point in time.
+ * @param userId - The room member's user ID.
+ * @returns The member or null if they do not exist.
+ */
+ getSentinelMember(userId) {
+ if (!userId) return null;
+ let sentinel = this.sentinels[userId];
+ if (sentinel === undefined) {
+ sentinel = new _roomMember.RoomMember(this.roomId, userId);
+ const member = this.members[userId];
+ if (member?.events.member) {
+ sentinel.setMembershipEvent(member.events.member, this);
+ }
+ this.sentinels[userId] = sentinel;
+ }
+ return sentinel;
+ }
+
+ /**
+ * Get state events from the state of the room.
+ * @param eventType - The event type of the state event.
+ * @param stateKey - Optional. The state_key of the state event. If
+ * this is `undefined` then all matching state events will be
+ * returned.
+ * @returns A list of events if state_key was
+ * `undefined`, else a single event (or null if no match found).
+ */
+
+ getStateEvents(eventType, stateKey) {
+ if (!this.events.has(eventType)) {
+ // no match
+ return stateKey === undefined ? [] : null;
+ }
+ if (stateKey === undefined) {
+ // return all values
+ return Array.from(this.events.get(eventType).values());
+ }
+ const event = this.events.get(eventType).get(stateKey);
+ return event ? event : null;
+ }
+ get hasLiveBeacons() {
+ return !!this.liveBeaconIds?.length;
+ }
+ get liveBeaconIds() {
+ return this._liveBeaconIds;
+ }
+
+ /**
+ * Creates a copy of this room state so that mutations to either won't affect the other.
+ * @returns the copy of the room state
+ */
+ clone() {
+ const copy = new RoomState(this.roomId, this.oobMemberFlags);
+
+ // Ugly hack: because setStateEvents will mark
+ // members as susperseding future out of bound members
+ // if loading is in progress (through oobMemberFlags)
+ // since these are not new members, we're merely copying them
+ // set the status to not started
+ // after copying, we set back the status
+ const status = this.oobMemberFlags.status;
+ this.oobMemberFlags.status = OobStatus.NotStarted;
+ Array.from(this.events.values()).forEach(eventsByStateKey => {
+ copy.setStateEvents(Array.from(eventsByStateKey.values()));
+ });
+
+ // Ugly hack: see above
+ this.oobMemberFlags.status = status;
+ if (this.summaryInvitedMemberCount !== null) {
+ copy.setInvitedMemberCount(this.getInvitedMemberCount());
+ }
+ if (this.summaryJoinedMemberCount !== null) {
+ copy.setJoinedMemberCount(this.getJoinedMemberCount());
+ }
+
+ // copy out of band flags if needed
+ if (this.oobMemberFlags.status == OobStatus.Finished) {
+ // copy markOutOfBand flags
+ this.getMembers().forEach(member => {
+ if (member.isOutOfBand()) {
+ copy.getMember(member.userId)?.markOutOfBand();
+ }
+ });
+ }
+ return copy;
+ }
+
+ /**
+ * Add previously unknown state events.
+ * When lazy loading members while back-paginating,
+ * the relevant room state for the timeline chunk at the end
+ * of the chunk can be set with this method.
+ * @param events - state events to prepend
+ */
+ setUnknownStateEvents(events) {
+ const unknownStateEvents = events.filter(event => {
+ return !this.events.has(event.getType()) || !this.events.get(event.getType()).has(event.getStateKey());
+ });
+ this.setStateEvents(unknownStateEvents);
+ }
+
+ /**
+ * Add an array of one or more state MatrixEvents, overwriting any existing
+ * state with the same `{type, stateKey}` tuple. Will fire "RoomState.events"
+ * for every event added. May fire "RoomState.members" if there are
+ * `m.room.member` events. May fire "RoomStateEvent.Marker" if there are
+ * `UNSTABLE_MSC2716_MARKER` events.
+ * @param stateEvents - a list of state events for this room.
+ *
+ * @remarks
+ * Fires {@link RoomStateEvent.Members}
+ * Fires {@link RoomStateEvent.NewMember}
+ * Fires {@link RoomStateEvent.Events}
+ * Fires {@link RoomStateEvent.Marker}
+ */
+ setStateEvents(stateEvents, markerFoundOptions) {
+ this.updateModifiedTime();
+
+ // update the core event dict
+ stateEvents.forEach(event => {
+ if (event.getRoomId() !== this.roomId || !event.isState()) return;
+ if (_beacon2.M_BEACON_INFO.matches(event.getType())) {
+ this.setBeacon(event);
+ }
+ const lastStateEvent = this.getStateEventMatching(event);
+ this.setStateEvent(event);
+ if (event.getType() === _event.EventType.RoomMember) {
+ this.updateDisplayNameCache(event.getStateKey(), event.getContent().displayname ?? "");
+ this.updateThirdPartyTokenCache(event);
+ }
+ this.emit(RoomStateEvent.Events, event, this, lastStateEvent);
+ });
+ this.onBeaconLivenessChange();
+ // update higher level data structures. This needs to be done AFTER the
+ // core event dict as these structures may depend on other state events in
+ // the given array (e.g. disambiguating display names in one go to do both
+ // clashing names rather than progressively which only catches 1 of them).
+ stateEvents.forEach(event => {
+ if (event.getRoomId() !== this.roomId || !event.isState()) return;
+ if (event.getType() === _event.EventType.RoomMember) {
+ const userId = event.getStateKey();
+
+ // leave events apparently elide the displayname or avatar_url,
+ // so let's fake one up so that we don't leak user ids
+ // into the timeline
+ if (event.getContent().membership === "leave" || event.getContent().membership === "ban") {
+ event.getContent().avatar_url = event.getContent().avatar_url || event.getPrevContent().avatar_url;
+ event.getContent().displayname = event.getContent().displayname || event.getPrevContent().displayname;
+ }
+ const member = this.getOrCreateMember(userId, event);
+ member.setMembershipEvent(event, this);
+ this.updateMember(member);
+ this.emit(RoomStateEvent.Members, event, this, member);
+ } else if (event.getType() === _event.EventType.RoomPowerLevels) {
+ // events with unknown state keys should be ignored
+ // and should not aggregate onto members power levels
+ if (event.getStateKey() !== "") {
+ return;
+ }
+ const members = Object.values(this.members);
+ members.forEach(member => {
+ // We only propagate `RoomState.members` event if the
+ // power levels has been changed
+ // large room suffer from large re-rendering especially when not needed
+ const oldLastModified = member.getLastModifiedTime();
+ member.setPowerLevelEvent(event);
+ if (oldLastModified !== member.getLastModifiedTime()) {
+ this.emit(RoomStateEvent.Members, event, this, member);
+ }
+ });
+
+ // assume all our sentinels are now out-of-date
+ this.sentinels = {};
+ } else if (_event.UNSTABLE_MSC2716_MARKER.matches(event.getType())) {
+ this.emit(RoomStateEvent.Marker, event, markerFoundOptions);
+ }
+ });
+ this.emit(RoomStateEvent.Update, this);
+ }
+ async processBeaconEvents(events, matrixClient) {
+ if (!events.length ||
+ // discard locations if we have no beacons
+ !this.beacons.size) {
+ return;
+ }
+ const beaconByEventIdDict = [...this.beacons.values()].reduce((dict, beacon) => {
+ dict[beacon.beaconInfoId] = beacon;
+ return dict;
+ }, {});
+ const processBeaconRelation = (beaconInfoEventId, event) => {
+ if (!_beacon2.M_BEACON.matches(event.getType())) {
+ return;
+ }
+ const beacon = beaconByEventIdDict[beaconInfoEventId];
+ if (beacon) {
+ beacon.addLocations([event]);
+ }
+ };
+ for (const event of events) {
+ const relatedToEventId = event.getRelation()?.event_id;
+ // not related to a beacon we know about; discard
+ if (!relatedToEventId || !beaconByEventIdDict[relatedToEventId]) return;
+ if (!_beacon2.M_BEACON.matches(event.getType()) && !event.isEncrypted()) return;
+ try {
+ await matrixClient.decryptEventIfNeeded(event);
+ processBeaconRelation(relatedToEventId, event);
+ } catch {
+ if (event.isDecryptionFailure()) {
+ // add an event listener for once the event is decrypted.
+ event.once(_event2.MatrixEventEvent.Decrypted, async () => {
+ processBeaconRelation(relatedToEventId, event);
+ });
+ }
+ }
+ }
+ }
+
+ /**
+ * Looks up a member by the given userId, and if it doesn't exist,
+ * create it and emit the `RoomState.newMember` event.
+ * This method makes sure the member is added to the members dictionary
+ * before emitting, as this is done from setStateEvents and setOutOfBandMember.
+ * @param userId - the id of the user to look up
+ * @param event - the membership event for the (new) member. Used to emit.
+ * @returns the member, existing or newly created.
+ *
+ * @remarks
+ * Fires {@link RoomStateEvent.NewMember}
+ */
+ getOrCreateMember(userId, event) {
+ let member = this.members[userId];
+ if (!member) {
+ member = new _roomMember.RoomMember(this.roomId, userId);
+ // add member to members before emitting any events,
+ // as event handlers often lookup the member
+ this.members[userId] = member;
+ this.emit(RoomStateEvent.NewMember, event, this, member);
+ }
+ return member;
+ }
+ setStateEvent(event) {
+ if (!this.events.has(event.getType())) {
+ this.events.set(event.getType(), new Map());
+ }
+ this.events.get(event.getType()).set(event.getStateKey(), event);
+ }
+
+ /**
+ * @experimental
+ */
+ setBeacon(event) {
+ const beaconIdentifier = (0, _beacon.getBeaconInfoIdentifier)(event);
+ if (this.beacons.has(beaconIdentifier)) {
+ const beacon = this.beacons.get(beaconIdentifier);
+ if (event.isRedacted()) {
+ if (beacon.beaconInfoId === event.getRedactionEvent()?.redacts) {
+ beacon.destroy();
+ this.beacons.delete(beaconIdentifier);
+ }
+ return;
+ }
+ return beacon.update(event);
+ }
+ if (event.isRedacted()) {
+ return;
+ }
+ const beacon = new _beacon.Beacon(event);
+ this.reEmitter.reEmit(beacon, [_beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]);
+ this.emit(_beacon.BeaconEvent.New, event, beacon);
+ beacon.on(_beacon.BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
+ beacon.on(_beacon.BeaconEvent.Destroy, this.onBeaconLivenessChange.bind(this));
+ this.beacons.set(beacon.identifier, beacon);
+ }
+
+ /**
+ * @experimental
+ * Check liveness of room beacons
+ * emit RoomStateEvent.BeaconLiveness event
+ */
+ onBeaconLivenessChange() {
+ this._liveBeaconIds = Array.from(this.beacons.values()).filter(beacon => beacon.isLive).map(beacon => beacon.identifier);
+ this.emit(RoomStateEvent.BeaconLiveness, this, this.hasLiveBeacons);
+ }
+ getStateEventMatching(event) {
+ return this.events.get(event.getType())?.get(event.getStateKey()) ?? null;
+ }
+ updateMember(member) {
+ // this member may have a power level already, so set it.
+ const pwrLvlEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, "");
+ if (pwrLvlEvent) {
+ member.setPowerLevelEvent(pwrLvlEvent);
+ }
+
+ // blow away the sentinel which is now outdated
+ delete this.sentinels[member.userId];
+ this.members[member.userId] = member;
+ this.joinedMemberCount = null;
+ this.invitedMemberCount = null;
+ }
+
+ /**
+ * Get the out-of-band members loading state, whether loading is needed or not.
+ * Note that loading might be in progress and hence isn't needed.
+ * @returns whether or not the members of this room need to be loaded
+ */
+ needsOutOfBandMembers() {
+ return this.oobMemberFlags.status === OobStatus.NotStarted;
+ }
+
+ /**
+ * Check if loading of out-of-band-members has completed
+ *
+ * @returns true if the full membership list of this room has been loaded. False if it is not started or is in
+ * progress.
+ */
+ outOfBandMembersReady() {
+ return this.oobMemberFlags.status === OobStatus.Finished;
+ }
+
+ /**
+ * Mark this room state as waiting for out-of-band members,
+ * ensuring it doesn't ask for them to be requested again
+ * through needsOutOfBandMembers
+ */
+ markOutOfBandMembersStarted() {
+ if (this.oobMemberFlags.status !== OobStatus.NotStarted) {
+ return;
+ }
+ this.oobMemberFlags.status = OobStatus.InProgress;
+ }
+
+ /**
+ * Mark this room state as having failed to fetch out-of-band members
+ */
+ markOutOfBandMembersFailed() {
+ if (this.oobMemberFlags.status !== OobStatus.InProgress) {
+ return;
+ }
+ this.oobMemberFlags.status = OobStatus.NotStarted;
+ }
+
+ /**
+ * Clears the loaded out-of-band members
+ */
+ clearOutOfBandMembers() {
+ let count = 0;
+ Object.keys(this.members).forEach(userId => {
+ const member = this.members[userId];
+ if (member.isOutOfBand()) {
+ ++count;
+ delete this.members[userId];
+ }
+ });
+ _logger.logger.log(`LL: RoomState removed ${count} members...`);
+ this.oobMemberFlags.status = OobStatus.NotStarted;
+ }
+
+ /**
+ * Sets the loaded out-of-band members.
+ * @param stateEvents - array of membership state events
+ */
+ setOutOfBandMembers(stateEvents) {
+ _logger.logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
+ if (this.oobMemberFlags.status !== OobStatus.InProgress) {
+ return;
+ }
+ _logger.logger.log(`LL: RoomState put in finished state ...`);
+ this.oobMemberFlags.status = OobStatus.Finished;
+ stateEvents.forEach(e => this.setOutOfBandMember(e));
+ this.emit(RoomStateEvent.Update, this);
+ }
+
+ /**
+ * Sets a single out of band member, used by both setOutOfBandMembers and clone
+ * @param stateEvent - membership state event
+ */
+ setOutOfBandMember(stateEvent) {
+ if (stateEvent.getType() !== _event.EventType.RoomMember) {
+ return;
+ }
+ const userId = stateEvent.getStateKey();
+ const existingMember = this.getMember(userId);
+ // never replace members received as part of the sync
+ if (existingMember && !existingMember.isOutOfBand()) {
+ return;
+ }
+ const member = this.getOrCreateMember(userId, stateEvent);
+ member.setMembershipEvent(stateEvent, this);
+ // needed to know which members need to be stored seperately
+ // as they are not part of the sync accumulator
+ // this is cleared by setMembershipEvent so when it's updated through /sync
+ member.markOutOfBand();
+ this.updateDisplayNameCache(member.userId, member.name);
+ this.setStateEvent(stateEvent);
+ this.updateMember(member);
+ this.emit(RoomStateEvent.Members, stateEvent, this, member);
+ }
+
+ /**
+ * Set the current typing event for this room.
+ * @param event - The typing event
+ */
+ setTypingEvent(event) {
+ Object.values(this.members).forEach(function (member) {
+ member.setTypingEvent(event);
+ });
+ }
+
+ /**
+ * Get the m.room.member event which has the given third party invite token.
+ *
+ * @param token - The token
+ * @returns The m.room.member event or null
+ */
+ getInviteForThreePidToken(token) {
+ return this.tokenToInvite[token] || null;
+ }
+
+ /**
+ * Update the last modified time to the current time.
+ */
+ updateModifiedTime() {
+ this.modified = Date.now();
+ }
+
+ /**
+ * Get the timestamp when this room state was last updated. This timestamp is
+ * updated when this object has received new state events.
+ * @returns The timestamp
+ */
+ getLastModifiedTime() {
+ return this.modified;
+ }
+
+ /**
+ * Get user IDs with the specified or similar display names.
+ * @param displayName - The display name to get user IDs from.
+ * @returns An array of user IDs or an empty array.
+ */
+ getUserIdsWithDisplayName(displayName) {
+ return this.displayNameToUserIds.get((0, _utils.removeHiddenChars)(displayName)) ?? [];
+ }
+
+ /**
+ * Returns true if userId is in room, event is not redacted and either sender of
+ * mxEvent or has power level sufficient to redact events other than their own.
+ * @param mxEvent - The event to test permission for
+ * @param userId - The user ID of the user to test permission for
+ * @returns true if the given used ID can redact given event
+ */
+ maySendRedactionForEvent(mxEvent, userId) {
+ const member = this.getMember(userId);
+ if (!member || member.membership === "leave") return false;
+ if (mxEvent.status || mxEvent.isRedacted()) return false;
+
+ // The user may have been the sender, but they can't redact their own message
+ // if redactions are blocked.
+ const canRedact = this.maySendEvent(_event.EventType.RoomRedaction, userId);
+ if (mxEvent.getSender() === userId) return canRedact;
+ return this.hasSufficientPowerLevelFor("redact", member.powerLevel);
+ }
+
+ /**
+ * Returns true if the given power level is sufficient for action
+ * @param action - The type of power level to check
+ * @param powerLevel - The power level of the member
+ * @returns true if the given power level is sufficient
+ */
+ hasSufficientPowerLevelFor(action, powerLevel) {
+ const powerLevelsEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, "");
+ let powerLevels = {};
+ if (powerLevelsEvent) {
+ powerLevels = powerLevelsEvent.getContent();
+ }
+ let requiredLevel = 50;
+ if ((0, _utils.isNumber)(powerLevels[action])) {
+ requiredLevel = powerLevels[action];
+ }
+ return powerLevel >= requiredLevel;
+ }
+
+ /**
+ * Short-form for maySendEvent('m.room.message', userId)
+ * @param userId - The user ID of the user to test permission for
+ * @returns true if the given user ID should be permitted to send
+ * message events into the given room.
+ */
+ maySendMessage(userId) {
+ return this.maySendEventOfType(_event.EventType.RoomMessage, userId, false);
+ }
+
+ /**
+ * Returns true if the given user ID has permission to send a normal
+ * event of type `eventType` into this room.
+ * @param eventType - The type of event to test
+ * @param userId - The user ID of the user to test permission for
+ * @returns true if the given user ID should be permitted to send
+ * the given type of event into this room,
+ * according to the room's state.
+ */
+ maySendEvent(eventType, userId) {
+ return this.maySendEventOfType(eventType, userId, false);
+ }
+
+ /**
+ * Returns true if the given MatrixClient has permission to send a state
+ * event of type `stateEventType` into this room.
+ * @param stateEventType - The type of state events to test
+ * @param cli - The client to test permission for
+ * @returns true if the given client should be permitted to send
+ * the given type of state event into this room,
+ * according to the room's state.
+ */
+ mayClientSendStateEvent(stateEventType, cli) {
+ if (cli.isGuest() || !cli.credentials.userId) {
+ return false;
+ }
+ return this.maySendStateEvent(stateEventType, cli.credentials.userId);
+ }
+
+ /**
+ * Returns true if the given user ID has permission to send a state
+ * event of type `stateEventType` into this room.
+ * @param stateEventType - The type of state events to test
+ * @param userId - The user ID of the user to test permission for
+ * @returns true if the given user ID should be permitted to send
+ * the given type of state event into this room,
+ * according to the room's state.
+ */
+ maySendStateEvent(stateEventType, userId) {
+ return this.maySendEventOfType(stateEventType, userId, true);
+ }
+
+ /**
+ * Returns true if the given user ID has permission to send a normal or state
+ * event of type `eventType` into this room.
+ * @param eventType - The type of event to test
+ * @param userId - The user ID of the user to test permission for
+ * @param state - If true, tests if the user may send a state
+ event of this type. Otherwise tests whether
+ they may send a regular event.
+ * @returns true if the given user ID should be permitted to send
+ * the given type of event into this room,
+ * according to the room's state.
+ */
+ maySendEventOfType(eventType, userId, state) {
+ const powerLevelsEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, "");
+ let powerLevels;
+ let eventsLevels = {};
+ let stateDefault = 0;
+ let eventsDefault = 0;
+ let powerLevel = 0;
+ if (powerLevelsEvent) {
+ powerLevels = powerLevelsEvent.getContent();
+ eventsLevels = powerLevels.events || {};
+ if (Number.isSafeInteger(powerLevels.state_default)) {
+ stateDefault = powerLevels.state_default;
+ } else {
+ stateDefault = 50;
+ }
+ const userPowerLevel = powerLevels.users && powerLevels.users[userId];
+ if (Number.isSafeInteger(userPowerLevel)) {
+ powerLevel = userPowerLevel;
+ } else if (Number.isSafeInteger(powerLevels.users_default)) {
+ powerLevel = powerLevels.users_default;
+ }
+ if (Number.isSafeInteger(powerLevels.events_default)) {
+ eventsDefault = powerLevels.events_default;
+ }
+ }
+ let requiredLevel = state ? stateDefault : eventsDefault;
+ if (Number.isSafeInteger(eventsLevels[eventType])) {
+ requiredLevel = eventsLevels[eventType];
+ }
+ return powerLevel >= requiredLevel;
+ }
+
+ /**
+ * Returns true if the given user ID has permission to trigger notification
+ * of type `notifLevelKey`
+ * @param notifLevelKey - The level of notification to test (eg. 'room')
+ * @param userId - The user ID of the user to test permission for
+ * @returns true if the given user ID has permission to trigger a
+ * notification of this type.
+ */
+ mayTriggerNotifOfType(notifLevelKey, userId) {
+ const member = this.getMember(userId);
+ if (!member) {
+ return false;
+ }
+ const powerLevelsEvent = this.getStateEvents(_event.EventType.RoomPowerLevels, "");
+ let notifLevel = 50;
+ if (powerLevelsEvent && powerLevelsEvent.getContent() && powerLevelsEvent.getContent().notifications && (0, _utils.isNumber)(powerLevelsEvent.getContent().notifications[notifLevelKey])) {
+ notifLevel = powerLevelsEvent.getContent().notifications[notifLevelKey];
+ }
+ return member.powerLevel >= notifLevel;
+ }
+
+ /**
+ * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
+ * @returns the join_rule applied to this room
+ */
+ getJoinRule() {
+ const joinRuleEvent = this.getStateEvents(_event.EventType.RoomJoinRules, "");
+ const joinRuleContent = joinRuleEvent?.getContent() ?? {};
+ return joinRuleContent["join_rule"] || _partials.JoinRule.Invite;
+ }
+
+ /**
+ * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
+ * @returns the history_visibility applied to this room
+ */
+ getHistoryVisibility() {
+ const historyVisibilityEvent = this.getStateEvents(_event.EventType.RoomHistoryVisibility, "");
+ const historyVisibilityContent = historyVisibilityEvent?.getContent() ?? {};
+ return historyVisibilityContent["history_visibility"] || _partials.HistoryVisibility.Shared;
+ }
+
+ /**
+ * Returns the guest access based on the m.room.guest_access state event, defaulting to `shared`.
+ * @returns the guest_access applied to this room
+ */
+ getGuestAccess() {
+ const guestAccessEvent = this.getStateEvents(_event.EventType.RoomGuestAccess, "");
+ const guestAccessContent = guestAccessEvent?.getContent() ?? {};
+ return guestAccessContent["guest_access"] || _partials.GuestAccess.Forbidden;
+ }
+
+ /**
+ * Find the predecessor room based on this room state.
+ *
+ * @param msc3946ProcessDynamicPredecessor - if true, look for an
+ * m.room.predecessor state event and use it if found (MSC3946).
+ * @returns null if this room has no predecessor. Otherwise, returns
+ * the roomId, last eventId and viaServers of the predecessor room.
+ *
+ * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events
+ * as well as m.room.create events to find predecessors.
+ *
+ * Note: if an m.predecessor event is used, eventId may be undefined
+ * since last_known_event_id is optional.
+ *
+ * Note: viaServers may be undefined, and will definitely be undefined if
+ * this predecessor comes from a RoomCreate event (rather than a
+ * RoomPredecessor, which has the optional via_servers property).
+ */
+ findPredecessor(msc3946ProcessDynamicPredecessor = false) {
+ // Note: the tests for this function are against Room.findPredecessor,
+ // which just calls through to here.
+
+ if (msc3946ProcessDynamicPredecessor) {
+ const predecessorEvent = this.getStateEvents(_event.EventType.RoomPredecessor, "");
+ if (predecessorEvent) {
+ const content = predecessorEvent.getContent();
+ const roomId = content.predecessor_room_id;
+ let eventId = content.last_known_event_id;
+ if (typeof eventId !== "string") {
+ eventId = undefined;
+ }
+ let viaServers = content.via_servers;
+ if (!Array.isArray(viaServers)) {
+ viaServers = undefined;
+ }
+ if (typeof roomId === "string") {
+ return {
+ roomId,
+ eventId,
+ viaServers
+ };
+ }
+ }
+ }
+ const createEvent = this.getStateEvents(_event.EventType.RoomCreate, "");
+ if (createEvent) {
+ const predecessor = createEvent.getContent()["predecessor"];
+ if (predecessor) {
+ const roomId = predecessor["room_id"];
+ if (typeof roomId === "string") {
+ let eventId = predecessor["event_id"];
+ if (typeof eventId !== "string" || eventId === "") {
+ eventId = undefined;
+ }
+ return {
+ roomId,
+ eventId
+ };
+ }
+ }
+ }
+ return null;
+ }
+ updateThirdPartyTokenCache(memberEvent) {
+ if (!memberEvent.getContent().third_party_invite) {
+ return;
+ }
+ const token = (memberEvent.getContent().third_party_invite.signed || {}).token;
+ if (!token) {
+ return;
+ }
+ const threePidInvite = this.getStateEvents(_event.EventType.RoomThirdPartyInvite, token);
+ if (!threePidInvite) {
+ return;
+ }
+ this.tokenToInvite[token] = memberEvent;
+ }
+ updateDisplayNameCache(userId, displayName) {
+ const oldName = this.userIdsToDisplayNames[userId];
+ delete this.userIdsToDisplayNames[userId];
+ if (oldName) {
+ // Remove the old name from the cache.
+ // We clobber the user_id > name lookup but the name -> [user_id] lookup
+ // means we need to remove that user ID from that array rather than nuking
+ // the lot.
+ const strippedOldName = (0, _utils.removeHiddenChars)(oldName);
+ const existingUserIds = this.displayNameToUserIds.get(strippedOldName);
+ if (existingUserIds) {
+ // remove this user ID from this array
+ const filteredUserIDs = existingUserIds.filter(id => id !== userId);
+ this.displayNameToUserIds.set(strippedOldName, filteredUserIDs);
+ }
+ }
+ this.userIdsToDisplayNames[userId] = displayName;
+ const strippedDisplayname = displayName && (0, _utils.removeHiddenChars)(displayName);
+ // an empty stripped displayname (undefined/'') will be set to MXID in room-member.js
+ if (strippedDisplayname) {
+ const arr = this.displayNameToUserIds.get(strippedDisplayname) ?? [];
+ arr.push(userId);
+ this.displayNameToUserIds.set(strippedDisplayname, arr);
+ }
+ }
+}
+exports.RoomState = RoomState; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js
new file mode 100644
index 0000000000..44296632b4
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/room-summary.js
@@ -0,0 +1,34 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RoomSummary = void 0;
+/*
+Copyright 2015 - 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.
+*/
+
+/**
+ * Construct a new Room Summary. A summary can be used for display on a recent
+ * list, without having to load the entire room list into memory.
+ * @param roomId - Required. The ID of this room.
+ * @param info - Optional. The summary info. Additional keys are supported.
+ */
+class RoomSummary {
+ constructor(roomId, info) {
+ this.roomId = roomId;
+ }
+}
+exports.RoomSummary = RoomSummary; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/room.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/room.js
new file mode 100644
index 0000000000..d1bc837971
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/room.js
@@ -0,0 +1,3079 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.RoomNameType = exports.RoomEvent = exports.Room = exports.NotificationCountType = exports.KNOWN_SAFE_ROOM_VERSION = void 0;
+var _matrixEventsSdk = require("matrix-events-sdk");
+var _eventTimelineSet = require("./event-timeline-set");
+var _eventTimeline = require("./event-timeline");
+var _contentRepo = require("../content-repo");
+var _utils = require("../utils");
+var _event = require("./event");
+var _eventStatus = require("./event-status");
+var _roomMember = require("./room-member");
+var _roomSummary = require("./room-summary");
+var _logger = require("../logger");
+var _ReEmitter = require("../ReEmitter");
+var _event2 = require("../@types/event");
+var _client = require("../client");
+var _filter = require("../filter");
+var _roomState = require("./room-state");
+var _beacon = require("./beacon");
+var _thread = require("./thread");
+var _read_receipts = require("../@types/read_receipts");
+var _relationsContainer = require("./relations-container");
+var _readReceipt = require("./read-receipt");
+var _poll = require("./poll");
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2015 - 2023 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.
+ */
+// These constants are used as sane defaults when the homeserver doesn't support
+// the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be
+// the same as the common default room version whereas SAFE_ROOM_VERSIONS are the
+// room versions which are considered okay for people to run without being asked
+// to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers
+// return an m.room_versions capability.
+const KNOWN_SAFE_ROOM_VERSION = "10";
+exports.KNOWN_SAFE_ROOM_VERSION = KNOWN_SAFE_ROOM_VERSION;
+const SAFE_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"];
+// When inserting a visibility event affecting event `eventId`, we
+// need to scan through existing visibility events for `eventId`.
+// In theory, this could take an unlimited amount of time if:
+//
+// - the visibility event was sent by a moderator; and
+// - `eventId` already has many visibility changes (usually, it should
+// be 2 or less); and
+// - for some reason, the visibility changes are received out of order
+// (usually, this shouldn't happen at all).
+//
+// For this reason, we limit the number of events to scan through,
+// expecting that a broken visibility change for a single event in
+// an extremely uncommon case (possibly a DoS) is a small
+// price to pay to keep matrix-js-sdk responsive.
+const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30;
+let NotificationCountType = /*#__PURE__*/function (NotificationCountType) {
+ NotificationCountType["Highlight"] = "highlight";
+ NotificationCountType["Total"] = "total";
+ return NotificationCountType;
+}({});
+exports.NotificationCountType = NotificationCountType;
+let RoomEvent = /*#__PURE__*/function (RoomEvent) {
+ RoomEvent["MyMembership"] = "Room.myMembership";
+ RoomEvent["Tags"] = "Room.tags";
+ RoomEvent["AccountData"] = "Room.accountData";
+ RoomEvent["Receipt"] = "Room.receipt";
+ RoomEvent["Name"] = "Room.name";
+ RoomEvent["Redaction"] = "Room.redaction";
+ RoomEvent["RedactionCancelled"] = "Room.redactionCancelled";
+ RoomEvent["LocalEchoUpdated"] = "Room.localEchoUpdated";
+ RoomEvent["Timeline"] = "Room.timeline";
+ RoomEvent["TimelineReset"] = "Room.timelineReset";
+ RoomEvent["TimelineRefresh"] = "Room.TimelineRefresh";
+ RoomEvent["OldStateUpdated"] = "Room.OldStateUpdated";
+ RoomEvent["CurrentStateUpdated"] = "Room.CurrentStateUpdated";
+ RoomEvent["HistoryImportedWithinTimeline"] = "Room.historyImportedWithinTimeline";
+ RoomEvent["UnreadNotifications"] = "Room.UnreadNotifications";
+ return RoomEvent;
+}({});
+exports.RoomEvent = RoomEvent;
+class Room extends _readReceipt.ReadReceipt {
+ /**
+ * Construct a new Room.
+ *
+ * <p>For a room, we store an ordered sequence of timelines, which may or may not
+ * be continuous. Each timeline lists a series of events, as well as tracking
+ * the room state at the start and the end of the timeline. It also tracks
+ * forward and backward pagination tokens, as well as containing links to the
+ * next timeline in the sequence.
+ *
+ * <p>There is one special timeline - the 'live' timeline, which represents the
+ * timeline to which events are being added in real-time as they are received
+ * from the /sync API. Note that you should not retain references to this
+ * timeline - even if it is the current timeline right now, it may not remain
+ * so if the server gives us a timeline gap in /sync.
+ *
+ * <p>In order that we can find events from their ids later, we also maintain a
+ * map from event_id to timeline and index.
+ *
+ * @param roomId - Required. The ID of this room.
+ * @param client - Required. The client, used to lazy load members.
+ * @param myUserId - Required. The ID of the syncing user.
+ * @param opts - Configuration options
+ */
+ constructor(roomId, client, myUserId, opts = {}) {
+ super();
+ // In some cases, we add listeners for every displayed Matrix event, so it's
+ // common to have quite a few more than the default limit.
+ this.roomId = roomId;
+ this.client = client;
+ this.myUserId = myUserId;
+ this.opts = opts;
+ _defineProperty(this, "reEmitter", void 0);
+ _defineProperty(this, "txnToEvent", new Map());
+ // Pending in-flight requests { string: MatrixEvent }
+ _defineProperty(this, "notificationCounts", {});
+ _defineProperty(this, "threadNotifications", new Map());
+ _defineProperty(this, "cachedThreadReadReceipts", new Map());
+ // Useful to know at what point the current user has started using threads in this room
+ _defineProperty(this, "oldestThreadedReceiptTs", Infinity);
+ /**
+ * A record of the latest unthread receipts per user
+ * This is useful in determining whether a user has read a thread or not
+ */
+ _defineProperty(this, "unthreadedReceipts", new Map());
+ _defineProperty(this, "timelineSets", void 0);
+ _defineProperty(this, "polls", new Map());
+ /**
+ * Empty array if the timeline sets have not been initialised. After initialisation:
+ * 0: All threads
+ * 1: Threads the current user has participated in
+ */
+ _defineProperty(this, "threadsTimelineSets", []);
+ // any filtered timeline sets we're maintaining for this room
+ _defineProperty(this, "filteredTimelineSets", {});
+ // filter_id: timelineSet
+ _defineProperty(this, "timelineNeedsRefresh", false);
+ _defineProperty(this, "pendingEventList", void 0);
+ // read by megolm via getter; boolean value - null indicates "use global value"
+ _defineProperty(this, "blacklistUnverifiedDevices", void 0);
+ _defineProperty(this, "selfMembership", void 0);
+ _defineProperty(this, "summaryHeroes", null);
+ // flags to stop logspam about missing m.room.create events
+ _defineProperty(this, "getTypeWarning", false);
+ _defineProperty(this, "getVersionWarning", false);
+ _defineProperty(this, "membersPromise", void 0);
+ // XXX: These should be read-only
+ /**
+ * The human-readable display name for this room.
+ */
+ _defineProperty(this, "name", void 0);
+ /**
+ * The un-homoglyphed name for this room.
+ */
+ _defineProperty(this, "normalizedName", void 0);
+ /**
+ * Dict of room tags; the keys are the tag name and the values
+ * are any metadata associated with the tag - e.g. `{ "fav" : { order: 1 } }`
+ */
+ _defineProperty(this, "tags", {});
+ // $tagName: { $metadata: $value }
+ /**
+ * accountData Dict of per-room account_data events; the keys are the
+ * event type and the values are the events.
+ */
+ _defineProperty(this, "accountData", new Map());
+ // $eventType: $event
+ /**
+ * The room summary.
+ */
+ _defineProperty(this, "summary", null);
+ /**
+ * The live event timeline for this room, with the oldest event at index 0.
+ *
+ * @deprecated Present for backwards compatibility.
+ * Use getLiveTimeline().getEvents() instead
+ */
+ _defineProperty(this, "timeline", void 0);
+ /**
+ * oldState The state of the room at the time of the oldest event in the live timeline.
+ *
+ * @deprecated Present for backwards compatibility.
+ * Use getLiveTimeline().getState(EventTimeline.BACKWARDS) instead
+ */
+ _defineProperty(this, "oldState", void 0);
+ /**
+ * currentState The state of the room at the time of the newest event in the timeline.
+ *
+ * @deprecated Present for backwards compatibility.
+ * Use getLiveTimeline().getState(EventTimeline.FORWARDS) instead.
+ */
+ _defineProperty(this, "currentState", void 0);
+ _defineProperty(this, "relations", new _relationsContainer.RelationsContainer(this.client, this));
+ /**
+ * A collection of events known by the client
+ * This is not a comprehensive list of the threads that exist in this room
+ */
+ _defineProperty(this, "threads", new Map());
+ /**
+ * @deprecated This value is unreliable. It may not contain the last thread.
+ * Use {@link Room.getLastThread} instead.
+ */
+ _defineProperty(this, "lastThread", void 0);
+ /**
+ * A mapping of eventId to all visibility changes to apply
+ * to the event, by chronological order, as per
+ * https://github.com/matrix-org/matrix-doc/pull/3531
+ *
+ * # Invariants
+ *
+ * - within each list, all events are classed by
+ * chronological order;
+ * - all events are events such that
+ * `asVisibilityEvent()` returns a non-null `IVisibilityChange`;
+ * - within each list with key `eventId`, all events
+ * are in relation to `eventId`.
+ *
+ * @experimental
+ */
+ _defineProperty(this, "visibilityEvents", new Map());
+ _defineProperty(this, "threadTimelineSetsPromise", null);
+ _defineProperty(this, "threadsReady", false);
+ _defineProperty(this, "updateThreadRootEvents", (thread, toStartOfTimeline, recreateEvent) => {
+ if (thread.length) {
+ this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent);
+ if (thread.hasCurrentUserParticipated) {
+ this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent);
+ }
+ }
+ });
+ _defineProperty(this, "updateThreadRootEvent", (timelineSet, thread, toStartOfTimeline, recreateEvent) => {
+ if (timelineSet && thread.rootEvent) {
+ if (recreateEvent) {
+ timelineSet.removeEvent(thread.id);
+ }
+ if (_thread.Thread.hasServerSideSupport) {
+ timelineSet.addLiveEvent(thread.rootEvent, {
+ duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Replace,
+ fromCache: false,
+ roomState: this.currentState
+ });
+ } else {
+ timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), {
+ toStartOfTimeline
+ });
+ }
+ }
+ });
+ _defineProperty(this, "applyRedaction", event => {
+ if (event.isRedaction()) {
+ const redactId = event.event.redacts;
+
+ // if we know about this event, redact its contents now.
+ const redactedEvent = redactId ? this.findEventById(redactId) : undefined;
+ if (redactedEvent) {
+ redactedEvent.makeRedacted(event);
+
+ // If this is in the current state, replace it with the redacted version
+ if (redactedEvent.isState()) {
+ const currentStateEvent = this.currentState.getStateEvents(redactedEvent.getType(), redactedEvent.getStateKey());
+ if (currentStateEvent?.getId() === redactedEvent.getId()) {
+ this.currentState.setStateEvents([redactedEvent]);
+ }
+ }
+ this.emit(RoomEvent.Redaction, event, this);
+
+ // TODO: we stash user displaynames (among other things) in
+ // RoomMember objects which are then attached to other events
+ // (in the sender and target fields). We should get those
+ // RoomMember objects to update themselves when the events that
+ // they are based on are changed.
+
+ // Remove any visibility change on this event.
+ this.visibilityEvents.delete(redactId);
+
+ // If this event is a visibility change event, remove it from the
+ // list of visibility changes and update any event affected by it.
+ if (redactedEvent.isVisibilityEvent()) {
+ this.redactVisibilityChangeEvent(event);
+ }
+ }
+
+ // FIXME: apply redactions to notification list
+
+ // NB: We continue to add the redaction event to the timeline so
+ // clients can say "so and so redacted an event" if they wish to. Also
+ // this may be needed to trigger an update.
+ }
+ });
+ this.setMaxListeners(100);
+ this.reEmitter = new _ReEmitter.TypedReEmitter(this);
+ opts.pendingEventOrdering = opts.pendingEventOrdering || _client.PendingEventOrdering.Chronological;
+ this.name = roomId;
+ this.normalizedName = roomId;
+
+ // all our per-room timeline sets. the first one is the unfiltered ones;
+ // the subsequent ones are the filtered ones in no particular order.
+ this.timelineSets = [new _eventTimelineSet.EventTimelineSet(this, opts)];
+ this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [RoomEvent.Timeline, RoomEvent.TimelineReset]);
+ this.fixUpLegacyTimelineFields();
+ if (this.opts.pendingEventOrdering === _client.PendingEventOrdering.Detached) {
+ this.pendingEventList = [];
+ this.client.store.getPendingEvents(this.roomId).then(events => {
+ const mapper = this.client.getEventMapper({
+ toDevice: false,
+ decrypt: false
+ });
+ events.forEach(async serializedEvent => {
+ const event = mapper(serializedEvent);
+ await client.decryptEventIfNeeded(event);
+ event.setStatus(_eventStatus.EventStatus.NOT_SENT);
+ this.addPendingEvent(event, event.getTxnId());
+ });
+ });
+ }
+
+ // awaited by getEncryptionTargetMembers while room members are loading
+ if (!this.opts.lazyLoadMembers) {
+ this.membersPromise = Promise.resolve(false);
+ } else {
+ this.membersPromise = undefined;
+ }
+ }
+ async createThreadsTimelineSets() {
+ if (this.threadTimelineSetsPromise) {
+ return this.threadTimelineSetsPromise;
+ }
+ if (this.client?.supportsThreads()) {
+ try {
+ this.threadTimelineSetsPromise = Promise.all([this.createThreadTimelineSet(), this.createThreadTimelineSet(_thread.ThreadFilterType.My)]);
+ const timelineSets = await this.threadTimelineSetsPromise;
+ this.threadsTimelineSets[0] = timelineSets[0];
+ this.threadsTimelineSets[1] = timelineSets[1];
+ return timelineSets;
+ } catch (e) {
+ this.threadTimelineSetsPromise = null;
+ return null;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Bulk decrypt critical events in a room
+ *
+ * Critical events represents the minimal set of events to decrypt
+ * for a typical UI to function properly
+ *
+ * - Last event of every room (to generate likely message preview)
+ * - All events up to the read receipt (to calculate an accurate notification count)
+ *
+ * @returns Signals when all events have been decrypted
+ */
+ async decryptCriticalEvents() {
+ if (!this.client.isCryptoEnabled()) return;
+ const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId(), true);
+ const events = this.getLiveTimeline().getEvents();
+ const readReceiptTimelineIndex = events.findIndex(matrixEvent => {
+ return matrixEvent.event.event_id === readReceiptEventId;
+ });
+ const decryptionPromises = events.slice(readReceiptTimelineIndex).reverse().map(event => this.client.decryptEventIfNeeded(event, {
+ isRetry: true
+ }));
+ await Promise.allSettled(decryptionPromises);
+ }
+
+ /**
+ * Bulk decrypt events in a room
+ *
+ * @returns Signals when all events have been decrypted
+ */
+ async decryptAllEvents() {
+ if (!this.client.isCryptoEnabled()) return;
+ const decryptionPromises = this.getUnfilteredTimelineSet().getLiveTimeline().getEvents().slice(0) // copy before reversing
+ .reverse().map(event => this.client.decryptEventIfNeeded(event, {
+ isRetry: true
+ }));
+ await Promise.allSettled(decryptionPromises);
+ }
+
+ /**
+ * Gets the creator of the room
+ * @returns The creator of the room, or null if it could not be determined
+ */
+ getCreator() {
+ const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, "");
+ return createEvent?.getContent()["creator"] ?? null;
+ }
+
+ /**
+ * Gets the version of the room
+ * @returns The version of the room, or null if it could not be determined
+ */
+ getVersion() {
+ const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, "");
+ if (!createEvent) {
+ if (!this.getVersionWarning) {
+ _logger.logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event");
+ this.getVersionWarning = true;
+ }
+ return "1";
+ }
+ return createEvent.getContent()["room_version"] ?? "1";
+ }
+
+ /**
+ * Determines whether this room needs to be upgraded to a new version
+ * @returns What version the room should be upgraded to, or null if
+ * the room does not require upgrading at this time.
+ * @deprecated Use #getRecommendedVersion() instead
+ */
+ shouldUpgradeToVersion() {
+ // TODO: Remove this function.
+ // This makes assumptions about which versions are safe, and can easily
+ // be wrong. Instead, people are encouraged to use getRecommendedVersion
+ // which determines a safer value. This function doesn't use that function
+ // because this is not async-capable, and to avoid breaking the contract
+ // we're deprecating this.
+
+ if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) {
+ return KNOWN_SAFE_ROOM_VERSION;
+ }
+ return null;
+ }
+
+ /**
+ * Determines the recommended room version for the room. This returns an
+ * object with 3 properties: `version` as the new version the
+ * room should be upgraded to (may be the same as the current version);
+ * `needsUpgrade` to indicate if the room actually can be
+ * upgraded (ie: does the current version not match?); and `urgent`
+ * to indicate if the new version patches a vulnerability in a previous
+ * version.
+ * @returns
+ * Resolves to the version the room should be upgraded to.
+ */
+ async getRecommendedVersion() {
+ const capabilities = await this.client.getCapabilities();
+ let versionCap = capabilities["m.room_versions"];
+ if (!versionCap) {
+ versionCap = {
+ default: KNOWN_SAFE_ROOM_VERSION,
+ available: {}
+ };
+ for (const safeVer of SAFE_ROOM_VERSIONS) {
+ versionCap.available[safeVer] = _client.RoomVersionStability.Stable;
+ }
+ }
+ let result = this.checkVersionAgainstCapability(versionCap);
+ if (result.urgent && result.needsUpgrade) {
+ // Something doesn't feel right: we shouldn't need to update
+ // because the version we're on should be in the protocol's
+ // namespace. This usually means that the server was updated
+ // before the client was, making us think the newest possible
+ // room version is not stable. As a solution, we'll refresh
+ // the capability we're using to determine this.
+ _logger.logger.warn("Refreshing room version capability because the server looks " + "to be supporting a newer room version we don't know about.");
+ const caps = await this.client.getCapabilities(true);
+ versionCap = caps["m.room_versions"];
+ if (!versionCap) {
+ _logger.logger.warn("No room version capability - assuming upgrade required.");
+ return result;
+ } else {
+ result = this.checkVersionAgainstCapability(versionCap);
+ }
+ }
+ return result;
+ }
+ checkVersionAgainstCapability(versionCap) {
+ const currentVersion = this.getVersion();
+ _logger.logger.log(`[${this.roomId}] Current version: ${currentVersion}`);
+ _logger.logger.log(`[${this.roomId}] Version capability: `, versionCap);
+ const result = {
+ version: currentVersion,
+ needsUpgrade: false,
+ urgent: false
+ };
+
+ // If the room is on the default version then nothing needs to change
+ if (currentVersion === versionCap.default) return result;
+ const stableVersions = Object.keys(versionCap.available).filter(v => versionCap.available[v] === "stable");
+
+ // Check if the room is on an unstable version. We determine urgency based
+ // off the version being in the Matrix spec namespace or not (if the version
+ // is in the current namespace and unstable, the room is probably vulnerable).
+ if (!stableVersions.includes(currentVersion)) {
+ result.version = versionCap.default;
+ result.needsUpgrade = true;
+ result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g);
+ if (result.urgent) {
+ _logger.logger.warn(`URGENT upgrade required on ${this.roomId}`);
+ } else {
+ _logger.logger.warn(`Non-urgent upgrade required on ${this.roomId}`);
+ }
+ return result;
+ }
+
+ // The room is on a stable, but non-default, version by this point.
+ // No upgrade needed.
+ return result;
+ }
+
+ /**
+ * Determines whether the given user is permitted to perform a room upgrade
+ * @param userId - The ID of the user to test against
+ * @returns True if the given user is permitted to upgrade the room
+ */
+ userMayUpgradeRoom(userId) {
+ return this.currentState.maySendStateEvent(_event2.EventType.RoomTombstone, userId);
+ }
+
+ /**
+ * Get the list of pending sent events for this room
+ *
+ * @returns A list of the sent events
+ * waiting for remote echo.
+ *
+ * @throws If `opts.pendingEventOrdering` was not 'detached'
+ */
+ getPendingEvents() {
+ if (!this.pendingEventList) {
+ throw new Error("Cannot call getPendingEvents with pendingEventOrdering == " + this.opts.pendingEventOrdering);
+ }
+ return this.pendingEventList;
+ }
+
+ /**
+ * Removes a pending event for this room
+ *
+ * @returns True if an element was removed.
+ */
+ removePendingEvent(eventId) {
+ if (!this.pendingEventList) {
+ throw new Error("Cannot call removePendingEvent with pendingEventOrdering == " + this.opts.pendingEventOrdering);
+ }
+ const removed = (0, _utils.removeElement)(this.pendingEventList, function (ev) {
+ return ev.getId() == eventId;
+ }, false);
+ this.savePendingEvents();
+ return removed;
+ }
+
+ /**
+ * Check whether the pending event list contains a given event by ID.
+ * If pending event ordering is not "detached" then this returns false.
+ *
+ * @param eventId - The event ID to check for.
+ */
+ hasPendingEvent(eventId) {
+ return this.pendingEventList?.some(event => event.getId() === eventId) ?? false;
+ }
+
+ /**
+ * Get a specific event from the pending event list, if configured, null otherwise.
+ *
+ * @param eventId - The event ID to check for.
+ */
+ getPendingEvent(eventId) {
+ return this.pendingEventList?.find(event => event.getId() === eventId) ?? null;
+ }
+
+ /**
+ * Get the live unfiltered timeline for this room.
+ *
+ * @returns live timeline
+ */
+ getLiveTimeline() {
+ return this.getUnfilteredTimelineSet().getLiveTimeline();
+ }
+
+ /**
+ * Get the timestamp of the last message in the room
+ *
+ * @returns the timestamp of the last message in the room
+ */
+ getLastActiveTimestamp() {
+ const timeline = this.getLiveTimeline();
+ const events = timeline.getEvents();
+ if (events.length) {
+ const lastEvent = events[events.length - 1];
+ return lastEvent.getTs();
+ } else {
+ return Number.MIN_SAFE_INTEGER;
+ }
+ }
+
+ /**
+ * Returns the last live event of this room.
+ * "last" means latest timestamp.
+ * Instead of using timestamps, it would be better to do the comparison based on the order of the homeserver DAG.
+ * Unfortunately, this information is currently not available in the client.
+ * See {@link https://github.com/matrix-org/matrix-js-sdk/issues/3325}.
+ * "live of this room" means from all live timelines: the room and the threads.
+ *
+ * @returns MatrixEvent if there is a last event; else undefined.
+ */
+ getLastLiveEvent() {
+ const roomEvents = this.getLiveTimeline().getEvents();
+ const lastRoomEvent = roomEvents[roomEvents.length - 1];
+ const lastThread = this.getLastThread();
+ if (!lastThread) return lastRoomEvent;
+ const lastThreadEvent = lastThread.events[lastThread.events.length - 1];
+ return (lastRoomEvent?.getTs() ?? 0) > (lastThreadEvent?.getTs() ?? 0) ? lastRoomEvent : lastThreadEvent;
+ }
+
+ /**
+ * Returns the last thread of this room.
+ * "last" means latest timestamp of the last thread event.
+ * Instead of using timestamps, it would be better to do the comparison based on the order of the homeserver DAG.
+ * Unfortunately, this information is currently not available in the client.
+ * See {@link https://github.com/matrix-org/matrix-js-sdk/issues/3325}.
+ *
+ * @returns the thread with the most recent event in its live time line. undefined if there is no thread.
+ */
+ getLastThread() {
+ return this.getThreads().reduce((lastThread, thread) => {
+ if (!lastThread) return thread;
+ const threadEvent = thread.events[thread.events.length - 1];
+ const lastThreadEvent = lastThread.events[lastThread.events.length - 1];
+ if ((threadEvent?.getTs() ?? 0) >= (lastThreadEvent?.getTs() ?? 0)) {
+ // Last message of current thread is newer → new last thread.
+ // Equal also means newer, because it was added to the thread map later.
+ return thread;
+ }
+ return lastThread;
+ }, undefined);
+ }
+
+ /**
+ * @returns the membership type (join | leave | invite) for the logged in user
+ */
+ getMyMembership() {
+ return this.selfMembership ?? "leave";
+ }
+
+ /**
+ * If this room is a DM we're invited to,
+ * try to find out who invited us
+ * @returns user id of the inviter
+ */
+ getDMInviter() {
+ const me = this.getMember(this.myUserId);
+ if (me) {
+ return me.getDMInviter();
+ }
+ if (this.selfMembership === "invite") {
+ // fall back to summary information
+ const memberCount = this.getInvitedAndJoinedMemberCount();
+ if (memberCount === 2) {
+ return this.summaryHeroes?.[0];
+ }
+ }
+ }
+
+ /**
+ * Assuming this room is a DM room, tries to guess with which user.
+ * @returns user id of the other member (could be syncing user)
+ */
+ guessDMUserId() {
+ const me = this.getMember(this.myUserId);
+ if (me) {
+ const inviterId = me.getDMInviter();
+ if (inviterId) {
+ return inviterId;
+ }
+ }
+ // Remember, we're assuming this room is a DM, so returning the first member we find should be fine
+ if (Array.isArray(this.summaryHeroes) && this.summaryHeroes.length) {
+ return this.summaryHeroes[0];
+ }
+ const members = this.currentState.getMembers();
+ const anyMember = members.find(m => m.userId !== this.myUserId);
+ if (anyMember) {
+ return anyMember.userId;
+ }
+ // it really seems like I'm the only user in the room
+ // so I probably created a room with just me in it
+ // and marked it as a DM. Ok then
+ return this.myUserId;
+ }
+ getAvatarFallbackMember() {
+ const memberCount = this.getInvitedAndJoinedMemberCount();
+ if (memberCount > 2) {
+ return;
+ }
+ const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length;
+ if (hasHeroes) {
+ const availableMember = this.summaryHeroes.map(userId => {
+ return this.getMember(userId);
+ }).find(member => !!member);
+ if (availableMember) {
+ return availableMember;
+ }
+ }
+ const members = this.currentState.getMembers();
+ // could be different than memberCount
+ // as this includes left members
+ if (members.length <= 2) {
+ const availableMember = members.find(m => {
+ return m.userId !== this.myUserId;
+ });
+ if (availableMember) {
+ return availableMember;
+ }
+ }
+ // if all else fails, try falling back to a user,
+ // and create a one-off member for it
+ if (hasHeroes) {
+ const availableUser = this.summaryHeroes.map(userId => {
+ return this.client.getUser(userId);
+ }).find(user => !!user);
+ if (availableUser) {
+ const member = new _roomMember.RoomMember(this.roomId, availableUser.userId);
+ member.user = availableUser;
+ return member;
+ }
+ }
+ }
+
+ /**
+ * Sets the membership this room was received as during sync
+ * @param membership - join | leave | invite
+ */
+ updateMyMembership(membership) {
+ const prevMembership = this.selfMembership;
+ this.selfMembership = membership;
+ if (prevMembership !== membership) {
+ if (membership === "leave") {
+ this.cleanupAfterLeaving();
+ }
+ this.emit(RoomEvent.MyMembership, this, membership, prevMembership);
+ }
+ }
+ async loadMembersFromServer() {
+ const lastSyncToken = this.client.store.getSyncToken();
+ const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken ?? undefined);
+ return response.chunk;
+ }
+ async loadMembers() {
+ // were the members loaded from the server?
+ let fromServer = false;
+ let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId);
+ // If the room is encrypted, we always fetch members from the server at
+ // least once, in case the latest state wasn't persisted properly. Note
+ // that this function is only called once (unless loading the members
+ // fails), since loadMembersIfNeeded always returns this.membersPromise
+ // if set, which will be the result of the first (successful) call.
+ if (rawMembersEvents === null || this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) {
+ fromServer = true;
+ rawMembersEvents = await this.loadMembersFromServer();
+ _logger.logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
+ }
+ const memberEvents = rawMembersEvents.filter(_utils.noUnsafeEventProps).map(this.client.getEventMapper());
+ return {
+ memberEvents,
+ fromServer
+ };
+ }
+
+ /**
+ * Check if loading of out-of-band-members has completed
+ *
+ * @returns true if the full membership list of this room has been loaded (including if lazy-loading is disabled).
+ * False if the load is not started or is in progress.
+ */
+ membersLoaded() {
+ if (!this.opts.lazyLoadMembers) {
+ return true;
+ }
+ return this.currentState.outOfBandMembersReady();
+ }
+
+ /**
+ * Preloads the member list in case lazy loading
+ * of memberships is in use. Can be called multiple times,
+ * it will only preload once.
+ * @returns when preloading is done and
+ * accessing the members on the room will take
+ * all members in the room into account
+ */
+ loadMembersIfNeeded() {
+ if (this.membersPromise) {
+ return this.membersPromise;
+ }
+
+ // mark the state so that incoming messages while
+ // the request is in flight get marked as superseding
+ // the OOB members
+ this.currentState.markOutOfBandMembersStarted();
+ const inMemoryUpdate = this.loadMembers().then(result => {
+ this.currentState.setOutOfBandMembers(result.memberEvents);
+ return result.fromServer;
+ }).catch(err => {
+ // allow retries on fail
+ this.membersPromise = undefined;
+ this.currentState.markOutOfBandMembersFailed();
+ throw err;
+ });
+ // update members in storage, but don't wait for it
+ inMemoryUpdate.then(fromServer => {
+ if (fromServer) {
+ const oobMembers = this.currentState.getMembers().filter(m => m.isOutOfBand()).map(m => m.events.member?.event);
+ _logger.logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`);
+ const store = this.client.store;
+ return store.setOutOfBandMembers(this.roomId, oobMembers)
+ // swallow any IDB error as we don't want to fail
+ // because of this
+ .catch(err => {
+ _logger.logger.log("LL: storing OOB room members failed, oh well", err);
+ });
+ }
+ }).catch(err => {
+ // as this is not awaited anywhere,
+ // at least show the error in the console
+ _logger.logger.error(err);
+ });
+ this.membersPromise = inMemoryUpdate;
+ return this.membersPromise;
+ }
+
+ /**
+ * Removes the lazily loaded members from storage if needed
+ */
+ async clearLoadedMembersIfNeeded() {
+ if (this.opts.lazyLoadMembers && this.membersPromise) {
+ await this.loadMembersIfNeeded();
+ await this.client.store.clearOutOfBandMembers(this.roomId);
+ this.currentState.clearOutOfBandMembers();
+ this.membersPromise = undefined;
+ }
+ }
+
+ /**
+ * called when sync receives this room in the leave section
+ * to do cleanup after leaving a room. Possibly called multiple times.
+ */
+ cleanupAfterLeaving() {
+ this.clearLoadedMembersIfNeeded().catch(err => {
+ _logger.logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`);
+ _logger.logger.log(err);
+ });
+ }
+
+ /**
+ * Empty out the current live timeline and re-request it. This is used when
+ * historical messages are imported into the room via MSC2716 `/batch_send`
+ * because the client may already have that section of the timeline loaded.
+ * We need to force the client to throw away their current timeline so that
+ * when they back paginate over the area again with the historical messages
+ * in between, it grabs the newly imported messages. We can listen for
+ * `UNSTABLE_MSC2716_MARKER`, in order to tell when historical messages are ready
+ * to be discovered in the room and the timeline needs a refresh. The SDK
+ * emits a `RoomEvent.HistoryImportedWithinTimeline` event when we detect a
+ * valid marker and can check the needs refresh status via
+ * `room.getTimelineNeedsRefresh()`.
+ */
+ async refreshLiveTimeline() {
+ const liveTimelineBefore = this.getLiveTimeline();
+ const forwardPaginationToken = liveTimelineBefore.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS);
+ const backwardPaginationToken = liveTimelineBefore.getPaginationToken(_eventTimeline.EventTimeline.BACKWARDS);
+ const eventsBefore = liveTimelineBefore.getEvents();
+ const mostRecentEventInTimeline = eventsBefore[eventsBefore.length - 1];
+ _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] at ` + `mostRecentEventInTimeline=${mostRecentEventInTimeline && mostRecentEventInTimeline.getId()} ` + `liveTimelineBefore=${liveTimelineBefore.toString()} ` + `forwardPaginationToken=${forwardPaginationToken} ` + `backwardPaginationToken=${backwardPaginationToken}`);
+
+ // Get the main TimelineSet
+ const timelineSet = this.getUnfilteredTimelineSet();
+ let newTimeline;
+ // If there isn't any event in the timeline, let's go fetch the latest
+ // event and construct a timeline from it.
+ //
+ // This should only really happen if the user ran into an error
+ // with refreshing the timeline before which left them in a blank
+ // timeline from `resetLiveTimeline`.
+ if (!mostRecentEventInTimeline) {
+ newTimeline = await this.client.getLatestTimeline(timelineSet);
+ } else {
+ // Empty out all of `this.timelineSets`. But we also need to keep the
+ // same `timelineSet` references around so the React code updates
+ // properly and doesn't ignore the room events we emit because it checks
+ // that the `timelineSet` references are the same. We need the
+ // `timelineSet` empty so that the `client.getEventTimeline(...)` call
+ // later, will call `/context` and create a new timeline instead of
+ // returning the same one.
+ this.resetLiveTimeline(null, null);
+
+ // Make the UI timeline show the new blank live timeline we just
+ // reset so that if the network fails below it's showing the
+ // accurate state of what we're working with instead of the
+ // disconnected one in the TimelineWindow which is just hanging
+ // around by reference.
+ this.emit(RoomEvent.TimelineRefresh, this, timelineSet);
+
+ // Use `client.getEventTimeline(...)` to construct a new timeline from a
+ // `/context` response state and events for the most recent event before
+ // we reset everything. The `timelineSet` we pass in needs to be empty
+ // in order for this function to call `/context` and generate a new
+ // timeline.
+ newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId());
+ }
+
+ // If a racing `/sync` beat us to creating a new timeline, use that
+ // instead because it's the latest in the room and any new messages in
+ // the scrollback will include the history.
+ const liveTimeline = timelineSet.getLiveTimeline();
+ if (!liveTimeline || liveTimeline.getPaginationToken(_eventTimeline.Direction.Forward) === null && liveTimeline.getPaginationToken(_eventTimeline.Direction.Backward) === null && liveTimeline.getEvents().length === 0) {
+ _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] using our new live timeline`);
+ // Set the pagination token back to the live sync token (`null`) instead
+ // of using the `/context` historical token (ex. `t12-13_0_0_0_0_0_0_0_0`)
+ // so that it matches the next response from `/sync` and we can properly
+ // continue the timeline.
+ newTimeline.setPaginationToken(forwardPaginationToken, _eventTimeline.EventTimeline.FORWARDS);
+
+ // Set our new fresh timeline as the live timeline to continue syncing
+ // forwards and back paginating from.
+ timelineSet.setLiveTimeline(newTimeline);
+ // Fixup `this.oldstate` so that `scrollback` has the pagination tokens
+ // available
+ this.fixUpLegacyTimelineFields();
+ } else {
+ _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] \`/sync\` or some other request beat us to creating a new ` + `live timeline after we reset it. We'll use that instead since any events in the scrollback from ` + `this timeline will include the history.`);
+ }
+
+ // The timeline has now been refreshed ✅
+ this.setTimelineNeedsRefresh(false);
+
+ // Emit an event which clients can react to and re-load the timeline
+ // from the SDK
+ this.emit(RoomEvent.TimelineRefresh, this, timelineSet);
+ }
+
+ /**
+ * Reset the live timeline of all timelineSets, and start new ones.
+ *
+ * <p>This is used when /sync returns a 'limited' timeline.
+ *
+ * @param backPaginationToken - token for back-paginating the new timeline
+ * @param forwardPaginationToken - token for forward-paginating the old live timeline,
+ * if absent or null, all timelines are reset, removing old ones (including the previous live
+ * timeline which would otherwise be unable to paginate forwards without this token).
+ * Removing just the old live timeline whilst preserving previous ones is not supported.
+ */
+ resetLiveTimeline(backPaginationToken, forwardPaginationToken) {
+ for (const timelineSet of this.timelineSets) {
+ timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined);
+ }
+ for (const thread of this.threads.values()) {
+ thread.resetLiveTimeline(backPaginationToken, forwardPaginationToken);
+ }
+ this.fixUpLegacyTimelineFields();
+ }
+
+ /**
+ * Fix up this.timeline, this.oldState and this.currentState
+ *
+ * @internal
+ */
+ fixUpLegacyTimelineFields() {
+ const previousOldState = this.oldState;
+ const previousCurrentState = this.currentState;
+
+ // maintain this.timeline as a reference to the live timeline,
+ // and this.oldState and this.currentState as references to the
+ // state at the start and end of that timeline. These are more
+ // for backwards-compatibility than anything else.
+ this.timeline = this.getLiveTimeline().getEvents();
+ this.oldState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.BACKWARDS);
+ this.currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
+
+ // Let people know to register new listeners for the new state
+ // references. The reference won't necessarily change every time so only
+ // emit when we see a change.
+ if (previousOldState !== this.oldState) {
+ this.emit(RoomEvent.OldStateUpdated, this, previousOldState, this.oldState);
+ }
+ if (previousCurrentState !== this.currentState) {
+ this.emit(RoomEvent.CurrentStateUpdated, this, previousCurrentState, this.currentState);
+
+ // Re-emit various events on the current room state
+ // TODO: If currentState really only exists for backwards
+ // compatibility, shouldn't we be doing this some other way?
+ this.reEmitter.stopReEmitting(previousCurrentState, [_roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update, _roomState.RoomStateEvent.Marker, _beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]);
+ this.reEmitter.reEmit(this.currentState, [_roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update, _roomState.RoomStateEvent.Marker, _beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]);
+ }
+ }
+
+ /**
+ * Returns whether there are any devices in the room that are unverified
+ *
+ * Note: Callers should first check if crypto is enabled on this device. If it is
+ * disabled, then we aren't tracking room devices at all, so we can't answer this, and an
+ * error will be thrown.
+ *
+ * @returns the result
+ */
+ async hasUnverifiedDevices() {
+ if (!this.client.isRoomEncrypted(this.roomId)) {
+ return false;
+ }
+ const e2eMembers = await this.getEncryptionTargetMembers();
+ for (const member of e2eMembers) {
+ const devices = this.client.getStoredDevicesForUser(member.userId);
+ if (devices.some(device => device.isUnverified())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Return the timeline sets for this room.
+ * @returns array of timeline sets for this room
+ */
+ getTimelineSets() {
+ return this.timelineSets;
+ }
+
+ /**
+ * Helper to return the main unfiltered timeline set for this room
+ * @returns room's unfiltered timeline set
+ */
+ getUnfilteredTimelineSet() {
+ return this.timelineSets[0];
+ }
+
+ /**
+ * Get the timeline which contains the given event from the unfiltered set, if any
+ *
+ * @param eventId - event ID to look for
+ * @returns timeline containing
+ * the given event, or null if unknown
+ */
+ getTimelineForEvent(eventId) {
+ const event = this.findEventById(eventId);
+ const thread = this.findThreadForEvent(event);
+ if (thread) {
+ return thread.timelineSet.getTimelineForEvent(eventId);
+ } else {
+ return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId);
+ }
+ }
+
+ /**
+ * Add a new timeline to this room's unfiltered timeline set
+ *
+ * @returns newly-created timeline
+ */
+ addTimeline() {
+ return this.getUnfilteredTimelineSet().addTimeline();
+ }
+
+ /**
+ * Whether the timeline needs to be refreshed in order to pull in new
+ * historical messages that were imported.
+ * @param value - The value to set
+ */
+ setTimelineNeedsRefresh(value) {
+ this.timelineNeedsRefresh = value;
+ }
+
+ /**
+ * Whether the timeline needs to be refreshed in order to pull in new
+ * historical messages that were imported.
+ * @returns .
+ */
+ getTimelineNeedsRefresh() {
+ return this.timelineNeedsRefresh;
+ }
+
+ /**
+ * Get an event which is stored in our unfiltered timeline set, or in a thread
+ *
+ * @param eventId - event ID to look for
+ * @returns the given event, or undefined if unknown
+ */
+ findEventById(eventId) {
+ let event = this.getUnfilteredTimelineSet().findEventById(eventId);
+ if (!event) {
+ const threads = this.getThreads();
+ for (let i = 0; i < threads.length; i++) {
+ const thread = threads[i];
+ event = thread.findEventById(eventId);
+ if (event) {
+ return event;
+ }
+ }
+ }
+ return event;
+ }
+
+ /**
+ * Get one of the notification counts for this room
+ * @param type - The type of notification count to get. default: 'total'
+ * @returns The notification count, or undefined if there is no count
+ * for this type.
+ */
+ getUnreadNotificationCount(type = NotificationCountType.Total) {
+ let count = this.getRoomUnreadNotificationCount(type);
+ for (const threadNotification of this.threadNotifications.values()) {
+ count += threadNotification[type] ?? 0;
+ }
+ return count;
+ }
+
+ /**
+ * Get the notification for the event context (room or thread timeline)
+ */
+ getUnreadCountForEventContext(type = NotificationCountType.Total, event) {
+ const isThreadEvent = !!event.threadRootId && !event.isThreadRoot;
+ return (isThreadEvent ? this.getThreadUnreadNotificationCount(event.threadRootId, type) : this.getRoomUnreadNotificationCount(type)) ?? 0;
+ }
+
+ /**
+ * Get one of the notification counts for this room
+ * @param type - The type of notification count to get. default: 'total'
+ * @returns The notification count, or undefined if there is no count
+ * for this type.
+ */
+ getRoomUnreadNotificationCount(type = NotificationCountType.Total) {
+ return this.notificationCounts[type] ?? 0;
+ }
+
+ /**
+ * Get one of the notification counts for a thread
+ * @param threadId - the root event ID
+ * @param type - The type of notification count to get. default: 'total'
+ * @returns The notification count, or undefined if there is no count
+ * for this type.
+ */
+ getThreadUnreadNotificationCount(threadId, type = NotificationCountType.Total) {
+ return this.threadNotifications.get(threadId)?.[type] ?? 0;
+ }
+
+ /**
+ * Checks if the current room has unread thread notifications
+ * @returns
+ */
+ hasThreadUnreadNotification() {
+ for (const notification of this.threadNotifications.values()) {
+ if ((notification.highlight ?? 0) > 0 || (notification.total ?? 0) > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Swet one of the notification count for a thread
+ * @param threadId - the root event ID
+ * @param type - The type of notification count to get. default: 'total'
+ * @returns
+ */
+ setThreadUnreadNotificationCount(threadId, type, count) {
+ const notification = _objectSpread({
+ highlight: this.threadNotifications.get(threadId)?.highlight,
+ total: this.threadNotifications.get(threadId)?.total
+ }, {
+ [type]: count
+ });
+ this.threadNotifications.set(threadId, notification);
+ this.emit(RoomEvent.UnreadNotifications, notification, threadId);
+ }
+
+ /**
+ * @returns the notification count type for all the threads in the room
+ */
+ get threadsAggregateNotificationType() {
+ let type = null;
+ for (const threadNotification of this.threadNotifications.values()) {
+ if ((threadNotification.highlight ?? 0) > 0) {
+ return NotificationCountType.Highlight;
+ } else if ((threadNotification.total ?? 0) > 0 && !type) {
+ type = NotificationCountType.Total;
+ }
+ }
+ return type;
+ }
+
+ /**
+ * Resets the thread notifications for this room
+ */
+ resetThreadUnreadNotificationCount(notificationsToKeep) {
+ if (notificationsToKeep) {
+ for (const [threadId] of this.threadNotifications) {
+ if (!notificationsToKeep.includes(threadId)) {
+ this.threadNotifications.delete(threadId);
+ }
+ }
+ } else {
+ this.threadNotifications.clear();
+ }
+ this.emit(RoomEvent.UnreadNotifications);
+ }
+
+ /**
+ * Set one of the notification counts for this room
+ * @param type - The type of notification count to set.
+ * @param count - The new count
+ */
+ setUnreadNotificationCount(type, count) {
+ this.notificationCounts[type] = count;
+ this.emit(RoomEvent.UnreadNotifications, this.notificationCounts);
+ }
+ setUnread(type, count) {
+ return this.setUnreadNotificationCount(type, count);
+ }
+ setSummary(summary) {
+ const heroes = summary["m.heroes"];
+ const joinedCount = summary["m.joined_member_count"];
+ const invitedCount = summary["m.invited_member_count"];
+ if (Number.isInteger(joinedCount)) {
+ this.currentState.setJoinedMemberCount(joinedCount);
+ }
+ if (Number.isInteger(invitedCount)) {
+ this.currentState.setInvitedMemberCount(invitedCount);
+ }
+ if (Array.isArray(heroes)) {
+ // be cautious about trusting server values,
+ // and make sure heroes doesn't contain our own id
+ // just to be sure
+ this.summaryHeroes = heroes.filter(userId => {
+ return userId !== this.myUserId;
+ });
+ }
+ }
+
+ /**
+ * Whether to send encrypted messages to devices within this room.
+ * @param value - true to blacklist unverified devices, null
+ * to use the global value for this room.
+ */
+ setBlacklistUnverifiedDevices(value) {
+ this.blacklistUnverifiedDevices = value;
+ }
+
+ /**
+ * Whether to send encrypted messages to devices within this room.
+ * @returns true if blacklisting unverified devices, null
+ * if the global value should be used for this room.
+ */
+ getBlacklistUnverifiedDevices() {
+ if (this.blacklistUnverifiedDevices === undefined) return null;
+ return this.blacklistUnverifiedDevices;
+ }
+
+ /**
+ * Get the avatar URL for a room if one was set.
+ * @param baseUrl - The homeserver base URL. See
+ * {@link MatrixClient#getHomeserverUrl}.
+ * @param width - The desired width of the thumbnail.
+ * @param height - The desired height of the thumbnail.
+ * @param resizeMethod - The thumbnail resize method to use, either
+ * "crop" or "scale".
+ * @param allowDefault - True to allow an identicon for this room if an
+ * avatar URL wasn't explicitly set. Default: true. (Deprecated)
+ * @returns the avatar URL or null.
+ */
+ getAvatarUrl(baseUrl, width, height, resizeMethod, allowDefault = true) {
+ const roomAvatarEvent = this.currentState.getStateEvents(_event2.EventType.RoomAvatar, "");
+ if (!roomAvatarEvent && !allowDefault) {
+ return null;
+ }
+ const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
+ if (mainUrl) {
+ return (0, _contentRepo.getHttpUriForMxc)(baseUrl, mainUrl, width, height, resizeMethod);
+ }
+ return null;
+ }
+
+ /**
+ * Get the mxc avatar url for the room, if one was set.
+ * @returns the mxc avatar url or falsy
+ */
+ getMxcAvatarUrl() {
+ return this.currentState.getStateEvents(_event2.EventType.RoomAvatar, "")?.getContent()?.url || null;
+ }
+
+ /**
+ * Get this room's canonical alias
+ * The alias returned by this function may not necessarily
+ * still point to this room.
+ * @returns The room's canonical alias, or null if there is none
+ */
+ getCanonicalAlias() {
+ const canonicalAlias = this.currentState.getStateEvents(_event2.EventType.RoomCanonicalAlias, "");
+ if (canonicalAlias) {
+ return canonicalAlias.getContent().alias || null;
+ }
+ return null;
+ }
+
+ /**
+ * Get this room's alternative aliases
+ * @returns The room's alternative aliases, or an empty array
+ */
+ getAltAliases() {
+ const canonicalAlias = this.currentState.getStateEvents(_event2.EventType.RoomCanonicalAlias, "");
+ if (canonicalAlias) {
+ return canonicalAlias.getContent().alt_aliases || [];
+ }
+ return [];
+ }
+
+ /**
+ * Add events to a timeline
+ *
+ * <p>Will fire "Room.timeline" for each event added.
+ *
+ * @param events - A list of events to add.
+ *
+ * @param toStartOfTimeline - True to add these events to the start
+ * (oldest) instead of the end (newest) of the timeline. If true, the oldest
+ * event will be the <b>last</b> element of 'events'.
+ *
+ * @param timeline - timeline to
+ * add events to.
+ *
+ * @param paginationToken - token for the next batch of events
+ *
+ * @remarks
+ * Fires {@link RoomEvent.Timeline}
+ */
+ addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken) {
+ timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken);
+ }
+
+ /**
+ * Get the instance of the thread associated with the current event
+ * @param eventId - the ID of the current event
+ * @returns a thread instance if known
+ */
+ getThread(eventId) {
+ return this.threads.get(eventId) ?? null;
+ }
+
+ /**
+ * Get all the known threads in the room
+ */
+ getThreads() {
+ return Array.from(this.threads.values());
+ }
+
+ /**
+ * Get a member from the current room state.
+ * @param userId - The user ID of the member.
+ * @returns The member or `null`.
+ */
+ getMember(userId) {
+ return this.currentState.getMember(userId);
+ }
+
+ /**
+ * Get all currently loaded members from the current
+ * room state.
+ * @returns Room members
+ */
+ getMembers() {
+ return this.currentState.getMembers();
+ }
+
+ /**
+ * Get a list of members whose membership state is "join".
+ * @returns A list of currently joined members.
+ */
+ getJoinedMembers() {
+ return this.getMembersWithMembership("join");
+ }
+
+ /**
+ * Returns the number of joined members in this room
+ * This method caches the result.
+ * This is a wrapper around the method of the same name in roomState, returning
+ * its result for the room's current state.
+ * @returns The number of members in this room whose membership is 'join'
+ */
+ getJoinedMemberCount() {
+ return this.currentState.getJoinedMemberCount();
+ }
+
+ /**
+ * Returns the number of invited members in this room
+ * @returns The number of members in this room whose membership is 'invite'
+ */
+ getInvitedMemberCount() {
+ return this.currentState.getInvitedMemberCount();
+ }
+
+ /**
+ * Returns the number of invited + joined members in this room
+ * @returns The number of members in this room whose membership is 'invite' or 'join'
+ */
+ getInvitedAndJoinedMemberCount() {
+ return this.getInvitedMemberCount() + this.getJoinedMemberCount();
+ }
+
+ /**
+ * Get a list of members with given membership state.
+ * @param membership - The membership state.
+ * @returns A list of members with the given membership state.
+ */
+ getMembersWithMembership(membership) {
+ return this.currentState.getMembers().filter(function (m) {
+ return m.membership === membership;
+ });
+ }
+
+ /**
+ * Get a list of members we should be encrypting for in this room
+ * @returns A list of members who
+ * we should encrypt messages for in this room.
+ */
+ async getEncryptionTargetMembers() {
+ await this.loadMembersIfNeeded();
+ let members = this.getMembersWithMembership("join");
+ if (this.shouldEncryptForInvitedMembers()) {
+ members = members.concat(this.getMembersWithMembership("invite"));
+ }
+ return members;
+ }
+
+ /**
+ * Determine whether we should encrypt messages for invited users in this room
+ * @returns if we should encrypt messages for invited users
+ */
+ shouldEncryptForInvitedMembers() {
+ const ev = this.currentState.getStateEvents(_event2.EventType.RoomHistoryVisibility, "");
+ return ev?.getContent()?.history_visibility !== "joined";
+ }
+
+ /**
+ * Get the default room name (i.e. what a given user would see if the
+ * room had no m.room.name)
+ * @param userId - The userId from whose perspective we want
+ * to calculate the default name
+ * @returns The default room name
+ */
+ getDefaultRoomName(userId) {
+ return this.calculateRoomName(userId, true);
+ }
+
+ /**
+ * Check if the given user_id has the given membership state.
+ * @param userId - The user ID to check.
+ * @param membership - The membership e.g. `'join'`
+ * @returns True if this user_id has the given membership state.
+ */
+ hasMembershipState(userId, membership) {
+ const member = this.getMember(userId);
+ if (!member) {
+ return false;
+ }
+ return member.membership === membership;
+ }
+
+ /**
+ * Add a timelineSet for this room with the given filter
+ * @param filter - The filter to be applied to this timelineSet
+ * @param opts - Configuration options
+ * @returns The timelineSet
+ */
+ getOrCreateFilteredTimelineSet(filter, {
+ prepopulateTimeline = true,
+ useSyncEvents = true,
+ pendingEvents = true
+ } = {}) {
+ if (this.filteredTimelineSets[filter.filterId]) {
+ return this.filteredTimelineSets[filter.filterId];
+ }
+ const opts = Object.assign({
+ filter,
+ pendingEvents
+ }, this.opts);
+ const timelineSet = new _eventTimelineSet.EventTimelineSet(this, opts);
+ this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
+ if (useSyncEvents) {
+ this.filteredTimelineSets[filter.filterId] = timelineSet;
+ this.timelineSets.push(timelineSet);
+ }
+ const unfilteredLiveTimeline = this.getLiveTimeline();
+ // Not all filter are possible to replicate client-side only
+ // When that's the case we do not want to prepopulate from the live timeline
+ // as we would get incorrect results compared to what the server would send back
+ if (prepopulateTimeline) {
+ // populate up the new timelineSet with filtered events from our live
+ // unfiltered timeline.
+ //
+ // XXX: This is risky as our timeline
+ // may have grown huge and so take a long time to filter.
+ // see https://github.com/vector-im/vector-web/issues/2109
+
+ unfilteredLiveTimeline.getEvents().forEach(function (event) {
+ timelineSet.addLiveEvent(event);
+ });
+
+ // find the earliest unfiltered timeline
+ let timeline = unfilteredLiveTimeline;
+ while (timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS)) {
+ timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS);
+ }
+ timelineSet.getLiveTimeline().setPaginationToken(timeline.getPaginationToken(_eventTimeline.EventTimeline.BACKWARDS), _eventTimeline.EventTimeline.BACKWARDS);
+ } else if (useSyncEvents) {
+ const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(_eventTimeline.Direction.Forward);
+ timelineSet.getLiveTimeline().setPaginationToken(livePaginationToken, _eventTimeline.Direction.Backward);
+ }
+
+ // alternatively, we could try to do something like this to try and re-paginate
+ // in the filtered events from nothing, but Mark says it's an abuse of the API
+ // to do so:
+ //
+ // timelineSet.resetLiveTimeline(
+ // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS)
+ // );
+
+ return timelineSet;
+ }
+ async getThreadListFilter(filterType = _thread.ThreadFilterType.All) {
+ const myUserId = this.client.getUserId();
+ const filter = new _filter.Filter(myUserId);
+ const definition = {
+ room: {
+ timeline: {
+ [_thread.FILTER_RELATED_BY_REL_TYPES.name]: [_thread.THREAD_RELATION_TYPE.name]
+ }
+ }
+ };
+ if (filterType === _thread.ThreadFilterType.My) {
+ definition.room.timeline[_thread.FILTER_RELATED_BY_SENDERS.name] = [myUserId];
+ }
+ filter.setDefinition(definition);
+ const filterId = await this.client.getOrCreateFilter(`THREAD_PANEL_${this.roomId}_${filterType}`, filter);
+ filter.filterId = filterId;
+ return filter;
+ }
+ async createThreadTimelineSet(filterType) {
+ let timelineSet;
+ if (_thread.Thread.hasServerSideListSupport) {
+ timelineSet = new _eventTimelineSet.EventTimelineSet(this, _objectSpread(_objectSpread({}, this.opts), {}, {
+ pendingEvents: false
+ }), undefined, undefined, filterType ?? _thread.ThreadFilterType.All);
+ this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
+ } else if (_thread.Thread.hasServerSideSupport) {
+ const filter = await this.getThreadListFilter(filterType);
+ timelineSet = this.getOrCreateFilteredTimelineSet(filter, {
+ prepopulateTimeline: false,
+ useSyncEvents: false,
+ pendingEvents: false
+ });
+ } else {
+ timelineSet = new _eventTimelineSet.EventTimelineSet(this, {
+ pendingEvents: false
+ });
+ Array.from(this.threads).forEach(([, thread]) => {
+ if (thread.length === 0) return;
+ const currentUserParticipated = thread.timeline.some(event => {
+ return event.getSender() === this.client.getUserId();
+ });
+ if (filterType !== _thread.ThreadFilterType.My || currentUserParticipated) {
+ timelineSet.getLiveTimeline().addEvent(thread.rootEvent, {
+ toStartOfTimeline: false
+ });
+ }
+ });
+ }
+ return timelineSet;
+ }
+ /**
+ * Takes the given thread root events and creates threads for them.
+ */
+ processThreadRoots(events, toStartOfTimeline) {
+ for (const rootEvent of events) {
+ _eventTimeline.EventTimeline.setEventMetadata(rootEvent, this.currentState, toStartOfTimeline);
+ if (!this.getThread(rootEvent.getId())) {
+ this.createThread(rootEvent.getId(), rootEvent, [], toStartOfTimeline);
+ }
+ }
+ }
+
+ /**
+ * Fetch the bare minimum of room threads required for the thread list to work reliably.
+ * With server support that means fetching one page.
+ * Without server support that means fetching as much at once as the server allows us to.
+ */
+ async fetchRoomThreads() {
+ if (this.threadsReady || !this.client.supportsThreads()) {
+ return;
+ }
+ if (_thread.Thread.hasServerSideListSupport) {
+ await Promise.all([this.fetchRoomThreadList(_thread.ThreadFilterType.All), this.fetchRoomThreadList(_thread.ThreadFilterType.My)]);
+ } else {
+ const allThreadsFilter = await this.getThreadListFilter();
+ const {
+ chunk: events
+ } = await this.client.createMessagesRequest(this.roomId, "", Number.MAX_SAFE_INTEGER, _eventTimeline.Direction.Backward, allThreadsFilter);
+ if (!events.length) return;
+
+ // Sorted by last_reply origin_server_ts
+ const threadRoots = events.map(this.client.getEventMapper()).sort((eventA, eventB) => {
+ /**
+ * `origin_server_ts` in a decentralised world is far from ideal
+ * but for lack of any better, we will have to use this
+ * Long term the sorting should be handled by homeservers and this
+ * is only meant as a short term patch
+ */
+ const threadAMetadata = eventA.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name);
+ const threadBMetadata = eventB.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name);
+ return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts;
+ });
+ let latestMyThreadsRootEvent;
+ const roomState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
+ for (const rootEvent of threadRoots) {
+ const opts = {
+ duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Ignore,
+ fromCache: false,
+ roomState
+ };
+ this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts);
+ const threadRelationship = rootEvent.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name);
+ if (threadRelationship?.current_user_participated) {
+ this.threadsTimelineSets[1]?.addLiveEvent(rootEvent, opts);
+ latestMyThreadsRootEvent = rootEvent;
+ }
+ }
+ this.processThreadRoots(threadRoots, true);
+ this.client.decryptEventIfNeeded(threadRoots[threadRoots.length - 1]);
+ if (latestMyThreadsRootEvent) {
+ this.client.decryptEventIfNeeded(latestMyThreadsRootEvent);
+ }
+ }
+ this.on(_thread.ThreadEvent.NewReply, this.onThreadNewReply);
+ this.on(_thread.ThreadEvent.Delete, this.onThreadDelete);
+ this.threadsReady = true;
+ }
+
+ /**
+ * Calls {@link processPollEvent} for a list of events.
+ *
+ * @param events - List of events
+ */
+ async processPollEvents(events) {
+ for (const event of events) {
+ try {
+ // Continue if the event is a clear text, non-poll event.
+ if (!event.isEncrypted() && !(0, _poll.isPollEvent)(event)) continue;
+
+ /**
+ * Try to decrypt the event. Promise resolution does not guarantee a successful decryption.
+ * Retry is handled in {@link processPollEvent}.
+ */
+ await this.client.decryptEventIfNeeded(event);
+ this.processPollEvent(event);
+ } catch (err) {
+ _logger.logger.warn("Error processing poll event", event.getId(), err);
+ }
+ }
+ }
+
+ /**
+ * Processes poll events:
+ * If the event has a decryption failure, it will listen for decryption and tries again.
+ * If it is a poll start event (`m.poll.start`),
+ * it creates and stores a Poll model and emits a PollEvent.New event.
+ * If the event is related to a poll, it will add it to the poll.
+ * Noop for other cases.
+ *
+ * @param event - Event that could be a poll event
+ */
+ async processPollEvent(event) {
+ if (event.isDecryptionFailure()) {
+ event.once(_event.MatrixEventEvent.Decrypted, maybeDecryptedEvent => {
+ this.processPollEvent(maybeDecryptedEvent);
+ });
+ return;
+ }
+ if (_matrixEventsSdk.M_POLL_START.matches(event.getType())) {
+ try {
+ const poll = new _poll.Poll(event, this.client, this);
+ this.polls.set(event.getId(), poll);
+ this.emit(_poll.PollEvent.New, poll);
+ } catch {}
+ // poll creation can fail for malformed poll start events
+ return;
+ }
+ const relationEventId = event.relationEventId;
+ if (relationEventId && this.polls.has(relationEventId)) {
+ const poll = this.polls.get(relationEventId);
+ poll?.onNewRelation(event);
+ }
+ }
+
+ /**
+ * Fetch a single page of threadlist messages for the specific thread filter
+ * @internal
+ */
+ async fetchRoomThreadList(filter) {
+ if (this.threadsTimelineSets.length === 0) return;
+ const timelineSet = filter === _thread.ThreadFilterType.My ? this.threadsTimelineSets[1] : this.threadsTimelineSets[0];
+ const {
+ chunk: events,
+ end
+ } = await this.client.createThreadListMessagesRequest(this.roomId, null, undefined, _eventTimeline.Direction.Backward, timelineSet.threadListType, timelineSet.getFilter());
+ timelineSet.getLiveTimeline().setPaginationToken(end ?? null, _eventTimeline.Direction.Backward);
+ if (!events.length) return;
+ const matrixEvents = events.map(this.client.getEventMapper());
+ this.processThreadRoots(matrixEvents, true);
+ const roomState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
+ for (const rootEvent of matrixEvents) {
+ timelineSet.addLiveEvent(rootEvent, {
+ duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Replace,
+ fromCache: false,
+ roomState
+ });
+ }
+ }
+ onThreadNewReply(thread) {
+ this.updateThreadRootEvents(thread, false, true);
+ }
+ onThreadDelete(thread) {
+ this.threads.delete(thread.id);
+ const timeline = this.getTimelineForEvent(thread.id);
+ const roomEvent = timeline?.getEvents()?.find(it => it.getId() === thread.id);
+ if (roomEvent) {
+ thread.clearEventMetadata(roomEvent);
+ } else {
+ _logger.logger.debug("onThreadDelete: Could not find root event in room timeline");
+ }
+ for (const timelineSet of this.threadsTimelineSets) {
+ timelineSet.removeEvent(thread.id);
+ }
+ }
+
+ /**
+ * Forget the timelineSet for this room with the given filter
+ *
+ * @param filter - the filter whose timelineSet is to be forgotten
+ */
+ removeFilteredTimelineSet(filter) {
+ const timelineSet = this.filteredTimelineSets[filter.filterId];
+ delete this.filteredTimelineSets[filter.filterId];
+ const i = this.timelineSets.indexOf(timelineSet);
+ if (i > -1) {
+ this.timelineSets.splice(i, 1);
+ }
+ }
+ eventShouldLiveIn(event, events, roots) {
+ if (!this.client?.supportsThreads()) {
+ return {
+ shouldLiveInRoom: true,
+ shouldLiveInThread: false
+ };
+ }
+
+ // A thread root is always shown in both timelines
+ if (event.isThreadRoot || roots?.has(event.getId())) {
+ return {
+ shouldLiveInRoom: true,
+ shouldLiveInThread: true,
+ threadId: event.getId()
+ };
+ }
+
+ // A thread relation is always only shown in a thread
+ if (event.isRelation(_thread.THREAD_RELATION_TYPE.name)) {
+ return {
+ shouldLiveInRoom: false,
+ shouldLiveInThread: true,
+ threadId: event.threadRootId
+ };
+ }
+ const parentEventId = event.getAssociatedId();
+ let parentEvent;
+ if (parentEventId) {
+ parentEvent = this.findEventById(parentEventId) ?? events?.find(e => e.getId() === parentEventId);
+ }
+
+ // Treat relations and redactions as extensions of their parents so evaluate parentEvent instead
+ if (parentEvent && (event.isRelation() || event.isRedaction())) {
+ return this.eventShouldLiveIn(parentEvent, events, roots);
+ }
+ if (!event.isRelation()) {
+ return {
+ shouldLiveInRoom: true,
+ shouldLiveInThread: false
+ };
+ }
+
+ // Edge case where we know the event is a relation but don't have the parentEvent
+ if (roots?.has(event.relationEventId)) {
+ return {
+ shouldLiveInRoom: true,
+ shouldLiveInThread: true,
+ threadId: event.relationEventId
+ };
+ }
+ const unsigned = event.getUnsigned();
+ if (typeof unsigned[_event2.UNSIGNED_THREAD_ID_FIELD.name] === "string") {
+ return {
+ shouldLiveInRoom: false,
+ shouldLiveInThread: true,
+ threadId: unsigned[_event2.UNSIGNED_THREAD_ID_FIELD.name]
+ };
+ }
+
+ // We've exhausted all scenarios,
+ // we cannot assume that it lives in the main timeline as this may be a relation for an unknown thread
+ // adding the event in the wrong timeline causes stuck notifications and can break ability to send read receipts
+ return {
+ shouldLiveInRoom: false,
+ shouldLiveInThread: false
+ };
+ }
+ findThreadForEvent(event) {
+ if (!event) return null;
+ const {
+ threadId
+ } = this.eventShouldLiveIn(event);
+ return threadId ? this.getThread(threadId) : null;
+ }
+ addThreadedEvents(threadId, events, toStartOfTimeline = false) {
+ const thread = this.getThread(threadId);
+ if (thread) {
+ thread.addEvents(events, toStartOfTimeline);
+ } else {
+ const rootEvent = this.findEventById(threadId) ?? events.find(e => e.getId() === threadId);
+ this.createThread(threadId, rootEvent, events, toStartOfTimeline);
+ }
+ }
+
+ /**
+ * Adds events to a thread's timeline. Will fire "Thread.update"
+ */
+ processThreadedEvents(events, toStartOfTimeline) {
+ events.forEach(this.applyRedaction);
+ const eventsByThread = {};
+ for (const event of events) {
+ const {
+ threadId,
+ shouldLiveInThread
+ } = this.eventShouldLiveIn(event);
+ if (shouldLiveInThread && !eventsByThread[threadId]) {
+ eventsByThread[threadId] = [];
+ }
+ eventsByThread[threadId]?.push(event);
+ }
+ Object.entries(eventsByThread).map(([threadId, threadEvents]) => this.addThreadedEvents(threadId, threadEvents, toStartOfTimeline));
+ }
+ createThread(threadId, rootEvent, events = [], toStartOfTimeline) {
+ if (this.threads.has(threadId)) {
+ return this.threads.get(threadId);
+ }
+ if (rootEvent) {
+ const relatedEvents = this.relations.getAllChildEventsForEvent(rootEvent.getId());
+ if (relatedEvents?.length) {
+ // Include all relations of the root event, given it'll be visible in both timelines,
+ // except `m.replace` as that will already be applied atop the event using `MatrixEvent::makeReplaced`
+ events = events.concat(relatedEvents.filter(e => !e.isRelation(_event2.RelationType.Replace)));
+ }
+ }
+ const thread = new _thread.Thread(threadId, rootEvent, {
+ room: this,
+ client: this.client,
+ pendingEventOrdering: this.opts.pendingEventOrdering,
+ receipts: this.cachedThreadReadReceipts.get(threadId) ?? []
+ });
+
+ // All read receipts should now come down from sync, we do not need to keep
+ // a reference to the cached receipts anymore.
+ this.cachedThreadReadReceipts.delete(threadId);
+
+ // If we managed to create a thread and figure out its `id` then we can use it
+ // This has to happen before thread.addEvents, because that adds events to the eventtimeline, and the
+ // eventtimeline sometimes looks up thread information via the room.
+ this.threads.set(thread.id, thread);
+
+ // This is necessary to be able to jump to events in threads:
+ // If we jump to an event in a thread where neither the event, nor the root,
+ // nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread,
+ // and pass the event through this.
+ thread.addEvents(events, false);
+ this.reEmitter.reEmit(thread, [_thread.ThreadEvent.Delete, _thread.ThreadEvent.Update, _thread.ThreadEvent.NewReply, RoomEvent.Timeline, RoomEvent.TimelineReset]);
+ const isNewer = this.lastThread?.rootEvent && rootEvent?.localTimestamp && this.lastThread.rootEvent?.localTimestamp < rootEvent?.localTimestamp;
+ if (!this.lastThread || isNewer) {
+ this.lastThread = thread;
+ }
+ if (this.threadsReady) {
+ this.updateThreadRootEvents(thread, toStartOfTimeline, false);
+ }
+ this.emit(_thread.ThreadEvent.New, thread, toStartOfTimeline);
+ return thread;
+ }
+ processLiveEvent(event) {
+ this.applyRedaction(event);
+
+ // Implement MSC3531: hiding messages.
+ if (event.isVisibilityEvent()) {
+ // This event changes the visibility of another event, record
+ // the visibility change, inform clients if necessary.
+ this.applyNewVisibilityEvent(event);
+ }
+ // If any pending visibility change is waiting for this (older) event,
+ this.applyPendingVisibilityEvents(event);
+
+ // Sliding Sync modifications:
+ // The proxy cannot guarantee every sent event will have a transaction_id field, so we need
+ // to check the event ID against the list of pending events if there is no transaction ID
+ // field. Only do this for events sent by us though as it's potentially expensive to loop
+ // the pending events map.
+ const txnId = event.getUnsigned().transaction_id;
+ if (!txnId && event.getSender() === this.myUserId) {
+ // check the txn map for a matching event ID
+ for (const [tid, localEvent] of this.txnToEvent) {
+ if (localEvent.getId() === event.getId()) {
+ _logger.logger.debug("processLiveEvent: found sent event without txn ID: ", tid, event.getId());
+ // update the unsigned field so we can re-use the same codepaths
+ const unsigned = event.getUnsigned();
+ unsigned.transaction_id = tid;
+ event.setUnsigned(unsigned);
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Add an event to the end of this room's live timelines. Will fire
+ * "Room.timeline".
+ *
+ * @param event - Event to be added
+ * @param addLiveEventOptions - addLiveEvent options
+ * @internal
+ *
+ * @remarks
+ * Fires {@link RoomEvent.Timeline}
+ */
+ addLiveEvent(event, addLiveEventOptions) {
+ const {
+ duplicateStrategy,
+ timelineWasEmpty,
+ fromCache
+ } = addLiveEventOptions;
+
+ // add to our timeline sets
+ for (const timelineSet of this.timelineSets) {
+ timelineSet.addLiveEvent(event, {
+ duplicateStrategy,
+ fromCache,
+ timelineWasEmpty
+ });
+ }
+
+ // synthesize and inject implicit read receipts
+ // Done after adding the event because otherwise the app would get a read receipt
+ // pointing to an event that wasn't yet in the timeline
+ // Don't synthesize RR for m.room.redaction as this causes the RR to go missing.
+ if (event.sender && event.getType() !== _event2.EventType.RoomRedaction) {
+ this.addReceipt((0, _readReceipt.synthesizeReceipt)(event.sender.userId, event, _read_receipts.ReceiptType.Read), true);
+
+ // Any live events from a user could be taken as implicit
+ // presence information: evidence that they are currently active.
+ // ...except in a world where we use 'user.currentlyActive' to reduce
+ // presence spam, this isn't very useful - we'll get a transition when
+ // they are no longer currently active anyway. So don't bother to
+ // reset the lastActiveAgo and lastPresenceTs from the RoomState's user.
+ }
+ }
+
+ /**
+ * Add a pending outgoing event to this room.
+ *
+ * <p>The event is added to either the pendingEventList, or the live timeline,
+ * depending on the setting of opts.pendingEventOrdering.
+ *
+ * <p>This is an internal method, intended for use by MatrixClient.
+ *
+ * @param event - The event to add.
+ *
+ * @param txnId - Transaction id for this outgoing event
+ *
+ * @throws if the event doesn't have status SENDING, or we aren't given a
+ * unique transaction id.
+ *
+ * @remarks
+ * Fires {@link RoomEvent.LocalEchoUpdated}
+ */
+ addPendingEvent(event, txnId) {
+ if (event.status !== _eventStatus.EventStatus.SENDING && event.status !== _eventStatus.EventStatus.NOT_SENT) {
+ throw new Error("addPendingEvent called on an event with status " + event.status);
+ }
+ if (this.txnToEvent.get(txnId)) {
+ throw new Error("addPendingEvent called on an event with known txnId " + txnId);
+ }
+
+ // call setEventMetadata to set up event.sender etc
+ // as event is shared over all timelineSets, we set up its metadata based
+ // on the unfiltered timelineSet.
+ _eventTimeline.EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS), false);
+ this.txnToEvent.set(txnId, event);
+ if (this.pendingEventList) {
+ if (this.pendingEventList.some(e => e.status === _eventStatus.EventStatus.NOT_SENT)) {
+ _logger.logger.warn("Setting event as NOT_SENT due to messages in the same state");
+ event.setStatus(_eventStatus.EventStatus.NOT_SENT);
+ }
+ this.pendingEventList.push(event);
+ this.savePendingEvents();
+ if (event.isRelation()) {
+ // For pending events, add them to the relations collection immediately.
+ // (The alternate case below already covers this as part of adding to
+ // the timeline set.)
+ this.aggregateNonLiveRelation(event);
+ }
+ if (event.isRedaction()) {
+ const redactId = event.event.redacts;
+ let redactedEvent = this.pendingEventList.find(e => e.getId() === redactId);
+ if (!redactedEvent && redactId) {
+ redactedEvent = this.findEventById(redactId);
+ }
+ if (redactedEvent) {
+ redactedEvent.markLocallyRedacted(event);
+ this.emit(RoomEvent.Redaction, event, this);
+ }
+ }
+ } else {
+ for (const timelineSet of this.timelineSets) {
+ if (timelineSet.getFilter()) {
+ if (timelineSet.getFilter().filterRoomTimeline([event]).length) {
+ timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
+ toStartOfTimeline: false
+ });
+ }
+ } else {
+ timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), {
+ toStartOfTimeline: false
+ });
+ }
+ }
+ }
+ this.emit(RoomEvent.LocalEchoUpdated, event, this);
+ }
+
+ /**
+ * Persists all pending events to local storage
+ *
+ * If the current room is encrypted only encrypted events will be persisted
+ * all messages that are not yet encrypted will be discarded
+ *
+ * This is because the flow of EVENT_STATUS transition is
+ * `queued => sending => encrypting => sending => sent`
+ *
+ * Steps 3 and 4 are skipped for unencrypted room.
+ * It is better to discard an unencrypted message rather than persisting
+ * it locally for everyone to read
+ */
+ savePendingEvents() {
+ if (this.pendingEventList) {
+ const pendingEvents = this.pendingEventList.map(event => {
+ return _objectSpread(_objectSpread({}, event.event), {}, {
+ txn_id: event.getTxnId()
+ });
+ }).filter(event => {
+ // Filter out the unencrypted messages if the room is encrypted
+ const isEventEncrypted = event.type === _event2.EventType.RoomMessageEncrypted;
+ const isRoomEncrypted = this.client.isRoomEncrypted(this.roomId);
+ return isEventEncrypted || !isRoomEncrypted;
+ });
+ this.client.store.setPendingEvents(this.roomId, pendingEvents);
+ }
+ }
+
+ /**
+ * Used to aggregate the local echo for a relation, and also
+ * for re-applying a relation after it's redaction has been cancelled,
+ * as the local echo for the redaction of the relation would have
+ * un-aggregated the relation. Note that this is different from regular messages,
+ * which are just kept detached for their local echo.
+ *
+ * Also note that live events are aggregated in the live EventTimelineSet.
+ * @param event - the relation event that needs to be aggregated.
+ */
+ aggregateNonLiveRelation(event) {
+ this.relations.aggregateChildEvent(event);
+ }
+ getEventForTxnId(txnId) {
+ return this.txnToEvent.get(txnId);
+ }
+
+ /**
+ * Deal with the echo of a message we sent.
+ *
+ * <p>We move the event to the live timeline if it isn't there already, and
+ * update it.
+ *
+ * @param remoteEvent - The event received from
+ * /sync
+ * @param localEvent - The local echo, which
+ * should be either in the pendingEventList or the timeline.
+ *
+ * @internal
+ *
+ * @remarks
+ * Fires {@link RoomEvent.LocalEchoUpdated}
+ */
+ handleRemoteEcho(remoteEvent, localEvent) {
+ const oldEventId = localEvent.getId();
+ const newEventId = remoteEvent.getId();
+ const oldStatus = localEvent.status;
+ _logger.logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`);
+
+ // no longer pending
+ this.txnToEvent.delete(remoteEvent.getUnsigned().transaction_id);
+
+ // if it's in the pending list, remove it
+ if (this.pendingEventList) {
+ this.removePendingEvent(oldEventId);
+ }
+
+ // replace the event source (this will preserve the plaintext payload if
+ // any, which is good, because we don't want to try decoding it again).
+ localEvent.handleRemoteEcho(remoteEvent.event);
+ const {
+ shouldLiveInRoom,
+ threadId
+ } = this.eventShouldLiveIn(remoteEvent);
+ const thread = threadId ? this.getThread(threadId) : null;
+ thread?.setEventMetadata(localEvent);
+ thread?.timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
+ if (shouldLiveInRoom) {
+ for (const timelineSet of this.timelineSets) {
+ // if it's already in the timeline, update the timeline map. If it's not, add it.
+ timelineSet.handleRemoteEcho(localEvent, oldEventId, newEventId);
+ }
+ }
+ this.emit(RoomEvent.LocalEchoUpdated, localEvent, this, oldEventId, oldStatus);
+ }
+
+ /**
+ * Update the status / event id on a pending event, to reflect its transmission
+ * progress.
+ *
+ * <p>This is an internal method.
+ *
+ * @param event - local echo event
+ * @param newStatus - status to assign
+ * @param newEventId - new event id to assign. Ignored unless newStatus == EventStatus.SENT.
+ *
+ * @remarks
+ * Fires {@link RoomEvent.LocalEchoUpdated}
+ */
+ updatePendingEvent(event, newStatus, newEventId) {
+ _logger.logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` + `event ID ${event.getId()} -> ${newEventId}`);
+
+ // if the message was sent, we expect an event id
+ if (newStatus == _eventStatus.EventStatus.SENT && !newEventId) {
+ throw new Error("updatePendingEvent called with status=SENT, but no new event id");
+ }
+
+ // SENT races against /sync, so we have to special-case it.
+ if (newStatus == _eventStatus.EventStatus.SENT) {
+ const timeline = this.getTimelineForEvent(newEventId);
+ if (timeline) {
+ // we've already received the event via the event stream.
+ // nothing more to do here, assuming the transaction ID was correctly matched.
+ // Let's check that.
+ const remoteEvent = this.findEventById(newEventId);
+ const remoteTxnId = remoteEvent?.getUnsigned().transaction_id;
+ if (!remoteTxnId && remoteEvent) {
+ // This code path is mostly relevant for the Sliding Sync proxy.
+ // The remote event did not contain a transaction ID, so we did not handle
+ // the remote echo yet. Handle it now.
+ const unsigned = remoteEvent.getUnsigned();
+ unsigned.transaction_id = event.getTxnId();
+ remoteEvent.setUnsigned(unsigned);
+ // the remote event is _already_ in the timeline, so we need to remove it so
+ // we can convert the local event into the final event.
+ this.removeEvent(remoteEvent.getId());
+ this.handleRemoteEcho(remoteEvent, event);
+ }
+ return;
+ }
+ }
+ const oldStatus = event.status;
+ const oldEventId = event.getId();
+ if (!oldStatus) {
+ throw new Error("updatePendingEventStatus called on an event which is not a local echo.");
+ }
+ const allowed = ALLOWED_TRANSITIONS[oldStatus];
+ if (!allowed?.includes(newStatus)) {
+ throw new Error(`Invalid EventStatus transition ${oldStatus}->${newStatus}`);
+ }
+ event.setStatus(newStatus);
+ if (newStatus == _eventStatus.EventStatus.SENT) {
+ // update the event id
+ event.replaceLocalEventId(newEventId);
+ const {
+ shouldLiveInRoom,
+ threadId
+ } = this.eventShouldLiveIn(event);
+ const thread = threadId ? this.getThread(threadId) : undefined;
+ thread?.setEventMetadata(event);
+ thread?.timelineSet.replaceEventId(oldEventId, newEventId);
+ if (shouldLiveInRoom) {
+ // if the event was already in the timeline (which will be the case if
+ // opts.pendingEventOrdering==chronological), we need to update the
+ // timeline map.
+ for (const timelineSet of this.timelineSets) {
+ timelineSet.replaceEventId(oldEventId, newEventId);
+ }
+ }
+ } else if (newStatus == _eventStatus.EventStatus.CANCELLED) {
+ // remove it from the pending event list, or the timeline.
+ if (this.pendingEventList) {
+ const removedEvent = this.getPendingEvent(oldEventId);
+ this.removePendingEvent(oldEventId);
+ if (removedEvent?.isRedaction()) {
+ this.revertRedactionLocalEcho(removedEvent);
+ }
+ }
+ this.removeEvent(oldEventId);
+ }
+ this.savePendingEvents();
+ this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus);
+ }
+ revertRedactionLocalEcho(redactionEvent) {
+ const redactId = redactionEvent.event.redacts;
+ if (!redactId) {
+ return;
+ }
+ const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId);
+ if (redactedEvent) {
+ redactedEvent.unmarkLocallyRedacted();
+ // re-render after undoing redaction
+ this.emit(RoomEvent.RedactionCancelled, redactionEvent, this);
+ // reapply relation now redaction failed
+ if (redactedEvent.isRelation()) {
+ this.aggregateNonLiveRelation(redactedEvent);
+ }
+ }
+ }
+
+ /**
+ * Add some events to this room. This can include state events, message
+ * events and typing notifications. These events are treated as "live" so
+ * they will go to the end of the timeline.
+ *
+ * @param events - A list of events to add.
+ * @param addLiveEventOptions - addLiveEvent options
+ * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'.
+ */
+
+ /**
+ * @deprecated In favor of the overload with `IAddLiveEventOptions`
+ */
+
+ async addLiveEvents(events, duplicateStrategyOrOpts, fromCache = false) {
+ let duplicateStrategy = duplicateStrategyOrOpts;
+ let timelineWasEmpty = false;
+ if (typeof duplicateStrategyOrOpts === "object") {
+ ({
+ duplicateStrategy,
+ fromCache = false,
+ /* roomState, (not used here) */
+ timelineWasEmpty
+ } = duplicateStrategyOrOpts);
+ } else if (duplicateStrategyOrOpts !== undefined) {
+ // Deprecation warning
+ // FIXME: Remove after 2023-06-01 (technical debt)
+ _logger.logger.warn("Overload deprecated: " + "`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` " + "is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`");
+ }
+ if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) {
+ throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'");
+ }
+
+ // sanity check that the live timeline is still live
+ for (let i = 0; i < this.timelineSets.length; i++) {
+ const liveTimeline = this.timelineSets[i].getLiveTimeline();
+ if (liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS)) {
+ throw new Error("live timeline " + i + " is no longer live - it has a pagination token " + "(" + liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS) + ")");
+ }
+ if (liveTimeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS)) {
+ throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`);
+ }
+ }
+ const threadRoots = this.findThreadRoots(events);
+ const eventsByThread = {};
+ const options = {
+ duplicateStrategy,
+ fromCache,
+ timelineWasEmpty
+ };
+
+ // List of extra events to check for being parents of any relations encountered
+ const neighbouringEvents = [...events];
+ for (const event of events) {
+ // TODO: We should have a filter to say "only add state event types X Y Z to the timeline".
+ this.processLiveEvent(event);
+ if (event.getUnsigned().transaction_id) {
+ const existingEvent = this.txnToEvent.get(event.getUnsigned().transaction_id);
+ if (existingEvent) {
+ // remote echo of an event we sent earlier
+ this.handleRemoteEcho(event, existingEvent);
+ continue; // we can skip adding the event to the timeline sets, it is already there
+ }
+ }
+
+ let {
+ shouldLiveInRoom,
+ shouldLiveInThread,
+ threadId
+ } = this.eventShouldLiveIn(event, neighbouringEvents, threadRoots);
+ if (!shouldLiveInThread && !shouldLiveInRoom && event.isRelation()) {
+ try {
+ const parentEvent = new _event.MatrixEvent(await this.client.fetchRoomEvent(this.roomId, event.relationEventId));
+ neighbouringEvents.push(parentEvent);
+ if (parentEvent.threadRootId) {
+ threadRoots.add(parentEvent.threadRootId);
+ const unsigned = event.getUnsigned();
+ unsigned[_event2.UNSIGNED_THREAD_ID_FIELD.name] = parentEvent.threadRootId;
+ event.setUnsigned(unsigned);
+ }
+ ({
+ shouldLiveInRoom,
+ shouldLiveInThread,
+ threadId
+ } = this.eventShouldLiveIn(event, neighbouringEvents, threadRoots));
+ } catch (e) {
+ _logger.logger.error("Failed to load parent event of unhandled relation", e);
+ }
+ }
+ if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) {
+ eventsByThread[threadId ?? ""] = [];
+ }
+ eventsByThread[threadId ?? ""]?.push(event);
+ if (shouldLiveInRoom) {
+ this.addLiveEvent(event, options);
+ } else if (!shouldLiveInThread && event.isRelation()) {
+ this.relations.aggregateChildEvent(event);
+ }
+ }
+ Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
+ this.addThreadedEvents(threadId, threadEvents, false);
+ });
+ }
+ partitionThreadedEvents(events) {
+ // Indices to the events array, for readability
+ const ROOM = 0;
+ const THREAD = 1;
+ const UNKNOWN_RELATION = 2;
+ if (this.client.supportsThreads()) {
+ const threadRoots = this.findThreadRoots(events);
+ return events.reduce((memo, event) => {
+ const {
+ shouldLiveInRoom,
+ shouldLiveInThread,
+ threadId
+ } = this.eventShouldLiveIn(event, events, threadRoots);
+ if (shouldLiveInRoom) {
+ memo[ROOM].push(event);
+ }
+ if (shouldLiveInThread) {
+ event.setThreadId(threadId ?? "");
+ memo[THREAD].push(event);
+ }
+ if (!shouldLiveInThread && !shouldLiveInRoom) {
+ memo[UNKNOWN_RELATION].push(event);
+ }
+ return memo;
+ }, [[], [], []]);
+ } else {
+ // When `experimentalThreadSupport` is disabled treat all events as timelineEvents
+ return [events, [], []];
+ }
+ }
+
+ /**
+ * Given some events, find the IDs of all the thread roots that are referred to by them.
+ */
+ findThreadRoots(events) {
+ const threadRoots = new Set();
+ for (const event of events) {
+ if (event.isRelation(_thread.THREAD_RELATION_TYPE.name)) {
+ threadRoots.add(event.relationEventId ?? "");
+ }
+ const unsigned = event.getUnsigned();
+ if (typeof unsigned[_event2.UNSIGNED_THREAD_ID_FIELD.name] === "string") {
+ threadRoots.add(unsigned[_event2.UNSIGNED_THREAD_ID_FIELD.name]);
+ }
+ }
+ return threadRoots;
+ }
+
+ /**
+ * Add a receipt event to the room.
+ * @param event - The m.receipt event.
+ * @param synthetic - True if this event is implicit.
+ */
+ addReceipt(event, synthetic = false) {
+ const content = event.getContent();
+ Object.keys(content).forEach(eventId => {
+ Object.keys(content[eventId]).forEach(receiptType => {
+ Object.keys(content[eventId][receiptType]).forEach(userId => {
+ const receipt = content[eventId][receiptType][userId];
+ const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === _read_receipts.MAIN_ROOM_TIMELINE;
+ const receiptDestination = receiptForMainTimeline ? this : this.threads.get(receipt.thread_id ?? "");
+ if (receiptDestination) {
+ receiptDestination.addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic);
+
+ // If the read receipt sent for the logged in user matches
+ // the last event of the live timeline, then we know for a fact
+ // that the user has read that message.
+ // We can mark the room as read and not wait for the local echo
+ // from synapse
+ // This needs to be done after the initial sync as we do not want this
+ // logic to run whilst the room is being initialised
+ if (this.client.isInitialSyncComplete() && userId === this.client.getUserId()) {
+ const lastEvent = receiptDestination.timeline[receiptDestination.timeline.length - 1];
+ if (lastEvent && eventId === lastEvent.getId() && userId === lastEvent.getSender()) {
+ receiptDestination.setUnread(NotificationCountType.Total, 0);
+ receiptDestination.setUnread(NotificationCountType.Highlight, 0);
+ }
+ }
+ } else {
+ // The thread does not exist locally, keep the read receipt
+ // in a cache locally, and re-apply the `addReceipt` logic
+ // when the thread is created
+ this.cachedThreadReadReceipts.set(receipt.thread_id, [...(this.cachedThreadReadReceipts.get(receipt.thread_id) ?? []), {
+ eventId,
+ receiptType,
+ userId,
+ receipt,
+ synthetic
+ }]);
+ }
+ const me = this.client.getUserId();
+ // Track the time of the current user's oldest threaded receipt in the room.
+ if (userId === me && !receiptForMainTimeline && receipt.ts < this.oldestThreadedReceiptTs) {
+ this.oldestThreadedReceiptTs = receipt.ts;
+ }
+
+ // Track each user's unthreaded read receipt.
+ if (!receipt.thread_id && receipt.ts > (this.unthreadedReceipts.get(userId)?.ts ?? 0)) {
+ this.unthreadedReceipts.set(userId, receipt);
+ }
+ });
+ });
+ });
+
+ // send events after we've regenerated the structure & cache, otherwise things that
+ // listened for the event would read stale data.
+ this.emit(RoomEvent.Receipt, event, this);
+ }
+
+ /**
+ * Adds/handles ephemeral events such as typing notifications and read receipts.
+ * @param events - A list of events to process
+ */
+ addEphemeralEvents(events) {
+ for (const event of events) {
+ if (event.getType() === _event2.EventType.Typing) {
+ this.currentState.setTypingEvent(event);
+ } else if (event.getType() === _event2.EventType.Receipt) {
+ this.addReceipt(event);
+ } // else ignore - life is too short for us to care about these events
+ }
+ }
+
+ /**
+ * Removes events from this room.
+ * @param eventIds - A list of eventIds to remove.
+ */
+ removeEvents(eventIds) {
+ for (const eventId of eventIds) {
+ this.removeEvent(eventId);
+ }
+ }
+
+ /**
+ * Removes a single event from this room.
+ *
+ * @param eventId - The id of the event to remove
+ *
+ * @returns true if the event was removed from any of the room's timeline sets
+ */
+ removeEvent(eventId) {
+ let removedAny = false;
+ for (const timelineSet of this.timelineSets) {
+ const removed = timelineSet.removeEvent(eventId);
+ if (removed) {
+ if (removed.isRedaction()) {
+ this.revertRedactionLocalEcho(removed);
+ }
+ removedAny = true;
+ }
+ }
+ return removedAny;
+ }
+
+ /**
+ * Recalculate various aspects of the room, including the room name and
+ * room summary. Call this any time the room's current state is modified.
+ * May fire "Room.name" if the room name is updated.
+ *
+ * @remarks
+ * Fires {@link RoomEvent.Name}
+ */
+ recalculate() {
+ // set fake stripped state events if this is an invite room so logic remains
+ // consistent elsewhere.
+ const membershipEvent = this.currentState.getStateEvents(_event2.EventType.RoomMember, this.myUserId);
+ if (membershipEvent) {
+ const membership = membershipEvent.getContent().membership;
+ this.updateMyMembership(membership);
+ if (membership === "invite") {
+ const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || [];
+ strippedStateEvents.forEach(strippedEvent => {
+ const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key);
+ if (!existingEvent) {
+ // set the fake stripped event instead
+ this.currentState.setStateEvents([new _event.MatrixEvent({
+ type: strippedEvent.type,
+ state_key: strippedEvent.state_key,
+ content: strippedEvent.content,
+ event_id: "$fake" + Date.now(),
+ room_id: this.roomId,
+ user_id: this.myUserId // technically a lie
+ })]);
+ }
+ });
+ }
+ }
+
+ const oldName = this.name;
+ this.name = this.calculateRoomName(this.myUserId);
+ this.normalizedName = (0, _utils.normalize)(this.name);
+ this.summary = new _roomSummary.RoomSummary(this.roomId, {
+ title: this.name
+ });
+ if (oldName !== this.name) {
+ this.emit(RoomEvent.Name, this);
+ }
+ }
+
+ /**
+ * Update the room-tag event for the room. The previous one is overwritten.
+ * @param event - the m.tag event
+ */
+ addTags(event) {
+ // event content looks like:
+ // content: {
+ // tags: {
+ // $tagName: { $metadata: $value },
+ // $tagName: { $metadata: $value },
+ // }
+ // }
+
+ // XXX: do we need to deep copy here?
+ this.tags = event.getContent().tags || {};
+
+ // XXX: we could do a deep-comparison to see if the tags have really
+ // changed - but do we want to bother?
+ this.emit(RoomEvent.Tags, event, this);
+ }
+
+ /**
+ * Update the account_data events for this room, overwriting events of the same type.
+ * @param events - an array of account_data events to add
+ */
+ addAccountData(events) {
+ for (const event of events) {
+ if (event.getType() === "m.tag") {
+ this.addTags(event);
+ }
+ const eventType = event.getType();
+ const lastEvent = this.accountData.get(eventType);
+ this.accountData.set(eventType, event);
+ this.emit(RoomEvent.AccountData, event, this, lastEvent);
+ }
+ }
+
+ /**
+ * Access account_data event of given event type for this room
+ * @param type - the type of account_data event to be accessed
+ * @returns the account_data event in question
+ */
+ getAccountData(type) {
+ return this.accountData.get(type);
+ }
+
+ /**
+ * Returns whether the syncing user has permission to send a message in the room
+ * @returns true if the user should be permitted to send
+ * message events into the room.
+ */
+ maySendMessage() {
+ return this.getMyMembership() === "join" && (this.client.isRoomEncrypted(this.roomId) ? this.currentState.maySendEvent(_event2.EventType.RoomMessageEncrypted, this.myUserId) : this.currentState.maySendEvent(_event2.EventType.RoomMessage, this.myUserId));
+ }
+
+ /**
+ * Returns whether the given user has permissions to issue an invite for this room.
+ * @param userId - the ID of the Matrix user to check permissions for
+ * @returns true if the user should be permitted to issue invites for this room.
+ */
+ canInvite(userId) {
+ let canInvite = this.getMyMembership() === "join";
+ const powerLevelsEvent = this.currentState.getStateEvents(_event2.EventType.RoomPowerLevels, "");
+ const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent();
+ const me = this.getMember(userId);
+ if (powerLevels && me && powerLevels.invite > me.powerLevel) {
+ canInvite = false;
+ }
+ return canInvite;
+ }
+
+ /**
+ * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`.
+ * @returns the join_rule applied to this room
+ */
+ getJoinRule() {
+ return this.currentState.getJoinRule();
+ }
+
+ /**
+ * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
+ * @returns the history_visibility applied to this room
+ */
+ getHistoryVisibility() {
+ return this.currentState.getHistoryVisibility();
+ }
+
+ /**
+ * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`.
+ * @returns the history_visibility applied to this room
+ */
+ getGuestAccess() {
+ return this.currentState.getGuestAccess();
+ }
+
+ /**
+ * Returns the type of the room from the `m.room.create` event content or undefined if none is set
+ * @returns the type of the room.
+ */
+ getType() {
+ const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, "");
+ if (!createEvent) {
+ if (!this.getTypeWarning) {
+ _logger.logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event");
+ this.getTypeWarning = true;
+ }
+ return undefined;
+ }
+ return createEvent.getContent()[_event2.RoomCreateTypeField];
+ }
+
+ /**
+ * Returns whether the room is a space-room as defined by MSC1772.
+ * @returns true if the room's type is RoomType.Space
+ */
+ isSpaceRoom() {
+ return this.getType() === _event2.RoomType.Space;
+ }
+
+ /**
+ * Returns whether the room is a call-room as defined by MSC3417.
+ * @returns true if the room's type is RoomType.UnstableCall
+ */
+ isCallRoom() {
+ return this.getType() === _event2.RoomType.UnstableCall;
+ }
+
+ /**
+ * Returns whether the room is a video room.
+ * @returns true if the room's type is RoomType.ElementVideo
+ */
+ isElementVideoRoom() {
+ return this.getType() === _event2.RoomType.ElementVideo;
+ }
+
+ /**
+ * Find the predecessor of this room.
+ *
+ * @param msc3946ProcessDynamicPredecessor - if true, look for an
+ * m.room.predecessor state event and use it if found (MSC3946).
+ * @returns null if this room has no predecessor. Otherwise, returns
+ * the roomId, last eventId and viaServers of the predecessor room.
+ *
+ * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events
+ * as well as m.room.create events to find predecessors.
+ *
+ * Note: if an m.predecessor event is used, eventId may be undefined
+ * since last_known_event_id is optional.
+ *
+ * Note: viaServers may be undefined, and will definitely be undefined if
+ * this predecessor comes from a RoomCreate event (rather than a
+ * RoomPredecessor, which has the optional via_servers property).
+ */
+ findPredecessor(msc3946ProcessDynamicPredecessor = false) {
+ const currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
+ if (!currentState) {
+ return null;
+ }
+ return currentState.findPredecessor(msc3946ProcessDynamicPredecessor);
+ }
+ roomNameGenerator(state) {
+ if (this.client.roomNameGenerator) {
+ const name = this.client.roomNameGenerator(this.roomId, state);
+ if (name !== null) {
+ return name;
+ }
+ }
+ switch (state.type) {
+ case RoomNameType.Actual:
+ return state.name;
+ case RoomNameType.Generated:
+ switch (state.subtype) {
+ case "Inviting":
+ return `Inviting ${memberNamesToRoomName(state.names, state.count)}`;
+ default:
+ return memberNamesToRoomName(state.names, state.count);
+ }
+ case RoomNameType.EmptyRoom:
+ if (state.oldName) {
+ return `Empty room (was ${state.oldName})`;
+ } else {
+ return "Empty room";
+ }
+ }
+ }
+
+ /**
+ * This is an internal method. Calculates the name of the room from the current
+ * room state.
+ * @param userId - The client's user ID. Used to filter room members
+ * correctly.
+ * @param ignoreRoomNameEvent - Return the implicit room name that we'd see if there
+ * was no m.room.name event.
+ * @returns The calculated room name.
+ */
+ calculateRoomName(userId, ignoreRoomNameEvent = false) {
+ if (!ignoreRoomNameEvent) {
+ // check for an alias, if any. for now, assume first alias is the
+ // official one.
+ const mRoomName = this.currentState.getStateEvents(_event2.EventType.RoomName, "");
+ if (mRoomName?.getContent().name) {
+ return this.roomNameGenerator({
+ type: RoomNameType.Actual,
+ name: mRoomName.getContent().name
+ });
+ }
+ }
+ const alias = this.getCanonicalAlias();
+ if (alias) {
+ return this.roomNameGenerator({
+ type: RoomNameType.Actual,
+ name: alias
+ });
+ }
+ const joinedMemberCount = this.currentState.getJoinedMemberCount();
+ const invitedMemberCount = this.currentState.getInvitedMemberCount();
+ // -1 because these numbers include the syncing user
+ let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1;
+
+ // get service members (e.g. helper bots) for exclusion
+ let excludedUserIds = [];
+ const mFunctionalMembers = this.currentState.getStateEvents(_event2.UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, "");
+ if (Array.isArray(mFunctionalMembers?.getContent().service_members)) {
+ excludedUserIds = mFunctionalMembers.getContent().service_members;
+ }
+
+ // get members that are NOT ourselves and are actually in the room.
+ let otherNames = [];
+ if (this.summaryHeroes) {
+ // if we have a summary, the member state events should be in the room state
+ this.summaryHeroes.forEach(userId => {
+ // filter service members
+ if (excludedUserIds.includes(userId)) {
+ inviteJoinCount--;
+ return;
+ }
+ const member = this.getMember(userId);
+ otherNames.push(member ? member.name : userId);
+ });
+ } else {
+ let otherMembers = this.currentState.getMembers().filter(m => {
+ return m.userId !== userId && (m.membership === "invite" || m.membership === "join");
+ });
+ otherMembers = otherMembers.filter(({
+ userId
+ }) => {
+ // filter service members
+ if (excludedUserIds.includes(userId)) {
+ inviteJoinCount--;
+ return false;
+ }
+ return true;
+ });
+ // make sure members have stable order
+ otherMembers.sort((a, b) => (0, _utils.compare)(a.userId, b.userId));
+ // only 5 first members, immitate summaryHeroes
+ otherMembers = otherMembers.slice(0, 5);
+ otherNames = otherMembers.map(m => m.name);
+ }
+ if (inviteJoinCount) {
+ return this.roomNameGenerator({
+ type: RoomNameType.Generated,
+ names: otherNames,
+ count: inviteJoinCount
+ });
+ }
+ const myMembership = this.getMyMembership();
+ // if I have created a room and invited people through
+ // 3rd party invites
+ if (myMembership == "join") {
+ const thirdPartyInvites = this.currentState.getStateEvents(_event2.EventType.RoomThirdPartyInvite);
+ if (thirdPartyInvites?.length) {
+ const thirdPartyNames = thirdPartyInvites.map(i => {
+ return i.getContent().display_name;
+ });
+ return this.roomNameGenerator({
+ type: RoomNameType.Generated,
+ subtype: "Inviting",
+ names: thirdPartyNames,
+ count: thirdPartyNames.length + 1
+ });
+ }
+ }
+
+ // let's try to figure out who was here before
+ let leftNames = otherNames;
+ // if we didn't have heroes, try finding them in the room state
+ if (!leftNames.length) {
+ leftNames = this.currentState.getMembers().filter(m => {
+ return m.userId !== userId && m.membership !== "invite" && m.membership !== "join";
+ }).map(m => m.name);
+ }
+ let oldName;
+ if (leftNames.length) {
+ oldName = this.roomNameGenerator({
+ type: RoomNameType.Generated,
+ names: leftNames,
+ count: leftNames.length + 1
+ });
+ }
+ return this.roomNameGenerator({
+ type: RoomNameType.EmptyRoom,
+ oldName
+ });
+ }
+
+ /**
+ * When we receive a new visibility change event:
+ *
+ * - store this visibility change alongside the timeline, in case we
+ * later need to apply it to an event that we haven't received yet;
+ * - if we have already received the event whose visibility has changed,
+ * patch it to reflect the visibility change and inform listeners.
+ */
+ applyNewVisibilityEvent(event) {
+ const visibilityChange = event.asVisibilityChange();
+ if (!visibilityChange) {
+ // The event is ill-formed.
+ return;
+ }
+
+ // Ignore visibility change events that are not emitted by moderators.
+ const userId = event.getSender();
+ if (!userId) {
+ return;
+ }
+ const isPowerSufficient = _event2.EVENT_VISIBILITY_CHANGE_TYPE.name && this.currentState.maySendStateEvent(_event2.EVENT_VISIBILITY_CHANGE_TYPE.name, userId) || _event2.EVENT_VISIBILITY_CHANGE_TYPE.altName && this.currentState.maySendStateEvent(_event2.EVENT_VISIBILITY_CHANGE_TYPE.altName, userId);
+ if (!isPowerSufficient) {
+ // Powerlevel is insufficient.
+ return;
+ }
+
+ // Record this change in visibility.
+ // If the event is not in our timeline and we only receive it later,
+ // we may need to apply the visibility change at a later date.
+
+ const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId);
+ if (visibilityEventsOnOriginalEvent) {
+ // It would be tempting to simply erase the latest visibility change
+ // but we need to record all of the changes in case the latest change
+ // is ever redacted.
+ //
+ // In practice, linear scans through `visibilityEvents` should be fast.
+ // However, to protect against a potential DoS attack, we limit the
+ // number of iterations in this loop.
+ let index = visibilityEventsOnOriginalEvent.length - 1;
+ const min = Math.max(0, visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH);
+ for (; index >= min; --index) {
+ const target = visibilityEventsOnOriginalEvent[index];
+ if (target.getTs() < event.getTs()) {
+ break;
+ }
+ }
+ if (index === -1) {
+ visibilityEventsOnOriginalEvent.unshift(event);
+ } else {
+ visibilityEventsOnOriginalEvent.splice(index + 1, 0, event);
+ }
+ } else {
+ this.visibilityEvents.set(visibilityChange.eventId, [event]);
+ }
+
+ // Finally, let's check if the event is already in our timeline.
+ // If so, we need to patch it and inform listeners.
+
+ const originalEvent = this.findEventById(visibilityChange.eventId);
+ if (!originalEvent) {
+ return;
+ }
+ originalEvent.applyVisibilityEvent(visibilityChange);
+ }
+ redactVisibilityChangeEvent(event) {
+ // Sanity checks.
+ if (!event.isVisibilityEvent) {
+ throw new Error("expected a visibility change event");
+ }
+ const relation = event.getRelation();
+ const originalEventId = relation?.event_id;
+ const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId);
+ if (!visibilityEventsOnOriginalEvent) {
+ // No visibility changes on the original event.
+ // In particular, this change event was not recorded,
+ // most likely because it was ill-formed.
+ return;
+ }
+ const index = visibilityEventsOnOriginalEvent.findIndex(change => change.getId() === event.getId());
+ if (index === -1) {
+ // This change event was not recorded, most likely because
+ // it was ill-formed.
+ return;
+ }
+ // Remove visibility change.
+ visibilityEventsOnOriginalEvent.splice(index, 1);
+
+ // If we removed the latest visibility change event, propagate changes.
+ if (index === visibilityEventsOnOriginalEvent.length) {
+ const originalEvent = this.findEventById(originalEventId);
+ if (!originalEvent) {
+ return;
+ }
+ if (index === 0) {
+ // We have just removed the only visibility change event.
+ this.visibilityEvents.delete(originalEventId);
+ originalEvent.applyVisibilityEvent();
+ } else {
+ const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1];
+ const newVisibility = newEvent.asVisibilityChange();
+ if (!newVisibility) {
+ // Event is ill-formed.
+ // This breaks our invariant.
+ throw new Error("at this stage, visibility changes should be well-formed");
+ }
+ originalEvent.applyVisibilityEvent(newVisibility);
+ }
+ }
+ }
+
+ /**
+ * When we receive an event whose visibility has been altered by
+ * a (more recent) visibility change event, patch the event in
+ * place so that clients now not to display it.
+ *
+ * @param event - Any matrix event. If this event has at least one a
+ * pending visibility change event, apply the latest visibility
+ * change event.
+ */
+ applyPendingVisibilityEvents(event) {
+ const visibilityEvents = this.visibilityEvents.get(event.getId());
+ if (!visibilityEvents || visibilityEvents.length == 0) {
+ // No pending visibility change in store.
+ return;
+ }
+ const visibilityEvent = visibilityEvents[visibilityEvents.length - 1];
+ const visibilityChange = visibilityEvent.asVisibilityChange();
+ if (!visibilityChange) {
+ return;
+ }
+ if (visibilityChange.visible) {
+ // Events are visible by default, no need to apply a visibility change.
+ // Note that we need to keep the visibility changes in `visibilityEvents`,
+ // in case we later fetch an older visibility change event that is superseded
+ // by `visibilityChange`.
+ }
+ if (visibilityEvent.getTs() < event.getTs()) {
+ // Something is wrong, the visibility change cannot happen before the
+ // event. Presumably an ill-formed event.
+ return;
+ }
+ event.applyVisibilityEvent(visibilityChange);
+ }
+
+ /**
+ * Find when a client has gained thread capabilities by inspecting the oldest
+ * threaded receipt
+ * @returns the timestamp of the oldest threaded receipt
+ */
+ getOldestThreadedReceiptTs() {
+ return this.oldestThreadedReceiptTs;
+ }
+
+ /**
+ * 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).
+ */
+ getLastUnthreadedReceiptFor(userId) {
+ return this.unthreadedReceipts.get(userId);
+ }
+
+ /**
+ * This issue should also be addressed on synapse's side and is tracked as part
+ * of https://github.com/matrix-org/synapse/issues/14837
+ *
+ *
+ * We consider a room fully read if the current user has sent
+ * the last event in the live timeline of that context and if the read receipt
+ * we have on record matches.
+ * This also detects all unread threads and applies the same logic to those
+ * contexts
+ */
+ fixupNotifications(userId) {
+ super.fixupNotifications(userId);
+ const unreadThreads = this.getThreads().filter(thread => this.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) > 0);
+ for (const thread of unreadThreads) {
+ thread.fixupNotifications(userId);
+ }
+ }
+}
+
+// a map from current event status to a list of allowed next statuses
+exports.Room = Room;
+const ALLOWED_TRANSITIONS = {
+ [_eventStatus.EventStatus.ENCRYPTING]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.CANCELLED],
+ [_eventStatus.EventStatus.SENDING]: [_eventStatus.EventStatus.ENCRYPTING, _eventStatus.EventStatus.QUEUED, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.SENT],
+ [_eventStatus.EventStatus.QUEUED]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.CANCELLED],
+ [_eventStatus.EventStatus.SENT]: [],
+ [_eventStatus.EventStatus.NOT_SENT]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.QUEUED, _eventStatus.EventStatus.CANCELLED],
+ [_eventStatus.EventStatus.CANCELLED]: []
+};
+let RoomNameType = /*#__PURE__*/function (RoomNameType) {
+ RoomNameType[RoomNameType["EmptyRoom"] = 0] = "EmptyRoom";
+ RoomNameType[RoomNameType["Generated"] = 1] = "Generated";
+ RoomNameType[RoomNameType["Actual"] = 2] = "Actual";
+ return RoomNameType;
+}({});
+exports.RoomNameType = RoomNameType;
+// Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn
+function memberNamesToRoomName(names, count) {
+ const countWithoutMe = count - 1;
+ if (!names.length) {
+ return "Empty room";
+ } else if (names.length === 1 && countWithoutMe <= 1) {
+ return names[0];
+ } else if (names.length === 2 && countWithoutMe <= 2) {
+ return `${names[0]} and ${names[1]}`;
+ } else {
+ const plural = countWithoutMe > 1;
+ if (plural) {
+ return `${names[0]} and ${countWithoutMe} others`;
+ } else {
+ return `${names[0]} and 1 other`;
+ }
+ }
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/search-result.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/search-result.js
new file mode 100644
index 0000000000..e0b1137236
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/search-result.js
@@ -0,0 +1,58 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.SearchResult = void 0;
+var _eventContext = require("./event-context");
+/*
+Copyright 2015 - 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.
+*/
+
+class SearchResult {
+ /**
+ * Create a SearchResponse from the response to /search
+ */
+
+ static fromJson(jsonObj, eventMapper) {
+ const jsonContext = jsonObj.context || {};
+ let eventsBefore = (jsonContext.events_before || []).map(eventMapper);
+ let eventsAfter = (jsonContext.events_after || []).map(eventMapper);
+ const context = new _eventContext.EventContext(eventMapper(jsonObj.result));
+
+ // Filter out any contextual events which do not correspond to the same timeline (thread or room)
+ const threadRootId = context.ourEvent.threadRootId;
+ eventsBefore = eventsBefore.filter(e => e.threadRootId === threadRootId);
+ eventsAfter = eventsAfter.filter(e => e.threadRootId === threadRootId);
+ context.setPaginateToken(jsonContext.start, true);
+ context.addEvents(eventsBefore, true);
+ context.addEvents(eventsAfter, false);
+ context.setPaginateToken(jsonContext.end, false);
+ return new SearchResult(jsonObj.rank, context);
+ }
+
+ /**
+ * Construct a new SearchResult
+ *
+ * @param rank - where this SearchResult ranks in the results
+ * @param context - the matching event and its
+ * context
+ */
+ constructor(rank, context) {
+ this.rank = rank;
+ this.context = context;
+ }
+}
+exports.SearchResult = SearchResult; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/thread.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/thread.js
new file mode 100644
index 0000000000..4471d512e2
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/thread.js
@@ -0,0 +1,649 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.ThreadFilterType = exports.ThreadEvent = exports.Thread = exports.THREAD_RELATION_TYPE = exports.FeatureSupport = exports.FILTER_RELATED_BY_SENDERS = exports.FILTER_RELATED_BY_REL_TYPES = void 0;
+exports.determineFeatureSupport = determineFeatureSupport;
+exports.threadFilterTypeToFilter = threadFilterTypeToFilter;
+var _client = require("../client");
+var _ReEmitter = require("../ReEmitter");
+var _event = require("../@types/event");
+var _event2 = require("./event");
+var _eventTimeline = require("./event-timeline");
+var _eventTimelineSet = require("./event-timeline-set");
+var _room = require("./room");
+var _NamespacedValue = require("../NamespacedValue");
+var _logger = require("../logger");
+var _readReceipt = require("./read-receipt");
+var _read_receipts = require("../@types/read_receipts");
+var _feature = require("../feature");
+function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; }
+function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; }
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2021 - 2023 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.
+ */
+let ThreadEvent = /*#__PURE__*/function (ThreadEvent) {
+ ThreadEvent["New"] = "Thread.new";
+ ThreadEvent["Update"] = "Thread.update";
+ ThreadEvent["NewReply"] = "Thread.newReply";
+ ThreadEvent["ViewThread"] = "Thread.viewThread";
+ ThreadEvent["Delete"] = "Thread.delete";
+ return ThreadEvent;
+}({});
+exports.ThreadEvent = ThreadEvent;
+let FeatureSupport = /*#__PURE__*/function (FeatureSupport) {
+ FeatureSupport[FeatureSupport["None"] = 0] = "None";
+ FeatureSupport[FeatureSupport["Experimental"] = 1] = "Experimental";
+ FeatureSupport[FeatureSupport["Stable"] = 2] = "Stable";
+ return FeatureSupport;
+}({});
+exports.FeatureSupport = FeatureSupport;
+function determineFeatureSupport(stable, unstable) {
+ if (stable) {
+ return FeatureSupport.Stable;
+ } else if (unstable) {
+ return FeatureSupport.Experimental;
+ } else {
+ return FeatureSupport.None;
+ }
+}
+class Thread extends _readReceipt.ReadReceipt {
+ constructor(id, rootEvent, opts) {
+ super();
+ this.id = id;
+ this.rootEvent = rootEvent;
+ /**
+ * A reference to all the events ID at the bottom of the threads
+ */
+ _defineProperty(this, "timelineSet", void 0);
+ _defineProperty(this, "timeline", []);
+ _defineProperty(this, "_currentUserParticipated", false);
+ _defineProperty(this, "reEmitter", void 0);
+ _defineProperty(this, "lastEvent", void 0);
+ _defineProperty(this, "replyCount", 0);
+ _defineProperty(this, "lastPendingEvent", void 0);
+ _defineProperty(this, "pendingReplyCount", 0);
+ _defineProperty(this, "room", void 0);
+ _defineProperty(this, "client", void 0);
+ _defineProperty(this, "pendingEventOrdering", void 0);
+ _defineProperty(this, "initialEventsFetched", !Thread.hasServerSideSupport);
+ /**
+ * An array of events to add to the timeline once the thread has been initialised
+ * with server suppport.
+ */
+ _defineProperty(this, "replayEvents", []);
+ _defineProperty(this, "onBeforeRedaction", (event, redaction) => {
+ if (event?.isRelation(THREAD_RELATION_TYPE.name) && this.room.eventShouldLiveIn(event).threadId === this.id && event.getId() !== this.id &&
+ // the root event isn't counted in the length so ignore this redaction
+ !redaction.status // only respect it when it succeeds
+ ) {
+ this.replyCount--;
+ this.updatePendingReplyCount();
+ this.emit(ThreadEvent.Update, this);
+ }
+ });
+ _defineProperty(this, "onRedaction", async event => {
+ if (event.threadRootId !== this.id) return; // ignore redactions for other timelines
+ if (this.replyCount <= 0) {
+ for (const threadEvent of this.timeline) {
+ this.clearEventMetadata(threadEvent);
+ }
+ this.lastEvent = this.rootEvent;
+ this._currentUserParticipated = false;
+ this.emit(ThreadEvent.Delete, this);
+ } else {
+ await this.updateThreadMetadata();
+ }
+ });
+ _defineProperty(this, "onTimelineEvent", (event, room, toStartOfTimeline) => {
+ // Add a synthesized receipt when paginating forward in the timeline
+ if (!toStartOfTimeline) {
+ const sender = event.getSender();
+ if (sender && room && this.shouldSendLocalEchoReceipt(sender, event)) {
+ room.addLocalEchoReceipt(sender, event, _read_receipts.ReceiptType.Read);
+ }
+ }
+ this.onEcho(event, toStartOfTimeline ?? false);
+ });
+ _defineProperty(this, "onLocalEcho", event => {
+ this.onEcho(event, false);
+ });
+ _defineProperty(this, "onEcho", async (event, toStartOfTimeline) => {
+ if (event.threadRootId !== this.id) return; // ignore echoes for other timelines
+ if (this.lastEvent === event) return; // ignore duplicate events
+ await this.updateThreadMetadata();
+ if (!event.isRelation(THREAD_RELATION_TYPE.name)) return; // don't send a new reply event for reactions or edits
+ if (toStartOfTimeline) return; // ignore messages added to the start of the timeline
+ this.emit(ThreadEvent.NewReply, this, event);
+ });
+ if (!opts?.room) {
+ // Logging/debugging for https://github.com/vector-im/element-web/issues/22141
+ // Hope is that we end up with a more obvious stack trace.
+ throw new Error("element-web#22141: A thread requires a room in order to function");
+ }
+ this.room = opts.room;
+ this.client = opts.client;
+ this.pendingEventOrdering = opts.pendingEventOrdering ?? _client.PendingEventOrdering.Chronological;
+ this.timelineSet = new _eventTimelineSet.EventTimelineSet(this.room, {
+ timelineSupport: true,
+ pendingEvents: true
+ }, this.client, this);
+ this.reEmitter = new _ReEmitter.TypedReEmitter(this);
+ this.reEmitter.reEmit(this.timelineSet, [_room.RoomEvent.Timeline, _room.RoomEvent.TimelineReset]);
+ this.room.on(_event2.MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
+ this.room.on(_room.RoomEvent.Redaction, this.onRedaction);
+ this.room.on(_room.RoomEvent.LocalEchoUpdated, this.onLocalEcho);
+ this.timelineSet.on(_room.RoomEvent.Timeline, this.onTimelineEvent);
+ this.processReceipts(opts.receipts);
+
+ // even if this thread is thought to be originating from this client, we initialise it as we may be in a
+ // gappy sync and a thread around this event may already exist.
+ this.updateThreadMetadata();
+ this.setEventMetadata(this.rootEvent);
+ }
+ async fetchRootEvent() {
+ this.rootEvent = this.room.findEventById(this.id);
+ // If the rootEvent does not exist in the local stores, then fetch it from the server.
+ try {
+ const eventData = await this.client.fetchRoomEvent(this.roomId, this.id);
+ const mapper = this.client.getEventMapper();
+ this.rootEvent = mapper(eventData); // will merge with existing event object if such is known
+ } catch (e) {
+ _logger.logger.error("Failed to fetch thread root to construct thread with", e);
+ }
+ await this.processEvent(this.rootEvent);
+ }
+ static setServerSideSupport(status) {
+ Thread.hasServerSideSupport = status;
+ if (status !== FeatureSupport.Stable) {
+ FILTER_RELATED_BY_SENDERS.setPreferUnstable(true);
+ FILTER_RELATED_BY_REL_TYPES.setPreferUnstable(true);
+ THREAD_RELATION_TYPE.setPreferUnstable(true);
+ }
+ }
+ static setServerSideListSupport(status) {
+ Thread.hasServerSideListSupport = status;
+ }
+ static setServerSideFwdPaginationSupport(status) {
+ Thread.hasServerSideFwdPaginationSupport = status;
+ }
+ shouldSendLocalEchoReceipt(sender, event) {
+ const recursionSupport = this.client.canSupport.get(_feature.Feature.RelationsRecursion) ?? _feature.ServerSupport.Unsupported;
+ if (recursionSupport === _feature.ServerSupport.Unsupported) {
+ // Normally we add a local receipt, but if we don't have
+ // recursion support, then events may arrive out of order, so we
+ // only create a receipt if it's after our existing receipt.
+ const oldReceiptEventId = this.getReadReceiptForUserId(sender)?.eventId;
+ if (oldReceiptEventId) {
+ const receiptEvent = this.findEventById(oldReceiptEventId);
+ if (receiptEvent && receiptEvent.getTs() > event.getTs()) {
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+ get roomState() {
+ return this.room.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS);
+ }
+ addEventToTimeline(event, toStartOfTimeline) {
+ if (!this.findEventById(event.getId())) {
+ this.timelineSet.addEventToTimeline(event, this.liveTimeline, {
+ toStartOfTimeline,
+ fromCache: false,
+ roomState: this.roomState
+ });
+ this.timeline = this.events;
+ }
+ }
+
+ /**
+ * TEMPORARY. Only call this when MSC3981 is not available, and we have some
+ * late-arriving events to insert, because we recursively found them as part
+ * of populating a thread. When we have MSC3981 we won't need it, because
+ * they will all be supplied by the homeserver in one request, and they will
+ * already be in the right order in that response.
+ * This is a copy of addEventToTimeline above, modified to call
+ * insertEventIntoTimeline so this event is inserted into our best guess of
+ * the right place based on timestamp. (We should be using Sync Order but we
+ * don't have it.)
+ *
+ * @internal
+ */
+ insertEventIntoTimeline(event) {
+ const eventId = event.getId();
+ if (!eventId) {
+ return;
+ }
+ // If the event is already in this thread, bail out
+ if (this.findEventById(eventId)) {
+ return;
+ }
+ this.timelineSet.insertEventIntoTimeline(event, this.liveTimeline, this.roomState);
+
+ // As far as we know, timeline should always be the same as events
+ this.timeline = this.events;
+ }
+ addEvents(events, toStartOfTimeline) {
+ events.forEach(ev => this.addEvent(ev, toStartOfTimeline, false));
+ this.updateThreadMetadata();
+ }
+
+ /**
+ * Add an event to the thread and updates
+ * the tail/root references if needed
+ * Will fire "Thread.update"
+ * @param event - The event to add
+ * @param toStartOfTimeline - whether the event is being added
+ * to the start (and not the end) of the timeline.
+ * @param emit - whether to emit the Update event if the thread was updated or not.
+ */
+ async addEvent(event, toStartOfTimeline, emit = true) {
+ this.setEventMetadata(event);
+ const lastReply = this.lastReply();
+ const isNewestReply = !lastReply || event.localTimestamp >= lastReply.localTimestamp;
+
+ // Add all incoming events to the thread's timeline set when there's no server support
+ if (!Thread.hasServerSideSupport) {
+ // all the relevant membership info to hydrate events with a sender
+ // is held in the main room timeline
+ // We want to fetch the room state from there and pass it down to this thread
+ // timeline set to let it reconcile an event with its relevant RoomMember
+ this.addEventToTimeline(event, toStartOfTimeline);
+ this.client.decryptEventIfNeeded(event, {});
+ } else if (!toStartOfTimeline && this.initialEventsFetched && isNewestReply) {
+ this.addEventToTimeline(event, false);
+ this.fetchEditsWhereNeeded(event);
+ } else if (event.isRelation(_event.RelationType.Annotation) || event.isRelation(_event.RelationType.Replace)) {
+ if (!this.initialEventsFetched) {
+ /**
+ * A thread can be fully discovered via a single sync response
+ * And when that's the case we still ask the server to do an initialisation
+ * as it's the safest to ensure we have everything.
+ * However when we are in that scenario we might loose annotation or edits
+ *
+ * This fix keeps a reference to those events and replay them once the thread
+ * has been initialised properly.
+ */
+ this.replayEvents?.push(event);
+ } else {
+ const recursionSupport = this.client.canSupport.get(_feature.Feature.RelationsRecursion) ?? _feature.ServerSupport.Unsupported;
+ if (recursionSupport === _feature.ServerSupport.Unsupported) {
+ this.insertEventIntoTimeline(event);
+ } else {
+ this.addEventToTimeline(event, toStartOfTimeline);
+ }
+ }
+ // Apply annotations and replace relations to the relations of the timeline only
+ this.timelineSet.relations?.aggregateParentEvent(event);
+ this.timelineSet.relations?.aggregateChildEvent(event, this.timelineSet);
+ return;
+ }
+
+ // If no thread support exists we want to count all thread relation
+ // added as a reply. We can't rely on the bundled relationships count
+ if ((!Thread.hasServerSideSupport || !this.rootEvent) && event.isRelation(THREAD_RELATION_TYPE.name)) {
+ this.replyCount++;
+ }
+ if (emit) {
+ this.emit(ThreadEvent.NewReply, this, event);
+ this.updateThreadMetadata();
+ }
+ }
+ async processEvent(event) {
+ if (event) {
+ this.setEventMetadata(event);
+ await this.fetchEditsWhereNeeded(event);
+ }
+ this.timeline = this.events;
+ }
+
+ /**
+ * Processes the receipts that were caught during initial sync
+ * When clients become aware of a thread, they try to retrieve those read receipts
+ * and apply them to the current thread
+ * @param receipts - A collection of the receipts cached from initial sync
+ */
+ processReceipts(receipts = []) {
+ for (const {
+ eventId,
+ receiptType,
+ userId,
+ receipt,
+ synthetic
+ } of receipts) {
+ this.addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic);
+ }
+ }
+ getRootEventBundledRelationship(rootEvent = this.rootEvent) {
+ return rootEvent?.getServerAggregatedRelation(THREAD_RELATION_TYPE.name);
+ }
+ async processRootEvent() {
+ const bundledRelationship = this.getRootEventBundledRelationship();
+ if (Thread.hasServerSideSupport && bundledRelationship) {
+ this.replyCount = bundledRelationship.count;
+ this._currentUserParticipated = !!bundledRelationship.current_user_participated;
+ const mapper = this.client.getEventMapper();
+ // re-insert roomId
+ this.lastEvent = mapper(_objectSpread(_objectSpread({}, bundledRelationship.latest_event), {}, {
+ room_id: this.roomId
+ }));
+ this.updatePendingReplyCount();
+ await this.processEvent(this.lastEvent);
+ }
+ }
+ updatePendingReplyCount() {
+ const unfilteredPendingEvents = this.pendingEventOrdering === _client.PendingEventOrdering.Detached ? this.room.getPendingEvents() : this.events;
+ const pendingEvents = unfilteredPendingEvents.filter(ev => ev.threadRootId === this.id && ev.isRelation(THREAD_RELATION_TYPE.name) && ev.status !== null && ev.getId() !== this.lastEvent?.getId());
+ this.lastPendingEvent = pendingEvents.length ? pendingEvents[pendingEvents.length - 1] : undefined;
+ this.pendingReplyCount = pendingEvents.length;
+ }
+
+ /**
+ * Reset the live timeline of all timelineSets, and start new ones.
+ *
+ * <p>This is used when /sync returns a 'limited' timeline. 'Limited' means that there's a gap between the messages
+ * /sync returned, and the last known message in our timeline. In such a case, our live timeline isn't live anymore
+ * and has to be replaced by a new one. To make sure we can continue paginating our timelines correctly, we have to
+ * set new pagination tokens on the old and the new timeline.
+ *
+ * @param backPaginationToken - token for back-paginating the new timeline
+ * @param forwardPaginationToken - token for forward-paginating the old live timeline,
+ * if absent or null, all timelines are reset, removing old ones (including the previous live
+ * timeline which would otherwise be unable to paginate forwards without this token).
+ * Removing just the old live timeline whilst preserving previous ones is not supported.
+ */
+ async resetLiveTimeline(backPaginationToken, forwardPaginationToken) {
+ const oldLive = this.liveTimeline;
+ this.timelineSet.resetLiveTimeline(backPaginationToken ?? undefined, forwardPaginationToken ?? undefined);
+ const newLive = this.liveTimeline;
+
+ // FIXME: Remove the following as soon as https://github.com/matrix-org/synapse/issues/14830 is resolved.
+ //
+ // The pagination API for thread timelines currently can't handle the type of pagination tokens returned by sync
+ //
+ // To make this work anyway, we'll have to transform them into one of the types that the API can handle.
+ // One option is passing the tokens to /messages, which can handle sync tokens, and returns the right format.
+ // /messages does not return new tokens on requests with a limit of 0.
+ // This means our timelines might overlap a slight bit, but that's not an issue, as we deduplicate messages
+ // anyway.
+
+ let newBackward;
+ let oldForward;
+ if (backPaginationToken) {
+ const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, _eventTimeline.Direction.Forward);
+ newBackward = res.end;
+ }
+ if (forwardPaginationToken) {
+ const res = await this.client.createMessagesRequest(this.roomId, forwardPaginationToken, 1, _eventTimeline.Direction.Backward);
+ oldForward = res.start;
+ }
+ // Only replace the token if we don't have paginated away from this position already. This situation doesn't
+ // occur today, but if the above issue is resolved, we'd have to go down this path.
+ if (forwardPaginationToken && oldLive.getPaginationToken(_eventTimeline.Direction.Forward) === forwardPaginationToken) {
+ oldLive.setPaginationToken(oldForward ?? null, _eventTimeline.Direction.Forward);
+ }
+ if (backPaginationToken && newLive.getPaginationToken(_eventTimeline.Direction.Backward) === backPaginationToken) {
+ newLive.setPaginationToken(newBackward ?? null, _eventTimeline.Direction.Backward);
+ }
+ }
+ async updateThreadMetadata() {
+ this.updatePendingReplyCount();
+ if (Thread.hasServerSideSupport) {
+ // Ensure we show *something* as soon as possible, we'll update it as soon as we get better data, but we
+ // don't want the thread preview to be empty if we can avoid it
+ if (!this.initialEventsFetched) {
+ await this.processRootEvent();
+ }
+ await this.fetchRootEvent();
+ }
+ await this.processRootEvent();
+ if (!this.initialEventsFetched) {
+ this.initialEventsFetched = true;
+ // fetch initial event to allow proper pagination
+ try {
+ // if the thread has regular events, this will just load the last reply.
+ // if the thread is newly created, this will load the root event.
+ if (this.replyCount === 0 && this.rootEvent) {
+ this.timelineSet.addEventsToTimeline([this.rootEvent], true, this.liveTimeline, null);
+ this.liveTimeline.setPaginationToken(null, _eventTimeline.Direction.Backward);
+ } else {
+ await this.client.paginateEventTimeline(this.liveTimeline, {
+ backwards: true,
+ limit: Math.max(1, this.length)
+ });
+ }
+ for (const event of this.replayEvents) {
+ this.addEvent(event, false);
+ }
+ this.replayEvents = null;
+ // just to make sure that, if we've created a timeline window for this thread before the thread itself
+ // existed (e.g. when creating a new thread), we'll make sure the panel is force refreshed correctly.
+ this.emit(_room.RoomEvent.TimelineReset, this.room, this.timelineSet, true);
+ } catch (e) {
+ _logger.logger.error("Failed to load start of newly created thread: ", e);
+ this.initialEventsFetched = false;
+ }
+ }
+ this.emit(ThreadEvent.Update, this);
+ }
+
+ // XXX: Workaround for https://github.com/matrix-org/matrix-spec-proposals/pull/2676/files#r827240084
+ async fetchEditsWhereNeeded(...events) {
+ const recursionSupport = this.client.canSupport.get(_feature.Feature.RelationsRecursion) ?? _feature.ServerSupport.Unsupported;
+ if (recursionSupport === _feature.ServerSupport.Unsupported) {
+ return Promise.all(events.filter(isAnEncryptedThreadMessage).map(async event => {
+ try {
+ const relations = await this.client.relations(this.roomId, event.getId(), _event.RelationType.Replace, event.getType(), {
+ limit: 1
+ });
+ if (relations.events.length) {
+ const editEvent = relations.events[0];
+ event.makeReplaced(editEvent);
+ this.insertEventIntoTimeline(editEvent);
+ }
+ } catch (e) {
+ _logger.logger.error("Failed to load edits for encrypted thread event", e);
+ }
+ }));
+ }
+ }
+ setEventMetadata(event) {
+ if (event) {
+ _eventTimeline.EventTimeline.setEventMetadata(event, this.roomState, false);
+ event.setThread(this);
+ }
+ }
+ clearEventMetadata(event) {
+ if (event) {
+ event.setThread(undefined);
+ delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name];
+ }
+ }
+
+ /**
+ * Finds an event by ID in the current thread
+ */
+ findEventById(eventId) {
+ return this.timelineSet.findEventById(eventId);
+ }
+
+ /**
+ * Return last reply to the thread, if known.
+ */
+ lastReply(matches = () => true) {
+ for (let i = this.timeline.length - 1; i >= 0; i--) {
+ const event = this.timeline[i];
+ if (matches(event)) {
+ return event;
+ }
+ }
+ return null;
+ }
+ get roomId() {
+ return this.room.roomId;
+ }
+
+ /**
+ * The number of messages in the thread
+ * Only count rel_type=m.thread as we want to
+ * exclude annotations from that number
+ */
+ get length() {
+ return this.replyCount + this.pendingReplyCount;
+ }
+
+ /**
+ * A getter for the last event of the thread.
+ * This might be a synthesized event, if so, it will not emit any events to listeners.
+ */
+ get replyToEvent() {
+ return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply();
+ }
+ get events() {
+ return this.liveTimeline.getEvents();
+ }
+ has(eventId) {
+ return this.timelineSet.findEventById(eventId) instanceof _event2.MatrixEvent;
+ }
+ get hasCurrentUserParticipated() {
+ return this._currentUserParticipated;
+ }
+ get liveTimeline() {
+ return this.timelineSet.getLiveTimeline();
+ }
+ getUnfilteredTimelineSet() {
+ return this.timelineSet;
+ }
+ addReceipt(event, synthetic) {
+ throw new Error("Unsupported function on the thread model");
+ }
+
+ /**
+ * Get the ID of the event that a given user has read up to within this thread,
+ * or null if we have received no read receipt (at all) 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) {
+ const isCurrentUser = userId === this.client.getUserId();
+ const lastReply = this.timeline[this.timeline.length - 1];
+ if (isCurrentUser && lastReply) {
+ // If the last activity in a thread is prior to the first threaded read receipt
+ // sent in the room (suggesting that it was sent before the user started
+ // using a client that supported threaded read receipts), we want to
+ // consider this thread as read.
+ const beforeFirstThreadedReceipt = lastReply.getTs() < this.room.getOldestThreadedReceiptTs();
+ const lastReplyId = lastReply.getId();
+ // Some unsent events do not have an ID, we do not want to consider them read
+ if (beforeFirstThreadedReceipt && lastReplyId) {
+ return lastReplyId;
+ }
+ }
+ const readUpToId = super.getEventReadUpTo(userId, ignoreSynthesized);
+
+ // Check whether the unthreaded read receipt for that user is more recent
+ // than the read receipt inside that thread.
+ if (lastReply) {
+ const unthreadedReceipt = this.room.getLastUnthreadedReceiptFor(userId);
+ if (!unthreadedReceipt) {
+ return readUpToId;
+ }
+ for (let i = this.timeline?.length - 1; i >= 0; --i) {
+ const ev = this.timeline[i];
+ // If we encounter the `readUpToId` we do not need to look further
+ // there is no "more recent" unthreaded read receipt
+ if (ev.getId() === readUpToId) return readUpToId;
+
+ // Inspecting events from most recent to oldest, we're checking
+ // whether an unthreaded read receipt is more recent that the current event.
+ // We usually prefer relying on the order of the DAG but in this scenario
+ // it is not possible and we have to rely on timestamp
+ if (ev.getTs() < unthreadedReceipt.ts) return ev.getId() ?? readUpToId;
+ }
+ }
+ return readUpToId;
+ }
+
+ /**
+ * Determine if the given user has read a particular event.
+ *
+ * It is invalid to call this method with an event that is not part of this thread.
+ *
+ * This is not a definitive check as it only checks the events that have been
+ * loaded client-side 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) {
+ if (userId === this.client.getUserId()) {
+ // Consider an event read if it's part of a thread that is before the
+ // first threaded receipt sent in that room. It is likely that it is
+ // part of a thread that was created before MSC3771 was implemented.
+ // Or before the last unthreaded receipt for the logged in user
+ const beforeFirstThreadedReceipt = (this.lastReply()?.getTs() ?? 0) < this.room.getOldestThreadedReceiptTs();
+ const unthreadedReceiptTs = this.room.getLastUnthreadedReceiptFor(userId)?.ts ?? 0;
+ const beforeLastUnthreadedReceipt = (this?.lastReply()?.getTs() ?? 0) < unthreadedReceiptTs;
+ if (beforeFirstThreadedReceipt || beforeLastUnthreadedReceipt) {
+ return true;
+ }
+ }
+ return super.hasUserReadEvent(userId, eventId);
+ }
+ setUnread(type, count) {
+ return this.room.setThreadUnreadNotificationCount(this.id, type, count);
+ }
+}
+
+/**
+ * Decide whether an event deserves to have its potential edits fetched.
+ *
+ * @returns true if this event is encrypted and is a message that is part of a
+ * thread - either inside it, or a root.
+ */
+exports.Thread = Thread;
+_defineProperty(Thread, "hasServerSideSupport", FeatureSupport.None);
+_defineProperty(Thread, "hasServerSideListSupport", FeatureSupport.None);
+_defineProperty(Thread, "hasServerSideFwdPaginationSupport", FeatureSupport.None);
+function isAnEncryptedThreadMessage(event) {
+ return event.isEncrypted() && (event.isRelation(THREAD_RELATION_TYPE.name) || event.isThreadRoot);
+}
+const FILTER_RELATED_BY_SENDERS = new _NamespacedValue.ServerControlledNamespacedValue("related_by_senders", "io.element.relation_senders");
+exports.FILTER_RELATED_BY_SENDERS = FILTER_RELATED_BY_SENDERS;
+const FILTER_RELATED_BY_REL_TYPES = new _NamespacedValue.ServerControlledNamespacedValue("related_by_rel_types", "io.element.relation_types");
+exports.FILTER_RELATED_BY_REL_TYPES = FILTER_RELATED_BY_REL_TYPES;
+const THREAD_RELATION_TYPE = new _NamespacedValue.ServerControlledNamespacedValue("m.thread", "io.element.thread");
+exports.THREAD_RELATION_TYPE = THREAD_RELATION_TYPE;
+let ThreadFilterType = /*#__PURE__*/function (ThreadFilterType) {
+ ThreadFilterType[ThreadFilterType["My"] = 0] = "My";
+ ThreadFilterType[ThreadFilterType["All"] = 1] = "All";
+ return ThreadFilterType;
+}({});
+exports.ThreadFilterType = ThreadFilterType;
+function threadFilterTypeToFilter(type) {
+ switch (type) {
+ case ThreadFilterType.My:
+ return "participated";
+ default:
+ return "all";
+ }
+} \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/typed-event-emitter.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/typed-event-emitter.js
new file mode 100644
index 0000000000..c1160948ba
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/typed-event-emitter.js
@@ -0,0 +1,200 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.TypedEventEmitter = exports.EventEmitterEvents = void 0;
+var _events = require("events");
+/*
+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.
+*/
+// eslint-disable-next-line no-restricted-imports
+/** Events emitted by EventEmitter itself */
+let EventEmitterEvents = /*#__PURE__*/function (EventEmitterEvents) {
+ EventEmitterEvents["NewListener"] = "newListener";
+ EventEmitterEvents["RemoveListener"] = "removeListener";
+ EventEmitterEvents["Error"] = "error";
+ return EventEmitterEvents;
+}({});
+/** Base class for types mapping from event name to the type of listeners to that event */
+/**
+ * The expected type of a listener function for a particular event.
+ *
+ * Type parameters:
+ * * `E` - List of all events emitted by the `TypedEventEmitter`. Normally an enum type.
+ * * `A` - A type providing mappings from event names to listener types.
+ * * `T` - The name of the actual event that this listener is for. Normally one of the types in `E` or
+ * {@link EventEmitterEvents}.
+ */
+exports.EventEmitterEvents = EventEmitterEvents;
+/**
+ * Typed Event Emitter class which can act as a Base Model for all our model
+ * and communication events.
+ * This makes it much easier for us to distinguish between events, as we now need
+ * to properly type this, so that our events are not stringly-based and prone
+ * to silly typos.
+ *
+ * Type parameters:
+ * * `Events` - List of all events emitted by this `TypedEventEmitter`. Normally an enum type.
+ * * `Arguments` - A {@link ListenerMap} type providing mappings from event names to listener types.
+ * * `SuperclassArguments` - TODO: not really sure. Alternative listener mappings, I think? But only honoured for `.emit`?
+ */
+class TypedEventEmitter extends _events.EventEmitter {
+ /**
+ * Alias for {@link TypedEventEmitter#on}.
+ */
+ addListener(event, listener) {
+ return super.addListener(event, listener);
+ }
+
+ /**
+ * Synchronously calls each of the listeners registered for the event named
+ * `event`, in the order they were registered, passing the supplied arguments
+ * to each.
+ *
+ * @param event - The name of the event to emit
+ * @param args - Arguments to pass to the listener
+ * @returns `true` if the event had listeners, `false` otherwise.
+ */
+
+ emit(event, ...args) {
+ return super.emit(event, ...args);
+ }
+
+ /**
+ * Returns the number of listeners listening to the event named `event`.
+ *
+ * @param event - The name of the event being listened for
+ */
+ listenerCount(event) {
+ return super.listenerCount(event);
+ }
+
+ /**
+ * Returns a copy of the array of listeners for the event named `event`.
+ */
+ listeners(event) {
+ return super.listeners(event);
+ }
+
+ /**
+ * Alias for {@link TypedEventEmitter#removeListener}
+ */
+ off(event, listener) {
+ return super.off(event, listener);
+ }
+
+ /**
+ * Adds the `listener` function to the end of the listeners array for the
+ * event named `event`.
+ *
+ * No checks are made to see if the `listener` has already been added. Multiple calls
+ * passing the same combination of `event` and `listener` will result in the `listener`
+ * being added, and called, multiple times.
+ *
+ * By default, event listeners are invoked in the order they are added. The
+ * {@link TypedEventEmitter#prependListener} method can be used as an alternative to add the
+ * event listener to the beginning of the listeners array.
+ *
+ * @param event - The name of the event.
+ * @param listener - The callback function
+ *
+ * @returns a reference to the `EventEmitter`, so that calls can be chained.
+ */
+ on(event, listener) {
+ return super.on(event, listener);
+ }
+
+ /**
+ * Adds a **one-time** `listener` function for the event named `event`. The
+ * next time `event` is triggered, this listener is removed and then invoked.
+ *
+ * Returns a reference to the `EventEmitter`, so that calls can be chained.
+ *
+ * By default, event listeners are invoked in the order they are added.
+ * The {@link TypedEventEmitter#prependOnceListener} method can be used as an alternative to add the
+ * event listener to the beginning of the listeners array.
+ *
+ * @param event - The name of the event.
+ * @param listener - The callback function
+ *
+ * @returns a reference to the `EventEmitter`, so that calls can be chained.
+ */
+ once(event, listener) {
+ return super.once(event, listener);
+ }
+
+ /**
+ * Adds the `listener` function to the _beginning_ of the listeners array for the
+ * event named `event`.
+ *
+ * No checks are made to see if the `listener` has already been added. Multiple calls
+ * passing the same combination of `event` and `listener` will result in the `listener`
+ * being added, and called, multiple times.
+ *
+ * @param event - The name of the event.
+ * @param listener - The callback function
+ *
+ * @returns a reference to the `EventEmitter`, so that calls can be chained.
+ */
+ prependListener(event, listener) {
+ return super.prependListener(event, listener);
+ }
+
+ /**
+ * Adds a **one-time**`listener` function for the event named `event` to the _beginning_ of the listeners array.
+ * The next time `event` is triggered, this listener is removed, and then invoked.
+ *
+ * @param event - The name of the event.
+ * @param listener - The callback function
+ *
+ * @returns a reference to the `EventEmitter`, so that calls can be chained.
+ */
+ prependOnceListener(event, listener) {
+ return super.prependOnceListener(event, listener);
+ }
+
+ /**
+ * Removes all listeners, or those of the specified `event`.
+ *
+ * It is bad practice to remove listeners added elsewhere in the code,
+ * particularly when the `EventEmitter` instance was created by some other
+ * component or module (e.g. sockets or file streams).
+ *
+ * @param event - The name of the event. If undefined, all listeners everywhere are removed.
+ * @returns a reference to the `EventEmitter`, so that calls can be chained.
+ */
+ removeAllListeners(event) {
+ return super.removeAllListeners(event);
+ }
+
+ /**
+ * Removes the specified `listener` from the listener array for the event named `event`.
+ *
+ * @returns a reference to the `EventEmitter`, so that calls can be chained.
+ */
+ removeListener(event, listener) {
+ return super.removeListener(event, listener);
+ }
+
+ /**
+ * Returns a copy of the array of listeners for the event named `eventName`,
+ * including any wrappers (such as those created by `.once()`).
+ */
+ rawListeners(event) {
+ return super.rawListeners(event);
+ }
+}
+exports.TypedEventEmitter = TypedEventEmitter; \ No newline at end of file
diff --git a/comm/chat/protocols/matrix/lib/matrix-sdk/models/user.js b/comm/chat/protocols/matrix/lib/matrix-sdk/models/user.js
new file mode 100644
index 0000000000..bc941a8bb3
--- /dev/null
+++ b/comm/chat/protocols/matrix/lib/matrix-sdk/models/user.js
@@ -0,0 +1,211 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.UserEvent = exports.User = void 0;
+var _typedEventEmitter = require("./typed-event-emitter");
+function _defineProperty(obj, key, value) { key = _toPropertyKey(key); if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+function _toPropertyKey(arg) { var key = _toPrimitive(arg, "string"); return typeof key === "symbol" ? key : String(key); }
+function _toPrimitive(input, hint) { if (typeof input !== "object" || input === null) return input; var prim = input[Symbol.toPrimitive]; if (prim !== undefined) { var res = prim.call(input, hint || "default"); if (typeof res !== "object") return res; throw new TypeError("@@toPrimitive must return a primitive value."); } return (hint === "string" ? String : Number)(input); } /*
+ Copyright 2015 - 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.
+ */
+let UserEvent = /*#__PURE__*/function (UserEvent) {
+ UserEvent["DisplayName"] = "User.displayName";
+ UserEvent["AvatarUrl"] = "User.avatarUrl";
+ UserEvent["Presence"] = "User.presence";
+ UserEvent["CurrentlyActive"] = "User.currentlyActive";
+ UserEvent["LastPresenceTs"] = "User.lastPresenceTs";
+ return UserEvent;
+}({});
+exports.UserEvent = UserEvent;
+class User extends _typedEventEmitter.TypedEventEmitter {
+ /**
+ * Construct a new User. A User must have an ID and can optionally have extra information associated with it.
+ * @param userId - Required. The ID of this user.
+ */
+ constructor(userId) {
+ super();
+ this.userId = userId;
+ _defineProperty(this, "modified", -1);
+ /**
+ * The 'displayname' of the user if known.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "displayName", void 0);
+ _defineProperty(this, "rawDisplayName", void 0);
+ /**
+ * The 'avatar_url' of the user if known.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "avatarUrl", void 0);
+ /**
+ * The presence status message if known.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "presenceStatusMsg", void 0);
+ /**
+ * The presence enum if known.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "presence", "offline");
+ /**
+ * Timestamp (ms since the epoch) for when we last received presence data for this user.
+ * We can subtract lastActiveAgo from this to approximate an absolute value for when a user was last active.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "lastActiveAgo", 0);
+ /**
+ * The time elapsed in ms since the user interacted proactively with the server,
+ * or we saw a message from the user
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "lastPresenceTs", 0);
+ /**
+ * Whether we should consider lastActiveAgo to be an approximation
+ * and that the user should be seen as active 'now'
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "currentlyActive", false);
+ /**
+ * The events describing this user.
+ * @privateRemarks
+ * Should be read-only
+ */
+ _defineProperty(this, "events", {});
+ this.displayName = userId;
+ this.rawDisplayName = userId;
+ this.updateModifiedTime();
+ }
+
+ /**
+ * Update this User with the given presence event. May fire "User.presence",
+ * "User.avatarUrl" and/or "User.displayName" if this event updates this user's
+ * properties.
+ * @param event - The `m.presence` event.
+ *
+ * @remarks
+ * Fires {@link UserEvent.Presence}
+ * Fires {@link UserEvent.DisplayName}
+ * Fires {@link UserEvent.AvatarUrl}
+ */
+ setPresenceEvent(event) {
+ if (event.getType() !== "m.presence") {
+ return;
+ }
+ const firstFire = this.events.presence === null;
+ this.events.presence = event;
+ const eventsToFire = [];
+ if (event.getContent().presence !== this.presence || firstFire) {
+ eventsToFire.push(UserEvent.Presence);
+ }
+ if (event.getContent().avatar_url && event.getContent().avatar_url !== this.avatarUrl) {
+ eventsToFire.push(UserEvent.AvatarUrl);
+ }
+ if (event.getContent().displayname && event.getContent().displayname !== this.displayName) {
+ eventsToFire.push(UserEvent.DisplayName);
+ }
+ if (event.getContent().currently_active !== undefined && event.getContent().currently_active !== this.currentlyActive) {
+ eventsToFire.push(UserEvent.CurrentlyActive);
+ }
+ this.presence = event.getContent().presence;
+ eventsToFire.push(UserEvent.LastPresenceTs);
+ if (event.getContent().status_msg) {
+ this.presenceStatusMsg = event.getContent().status_msg;
+ }
+ if (event.getContent().displayname) {
+ this.displayName = event.getContent().displayname;
+ }
+ if (event.getContent().avatar_url) {
+ this.avatarUrl = event.getContent().avatar_url;
+ }
+ this.lastActiveAgo = event.getContent().last_active_ago;
+ this.lastPresenceTs = Date.now();
+ this.currentlyActive = event.getContent().currently_active;
+ this.updateModifiedTime();
+ for (const eventToFire of eventsToFire) {
+ this.emit(eventToFire, event, this);
+ }
+ }
+
+ /**
+ * Manually set this user's display name. No event is emitted in response to this
+ * as there is no underlying MatrixEvent to emit with.
+ * @param name - The new display name.
+ */
+ setDisplayName(name) {
+ const oldName = this.displayName;
+ this.displayName = name;
+ if (name !== oldName) {
+ this.updateModifiedTime();
+ }
+ }
+
+ /**
+ * Manually set this user's non-disambiguated display name. No event is emitted
+ * in response to this as there is no underlying MatrixEvent to emit with.
+ * @param name - The new display name.
+ */
+ setRawDisplayName(name) {
+ this.rawDisplayName = name;
+ }
+
+ /**
+ * Manually set this user's avatar URL. No event is emitted in response to this
+ * as there is no underlying MatrixEvent to emit with.
+ * @param url - The new avatar URL.
+ */
+ setAvatarUrl(url) {
+ const oldUrl = this.avatarUrl;
+ this.avatarUrl = url;
+ if (url !== oldUrl) {
+ this.updateModifiedTime();
+ }
+ }
+
+ /**
+ * Update the last modified time to the current time.
+ */
+ updateModifiedTime() {
+ this.modified = Date.now();
+ }
+
+ /**
+ * Get the timestamp when this User was last updated. This timestamp is
+ * updated when this User receives a new Presence event which has updated a
+ * property on this object. It is updated <i>before</i> firing events.
+ * @returns The timestamp
+ */
+ getLastModifiedTime() {
+ return this.modified;
+ }
+
+ /**
+ * Get the absolute timestamp when this User was last known active on the server.
+ * It is *NOT* accurate if this.currentlyActive is true.
+ * @returns The timestamp
+ */
+ getLastActiveTs() {
+ return this.lastPresenceTs - this.lastActiveAgo;
+ }
+}
+exports.User = User; \ No newline at end of file