"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.RoomNameType = exports.RoomEvent = exports.Room = exports.NotificationCountType = exports.KNOWN_SAFE_ROOM_VERSION = void 0; var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty")); var _matrixEventsSdk = require("matrix-events-sdk"); var _eventTimelineSet = require("./event-timeline-set"); var _eventTimeline = require("./event-timeline"); var _contentRepo = require("../content-repo"); var utils = _interopRequireWildcard(require("../utils")); var _event = require("./event"); var _eventStatus = require("./event-status"); var _roomMember = require("./room-member"); var _roomSummary = require("./room-summary"); var _logger = require("../logger"); var _ReEmitter = require("../ReEmitter"); var _event2 = require("../@types/event"); var _client = require("../client"); var _filter = require("../filter"); var _roomState = require("./room-state"); var _beacon = require("./beacon"); var _thread = require("./thread"); var _read_receipts = require("../@types/read_receipts"); var _relationsContainer = require("./relations-container"); var _readReceipt = require("./read-receipt"); var _poll = require("./poll"); function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); } function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { (0, _defineProperty2.default)(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } // These constants are used as sane defaults when the homeserver doesn't support // the m.room_versions capability. In practice, KNOWN_SAFE_ROOM_VERSION should be // the same as the common default room version whereas SAFE_ROOM_VERSIONS are the // room versions which are considered okay for people to run without being asked // to upgrade (ie: "stable"). Eventually, we should remove these when all homeservers // return an m.room_versions capability. const KNOWN_SAFE_ROOM_VERSION = "9"; exports.KNOWN_SAFE_ROOM_VERSION = KNOWN_SAFE_ROOM_VERSION; const SAFE_ROOM_VERSIONS = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; // When inserting a visibility event affecting event `eventId`, we // need to scan through existing visibility events for `eventId`. // In theory, this could take an unlimited amount of time if: // // - the visibility event was sent by a moderator; and // - `eventId` already has many visibility changes (usually, it should // be 2 or less); and // - for some reason, the visibility changes are received out of order // (usually, this shouldn't happen at all). // // For this reason, we limit the number of events to scan through, // expecting that a broken visibility change for a single event in // an extremely uncommon case (possibly a DoS) is a small // price to pay to keep matrix-js-sdk responsive. const MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH = 30; let NotificationCountType; exports.NotificationCountType = NotificationCountType; (function (NotificationCountType) { NotificationCountType["Highlight"] = "highlight"; NotificationCountType["Total"] = "total"; })(NotificationCountType || (exports.NotificationCountType = NotificationCountType = {})); let RoomEvent; exports.RoomEvent = RoomEvent; (function (RoomEvent) { RoomEvent["MyMembership"] = "Room.myMembership"; RoomEvent["Tags"] = "Room.tags"; RoomEvent["AccountData"] = "Room.accountData"; RoomEvent["Receipt"] = "Room.receipt"; RoomEvent["Name"] = "Room.name"; RoomEvent["Redaction"] = "Room.redaction"; RoomEvent["RedactionCancelled"] = "Room.redactionCancelled"; RoomEvent["LocalEchoUpdated"] = "Room.localEchoUpdated"; RoomEvent["Timeline"] = "Room.timeline"; RoomEvent["TimelineReset"] = "Room.timelineReset"; RoomEvent["TimelineRefresh"] = "Room.TimelineRefresh"; RoomEvent["OldStateUpdated"] = "Room.OldStateUpdated"; RoomEvent["CurrentStateUpdated"] = "Room.CurrentStateUpdated"; RoomEvent["HistoryImportedWithinTimeline"] = "Room.historyImportedWithinTimeline"; RoomEvent["UnreadNotifications"] = "Room.UnreadNotifications"; })(RoomEvent || (exports.RoomEvent = RoomEvent = {})); class Room extends _readReceipt.ReadReceipt { // Pending in-flight requests { string: MatrixEvent } // Useful to know at what point the current user has started using threads in this room /** * A record of the latest unthread receipts per user * This is useful in determining whether a user has read a thread or not */ // any filtered timeline sets we're maintaining for this room // filter_id: timelineSet // read by megolm via getter; boolean value - null indicates "use global value" // flags to stop logspam about missing m.room.create events // XXX: These should be read-only /** * The human-readable display name for this room. */ /** * The un-homoglyphed name for this room. */ /** * 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 } }` */ // $tagName: { $metadata: $value } /** * accountData Dict of per-room account_data events; the keys are the * event type and the values are the events. */ // $eventType: $event /** * The room summary. */ // legacy fields /** * The live event timeline for this room, with the oldest event at index 0. * Present for backwards compatibility - prefer getLiveTimeline().getEvents() */ /** * 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). */ /** * 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). */ /** * A collection of events known by the client * This is not a comprehensive list of the threads that exist in this room */ /** * 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 */ /** * Construct a new Room. * *
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. * *
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. * *
In order that we can find events from their ids later, we also maintain a * map from event_id to timeline and index. * * @param roomId - Required. The ID of this room. * @param client - Required. The client, used to lazy load members. * @param myUserId - Required. The ID of the syncing user. * @param opts - Configuration options */ constructor(roomId, client, myUserId, opts = {}) { super(); // In some cases, we add listeners for every displayed Matrix event, so it's // common to have quite a few more than the default limit. this.roomId = roomId; this.client = client; this.myUserId = myUserId; this.opts = opts; (0, _defineProperty2.default)(this, "reEmitter", void 0); (0, _defineProperty2.default)(this, "txnToEvent", new Map()); (0, _defineProperty2.default)(this, "notificationCounts", {}); (0, _defineProperty2.default)(this, "threadNotifications", new Map()); (0, _defineProperty2.default)(this, "cachedThreadReadReceipts", new Map()); (0, _defineProperty2.default)(this, "oldestThreadedReceiptTs", Infinity); (0, _defineProperty2.default)(this, "unthreadedReceipts", new Map()); (0, _defineProperty2.default)(this, "timelineSets", void 0); (0, _defineProperty2.default)(this, "polls", new Map()); (0, _defineProperty2.default)(this, "threadsTimelineSets", []); (0, _defineProperty2.default)(this, "filteredTimelineSets", {}); (0, _defineProperty2.default)(this, "timelineNeedsRefresh", false); (0, _defineProperty2.default)(this, "pendingEventList", void 0); (0, _defineProperty2.default)(this, "blacklistUnverifiedDevices", void 0); (0, _defineProperty2.default)(this, "selfMembership", void 0); (0, _defineProperty2.default)(this, "summaryHeroes", null); (0, _defineProperty2.default)(this, "getTypeWarning", false); (0, _defineProperty2.default)(this, "getVersionWarning", false); (0, _defineProperty2.default)(this, "membersPromise", void 0); (0, _defineProperty2.default)(this, "name", void 0); (0, _defineProperty2.default)(this, "normalizedName", void 0); (0, _defineProperty2.default)(this, "tags", {}); (0, _defineProperty2.default)(this, "accountData", new Map()); (0, _defineProperty2.default)(this, "summary", null); (0, _defineProperty2.default)(this, "timeline", void 0); (0, _defineProperty2.default)(this, "oldState", void 0); (0, _defineProperty2.default)(this, "currentState", void 0); (0, _defineProperty2.default)(this, "relations", new _relationsContainer.RelationsContainer(this.client, this)); (0, _defineProperty2.default)(this, "threads", new Map()); (0, _defineProperty2.default)(this, "lastThread", void 0); (0, _defineProperty2.default)(this, "visibilityEvents", new Map()); (0, _defineProperty2.default)(this, "threadTimelineSetsPromise", null); (0, _defineProperty2.default)(this, "threadsReady", false); (0, _defineProperty2.default)(this, "updateThreadRootEvents", (thread, toStartOfTimeline, recreateEvent) => { if (thread.length) { var _this$threadsTimeline; this.updateThreadRootEvent((_this$threadsTimeline = this.threadsTimelineSets) === null || _this$threadsTimeline === void 0 ? void 0 : _this$threadsTimeline[0], thread, toStartOfTimeline, recreateEvent); if (thread.hasCurrentUserParticipated) { var _this$threadsTimeline2; this.updateThreadRootEvent((_this$threadsTimeline2 = this.threadsTimelineSets) === null || _this$threadsTimeline2 === void 0 ? void 0 : _this$threadsTimeline2[1], thread, toStartOfTimeline, recreateEvent); } } }); (0, _defineProperty2.default)(this, "updateThreadRootEvent", (timelineSet, thread, toStartOfTimeline, recreateEvent) => { if (timelineSet && thread.rootEvent) { if (recreateEvent) { timelineSet.removeEvent(thread.id); } if (_thread.Thread.hasServerSideSupport) { timelineSet.addLiveEvent(thread.rootEvent, { duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Replace, fromCache: false, roomState: this.currentState }); } else { timelineSet.addEventToTimeline(thread.rootEvent, timelineSet.getLiveTimeline(), { toStartOfTimeline }); } } }); (0, _defineProperty2.default)(this, "applyRedaction", event => { if (event.isRedaction()) { const redactId = event.event.redacts; // if we know about this event, redact its contents now. const redactedEvent = redactId ? this.findEventById(redactId) : undefined; if (redactedEvent) { redactedEvent.makeRedacted(event); // If this is in the current state, replace it with the redacted version if (redactedEvent.isState()) { const currentStateEvent = this.currentState.getStateEvents(redactedEvent.getType(), redactedEvent.getStateKey()); if ((currentStateEvent === null || currentStateEvent === void 0 ? void 0 : currentStateEvent.getId()) === redactedEvent.getId()) { this.currentState.setStateEvents([redactedEvent]); } } this.emit(RoomEvent.Redaction, event, this); // TODO: we stash user displaynames (among other things) in // RoomMember objects which are then attached to other events // (in the sender and target fields). We should get those // RoomMember objects to update themselves when the events that // they are based on are changed. // Remove any visibility change on this event. this.visibilityEvents.delete(redactId); // If this event is a visibility change event, remove it from the // list of visibility changes and update any event affected by it. if (redactedEvent.isVisibilityEvent()) { this.redactVisibilityChangeEvent(event); } } // FIXME: apply redactions to notification list // NB: We continue to add the redaction event to the timeline so // clients can say "so and so redacted an event" if they wish to. Also // this may be needed to trigger an update. } }); this.setMaxListeners(100); this.reEmitter = new _ReEmitter.TypedReEmitter(this); opts.pendingEventOrdering = opts.pendingEventOrdering || _client.PendingEventOrdering.Chronological; this.name = roomId; this.normalizedName = roomId; // all our per-room timeline sets. the first one is the unfiltered ones; // the subsequent ones are the filtered ones in no particular order. this.timelineSets = [new _eventTimelineSet.EventTimelineSet(this, opts)]; this.reEmitter.reEmit(this.getUnfilteredTimelineSet(), [RoomEvent.Timeline, RoomEvent.TimelineReset]); this.fixUpLegacyTimelineFields(); if (this.opts.pendingEventOrdering === _client.PendingEventOrdering.Detached) { this.pendingEventList = []; this.client.store.getPendingEvents(this.roomId).then(events => { const mapper = this.client.getEventMapper({ toDevice: false, decrypt: false }); events.forEach(async serializedEvent => { const event = mapper(serializedEvent); await client.decryptEventIfNeeded(event); event.setStatus(_eventStatus.EventStatus.NOT_SENT); this.addPendingEvent(event, event.getTxnId()); }); }); } // awaited by getEncryptionTargetMembers while room members are loading if (!this.opts.lazyLoadMembers) { this.membersPromise = Promise.resolve(false); } else { this.membersPromise = undefined; } } async createThreadsTimelineSets() { var _this$client; if (this.threadTimelineSetsPromise) { return this.threadTimelineSetsPromise; } if ((_this$client = this.client) !== null && _this$client !== void 0 && _this$client.supportsThreads()) { try { this.threadTimelineSetsPromise = Promise.all([this.createThreadTimelineSet(), this.createThreadTimelineSet(_thread.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 */ async decryptCriticalEvents() { if (!this.client.isCryptoEnabled()) return; const readReceiptEventId = this.getEventReadUpTo(this.client.getUserId(), true); const events = this.getLiveTimeline().getEvents(); const readReceiptTimelineIndex = events.findIndex(matrixEvent => { return matrixEvent.event.event_id === readReceiptEventId; }); const decryptionPromises = events.slice(readReceiptTimelineIndex).reverse().map(event => this.client.decryptEventIfNeeded(event, { isRetry: true })); await Promise.allSettled(decryptionPromises); } /** * Bulk decrypt events in a room * * @returns Signals when all events have been decrypted */ async decryptAllEvents() { if (!this.client.isCryptoEnabled()) return; const decryptionPromises = this.getUnfilteredTimelineSet().getLiveTimeline().getEvents().slice(0) // copy before reversing .reverse().map(event => this.client.decryptEventIfNeeded(event, { isRetry: true })); await Promise.allSettled(decryptionPromises); } /** * Gets the creator of the room * @returns The creator of the room, or null if it could not be determined */ getCreator() { var _createEvent$getConte; const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); return (_createEvent$getConte = createEvent === null || createEvent === void 0 ? void 0 : createEvent.getContent()["creator"]) !== null && _createEvent$getConte !== void 0 ? _createEvent$getConte : null; } /** * Gets the version of the room * @returns The version of the room, or null if it could not be determined */ getVersion() { var _createEvent$getConte2; const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); if (!createEvent) { if (!this.getVersionWarning) { _logger.logger.warn("[getVersion] Room " + this.roomId + " does not have an m.room.create event"); this.getVersionWarning = true; } return "1"; } return (_createEvent$getConte2 = createEvent.getContent()["room_version"]) !== null && _createEvent$getConte2 !== void 0 ? _createEvent$getConte2 : "1"; } /** * Determines whether this room needs to be upgraded to a new version * @returns What version the room should be upgraded to, or null if * the room does not require upgrading at this time. * @deprecated Use #getRecommendedVersion() instead */ shouldUpgradeToVersion() { // TODO: Remove this function. // This makes assumptions about which versions are safe, and can easily // be wrong. Instead, people are encouraged to use getRecommendedVersion // which determines a safer value. This function doesn't use that function // because this is not async-capable, and to avoid breaking the contract // we're deprecating this. if (!SAFE_ROOM_VERSIONS.includes(this.getVersion())) { return KNOWN_SAFE_ROOM_VERSION; } return null; } /** * Determines the recommended room version for the room. This returns an * object with 3 properties: `version` as the new version the * room should be upgraded to (may be the same as the current version); * `needsUpgrade` to indicate if the room actually can be * upgraded (ie: does the current version not match?); and `urgent` * to indicate if the new version patches a vulnerability in a previous * version. * @returns * Resolves to the version the room should be upgraded to. */ async getRecommendedVersion() { const capabilities = await this.client.getCapabilities(); let versionCap = capabilities["m.room_versions"]; if (!versionCap) { versionCap = { default: KNOWN_SAFE_ROOM_VERSION, available: {} }; for (const safeVer of SAFE_ROOM_VERSIONS) { versionCap.available[safeVer] = _client.RoomVersionStability.Stable; } } let result = this.checkVersionAgainstCapability(versionCap); if (result.urgent && result.needsUpgrade) { // Something doesn't feel right: we shouldn't need to update // because the version we're on should be in the protocol's // namespace. This usually means that the server was updated // before the client was, making us think the newest possible // room version is not stable. As a solution, we'll refresh // the capability we're using to determine this. _logger.logger.warn("Refreshing room version capability because the server looks " + "to be supporting a newer room version we don't know about."); const caps = await this.client.getCapabilities(true); versionCap = caps["m.room_versions"]; if (!versionCap) { _logger.logger.warn("No room version capability - assuming upgrade required."); return result; } else { result = this.checkVersionAgainstCapability(versionCap); } } return result; } checkVersionAgainstCapability(versionCap) { const currentVersion = this.getVersion(); _logger.logger.log(`[${this.roomId}] Current version: ${currentVersion}`); _logger.logger.log(`[${this.roomId}] Version capability: `, versionCap); const result = { version: currentVersion, needsUpgrade: false, urgent: false }; // If the room is on the default version then nothing needs to change if (currentVersion === versionCap.default) return result; const stableVersions = Object.keys(versionCap.available).filter(v => versionCap.available[v] === "stable"); // Check if the room is on an unstable version. We determine urgency based // off the version being in the Matrix spec namespace or not (if the version // is in the current namespace and unstable, the room is probably vulnerable). if (!stableVersions.includes(currentVersion)) { result.version = versionCap.default; result.needsUpgrade = true; result.urgent = !!this.getVersion().match(/^[0-9]+[0-9.]*$/g); if (result.urgent) { _logger.logger.warn(`URGENT upgrade required on ${this.roomId}`); } else { _logger.logger.warn(`Non-urgent upgrade required on ${this.roomId}`); } return result; } // The room is on a stable, but non-default, version by this point. // No upgrade needed. return result; } /** * Determines whether the given user is permitted to perform a room upgrade * @param userId - The ID of the user to test against * @returns True if the given user is permitted to upgrade the room */ userMayUpgradeRoom(userId) { return this.currentState.maySendStateEvent(_event2.EventType.RoomTombstone, userId); } /** * Get the list of pending sent events for this room * * @returns A list of the sent events * waiting for remote echo. * * @throws If `opts.pendingEventOrdering` was not 'detached' */ getPendingEvents() { if (!this.pendingEventList) { throw new Error("Cannot call getPendingEvents with pendingEventOrdering == " + this.opts.pendingEventOrdering); } return this.pendingEventList; } /** * Removes a pending event for this room * * @returns True if an element was removed. */ removePendingEvent(eventId) { if (!this.pendingEventList) { throw new Error("Cannot call removePendingEvent with pendingEventOrdering == " + this.opts.pendingEventOrdering); } const removed = utils.removeElement(this.pendingEventList, function (ev) { return ev.getId() == eventId; }, false); this.savePendingEvents(); return removed; } /** * Check whether the pending event list contains a given event by ID. * If pending event ordering is not "detached" then this returns false. * * @param eventId - The event ID to check for. */ hasPendingEvent(eventId) { var _this$pendingEventLis, _this$pendingEventLis2; return (_this$pendingEventLis = (_this$pendingEventLis2 = this.pendingEventList) === null || _this$pendingEventLis2 === void 0 ? void 0 : _this$pendingEventLis2.some(event => event.getId() === eventId)) !== null && _this$pendingEventLis !== void 0 ? _this$pendingEventLis : false; } /** * Get a specific event from the pending event list, if configured, null otherwise. * * @param eventId - The event ID to check for. */ getPendingEvent(eventId) { var _this$pendingEventLis3, _this$pendingEventLis4; return (_this$pendingEventLis3 = (_this$pendingEventLis4 = this.pendingEventList) === null || _this$pendingEventLis4 === void 0 ? void 0 : _this$pendingEventLis4.find(event => event.getId() === eventId)) !== null && _this$pendingEventLis3 !== void 0 ? _this$pendingEventLis3 : null; } /** * Get the live unfiltered timeline for this room. * * @returns live timeline */ getLiveTimeline() { return this.getUnfilteredTimelineSet().getLiveTimeline(); } /** * Get the timestamp of the last message in the room * * @returns the timestamp of the last message in the room */ getLastActiveTimestamp() { const timeline = this.getLiveTimeline(); const events = timeline.getEvents(); if (events.length) { const lastEvent = events[events.length - 1]; return lastEvent.getTs(); } else { return Number.MIN_SAFE_INTEGER; } } /** * @returns the membership type (join | leave | invite) for the logged in user */ getMyMembership() { var _this$selfMembership; return (_this$selfMembership = this.selfMembership) !== null && _this$selfMembership !== void 0 ? _this$selfMembership : "leave"; } /** * If this room is a DM we're invited to, * try to find out who invited us * @returns user id of the inviter */ getDMInviter() { const me = this.getMember(this.myUserId); if (me) { return me.getDMInviter(); } if (this.selfMembership === "invite") { // fall back to summary information const memberCount = this.getInvitedAndJoinedMemberCount(); if (memberCount === 2) { var _this$summaryHeroes; return (_this$summaryHeroes = this.summaryHeroes) === null || _this$summaryHeroes === void 0 ? void 0 : _this$summaryHeroes[0]; } } } /** * Assuming this room is a DM room, tries to guess with which user. * @returns user id of the other member (could be syncing user) */ guessDMUserId() { const me = this.getMember(this.myUserId); if (me) { const inviterId = me.getDMInviter(); if (inviterId) { return inviterId; } } // Remember, we're assuming this room is a DM, so returning the first member we find should be fine if (Array.isArray(this.summaryHeroes) && this.summaryHeroes.length) { return this.summaryHeroes[0]; } const members = this.currentState.getMembers(); const anyMember = members.find(m => m.userId !== this.myUserId); if (anyMember) { return anyMember.userId; } // it really seems like I'm the only user in the room // so I probably created a room with just me in it // and marked it as a DM. Ok then return this.myUserId; } getAvatarFallbackMember() { const memberCount = this.getInvitedAndJoinedMemberCount(); if (memberCount > 2) { return; } const hasHeroes = Array.isArray(this.summaryHeroes) && this.summaryHeroes.length; if (hasHeroes) { const availableMember = this.summaryHeroes.map(userId => { return this.getMember(userId); }).find(member => !!member); if (availableMember) { return availableMember; } } const members = this.currentState.getMembers(); // could be different than memberCount // as this includes left members if (members.length <= 2) { const availableMember = members.find(m => { return m.userId !== this.myUserId; }); if (availableMember) { return availableMember; } } // if all else fails, try falling back to a user, // and create a one-off member for it if (hasHeroes) { const availableUser = this.summaryHeroes.map(userId => { return this.client.getUser(userId); }).find(user => !!user); if (availableUser) { const member = new _roomMember.RoomMember(this.roomId, availableUser.userId); member.user = availableUser; return member; } } } /** * Sets the membership this room was received as during sync * @param membership - join | leave | invite */ updateMyMembership(membership) { const prevMembership = this.selfMembership; this.selfMembership = membership; if (prevMembership !== membership) { if (membership === "leave") { this.cleanupAfterLeaving(); } this.emit(RoomEvent.MyMembership, this, membership, prevMembership); } } async loadMembersFromServer() { const lastSyncToken = this.client.store.getSyncToken(); const response = await this.client.members(this.roomId, undefined, "leave", lastSyncToken !== null && lastSyncToken !== void 0 ? lastSyncToken : undefined); return response.chunk; } async loadMembers() { // were the members loaded from the server? let fromServer = false; let rawMembersEvents = await this.client.store.getOutOfBandMembers(this.roomId); // If the room is encrypted, we always fetch members from the server at // least once, in case the latest state wasn't persisted properly. Note // that this function is only called once (unless loading the members // fails), since loadMembersIfNeeded always returns this.membersPromise // if set, which will be the result of the first (successful) call. if (rawMembersEvents === null || this.client.isCryptoEnabled() && this.client.isRoomEncrypted(this.roomId)) { fromServer = true; rawMembersEvents = await this.loadMembersFromServer(); _logger.logger.log(`LL: got ${rawMembersEvents.length} ` + `members from server for room ${this.roomId}`); } const memberEvents = rawMembersEvents.filter(utils.noUnsafeEventProps).map(this.client.getEventMapper()); return { memberEvents, fromServer }; } /** * Check if loading of out-of-band-members has completed * * @returns true if the full membership list of this room has been loaded (including if lazy-loading is disabled). * False if the load is not started or is in progress. */ membersLoaded() { if (!this.opts.lazyLoadMembers) { return true; } return this.currentState.outOfBandMembersReady(); } /** * Preloads the member list in case lazy loading * of memberships is in use. Can be called multiple times, * it will only preload once. * @returns when preloading is done and * accessing the members on the room will take * all members in the room into account */ loadMembersIfNeeded() { if (this.membersPromise) { return this.membersPromise; } // mark the state so that incoming messages while // the request is in flight get marked as superseding // the OOB members this.currentState.markOutOfBandMembersStarted(); const inMemoryUpdate = this.loadMembers().then(result => { this.currentState.setOutOfBandMembers(result.memberEvents); return result.fromServer; }).catch(err => { // allow retries on fail this.membersPromise = undefined; this.currentState.markOutOfBandMembersFailed(); throw err; }); // update members in storage, but don't wait for it inMemoryUpdate.then(fromServer => { if (fromServer) { const oobMembers = this.currentState.getMembers().filter(m => m.isOutOfBand()).map(m => { var _m$events$member; return (_m$events$member = m.events.member) === null || _m$events$member === void 0 ? void 0 : _m$events$member.event; }); _logger.logger.log(`LL: telling store to write ${oobMembers.length}` + ` members for room ${this.roomId}`); const store = this.client.store; return store.setOutOfBandMembers(this.roomId, oobMembers) // swallow any IDB error as we don't want to fail // because of this .catch(err => { _logger.logger.log("LL: storing OOB room members failed, oh well", err); }); } }).catch(err => { // as this is not awaited anywhere, // at least show the error in the console _logger.logger.error(err); }); this.membersPromise = inMemoryUpdate; return this.membersPromise; } /** * Removes the lazily loaded members from storage if needed */ async clearLoadedMembersIfNeeded() { if (this.opts.lazyLoadMembers && this.membersPromise) { await this.loadMembersIfNeeded(); await this.client.store.clearOutOfBandMembers(this.roomId); this.currentState.clearOutOfBandMembers(); this.membersPromise = undefined; } } /** * called when sync receives this room in the leave section * to do cleanup after leaving a room. Possibly called multiple times. */ cleanupAfterLeaving() { this.clearLoadedMembersIfNeeded().catch(err => { _logger.logger.error(`error after clearing loaded members from ` + `room ${this.roomId} after leaving`); _logger.logger.log(err); }); } /** * Empty out the current live timeline and re-request it. This is used when * historical messages are imported into the room via MSC2716 `/batch_send` * because the client may already have that section of the timeline loaded. * We need to force the client to throw away their current timeline so that * when they back paginate over the area again with the historical messages * in between, it grabs the newly imported messages. We can listen for * `UNSTABLE_MSC2716_MARKER`, in order to tell when historical messages are ready * to be discovered in the room and the timeline needs a refresh. The SDK * emits a `RoomEvent.HistoryImportedWithinTimeline` event when we detect a * valid marker and can check the needs refresh status via * `room.getTimelineNeedsRefresh()`. */ async refreshLiveTimeline() { const liveTimelineBefore = this.getLiveTimeline(); const forwardPaginationToken = liveTimelineBefore.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS); const backwardPaginationToken = liveTimelineBefore.getPaginationToken(_eventTimeline.EventTimeline.BACKWARDS); const eventsBefore = liveTimelineBefore.getEvents(); const mostRecentEventInTimeline = eventsBefore[eventsBefore.length - 1]; _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] at ` + `mostRecentEventInTimeline=${mostRecentEventInTimeline && mostRecentEventInTimeline.getId()} ` + `liveTimelineBefore=${liveTimelineBefore.toString()} ` + `forwardPaginationToken=${forwardPaginationToken} ` + `backwardPaginationToken=${backwardPaginationToken}`); // Get the main TimelineSet const timelineSet = this.getUnfilteredTimelineSet(); let newTimeline; // If there isn't any event in the timeline, let's go fetch the latest // event and construct a timeline from it. // // This should only really happen if the user ran into an error // with refreshing the timeline before which left them in a blank // timeline from `resetLiveTimeline`. if (!mostRecentEventInTimeline) { newTimeline = await this.client.getLatestTimeline(timelineSet); } else { // Empty out all of `this.timelineSets`. But we also need to keep the // same `timelineSet` references around so the React code updates // properly and doesn't ignore the room events we emit because it checks // that the `timelineSet` references are the same. We need the // `timelineSet` empty so that the `client.getEventTimeline(...)` call // later, will call `/context` and create a new timeline instead of // returning the same one. this.resetLiveTimeline(null, null); // Make the UI timeline show the new blank live timeline we just // reset so that if the network fails below it's showing the // accurate state of what we're working with instead of the // disconnected one in the TimelineWindow which is just hanging // around by reference. this.emit(RoomEvent.TimelineRefresh, this, timelineSet); // Use `client.getEventTimeline(...)` to construct a new timeline from a // `/context` response state and events for the most recent event before // we reset everything. The `timelineSet` we pass in needs to be empty // in order for this function to call `/context` and generate a new // timeline. newTimeline = await this.client.getEventTimeline(timelineSet, mostRecentEventInTimeline.getId()); } // If a racing `/sync` beat us to creating a new timeline, use that // instead because it's the latest in the room and any new messages in // the scrollback will include the history. const liveTimeline = timelineSet.getLiveTimeline(); if (!liveTimeline || liveTimeline.getPaginationToken(_eventTimeline.Direction.Forward) === null && liveTimeline.getPaginationToken(_eventTimeline.Direction.Backward) === null && liveTimeline.getEvents().length === 0) { _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] using our new live timeline`); // Set the pagination token back to the live sync token (`null`) instead // of using the `/context` historical token (ex. `t12-13_0_0_0_0_0_0_0_0`) // so that it matches the next response from `/sync` and we can properly // continue the timeline. newTimeline.setPaginationToken(forwardPaginationToken, _eventTimeline.EventTimeline.FORWARDS); // Set our new fresh timeline as the live timeline to continue syncing // forwards and back paginating from. timelineSet.setLiveTimeline(newTimeline); // Fixup `this.oldstate` so that `scrollback` has the pagination tokens // available this.fixUpLegacyTimelineFields(); } else { _logger.logger.log(`[refreshLiveTimeline for ${this.roomId}] \`/sync\` or some other request beat us to creating a new ` + `live timeline after we reset it. We'll use that instead since any events in the scrollback from ` + `this timeline will include the history.`); } // The timeline has now been refreshed ✅ this.setTimelineNeedsRefresh(false); // Emit an event which clients can react to and re-load the timeline // from the SDK this.emit(RoomEvent.TimelineRefresh, this, timelineSet); } /** * Reset the live timeline of all timelineSets, and start new ones. * *
This is used when /sync returns a 'limited' timeline. * * @param backPaginationToken - token for back-paginating the new timeline * @param forwardPaginationToken - token for forward-paginating the old live timeline, * if absent or null, all timelines are reset, removing old ones (including the previous live * timeline which would otherwise be unable to paginate forwards without this token). * Removing just the old live timeline whilst preserving previous ones is not supported. */ resetLiveTimeline(backPaginationToken, forwardPaginationToken) { for (const timelineSet of this.timelineSets) { timelineSet.resetLiveTimeline(backPaginationToken !== null && backPaginationToken !== void 0 ? backPaginationToken : undefined, forwardPaginationToken !== null && forwardPaginationToken !== void 0 ? forwardPaginationToken : undefined); } for (const thread of this.threads.values()) { thread.resetLiveTimeline(backPaginationToken, forwardPaginationToken); } this.fixUpLegacyTimelineFields(); } /** * Fix up this.timeline, this.oldState and this.currentState * * @internal */ fixUpLegacyTimelineFields() { const previousOldState = this.oldState; const previousCurrentState = this.currentState; // maintain this.timeline as a reference to the live timeline, // and this.oldState and this.currentState as references to the // state at the start and end of that timeline. These are more // for backwards-compatibility than anything else. this.timeline = this.getLiveTimeline().getEvents(); this.oldState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.BACKWARDS); this.currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); // Let people know to register new listeners for the new state // references. The reference won't necessarily change every time so only // emit when we see a change. if (previousOldState !== this.oldState) { this.emit(RoomEvent.OldStateUpdated, this, previousOldState, this.oldState); } if (previousCurrentState !== this.currentState) { this.emit(RoomEvent.CurrentStateUpdated, this, previousCurrentState, this.currentState); // Re-emit various events on the current room state // TODO: If currentState really only exists for backwards // compatibility, shouldn't we be doing this some other way? this.reEmitter.stopReEmitting(previousCurrentState, [_roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update, _roomState.RoomStateEvent.Marker, _beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]); this.reEmitter.reEmit(this.currentState, [_roomState.RoomStateEvent.Events, _roomState.RoomStateEvent.Members, _roomState.RoomStateEvent.NewMember, _roomState.RoomStateEvent.Update, _roomState.RoomStateEvent.Marker, _beacon.BeaconEvent.New, _beacon.BeaconEvent.Update, _beacon.BeaconEvent.Destroy, _beacon.BeaconEvent.LivenessChange]); } } /** * Returns whether there are any devices in the room that are unverified * * Note: Callers should first check if crypto is enabled on this device. If it is * disabled, then we aren't tracking room devices at all, so we can't answer this, and an * error will be thrown. * * @returns the result */ async hasUnverifiedDevices() { if (!this.client.isRoomEncrypted(this.roomId)) { return false; } const e2eMembers = await this.getEncryptionTargetMembers(); for (const member of e2eMembers) { const devices = this.client.getStoredDevicesForUser(member.userId); if (devices.some(device => device.isUnverified())) { return true; } } return false; } /** * Return the timeline sets for this room. * @returns array of timeline sets for this room */ getTimelineSets() { return this.timelineSets; } /** * Helper to return the main unfiltered timeline set for this room * @returns room's unfiltered timeline set */ getUnfilteredTimelineSet() { return this.timelineSets[0]; } /** * Get the timeline which contains the given event from the unfiltered set, if any * * @param eventId - event ID to look for * @returns timeline containing * the given event, or null if unknown */ getTimelineForEvent(eventId) { const event = this.findEventById(eventId); const thread = this.findThreadForEvent(event); if (thread) { return thread.timelineSet.getTimelineForEvent(eventId); } else { return this.getUnfilteredTimelineSet().getTimelineForEvent(eventId); } } /** * Add a new timeline to this room's unfiltered timeline set * * @returns newly-created timeline */ addTimeline() { return this.getUnfilteredTimelineSet().addTimeline(); } /** * Whether the timeline needs to be refreshed in order to pull in new * historical messages that were imported. * @param value - The value to set */ setTimelineNeedsRefresh(value) { this.timelineNeedsRefresh = value; } /** * Whether the timeline needs to be refreshed in order to pull in new * historical messages that were imported. * @returns . */ getTimelineNeedsRefresh() { return this.timelineNeedsRefresh; } /** * Get an event which is stored in our unfiltered timeline set, or in a thread * * @param eventId - event ID to look for * @returns the given event, or undefined if unknown */ findEventById(eventId) { let event = this.getUnfilteredTimelineSet().findEventById(eventId); if (!event) { const threads = this.getThreads(); for (let i = 0; i < threads.length; i++) { const thread = threads[i]; event = thread.findEventById(eventId); if (event) { return event; } } } return event; } /** * Get one of the notification counts for this room * @param type - The type of notification count to get. default: 'total' * @returns The notification count, or undefined if there is no count * for this type. */ getUnreadNotificationCount(type = NotificationCountType.Total) { let count = this.getRoomUnreadNotificationCount(type); for (const threadNotification of this.threadNotifications.values()) { var _threadNotification$t; count += (_threadNotification$t = threadNotification[type]) !== null && _threadNotification$t !== void 0 ? _threadNotification$t : 0; } return count; } /** * Get the notification for the event context (room or thread timeline) */ getUnreadCountForEventContext(type = NotificationCountType.Total, event) { var _ref; const isThreadEvent = !!event.threadRootId && !event.isThreadRoot; return (_ref = isThreadEvent ? this.getThreadUnreadNotificationCount(event.threadRootId, type) : this.getRoomUnreadNotificationCount(type)) !== null && _ref !== void 0 ? _ref : 0; } /** * Get one of the notification counts for this room * @param type - The type of notification count to get. default: 'total' * @returns The notification count, or undefined if there is no count * for this type. */ getRoomUnreadNotificationCount(type = NotificationCountType.Total) { var _this$notificationCou; return (_this$notificationCou = this.notificationCounts[type]) !== null && _this$notificationCou !== void 0 ? _this$notificationCou : 0; } /** * Get one of the notification counts for a thread * @param threadId - the root event ID * @param type - The type of notification count to get. default: 'total' * @returns The notification count, or undefined if there is no count * for this type. */ getThreadUnreadNotificationCount(threadId, type = NotificationCountType.Total) { var _this$threadNotificat, _this$threadNotificat2; return (_this$threadNotificat = (_this$threadNotificat2 = this.threadNotifications.get(threadId)) === null || _this$threadNotificat2 === void 0 ? void 0 : _this$threadNotificat2[type]) !== null && _this$threadNotificat !== void 0 ? _this$threadNotificat : 0; } /** * Checks if the current room has unread thread notifications * @returns */ hasThreadUnreadNotification() { for (const notification of this.threadNotifications.values()) { var _notification$highlig, _notification$total; if (((_notification$highlig = notification.highlight) !== null && _notification$highlig !== void 0 ? _notification$highlig : 0) > 0 || ((_notification$total = notification.total) !== null && _notification$total !== void 0 ? _notification$total : 0) > 0) { return true; } } return false; } /** * Swet one of the notification count for a thread * @param threadId - the root event ID * @param type - The type of notification count to get. default: 'total' * @returns */ setThreadUnreadNotificationCount(threadId, type, count) { var _this$threadNotificat3, _this$threadNotificat4; const notification = _objectSpread({ highlight: (_this$threadNotificat3 = this.threadNotifications.get(threadId)) === null || _this$threadNotificat3 === void 0 ? void 0 : _this$threadNotificat3.highlight, total: (_this$threadNotificat4 = this.threadNotifications.get(threadId)) === null || _this$threadNotificat4 === void 0 ? void 0 : _this$threadNotificat4.total }, { [type]: count }); this.threadNotifications.set(threadId, notification); this.emit(RoomEvent.UnreadNotifications, notification, threadId); } /** * @returns the notification count type for all the threads in the room */ get threadsAggregateNotificationType() { let type = null; for (const threadNotification of this.threadNotifications.values()) { var _threadNotification$h, _threadNotification$t2; if (((_threadNotification$h = threadNotification.highlight) !== null && _threadNotification$h !== void 0 ? _threadNotification$h : 0) > 0) { return NotificationCountType.Highlight; } else if (((_threadNotification$t2 = threadNotification.total) !== null && _threadNotification$t2 !== void 0 ? _threadNotification$t2 : 0) > 0 && !type) { type = NotificationCountType.Total; } } return type; } /** * Resets the thread notifications for this room */ resetThreadUnreadNotificationCount(notificationsToKeep) { if (notificationsToKeep) { for (const [threadId] of this.threadNotifications) { if (!notificationsToKeep.includes(threadId)) { this.threadNotifications.delete(threadId); } } } else { this.threadNotifications.clear(); } this.emit(RoomEvent.UnreadNotifications); } /** * Set one of the notification counts for this room * @param type - The type of notification count to set. * @param count - The new count */ setUnreadNotificationCount(type, count) { this.notificationCounts[type] = count; this.emit(RoomEvent.UnreadNotifications, this.notificationCounts); } setUnread(type, count) { return this.setUnreadNotificationCount(type, count); } setSummary(summary) { const heroes = summary["m.heroes"]; const joinedCount = summary["m.joined_member_count"]; const invitedCount = summary["m.invited_member_count"]; if (Number.isInteger(joinedCount)) { this.currentState.setJoinedMemberCount(joinedCount); } if (Number.isInteger(invitedCount)) { this.currentState.setInvitedMemberCount(invitedCount); } if (Array.isArray(heroes)) { // be cautious about trusting server values, // and make sure heroes doesn't contain our own id // just to be sure this.summaryHeroes = heroes.filter(userId => { return userId !== this.myUserId; }); } } /** * Whether to send encrypted messages to devices within this room. * @param value - true to blacklist unverified devices, null * to use the global value for this room. */ setBlacklistUnverifiedDevices(value) { this.blacklistUnverifiedDevices = value; } /** * Whether to send encrypted messages to devices within this room. * @returns true if blacklisting unverified devices, null * if the global value should be used for this room. */ getBlacklistUnverifiedDevices() { if (this.blacklistUnverifiedDevices === undefined) return null; return this.blacklistUnverifiedDevices; } /** * Get the avatar URL for a room if one was set. * @param baseUrl - The homeserver base URL. See * {@link MatrixClient#getHomeserverUrl}. * @param width - The desired width of the thumbnail. * @param height - The desired height of the thumbnail. * @param resizeMethod - The thumbnail resize method to use, either * "crop" or "scale". * @param allowDefault - True to allow an identicon for this room if an * avatar URL wasn't explicitly set. Default: true. (Deprecated) * @returns the avatar URL or null. */ getAvatarUrl(baseUrl, width, height, resizeMethod, allowDefault = true) { const roomAvatarEvent = this.currentState.getStateEvents(_event2.EventType.RoomAvatar, ""); if (!roomAvatarEvent && !allowDefault) { return null; } const mainUrl = roomAvatarEvent ? roomAvatarEvent.getContent().url : null; if (mainUrl) { return (0, _contentRepo.getHttpUriForMxc)(baseUrl, mainUrl, width, height, resizeMethod); } return null; } /** * Get the mxc avatar url for the room, if one was set. * @returns the mxc avatar url or falsy */ getMxcAvatarUrl() { var _this$currentState$ge, _this$currentState$ge2; return ((_this$currentState$ge = this.currentState.getStateEvents(_event2.EventType.RoomAvatar, "")) === null || _this$currentState$ge === void 0 ? void 0 : (_this$currentState$ge2 = _this$currentState$ge.getContent()) === null || _this$currentState$ge2 === void 0 ? void 0 : _this$currentState$ge2.url) || null; } /** * Get this room's canonical alias * The alias returned by this function may not necessarily * still point to this room. * @returns The room's canonical alias, or null if there is none */ getCanonicalAlias() { const canonicalAlias = this.currentState.getStateEvents(_event2.EventType.RoomCanonicalAlias, ""); if (canonicalAlias) { return canonicalAlias.getContent().alias || null; } return null; } /** * Get this room's alternative aliases * @returns The room's alternative aliases, or an empty array */ getAltAliases() { const canonicalAlias = this.currentState.getStateEvents(_event2.EventType.RoomCanonicalAlias, ""); if (canonicalAlias) { return canonicalAlias.getContent().alt_aliases || []; } return []; } /** * Add events to a timeline * *
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 last element of 'events'. * * @param timeline - timeline to * add events to. * * @param paginationToken - token for the next batch of events * * @remarks * Fires {@link RoomEvent.Timeline} */ addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken) { timeline.getTimelineSet().addEventsToTimeline(events, toStartOfTimeline, timeline, paginationToken); } /** * Get the instance of the thread associated with the current event * @param eventId - the ID of the current event * @returns a thread instance if known */ getThread(eventId) { var _this$threads$get; return (_this$threads$get = this.threads.get(eventId)) !== null && _this$threads$get !== void 0 ? _this$threads$get : null; } /** * Get all the known threads in the room */ getThreads() { return Array.from(this.threads.values()); } /** * Get a member from the current room state. * @param userId - The user ID of the member. * @returns The member or `null`. */ getMember(userId) { return this.currentState.getMember(userId); } /** * Get all currently loaded members from the current * room state. * @returns Room members */ getMembers() { return this.currentState.getMembers(); } /** * Get a list of members whose membership state is "join". * @returns A list of currently joined members. */ getJoinedMembers() { return this.getMembersWithMembership("join"); } /** * Returns the number of joined members in this room * This method caches the result. * This is a wrapper around the method of the same name in roomState, returning * its result for the room's current state. * @returns The number of members in this room whose membership is 'join' */ getJoinedMemberCount() { return this.currentState.getJoinedMemberCount(); } /** * Returns the number of invited members in this room * @returns The number of members in this room whose membership is 'invite' */ getInvitedMemberCount() { return this.currentState.getInvitedMemberCount(); } /** * Returns the number of invited + joined members in this room * @returns The number of members in this room whose membership is 'invite' or 'join' */ getInvitedAndJoinedMemberCount() { return this.getInvitedMemberCount() + this.getJoinedMemberCount(); } /** * Get a list of members with given membership state. * @param membership - The membership state. * @returns A list of members with the given membership state. */ getMembersWithMembership(membership) { return this.currentState.getMembers().filter(function (m) { return m.membership === membership; }); } /** * Get a list of members we should be encrypting for in this room * @returns A list of members who * we should encrypt messages for in this room. */ async getEncryptionTargetMembers() { await this.loadMembersIfNeeded(); let members = this.getMembersWithMembership("join"); if (this.shouldEncryptForInvitedMembers()) { members = members.concat(this.getMembersWithMembership("invite")); } return members; } /** * Determine whether we should encrypt messages for invited users in this room * @returns if we should encrypt messages for invited users */ shouldEncryptForInvitedMembers() { var _ev$getContent; const ev = this.currentState.getStateEvents(_event2.EventType.RoomHistoryVisibility, ""); return (ev === null || ev === void 0 ? void 0 : (_ev$getContent = ev.getContent()) === null || _ev$getContent === void 0 ? void 0 : _ev$getContent.history_visibility) !== "joined"; } /** * Get the default room name (i.e. what a given user would see if the * room had no m.room.name) * @param userId - The userId from whose perspective we want * to calculate the default name * @returns The default room name */ getDefaultRoomName(userId) { return this.calculateRoomName(userId, true); } /** * Check if the given user_id has the given membership state. * @param userId - The user ID to check. * @param membership - The membership e.g. `'join'` * @returns True if this user_id has the given membership state. */ hasMembershipState(userId, membership) { const member = this.getMember(userId); if (!member) { return false; } return member.membership === membership; } /** * Add a timelineSet for this room with the given filter * @param filter - The filter to be applied to this timelineSet * @param opts - Configuration options * @returns The timelineSet */ getOrCreateFilteredTimelineSet(filter, { prepopulateTimeline = true, useSyncEvents = true, pendingEvents = true } = {}) { if (this.filteredTimelineSets[filter.filterId]) { return this.filteredTimelineSets[filter.filterId]; } const opts = Object.assign({ filter, pendingEvents }, this.opts); const timelineSet = new _eventTimelineSet.EventTimelineSet(this, opts); this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]); if (useSyncEvents) { this.filteredTimelineSets[filter.filterId] = timelineSet; this.timelineSets.push(timelineSet); } const unfilteredLiveTimeline = this.getLiveTimeline(); // Not all filter are possible to replicate client-side only // When that's the case we do not want to prepopulate from the live timeline // as we would get incorrect results compared to what the server would send back if (prepopulateTimeline) { // populate up the new timelineSet with filtered events from our live // unfiltered timeline. // // XXX: This is risky as our timeline // may have grown huge and so take a long time to filter. // see https://github.com/vector-im/vector-web/issues/2109 unfilteredLiveTimeline.getEvents().forEach(function (event) { timelineSet.addLiveEvent(event); }); // find the earliest unfiltered timeline let timeline = unfilteredLiveTimeline; while (timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS)) { timeline = timeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.BACKWARDS); } timelineSet.getLiveTimeline().setPaginationToken(timeline.getPaginationToken(_eventTimeline.EventTimeline.BACKWARDS), _eventTimeline.EventTimeline.BACKWARDS); } else if (useSyncEvents) { const livePaginationToken = unfilteredLiveTimeline.getPaginationToken(_eventTimeline.Direction.Forward); timelineSet.getLiveTimeline().setPaginationToken(livePaginationToken, _eventTimeline.Direction.Backward); } // alternatively, we could try to do something like this to try and re-paginate // in the filtered events from nothing, but Mark says it's an abuse of the API // to do so: // // timelineSet.resetLiveTimeline( // unfilteredLiveTimeline.getPaginationToken(EventTimeline.FORWARDS) // ); return timelineSet; } async getThreadListFilter(filterType = _thread.ThreadFilterType.All) { const myUserId = this.client.getUserId(); const filter = new _filter.Filter(myUserId); const definition = { room: { timeline: { [_thread.FILTER_RELATED_BY_REL_TYPES.name]: [_thread.THREAD_RELATION_TYPE.name] } } }; if (filterType === _thread.ThreadFilterType.My) { definition.room.timeline[_thread.FILTER_RELATED_BY_SENDERS.name] = [myUserId]; } filter.setDefinition(definition); const filterId = await this.client.getOrCreateFilter(`THREAD_PANEL_${this.roomId}_${filterType}`, filter); filter.filterId = filterId; return filter; } async createThreadTimelineSet(filterType) { let timelineSet; if (_thread.Thread.hasServerSideListSupport) { timelineSet = new _eventTimelineSet.EventTimelineSet(this, _objectSpread(_objectSpread({}, this.opts), {}, { pendingEvents: false }), undefined, undefined, filterType !== null && filterType !== void 0 ? filterType : _thread.ThreadFilterType.All); this.reEmitter.reEmit(timelineSet, [RoomEvent.Timeline, RoomEvent.TimelineReset]); } else if (_thread.Thread.hasServerSideSupport) { const filter = await this.getThreadListFilter(filterType); timelineSet = this.getOrCreateFilteredTimelineSet(filter, { prepopulateTimeline: false, useSyncEvents: false, pendingEvents: false }); } else { timelineSet = new _eventTimelineSet.EventTimelineSet(this, { pendingEvents: false }); Array.from(this.threads).forEach(([, thread]) => { if (thread.length === 0) return; const currentUserParticipated = thread.timeline.some(event => { return event.getSender() === this.client.getUserId(); }); if (filterType !== _thread.ThreadFilterType.My || currentUserParticipated) { timelineSet.getLiveTimeline().addEvent(thread.rootEvent, { toStartOfTimeline: false }); } }); } return timelineSet; } /** * Takes the given thread root events and creates threads for them. */ processThreadRoots(events, toStartOfTimeline) { for (const rootEvent of events) { _eventTimeline.EventTimeline.setEventMetadata(rootEvent, this.currentState, toStartOfTimeline); if (!this.getThread(rootEvent.getId())) { this.createThread(rootEvent.getId(), rootEvent, [], toStartOfTimeline); } } } /** * Fetch the bare minimum of room threads required for the thread list to work reliably. * With server support that means fetching one page. * Without server support that means fetching as much at once as the server allows us to. */ async fetchRoomThreads() { if (this.threadsReady || !this.client.supportsThreads()) { return; } if (_thread.Thread.hasServerSideListSupport) { await Promise.all([this.fetchRoomThreadList(_thread.ThreadFilterType.All), this.fetchRoomThreadList(_thread.ThreadFilterType.My)]); } else { const allThreadsFilter = await this.getThreadListFilter(); const { chunk: events } = await this.client.createMessagesRequest(this.roomId, "", Number.MAX_SAFE_INTEGER, _eventTimeline.Direction.Backward, allThreadsFilter); if (!events.length) return; // Sorted by last_reply origin_server_ts const threadRoots = events.map(this.client.getEventMapper()).sort((eventA, eventB) => { /** * `origin_server_ts` in a decentralised world is far from ideal * but for lack of any better, we will have to use this * Long term the sorting should be handled by homeservers and this * is only meant as a short term patch */ const threadAMetadata = eventA.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name); const threadBMetadata = eventB.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name); return threadAMetadata.latest_event.origin_server_ts - threadBMetadata.latest_event.origin_server_ts; }); let latestMyThreadsRootEvent; const roomState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); for (const rootEvent of threadRoots) { var _this$threadsTimeline3; const opts = { duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Ignore, fromCache: false, roomState }; (_this$threadsTimeline3 = this.threadsTimelineSets[0]) === null || _this$threadsTimeline3 === void 0 ? void 0 : _this$threadsTimeline3.addLiveEvent(rootEvent, opts); const threadRelationship = rootEvent.getServerAggregatedRelation(_thread.THREAD_RELATION_TYPE.name); if (threadRelationship !== null && threadRelationship !== void 0 && threadRelationship.current_user_participated) { var _this$threadsTimeline4; (_this$threadsTimeline4 = this.threadsTimelineSets[1]) === null || _this$threadsTimeline4 === void 0 ? void 0 : _this$threadsTimeline4.addLiveEvent(rootEvent, opts); latestMyThreadsRootEvent = rootEvent; } } this.processThreadRoots(threadRoots, true); this.client.decryptEventIfNeeded(threadRoots[threadRoots.length - 1]); if (latestMyThreadsRootEvent) { this.client.decryptEventIfNeeded(latestMyThreadsRootEvent); } } this.on(_thread.ThreadEvent.NewReply, this.onThreadNewReply); this.on(_thread.ThreadEvent.Delete, this.onThreadDelete); this.threadsReady = true; } async processPollEvents(events) { const processPollStartEvent = event => { if (!_matrixEventsSdk.M_POLL_START.matches(event.getType())) return; try { const poll = new _poll.Poll(event, this.client, this); this.polls.set(event.getId(), poll); this.emit(_poll.PollEvent.New, poll); } catch {} // poll creation can fail for malformed poll start events }; const processPollRelationEvent = event => { const relationEventId = event.relationEventId; if (relationEventId && this.polls.has(relationEventId)) { const poll = this.polls.get(relationEventId); poll === null || poll === void 0 ? void 0 : poll.onNewRelation(event); } }; const processPollEvent = event => { 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 */ async fetchRoomThreadList(filter) { const timelineSet = filter === _thread.ThreadFilterType.My ? this.threadsTimelineSets[1] : this.threadsTimelineSets[0]; const { chunk: events, end } = await this.client.createThreadListMessagesRequest(this.roomId, null, undefined, _eventTimeline.Direction.Backward, timelineSet.threadListType, timelineSet.getFilter()); timelineSet.getLiveTimeline().setPaginationToken(end !== null && end !== void 0 ? end : null, _eventTimeline.Direction.Backward); if (!events.length) return; const matrixEvents = events.map(this.client.getEventMapper()); this.processThreadRoots(matrixEvents, true); const roomState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); for (const rootEvent of matrixEvents) { timelineSet.addLiveEvent(rootEvent, { duplicateStrategy: _eventTimelineSet.DuplicateStrategy.Replace, fromCache: false, roomState }); } } onThreadNewReply(thread) { this.updateThreadRootEvents(thread, false, true); } onThreadDelete(thread) { var _timeline$getEvents; this.threads.delete(thread.id); const timeline = this.getTimelineForEvent(thread.id); const roomEvent = timeline === null || timeline === void 0 ? void 0 : (_timeline$getEvents = timeline.getEvents()) === null || _timeline$getEvents === void 0 ? void 0 : _timeline$getEvents.find(it => it.getId() === thread.id); if (roomEvent) { thread.clearEventMetadata(roomEvent); } else { _logger.logger.debug("onThreadDelete: Could not find root event in room timeline"); } for (const timelineSet of this.threadsTimelineSets) { timelineSet.removeEvent(thread.id); } } /** * Forget the timelineSet for this room with the given filter * * @param filter - the filter whose timelineSet is to be forgotten */ removeFilteredTimelineSet(filter) { const timelineSet = this.filteredTimelineSets[filter.filterId]; delete this.filteredTimelineSets[filter.filterId]; const i = this.timelineSets.indexOf(timelineSet); if (i > -1) { this.timelineSets.splice(i, 1); } } eventShouldLiveIn(event, events, roots) { var _this$client2; if (!((_this$client2 = this.client) !== null && _this$client2 !== void 0 && _this$client2.supportsThreads())) { return { shouldLiveInRoom: true, shouldLiveInThread: false }; } // A thread root is always shown in both timelines if (event.isThreadRoot || roots !== null && roots !== void 0 && roots.has(event.getId())) { return { shouldLiveInRoom: true, shouldLiveInThread: true, threadId: event.getId() }; } // A thread relation is always only shown in a thread if (event.isRelation(_thread.THREAD_RELATION_TYPE.name)) { return { shouldLiveInRoom: false, shouldLiveInThread: true, threadId: event.threadRootId }; } const parentEventId = event.getAssociatedId(); let parentEvent; if (parentEventId) { var _this$findEventById; parentEvent = (_this$findEventById = this.findEventById(parentEventId)) !== null && _this$findEventById !== void 0 ? _this$findEventById : events === null || events === void 0 ? void 0 : 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 !== null && roots !== void 0 && 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 }; } findThreadForEvent(event) { if (!event) return null; const { threadId } = this.eventShouldLiveIn(event); return threadId ? this.getThread(threadId) : null; } addThreadedEvents(threadId, events, toStartOfTimeline = false) { let thread = this.getThread(threadId); if (!thread) { var _this$findEventById2; const rootEvent = (_this$findEventById2 = this.findEventById(threadId)) !== null && _this$findEventById2 !== void 0 ? _this$findEventById2 : 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" */ processThreadedEvents(events, toStartOfTimeline) { events.forEach(this.applyRedaction); const eventsByThread = {}; for (const event of events) { var _eventsByThread; const { threadId, shouldLiveInThread } = this.eventShouldLiveIn(event); if (shouldLiveInThread && !eventsByThread[threadId]) { eventsByThread[threadId] = []; } (_eventsByThread = eventsByThread[threadId]) === null || _eventsByThread === void 0 ? void 0 : _eventsByThread.push(event); } Object.entries(eventsByThread).map(([threadId, threadEvents]) => this.addThreadedEvents(threadId, threadEvents, toStartOfTimeline)); } createThread(threadId, rootEvent, events = [], toStartOfTimeline) { var _this$cachedThreadRea, _this$lastThread, _this$lastThread$root; if (this.threads.has(threadId)) { return this.threads.get(threadId); } if (rootEvent) { const relatedEvents = this.relations.getAllChildEventsForEvent(rootEvent.getId()); if (relatedEvents !== null && relatedEvents !== void 0 && relatedEvents.length) { // Include all relations of the root event, given it'll be visible in both timelines, // except `m.replace` as that will already be applied atop the event using `MatrixEvent::makeReplaced` events = events.concat(relatedEvents.filter(e => !e.isRelation(_event2.RelationType.Replace))); } } const thread = new _thread.Thread(threadId, rootEvent, { room: this, client: this.client, pendingEventOrdering: this.opts.pendingEventOrdering, receipts: (_this$cachedThreadRea = this.cachedThreadReadReceipts.get(threadId)) !== null && _this$cachedThreadRea !== void 0 ? _this$cachedThreadRea : [] }); // All read receipts should now come down from sync, we do not need to keep // a reference to the cached receipts anymore. this.cachedThreadReadReceipts.delete(threadId); // If we managed to create a thread and figure out its `id` then we can use it // This has to happen before thread.addEvents, because that adds events to the eventtimeline, and the // eventtimeline sometimes looks up thread information via the room. this.threads.set(thread.id, thread); // This is necessary to be able to jump to events in threads: // If we jump to an event in a thread where neither the event, nor the root, // nor any thread event are loaded yet, we'll load the event as well as the thread root, create the thread, // and pass the event through this. thread.addEvents(events, false); this.reEmitter.reEmit(thread, [_thread.ThreadEvent.Delete, _thread.ThreadEvent.Update, _thread.ThreadEvent.NewReply, RoomEvent.Timeline, RoomEvent.TimelineReset]); const isNewer = ((_this$lastThread = this.lastThread) === null || _this$lastThread === void 0 ? void 0 : _this$lastThread.rootEvent) && (rootEvent === null || rootEvent === void 0 ? void 0 : rootEvent.localTimestamp) && ((_this$lastThread$root = this.lastThread.rootEvent) === null || _this$lastThread$root === void 0 ? void 0 : _this$lastThread$root.localTimestamp) < (rootEvent === null || rootEvent === void 0 ? void 0 : rootEvent.localTimestamp); if (!this.lastThread || isNewer) { this.lastThread = thread; } if (this.threadsReady) { this.updateThreadRootEvents(thread, toStartOfTimeline, false); } this.emit(_thread.ThreadEvent.New, thread, toStartOfTimeline); return thread; } processLiveEvent(event) { this.applyRedaction(event); // Implement MSC3531: hiding messages. if (event.isVisibilityEvent()) { // This event changes the visibility of another event, record // the visibility change, inform clients if necessary. this.applyNewVisibilityEvent(event); } // If any pending visibility change is waiting for this (older) event, this.applyPendingVisibilityEvents(event); // Sliding Sync modifications: // The proxy cannot guarantee every sent event will have a transaction_id field, so we need // to check the event ID against the list of pending events if there is no transaction ID // field. Only do this for events sent by us though as it's potentially expensive to loop // the pending events map. const txnId = event.getUnsigned().transaction_id; if (!txnId && event.getSender() === this.myUserId) { // check the txn map for a matching event ID for (const [tid, localEvent] of this.txnToEvent) { if (localEvent.getId() === event.getId()) { _logger.logger.debug("processLiveEvent: found sent event without txn ID: ", tid, event.getId()); // update the unsigned field so we can re-use the same codepaths const unsigned = event.getUnsigned(); unsigned.transaction_id = tid; event.setUnsigned(unsigned); break; } } } } /** * Add an event to the end of this room's live timelines. Will fire * "Room.timeline". * * @param event - Event to be added * @param addLiveEventOptions - addLiveEvent options * @internal * * @remarks * Fires {@link RoomEvent.Timeline} */ addLiveEvent(event, addLiveEventOptions) { const { duplicateStrategy, timelineWasEmpty, fromCache } = addLiveEventOptions; // add to our timeline sets for (const timelineSet of this.timelineSets) { timelineSet.addLiveEvent(event, { duplicateStrategy, fromCache, timelineWasEmpty }); } // synthesize and inject implicit read receipts // Done after adding the event because otherwise the app would get a read receipt // pointing to an event that wasn't yet in the timeline // Don't synthesize RR for m.room.redaction as this causes the RR to go missing. if (event.sender && event.getType() !== _event2.EventType.RoomRedaction) { this.addReceipt((0, _readReceipt.synthesizeReceipt)(event.sender.userId, event, _read_receipts.ReceiptType.Read), true); // Any live events from a user could be taken as implicit // presence information: evidence that they are currently active. // ...except in a world where we use 'user.currentlyActive' to reduce // presence spam, this isn't very useful - we'll get a transition when // they are no longer currently active anyway. So don't bother to // reset the lastActiveAgo and lastPresenceTs from the RoomState's user. } } /** * Add a pending outgoing event to this room. * *
The event is added to either the pendingEventList, or the live timeline, * depending on the setting of opts.pendingEventOrdering. * *
This is an internal method, intended for use by MatrixClient. * * @param event - The event to add. * * @param txnId - Transaction id for this outgoing event * * @throws if the event doesn't have status SENDING, or we aren't given a * unique transaction id. * * @remarks * Fires {@link RoomEvent.LocalEchoUpdated} */ addPendingEvent(event, txnId) { if (event.status !== _eventStatus.EventStatus.SENDING && event.status !== _eventStatus.EventStatus.NOT_SENT) { throw new Error("addPendingEvent called on an event with status " + event.status); } if (this.txnToEvent.get(txnId)) { throw new Error("addPendingEvent called on an event with known txnId " + txnId); } // call setEventMetadata to set up event.sender etc // as event is shared over all timelineSets, we set up its metadata based // on the unfiltered timelineSet. _eventTimeline.EventTimeline.setEventMetadata(event, this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS), false); this.txnToEvent.set(txnId, event); if (this.pendingEventList) { if (this.pendingEventList.some(e => e.status === _eventStatus.EventStatus.NOT_SENT)) { _logger.logger.warn("Setting event as NOT_SENT due to messages in the same state"); event.setStatus(_eventStatus.EventStatus.NOT_SENT); } this.pendingEventList.push(event); this.savePendingEvents(); if (event.isRelation()) { // For pending events, add them to the relations collection immediately. // (The alternate case below already covers this as part of adding to // the timeline set.) this.aggregateNonLiveRelation(event); } if (event.isRedaction()) { const redactId = event.event.redacts; let redactedEvent = this.pendingEventList.find(e => e.getId() === redactId); if (!redactedEvent && redactId) { redactedEvent = this.findEventById(redactId); } if (redactedEvent) { redactedEvent.markLocallyRedacted(event); this.emit(RoomEvent.Redaction, event, this); } } } else { for (const timelineSet of this.timelineSets) { if (timelineSet.getFilter()) { if (timelineSet.getFilter().filterRoomTimeline([event]).length) { timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), { toStartOfTimeline: false }); } } else { timelineSet.addEventToTimeline(event, timelineSet.getLiveTimeline(), { toStartOfTimeline: false }); } } } this.emit(RoomEvent.LocalEchoUpdated, event, this); } /** * Persists all pending events to local storage * * If the current room is encrypted only encrypted events will be persisted * all messages that are not yet encrypted will be discarded * * This is because the flow of EVENT_STATUS transition is * `queued => sending => encrypting => sending => sent` * * Steps 3 and 4 are skipped for unencrypted room. * It is better to discard an unencrypted message rather than persisting * it locally for everyone to read */ savePendingEvents() { if (this.pendingEventList) { const pendingEvents = this.pendingEventList.map(event => { return _objectSpread(_objectSpread({}, event.event), {}, { txn_id: event.getTxnId() }); }).filter(event => { // Filter out the unencrypted messages if the room is encrypted const isEventEncrypted = event.type === _event2.EventType.RoomMessageEncrypted; const isRoomEncrypted = this.client.isRoomEncrypted(this.roomId); return isEventEncrypted || !isRoomEncrypted; }); this.client.store.setPendingEvents(this.roomId, pendingEvents); } } /** * Used to aggregate the local echo for a relation, and also * for re-applying a relation after it's redaction has been cancelled, * as the local echo for the redaction of the relation would have * un-aggregated the relation. Note that this is different from regular messages, * which are just kept detached for their local echo. * * Also note that live events are aggregated in the live EventTimelineSet. * @param event - the relation event that needs to be aggregated. */ aggregateNonLiveRelation(event) { this.relations.aggregateChildEvent(event); } getEventForTxnId(txnId) { return this.txnToEvent.get(txnId); } /** * Deal with the echo of a message we sent. * *
We move the event to the live timeline if it isn't there already, and * update it. * * @param remoteEvent - The event received from * /sync * @param localEvent - The local echo, which * should be either in the pendingEventList or the timeline. * * @internal * * @remarks * Fires {@link RoomEvent.LocalEchoUpdated} */ handleRemoteEcho(remoteEvent, localEvent) { const oldEventId = localEvent.getId(); const newEventId = remoteEvent.getId(); const oldStatus = localEvent.status; _logger.logger.debug(`Got remote echo for event ${oldEventId} -> ${newEventId} old status ${oldStatus}`); // no longer pending this.txnToEvent.delete(remoteEvent.getUnsigned().transaction_id); // if it's in the pending list, remove it if (this.pendingEventList) { this.removePendingEvent(oldEventId); } // replace the event source (this will preserve the plaintext payload if // any, which is good, because we don't want to try decoding it again). localEvent.handleRemoteEcho(remoteEvent.event); const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(remoteEvent); const thread = threadId ? this.getThread(threadId) : null; thread === null || thread === void 0 ? void 0 : thread.setEventMetadata(localEvent); thread === null || thread === void 0 ? void 0 : 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. * *
This is an internal method. * * @param event - local echo event * @param newStatus - status to assign * @param newEventId - new event id to assign. Ignored unless newStatus == EventStatus.SENT. * * @remarks * Fires {@link RoomEvent.LocalEchoUpdated} */ updatePendingEvent(event, newStatus, newEventId) { _logger.logger.log(`setting pendingEvent status to ${newStatus} in ${event.getRoomId()} ` + `event ID ${event.getId()} -> ${newEventId}`); // if the message was sent, we expect an event id if (newStatus == _eventStatus.EventStatus.SENT && !newEventId) { throw new Error("updatePendingEvent called with status=SENT, but no new event id"); } // SENT races against /sync, so we have to special-case it. if (newStatus == _eventStatus.EventStatus.SENT) { const timeline = this.getTimelineForEvent(newEventId); if (timeline) { // we've already received the event via the event stream. // nothing more to do here, assuming the transaction ID was correctly matched. // Let's check that. const remoteEvent = this.findEventById(newEventId); const remoteTxnId = remoteEvent === null || remoteEvent === void 0 ? void 0 : 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 !== null && allowed !== void 0 && allowed.includes(newStatus))) { throw new Error(`Invalid EventStatus transition ${oldStatus}->${newStatus}`); } event.setStatus(newStatus); if (newStatus == _eventStatus.EventStatus.SENT) { // update the event id event.replaceLocalEventId(newEventId); const { shouldLiveInRoom, threadId } = this.eventShouldLiveIn(event); const thread = threadId ? this.getThread(threadId) : undefined; thread === null || thread === void 0 ? void 0 : thread.setEventMetadata(event); thread === null || thread === void 0 ? void 0 : thread.timelineSet.replaceEventId(oldEventId, newEventId); if (shouldLiveInRoom) { // if the event was already in the timeline (which will be the case if // opts.pendingEventOrdering==chronological), we need to update the // timeline map. for (const timelineSet of this.timelineSets) { timelineSet.replaceEventId(oldEventId, newEventId); } } } else if (newStatus == _eventStatus.EventStatus.CANCELLED) { // remove it from the pending event list, or the timeline. if (this.pendingEventList) { const removedEvent = this.getPendingEvent(oldEventId); this.removePendingEvent(oldEventId); if (removedEvent !== null && removedEvent !== void 0 && removedEvent.isRedaction()) { this.revertRedactionLocalEcho(removedEvent); } } this.removeEvent(oldEventId); } this.savePendingEvents(); this.emit(RoomEvent.LocalEchoUpdated, event, this, oldEventId, oldStatus); } revertRedactionLocalEcho(redactionEvent) { const redactId = redactionEvent.event.redacts; if (!redactId) { return; } const redactedEvent = this.getUnfilteredTimelineSet().findEventById(redactId); if (redactedEvent) { redactedEvent.unmarkLocallyRedacted(); // re-render after undoing redaction this.emit(RoomEvent.RedactionCancelled, redactionEvent, this); // reapply relation now redaction failed if (redactedEvent.isRelation()) { this.aggregateNonLiveRelation(redactedEvent); } } } /** * Add some events to this room. This can include state events, message * events and typing notifications. These events are treated as "live" so * they will go to the end of the timeline. * * @param events - A list of events to add. * @param addLiveEventOptions - addLiveEvent options * @throws If `duplicateStrategy` is not falsey, 'replace' or 'ignore'. */ addLiveEvents(events, duplicateStrategyOrOpts, fromCache = false) { let duplicateStrategy = duplicateStrategyOrOpts; let timelineWasEmpty = false; if (typeof duplicateStrategyOrOpts === "object") { ({ duplicateStrategy, fromCache = false, /* roomState, (not used here) */ timelineWasEmpty } = duplicateStrategyOrOpts); } else if (duplicateStrategyOrOpts !== undefined) { // Deprecation warning // FIXME: Remove after 2023-06-01 (technical debt) _logger.logger.warn("Overload deprecated: " + "`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` " + "is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`"); } if (duplicateStrategy && ["replace", "ignore"].indexOf(duplicateStrategy) === -1) { throw new Error("duplicateStrategy MUST be either 'replace' or 'ignore'"); } // sanity check that the live timeline is still live for (let i = 0; i < this.timelineSets.length; i++) { const liveTimeline = this.timelineSets[i].getLiveTimeline(); if (liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS)) { throw new Error("live timeline " + i + " is no longer live - it has a pagination token " + "(" + liveTimeline.getPaginationToken(_eventTimeline.EventTimeline.FORWARDS) + ")"); } if (liveTimeline.getNeighbouringTimeline(_eventTimeline.EventTimeline.FORWARDS)) { throw new Error(`live timeline ${i} is no longer live - it has a neighbouring timeline`); } } const threadRoots = this.findThreadRoots(events); const eventsByThread = {}; const options = { duplicateStrategy, fromCache, timelineWasEmpty }; for (const event of events) { var _eventsByThread2; // 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 !== null && threadId !== void 0 ? threadId : ""]) { eventsByThread[threadId !== null && threadId !== void 0 ? threadId : ""] = []; } (_eventsByThread2 = eventsByThread[threadId !== null && threadId !== void 0 ? threadId : ""]) === null || _eventsByThread2 === void 0 ? void 0 : _eventsByThread2.push(event); if (shouldLiveInRoom) { this.addLiveEvent(event, options); } } Object.entries(eventsByThread).forEach(([threadId, threadEvents]) => { this.addThreadedEvents(threadId, threadEvents, false); }); } partitionThreadedEvents(events) { // Indices to the events array, for readability const ROOM = 0; const THREAD = 1; if (this.client.supportsThreads()) { const threadRoots = this.findThreadRoots(events); return events.reduce((memo, event) => { const { shouldLiveInRoom, shouldLiveInThread, threadId } = this.eventShouldLiveIn(event, events, threadRoots); if (shouldLiveInRoom) { memo[ROOM].push(event); } if (shouldLiveInThread) { event.setThreadId(threadId !== null && threadId !== void 0 ? threadId : ""); memo[THREAD].push(event); } return memo; }, [[], []]); } else { // When `experimentalThreadSupport` is disabled treat all events as timelineEvents return [events, []]; } } /** * Given some events, find the IDs of all the thread roots that are referred to by them. */ findThreadRoots(events) { const threadRoots = new Set(); for (const event of events) { if (event.isRelation(_thread.THREAD_RELATION_TYPE.name)) { var _event$relationEventI; threadRoots.add((_event$relationEventI = event.relationEventId) !== null && _event$relationEventI !== void 0 ? _event$relationEventI : ""); } } return threadRoots; } /** * Add a receipt event to the room. * @param event - The m.receipt event. * @param synthetic - True if this event is implicit. */ addReceipt(event, synthetic = false) { const content = event.getContent(); Object.keys(content).forEach(eventId => { Object.keys(content[eventId]).forEach(receiptType => { Object.keys(content[eventId][receiptType]).forEach(userId => { var _receipt$thread_id, _this$unthreadedRecei, _this$unthreadedRecei2; const receipt = content[eventId][receiptType][userId]; const receiptForMainTimeline = !receipt.thread_id || receipt.thread_id === _read_receipts.MAIN_ROOM_TIMELINE; const receiptDestination = receiptForMainTimeline ? this : this.threads.get((_receipt$thread_id = receipt.thread_id) !== null && _receipt$thread_id !== void 0 ? _receipt$thread_id : ""); if (receiptDestination) { receiptDestination.addReceiptToStructure(eventId, receiptType, userId, receipt, synthetic); // If the read receipt sent for the logged in user matches // the last event of the live timeline, then we know for a fact // that the user has read that message. // We can mark the room as read and not wait for the local echo // from synapse // This needs to be done after the initial sync as we do not want this // logic to run whilst the room is being initialised if (this.client.isInitialSyncComplete() && userId === this.client.getUserId()) { const lastEvent = receiptDestination.timeline[receiptDestination.timeline.length - 1]; if (lastEvent && eventId === lastEvent.getId() && userId === lastEvent.getSender()) { receiptDestination.setUnread(NotificationCountType.Total, 0); receiptDestination.setUnread(NotificationCountType.Highlight, 0); } } } else { var _this$cachedThreadRea2; // 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$cachedThreadRea2 = this.cachedThreadReadReceipts.get(receipt.thread_id)) !== null && _this$cachedThreadRea2 !== void 0 ? _this$cachedThreadRea2 : []), { 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$unthreadedRecei = (_this$unthreadedRecei2 = this.unthreadedReceipts.get(userId)) === null || _this$unthreadedRecei2 === void 0 ? void 0 : _this$unthreadedRecei2.ts) !== null && _this$unthreadedRecei !== void 0 ? _this$unthreadedRecei : 0)) { this.unthreadedReceipts.set(userId, receipt); } }); }); }); // send events after we've regenerated the structure & cache, otherwise things that // listened for the event would read stale data. this.emit(RoomEvent.Receipt, event, this); } /** * Adds/handles ephemeral events such as typing notifications and read receipts. * @param events - A list of events to process */ addEphemeralEvents(events) { for (const event of events) { if (event.getType() === _event2.EventType.Typing) { this.currentState.setTypingEvent(event); } else if (event.getType() === _event2.EventType.Receipt) { this.addReceipt(event); } // else ignore - life is too short for us to care about these events } } /** * Removes events from this room. * @param eventIds - A list of eventIds to remove. */ removeEvents(eventIds) { for (const eventId of eventIds) { this.removeEvent(eventId); } } /** * Removes a single event from this room. * * @param eventId - The id of the event to remove * * @returns true if the event was removed from any of the room's timeline sets */ removeEvent(eventId) { let removedAny = false; for (const timelineSet of this.timelineSets) { const removed = timelineSet.removeEvent(eventId); if (removed) { if (removed.isRedaction()) { this.revertRedactionLocalEcho(removed); } removedAny = true; } } return removedAny; } /** * Recalculate various aspects of the room, including the room name and * room summary. Call this any time the room's current state is modified. * May fire "Room.name" if the room name is updated. * * @remarks * Fires {@link RoomEvent.Name} */ recalculate() { // set fake stripped state events if this is an invite room so logic remains // consistent elsewhere. const membershipEvent = this.currentState.getStateEvents(_event2.EventType.RoomMember, this.myUserId); if (membershipEvent) { const membership = membershipEvent.getContent().membership; this.updateMyMembership(membership); if (membership === "invite") { const strippedStateEvents = membershipEvent.getUnsigned().invite_room_state || []; strippedStateEvents.forEach(strippedEvent => { const existingEvent = this.currentState.getStateEvents(strippedEvent.type, strippedEvent.state_key); if (!existingEvent) { // set the fake stripped event instead this.currentState.setStateEvents([new _event.MatrixEvent({ type: strippedEvent.type, state_key: strippedEvent.state_key, content: strippedEvent.content, event_id: "$fake" + Date.now(), room_id: this.roomId, user_id: this.myUserId // technically a lie })]); } }); } } const oldName = this.name; this.name = this.calculateRoomName(this.myUserId); this.normalizedName = (0, utils.normalize)(this.name); this.summary = new _roomSummary.RoomSummary(this.roomId, { title: this.name }); if (oldName !== this.name) { this.emit(RoomEvent.Name, this); } } /** * Update the room-tag event for the room. The previous one is overwritten. * @param event - the m.tag event */ addTags(event) { // event content looks like: // content: { // tags: { // $tagName: { $metadata: $value }, // $tagName: { $metadata: $value }, // } // } // XXX: do we need to deep copy here? this.tags = event.getContent().tags || {}; // XXX: we could do a deep-comparison to see if the tags have really // changed - but do we want to bother? this.emit(RoomEvent.Tags, event, this); } /** * Update the account_data events for this room, overwriting events of the same type. * @param events - an array of account_data events to add */ addAccountData(events) { for (const event of events) { if (event.getType() === "m.tag") { this.addTags(event); } const eventType = event.getType(); const lastEvent = this.accountData.get(eventType); this.accountData.set(eventType, event); this.emit(RoomEvent.AccountData, event, this, lastEvent); } } /** * Access account_data event of given event type for this room * @param type - the type of account_data event to be accessed * @returns the account_data event in question */ getAccountData(type) { return this.accountData.get(type); } /** * Returns whether the syncing user has permission to send a message in the room * @returns true if the user should be permitted to send * message events into the room. */ maySendMessage() { return this.getMyMembership() === "join" && (this.client.isRoomEncrypted(this.roomId) ? this.currentState.maySendEvent(_event2.EventType.RoomMessageEncrypted, this.myUserId) : this.currentState.maySendEvent(_event2.EventType.RoomMessage, this.myUserId)); } /** * Returns whether the given user has permissions to issue an invite for this room. * @param userId - the ID of the Matrix user to check permissions for * @returns true if the user should be permitted to issue invites for this room. */ canInvite(userId) { let canInvite = this.getMyMembership() === "join"; const powerLevelsEvent = this.currentState.getStateEvents(_event2.EventType.RoomPowerLevels, ""); const powerLevels = powerLevelsEvent && powerLevelsEvent.getContent(); const me = this.getMember(userId); if (powerLevels && me && powerLevels.invite > me.powerLevel) { canInvite = false; } return canInvite; } /** * Returns the join rule based on the m.room.join_rule state event, defaulting to `invite`. * @returns the join_rule applied to this room */ getJoinRule() { return this.currentState.getJoinRule(); } /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. * @returns the history_visibility applied to this room */ getHistoryVisibility() { return this.currentState.getHistoryVisibility(); } /** * Returns the history visibility based on the m.room.history_visibility state event, defaulting to `shared`. * @returns the history_visibility applied to this room */ getGuestAccess() { return this.currentState.getGuestAccess(); } /** * Returns the type of the room from the `m.room.create` event content or undefined if none is set * @returns the type of the room. */ getType() { const createEvent = this.currentState.getStateEvents(_event2.EventType.RoomCreate, ""); if (!createEvent) { if (!this.getTypeWarning) { _logger.logger.warn("[getType] Room " + this.roomId + " does not have an m.room.create event"); this.getTypeWarning = true; } return undefined; } return createEvent.getContent()[_event2.RoomCreateTypeField]; } /** * Returns whether the room is a space-room as defined by MSC1772. * @returns true if the room's type is RoomType.Space */ isSpaceRoom() { return this.getType() === _event2.RoomType.Space; } /** * Returns whether the room is a call-room as defined by MSC3417. * @returns true if the room's type is RoomType.UnstableCall */ isCallRoom() { return this.getType() === _event2.RoomType.UnstableCall; } /** * Returns whether the room is a video room. * @returns true if the room's type is RoomType.ElementVideo */ isElementVideoRoom() { return this.getType() === _event2.RoomType.ElementVideo; } /** * Find the predecessor of this room. * * @param msc3946ProcessDynamicPredecessor - if true, look for an * m.room.predecessor state event and use it if found (MSC3946). * @returns null if this room has no predecessor. Otherwise, returns * the roomId, last eventId and viaServers of the predecessor room. * * If msc3946ProcessDynamicPredecessor is true, use m.predecessor events * as well as m.room.create events to find predecessors. * * Note: if an m.predecessor event is used, eventId may be undefined * since last_known_event_id is optional. * * Note: viaServers may be undefined, and will definitely be undefined if * this predecessor comes from a RoomCreate event (rather than a * RoomPredecessor, which has the optional via_servers property). */ findPredecessor(msc3946ProcessDynamicPredecessor = false) { const currentState = this.getLiveTimeline().getState(_eventTimeline.EventTimeline.FORWARDS); if (!currentState) { return null; } return currentState.findPredecessor(msc3946ProcessDynamicPredecessor); } roomNameGenerator(state) { if (this.client.roomNameGenerator) { const name = this.client.roomNameGenerator(this.roomId, state); if (name !== null) { return name; } } switch (state.type) { case RoomNameType.Actual: return state.name; case RoomNameType.Generated: switch (state.subtype) { case "Inviting": return `Inviting ${memberNamesToRoomName(state.names, state.count)}`; default: return memberNamesToRoomName(state.names, state.count); } case RoomNameType.EmptyRoom: if (state.oldName) { return `Empty room (was ${state.oldName})`; } else { return "Empty room"; } } } /** * This is an internal method. Calculates the name of the room from the current * room state. * @param userId - The client's user ID. Used to filter room members * correctly. * @param ignoreRoomNameEvent - Return the implicit room name that we'd see if there * was no m.room.name event. * @returns The calculated room name. */ calculateRoomName(userId, ignoreRoomNameEvent = false) { if (!ignoreRoomNameEvent) { // check for an alias, if any. for now, assume first alias is the // official one. const mRoomName = this.currentState.getStateEvents(_event2.EventType.RoomName, ""); if (mRoomName !== null && mRoomName !== void 0 && mRoomName.getContent().name) { return this.roomNameGenerator({ type: RoomNameType.Actual, name: mRoomName.getContent().name }); } } const alias = this.getCanonicalAlias(); if (alias) { return this.roomNameGenerator({ type: RoomNameType.Actual, name: alias }); } const joinedMemberCount = this.currentState.getJoinedMemberCount(); const invitedMemberCount = this.currentState.getInvitedMemberCount(); // -1 because these numbers include the syncing user let inviteJoinCount = joinedMemberCount + invitedMemberCount - 1; // get service members (e.g. helper bots) for exclusion let excludedUserIds = []; const mFunctionalMembers = this.currentState.getStateEvents(_event2.UNSTABLE_ELEMENT_FUNCTIONAL_USERS.name, ""); if (Array.isArray(mFunctionalMembers === null || mFunctionalMembers === void 0 ? void 0 : mFunctionalMembers.getContent().service_members)) { excludedUserIds = mFunctionalMembers.getContent().service_members; } // get members that are NOT ourselves and are actually in the room. let otherNames = []; if (this.summaryHeroes) { // if we have a summary, the member state events should be in the room state this.summaryHeroes.forEach(userId => { // filter service members if (excludedUserIds.includes(userId)) { inviteJoinCount--; return; } const member = this.getMember(userId); otherNames.push(member ? member.name : userId); }); } else { let otherMembers = this.currentState.getMembers().filter(m => { return m.userId !== userId && (m.membership === "invite" || m.membership === "join"); }); otherMembers = otherMembers.filter(({ userId }) => { // filter service members if (excludedUserIds.includes(userId)) { inviteJoinCount--; return false; } return true; }); // make sure members have stable order otherMembers.sort((a, b) => utils.compare(a.userId, b.userId)); // only 5 first members, immitate summaryHeroes otherMembers = otherMembers.slice(0, 5); otherNames = otherMembers.map(m => m.name); } if (inviteJoinCount) { return this.roomNameGenerator({ type: RoomNameType.Generated, names: otherNames, count: inviteJoinCount }); } const myMembership = this.getMyMembership(); // if I have created a room and invited people through // 3rd party invites if (myMembership == "join") { const thirdPartyInvites = this.currentState.getStateEvents(_event2.EventType.RoomThirdPartyInvite); if (thirdPartyInvites !== null && thirdPartyInvites !== void 0 && thirdPartyInvites.length) { const thirdPartyNames = thirdPartyInvites.map(i => { return i.getContent().display_name; }); return this.roomNameGenerator({ type: RoomNameType.Generated, subtype: "Inviting", names: thirdPartyNames, count: thirdPartyNames.length + 1 }); } } // let's try to figure out who was here before let leftNames = otherNames; // if we didn't have heroes, try finding them in the room state if (!leftNames.length) { leftNames = this.currentState.getMembers().filter(m => { return m.userId !== userId && m.membership !== "invite" && m.membership !== "join"; }).map(m => m.name); } let oldName; if (leftNames.length) { oldName = this.roomNameGenerator({ type: RoomNameType.Generated, names: leftNames, count: leftNames.length + 1 }); } return this.roomNameGenerator({ type: RoomNameType.EmptyRoom, oldName }); } /** * When we receive a new visibility change event: * * - store this visibility change alongside the timeline, in case we * later need to apply it to an event that we haven't received yet; * - if we have already received the event whose visibility has changed, * patch it to reflect the visibility change and inform listeners. */ applyNewVisibilityEvent(event) { const visibilityChange = event.asVisibilityChange(); if (!visibilityChange) { // The event is ill-formed. return; } // Ignore visibility change events that are not emitted by moderators. const userId = event.getSender(); if (!userId) { return; } const isPowerSufficient = _event2.EVENT_VISIBILITY_CHANGE_TYPE.name && this.currentState.maySendStateEvent(_event2.EVENT_VISIBILITY_CHANGE_TYPE.name, userId) || _event2.EVENT_VISIBILITY_CHANGE_TYPE.altName && this.currentState.maySendStateEvent(_event2.EVENT_VISIBILITY_CHANGE_TYPE.altName, userId); if (!isPowerSufficient) { // Powerlevel is insufficient. return; } // Record this change in visibility. // If the event is not in our timeline and we only receive it later, // we may need to apply the visibility change at a later date. const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(visibilityChange.eventId); if (visibilityEventsOnOriginalEvent) { // It would be tempting to simply erase the latest visibility change // but we need to record all of the changes in case the latest change // is ever redacted. // // In practice, linear scans through `visibilityEvents` should be fast. // However, to protect against a potential DoS attack, we limit the // number of iterations in this loop. let index = visibilityEventsOnOriginalEvent.length - 1; const min = Math.max(0, visibilityEventsOnOriginalEvent.length - MAX_NUMBER_OF_VISIBILITY_EVENTS_TO_SCAN_THROUGH); for (; index >= min; --index) { const target = visibilityEventsOnOriginalEvent[index]; if (target.getTs() < event.getTs()) { break; } } if (index === -1) { visibilityEventsOnOriginalEvent.unshift(event); } else { visibilityEventsOnOriginalEvent.splice(index + 1, 0, event); } } else { this.visibilityEvents.set(visibilityChange.eventId, [event]); } // Finally, let's check if the event is already in our timeline. // If so, we need to patch it and inform listeners. const originalEvent = this.findEventById(visibilityChange.eventId); if (!originalEvent) { return; } originalEvent.applyVisibilityEvent(visibilityChange); } redactVisibilityChangeEvent(event) { // Sanity checks. if (!event.isVisibilityEvent) { throw new Error("expected a visibility change event"); } const relation = event.getRelation(); const originalEventId = relation === null || relation === void 0 ? void 0 : relation.event_id; const visibilityEventsOnOriginalEvent = this.visibilityEvents.get(originalEventId); if (!visibilityEventsOnOriginalEvent) { // No visibility changes on the original event. // In particular, this change event was not recorded, // most likely because it was ill-formed. return; } const index = visibilityEventsOnOriginalEvent.findIndex(change => change.getId() === event.getId()); if (index === -1) { // This change event was not recorded, most likely because // it was ill-formed. return; } // Remove visibility change. visibilityEventsOnOriginalEvent.splice(index, 1); // If we removed the latest visibility change event, propagate changes. if (index === visibilityEventsOnOriginalEvent.length) { const originalEvent = this.findEventById(originalEventId); if (!originalEvent) { return; } if (index === 0) { // We have just removed the only visibility change event. this.visibilityEvents.delete(originalEventId); originalEvent.applyVisibilityEvent(); } else { const newEvent = visibilityEventsOnOriginalEvent[visibilityEventsOnOriginalEvent.length - 1]; const newVisibility = newEvent.asVisibilityChange(); if (!newVisibility) { // Event is ill-formed. // This breaks our invariant. throw new Error("at this stage, visibility changes should be well-formed"); } originalEvent.applyVisibilityEvent(newVisibility); } } } /** * When we receive an event whose visibility has been altered by * a (more recent) visibility change event, patch the event in * place so that clients now not to display it. * * @param event - Any matrix event. If this event has at least one a * pending visibility change event, apply the latest visibility * change event. */ applyPendingVisibilityEvents(event) { const visibilityEvents = this.visibilityEvents.get(event.getId()); if (!visibilityEvents || visibilityEvents.length == 0) { // No pending visibility change in store. return; } const visibilityEvent = visibilityEvents[visibilityEvents.length - 1]; const visibilityChange = visibilityEvent.asVisibilityChange(); if (!visibilityChange) { return; } if (visibilityChange.visible) { // Events are visible by default, no need to apply a visibility change. // Note that we need to keep the visibility changes in `visibilityEvents`, // in case we later fetch an older visibility change event that is superseded // by `visibilityChange`. } if (visibilityEvent.getTs() < event.getTs()) { // Something is wrong, the visibility change cannot happen before the // event. Presumably an ill-formed event. return; } event.applyVisibilityEvent(visibilityChange); } /** * Find when a client has gained thread capabilities by inspecting the oldest * threaded receipt * @returns the timestamp of the oldest threaded receipt */ getOldestThreadedReceiptTs() { return this.oldestThreadedReceiptTs; } /** * Returns the most recent unthreaded receipt for a given user * @param userId - the MxID of the User * @returns an unthreaded Receipt. Can be undefined if receipts have been disabled * or a user chooses to use private read receipts (or we have simply not received * a receipt from this user yet). */ getLastUnthreadedReceiptFor(userId) { return this.unthreadedReceipts.get(userId); } /** * This issue should also be addressed on synapse's side and is tracked as part * of https://github.com/matrix-org/synapse/issues/14837 * * * We consider a room fully read if the current user has sent * the last event in the live timeline of that context and if the read receipt * we have on record matches. * This also detects all unread threads and applies the same logic to those * contexts */ fixupNotifications(userId) { super.fixupNotifications(userId); const unreadThreads = this.getThreads().filter(thread => this.getThreadUnreadNotificationCount(thread.id, NotificationCountType.Total) > 0); for (const thread of unreadThreads) { thread.fixupNotifications(userId); } } } // a map from current event status to a list of allowed next statuses exports.Room = Room; const ALLOWED_TRANSITIONS = { [_eventStatus.EventStatus.ENCRYPTING]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.CANCELLED], [_eventStatus.EventStatus.SENDING]: [_eventStatus.EventStatus.ENCRYPTING, _eventStatus.EventStatus.QUEUED, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.SENT], [_eventStatus.EventStatus.QUEUED]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.NOT_SENT, _eventStatus.EventStatus.CANCELLED], [_eventStatus.EventStatus.SENT]: [], [_eventStatus.EventStatus.NOT_SENT]: [_eventStatus.EventStatus.SENDING, _eventStatus.EventStatus.QUEUED, _eventStatus.EventStatus.CANCELLED], [_eventStatus.EventStatus.CANCELLED]: [] }; let RoomNameType; exports.RoomNameType = RoomNameType; (function (RoomNameType) { RoomNameType[RoomNameType["EmptyRoom"] = 0] = "EmptyRoom"; RoomNameType[RoomNameType["Generated"] = 1] = "Generated"; RoomNameType[RoomNameType["Actual"] = 2] = "Actual"; })(RoomNameType || (exports.RoomNameType = RoomNameType = {})); // Can be overriden by IMatrixClientCreateOpts::memberNamesToRoomNameFn function memberNamesToRoomName(names, count) { const countWithoutMe = count - 1; if (!names.length) { return "Empty room"; } else if (names.length === 1 && countWithoutMe <= 1) { return names[0]; } else if (names.length === 2 && countWithoutMe <= 2) { return `${names[0]} and ${names[1]}`; } else { const plural = countWithoutMe > 1; if (plural) { return `${names[0]} and ${countWithoutMe} others`; } else { return `${names[0]} and 1 other`; } } } //# sourceMappingURL=room.js.map