summaryrefslogtreecommitdiff
path: root/includes/external/matrix/node_modules/matrix-js-sdk/src/models
diff options
context:
space:
mode:
authorRaindropsSys <raindrops@equestria.dev>2023-11-17 23:25:29 +0100
committerRaindropsSys <raindrops@equestria.dev>2023-11-17 23:25:29 +0100
commit953ddd82e48dd206cef5ac94456549aed13b3ad5 (patch)
tree8f003106ee2e7f422e5a22d2ee04d0db302e66c0 /includes/external/matrix/node_modules/matrix-js-sdk/src/models
parent62a9199846b0c07c03218703b33e8385764f42d9 (diff)
downloadpluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.tar.gz
pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.tar.bz2
pluralconnect-953ddd82e48dd206cef5ac94456549aed13b3ad5.zip
Updated 30 files and deleted 2976 files (automated)
Diffstat (limited to 'includes/external/matrix/node_modules/matrix-js-sdk/src/models')
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts258
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts566
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts38
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts209
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts110
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts39
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts906
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts458
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts1631
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts368
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts268
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts312
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts39
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts146
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts368
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts453
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts1081
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts44
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts3487
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts54
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts669
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts114
-rw-r--r--includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts281
23 files changed, 0 insertions, 11899 deletions
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts
deleted file mode 100644
index 27be4b8..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089Branch.ts
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { MatrixClient } from "../client";
-import { IEncryptedFile, RelationType, UNSTABLE_MSC3089_BRANCH } from "../@types/event";
-import { IContent, MatrixEvent } from "./event";
-import { MSC3089TreeSpace } from "./MSC3089TreeSpace";
-import { EventTimeline } from "./event-timeline";
-import { FileType } from "../http-api";
-import type { ISendEventResponse } from "../@types/requests";
-
-/**
- * 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.
- */
-export class MSC3089Branch {
- public constructor(
- private client: MatrixClient,
- public readonly indexEvent: MatrixEvent,
- public readonly directory: MSC3089TreeSpace,
- ) {
- // Nothing to do
- }
-
- /**
- * The file ID.
- */
- public get id(): string {
- const stateKey = this.indexEvent.getStateKey();
- if (!stateKey) {
- throw new Error("State key not found for branch");
- }
- return stateKey;
- }
-
- /**
- * Whether this branch is active/valid.
- */
- public get isActive(): boolean {
- return this.indexEvent.getContent()["active"] === true;
- }
-
- /**
- * Version for the file, one-indexed.
- */
- public get version(): number {
- return this.indexEvent.getContent()["version"] ?? 1;
- }
-
- private get roomId(): string {
- return this.indexEvent.getRoomId()!;
- }
-
- /**
- * Deletes the file from the tree, including all prior edits/versions.
- * @returns Promise which resolves when complete.
- */
- public async delete(): Promise<void> {
- await this.client.sendStateEvent(this.roomId, 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.
- */
- public getName(): string {
- 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.
- */
- public async setName(name: string): Promise<void> {
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- ...this.indexEvent.getContent(),
- name: name,
- },
- this.id,
- );
- }
-
- /**
- * Gets whether or not a file is locked.
- * @returns True if locked, false otherwise.
- */
- public isLocked(): boolean {
- 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.
- */
- public async setLocked(locked: boolean): Promise<void> {
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- ...this.indexEvent.getContent(),
- locked: locked,
- },
- this.id,
- );
- }
-
- /**
- * Gets information about the file needed to download it.
- * @returns Information about the file.
- */
- public async getFileInfo(): Promise<{ info: IEncryptedFile; httpUrl: string }> {
- 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.
- */
- public async getFileEvent(): Promise<MatrixEvent> {
- const room = this.client.getRoom(this.roomId);
- if (!room) throw new Error("Unknown room");
-
- let event: MatrixEvent | undefined = 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.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.
- */
- public async createNewVersion(
- name: string,
- encryptedContents: FileType,
- info: Partial<IEncryptedFile>,
- additionalContent?: IContent,
- ): Promise<ISendEventResponse> {
- const fileEventResponse = await this.directory.createFile(name, encryptedContents, info, {
- ...(additionalContent ?? {}),
- "m.new_content": true,
- "m.relates_to": {
- rel_type: RelationType.Replace,
- event_id: this.id,
- },
- });
-
- // Update the version of the new event
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- active: true,
- name: name,
- version: this.version + 1,
- },
- fileEventResponse["event_id"],
- );
-
- // Deprecate ourselves
- await this.client.sendStateEvent(
- this.roomId,
- UNSTABLE_MSC3089_BRANCH.name,
- {
- ...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.
- */
- public async getVersionHistory(): Promise<MSC3089Branch[]> {
- const fileHistory: MSC3089Branch[] = [];
- 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: MatrixEvent | undefined;
- 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;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts
deleted file mode 100644
index b0e71d9..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/MSC3089TreeSpace.ts
+++ /dev/null
@@ -1,566 +0,0 @@
-/*
-Copyright 2021 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import promiseRetry from "p-retry";
-
-import { MatrixClient } from "../client";
-import { EventType, IEncryptedFile, MsgType, UNSTABLE_MSC3089_BRANCH, UNSTABLE_MSC3089_LEAF } from "../@types/event";
-import { Room } from "./room";
-import { logger } from "../logger";
-import { IContent, MatrixEvent } from "./event";
-import {
- averageBetweenStrings,
- DEFAULT_ALPHABET,
- lexicographicCompare,
- nextString,
- prevString,
- simpleRetryOperation,
-} from "../utils";
-import { MSC3089Branch } from "./MSC3089Branch";
-import { isRoomSharedHistory } from "../crypto/algorithms/megolm";
-import { ISendEventResponse } from "../@types/requests";
-import { FileType } from "../http-api";
-
-/**
- * The recommended defaults for a tree space's power levels. Note that this
- * is UNSTABLE and subject to breaking changes without notice.
- */
-export 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: {
- [EventType.RoomPowerLevels]: 100,
- [EventType.RoomHistoryVisibility]: 100,
- [EventType.RoomTombstone]: 100,
- [EventType.RoomEncryption]: 100,
- [EventType.RoomName]: 50,
- [EventType.RoomMessage]: 50,
- [EventType.RoomMessageEncrypted]: 50,
- [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.
- */
-export enum TreePermissions {
- Viewer = "viewer", // Default
- Editor = "editor", // "Moderator" or ~PL50
- Owner = "owner", // "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.
- */
-export class MSC3089TreeSpace {
- public readonly room: Room;
-
- public constructor(private client: MatrixClient, public readonly roomId: string) {
- this.room = this.client.getRoom(this.roomId)!;
-
- if (!this.room) throw new Error("Unknown room");
- }
-
- /**
- * Syntactic sugar for room ID of the Space.
- */
- public get id(): string {
- return this.roomId;
- }
-
- /**
- * Whether or not this is a top level space.
- */
- public get isTopLevel(): boolean {
- // 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(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.
- */
- public async setName(name: string): Promise<void> {
- await this.client.sendStateEvent(this.roomId, 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.
- */
- public async invite(userId: string, andSubspaces = true, shareHistoryKeys = true): Promise<void> {
- const promises: Promise<void>[] = [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 && isRoomSharedHistory(this.room)) {
- // noinspection JSIgnoredPromiseFromCall - we aren't concerned as much if this fails.
- this.client.sendSharedHistoryKeys(this.roomId, [userId]);
- }
- });
- }
-
- private retryInvite(userId: string): Promise<void> {
- return 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 promiseRetry.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.
- */
- public async setPermissions(userId: string, role: TreePermissions): Promise<void> {
- const currentPls = this.room.currentState.getStateEvents(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"]?.[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, 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.
- */
- public getPermissions(userId: string): TreePermissions {
- const currentPls = this.room.currentState.getStateEvents(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"]?.[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.
- */
- public async createDirectory(name: string): Promise<MSC3089TreeSpace> {
- const directory = await this.client.unstableCreateFileTree(name);
-
- await this.client.sendStateEvent(
- this.roomId,
- EventType.SpaceChild,
- {
- via: [this.client.getDomain()],
- },
- directory.roomId,
- );
-
- await this.client.sendStateEvent(
- directory.roomId,
- 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.
- */
- public getDirectories(): MSC3089TreeSpace[] {
- const trees: MSC3089TreeSpace[] = [];
- const children = this.room.currentState.getStateEvents(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.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.
- */
- public getDirectory(roomId: string): MSC3089TreeSpace | undefined {
- return this.getDirectories().find((r) => r.roomId === roomId);
- }
-
- /**
- * Deletes the tree, kicking all members and deleting **all subdirectories**.
- * @returns Promise which resolves when complete.
- */
- public async delete(): Promise<void> {
- const subdirectories = this.getDirectories();
- for (const dir of subdirectories) {
- await dir.delete();
- }
-
- const kickMemberships = ["invite", "knock", "join"];
- const members = this.room.currentState.getStateEvents(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);
- }
-
- private getOrderedChildren(children: MatrixEvent[]): { roomId: string; order: string }[] {
- const ordered: { roomId: string; order: string }[] = children
- .map((c) => ({ roomId: c.getStateKey(), order: c.getContent()["order"] }))
- .filter((c) => c.roomId) as { roomId: string; order: string }[];
- 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 lexicographicCompare(a.roomId, b.roomId);
- }
-
- const createTsA = roomA.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0;
- const createTsB = roomB.currentState.getStateEvents(EventType.RoomCreate, "")?.getTs() ?? 0;
- if (createTsA === createTsB) {
- return lexicographicCompare(a.roomId, b.roomId);
- }
- return createTsA - createTsB;
- } else {
- // both not-null orders
- return lexicographicCompare(a.order, b.order);
- }
- });
- return ordered;
- }
-
- private getParentRoom(): Room {
- const parents = this.room.currentState.getStateEvents(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.
- */
- public getOrder(): number {
- if (this.isTopLevel) return -1;
-
- const parentRoom = this.getParentRoom();
- const children = parentRoom.currentState.getStateEvents(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.
- */
- public async setOrder(index: number): Promise<void> {
- if (this.isTopLevel) throw new Error("Cannot set order of top level spaces currently");
-
- const parentRoom = this.getParentRoom();
- const children = parentRoom.currentState.getStateEvents(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 = DEFAULT_ALPHABET[0];
- let ensureBeforeIsSane = false;
- if (!prev) {
- // Move to front
- if (next?.order) {
- newOrder = prevString(next.order);
- }
- } else if (index === ordered.length - 1) {
- // Move to back
- if (next?.order) {
- newOrder = 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 = nextString(startOrder);
- } else {
- newOrder = averageBetweenStrings(startOrder, endOrder);
- }
- } else {
- if (startOrder) {
- // We're at the end (endOrder is null, so no explicit order)
- newOrder = nextString(startOrder);
- } else if (endOrder) {
- // We're at the start (startOrder is null, so nothing before us)
- newOrder = 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: string | undefined;
- 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 ? nextString(lastOrder) : DEFAULT_ALPHABET[0];
- const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, target.roomId);
- const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] };
- await this.client.sendStateEvent(
- parentRoom.roomId,
- EventType.SpaceChild,
- {
- ...content,
- order: lastOrder,
- },
- target.roomId,
- );
- } else {
- lastOrder = target.order;
- }
- }
- if (lastOrder) {
- newOrder = nextString(lastOrder);
- }
- }
-
- // TODO: Deal with order conflicts by reordering
-
- // Now we can finally update our own order state
- const currentChild = parentRoom.currentState.getStateEvents(EventType.SpaceChild, this.roomId);
- const content = currentChild?.getContent() ?? { via: [this.client.getDomain()] };
- await this.client.sendStateEvent(
- parentRoom.roomId,
- EventType.SpaceChild,
- {
- ...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.
- */
- public async createFile(
- name: string,
- encryptedContents: FileType,
- info: Partial<IEncryptedFile>,
- additionalContent?: IContent,
- ): Promise<ISendEventResponse> {
- const { content_uri: mxc } = await this.client.uploadContent(encryptedContents, {
- includeFilename: false,
- });
- info.url = mxc;
-
- const fileContent = {
- msgtype: 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, {
- ...additionalContent,
- ...fileContent,
- [UNSTABLE_MSC3089_LEAF.name]: {},
- });
-
- await this.client.sendStateEvent(
- this.roomId,
- 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.
- */
- public getFile(fileEventId: string): MSC3089Branch | null {
- const branch = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name, fileEventId);
- return branch ? new 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.
- */
- public listFiles(): MSC3089Branch[] {
- 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.
- */
- public listAllFiles(): MSC3089Branch[] {
- const branches = this.room.currentState.getStateEvents(UNSTABLE_MSC3089_BRANCH.name) ?? [];
- return branches.map((e) => new MSC3089Branch(this.client, e, this));
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts
deleted file mode 100644
index 8efc3ed..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/ToDeviceMessage.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
-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.
-*/
-
-export type ToDevicePayload = Record<string, any>;
-
-export interface ToDeviceMessage {
- userId: string;
- deviceId: string;
- payload: ToDevicePayload;
-}
-
-export interface ToDeviceBatch {
- eventType: string;
- batch: ToDeviceMessage[];
-}
-
-// Only used internally
-export interface ToDeviceBatchWithTxnId extends ToDeviceBatch {
- txnId: string;
-}
-
-// Only used internally
-export interface IndexedToDeviceBatch extends ToDeviceBatchWithTxnId {
- id: number;
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts
deleted file mode 100644
index 3801831..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/beacon.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { MBeaconEventContent } from "../@types/beacon";
-import { BeaconInfoState, BeaconLocationState, parseBeaconContent, parseBeaconInfoContent } from "../content-helpers";
-import { MatrixEvent } from "./event";
-import { sortEventsByLatestContentTimestamp } from "../utils";
-import { TypedEventEmitter } from "./typed-event-emitter";
-
-export enum BeaconEvent {
- New = "Beacon.new",
- Update = "Beacon.update",
- LivenessChange = "Beacon.LivenessChange",
- Destroy = "Beacon.Destroy",
- LocationUpdate = "Beacon.LocationUpdate",
-}
-
-export type BeaconEventHandlerMap = {
- [BeaconEvent.Update]: (event: MatrixEvent, beacon: Beacon) => void;
- [BeaconEvent.LivenessChange]: (isLive: boolean, beacon: Beacon) => void;
- [BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
- [BeaconEvent.LocationUpdate]: (locationState: BeaconLocationState) => void;
- [BeaconEvent.Destroy]: (beaconIdentifier: string) => void;
-};
-
-export const isTimestampInDuration = (startTimestamp: number, durationMs: number, timestamp: number): boolean =>
- timestamp >= startTimestamp && startTimestamp + durationMs >= timestamp;
-
-// beacon info events are uniquely identified by
-// `<roomId>_<state_key>`
-export type BeaconIdentifier = string;
-export const getBeaconInfoIdentifier = (event: MatrixEvent): BeaconIdentifier =>
- `${event.getRoomId()}_${event.getStateKey()}`;
-
-// https://github.com/matrix-org/matrix-spec-proposals/pull/3672
-export class Beacon extends TypedEventEmitter<Exclude<BeaconEvent, BeaconEvent.New>, BeaconEventHandlerMap> {
- public readonly roomId: string;
- // beaconInfo is assigned by setBeaconInfo in the constructor
- // ! to make tsc believe it is definitely assigned
- private _beaconInfo!: BeaconInfoState;
- private _isLive?: boolean;
- private livenessWatchTimeout?: ReturnType<typeof setTimeout>;
- private _latestLocationEvent?: MatrixEvent;
-
- public constructor(private rootEvent: MatrixEvent) {
- super();
- this.roomId = this.rootEvent.getRoomId()!;
- this.setBeaconInfo(this.rootEvent);
- }
-
- public get isLive(): boolean {
- return !!this._isLive;
- }
-
- public get identifier(): BeaconIdentifier {
- return getBeaconInfoIdentifier(this.rootEvent);
- }
-
- public get beaconInfoId(): string {
- return this.rootEvent.getId()!;
- }
-
- public get beaconInfoOwner(): string {
- return this.rootEvent.getStateKey()!;
- }
-
- public get beaconInfoEventType(): string {
- return this.rootEvent.getType();
- }
-
- public get beaconInfo(): BeaconInfoState {
- return this._beaconInfo;
- }
-
- public get latestLocationState(): BeaconLocationState | undefined {
- return this._latestLocationEvent && parseBeaconContent(this._latestLocationEvent.getContent());
- }
-
- public get latestLocationEvent(): MatrixEvent | undefined {
- return this._latestLocationEvent;
- }
-
- public update(beaconInfoEvent: MatrixEvent): void {
- 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();
- }
-
- public destroy(): void {
- 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
- */
- public monitorLiveness(): void {
- 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
- */
- public addLocations(beaconLocationEvents: MatrixEvent[]): void {
- // discard locations for beacons that are not live
- if (!this.isLive) {
- return;
- }
-
- const validLocationEvents = beaconLocationEvents.filter((event) => {
- const content = event.getContent<MBeaconEventContent>();
- const parsed = 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(sortEventsByLatestContentTimestamp)?.[0];
-
- if (latestLocationEvent) {
- this._latestLocationEvent = latestLocationEvent;
- this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!);
- }
- }
-
- private clearLatestLocation = (): void => {
- this._latestLocationEvent = undefined;
- this.emit(BeaconEvent.LocationUpdate, this.latestLocationState!);
- };
-
- private setBeaconInfo(event: MatrixEvent): void {
- this._beaconInfo = parseBeaconInfoContent(event.getContent());
- this.checkLiveness();
- }
-
- private checkLiveness(): void {
- 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);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts
deleted file mode 100644
index 0401cd5..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-context.ts
+++ /dev/null
@@ -1,110 +0,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.
-*/
-
-import { MatrixEvent } from "./event";
-import { Direction } from "./event-timeline";
-
-export class EventContext {
- private timeline: MatrixEvent[];
- private ourEventIndex = 0;
- private paginateTokens: Record<Direction, string | null> = {
- [Direction.Backward]: null,
- [Direction.Forward]: null,
- };
-
- /**
- * 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
- */
- public constructor(public readonly ourEvent: MatrixEvent) {
- 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.
- */
- public getEvent(): MatrixEvent {
- return this.timeline[this.ourEventIndex];
- }
-
- /**
- * Get the list of events in this context
- *
- * @returns An array of MatrixEvents
- */
- public getTimeline(): MatrixEvent[] {
- return this.timeline;
- }
-
- /**
- * Get the index in the timeline of our event
- */
- public getOurEventIndex(): number {
- return this.ourEventIndex;
- }
-
- /**
- * Get a pagination token.
- *
- * @param backwards - true to get the pagination token for going
- */
- public getPaginateToken(backwards = false): string | null {
- return this.paginateTokens[backwards ? Direction.Backward : 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
- */
- public setPaginateToken(token?: string, backwards = false): void {
- this.paginateTokens[backwards ? Direction.Backward : 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
- */
- public addEvents(events: MatrixEvent[], atStart = false): void {
- // 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);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts
deleted file mode 100644
index a5113e0..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-status.ts
+++ /dev/null
@@ -1,39 +0,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
- */
-export enum EventStatus {
- /** The event was not sent and will no longer be retried. */
- NOT_SENT = "not_sent",
-
- /** The message is being encrypted */
- ENCRYPTING = "encrypting",
-
- /** The event is in the process of being sent. */
- SENDING = "sending",
-
- /** The event is in a queue waiting to be sent. */
- QUEUED = "queued",
-
- /** The event has been sent to the server, but we have not yet received the echo. */
- SENT = "sent",
-
- /** The event was cancelled before it was successfully sent. */
- CANCELLED = "cancelled",
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts
deleted file mode 100644
index 5cb0499..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline-set.ts
+++ /dev/null
@@ -1,906 +0,0 @@
-/*
-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.
-*/
-
-import { EventTimeline, IAddEventOptions } from "./event-timeline";
-import { MatrixEvent } from "./event";
-import { logger } from "../logger";
-import { Room, RoomEvent } from "./room";
-import { Filter } from "../filter";
-import { RoomState } from "./room-state";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { RelationsContainer } from "./relations-container";
-import { MatrixClient } from "../client";
-import { Thread, ThreadFilterType } from "./thread";
-
-const DEBUG = true;
-
-/* istanbul ignore next */
-let debuglog: (...args: any[]) => void;
-if (DEBUG) {
- // using bind means that we get to keep useful line numbers in the console
- debuglog = logger.log.bind(logger);
-} else {
- /* istanbul ignore next */
- debuglog = function (): void {};
-}
-
-interface IOpts {
- // Set to true to enable improved timeline support.
- timelineSupport?: boolean;
- // The filter object, if any, for this timelineSet.
- filter?: Filter;
- pendingEvents?: boolean;
-}
-
-export enum DuplicateStrategy {
- Ignore = "ignore",
- Replace = "replace",
-}
-
-export interface IRoomTimelineData {
- // the timeline the event was added to/removed from
- timeline: EventTimeline;
- // true if the event was a real-time event added to the end of the live timeline
- liveEvent?: boolean;
-}
-
-export interface IAddEventToTimelineOptions
- extends Pick<IAddEventOptions, "toStartOfTimeline" | "roomState" | "timelineWasEmpty"> {
- /** Whether the sync response came from cache */
- fromCache?: boolean;
-}
-
-export interface IAddLiveEventOptions
- extends Pick<IAddEventToTimelineOptions, "fromCache" | "roomState" | "timelineWasEmpty"> {
- /** Applies to events in the timeline only. If this is 'replace' then if a
- * duplicate is encountered, the event passed to this function will replace
- * the existing event in the timeline. If this is not specified, or is
- * 'ignore', then the event passed to this function will be ignored
- * entirely, preserving the existing event in the timeline. Events are
- * identical based on their event ID <b>only</b>. */
- duplicateStrategy?: DuplicateStrategy;
-}
-
-type EmittedEvents = RoomEvent.Timeline | RoomEvent.TimelineReset;
-
-export type EventTimelineSetHandlerMap = {
- /**
- * Fires whenever the timeline in a room is updated.
- * @param event - The matrix event which caused this event to fire.
- * @param room - The room, if any, whose timeline was updated.
- * @param toStartOfTimeline - True if this event was added to the start
- * @param removed - True if this event has just been removed from the timeline
- * (beginning; oldest) of the timeline e.g. due to pagination.
- *
- * @param data - more data about the event
- *
- * @example
- * ```
- * matrixClient.on("Room.timeline",
- * function(event, room, toStartOfTimeline, removed, data) {
- * if (!toStartOfTimeline && data.liveEvent) {
- * var messageToAppend = room.timeline.[room.timeline.length - 1];
- * }
- * });
- * ```
- */
- [RoomEvent.Timeline]: (
- event: MatrixEvent,
- room: Room | undefined,
- toStartOfTimeline: boolean | undefined,
- removed: boolean,
- data: IRoomTimelineData,
- ) => void;
- /**
- * Fires whenever the live timeline in a room is reset.
- *
- * When we get a 'limited' sync (for example, after a network outage), we reset
- * the live timeline to be empty before adding the recent events to the new
- * timeline. This event is fired after the timeline is reset, and before the
- * new events are added.
- *
- * @param room - The room whose live timeline was reset, if any
- * @param timelineSet - timelineSet room whose live timeline was reset
- * @param resetAllTimelines - True if all timelines were reset.
- */
- [RoomEvent.TimelineReset]: (
- room: Room | undefined,
- eventTimelineSet: EventTimelineSet,
- resetAllTimelines: boolean,
- ) => void;
-};
-
-export class EventTimelineSet extends TypedEventEmitter<EmittedEvents, EventTimelineSetHandlerMap> {
- public readonly relations: RelationsContainer;
- private readonly timelineSupport: boolean;
- private readonly displayPendingEvents: boolean;
- private liveTimeline: EventTimeline;
- private timelines: EventTimeline[];
- private _eventIdToTimeline = new Map<string, EventTimeline>();
- private filter?: Filter;
-
- /**
- * 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)
- */
- public constructor(
- public readonly room: Room | undefined,
- opts: IOpts = {},
- client?: MatrixClient,
- public readonly thread?: Thread,
- public readonly threadListType: ThreadFilterType | null = null,
- ) {
- super();
-
- this.timelineSupport = Boolean(opts.timelineSupport);
- this.liveTimeline = new EventTimeline(this);
- this.displayPendingEvents = opts.pendingEvents !== false;
-
- // just a list - *not* ordered.
- this.timelines = [this.liveTimeline];
- this._eventIdToTimeline = new Map<string, EventTimeline>();
-
- this.filter = opts.filter;
-
- this.relations = this.room?.relations ?? new RelationsContainer(room?.client ?? client!);
- }
-
- /**
- * Get all the timelines in this set
- * @returns the timelines in this set
- */
- public getTimelines(): EventTimeline[] {
- return this.timelines;
- }
-
- /**
- * Get the filter object this timeline set is filtered on, if any
- * @returns the optional filter for this timelineSet
- */
- public getFilter(): Filter | undefined {
- 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
- */
- public setFilter(filter?: Filter): void {
- 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'
- */
- public getPendingEvents(): MatrixEvent[] {
- if (!this.room || !this.displayPendingEvents) {
- return [];
- }
-
- return this.room.getPendingEvents();
- }
- /**
- * Get the live timeline for this room.
- *
- * @returns live timeline
- */
- public getLiveTimeline(): EventTimeline {
- return this.liveTimeline;
- }
-
- /**
- * Set the live timeline for this room.
- *
- * @returns live timeline
- */
- public setLiveTimeline(timeline: EventTimeline): void {
- this.liveTimeline = timeline;
- }
-
- /**
- * Return the timeline (if any) this event is in.
- * @param eventId - the eventId being sought
- * @returns timeline
- */
- public eventIdToTimeline(eventId: string): EventTimeline | undefined {
- 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
- */
- public replaceEventId(oldEventId: string, newEventId: string): void {
- 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}
- */
- public resetLiveTimeline(backPaginationToken?: string, forwardPaginationToken?: string): void {
- // 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.FORWARDS)
- : oldTimeline.fork(EventTimeline.FORWARDS);
-
- if (resetAllTimelines) {
- this.timelines = [newTimeline];
- this._eventIdToTimeline = new Map<string, EventTimeline>();
- } 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.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.BACKWARDS);
-
- // Now we can swap the live timeline to the new one.
- this.liveTimeline = newTimeline;
- this.emit(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
- */
- public getTimelineForEvent(eventId?: string): EventTimeline | null {
- 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
- */
- public findEventById(eventId: string): MatrixEvent | undefined {
- 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
- */
- public addTimeline(): EventTimeline {
- 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(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}
- *
- */
- public addEventsToTimeline(
- events: MatrixEvent[],
- toStartOfTimeline: boolean,
- timeline: EventTimeline,
- paginationToken?: string | null,
- ): void {
- 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.BACKWARDS : EventTimeline.FORWARDS;
- const inverseDirection = toStartOfTimeline ? EventTimeline.FORWARDS : 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.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.BACKWARDS && existingIsLive;
- const forwardsIsLive = direction === 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.warn(
- "Refusing to set a preceding existingTimeLine on our " +
- "timeline as the existingTimeLine is live (" +
- existingTimeline +
- ")",
- );
- }
- if (forwardsIsLive) {
- 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.FORWARDS && timeline === this.liveTimeline) {
- logger.warn({ lastEventWasNew, didUpdate }); // for debugging
- 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
- */
- public addLiveEvent(
- event: MatrixEvent,
- { duplicateStrategy, fromCache, roomState, timelineWasEmpty }: IAddLiveEventOptions,
- ): void;
- /**
- * @deprecated In favor of the overload with `IAddLiveEventOptions`
- */
- public addLiveEvent(
- event: MatrixEvent,
- duplicateStrategy?: DuplicateStrategy,
- fromCache?: boolean,
- roomState?: RoomState,
- ): void;
- public addLiveEvent(
- event: MatrixEvent,
- duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
- fromCache = false,
- roomState?: RoomState,
- ): void {
- let duplicateStrategy = (duplicateStrategyOrOpts as DuplicateStrategy) || DuplicateStrategy.Ignore;
- let timelineWasEmpty: boolean | undefined;
- 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.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.FORWARDS);
- }
- 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}
- */
- public addEventToTimeline(
- event: MatrixEvent,
- timeline: EventTimeline,
- { toStartOfTimeline, fromCache, roomState, timelineWasEmpty }: IAddEventToTimelineOptions,
- ): void;
- /**
- * @deprecated In favor of the overload with `IAddEventToTimelineOptions`
- */
- public addEventToTimeline(
- event: MatrixEvent,
- timeline: EventTimeline,
- toStartOfTimeline: boolean,
- fromCache?: boolean,
- roomState?: RoomState,
- ): void;
- public addEventToTimeline(
- event: MatrixEvent,
- timeline: EventTimeline,
- toStartOfTimelineOrOpts: boolean | IAddEventToTimelineOptions,
- fromCache = false,
- roomState?: RoomState,
- ): void {
- let toStartOfTimeline = !!toStartOfTimelineOrOpts;
- let timelineWasEmpty: boolean | undefined;
- 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.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.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: IRoomTimelineData = {
- timeline: timeline,
- liveEvent: !toStartOfTimeline && timeline == this.liveTimeline && !fromCache,
- };
- this.emit(RoomEvent.Timeline, event, this.room, Boolean(toStartOfTimeline), 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}
- */
- public handleRemoteEcho(localEvent: MatrixEvent, oldEventId: string, newEventId: string): void {
- // 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.
- */
- public removeEvent(eventId: string): MatrixEvent | null {
- 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(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).
- */
- public compareEventOrdering(eventId1: string, eventId2: string): number | null {
- 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: number | undefined = undefined;
- let idx2: number | undefined = 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: EventTimeline | null = timeline1;
- while (tl) {
- if (tl === timeline2) {
- // timeline1 is before timeline2
- return -1;
- }
- tl = tl.getNeighbouringTimeline(EventTimeline.FORWARDS);
- }
-
- // now try backwards from timeline1
- tl = timeline1;
- while (tl) {
- if (tl === timeline2) {
- // timeline2 is before timeline1
- return 1;
- }
- tl = tl.getNeighbouringTimeline(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.
- */
- public canContain(event: MatrixEvent): boolean {
- 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;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts
deleted file mode 100644
index d1ba321..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event-timeline.ts
+++ /dev/null
@@ -1,458 +0,0 @@
-/*
-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.
-*/
-
-import { logger } from "../logger";
-import { IMarkerFoundOptions, RoomState } from "./room-state";
-import { EventTimelineSet } from "./event-timeline-set";
-import { MatrixEvent } from "./event";
-import { Filter } from "../filter";
-import { EventType } from "../@types/event";
-
-export interface IInitialiseStateOptions extends Pick<IMarkerFoundOptions, "timelineWasEmpty"> {
- // This is a separate interface without any extra stuff currently added on
- // top of `IMarkerFoundOptions` just because it feels like they have
- // different concerns. One shouldn't necessarily look to add to
- // `IMarkerFoundOptions` just because they want to add an extra option to
- // `initialiseState`.
-}
-
-export interface IAddEventOptions extends Pick<IMarkerFoundOptions, "timelineWasEmpty"> {
- /** Whether to insert the new event at the start of the timeline where the
- * oldest events are (timeline is in chronological order, oldest to most
- * recent) */
- toStartOfTimeline: boolean;
- /** The state events to reconcile metadata from */
- roomState?: RoomState;
-}
-
-export enum Direction {
- Backward = "b",
- Forward = "f",
-}
-
-export class EventTimeline {
- /**
- * Symbolic constant for methods which take a 'direction' argument:
- * refers to the start of the timeline, or backwards in time.
- */
- public static readonly BACKWARDS = Direction.Backward;
-
- /**
- * Symbolic constant for methods which take a 'direction' argument:
- * refers to the end of the timeline, or forwards in time.
- */
- public static readonly FORWARDS = Direction.Forward;
-
- /**
- * 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
- */
- public static setEventMetadata(event: MatrixEvent, stateContext: RoomState, toStartOfTimeline: boolean): void {
- // 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() === 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;
- }
- }
- }
-
- private readonly roomId: string | null;
- private readonly name: string;
- private events: MatrixEvent[] = [];
- private baseIndex = 0;
-
- private startState?: RoomState;
- private endState?: RoomState;
- // 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.
- private startToken: string | null = null;
- private endToken: string | null = null;
-
- private prevTimeline: EventTimeline | null = null;
- private nextTimeline: EventTimeline | null = null;
- public paginationRequests: Record<Direction, Promise<boolean> | null> = {
- [Direction.Backward]: null,
- [Direction.Forward]: null,
- };
-
- /**
- * 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
- */
- public constructor(private readonly eventTimelineSet: EventTimelineSet) {
- this.roomId = eventTimelineSet.room?.roomId ?? null;
- if (this.roomId) {
- this.startState = new RoomState(this.roomId);
- this.endState = new 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.
- */
- public initialiseState(stateEvents: MatrixEvent[], { timelineWasEmpty }: IInitialiseStateOptions = {}): void {
- 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
- */
- public forkLive(direction: Direction): EventTimeline {
- 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
- */
- public fork(direction: Direction): EventTimeline {
- 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
- */
- public getRoomId(): string | null {
- return this.roomId;
- }
-
- /**
- * Get the filter for this timeline's timelineSet (if any)
- * @returns filter
- */
- public getFilter(): Filter | undefined {
- return this.eventTimelineSet.getFilter();
- }
-
- /**
- * Get the timelineSet for this timeline
- * @returns timelineSet
- */
- public getTimelineSet(): EventTimelineSet {
- 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).
- */
- public getBaseIndex(): number {
- return this.baseIndex;
- }
-
- /**
- * Get the list of events in this context
- *
- * @returns An array of MatrixEvents
- */
- public getEvents(): MatrixEvent[] {
- 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
- */
- public getState(direction: Direction): RoomState | undefined {
- 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
- */
- public getPaginationToken(direction: Direction): string | null {
- 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.
- */
- public setPaginationToken(token: string | null, direction: Direction): void {
- 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.
- */
- public getNeighbouringTimeline(direction: Direction): EventTimeline | null {
- 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.
- */
- public setNeighbouringTimeline(neighbour: EventTimeline, direction: Direction): void {
- 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
- */
- public addEvent(event: MatrixEvent, { toStartOfTimeline, roomState, timelineWasEmpty }: IAddEventOptions): void;
- /**
- * @deprecated In favor of the overload with `IAddEventOptions`
- */
- public addEvent(event: MatrixEvent, toStartOfTimeline: boolean, roomState?: RoomState): void;
- public addEvent(
- event: MatrixEvent,
- toStartOfTimelineOrOpts: boolean | IAddEventOptions,
- roomState?: RoomState,
- ): void {
- let toStartOfTimeline = !!toStartOfTimelineOrOpts;
- let timelineWasEmpty: boolean | undefined;
- if (typeof toStartOfTimelineOrOpts === "object") {
- ({ toStartOfTimeline, roomState, timelineWasEmpty } = toStartOfTimelineOrOpts);
- } else if (toStartOfTimelineOrOpts !== undefined) {
- // Deprecation warning
- // FIXME: Remove after 2023-06-01 (technical debt)
- 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() === EventType.RoomMember && !toStartOfTimeline)) {
- EventTimeline.setEventMetadata(event, roomState!, toStartOfTimeline);
- }
- }
- }
-
- let insertIndex: number;
-
- if (toStartOfTimeline) {
- insertIndex = 0;
- } else {
- insertIndex = this.events.length;
- }
-
- this.events.splice(insertIndex, 0, event); // insert element
- if (toStartOfTimeline) {
- this.baseIndex++;
- }
- }
-
- /**
- * Remove an event from the timeline
- *
- * @param eventId - ID of event to be removed
- * @returns removed event, or null if not found
- */
- public removeEvent(eventId: string): MatrixEvent | null {
- 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
- */
- public toString(): string {
- return this.name;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts
deleted file mode 100644
index 2db3479..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/event.ts
+++ /dev/null
@@ -1,1631 +0,0 @@
-/*
-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.
- */
-
-import { ExtensibleEvent, ExtensibleEvents, Optional } from "matrix-events-sdk";
-
-import type { IEventDecryptionResult } from "../@types/crypto";
-import { logger } from "../logger";
-import { VerificationRequest } from "../crypto/verification/request/VerificationRequest";
-import { EVENT_VISIBILITY_CHANGE_TYPE, EventType, MsgType, RelationType } from "../@types/event";
-import { Crypto } from "../crypto";
-import { deepSortedObjectEntries, internaliseString } from "../utils";
-import { RoomMember } from "./room-member";
-import { Thread, ThreadEvent, EventHandlerMap as ThreadEventHandlerMap, THREAD_RELATION_TYPE } from "./thread";
-import { IActionsObject } from "../pushprocessor";
-import { TypedReEmitter } from "../ReEmitter";
-import { MatrixError } from "../http-api";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { EventStatus } from "./event-status";
-import { DecryptionError } from "../crypto/algorithms";
-import { CryptoBackend } from "../common-crypto/CryptoBackend";
-import { WITHHELD_MESSAGES } from "../crypto/OlmDevice";
-
-export { EventStatus } from "./event-status";
-
-/* eslint-disable camelcase */
-export interface IContent {
- [key: string]: any;
- "msgtype"?: MsgType | string;
- "membership"?: string;
- "avatar_url"?: string;
- "displayname"?: string;
- "m.relates_to"?: IEventRelation;
-
- "org.matrix.msc3952.mentions"?: IMentions;
-}
-
-type StrippedState = Required<Pick<IEvent, "content" | "state_key" | "type" | "sender">>;
-
-export interface IUnsigned {
- "age"?: number;
- "prev_sender"?: string;
- "prev_content"?: IContent;
- "redacted_because"?: IEvent;
- "transaction_id"?: string;
- "invite_room_state"?: StrippedState[];
- "m.relations"?: Record<RelationType | string, any>; // No common pattern for aggregated relations
-}
-
-export interface IThreadBundledRelationship {
- latest_event: IEvent;
- count: number;
- current_user_participated?: boolean;
-}
-
-export interface IEvent {
- event_id: string;
- type: string;
- content: IContent;
- sender: string;
- room_id?: string;
- origin_server_ts: number;
- txn_id?: string;
- state_key?: string;
- membership?: string;
- unsigned: IUnsigned;
- redacts?: string;
-
- /**
- * @deprecated in favour of `sender`
- */
- user_id?: string;
- /**
- * @deprecated in favour of `unsigned.prev_content`
- */
- prev_content?: IContent;
- /**
- * @deprecated in favour of `origin_server_ts`
- */
- age?: number;
-}
-
-export interface IAggregatedRelation {
- origin_server_ts: number;
- event_id?: string;
- sender?: string;
- type?: string;
- count?: number;
- key?: string;
-}
-
-export interface IEventRelation {
- "rel_type"?: RelationType | string;
- "event_id"?: string;
- "is_falling_back"?: boolean;
- "m.in_reply_to"?: {
- event_id?: string;
- };
- "key"?: string;
-}
-
-export interface IMentions {
- user_ids?: string[];
- room?: boolean;
-}
-
-/**
- * When an event is a visibility change event, as per MSC3531,
- * the visibility change implied by the event.
- */
-export interface IVisibilityChange {
- /**
- * If `true`, the target event should be made visible.
- * Otherwise, it should be hidden.
- */
- visible: boolean;
-
- /**
- * The event id affected.
- */
- eventId: string;
-
- /**
- * Optionally, a human-readable reason explaining why
- * the event was hidden. Ignored if the event was made
- * visible.
- */
- reason: string | null;
-}
-
-export interface IClearEvent {
- room_id?: string;
- type: string;
- content: Omit<IContent, "membership" | "avatar_url" | "displayname" | "m.relates_to">;
- unsigned?: IUnsigned;
-}
-/* eslint-enable camelcase */
-
-interface IKeyRequestRecipient {
- userId: string;
- deviceId: "*" | string;
-}
-
-export interface IDecryptOptions {
- // Emits "event.decrypted" if set to true
- emit?: boolean;
- // True if this is a retry (enables more logging)
- isRetry?: boolean;
- // whether the message should be re-decrypted if it was previously successfully decrypted with an untrusted key
- forceRedecryptIfUntrusted?: boolean;
-}
-
-/**
- * Message hiding, as specified by https://github.com/matrix-org/matrix-doc/pull/3531.
- */
-export type MessageVisibility = IMessageVisibilityHidden | IMessageVisibilityVisible;
-/**
- * Variant of `MessageVisibility` for the case in which the message should be displayed.
- */
-export interface IMessageVisibilityVisible {
- readonly visible: true;
-}
-/**
- * Variant of `MessageVisibility` for the case in which the message should be hidden.
- */
-export interface IMessageVisibilityHidden {
- readonly visible: false;
- /**
- * Optionally, a human-readable reason to show to the user indicating why the
- * message has been hidden (e.g. "Message Pending Moderation").
- */
- readonly reason: string | null;
-}
-// A singleton implementing `IMessageVisibilityVisible`.
-const MESSAGE_VISIBLE: IMessageVisibilityVisible = Object.freeze({ visible: true });
-
-export enum MatrixEventEvent {
- Decrypted = "Event.decrypted",
- BeforeRedaction = "Event.beforeRedaction",
- VisibilityChange = "Event.visibilityChange",
- LocalEventIdReplaced = "Event.localEventIdReplaced",
- Status = "Event.status",
- Replaced = "Event.replaced",
- RelationsCreated = "Event.relationsCreated",
-}
-
-export type MatrixEventEmittedEvents = MatrixEventEvent | ThreadEvent.Update;
-
-export type MatrixEventHandlerMap = {
- /**
- * Fires when an event is decrypted
- *
- * @param event - The matrix event which has been decrypted
- * @param err - The error that occurred during decryption, or `undefined` if no error occurred.
- */
- [MatrixEventEvent.Decrypted]: (event: MatrixEvent, err?: Error) => void;
- [MatrixEventEvent.BeforeRedaction]: (event: MatrixEvent, redactionEvent: MatrixEvent) => void;
- [MatrixEventEvent.VisibilityChange]: (event: MatrixEvent, visible: boolean) => void;
- [MatrixEventEvent.LocalEventIdReplaced]: (event: MatrixEvent) => void;
- [MatrixEventEvent.Status]: (event: MatrixEvent, status: EventStatus | null) => void;
- [MatrixEventEvent.Replaced]: (event: MatrixEvent) => void;
- [MatrixEventEvent.RelationsCreated]: (relationType: string, eventType: string) => void;
-} & Pick<ThreadEventHandlerMap, ThreadEvent.Update>;
-
-export class MatrixEvent extends TypedEventEmitter<MatrixEventEmittedEvents, MatrixEventHandlerMap> {
- private pushActions: IActionsObject | null = null;
- private _replacingEvent: MatrixEvent | null = null;
- private _localRedactionEvent: MatrixEvent | null = null;
- private _isCancelled = false;
- private clearEvent?: IClearEvent;
-
- /* 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.
- */
- private visibility: MessageVisibility = 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.
- private _hasCachedExtEv = false;
- private _cachedExtEv: Optional<ExtensibleEvent> = undefined;
-
- /* curve25519 key which we believe belongs to the sender of the event. See
- * getSenderKey()
- */
- private senderCurve25519Key: string | null = 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()
- */
- private claimedEd25519Key: string | null = null;
-
- /* curve25519 keys of devices involved in telling us about the
- * senderCurve25519Key and claimedEd25519Key.
- * See getForwardingCurve25519KeyChain().
- */
- private forwardingCurve25519KeyChain: string[] = [];
-
- /* where the decryption key is untrusted
- */
- private untrusted: boolean | null = null;
-
- /* if we have a process decrypting this event, a Promise which resolves
- * when it is finished. Normally null.
- */
- private decryptionPromise: Promise<void> | null = 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)
- */
- private 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.
- */
- private txnId?: string;
-
- /**
- * A reference to the thread this event belongs to
- */
- private thread?: Thread;
- private threadId?: string;
-
- /*
- * 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.
- */
- private 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.
- */
- public localTimestamp: number;
-
- /**
- * 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
- */
- public sender: RoomMember | null = 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
- */
- public target: RoomMember | null = null;
- /**
- * The sending status of the event.
- * @privateRemarks
- * Should be read-only
- */
- public status: EventStatus | null = null;
- /**
- * most recent error associated with sending the event, if any
- * @privateRemarks
- * Should be read-only
- */
- public error: MatrixError | null = 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
- */
- public 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.
- */
- public verificationRequest?: VerificationRequest;
-
- private readonly reEmitter: TypedReEmitter<MatrixEventEmittedEvents, MatrixEventHandlerMap>;
-
- /**
- * 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.
- */
- public constructor(public event: Partial<IEvent> = {}) {
- 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.
- (["state_key", "type", "sender", "room_id", "membership"] as const).forEach((prop) => {
- if (typeof event[prop] !== "string") return;
- event[prop] = internaliseString(event[prop]!);
- });
-
- (["membership", "avatar_url", "displayname"] as const).forEach((prop) => {
- if (typeof event.content?.[prop] !== "string") return;
- event.content[prop] = internaliseString(event.content[prop]!);
- });
-
- (["rel_type"] as const).forEach((prop) => {
- if (typeof event.content?.["m.relates_to"]?.[prop] !== "string") return;
- event.content["m.relates_to"][prop] = internaliseString(event.content["m.relates_to"][prop]!);
- });
-
- this.txnId = event.txn_id;
- this.localTimestamp = Date.now() - (this.getAge() ?? 0);
- this.reEmitter = new 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.
- */
- public get unstableExtensibleEvent(): Optional<ExtensibleEvent> {
- if (!this._hasCachedExtEv) {
- this._cachedExtEv = ExtensibleEvents.parse(this.getEffectiveEvent());
- }
- return this._cachedExtEv;
- }
-
- private invalidateExtensibleEvent(): void {
- // 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.
- */
- public getEffectiveEvent(): IEvent {
- const content = Object.assign({}, this.getContent()); // clone for mutation
-
- if (this.getWireType() === 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 }) as IEvent;
- }
-
- /**
- * Get the event_id for this event.
- * @returns The event ID, e.g. <code>$143350589368169JsLZx:localhost
- * </code>
- */
- public getId(): string | undefined {
- return this.event.event_id;
- }
-
- /**
- * Get the user_id for this event.
- * @returns The user ID, e.g. `@alice:matrix.org`
- */
- public getSender(): string | undefined {
- 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`
- */
- public getType(): EventType | string {
- 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.
- */
- public getWireType(): EventType | string {
- 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>
- */
- public getRoomId(): string | undefined {
- return this.event.room_id;
- }
-
- /**
- * Get the timestamp of this event.
- * @returns The event timestamp, e.g. `1433502692297`
- */
- public getTs(): number {
- 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)`
- */
- public getDate(): Date | null {
- 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
- * ```
- */
- public getDetails(): string {
- 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.
- */
- public getOriginalContent<T = IContent>(): T {
- if (this._localRedactionEvent) {
- return {} as T;
- }
- if (this.clearEvent) {
- return (this.clearEvent.content || {}) as T;
- }
- return (this.event.content || {}) as T;
- }
-
- /**
- * 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.
- */
- public getContent<T extends IContent = IContent>(): T {
- if (this._localRedactionEvent) {
- return {} as T;
- } 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.
- */
- public getWireContent(): IContent {
- return this.event.content || {};
- }
-
- /**
- * Get the event ID of the thread head
- */
- public get threadRootId(): string | undefined {
- const relatesTo = this.getWireContent()?.["m.relates_to"];
- if (relatesTo?.rel_type === 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
- */
- public get isThreadRoot(): boolean {
- const threadDetails = this.getServerAggregatedRelation<IThreadBundledRelationship>(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();
- }
-
- public get replyEventId(): string | undefined {
- return this.getWireContent()["m.relates_to"]?.["m.in_reply_to"]?.event_id;
- }
-
- public get relationEventId(): string | undefined {
- 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.
- */
- public getPrevContent(): IContent {
- // 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.
- */
- public getDirectionalContent(): IContent {
- 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.
- */
- public getAge(): number | undefined {
- 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.
- */
- public getLocalAge(): number {
- 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`.
- */
- public getStateKey(): string | undefined {
- return this.event.state_key;
- }
-
- /**
- * Check if this event is a state event.
- * @returns True if this is a state event.
- */
- public isState(): boolean {
- 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}
- */
- public makeEncrypted(
- cryptoType: string,
- cryptoContent: object,
- senderCurve25519Key: string,
- claimedEd25519Key: string,
- ): void {
- // 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.
- */
- public isBeingDecrypted(): boolean {
- return this.decryptionPromise != null;
- }
-
- public getDecryptionPromise(): Promise<void> | null {
- 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.
- */
- public isDecryptionFailure(): boolean {
- 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.
- */
- public get isEncryptedDisabledForUnverifiedDevices(): boolean {
- return this.isDecryptionFailure() && this.encryptedDisabledForUnverifiedDevices;
- }
-
- public shouldAttemptDecryption(): boolean {
- 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.
- */
- public async attemptDecryption(crypto: CryptoBackend, options: IDecryptOptions = {}): Promise<void> {
- // 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.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
- */
- public cancelAndResendKeyRequest(crypto: Crypto, userId: string): Promise<void> {
- 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
- */
- public getKeyRequestRecipients(userId: string): IKeyRequestRecipient[] {
- // send the request to all of our own devices
- const recipients = [
- {
- userId,
- deviceId: "*",
- },
- ];
-
- return recipients;
- }
-
- private async decryptionLoop(crypto: CryptoBackend, options: IDecryptOptions = {}): Promise<void> {
- // 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: IEventDecryptionResult;
- let err: Error | undefined = undefined;
- try {
- if (!crypto) {
- res = this.badEncryptedMessage("Encryption not enabled");
- } else {
- res = await crypto.decryptEvent(this);
- if (options.isRetry === true) {
- logger.info(`Decrypted event on retry (${this.getDetails()})`);
- }
- }
- } catch (e) {
- const detailedError = e instanceof DecryptionError ? (<DecryptionError>e).detailedString : String(e);
-
- err = e as Error;
-
- // 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.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.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.setPushActions(null);
-
- if (options.emit !== false) {
- this.emit(MatrixEventEvent.Decrypted, this, err);
- }
-
- return;
- }
- }
-
- private badEncryptedMessage(reason: string): IEventDecryptionResult {
- return {
- clearEvent: {
- type: EventType.RoomMessage,
- content: {
- msgtype: "m.bad.encrypted",
- body: "** Unable to decrypt: " + reason + " **",
- },
- },
- encryptedDisabledForUnverifiedDevices: reason === `DecryptionError: ${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}
- */
- private setClearData(decryptionResult: IEventDecryptionResult): void {
- 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
- */
- public getClearContent(): IContent | null {
- return this.clearEvent ? this.clearEvent.content : null;
- }
-
- /**
- * Check if the event is encrypted.
- * @returns True if this event is encrypted.
- */
- public isEncrypted(): boolean {
- return !this.isState() && this.event.type === 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
- */
- public getSenderKey(): string | null {
- return this.senderCurve25519Key;
- }
-
- /**
- * The additional keys the sender of this encrypted event claims to possess.
- *
- * Just a wrapper for #getClaimedEd25519Key (q.v.)
- */
- public getKeysClaimed(): Partial<Record<"ed25519", string>> {
- 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.
- */
- public getClaimedEd25519Key(): string | null {
- 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.
- */
- public getForwardingCurve25519KeyChain(): string[] {
- return this.forwardingCurve25519KeyChain;
- }
-
- /**
- * Whether the decryption key was obtained from an untrusted source. If so,
- * we cannot verify the authenticity of the message.
- */
- public isKeySourceUntrusted(): boolean | undefined {
- return !!this.untrusted;
- }
-
- public getUnsigned(): IUnsigned {
- return this.event.unsigned || {};
- }
-
- public setUnsigned(unsigned: IUnsigned): void {
- this.event.unsigned = unsigned;
- }
-
- public unmarkLocallyRedacted(): boolean {
- const value = this._localRedactionEvent;
- this._localRedactionEvent = null;
- if (this.event.unsigned) {
- this.event.unsigned.redacted_because = undefined;
- }
- return !!value;
- }
-
- public markLocallyRedacted(redactionEvent: MatrixEvent): void {
- 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 as IEvent;
- }
-
- /**
- * 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).
- */
- public applyVisibilityEvent(visibilityChange?: IVisibilityChange): void {
- 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.
- */
- public messageVisibility(): 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
- */
- public makeRedacted(redactionEvent: MatrixEvent): void {
- // 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 as IEvent;
-
- for (const key in this.event) {
- if (this.event.hasOwnProperty(key) && !REDACT_KEEP_KEYS.has(key)) {
- delete this.event[key as keyof IEvent];
- }
- }
-
- // 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() as keyof typeof REDACT_KEEP_CONTENT_MAP]
- : {};
- 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
- */
- public isRedacted(): boolean {
- return Boolean(this.getUnsigned().redacted_because);
- }
-
- /**
- * Check if this event is a redaction of another event
- *
- * @returns True if this event is a redaction
- */
- public isRedaction(): boolean {
- return this.getType() === 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`.
- */
- public asVisibilityChange(): IVisibilityChange | null {
- if (!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.
- */
- public isVisibilityEvent(): boolean {
- return 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
- */
- public getRedactionEvent(): IEvent | {} | null {
- 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
- */
- public getPushActions(): IActionsObject | null {
- return this.pushActions;
- }
-
- /**
- * Set the push actions for this event.
- *
- * @param pushActions - push actions
- */
- public setPushActions(pushActions: IActionsObject | null): void {
- this.pushActions = pushActions;
- }
-
- /**
- * Replace the `event` property and recalculate any properties based on it.
- * @param event - the object to assign to the `event` property
- */
- public handleRemoteEcho(event: object): void {
- 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.
- */
- public isSending(): boolean {
- return !!this.status;
- }
-
- /**
- * Update the event's sending status and emit an event as well.
- *
- * @param status - The new status
- */
- public setStatus(status: EventStatus | null): void {
- this.status = status;
- this.emit(MatrixEventEvent.Status, this, status);
- }
-
- public replaceLocalEventId(eventId: string): void {
- 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
- */
- public isRelation(relType?: string): boolean {
- // 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 === 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.
- */
- public getRelation(): IEventRelation | null {
- 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}
- */
- public makeReplaced(newEvent?: MatrixEvent): void {
- // 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.
- */
- public getAssociatedStatus(): EventStatus | null {
- if (this._replacingEvent) {
- return this._replacingEvent.status;
- } else if (this._localRedactionEvent) {
- return this._localRedactionEvent.status;
- }
- return this.status;
- }
-
- public getServerAggregatedRelation<T>(relType: RelationType | string): T | undefined {
- return this.getUnsigned()["m.relations"]?.[relType];
- }
-
- /**
- * Returns the event ID of the event replacing the content of this event, if any.
- */
- public replacingEventId(): string | undefined {
- const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(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.
- */
- public replacingEvent(): MatrixEvent | null {
- return this._replacingEvent;
- }
-
- /**
- * Returns the origin_server_ts of the event replacing the content of this event, if any.
- */
- public replacingEventDate(): Date | undefined {
- const replaceRelation = this.getServerAggregatedRelation<IAggregatedRelation>(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
- */
- public localRedactionEvent(): MatrixEvent | null {
- return this._localRedactionEvent;
- }
-
- /**
- * For relations and redactions, returns the event_id this event is referring to.
- */
- public getAssociatedId(): string | undefined {
- 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.
- */
- public hasAssocation(): boolean {
- return !!this.getAssociatedId();
- }
-
- /**
- * Checks if this event is associated with another event. See `getAssociatedId`.
- */
- public hasAssociation(): boolean {
- 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
- */
- public updateAssociatedId(eventId: string): void {
- 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.
- */
- public flagCancelled(cancelled = true): void {
- 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.
- */
- public isCancelled(): boolean {
- 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.
- */
- public toSnapshot(): MatrixEvent {
- 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 as keyof MatrixEvent] = 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.
- */
- public isEquivalentTo(otherEvent: MatrixEvent): boolean {
- if (!otherEvent) return false;
- if (otherEvent === this) return true;
- const myProps = deepSortedObjectEntries(this.event);
- const theirProps = 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.
- */
- public toJSON(): object {
- const event = this.getEffectiveEvent();
-
- if (!this.isEncrypted()) {
- return event;
- }
-
- return {
- decrypted: event,
- encrypted: this.event,
- };
- }
-
- public setVerificationRequest(request: VerificationRequest): void {
- this.verificationRequest = request;
- }
-
- public setTxnId(txnId: string): void {
- this.txnId = txnId;
- }
-
- public getTxnId(): string | undefined {
- return this.txnId;
- }
-
- /**
- * Set the instance of a thread associated with the current event
- * @param thread - the thread
- */
- public setThread(thread?: Thread): void {
- if (this.thread) {
- this.reEmitter.stopReEmitting(this.thread, [ThreadEvent.Update]);
- }
- this.thread = thread;
- this.setThreadId(thread?.id);
- if (thread) {
- this.reEmitter.reEmit(thread, [ThreadEvent.Update]);
- }
- }
-
- /**
- * Get the instance of the thread associated with the current event
- */
- public getThread(): Thread | undefined {
- return this.thread;
- }
-
- public setThreadId(threadId?: string): void {
- 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
- */
-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: Record<string, Record<string, 1>> = {
- [EventType.RoomMember]: { membership: 1 },
- [EventType.RoomCreate]: { creator: 1 },
- [EventType.RoomJoinRules]: { join_rule: 1 },
- [EventType.RoomPowerLevels]: {
- ban: 1,
- events: 1,
- events_default: 1,
- kick: 1,
- redact: 1,
- state_default: 1,
- users: 1,
- users_default: 1,
- },
-} as const;
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts
deleted file mode 100644
index 173ba62..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/invites-ignorer.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { UnstableValue } from "matrix-events-sdk";
-
-import { MatrixClient } from "../client";
-import { IContent, MatrixEvent } from "./event";
-import { EventTimeline } from "./event-timeline";
-import { Preset } from "../@types/partials";
-import { globToRegexp } from "../utils";
-import { Room } from "./room";
-
-/// The event type storing the user's individual policies.
-///
-/// Exported for testing purposes.
-export const POLICIES_ACCOUNT_EVENT_TYPE = new 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.
-export const IGNORE_INVITES_ACCOUNT_EVENT_KEY = new UnstableValue(
- "m.ignore.invites",
- "org.matrix.msc3847.ignore.invites",
-);
-
-/// The types of recommendations understood.
-enum PolicyRecommendation {
- Ban = "m.ban",
-}
-
-/**
- * The various scopes for policies.
- */
-export enum PolicyScope {
- /**
- * The policy deals with an individual user, e.g. reject invites
- * from this user.
- */
- User = "m.policy.user",
-
- /**
- * The policy deals with a room, e.g. reject invites towards
- * a specific room.
- */
- Room = "m.policy.room",
-
- /**
- * The policy deals with a server, e.g. reject invites from
- * this server.
- */
- Server = "m.policy.server",
-}
-
-/**
- * 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.
- */
-export class IgnoredInvites {
- public constructor(private readonly client: MatrixClient) {}
-
- /**
- * 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.
- */
- public async addRule(scope: PolicyScope, entity: string, reason: string): Promise<string> {
- 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.
- */
- public async removeRule(event: MatrixEvent): Promise<void> {
- 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.
- */
- public async addSource(roomId: string): Promise<boolean> {
- // 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.
- */
- public async getRuleForInvite({
- sender,
- roomId,
- }: {
- sender: string;
- roomId: string;
- }): Promise<Readonly<MatrixEvent | null>> {
- // 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.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: RegExp;
- try {
- regexp = new RegExp(globToRegexp(glob, false));
- } 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.
- */
- public async getOrCreateTargetRoom(): Promise<Room> {
- 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: 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.
- */
- public async getOrCreateSourceRooms(): Promise<Room[]> {
- const ignoreInvitesPolicies = this.getIgnoreInvitesPolicies();
- let sources: string[] = 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) as 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.
- */
- private getIgnoreInvitesPolicies(): { [key: string]: any } {
- return this.getPoliciesAndIgnoreInvitesPolicies().ignoreInvitesPolicies;
- }
-
- /**
- * Modify in place the `IGNORE_INVITES_POLICIES` object from account data.
- */
- private async withIgnoreInvitesPolicies(
- cb: (ignoreInvitesPolicies: { [key: string]: any }) => void,
- ): Promise<void> {
- 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.
- */
- private getPoliciesAndIgnoreInvitesPolicies(): {
- policies: { [key: string]: any };
- ignoreInvitesPolicies: { [key: string]: any };
- } {
- let policies: IContent = {};
- 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 };
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts
deleted file mode 100644
index 1d4344a..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/poll.ts
+++ /dev/null
@@ -1,268 +0,0 @@
-/*
-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.
-*/
-
-import { M_POLL_END, M_POLL_RESPONSE } from "../@types/polls";
-import { MatrixClient } from "../client";
-import { PollStartEvent } from "../extensible_events_v1/PollStartEvent";
-import { MatrixEvent } from "./event";
-import { Relations } from "./relations";
-import { Room } from "./room";
-import { TypedEventEmitter } from "./typed-event-emitter";
-
-export enum PollEvent {
- New = "Poll.new",
- End = "Poll.end",
- Update = "Poll.update",
- Responses = "Poll.Responses",
- Destroy = "Poll.Destroy",
- UndecryptableRelations = "Poll.UndecryptableRelations",
-}
-
-export type PollEventHandlerMap = {
- [PollEvent.Update]: (event: MatrixEvent, poll: Poll) => void;
- [PollEvent.Destroy]: (pollIdentifier: string) => void;
- [PollEvent.End]: () => void;
- [PollEvent.Responses]: (responses: Relations) => void;
- [PollEvent.UndecryptableRelations]: (count: number) => void;
-};
-
-const filterResponseRelations = (
- relationEvents: MatrixEvent[],
- pollEndTimestamp: number,
-): {
- responseEvents: MatrixEvent[];
-} => {
- const responseEvents = relationEvents.filter((event) => {
- if (event.isDecryptionFailure()) {
- return;
- }
- return (
- 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 };
-};
-
-export class Poll extends TypedEventEmitter<Exclude<PollEvent, PollEvent.New>, PollEventHandlerMap> {
- public readonly roomId: string;
- public readonly pollEvent: PollStartEvent;
- private _isFetchingResponses = false;
- private relationsNextBatch: string | undefined;
- private responses: null | Relations = null;
- private endEvent: MatrixEvent | undefined;
- /**
- * Keep track of undecryptable relations
- * As incomplete result sets affect poll results
- */
- private undecryptableRelationEventIds = new Set<string>();
-
- public constructor(public readonly rootEvent: MatrixEvent, private matrixClient: MatrixClient, private room: Room) {
- super();
- if (!this.rootEvent.getRoomId() || !this.rootEvent.getId()) {
- throw new Error("Invalid poll start event.");
- }
- this.roomId = this.rootEvent.getRoomId()!;
- this.pollEvent = this.rootEvent.unstableExtensibleEvent as unknown as PollStartEvent;
- }
-
- public get pollId(): string {
- return this.rootEvent.getId()!;
- }
-
- public get endEventId(): string | undefined {
- return this.endEvent?.getId();
- }
-
- public get isEnded(): boolean {
- return !!this.endEvent;
- }
-
- public get isFetchingResponses(): boolean {
- return this._isFetchingResponses;
- }
-
- public get undecryptableRelationsCount(): number {
- return this.undecryptableRelationEventIds.size;
- }
-
- public async getResponses(): Promise<Relations> {
- // 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
- */
- public onNewRelation(event: MatrixEvent): void {
- if (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);
- }
- }
-
- private async fetchResponses(): Promise<void> {
- 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("m.reference", M_POLL_RESPONSE.name, this.matrixClient, [M_POLL_RESPONSE.altName!]);
-
- const pollEndEvent = allRelations.events.find((event) => 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
- */
- private refilterResponsesOnEnd(): void {
- 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);
- }
-
- private countUndecryptableEvents = (events: MatrixEvent[]): void => {
- 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);
- }
- };
-
- private validateEndEvent(endEvent?: MatrixEvent): boolean {
- 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))
- );
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts
deleted file mode 100644
index 5858fe5..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/read-receipt.ts
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
- http://www.apache.org/licenses/LICENSE-2.0
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import {
- CachedReceipt,
- MAIN_ROOM_TIMELINE,
- Receipt,
- ReceiptCache,
- ReceiptType,
- WrappedReceipt,
-} from "../@types/read_receipts";
-import { ListenerMap, TypedEventEmitter } from "./typed-event-emitter";
-import * as utils from "../utils";
-import { MatrixEvent } from "./event";
-import { EventType } from "../@types/event";
-import { EventTimelineSet } from "./event-timeline-set";
-import { MapWithDefault } from "../utils";
-import { NotificationCountType } from "./room";
-
-export function synthesizeReceipt(userId: string, event: MatrixEvent, receiptType: ReceiptType): MatrixEvent {
- return new MatrixEvent({
- content: {
- [event.getId()!]: {
- [receiptType]: {
- [userId]: {
- ts: event.getTs(),
- thread_id: event.threadRootId ?? MAIN_ROOM_TIMELINE,
- },
- },
- },
- },
- type: EventType.Receipt,
- room_id: event.getRoomId(),
- });
-}
-
-const ReceiptPairRealIndex = 0;
-const ReceiptPairSyntheticIndex = 1;
-
-export abstract class ReadReceipt<
- Events extends string,
- Arguments extends ListenerMap<Events>,
- SuperclassArguments extends ListenerMap<any> = Arguments,
-> extends TypedEventEmitter<Events, Arguments, SuperclassArguments> {
- // receipts should clobber based on receipt_type and user_id pairs hence
- // the form of this structure. This is sub-optimal for the exposed APIs
- // which pass in an event ID and get back some receipts, so we also store
- // a pre-cached list for this purpose.
- // Map: receipt type → user Id → receipt
- private receipts = new MapWithDefault<string, Map<string, [WrappedReceipt | null, WrappedReceipt | null]>>(
- () => new Map(),
- );
- private receiptCacheByEventId: ReceiptCache = new Map();
-
- public abstract getUnfilteredTimelineSet(): EventTimelineSet;
- public abstract timeline: MatrixEvent[];
-
- /**
- * Gets the latest receipt for a given user in the room
- * @param userId - The id of the user for which we want the receipt
- * @param ignoreSynthesized - Whether to ignore synthesized receipts or not
- * @param receiptType - Optional. The type of the receipt we want to get
- * @returns the latest receipts of the chosen type for the chosen user
- */
- public getReadReceiptForUserId(
- userId: string,
- ignoreSynthesized = false,
- receiptType = ReceiptType.Read,
- ): WrappedReceipt | null {
- const [realReceipt, syntheticReceipt] = this.receipts.get(receiptType)?.get(userId) ?? [null, null];
- if (ignoreSynthesized) {
- return realReceipt;
- }
-
- return syntheticReceipt ?? realReceipt;
- }
-
- /**
- * Get the ID of the event that a given user has read up to, or null if we
- * have received no read receipts from them.
- * @param userId - The user ID to get read receipt event ID for
- * @param ignoreSynthesized - If true, return only receipts that have been
- * sent by the server, not implicit ones generated
- * by the JS SDK.
- * @returns ID of the latest event that the given user has read, or null.
- */
- public getEventReadUpTo(userId: string, ignoreSynthesized = false): string | null {
- // XXX: This is very very ugly and I hope I won't have to ever add a new
- // receipt type here again. IMHO this should be done by the server in
- // some more intelligent manner or the client should just use timestamps
-
- const timelineSet = this.getUnfilteredTimelineSet();
- const publicReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.Read);
- const privateReadReceipt = this.getReadReceiptForUserId(userId, ignoreSynthesized, ReceiptType.ReadPrivate);
-
- // If we have both, compare them
- let comparison: number | null | undefined;
- if (publicReadReceipt?.eventId && privateReadReceipt?.eventId) {
- comparison = timelineSet.compareEventOrdering(publicReadReceipt?.eventId, privateReadReceipt?.eventId);
- }
-
- // If we didn't get a comparison try to compare the ts of the receipts
- if (!comparison && publicReadReceipt?.data?.ts && privateReadReceipt?.data?.ts) {
- comparison = publicReadReceipt?.data?.ts - privateReadReceipt?.data?.ts;
- }
-
- // The public receipt is more likely to drift out of date so the private
- // one has precedence
- if (!comparison) return privateReadReceipt?.eventId ?? publicReadReceipt?.eventId ?? null;
-
- // If public read receipt is older, return the private one
- return (comparison < 0 ? privateReadReceipt?.eventId : publicReadReceipt?.eventId) ?? null;
- }
-
- public addReceiptToStructure(
- eventId: string,
- receiptType: ReceiptType,
- userId: string,
- receipt: Receipt,
- synthetic: boolean,
- ): void {
- const receiptTypesMap = this.receipts.getOrCreate(receiptType);
- let pair = receiptTypesMap.get(userId);
-
- if (!pair) {
- pair = [null, null];
- receiptTypesMap.set(userId, pair);
- }
-
- let existingReceipt = pair[ReceiptPairRealIndex];
- if (synthetic) {
- existingReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
- }
-
- if (existingReceipt) {
- // we only want to add this receipt if we think it is later than the one we already have.
- // This is managed server-side, but because we synthesize RRs locally we have to do it here too.
- const ordering = this.getUnfilteredTimelineSet().compareEventOrdering(existingReceipt.eventId, eventId);
- if (ordering !== null && ordering >= 0) {
- return;
- }
- }
-
- const wrappedReceipt: WrappedReceipt = {
- eventId,
- data: receipt,
- };
-
- const realReceipt = synthetic ? pair[ReceiptPairRealIndex] : wrappedReceipt;
- const syntheticReceipt = synthetic ? wrappedReceipt : pair[ReceiptPairSyntheticIndex];
-
- let ordering: number | null = null;
- if (realReceipt && syntheticReceipt) {
- ordering = this.getUnfilteredTimelineSet().compareEventOrdering(
- realReceipt.eventId,
- syntheticReceipt.eventId,
- );
- }
-
- const preferSynthetic = ordering === null || ordering < 0;
-
- // we don't bother caching just real receipts by event ID as there's nothing that would read it.
- // Take the current cached receipt before we overwrite the pair elements.
- const cachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
-
- if (synthetic && preferSynthetic) {
- pair[ReceiptPairSyntheticIndex] = wrappedReceipt;
- } else if (!synthetic) {
- pair[ReceiptPairRealIndex] = wrappedReceipt;
-
- if (!preferSynthetic) {
- pair[ReceiptPairSyntheticIndex] = null;
- }
- }
-
- const newCachedReceipt = pair[ReceiptPairSyntheticIndex] ?? pair[ReceiptPairRealIndex];
- if (cachedReceipt === newCachedReceipt) return;
-
- // clean up any previous cache entry
- if (cachedReceipt && this.receiptCacheByEventId.get(cachedReceipt.eventId)) {
- const previousEventId = cachedReceipt.eventId;
- // Remove the receipt we're about to clobber out of existence from the cache
- this.receiptCacheByEventId.set(
- previousEventId,
- this.receiptCacheByEventId.get(previousEventId)!.filter((r) => {
- return r.type !== receiptType || r.userId !== userId;
- }),
- );
-
- if (this.receiptCacheByEventId.get(previousEventId)!.length < 1) {
- this.receiptCacheByEventId.delete(previousEventId); // clean up the cache keys
- }
- }
-
- // cache the new one
- if (!this.receiptCacheByEventId.get(eventId)) {
- this.receiptCacheByEventId.set(eventId, []);
- }
- this.receiptCacheByEventId.get(eventId)!.push({
- userId: userId,
- type: receiptType as ReceiptType,
- data: receipt,
- });
- }
-
- /**
- * Get a list of receipts for the given event.
- * @param event - the event to get receipts for
- * @returns A list of receipts with a userId, type and data keys or
- * an empty list.
- */
- public getReceiptsForEvent(event: MatrixEvent): CachedReceipt[] {
- return this.receiptCacheByEventId.get(event.getId()!) || [];
- }
-
- public abstract addReceipt(event: MatrixEvent, synthetic: boolean): void;
-
- public abstract setUnread(type: NotificationCountType, count: number): void;
-
- /**
- * This issue should also be addressed on synapse's side and is tracked as part
- * of https://github.com/matrix-org/synapse/issues/14837
- *
- * Retrieves the read receipt for the logged in user and checks if it matches
- * the last event in the room and whether that event originated from the logged
- * in user.
- * Under those conditions we can consider the context as read. This is useful
- * because we never send read receipts against our own events
- * @param userId - the logged in user
- */
- public fixupNotifications(userId: string): void {
- const receipt = this.getReadReceiptForUserId(userId, false);
-
- const lastEvent = this.timeline[this.timeline.length - 1];
- if (lastEvent && receipt?.eventId === lastEvent.getId() && userId === lastEvent.getSender()) {
- this.setUnread(NotificationCountType.Total, 0);
- this.setUnread(NotificationCountType.Highlight, 0);
- }
- }
-
- /**
- * Add a temporary local-echo receipt to the room to reflect in the
- * client the fact that we've sent one.
- * @param userId - The user ID if the receipt sender
- * @param e - The event that is to be acknowledged
- * @param receiptType - The type of receipt
- */
- public addLocalEchoReceipt(userId: string, e: MatrixEvent, receiptType: ReceiptType): void {
- this.addReceipt(synthesizeReceipt(userId, e, receiptType), true);
- }
-
- /**
- * Get a list of user IDs who have <b>read up to</b> the given event.
- * @param event - the event to get read receipts for.
- * @returns A list of user IDs.
- */
- public getUsersReadUpTo(event: MatrixEvent): string[] {
- return this.getReceiptsForEvent(event)
- .filter(function (receipt) {
- return utils.isSupportedReceiptType(receipt.type);
- })
- .map(function (receipt) {
- return receipt.userId;
- });
- }
-
- /**
- * Determines if the given user has read a particular event ID with the known
- * history of the room. This is not a definitive check as it relies only on
- * what is available to the room at the time of execution.
- * @param userId - The user ID to check the read state of.
- * @param eventId - The event ID to check if the user read.
- * @returns True if the user has read the event, false otherwise.
- */
- public hasUserReadEvent(userId: string, eventId: string): boolean {
- const readUpToId = this.getEventReadUpTo(userId, false);
- if (readUpToId === eventId) return true;
-
- if (
- this.timeline?.length &&
- this.timeline[this.timeline.length - 1].getSender() &&
- this.timeline[this.timeline.length - 1].getSender() === userId
- ) {
- // It doesn't matter where the event is in the timeline, the user has read
- // it because they've sent the latest event.
- return true;
- }
-
- for (let i = this.timeline?.length - 1; i >= 0; --i) {
- const ev = this.timeline[i];
-
- // If we encounter the target event first, the user hasn't read it
- // however if we encounter the readUpToId first then the user has read
- // it. These rules apply because we're iterating bottom-up.
- if (ev.getId() === eventId) return false;
- if (ev.getId() === readUpToId) return true;
- }
-
- // We don't know if the user has read it, so assume not.
- return false;
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts
deleted file mode 100644
index a005169..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/related-relations.ts
+++ /dev/null
@@ -1,39 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { Relations, RelationsEvent, EventHandlerMap } from "./relations";
-import { MatrixEvent } from "./event";
-import { Listener } from "./typed-event-emitter";
-
-export class RelatedRelations {
- private relations: Relations[];
-
- public constructor(relations: Relations[]) {
- this.relations = relations.filter((r) => !!r);
- }
-
- public getRelations(): MatrixEvent[] {
- return this.relations.reduce<MatrixEvent[]>((c, p) => [...c, ...p.getRelations()], []);
- }
-
- public on<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>): void {
- this.relations.forEach((r) => r.on(ev, fn));
- }
-
- public off<T extends RelationsEvent>(ev: T, fn: Listener<RelationsEvent, EventHandlerMap, T>): void {
- this.relations.forEach((r) => r.off(ev, fn));
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts
deleted file mode 100644
index d328b1c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations-container.ts
+++ /dev/null
@@ -1,146 +0,0 @@
-/*
-Copyright 2022 The Matrix.org Foundation C.I.C.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-import { Relations } from "./relations";
-import { EventType, RelationType } from "../@types/event";
-import { EventStatus, MatrixEvent, MatrixEventEvent } from "./event";
-import { EventTimelineSet } from "./event-timeline-set";
-import { MatrixClient } from "../client";
-import { Room } from "./room";
-
-export class RelationsContainer {
- // A tree of objects to access a set of related children for an event, as in:
- // this.relations.get(parentEventId).get(relationType).get(relationEventType)
- private relations = new Map<string, Map<RelationType | string, Map<EventType | string, Relations>>>();
-
- public constructor(private readonly client: MatrixClient, private readonly room?: Room) {}
-
- /**
- * 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.
- */
- public getChildEventsForEvent(
- eventId: string,
- relationType: RelationType | string,
- eventType: EventType | string,
- ): Relations | undefined {
- return this.relations.get(eventId)?.get(relationType)?.get(eventType);
- }
-
- public getAllChildEventsForEvent(parentEventId: string): MatrixEvent[] {
- const relationsForEvent =
- this.relations.get(parentEventId) ?? new Map<RelationType | string, Map<EventType | string, Relations>>();
- const events: MatrixEvent[] = [];
- 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.
- */
- public aggregateParentEvent(event: MatrixEvent): void {
- 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.
- */
- public aggregateChildEvent(event: MatrixEvent, timelineSet?: EventTimelineSet): void {
- if (event.isRedacted() || event.status === EventStatus.CANCELLED) {
- return;
- }
-
- const relation = event.getRelation();
- if (!relation) return;
-
- const onEventDecrypted = (): void => {
- 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(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(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<RelationType | string, Map<EventType | string, Relations>>();
- this.relations.set(relatesToEventId!, relationsForEvent);
- }
-
- let relationsWithRelType = relationsForEvent.get(relationType!);
- if (!relationsWithRelType) {
- relationsWithRelType = new Map<EventType | string, Relations>();
- relationsForEvent.set(relationType!, relationsWithRelType);
- }
-
- let relationsWithEventType = relationsWithRelType.get(eventType);
- if (!relationsWithEventType) {
- relationsWithEventType = new 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);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts
deleted file mode 100644
index d2b637c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/relations.ts
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
-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.
-*/
-
-import { EventStatus, IAggregatedRelation, MatrixEvent, MatrixEventEvent } from "./event";
-import { logger } from "../logger";
-import { RelationType } from "../@types/event";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { MatrixClient } from "../client";
-import { Room } from "./room";
-
-export enum RelationsEvent {
- Add = "Relations.add",
- Remove = "Relations.remove",
- Redaction = "Relations.redaction",
-}
-
-export type EventHandlerMap = {
- [RelationsEvent.Add]: (event: MatrixEvent) => void;
- [RelationsEvent.Remove]: (event: MatrixEvent) => void;
- [RelationsEvent.Redaction]: (event: MatrixEvent) => void;
-};
-
-const matchesEventType = (eventType: string, targetEventType: string, altTargetEventTypes: string[] = []): boolean =>
- [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.
- */
-export class Relations extends TypedEventEmitter<RelationsEvent, EventHandlerMap> {
- private relationEventIds = new Set<string>();
- private relations = new Set<MatrixEvent>();
- private annotationsByKey: Record<string, Set<MatrixEvent>> = {};
- private annotationsBySender: Record<string, Set<MatrixEvent>> = {};
- private sortedAnnotationsByKey: [string, Set<MatrixEvent>][] = [];
- private targetEvent: MatrixEvent | null = null;
- private creationEmitted = false;
- private readonly client: MatrixClient;
-
- /**
- * @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
- */
- public constructor(
- public readonly relationType: RelationType | string,
- public readonly eventType: string,
- client: MatrixClient | Room,
- public readonly altEventTypes?: string[],
- ) {
- super();
- this.client = client instanceof Room ? client.client : client;
- }
-
- /**
- * Add relation events to this collection.
- *
- * @param event - The new relation event to be added.
- */
- public async addEvent(event: MatrixEvent): Promise<void> {
- if (this.relationEventIds.has(event.getId()!)) {
- return;
- }
-
- const relation = event.getRelation();
- if (!relation) {
- 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.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(MatrixEventEvent.Status, this.onEventStatus);
- }
-
- this.relations.add(event);
- this.relationEventIds.add(event.getId()!);
-
- if (this.relationType === RelationType.Annotation) {
- this.addAnnotationToAggregation(event);
- } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
- const lastReplacement = await this.getLastReplacement();
- this.targetEvent.makeReplaced(lastReplacement!);
- }
-
- event.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
-
- this.emit(RelationsEvent.Add, event);
-
- this.maybeEmitCreated();
- }
-
- /**
- * Remove relation event from this collection.
- *
- * @param event - The relation event to remove.
- */
- public async removeEvent(event: MatrixEvent): Promise<void> {
- if (!this.relations.has(event)) {
- return;
- }
-
- this.relations.delete(event);
-
- if (this.relationType === RelationType.Annotation) {
- this.removeAnnotationFromAggregation(event);
- } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
- const lastReplacement = await this.getLastReplacement();
- this.targetEvent.makeReplaced(lastReplacement!);
- }
-
- this.emit(RelationsEvent.Remove, event);
- }
-
- /**
- * Listens for event status changes to remove cancelled events.
- *
- * @param event - The event whose status has changed
- * @param status - The new status
- */
- private onEventStatus = (event: MatrixEvent, status: EventStatus | null): void => {
- if (!event.isSending()) {
- // Sending is done, so we don't need to listen anymore
- event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
- return;
- }
- if (status !== EventStatus.CANCELLED) {
- return;
- }
- // Event was cancelled, remove from the collection
- event.removeListener(MatrixEventEvent.Status, this.onEventStatus);
- this.removeEvent(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.
- */
- public getRelations(): MatrixEvent[] {
- return [...this.relations];
- }
-
- private addAnnotationToAggregation(event: MatrixEvent): void {
- 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);
- }
-
- private removeAnnotationFromAggregation(event: MatrixEvent): void {
- 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);
- }
- }
-
- /**
- * 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.
- */
- private onBeforeRedaction = async (redactedEvent: MatrixEvent): Promise<void> => {
- if (!this.relations.has(redactedEvent)) {
- return;
- }
-
- this.relations.delete(redactedEvent);
-
- if (this.relationType === RelationType.Annotation) {
- // Remove the redacted annotation from aggregation by key
- this.removeAnnotationFromAggregation(redactedEvent);
- } else if (this.relationType === RelationType.Replace && this.targetEvent && !this.targetEvent.isState()) {
- const lastReplacement = await this.getLastReplacement();
- this.targetEvent.makeReplaced(lastReplacement!);
- }
-
- redactedEvent.removeListener(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
-
- this.emit(RelationsEvent.Redaction, redactedEvent);
- };
-
- /**
- * 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).
- */
- public getSortedAnnotationsByKey(): [string, Set<MatrixEvent>][] | null {
- if (this.relationType !== 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.
- */
- public getAnnotationsBySender(): Record<string, Set<MatrixEvent>> | null {
- if (this.relationType !== 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`.
- */
- public async getLastReplacement(): Promise<MatrixEvent | null> {
- if (this.relationType !== 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<IAggregatedRelation>(RelationType.Replace);
- const minTs = replaceRelation?.origin_server_ts;
-
- const lastReplacement = this.getRelations().reduce<MatrixEvent | null>((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.
- */
- public async setTargetEvent(event: MatrixEvent): Promise<void> {
- if (this.targetEvent) {
- return;
- }
- this.targetEvent = event;
-
- if (this.relationType === 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();
- }
-
- private maybeEmitCreated(): void {
- 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(MatrixEventEvent.RelationsCreated, this.relationType, this.eventType);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts
deleted file mode 100644
index 116a93b..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-member.ts
+++ /dev/null
@@ -1,453 +0,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.
-*/
-
-import { getHttpUriForMxc } from "../content-repo";
-import * as utils from "../utils";
-import { User } from "./user";
-import { MatrixEvent } from "./event";
-import { RoomState } from "./room-state";
-import { logger } from "../logger";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { EventType } from "../@types/event";
-
-export enum RoomMemberEvent {
- Membership = "RoomMember.membership",
- Name = "RoomMember.name",
- PowerLevel = "RoomMember.powerLevel",
- Typing = "RoomMember.typing",
-}
-
-export type RoomMemberEventHandlerMap = {
- /**
- * Fires whenever any room member's membership state changes.
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.membership changed.
- * @param oldMembership - The previous membership state. Null if it's a new member.
- * @example
- * ```
- * matrixClient.on("RoomMember.membership", function(event, member, oldMembership){
- * var newState = member.membership;
- * });
- * ```
- */
- [RoomMemberEvent.Membership]: (event: MatrixEvent, member: RoomMember, oldMembership?: string) => void;
- /**
- * Fires whenever any room member's name changes.
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.name changed.
- * @param oldName - The previous name. Null if the member didn't have a name previously.
- * @example
- * ```
- * matrixClient.on("RoomMember.name", function(event, member){
- * var newName = member.name;
- * });
- * ```
- */
- [RoomMemberEvent.Name]: (event: MatrixEvent, member: RoomMember, oldName: string | null) => void;
- /**
- * Fires whenever any room member's power level changes.
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.powerLevel changed.
- * @example
- * ```
- * matrixClient.on("RoomMember.powerLevel", function(event, member){
- * var newPowerLevel = member.powerLevel;
- * var newNormPowerLevel = member.powerLevelNorm;
- * });
- * ```
- */
- [RoomMemberEvent.PowerLevel]: (event: MatrixEvent, member: RoomMember) => void;
- /**
- * Fires whenever any room member's typing state changes.
- * @param event - The matrix event which caused this event to fire.
- * @param member - The member whose RoomMember.typing changed.
- * @example
- * ```
- * matrixClient.on("RoomMember.typing", function(event, member){
- * var isTyping = member.typing;
- * });
- * ```
- */
- [RoomMemberEvent.Typing]: (event: MatrixEvent, member: RoomMember) => void;
-};
-
-export class RoomMember extends TypedEventEmitter<RoomMemberEvent, RoomMemberEventHandlerMap> {
- private _isOutOfBand = false;
- private modified = -1;
- public requestedProfileInfo = false; // used by sync.ts
-
- // XXX these should be read-only
- /**
- * True if the room member is currently typing.
- */
- public 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.
- */
- public name: string;
- /**
- * The ambiguous displayname of this room member.
- */
- public rawDisplayName: string;
- /**
- * The power level for this room member.
- */
- public powerLevel = 0;
- /**
- * The normalised power level (0-100) for this room member.
- */
- public powerLevelNorm = 0;
- /**
- * The User object for this room member, if one exists.
- */
- public user?: User;
- /**
- * The membership state for this room member e.g. 'join'.
- */
- public membership?: string;
- /**
- * True if the member's name is disambiguated.
- */
- public disambiguate = false;
- /**
- * The events describing this RoomMember.
- */
- public events: {
- /**
- * The m.room.member event for this RoomMember.
- */
- member?: MatrixEvent;
- } = {};
-
- /**
- * Construct a new room member.
- *
- * @param roomId - The room ID of the member.
- * @param userId - The user ID of the member.
- */
- public constructor(public readonly roomId: string, public readonly userId: string) {
- super();
-
- this.name = userId;
- this.rawDisplayName = userId;
- this.updateModifiedTime();
- }
-
- /**
- * Mark the member as coming from a channel that is not sync
- */
- public markOutOfBand(): void {
- 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.
- */
- public isOutOfBand(): boolean {
- 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}
- */
- public setMembershipEvent(event: MatrixEvent, roomState?: RoomState): void {
- const displayName = event.getDirectionalContent().displayname ?? "";
-
- if (event.getType() !== 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.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 = utils.removeDirectionOverrideChars(event.getDirectionalContent().displayname ?? "");
- if (!this.rawDisplayName || !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}
- */
- public setPowerLevelEvent(powerLevelEvent: MatrixEvent): void {
- if (powerLevelEvent.getType() !== EventType.RoomPowerLevels || powerLevelEvent.getStateKey() !== "") {
- return;
- }
-
- const evContent = powerLevelEvent.getDirectionalContent();
-
- let maxLevel = evContent.users_default || 0;
- const users: { [userId: string]: number } = evContent.users || {};
- Object.values(users).forEach((lvl: number) => {
- 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}
- */
- public setTypingEvent(event: MatrixEvent): void {
- 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.
- */
- private updateModifiedTime(): void {
- 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
- */
- public getLastModifiedTime(): number {
- return this.modified;
- }
-
- public isKicked(): boolean {
- 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
- */
- public getDMInviter(): string | undefined {
- // 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: string | undefined = 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.
- */
- public getAvatarUrl(
- baseUrl: string,
- width: number,
- height: number,
- resizeMethod: string,
- allowDefault = true,
- allowDirectLinks: boolean,
- ): string | null {
- const rawUrl = this.getMxcAvatarUrl();
-
- if (!rawUrl && !allowDefault) {
- return null;
- }
- const httpUrl = 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
- */
- public getMxcAvatarUrl(): string | undefined {
- if (this.events.member) {
- return this.events.member.getDirectionalContent().avatar_url;
- } else if (this.user) {
- return this.user.avatarUrl;
- }
- }
-}
-
-const MXID_PATTERN = /@.+:.+/;
-const LTR_RTL_PATTERN = /[\u200E\u200F\u202A-\u202F]/;
-
-function shouldDisambiguate(selfUserId: string, displayName?: string, roomState?: RoomState): boolean {
- 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 (!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: string, displayName: string | undefined, disambiguate: boolean): string {
- if (!displayName || displayName === selfUserId) return selfUserId;
-
- if (disambiguate) return 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 (!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 utils.removeDirectionOverrideChars(displayName);
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts
deleted file mode 100644
index f975b9c..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-state.ts
+++ /dev/null
@@ -1,1081 +0,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.
-*/
-
-import { RoomMember } from "./room-member";
-import { logger } from "../logger";
-import * as utils from "../utils";
-import { EventType, UNSTABLE_MSC2716_MARKER } from "../@types/event";
-import { IEvent, MatrixEvent, MatrixEventEvent } from "./event";
-import { MatrixClient } from "../client";
-import { GuestAccess, HistoryVisibility, IJoinRuleEventContent, JoinRule } from "../@types/partials";
-import { TypedEventEmitter } from "./typed-event-emitter";
-import { Beacon, BeaconEvent, BeaconEventHandlerMap, getBeaconInfoIdentifier, BeaconIdentifier } from "./beacon";
-import { TypedReEmitter } from "../ReEmitter";
-import { M_BEACON, M_BEACON_INFO } from "../@types/beacon";
-
-export interface IMarkerFoundOptions {
- /** Whether the timeline was empty before the marker event arrived in the
- * room. This could be happen in a variety of cases:
- * 1. From the initial sync
- * 2. It's the first state we're seeing after joining the room
- * 3. Or whether it's coming from `syncFromCache`
- *
- * A marker event refers to `UNSTABLE_MSC2716_MARKER` and indicates that
- * history was imported somewhere back in time. It specifically points to an
- * MSC2716 insertion event where the history was imported at. Marker events
- * are sent as state events so they are easily discoverable by clients and
- * homeservers and don't get lost in timeline gaps.
- */
- timelineWasEmpty?: boolean;
-}
-
-// possible statuses for out-of-band member loading
-enum OobStatus {
- NotStarted,
- InProgress,
- Finished,
-}
-
-export interface IPowerLevelsContent {
- users?: Record<string, number>;
- events?: Record<string, number>;
- // eslint-disable-next-line camelcase
- users_default?: number;
- // eslint-disable-next-line camelcase
- events_default?: number;
- // eslint-disable-next-line camelcase
- state_default?: number;
- ban?: number;
- kick?: number;
- redact?: number;
-}
-
-export enum RoomStateEvent {
- Events = "RoomState.events",
- Members = "RoomState.members",
- NewMember = "RoomState.newMember",
- Update = "RoomState.update", // signals batches of updates without specificity
- BeaconLiveness = "RoomState.BeaconLiveness",
- Marker = "RoomState.Marker",
-}
-
-export type RoomStateEventHandlerMap = {
- /**
- * Fires whenever the event dictionary in room state is updated.
- * @param event - The matrix event which caused this event to fire.
- * @param state - The room state whose RoomState.events dictionary
- * was updated.
- * @param prevEvent - The event being replaced by the new state, if
- * known. Note that this can differ from `getPrevContent()` on the new state event
- * as this is the store's view of the last state, not the previous state provided
- * by the server.
- * @example
- * ```
- * matrixClient.on("RoomState.events", function(event, state, prevEvent){
- * var newStateEvent = event;
- * });
- * ```
- */
- [RoomStateEvent.Events]: (event: MatrixEvent, state: RoomState, lastStateEvent: MatrixEvent | null) => void;
- /**
- * Fires whenever a member in the members dictionary is updated in any way.
- * @param event - The matrix event which caused this event to fire.
- * @param state - The room state whose RoomState.members dictionary
- * was updated.
- * @param member - The room member that was updated.
- * @example
- * ```
- * matrixClient.on("RoomState.members", function(event, state, member){
- * var newMembershipState = member.membership;
- * });
- * ```
- */
- [RoomStateEvent.Members]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
- /**
- * Fires whenever a member is added to the members dictionary. The RoomMember
- * will not be fully populated yet (e.g. no membership state) but will already
- * be available in the members dictionary.
- * @param event - The matrix event which caused this event to fire.
- * @param state - The room state whose RoomState.members dictionary
- * was updated with a new entry.
- * @param member - The room member that was added.
- * @example
- * ```
- * matrixClient.on("RoomState.newMember", function(event, state, member){
- * // add event listeners on 'member'
- * });
- * ```
- */
- [RoomStateEvent.NewMember]: (event: MatrixEvent, state: RoomState, member: RoomMember) => void;
- [RoomStateEvent.Update]: (state: RoomState) => void;
- [RoomStateEvent.BeaconLiveness]: (state: RoomState, hasLiveBeacons: boolean) => void;
- [RoomStateEvent.Marker]: (event: MatrixEvent, setStateOptions?: IMarkerFoundOptions) => void;
- [BeaconEvent.New]: (event: MatrixEvent, beacon: Beacon) => void;
-};
-
-type EmittedEvents = RoomStateEvent | BeaconEvent;
-type EventHandlerMap = RoomStateEventHandlerMap & BeaconEventHandlerMap;
-
-export class RoomState extends TypedEventEmitter<EmittedEvents, EventHandlerMap> {
- public readonly reEmitter = new TypedReEmitter<EmittedEvents, EventHandlerMap>(this);
- private sentinels: Record<string, RoomMember> = {}; // userId: RoomMember
- // stores fuzzy matches to a list of userIDs (applies utils.removeHiddenChars to keys)
- private displayNameToUserIds = new Map<string, string[]>();
- private userIdsToDisplayNames: Record<string, string> = {};
- private tokenToInvite: Record<string, MatrixEvent> = {}; // 3pid invite state_key to m.room.member invite
- private joinedMemberCount: number | null = 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
- private summaryJoinedMemberCount: number | null = null;
- // same for invited member count
- private invitedMemberCount: number | null = null;
- private summaryInvitedMemberCount: number | null = null;
- private modified = -1;
-
- // XXX: Should be read-only
- // The room member dictionary, keyed on the user's ID.
- public members: Record<string, RoomMember> = {}; // userId: RoomMember
- // The state events dictionary, keyed on the event type and then the state_key value.
- public events = new Map<string, Map<string, MatrixEvent>>(); // Map<eventType, Map<stateKey, MatrixEvent>>
- // The pagination token for this state.
- public paginationToken: string | null = null;
-
- public readonly beacons = new Map<BeaconIdentifier, Beacon>();
- private _liveBeaconIds: BeaconIdentifier[] = [];
-
- /**
- * 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.
- */
- public constructor(public readonly roomId: string, private oobMemberFlags = { status: OobStatus.NotStarted }) {
- super();
- 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'
- */
- public getJoinedMemberCount(): number {
- 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
- */
- public setJoinedMemberCount(count: number): void {
- this.summaryJoinedMemberCount = count;
- }
-
- /**
- * Returns the number of invited members in this room
- * @returns The number of members in this room whose membership is 'invite'
- */
- public getInvitedMemberCount(): number {
- 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
- */
- public setInvitedMemberCount(count: number): void {
- this.summaryInvitedMemberCount = count;
- }
-
- /**
- * Get all RoomMembers in this room.
- * @returns A list of RoomMembers.
- */
- public getMembers(): RoomMember[] {
- 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.
- */
- public getMembersExcept(excludedIds: string[]): RoomMember[] {
- 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.
- */
- public getMember(userId: string): RoomMember | null {
- 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.
- */
- public getSentinelMember(userId: string): RoomMember | null {
- if (!userId) return null;
- let sentinel = this.sentinels[userId];
-
- if (sentinel === undefined) {
- sentinel = new 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).
- */
- public getStateEvents(eventType: EventType | string): MatrixEvent[];
- public getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent | null;
- public getStateEvents(eventType: EventType | string, stateKey?: string): MatrixEvent[] | MatrixEvent | null {
- 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;
- }
-
- public get hasLiveBeacons(): boolean {
- return !!this.liveBeaconIds?.length;
- }
-
- public get liveBeaconIds(): BeaconIdentifier[] {
- 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
- */
- public clone(): RoomState {
- 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
- */
- public setUnknownStateEvents(events: MatrixEvent[]): void {
- 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}
- */
- public setStateEvents(stateEvents: MatrixEvent[], markerFoundOptions?: IMarkerFoundOptions): void {
- this.updateModifiedTime();
-
- // update the core event dict
- stateEvents.forEach((event) => {
- if (event.getRoomId() !== this.roomId || !event.isState()) return;
-
- if (M_BEACON_INFO.matches(event.getType())) {
- this.setBeacon(event);
- }
-
- const lastStateEvent = this.getStateEventMatching(event);
- this.setStateEvent(event);
- if (event.getType() === 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() === 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() === 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 (UNSTABLE_MSC2716_MARKER.matches(event.getType())) {
- this.emit(RoomStateEvent.Marker, event, markerFoundOptions);
- }
- });
-
- this.emit(RoomStateEvent.Update, this);
- }
-
- public async processBeaconEvents(events: MatrixEvent[], matrixClient: MatrixClient): Promise<void> {
- if (
- !events.length ||
- // discard locations if we have no beacons
- !this.beacons.size
- ) {
- return;
- }
-
- const beaconByEventIdDict = [...this.beacons.values()].reduce<Record<string, Beacon>>((dict, beacon) => {
- dict[beacon.beaconInfoId] = beacon;
- return dict;
- }, {});
-
- const processBeaconRelation = (beaconInfoEventId: string, event: MatrixEvent): void => {
- if (!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 (!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(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}
- */
- private getOrCreateMember(userId: string, event: MatrixEvent): RoomMember {
- let member = this.members[userId];
- if (!member) {
- member = new 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;
- }
-
- private setStateEvent(event: MatrixEvent): void {
- if (!this.events.has(event.getType())) {
- this.events.set(event.getType(), new Map());
- }
- this.events.get(event.getType())!.set(event.getStateKey()!, event);
- }
-
- /**
- * @experimental
- */
- private setBeacon(event: MatrixEvent): void {
- const beaconIdentifier = getBeaconInfoIdentifier(event);
-
- if (this.beacons.has(beaconIdentifier)) {
- const beacon = this.beacons.get(beaconIdentifier)!;
-
- if (event.isRedacted()) {
- if (beacon.beaconInfoId === (<IEvent>event.getRedactionEvent())?.redacts) {
- beacon.destroy();
- this.beacons.delete(beaconIdentifier);
- }
- return;
- }
-
- return beacon.update(event);
- }
-
- if (event.isRedacted()) {
- return;
- }
-
- const beacon = new Beacon(event);
-
- this.reEmitter.reEmit<BeaconEvent, BeaconEvent>(beacon, [
- BeaconEvent.New,
- BeaconEvent.Update,
- BeaconEvent.Destroy,
- BeaconEvent.LivenessChange,
- ]);
-
- this.emit(BeaconEvent.New, event, beacon);
- beacon.on(BeaconEvent.LivenessChange, this.onBeaconLivenessChange.bind(this));
- beacon.on(BeaconEvent.Destroy, this.onBeaconLivenessChange.bind(this));
-
- this.beacons.set(beacon.identifier, beacon);
- }
-
- /**
- * @experimental
- * Check liveness of room beacons
- * emit RoomStateEvent.BeaconLiveness event
- */
- private onBeaconLivenessChange(): void {
- this._liveBeaconIds = Array.from(this.beacons.values())
- .filter((beacon) => beacon.isLive)
- .map((beacon) => beacon.identifier);
-
- this.emit(RoomStateEvent.BeaconLiveness, this, this.hasLiveBeacons);
- }
-
- private getStateEventMatching(event: MatrixEvent): MatrixEvent | null {
- return this.events.get(event.getType())?.get(event.getStateKey()!) ?? null;
- }
-
- private updateMember(member: RoomMember): void {
- // this member may have a power level already, so set it.
- const pwrLvlEvent = this.getStateEvents(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
- */
- public needsOutOfBandMembers(): boolean {
- 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.
- */
- public outOfBandMembersReady(): boolean {
- 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
- */
- public markOutOfBandMembersStarted(): void {
- 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
- */
- public markOutOfBandMembersFailed(): void {
- if (this.oobMemberFlags.status !== OobStatus.InProgress) {
- return;
- }
- this.oobMemberFlags.status = OobStatus.NotStarted;
- }
-
- /**
- * Clears the loaded out-of-band members
- */
- public clearOutOfBandMembers(): void {
- let count = 0;
- Object.keys(this.members).forEach((userId) => {
- const member = this.members[userId];
- if (member.isOutOfBand()) {
- ++count;
- delete this.members[userId];
- }
- });
- 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
- */
- public setOutOfBandMembers(stateEvents: MatrixEvent[]): void {
- logger.log(`LL: RoomState about to set ${stateEvents.length} OOB members ...`);
- if (this.oobMemberFlags.status !== OobStatus.InProgress) {
- return;
- }
- 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
- */
- private setOutOfBandMember(stateEvent: MatrixEvent): void {
- if (stateEvent.getType() !== 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
- */
- public setTypingEvent(event: MatrixEvent): void {
- 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
- */
- public getInviteForThreePidToken(token: string): MatrixEvent | null {
- return this.tokenToInvite[token] || null;
- }
-
- /**
- * Update the last modified time to the current time.
- */
- private updateModifiedTime(): void {
- 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
- */
- public getLastModifiedTime(): number {
- 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.
- */
- public getUserIdsWithDisplayName(displayName: string): string[] {
- return this.displayNameToUserIds.get(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
- */
- public maySendRedactionForEvent(mxEvent: MatrixEvent, userId: string): boolean {
- 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(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
- */
- public hasSufficientPowerLevelFor(action: "ban" | "kick" | "redact", powerLevel: number): boolean {
- const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
-
- let powerLevels: IPowerLevelsContent = {};
- if (powerLevelsEvent) {
- powerLevels = powerLevelsEvent.getContent();
- }
-
- let requiredLevel = 50;
- if (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.
- */
- public maySendMessage(userId: string): boolean {
- return this.maySendEventOfType(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.
- */
- public maySendEvent(eventType: EventType | string, userId: string): boolean {
- 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.
- */
- public mayClientSendStateEvent(stateEventType: EventType | string, cli: MatrixClient): boolean {
- 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.
- */
- public maySendStateEvent(stateEventType: EventType | string, userId: string): boolean {
- 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.
- */
- private maySendEventOfType(eventType: EventType | string, userId: string, state: boolean): boolean {
- const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
-
- let powerLevels: IPowerLevelsContent;
- let eventsLevels: Record<EventType | string, number> = {};
-
- 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.
- */
- public mayTriggerNotifOfType(notifLevelKey: string, userId: string): boolean {
- const member = this.getMember(userId);
- if (!member) {
- return false;
- }
-
- const powerLevelsEvent = this.getStateEvents(EventType.RoomPowerLevels, "");
-
- let notifLevel = 50;
- if (
- powerLevelsEvent &&
- powerLevelsEvent.getContent() &&
- powerLevelsEvent.getContent().notifications &&
- 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
- */
- public getJoinRule(): JoinRule {
- const joinRuleEvent = this.getStateEvents(EventType.RoomJoinRules, "");
- const joinRuleContent: Partial<IJoinRuleEventContent> = joinRuleEvent?.getContent() ?? {};
- return joinRuleContent["join_rule"] || 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
- */
- public getHistoryVisibility(): HistoryVisibility {
- const historyVisibilityEvent = this.getStateEvents(EventType.RoomHistoryVisibility, "");
- const historyVisibilityContent = historyVisibilityEvent?.getContent() ?? {};
- return historyVisibilityContent["history_visibility"] || 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
- */
- public getGuestAccess(): GuestAccess {
- const guestAccessEvent = this.getStateEvents(EventType.RoomGuestAccess, "");
- const guestAccessContent = guestAccessEvent?.getContent() ?? {};
- return guestAccessContent["guest_access"] || 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).
- */
- public findPredecessor(
- msc3946ProcessDynamicPredecessor = false,
- ): { roomId: string; eventId?: string; viaServers?: string[] } | null {
- // Note: the tests for this function are against Room.findPredecessor,
- // which just calls through to here.
-
- if (msc3946ProcessDynamicPredecessor) {
- const predecessorEvent = this.getStateEvents(EventType.RoomPredecessor, "");
- if (predecessorEvent) {
- const content = predecessorEvent.getContent<{
- predecessor_room_id: string;
- last_known_event_id?: string;
- via_servers?: string[];
- }>();
- 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(EventType.RoomCreate, "");
- if (createEvent) {
- const predecessor = createEvent.getContent<{
- predecessor?: Partial<{
- room_id: string;
- event_id: string;
- }>;
- }>()["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;
- }
-
- private updateThirdPartyTokenCache(memberEvent: MatrixEvent): void {
- if (!memberEvent.getContent().third_party_invite) {
- return;
- }
- const token = (memberEvent.getContent().third_party_invite.signed || {}).token;
- if (!token) {
- return;
- }
- const threePidInvite = this.getStateEvents(EventType.RoomThirdPartyInvite, token);
- if (!threePidInvite) {
- return;
- }
- this.tokenToInvite[token] = memberEvent;
- }
-
- private updateDisplayNameCache(userId: string, displayName: string): void {
- 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 = 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 && 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);
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts
deleted file mode 100644
index 936ec1d..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room-summary.ts
+++ /dev/null
@@ -1,44 +0,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.
-*/
-
-export interface IRoomSummary {
- "m.heroes": string[];
- "m.joined_member_count"?: number;
- "m.invited_member_count"?: number;
-}
-
-interface IInfo {
- /** The title of the room (e.g. `m.room.name`) */
- title: string;
- /** The description of the room (e.g. `m.room.topic`) */
- desc?: string;
- /** The number of joined users. */
- numMembers?: number;
- /** The list of aliases for this room. */
- aliases?: string[];
- /** The timestamp for this room. */
- timestamp?: number;
-}
-
-/**
- * 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.
- */
-export class RoomSummary {
- public constructor(public readonly roomId: string, info?: IInfo) {}
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts
deleted file mode 100644
index 133b210..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/room.ts
+++ /dev/null
@@ -1,3487 +0,0 @@
-/*
-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.
-*/
-
-import { M_POLL_START, Optional } from "matrix-events-sdk";
-
-import {
- EventTimelineSet,
- DuplicateStrategy,
- IAddLiveEventOptions,
- EventTimelineSetHandlerMap,
-} from "./event-timeline-set";
-import { Direction, EventTimeline } from "./event-timeline";
-import { getHttpUriForMxc } from "../content-repo";
-import * as utils from "../utils";
-import { normalize, noUnsafeEventProps } from "../utils";
-import { IEvent, IThreadBundledRelationship, MatrixEvent, MatrixEventEvent, MatrixEventHandlerMap } from "./event";
-import { EventStatus } from "./event-status";
-import { RoomMember } from "./room-member";
-import { IRoomSummary, RoomSummary } from "./room-summary";
-import { logger } from "../logger";
-import { TypedReEmitter } from "../ReEmitter";
-import {
- EventType,
- RoomCreateTypeField,
- RoomType,
- UNSTABLE_ELEMENT_FUNCTIONAL_USERS,
- EVENT_VISIBILITY_CHANGE_TYPE,
- RelationType,
-} from "../@types/event";
-import { IRoomVersionsCapability, MatrixClient, PendingEventOrdering, RoomVersionStability } from "../client";
-import { GuestAccess, HistoryVisibility, JoinRule, ResizeMethod } from "../@types/partials";
-import { Filter, IFilterDefinition } from "../filter";
-import { RoomState, RoomStateEvent, RoomStateEventHandlerMap } from "./room-state";
-import { BeaconEvent, BeaconEventHandlerMap } from "./beacon";
-import {
- Thread,
- ThreadEvent,
- EventHandlerMap as ThreadHandlerMap,
- FILTER_RELATED_BY_REL_TYPES,
- THREAD_RELATION_TYPE,
- FILTER_RELATED_BY_SENDERS,
- ThreadFilterType,
-} from "./thread";
-import {
- CachedReceiptStructure,
- MAIN_ROOM_TIMELINE,
- Receipt,
- ReceiptContent,
- ReceiptType,
-} from "../@types/read_receipts";
-import { IStateEventWithRoomId } from "../@types/search";
-import { RelationsContainer } from "./relations-container";
-import { ReadReceipt, synthesizeReceipt } from "./read-receipt";
-import { Poll, PollEvent } from "./poll";
-
-// 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.
-export const KNOWN_SAFE_ROOM_VERSION = "9";
-const SAFE_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
-
-interface IOpts {
- /**
- * Controls where pending messages appear in a room's timeline.
- * If "<b>chronological</b>", messages will appear in the timeline when the call to `sendEvent` was made.
- * If "<b>detached</b>", pending messages will appear in a separate list,
- * accessible via {@link Room#getPendingEvents}.
- * Default: "chronological".
- */
- pendingEventOrdering?: PendingEventOrdering;
- /**
- * Set to true to enable improved timeline support.
- */
- timelineSupport?: boolean;
- lazyLoadMembers?: boolean;
-}
-
-export interface IRecommendedVersion {
- version: string;
- needsUpgrade: boolean;
- urgent: boolean;
-}
-
-// 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;
-
-export type NotificationCount = Partial<Record<NotificationCountType, number>>;
-
-export enum NotificationCountType {
- Highlight = "highlight",
- Total = "total",
-}
-
-export interface ICreateFilterOpts {
- // Populate the filtered timeline with already loaded events in the room
- // timeline. Useful to disable for some filters that can't be achieved by the
- // client in an efficient manner
- prepopulateTimeline?: boolean;
- useSyncEvents?: boolean;
- pendingEvents?: boolean;
-}
-
-export enum RoomEvent {
- MyMembership = "Room.myMembership",
- Tags = "Room.tags",
- AccountData = "Room.accountData",
- Receipt = "Room.receipt",
- Name = "Room.name",
- Redaction = "Room.redaction",
- RedactionCancelled = "Room.redactionCancelled",
- LocalEchoUpdated = "Room.localEchoUpdated",
- Timeline = "Room.timeline",
- TimelineReset = "Room.timelineReset",
- TimelineRefresh = "Room.TimelineRefresh",
- OldStateUpdated = "Room.OldStateUpdated",
- CurrentStateUpdated = "Room.CurrentStateUpdated",
- HistoryImportedWithinTimeline = "Room.historyImportedWithinTimeline",
- UnreadNotifications = "Room.UnreadNotifications",
-}
-
-export type RoomEmittedEvents =
- | RoomEvent
- | RoomStateEvent.Events
- | RoomStateEvent.Members
- | RoomStateEvent.NewMember
- | RoomStateEvent.Update
- | RoomStateEvent.Marker
- | ThreadEvent.New
- | ThreadEvent.Update
- | ThreadEvent.NewReply
- | ThreadEvent.Delete
- | MatrixEventEvent.BeforeRedaction
- | BeaconEvent.New
- | BeaconEvent.Update
- | BeaconEvent.Destroy
- | BeaconEvent.LivenessChange
- | PollEvent.New;
-
-export type RoomEventHandlerMap = {
- /**
- * Fires when the logged in user's membership in the room is updated.
- *
- * @param room - The room in which the membership has been updated
- * @param membership - The new membership value
- * @param prevMembership - The previous membership value
- */
- [RoomEvent.MyMembership]: (room: Room, membership: string, prevMembership?: string) => void;
- /**
- * Fires whenever a room's tags are updated.
- * @param event - The tags event
- * @param room - The room whose Room.tags was updated.
- * @example
- * ```
- * matrixClient.on("Room.tags", function(event, room){
- * var newTags = event.getContent().tags;
- * if (newTags["favourite"]) showStar(room);
- * });
- * ```
- */
- [RoomEvent.Tags]: (event: MatrixEvent, room: Room) => void;
- /**
- * Fires whenever a room's account_data is updated.
- * @param event - The account_data event
- * @param room - The room whose account_data was updated.
- * @param prevEvent - The event being replaced by
- * the new account data, if known.
- * @example
- * ```
- * matrixClient.on("Room.accountData", function(event, room, oldEvent){
- * if (event.getType() === "m.room.colorscheme") {
- * applyColorScheme(event.getContents());
- * }
- * });
- * ```
- */
- [RoomEvent.AccountData]: (event: MatrixEvent, room: Room, lastEvent?: MatrixEvent) => void;
- /**
- * Fires whenever a receipt is received for a room
- * @param event - The receipt event
- * @param room - The room whose receipts was updated.
- * @example
- * ```
- * matrixClient.on("Room.receipt", function(event, room){
- * var receiptContent = event.getContent();
- * });
- * ```
- */
- [RoomEvent.Receipt]: (event: MatrixEvent, room: Room) => void;
- /**
- * Fires whenever the name of a room is updated.
- * @param room - The room whose Room.name was updated.
- * @example
- * ```
- * matrixClient.on("Room.name", function(room){
- * var newName = room.name;
- * });
- * ```
- */
- [RoomEvent.Name]: (room: Room) => void;
- /**
- * Fires when an event we had previously received is redacted.
- *
- * (Note this is *not* fired when the redaction happens before we receive the
- * event).
- *
- * @param event - The matrix redaction event
- * @param room - The room containing the redacted event
- */
- [RoomEvent.Redaction]: (event: MatrixEvent, room: Room) => void;
- /**
- * Fires when an event that was previously redacted isn't anymore.
- * This happens when the redaction couldn't be sent and
- * was subsequently cancelled by the user. Redactions have a local echo
- * which is undone in this scenario.
- *
- * @param event - The matrix redaction event that was cancelled.
- * @param room - The room containing the unredacted event
- */
- [RoomEvent.RedactionCancelled]: (event: MatrixEvent, room: Room) => void;
- /**
- * Fires when the status of a transmitted event is updated.
- *
- * <p>When an event is first transmitted, a temporary copy of the event is
- * inserted into the timeline, with a temporary event id, and a status of
- * 'SENDING'.
- *
- * <p>Once the echo comes back from the server, the content of the event
- * (MatrixEvent.event) is replaced by the complete event from the homeserver,
- * thus updating its event id, as well as server-generated fields such as the
- * timestamp. Its status is set to null.
- *
- * <p>Once the /send request completes, if the remote echo has not already
- * arrived, the event is updated with a new event id and the status is set to
- * 'SENT'. The server-generated fields are of course not updated yet.
- *
- * <p>If the /send fails, In this case, the event's status is set to
- * 'NOT_SENT'. If it is later resent, the process starts again, setting the
- * status to 'SENDING'. Alternatively, the message may be cancelled, which
- * removes the event from the room, and sets the status to 'CANCELLED'.
- *
- * <p>This event is raised to reflect each of the transitions above.
- *
- * @param event - The matrix event which has been updated
- *
- * @param room - The room containing the redacted event
- *
- * @param oldEventId - The previous event id (the temporary event id,
- * except when updating a successfully-sent event when its echo arrives)
- *
- * @param oldStatus - The previous event status.
- */
- [RoomEvent.LocalEchoUpdated]: (
- event: MatrixEvent,
- room: Room,
- oldEventId?: string,
- oldStatus?: EventStatus | null,
- ) => void;
- [RoomEvent.OldStateUpdated]: (room: Room, previousRoomState: RoomState, roomState: RoomState) => void;
- [RoomEvent.CurrentStateUpdated]: (room: Room, previousRoomState: RoomState, roomState: RoomState) => void;
- [RoomEvent.HistoryImportedWithinTimeline]: (markerEvent: MatrixEvent, room: Room) => void;
- [RoomEvent.UnreadNotifications]: (unreadNotifications?: NotificationCount, threadId?: string) => void;
- [RoomEvent.TimelineRefresh]: (room: Room, eventTimelineSet: EventTimelineSet) => void;
- [ThreadEvent.New]: (thread: Thread, toStartOfTimeline: boolean) => void;
- /**
- * Fires when a new poll instance is added to the room state
- * @param poll - the new poll
- */
- [PollEvent.New]: (poll: Poll) => void;
-} & Pick<ThreadHandlerMap, ThreadEvent.Update | ThreadEvent.NewReply | ThreadEvent.Delete> &
- EventTimelineSetHandlerMap &
- Pick<MatrixEventHandlerMap, MatrixEventEvent.BeforeRedaction> &
- Pick<
- RoomStateEventHandlerMap,
- | RoomStateEvent.Events
- | RoomStateEvent.Members
- | RoomStateEvent.NewMember
- | RoomStateEvent.Update
- | RoomStateEvent.Marker
- | BeaconEvent.New
- > &
- Pick<BeaconEventHandlerMap, BeaconEvent.Update | BeaconEvent.Destroy | BeaconEvent.LivenessChange>;
-
-export class Room extends ReadReceipt<RoomEmittedEvents, RoomEventHandlerMap> {
- public readonly reEmitter: TypedReEmitter<RoomEmittedEvents, RoomEventHandlerMap>;
- private txnToEvent: Map<string, MatrixEvent> = new Map(); // Pending in-flight requests { string: MatrixEvent }
- private notificationCounts: NotificationCount = {};
- private readonly threadNotifications = new Map<string, NotificationCount>();
- public readonly cachedThreadReadReceipts = new Map<string, CachedReceiptStructure[]>();
- // Useful to know at what point the current user has started using threads in this room
- private 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
- */
- private unthreadedReceipts = new Map<string, Receipt>();
- private readonly timelineSets: EventTimelineSet[];
- public readonly polls: Map<string, Poll> = new Map<string, Poll>();
- public readonly threadsTimelineSets: EventTimelineSet[] = [];
- // any filtered timeline sets we're maintaining for this room
- private readonly filteredTimelineSets: Record<string, EventTimelineSet> = {}; // filter_id: timelineSet
- private timelineNeedsRefresh = false;
- private readonly pendingEventList?: MatrixEvent[];
- // read by megolm via getter; boolean value - null indicates "use global value"
- private blacklistUnverifiedDevices?: boolean;
- private selfMembership?: string;
- private summaryHeroes: string[] | null = null;
- // flags to stop logspam about missing m.room.create events
- private getTypeWarning = false;
- private getVersionWarning = false;
- private membersPromise?: Promise<boolean>;
-
- // XXX: These should be read-only
- /**
- * The human-readable display name for this room.
- */
- public name: string;
- /**
- * The un-homoglyphed name for this room.
- */
- public normalizedName: string;
- /**
- * 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 } }`
- */
- public tags: Record<string, Record<string, any>> = {}; // $tagName: { $metadata: $value }
- /**
- * accountData Dict of per-room account_data events; the keys are the
- * event type and the values are the events.
- */
- public accountData: Map<string, MatrixEvent> = new Map(); // $eventType: $event
- /**
- * The room summary.
- */
- public summary: RoomSummary | null = null;
- // legacy fields
- /**
- * The live event timeline for this room, with the oldest event at index 0.
- * Present for backwards compatibility - prefer getLiveTimeline().getEvents()
- */
- public timeline!: MatrixEvent[];
- /**
- * oldState The state of the room at the time of the oldest
- * event in the live timeline. Present for backwards compatibility -
- * prefer getLiveTimeline().getState(EventTimeline.BACKWARDS).
- */
- public oldState!: RoomState;
- /**
- * currentState The state of the room at the time of the
- * newest event in the timeline. Present for backwards compatibility -
- * prefer getLiveTimeline().getState(EventTimeline.FORWARDS).
- */
- public currentState!: RoomState;
- public readonly relations = new 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
- */
- private threads = new Map<string, Thread>();
- public lastThread?: Thread;
-
- /**
- * 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
- */
- private visibilityEvents = new Map<string, MatrixEvent[]>();
-
- /**
- * 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
- */
- public constructor(
- public readonly roomId: string,
- public readonly client: MatrixClient,
- public readonly myUserId: string,
- private readonly opts: IOpts = {},
- ) {
- 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.setMaxListeners(100);
- this.reEmitter = new TypedReEmitter(this);
-
- opts.pendingEventOrdering = opts.pendingEventOrdering || 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(this, opts)];
- this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [RoomEvent.Timeline, RoomEvent.TimelineReset]);
-
- this.fixUpLegacyTimelineFields();
-
- if (this.opts.pendingEventOrdering === 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: Partial<IEvent>) => {
- const event = mapper(serializedEvent);
- await client.decryptEventIfNeeded(event);
- event.setStatus(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;
- }
- }
-
- private threadTimelineSetsPromise: Promise<[EventTimelineSet, EventTimelineSet]> | null = null;
- public async createThreadsTimelineSets(): Promise<[EventTimelineSet, EventTimelineSet] | null> {
- if (this.threadTimelineSetsPromise) {
- return this.threadTimelineSetsPromise;
- }
-
- if (this.client?.supportsThreads()) {
- try {
- this.threadTimelineSetsPromise = Promise.all([
- this.createThreadTimelineSet(),
- this.createThreadTimelineSet(ThreadFilterType.My),
- ]);
- const timelineSets = await this.threadTimelineSetsPromise;
- this.threadsTimelineSets.push(...timelineSets);
- 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
- */
- public async decryptCriticalEvents(): Promise<void> {
- 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
- */
- public async decryptAllEvents(): Promise<void> {
- 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
- */
- public getCreator(): string | null {
- const createEvent = this.currentState.getStateEvents(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
- */
- public getVersion(): string {
- const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
- if (!createEvent) {
- if (!this.getVersionWarning) {
- 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
- */
- public shouldUpgradeToVersion(): string | null {
- // 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.
- */
- public async getRecommendedVersion(): Promise<IRecommendedVersion> {
- 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] = 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.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.warn("No room version capability - assuming upgrade required.");
- return result;
- } else {
- result = this.checkVersionAgainstCapability(versionCap);
- }
- }
-
- return result;
- }
-
- private checkVersionAgainstCapability(versionCap: IRoomVersionsCapability): IRecommendedVersion {
- const currentVersion = this.getVersion();
- logger.log(`[${this.roomId}] Current version: ${currentVersion}`);
- logger.log(`[${this.roomId}] Version capability: `, versionCap);
-
- const result: IRecommendedVersion = {
- 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.warn(`URGENT upgrade required on ${this.roomId}`);
- } else {
- 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
- */
- public userMayUpgradeRoom(userId: string): boolean {
- return this.currentState.maySendStateEvent(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'
- */
- public getPendingEvents(): MatrixEvent[] {
- 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.
- */
- public removePendingEvent(eventId: string): boolean {
- if (!this.pendingEventList) {
- throw new Error(
- "Cannot call removePendingEvent with pendingEventOrdering == " + this.opts.pendingEventOrdering,
- );
- }
-
- const removed = 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.
- */
- public hasPendingEvent(eventId: string): boolean {
- 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.
- */
- public getPendingEvent(eventId: string): MatrixEvent | null {
- return this.pendingEventList?.find((event) => event.getId() === eventId) ?? null;
- }
-
- /**
- * Get the live unfiltered timeline for this room.
- *
- * @returns live timeline
- */
- public getLiveTimeline(): EventTimeline {
- return this.getUnfilteredTimelineSet().getLiveTimeline();
- }
-
- /**
- * Get the timestamp of the last message in the room
- *
- * @returns the timestamp of the last message in the room
- */
- public getLastActiveTimestamp(): number {
- 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 membership type (join | leave | invite) for the logged in user
- */
- public getMyMembership(): string {
- 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
- */
- public getDMInviter(): string | undefined {
- 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)
- */
- public guessDMUserId(): string {
- 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;
- }
-
- public getAvatarFallbackMember(): RoomMember | undefined {
- 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(this.roomId, availableUser.userId);
- member.user = availableUser;
- return member;
- }
- }
- }
-
- /**
- * Sets the membership this room was received as during sync
- * @param membership - join | leave | invite
- */
- public updateMyMembership(membership: string): void {
- const prevMembership = this.selfMembership;
- this.selfMembership = membership;
- if (prevMembership !== membership) {
- if (membership === "leave") {
- this.cleanupAfterLeaving();
- }
- this.emit(RoomEvent.MyMembership, this, membership, prevMembership);
- }
- }
-
- private async loadMembersFromServer(): Promise<IStateEventWithRoomId[]> {
- const lastSyncToken = this.client.store.getSyncToken();
- const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken ?? undefined);
- return response.chunk;
- }
-
- private async loadMembers(): Promise<{ memberEvents: MatrixEvent[]; fromServer: boolean }> {
- // 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.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`);
- }
- const memberEvents = rawMembersEvents.filter(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.
- */
- public membersLoaded(): boolean {
- 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
- */
- public loadMembersIfNeeded(): Promise<boolean> {
- 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 as IStateEventWithRoomId);
- 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.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.error(err);
- });
-
- this.membersPromise = inMemoryUpdate;
-
- return this.membersPromise;
- }
-
- /**
- * Removes the lazily loaded members from storage if needed
- */
- public async clearLoadedMembersIfNeeded(): Promise<void> {
- 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.
- */
- private cleanupAfterLeaving(): void {
- this.clearLoadedMembersIfNeeded().catch((err) => {
- logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`);
- 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()`.
- */
- public async refreshLiveTimeline(): Promise<void> {
- const liveTimelineBefore = this.getLiveTimeline();
- const forwardPaginationToken = liveTimelineBefore.getPaginationToken(EventTimeline.FORWARDS);
- const backwardPaginationToken = liveTimelineBefore.getPaginationToken(EventTimeline.BACKWARDS);
- const eventsBefore = liveTimelineBefore.getEvents();
- const mostRecentEventInTimeline = eventsBefore[eventsBefore.length - 1];
- 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: Optional<EventTimeline>;
- // 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(Direction.Forward) === null &&
- liveTimeline.getPaginationToken(Direction.Backward) === null &&
- liveTimeline.getEvents().length === 0)
- ) {
- 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.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.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.
- */
- public resetLiveTimeline(backPaginationToken?: string | null, forwardPaginationToken?: string | null): void {
- 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
- */
- private fixUpLegacyTimelineFields(): void {
- 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.BACKWARDS)!;
- this.currentState = this.getLiveTimeline().getState(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, [
- RoomStateEvent.Events,
- RoomStateEvent.Members,
- RoomStateEvent.NewMember,
- RoomStateEvent.Update,
- RoomStateEvent.Marker,
- BeaconEvent.New,
- BeaconEvent.Update,
- BeaconEvent.Destroy,
- BeaconEvent.LivenessChange,
- ]);
- this.reEmitter.reEmit(this.currentState, [
- RoomStateEvent.Events,
- RoomStateEvent.Members,
- RoomStateEvent.NewMember,
- RoomStateEvent.Update,
- RoomStateEvent.Marker,
- BeaconEvent.New,
- BeaconEvent.Update,
- BeaconEvent.Destroy,
- 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
- */
- public async hasUnverifiedDevices(): Promise<boolean> {
- 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
- */
- public getTimelineSets(): EventTimelineSet[] {
- return this.timelineSets;
- }
-
- /**
- * Helper to return the main unfiltered timeline set for this room
- * @returns room's unfiltered timeline set
- */
- public getUnfilteredTimelineSet(): EventTimelineSet {
- 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
- */
- public getTimelineForEvent(eventId: string): EventTimeline | null {
- 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
- */
- public addTimeline(): EventTimeline {
- 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
- */
- public setTimelineNeedsRefresh(value: boolean): void {
- this.timelineNeedsRefresh = value;
- }
-
- /**
- * Whether the timeline needs to be refreshed in order to pull in new
- * historical messages that were imported.
- * @returns .
- */
- public getTimelineNeedsRefresh(): boolean {
- 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
- */
- public findEventById(eventId: string): MatrixEvent | undefined {
- 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.
- */
- public getUnreadNotificationCount(type = NotificationCountType.Total): number {
- 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)
- */
- public getUnreadCountForEventContext(type = NotificationCountType.Total, event: MatrixEvent): number {
- 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.
- */
- public getRoomUnreadNotificationCount(type = NotificationCountType.Total): number {
- 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.
- */
- public getThreadUnreadNotificationCount(threadId: string, type = NotificationCountType.Total): number {
- return this.threadNotifications.get(threadId)?.[type] ?? 0;
- }
-
- /**
- * Checks if the current room has unread thread notifications
- * @returns
- */
- public hasThreadUnreadNotification(): boolean {
- 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
- */
- public setThreadUnreadNotificationCount(threadId: string, type: NotificationCountType, count: number): void {
- const notification: NotificationCount = {
- 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
- */
- public get threadsAggregateNotificationType(): NotificationCountType | null {
- let type: NotificationCountType | null = 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
- */
- public resetThreadUnreadNotificationCount(notificationsToKeep?: string[]): void {
- 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
- */
- public setUnreadNotificationCount(type: NotificationCountType, count: number): void {
- this.notificationCounts[type] = count;
- this.emit(RoomEvent.UnreadNotifications, this.notificationCounts);
- }
-
- public setUnread(type: NotificationCountType, count: number): void {
- return this.setUnreadNotificationCount(type, count);
- }
-
- public setSummary(summary: IRoomSummary): void {
- 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.
- */
- public setBlacklistUnverifiedDevices(value: boolean): void {
- 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.
- */
- public getBlacklistUnverifiedDevices(): boolean | null {
- 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.
- */
- public getAvatarUrl(
- baseUrl: string,
- width: number,
- height: number,
- resizeMethod: ResizeMethod,
- allowDefault = true,
- ): string | null {
- const roomAvatarEvent = this.currentState.getStateEvents(EventType.RoomAvatar, "");
- if (!roomAvatarEvent && !allowDefault) {
- return null;
- }
-
- const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null;
- if (mainUrl) {
- return 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
- */
- public getMxcAvatarUrl(): string | null {
- return this.currentState.getStateEvents(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
- */
- public getCanonicalAlias(): string | null {
- const canonicalAlias = this.currentState.getStateEvents(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
- */
- public getAltAliases(): string[] {
- const canonicalAlias = this.currentState.getStateEvents(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}
- */
- public addEventsToTimeline(
- events: MatrixEvent[],
- toStartOfTimeline: boolean,
- timeline: EventTimeline,
- paginationToken?: string,
- ): void {
- 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
- */
- public getThread(eventId: string): Thread | null {
- return this.threads.get(eventId) ?? null;
- }
-
- /**
- * Get all the known threads in the room
- */
- public getThreads(): Thread[] {
- 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`.
- */
- public getMember(userId: string): RoomMember | null {
- return this.currentState.getMember(userId);
- }
-
- /**
- * Get all currently loaded members from the current
- * room state.
- * @returns Room members
- */
- public getMembers(): RoomMember[] {
- return this.currentState.getMembers();
- }
-
- /**
- * Get a list of members whose membership state is "join".
- * @returns A list of currently joined members.
- */
- public getJoinedMembers(): RoomMember[] {
- 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'
- */
- public getJoinedMemberCount(): number {
- 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'
- */
- public getInvitedMemberCount(): number {
- 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'
- */
- public getInvitedAndJoinedMemberCount(): number {
- 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.
- */
- public getMembersWithMembership(membership: string): RoomMember[] {
- 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.
- */
- public async getEncryptionTargetMembers(): Promise<RoomMember[]> {
- 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
- */
- public shouldEncryptForInvitedMembers(): boolean {
- const ev = this.currentState.getStateEvents(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
- */
- public getDefaultRoomName(userId: string): string {
- 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.
- */
- public hasMembershipState(userId: string, membership: string): boolean {
- 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
- */
- public getOrCreateFilteredTimelineSet(
- filter: Filter,
- { prepopulateTimeline = true, useSyncEvents = true, pendingEvents = true }: ICreateFilterOpts = {},
- ): EventTimelineSet {
- if (this.filteredTimelineSets[filter.filterId!]) {
- return this.filteredTimelineSets[filter.filterId!];
- }
- const opts = Object.assign({ filter, pendingEvents }, this.opts);
- const timelineSet = new 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.BACKWARDS)) {
- timeline = timeline.getNeighbouringTimeline(EventTimeline.BACKWARDS)!;
- }
-
- timelineSet
- .getLiveTimeline()
- .setPaginationToken(timeline.getPaginationToken(EventTimeline.BACKWARDS), EventTimeline.BACKWARDS);
- } else if (useSyncEvents) {
- const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(Direction.Forward);
- timelineSet.getLiveTimeline().setPaginationToken(livePaginationToken, 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;
- }
-
- private async getThreadListFilter(filterType = ThreadFilterType.All): Promise<Filter> {
- const myUserId = this.client.getUserId()!;
- const filter = new Filter(myUserId);
-
- const definition: IFilterDefinition = {
- room: {
- timeline: {
- [FILTER_RELATED_BY_REL_TYPES.name]: [THREAD_RELATION_TYPE.name],
- },
- },
- };
-
- if (filterType === ThreadFilterType.My) {
- definition!.room!.timeline![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;
- }
-
- private async createThreadTimelineSet(filterType?: ThreadFilterType): Promise<EventTimelineSet> {
- let timelineSet: EventTimelineSet;
- if (Thread.hasServerSideListSupport) {
- timelineSet = new EventTimelineSet(
- this,
- {
- ...this.opts,
- pendingEvents: false,
- },
- undefined,
- undefined,
- filterType ?? ThreadFilterType.All,
- );
- this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
- } else if (Thread.hasServerSideSupport) {
- const filter = await this.getThreadListFilter(filterType);
-
- timelineSet = this.getOrCreateFilteredTimelineSet(filter, {
- prepopulateTimeline: false,
- useSyncEvents: false,
- pendingEvents: false,
- });
- } else {
- timelineSet = new 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 !== ThreadFilterType.My || currentUserParticipated) {
- timelineSet.getLiveTimeline().addEvent(thread.rootEvent!, {
- toStartOfTimeline: false,
- });
- }
- });
- }
-
- return timelineSet;
- }
-
- private threadsReady = false;
-
- /**
- * Takes the given thread root events and creates threads for them.
- */
- public processThreadRoots(events: MatrixEvent[], toStartOfTimeline: boolean): void {
- for (const rootEvent of events) {
- 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.
- */
- public async fetchRoomThreads(): Promise<void> {
- if (this.threadsReady || !this.client.supportsThreads()) {
- return;
- }
-
- if (Thread.hasServerSideListSupport) {
- await Promise.all([
- this.fetchRoomThreadList(ThreadFilterType.All),
- this.fetchRoomThreadList(ThreadFilterType.My),
- ]);
- } else {
- const allThreadsFilter = await this.getThreadListFilter();
-
- const { chunk: events } = await this.client.createMessagesRequest(
- this.roomId,
- "",
- Number.MAX_SAFE_INTEGER,
- 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<IThreadBundledRelationship>(
- THREAD_RELATION_TYPE.name,
- )!;
- const threadBMetadata = eventB.getServerAggregatedRelation<IThreadBundledRelationship>(
- THREAD_RELATION_TYPE.name,
- )!;
- return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts;
- });
-
- let latestMyThreadsRootEvent: MatrixEvent | undefined;
- const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
- for (const rootEvent of threadRoots) {
- const opts = {
- duplicateStrategy: DuplicateStrategy.Ignore,
- fromCache: false,
- roomState,
- };
- this.threadsTimelineSets[0]?.addLiveEvent(rootEvent, opts);
-
- const threadRelationship = rootEvent.getServerAggregatedRelation<IThreadBundledRelationship>(
- 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(ThreadEvent.NewReply, this.onThreadNewReply);
- this.on(ThreadEvent.Delete, this.onThreadDelete);
- this.threadsReady = true;
- }
-
- public async processPollEvents(events: MatrixEvent[]): Promise<void> {
- const processPollStartEvent = (event: MatrixEvent): void => {
- if (!M_POLL_START.matches(event.getType())) return;
- try {
- const poll = new Poll(event, this.client, this);
- this.polls.set(event.getId()!, poll);
- this.emit(PollEvent.New, poll);
- } catch {}
- // poll creation can fail for malformed poll start events
- };
-
- const processPollRelationEvent = (event: MatrixEvent): void => {
- const relationEventId = event.relationEventId;
- if (relationEventId && this.polls.has(relationEventId)) {
- const poll = this.polls.get(relationEventId);
- poll?.onNewRelation(event);
- }
- };
-
- const processPollEvent = (event: MatrixEvent): void => {
- processPollStartEvent(event);
- processPollRelationEvent(event);
- };
-
- for (const event of events) {
- try {
- await this.client.decryptEventIfNeeded(event);
- processPollEvent(event);
- } catch {}
- }
- }
-
- /**
- * Fetch a single page of threadlist messages for the specific thread filter
- * @internal
- */
- private async fetchRoomThreadList(filter?: ThreadFilterType): Promise<void> {
- const timelineSet = filter === ThreadFilterType.My ? this.threadsTimelineSets[1] : this.threadsTimelineSets[0];
-
- const { chunk: events, end } = await this.client.createThreadListMessagesRequest(
- this.roomId,
- null,
- undefined,
- Direction.Backward,
- timelineSet.threadListType,
- timelineSet.getFilter(),
- );
-
- timelineSet.getLiveTimeline().setPaginationToken(end ?? null, Direction.Backward);
-
- if (!events.length) return;
-
- const matrixEvents = events.map(this.client.getEventMapper());
- this.processThreadRoots(matrixEvents, true);
- const roomState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
- for (const rootEvent of matrixEvents) {
- timelineSet.addLiveEvent(rootEvent, {
- duplicateStrategy: DuplicateStrategy.Replace,
- fromCache: false,
- roomState,
- });
- }
- }
-
- private onThreadNewReply(thread: Thread): void {
- this.updateThreadRootEvents(thread, false, true);
- }
-
- private onThreadDelete(thread: Thread): void {
- 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.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
- */
- public removeFilteredTimelineSet(filter: Filter): void {
- 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);
- }
- }
-
- public eventShouldLiveIn(
- event: MatrixEvent,
- events?: MatrixEvent[],
- roots?: Set<string>,
- ): {
- shouldLiveInRoom: boolean;
- shouldLiveInThread: boolean;
- threadId?: string;
- } {
- 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_RELATION_TYPE.name)) {
- return {
- shouldLiveInRoom: false,
- shouldLiveInThread: true,
- threadId: event.threadRootId,
- };
- }
-
- const parentEventId = event.getAssociatedId();
- let parentEvent: MatrixEvent | undefined;
- 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);
- }
-
- // 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,
- };
- }
-
- // We've exhausted all scenarios, can safely assume that this event should live in the room timeline only
- return {
- shouldLiveInRoom: true,
- shouldLiveInThread: false,
- };
- }
-
- public findThreadForEvent(event?: MatrixEvent): Thread | null {
- if (!event) return null;
-
- const { threadId } = this.eventShouldLiveIn(event);
- return threadId ? this.getThread(threadId) : null;
- }
-
- private addThreadedEvents(threadId: string, events: MatrixEvent[], toStartOfTimeline = false): void {
- let thread = this.getThread(threadId);
-
- if (!thread) {
- const rootEvent = this.findEventById(threadId) ?? events.find((e) => e.getId() === threadId);
- thread = this.createThread(threadId, rootEvent, events, toStartOfTimeline);
- }
-
- thread.addEvents(events, toStartOfTimeline);
- }
-
- /**
- * Adds events to a thread's timeline. Will fire "Thread.update"
- */
- public processThreadedEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
- events.forEach(this.applyRedaction);
-
- const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
- 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),
- );
- }
-
- private updateThreadRootEvents = (thread: Thread, toStartOfTimeline: boolean, recreateEvent: boolean): void => {
- if (thread.length) {
- this.updateThreadRootEvent(this.threadsTimelineSets?.[0], thread, toStartOfTimeline, recreateEvent);
- if (thread.hasCurrentUserParticipated) {
- this.updateThreadRootEvent(this.threadsTimelineSets?.[1], thread, toStartOfTimeline, recreateEvent);
- }
- }
- };
-
- private updateThreadRootEvent = (
- timelineSet: Optional<EventTimelineSet>,
- thread: Thread,
- toStartOfTimeline: boolean,
- recreateEvent: boolean,
- ): void => {
- if (timelineSet && thread.rootEvent) {
- if (recreateEvent) {
- timelineSet.removeEvent(thread.id);
- }
- if (Thread.hasServerSideSupport) {
- timelineSet.addLiveEvent(thread.rootEvent, {
- duplicateStrategy: DuplicateStrategy.Replace,
- fromCache: false,
- roomState: this.currentState,
- });
- } else {
- timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { toStartOfTimeline });
- }
- }
- };
-
- public createThread(
- threadId: string,
- rootEvent: MatrixEvent | undefined,
- events: MatrixEvent[] = [],
- toStartOfTimeline: boolean,
- ): Thread {
- 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(RelationType.Replace)));
- }
- }
-
- const thread = new 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, [
- ThreadEvent.Delete,
- ThreadEvent.Update,
- 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(ThreadEvent.New, thread, toStartOfTimeline);
-
- return thread;
- }
-
- private applyRedaction = (event: MatrixEvent): void => {
- 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.
- }
- };
-
- private processLiveEvent(event: MatrixEvent): void {
- 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.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}
- */
- private addLiveEvent(event: MatrixEvent, addLiveEventOptions: IAddLiveEventOptions): void {
- 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() !== EventType.RoomRedaction) {
- this.addReceipt(synthesizeReceipt(event.sender.userId, event, 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}
- */
- public addPendingEvent(event: MatrixEvent, txnId: string): void {
- if (event.status !== EventStatus.SENDING && event.status !== 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.setEventMetadata(event, this.getLiveTimeline().getState(EventTimeline.FORWARDS)!, false);
-
- this.txnToEvent.set(txnId, event);
- if (this.pendingEventList) {
- if (this.pendingEventList.some((e) => e.status === EventStatus.NOT_SENT)) {
- logger.warn("Setting event as NOT_SENT due to messages in the same state");
- event.setStatus(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
- */
- private savePendingEvents(): void {
- if (this.pendingEventList) {
- const pendingEvents = this.pendingEventList
- .map((event) => {
- return {
- ...event.event,
- txn_id: event.getTxnId(),
- };
- })
- .filter((event) => {
- // Filter out the unencrypted messages if the room is encrypted
- const isEventEncrypted = event.type === 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.
- */
- private aggregateNonLiveRelation(event: MatrixEvent): void {
- this.relations.aggregateChildEvent(event);
- }
-
- public getEventForTxnId(txnId: string): MatrixEvent | undefined {
- 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}
- */
- public handleRemoteEcho(remoteEvent: MatrixEvent, localEvent: MatrixEvent): void {
- const oldEventId = localEvent.getId()!;
- const newEventId = remoteEvent.getId()!;
- const oldStatus = localEvent.status;
-
- 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}
- */
- public updatePendingEvent(event: MatrixEvent, newStatus: EventStatus, newEventId?: string): void {
- 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.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.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.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.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);
- }
-
- private revertRedactionLocalEcho(redactionEvent: MatrixEvent): void {
- 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'.
- */
- public addLiveEvents(events: MatrixEvent[], addLiveEventOptions?: IAddLiveEventOptions): void;
- /**
- * @deprecated In favor of the overload with `IAddLiveEventOptions`
- */
- public addLiveEvents(events: MatrixEvent[], duplicateStrategy?: DuplicateStrategy, fromCache?: boolean): void;
- public addLiveEvents(
- events: MatrixEvent[],
- duplicateStrategyOrOpts?: DuplicateStrategy | IAddLiveEventOptions,
- fromCache = false,
- ): void {
- let duplicateStrategy: DuplicateStrategy | undefined = duplicateStrategyOrOpts as DuplicateStrategy;
- let timelineWasEmpty: boolean | undefined = 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.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.FORWARDS)) {
- throw new Error(
- "live timeline " +
- i +
- " is no longer live - it has a pagination token " +
- "(" +
- liveTimeline.getPaginationToken(EventTimeline.FORWARDS) +
- ")",
- );
- }
- if (liveTimeline.getNeighbouringTimeline(EventTimeline.FORWARDS)) {
- throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`);
- }
- }
-
- const threadRoots = this.findThreadRoots(events);
- const eventsByThread: { [threadId: string]: MatrixEvent[] } = {};
-
- const options: IAddLiveEventOptions = {
- duplicateStrategy,
- fromCache,
- timelineWasEmpty,
- };
-
- 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
- }
- }
-
- const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
- event,
- events,
- threadRoots,
- );
-
- if (shouldLiveInThread && !eventsByThread[threadId ?? ""]) {
- eventsByThread[threadId ?? ""] = [];
- }
- eventsByThread[threadId ?? ""]?.push(event);
-
- if (shouldLiveInRoom) {
- this.addLiveEvent(event, options);
- }
- }
-
- Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => {
- this.addThreadedEvents(threadId, threadEvents, false);
- });
- }
-
- public partitionThreadedEvents(
- events: MatrixEvent[],
- ): [timelineEvents: MatrixEvent[], threadedEvents: MatrixEvent[]] {
- // Indices to the events array, for readability
- const ROOM = 0;
- const THREAD = 1;
- if (this.client.supportsThreads()) {
- const threadRoots = this.findThreadRoots(events);
- return events.reduce(
- (memo, event: MatrixEvent) => {
- const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(
- event,
- events,
- threadRoots,
- );
-
- if (shouldLiveInRoom) {
- memo[ROOM].push(event);
- }
-
- if (shouldLiveInThread) {
- event.setThreadId(threadId ?? "");
- memo[THREAD].push(event);
- }
-
- return memo;
- },
- [[] as MatrixEvent[], [] as MatrixEvent[]],
- );
- } else {
- // When `experimentalThreadSupport` is disabled treat all events as timelineEvents
- return [events as MatrixEvent[], [] as MatrixEvent[]];
- }
- }
-
- /**
- * Given some events, find the IDs of all the thread roots that are referred to by them.
- */
- private findThreadRoots(events: MatrixEvent[]): Set<string> {
- const threadRoots = new Set<string>();
- for (const event of events) {
- if (event.isRelation(THREAD_RELATION_TYPE.name)) {
- threadRoots.add(event.relationEventId ?? "");
- }
- }
- return threadRoots;
- }
-
- /**
- * Add a receipt event to the room.
- * @param event - The m.receipt event.
- * @param synthetic - True if this event is implicit.
- */
- public addReceipt(event: MatrixEvent, synthetic = false): void {
- const content = event.getContent<ReceiptContent>();
- Object.keys(content).forEach((eventId: string) => {
- Object.keys(content[eventId]).forEach((receiptType: ReceiptType | string) => {
- Object.keys(content[eventId][receiptType]).forEach((userId: string) => {
- const receipt = content[eventId][receiptType][userId] as Receipt;
- const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === MAIN_ROOM_TIMELINE;
- const receiptDestination: Thread | this | undefined = receiptForMainTimeline
- ? this
- : this.threads.get(receipt.thread_id ?? "");
-
- if (receiptDestination) {
- receiptDestination.addReceiptToStructure(
- eventId,
- receiptType as 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
- */
- public addEphemeralEvents(events: MatrixEvent[]): void {
- for (const event of events) {
- if (event.getType() === EventType.Typing) {
- this.currentState.setTypingEvent(event);
- } else if (event.getType() === 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.
- */
- public removeEvents(eventIds: string[]): void {
- 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
- */
- public removeEvent(eventId: string): boolean {
- 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}
- */
- public recalculate(): void {
- // set fake stripped state events if this is an invite room so logic remains
- // consistent elsewhere.
- const membershipEvent = this.currentState.getStateEvents(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 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 = normalize(this.name);
- this.summary = new 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
- */
- public addTags(event: MatrixEvent): void {
- // 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
- */
- public addAccountData(events: MatrixEvent[]): void {
- 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
- */
- public getAccountData(type: EventType | string): MatrixEvent | undefined {
- 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.
- */
- public maySendMessage(): boolean {
- return (
- this.getMyMembership() === "join" &&
- (this.client.isRoomEncrypted(this.roomId)
- ? this.currentState.maySendEvent(EventType.RoomMessageEncrypted, this.myUserId)
- : this.currentState.maySendEvent(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.
- */
- public canInvite(userId: string): boolean {
- let canInvite = this.getMyMembership() === "join";
- const powerLevelsEvent = this.currentState.getStateEvents(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
- */
- public getJoinRule(): JoinRule {
- 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
- */
- public getHistoryVisibility(): HistoryVisibility {
- 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
- */
- public getGuestAccess(): GuestAccess {
- 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.
- */
- public getType(): RoomType | string | undefined {
- const createEvent = this.currentState.getStateEvents(EventType.RoomCreate, "");
- if (!createEvent) {
- if (!this.getTypeWarning) {
- logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event");
- this.getTypeWarning = true;
- }
- return undefined;
- }
- return createEvent.getContent()[RoomCreateTypeField];
- }
-
- /**
- * Returns whether the room is a space-room as defined by MSC1772.
- * @returns true if the room's type is RoomType.Space
- */
- public isSpaceRoom(): boolean {
- return this.getType() === RoomType.Space;
- }
-
- /**
- * Returns whether the room is a call-room as defined by MSC3417.
- * @returns true if the room's type is RoomType.UnstableCall
- */
- public isCallRoom(): boolean {
- return this.getType() === RoomType.UnstableCall;
- }
-
- /**
- * Returns whether the room is a video room.
- * @returns true if the room's type is RoomType.ElementVideo
- */
- public isElementVideoRoom(): boolean {
- return this.getType() === 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).
- */
- public findPredecessor(
- msc3946ProcessDynamicPredecessor = false,
- ): { roomId: string; eventId?: string; viaServers?: string[] } | null {
- const currentState = this.getLiveTimeline().getState(EventTimeline.FORWARDS);
- if (!currentState) {
- return null;
- }
- return currentState.findPredecessor(msc3946ProcessDynamicPredecessor);
- }
-
- private roomNameGenerator(state: RoomNameState): string {
- 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.
- */
- private calculateRoomName(userId: string, ignoreRoomNameEvent = false): string {
- if (!ignoreRoomNameEvent) {
- // check for an alias, if any. for now, assume first alias is the
- // official one.
- const mRoomName = this.currentState.getStateEvents(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: string[] = [];
- const mFunctionalMembers = this.currentState.getStateEvents(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: string[] = [];
- 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) => 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(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: string | undefined;
- 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.
- */
- private applyNewVisibilityEvent(event: MatrixEvent): void {
- 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 =
- (EVENT_VISIBILITY_CHANGE_TYPE.name &&
- this.currentState.maySendStateEvent(EVENT_VISIBILITY_CHANGE_TYPE.name, userId)) ||
- (EVENT_VISIBILITY_CHANGE_TYPE.altName &&
- this.currentState.maySendStateEvent(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);
- }
-
- private redactVisibilityChangeEvent(event: MatrixEvent): void {
- // 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.
- */
- private applyPendingVisibilityEvents(event: MatrixEvent): void {
- 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
- */
- public getOldestThreadedReceiptTs(): number {
- 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).
- */
- public getLastUnthreadedReceiptFor(userId: string): Receipt | undefined {
- 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
- */
- public fixupNotifications(userId: string): void {
- 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
-const ALLOWED_TRANSITIONS: Record<EventStatus, EventStatus[]> = {
- [EventStatus.ENCRYPTING]: [EventStatus.SENDING, EventStatus.NOT_SENT, EventStatus.CANCELLED],
- [EventStatus.SENDING]: [EventStatus.ENCRYPTING, EventStatus.QUEUED, EventStatus.NOT_SENT, EventStatus.SENT],
- [EventStatus.QUEUED]: [EventStatus.SENDING, EventStatus.NOT_SENT, EventStatus.CANCELLED],
- [EventStatus.SENT]: [],
- [EventStatus.NOT_SENT]: [EventStatus.SENDING, EventStatus.QUEUED, EventStatus.CANCELLED],
- [EventStatus.CANCELLED]: [],
-};
-
-export enum RoomNameType {
- EmptyRoom,
- Generated,
- Actual,
-}
-
-export interface EmptyRoomNameState {
- type: RoomNameType.EmptyRoom;
- oldName?: string;
-}
-
-export interface GeneratedRoomNameState {
- type: RoomNameType.Generated;
- subtype?: "Inviting";
- names: string[];
- count: number;
-}
-
-export interface ActualRoomNameState {
- type: RoomNameType.Actual;
- name: string;
-}
-
-export type RoomNameState = EmptyRoomNameState | GeneratedRoomNameState | ActualRoomNameState;
-
-// Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn
-function memberNamesToRoomName(names: string[], count: number): string {
- 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`;
- }
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts
deleted file mode 100644
index 21192a6..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/search-result.ts
+++ /dev/null
@@ -1,54 +0,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.
-*/
-
-import { EventContext } from "./event-context";
-import { EventMapper } from "../event-mapper";
-import { IResultContext, ISearchResult } from "../@types/search";
-
-export class SearchResult {
- /**
- * Create a SearchResponse from the response to /search
- */
-
- public static fromJson(jsonObj: ISearchResult, eventMapper: EventMapper): SearchResult {
- const jsonContext = jsonObj.context || ({} as IResultContext);
- let eventsBefore = (jsonContext.events_before || []).map(eventMapper);
- let eventsAfter = (jsonContext.events_after || []).map(eventMapper);
-
- const context = new 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
- */
- public constructor(public readonly rank: number, public readonly context: EventContext) {}
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts
deleted file mode 100644
index 9a4ead3..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/thread.ts
+++ /dev/null
@@ -1,669 +0,0 @@
-/*
-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.
-*/
-
-import { Optional } from "matrix-events-sdk";
-
-import { MatrixClient, PendingEventOrdering } from "../client";
-import { TypedReEmitter } from "../ReEmitter";
-import { RelationType } from "../@types/event";
-import { IThreadBundledRelationship, MatrixEvent, MatrixEventEvent } from "./event";
-import { Direction, EventTimeline } from "./event-timeline";
-import { EventTimelineSet, EventTimelineSetHandlerMap } from "./event-timeline-set";
-import { NotificationCountType, Room, RoomEvent } from "./room";
-import { RoomState } from "./room-state";
-import { ServerControlledNamespacedValue } from "../NamespacedValue";
-import { logger } from "../logger";
-import { ReadReceipt } from "./read-receipt";
-import { CachedReceiptStructure, ReceiptType } from "../@types/read_receipts";
-
-export enum ThreadEvent {
- New = "Thread.new",
- Update = "Thread.update",
- NewReply = "Thread.newReply",
- ViewThread = "Thread.viewThread",
- Delete = "Thread.delete",
-}
-
-type EmittedEvents = Exclude<ThreadEvent, ThreadEvent.New> | RoomEvent.Timeline | RoomEvent.TimelineReset;
-
-export type EventHandlerMap = {
- [ThreadEvent.Update]: (thread: Thread) => void;
- [ThreadEvent.NewReply]: (thread: Thread, event: MatrixEvent) => void;
- [ThreadEvent.ViewThread]: () => void;
- [ThreadEvent.Delete]: (thread: Thread) => void;
-} & EventTimelineSetHandlerMap;
-
-interface IThreadOpts {
- room: Room;
- client: MatrixClient;
- pendingEventOrdering?: PendingEventOrdering;
- receipts?: CachedReceiptStructure[];
-}
-
-export enum FeatureSupport {
- None = 0,
- Experimental = 1,
- Stable = 2,
-}
-
-export function determineFeatureSupport(stable: boolean, unstable: boolean): FeatureSupport {
- if (stable) {
- return FeatureSupport.Stable;
- } else if (unstable) {
- return FeatureSupport.Experimental;
- } else {
- return FeatureSupport.None;
- }
-}
-
-export class Thread extends ReadReceipt<EmittedEvents, EventHandlerMap> {
- public static hasServerSideSupport = FeatureSupport.None;
- public static hasServerSideListSupport = FeatureSupport.None;
- public static hasServerSideFwdPaginationSupport = FeatureSupport.None;
-
- /**
- * A reference to all the events ID at the bottom of the threads
- */
- public readonly timelineSet: EventTimelineSet;
- public timeline: MatrixEvent[] = [];
-
- private _currentUserParticipated = false;
-
- private reEmitter: TypedReEmitter<EmittedEvents, EventHandlerMap>;
-
- private lastEvent: MatrixEvent | undefined;
- private replyCount = 0;
- private lastPendingEvent: MatrixEvent | undefined;
- private pendingReplyCount = 0;
-
- public readonly room: Room;
- public readonly client: MatrixClient;
- private readonly pendingEventOrdering: PendingEventOrdering;
-
- public initialEventsFetched = !Thread.hasServerSideSupport;
- /**
- * An array of events to add to the timeline once the thread has been initialised
- * with server suppport.
- */
- public replayEvents: MatrixEvent[] | null = [];
-
- public constructor(public readonly id: string, public rootEvent: MatrixEvent | undefined, opts: IThreadOpts) {
- super();
-
- 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 ?? PendingEventOrdering.Chronological;
- this.timelineSet = new EventTimelineSet(
- this.room,
- {
- timelineSupport: true,
- pendingEvents: true,
- },
- this.client,
- this,
- );
- this.reEmitter = new TypedReEmitter(this);
-
- this.reEmitter.reEmit(this.timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]);
-
- this.room.on(MatrixEventEvent.BeforeRedaction, this.onBeforeRedaction);
- this.room.on(RoomEvent.Redaction, this.onRedaction);
- this.room.on(RoomEvent.LocalEchoUpdated, this.onLocalEcho);
- this.timelineSet.on(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);
- }
-
- private async fetchRootEvent(): Promise<void> {
- 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.error("Failed to fetch thread root to construct thread with", e);
- }
- await this.processEvent(this.rootEvent);
- }
-
- public static setServerSideSupport(status: FeatureSupport): void {
- 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);
- }
- }
-
- public static setServerSideListSupport(status: FeatureSupport): void {
- Thread.hasServerSideListSupport = status;
- }
-
- public static setServerSideFwdPaginationSupport(status: FeatureSupport): void {
- Thread.hasServerSideFwdPaginationSupport = status;
- }
-
- private onBeforeRedaction = (event: MatrixEvent, redaction: MatrixEvent): void => {
- 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);
- }
- };
-
- private onRedaction = async (event: MatrixEvent): Promise<void> => {
- 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();
- }
- };
-
- private onTimelineEvent = (
- event: MatrixEvent,
- room: Room | undefined,
- toStartOfTimeline: boolean | undefined,
- ): void => {
- // Add a synthesized receipt when paginating forward in the timeline
- if (!toStartOfTimeline) {
- room!.addLocalEchoReceipt(event.getSender()!, event, ReceiptType.Read);
- }
- this.onEcho(event, toStartOfTimeline ?? false);
- };
-
- private onLocalEcho = (event: MatrixEvent): void => {
- this.onEcho(event, false);
- };
-
- private onEcho = async (event: MatrixEvent, toStartOfTimeline: boolean): Promise<void> => {
- 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);
- };
-
- public get roomState(): RoomState {
- return this.room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
- }
-
- private addEventToTimeline(event: MatrixEvent, toStartOfTimeline: boolean): void {
- if (!this.findEventById(event.getId()!)) {
- this.timelineSet.addEventToTimeline(event, this.liveTimeline, {
- toStartOfTimeline,
- fromCache: false,
- roomState: this.roomState,
- });
- this.timeline = this.events;
- }
- }
-
- public addEvents(events: MatrixEvent[], toStartOfTimeline: boolean): void {
- 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.
- */
- public async addEvent(event: MatrixEvent, toStartOfTimeline: boolean, emit = true): Promise<void> {
- 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(RelationType.Annotation) || event.isRelation(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 {
- 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();
- }
- }
-
- public async processEvent(event: Optional<MatrixEvent>): Promise<void> {
- 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
- */
- private processReceipts(receipts: CachedReceiptStructure[] = []): void {
- for (const { eventId, receiptType, userId, receipt, synthetic } of receipts) {
- this.addReceiptToStructure(eventId, receiptType as ReceiptType, userId, receipt, synthetic);
- }
- }
-
- private getRootEventBundledRelationship(rootEvent = this.rootEvent): IThreadBundledRelationship | undefined {
- return rootEvent?.getServerAggregatedRelation<IThreadBundledRelationship>(THREAD_RELATION_TYPE.name);
- }
-
- private async processRootEvent(): Promise<void> {
- 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({
- ...bundledRelationship.latest_event,
- room_id: this.roomId,
- });
- this.updatePendingReplyCount();
- await this.processEvent(this.lastEvent);
- }
- }
-
- private updatePendingReplyCount(): void {
- const unfilteredPendingEvents =
- this.pendingEventOrdering === 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.
- */
- public async resetLiveTimeline(
- backPaginationToken?: string | null,
- forwardPaginationToken?: string | null,
- ): Promise<void> {
- 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: string | undefined;
- let oldForward: string | undefined;
- if (backPaginationToken) {
- const res = await this.client.createMessagesRequest(this.roomId, backPaginationToken, 1, Direction.Forward);
- newBackward = res.end;
- }
- if (forwardPaginationToken) {
- const res = await this.client.createMessagesRequest(
- this.roomId,
- forwardPaginationToken,
- 1,
- 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(Direction.Forward) === forwardPaginationToken) {
- oldLive.setPaginationToken(oldForward ?? null, Direction.Forward);
- }
- if (backPaginationToken && newLive.getPaginationToken(Direction.Backward) === backPaginationToken) {
- newLive.setPaginationToken(newBackward ?? null, Direction.Backward);
- }
- }
-
- private async updateThreadMetadata(): Promise<void> {
- 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, 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(RoomEvent.TimelineReset, this.room, this.timelineSet, true);
- } catch (e) {
- 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
- private async fetchEditsWhereNeeded(...events: MatrixEvent[]): Promise<unknown> {
- return Promise.all(
- events
- .filter((e) => e.isEncrypted())
- .map((event: MatrixEvent) => {
- if (event.isRelation()) return; // skip - relations don't get edits
- return this.client
- .relations(this.roomId, event.getId()!, RelationType.Replace, event.getType(), {
- limit: 1,
- })
- .then((relations) => {
- if (relations.events.length) {
- event.makeReplaced(relations.events[0]);
- }
- })
- .catch((e) => {
- logger.error("Failed to load edits for encrypted thread event", e);
- });
- }),
- );
- }
-
- public setEventMetadata(event: Optional<MatrixEvent>): void {
- if (event) {
- EventTimeline.setEventMetadata(event, this.roomState, false);
- event.setThread(this);
- }
- }
-
- public clearEventMetadata(event: Optional<MatrixEvent>): void {
- if (event) {
- event.setThread(undefined);
- delete event.event?.unsigned?.["m.relations"]?.[THREAD_RELATION_TYPE.name];
- }
- }
-
- /**
- * Finds an event by ID in the current thread
- */
- public findEventById(eventId: string): MatrixEvent | undefined {
- return this.timelineSet.findEventById(eventId);
- }
-
- /**
- * Return last reply to the thread, if known.
- */
- public lastReply(matches: (ev: MatrixEvent) => boolean = (): boolean => true): MatrixEvent | null {
- for (let i = this.timeline.length - 1; i >= 0; i--) {
- const event = this.timeline[i];
- if (matches(event)) {
- return event;
- }
- }
- return null;
- }
-
- public get roomId(): string {
- 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
- */
- public get length(): number {
- 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.
- */
- public get replyToEvent(): Optional<MatrixEvent> {
- return this.lastPendingEvent ?? this.lastEvent ?? this.lastReply();
- }
-
- public get events(): MatrixEvent[] {
- return this.liveTimeline.getEvents();
- }
-
- public has(eventId: string): boolean {
- return this.timelineSet.findEventById(eventId) instanceof MatrixEvent;
- }
-
- public get hasCurrentUserParticipated(): boolean {
- return this._currentUserParticipated;
- }
-
- public get liveTimeline(): EventTimeline {
- return this.timelineSet.getLiveTimeline();
- }
-
- public getUnfilteredTimelineSet(): EventTimelineSet {
- return this.timelineSet;
- }
-
- public addReceipt(event: MatrixEvent, synthetic: boolean): void {
- 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.
- */
- public getEventReadUpTo(userId: string, ignoreSynthesized?: boolean): string | null {
- 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.
- */
- public hasUserReadEvent(userId: string, eventId: string): boolean {
- 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);
- }
-
- public setUnread(type: NotificationCountType, count: number): void {
- return this.room.setThreadUnreadNotificationCount(this.id, type, count);
- }
-}
-
-export const FILTER_RELATED_BY_SENDERS = new ServerControlledNamespacedValue(
- "related_by_senders",
- "io.element.relation_senders",
-);
-export const FILTER_RELATED_BY_REL_TYPES = new ServerControlledNamespacedValue(
- "related_by_rel_types",
- "io.element.relation_types",
-);
-export const THREAD_RELATION_TYPE = new ServerControlledNamespacedValue("m.thread", "io.element.thread");
-
-export enum ThreadFilterType {
- "My",
- "All",
-}
-
-export function threadFilterTypeToFilter(type: ThreadFilterType | null): "all" | "participated" {
- switch (type) {
- case ThreadFilterType.My:
- return "participated";
- default:
- return "all";
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts
deleted file mode 100644
index 3cfe602..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/typed-event-emitter.ts
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
-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
-import { EventEmitter } from "events";
-
-export enum EventEmitterEvents {
- NewListener = "newListener",
- RemoveListener = "removeListener",
- Error = "error",
-}
-
-type AnyListener = (...args: any) => any;
-export type ListenerMap<E extends string> = { [eventName in E]: AnyListener };
-type EventEmitterEventListener = (eventName: string, listener: AnyListener) => void;
-type EventEmitterErrorListener = (error: Error) => void;
-
-export type Listener<E extends string, A extends ListenerMap<E>, T extends E | EventEmitterEvents> = T extends E
- ? A[T]
- : T extends EventEmitterEvents
- ? EventEmitterErrorListener
- : EventEmitterEventListener;
-
-/**
- * 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.
- */
-export class TypedEventEmitter<
- Events extends string,
- Arguments extends ListenerMap<Events>,
- SuperclassArguments extends ListenerMap<any> = Arguments,
-> extends EventEmitter {
- public addListener<T extends Events | EventEmitterEvents>(
- event: T,
- listener: Listener<Events, Arguments, T>,
- ): this {
- return super.addListener(event, listener);
- }
-
- public emit<T extends Events>(event: T, ...args: Parameters<SuperclassArguments[T]>): boolean;
- public emit<T extends Events>(event: T, ...args: Parameters<Arguments[T]>): boolean;
- public emit<T extends Events>(event: T, ...args: any[]): boolean {
- return super.emit(event, ...args);
- }
-
- public eventNames(): (Events | EventEmitterEvents)[] {
- return super.eventNames() as Array<Events | EventEmitterEvents>;
- }
-
- public listenerCount(event: Events | EventEmitterEvents): number {
- return super.listenerCount(event);
- }
-
- public listeners(event: Events | EventEmitterEvents): ReturnType<EventEmitter["listeners"]> {
- return super.listeners(event);
- }
-
- public off<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this {
- return super.off(event, listener);
- }
-
- public on<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this {
- return super.on(event, listener);
- }
-
- public once<T extends Events | EventEmitterEvents>(event: T, listener: Listener<Events, Arguments, T>): this {
- return super.once(event, listener);
- }
-
- public prependListener<T extends Events | EventEmitterEvents>(
- event: T,
- listener: Listener<Events, Arguments, T>,
- ): this {
- return super.prependListener(event, listener);
- }
-
- public prependOnceListener<T extends Events | EventEmitterEvents>(
- event: T,
- listener: Listener<Events, Arguments, T>,
- ): this {
- return super.prependOnceListener(event, listener);
- }
-
- public removeAllListeners(event?: Events | EventEmitterEvents): this {
- return super.removeAllListeners(event);
- }
-
- public removeListener<T extends Events | EventEmitterEvents>(
- event: T,
- listener: Listener<Events, Arguments, T>,
- ): this {
- return super.removeListener(event, listener);
- }
-
- public rawListeners(event: Events | EventEmitterEvents): ReturnType<EventEmitter["rawListeners"]> {
- return super.rawListeners(event);
- }
-}
diff --git a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts b/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts
deleted file mode 100644
index 054a174..0000000
--- a/includes/external/matrix/node_modules/matrix-js-sdk/src/models/user.ts
+++ /dev/null
@@ -1,281 +0,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.
-*/
-
-import { MatrixEvent } from "./event";
-import { TypedEventEmitter } from "./typed-event-emitter";
-
-export enum UserEvent {
- DisplayName = "User.displayName",
- AvatarUrl = "User.avatarUrl",
- Presence = "User.presence",
- CurrentlyActive = "User.currentlyActive",
- LastPresenceTs = "User.lastPresenceTs",
-}
-
-export type UserEventHandlerMap = {
- /**
- * Fires whenever any user's display name changes.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.displayName changed.
- * @example
- * ```
- * matrixClient.on("User.displayName", function(event, user){
- * var newName = user.displayName;
- * });
- * ```
- */
- [UserEvent.DisplayName]: (event: MatrixEvent | undefined, user: User) => void;
- /**
- * Fires whenever any user's avatar URL changes.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.avatarUrl changed.
- * @example
- * ```
- * matrixClient.on("User.avatarUrl", function(event, user){
- * var newUrl = user.avatarUrl;
- * });
- * ```
- */
- [UserEvent.AvatarUrl]: (event: MatrixEvent | undefined, user: User) => void;
- /**
- * Fires whenever any user's presence changes.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.presence changed.
- * @example
- * ```
- * matrixClient.on("User.presence", function(event, user){
- * var newPresence = user.presence;
- * });
- * ```
- */
- [UserEvent.Presence]: (event: MatrixEvent | undefined, user: User) => void;
- /**
- * Fires whenever any user's currentlyActive changes.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.currentlyActive changed.
- * @example
- * ```
- * matrixClient.on("User.currentlyActive", function(event, user){
- * var newCurrentlyActive = user.currentlyActive;
- * });
- * ```
- */
- [UserEvent.CurrentlyActive]: (event: MatrixEvent | undefined, user: User) => void;
- /**
- * Fires whenever any user's lastPresenceTs changes,
- * ie. whenever any presence event is received for a user.
- * @param event - The matrix event which caused this event to fire.
- * @param user - The user whose User.lastPresenceTs changed.
- * @example
- * ```
- * matrixClient.on("User.lastPresenceTs", function(event, user){
- * var newlastPresenceTs = user.lastPresenceTs;
- * });
- * ```
- */
- [UserEvent.LastPresenceTs]: (event: MatrixEvent | undefined, user: User) => void;
-};
-
-export class User extends TypedEventEmitter<UserEvent, UserEventHandlerMap> {
- private modified = -1;
-
- /**
- * The 'displayname' of the user if known.
- * @privateRemarks
- * Should be read-only
- */
- public displayName?: string;
- public rawDisplayName?: string;
- /**
- * The 'avatar_url' of the user if known.
- * @privateRemarks
- * Should be read-only
- */
- public avatarUrl?: string;
- /**
- * The presence status message if known.
- * @privateRemarks
- * Should be read-only
- */
- public presenceStatusMsg?: string;
- /**
- * The presence enum if known.
- * @privateRemarks
- * Should be read-only
- */
- public 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
- */
- public 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
- */
- public 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
- */
- public currentlyActive = false;
- /**
- * The events describing this user.
- * @privateRemarks
- * Should be read-only
- */
- public events: {
- /** The m.presence event for this user. */
- presence?: MatrixEvent;
- profile?: MatrixEvent;
- } = {};
-
- /**
- * 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.
- */
- public constructor(public readonly userId: string) {
- super();
- 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}
- */
- public setPresenceEvent(event: MatrixEvent): void {
- if (event.getType() !== "m.presence") {
- return;
- }
- const firstFire = this.events.presence === null;
- this.events.presence = event;
-
- const eventsToFire: UserEvent[] = [];
- 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.
- */
- public setDisplayName(name: string): void {
- 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.
- */
- public setRawDisplayName(name?: string): void {
- 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.
- */
- public setAvatarUrl(url?: string): void {
- const oldUrl = this.avatarUrl;
- this.avatarUrl = url;
- if (url !== oldUrl) {
- this.updateModifiedTime();
- }
- }
-
- /**
- * Update the last modified time to the current time.
- */
- private updateModifiedTime(): void {
- 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
- */
- public getLastModifiedTime(): number {
- 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
- */
- public getLastActiveTs(): number {
- return this.lastPresenceTs - this.lastActiveAgo;
- }
-}