matrix-js-sdk
Version:
Matrix Client-Server SDK for Javascript
1,135 lines (1,057 loc) • 123 kB
JavaScript
"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.
*
* <p>For a room, we store an ordered sequence of timelines, which may or may not
* be continuous. Each timeline lists a series of events, as well as tracking
* the room state at the start and the end of the timeline. It also tracks
* forward and backward pagination tokens, as well as containing links to the
* next timeline in the sequence.
*
* <p>There is one special timeline - the 'live' timeline, which represents the
* timeline to which events are being added in real-time as they are received
* from the /sync API. Note that you should not retain references to this
* timeline - even if it is the current timeline right now, it may not remain
* so if the server gives us a timeline gap in /sync.
*
* <p>In order that we can find events from their ids later, we also maintain a
* map from event_id to timeline and index.
*
* @param roomId - Required. The ID of this room.
* @param client - Required. The client, used to lazy load members.
* @param myUserId - Required. The ID of the syncing user.
* @param opts - Configuration options
*/
constructor(roomId, client, myUserId, opts = {}) {
super();
// In some cases, we add listeners for every displayed Matrix event, so it's
// common to have quite a few more than the default limit.
this.roomId = roomId;
this.client = client;
this.myUserId = myUserId;
this.opts = opts;
(0, _defineProperty2.default)(this, "reEmitter", void 0);
(0, _defineProperty2.default)(this, "txnToEvent", {});
(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", {});
(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.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.
*
* <p>This is used when /sync returns a 'limited' timeline.
*
* @param backPaginationToken - token for back-paginating the new timeline
* @param forwardPaginationToken - token for forward-paginating the old live timeline,
* if absent or null, all timelines are reset, removing old ones (including the previous live
* timeline which would otherwise be unable to paginate forwards without this token).
* Removing just the old live timeline whilst preserving previous ones is not supported.
*/
resetLiveTimeline(backPaginationToken, forwardPaginationToken) {
for (const timelineSet of this.timelineSets) {
timelineSet.resetLiveTimeline(backPaginationToken !== 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