2026-09-13 14:14:38 -04:00
"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 = require ( "../utils" ) ;
var _event = require ( "./event" ) ;
var _eventStatus = require ( "./event-status" ) ;
var _roomMember = require ( "./room-member" ) ;
var _roomSummary = require ( "./room-summary" ) ;
var _logger = require ( "../logger" ) ;
var _ReEmitter = require ( "../ReEmitter" ) ;
var _event2 = require ( "../@types/event" ) ;
var _client = require ( "../client" ) ;
var _filter = require ( "../filter" ) ;
var _roomState = require ( "./room-state" ) ;
var _beacon = require ( "./beacon" ) ;
var _thread = require ( "./thread" ) ;
var _read _receipts = require ( "../@types/read_receipts" ) ;
var _relationsContainer = require ( "./relations-container" ) ;
var _readReceipt = require ( "./read-receipt" ) ;
var _poll = require ( "./poll" ) ;
function ownKeys ( object , enumerableOnly ) { var keys = Object . keys ( object ) ; if ( Object . getOwnPropertySymbols ) { var symbols = Object . getOwnPropertySymbols ( object ) ; enumerableOnly && ( symbols = symbols . filter ( function ( sym ) { return Object . getOwnPropertyDescriptor ( object , sym ) . enumerable ; } ) ) , keys . push . apply ( keys , symbols ) ; } return keys ; }
function _objectSpread ( target ) { for ( var i = 1 ; i < arguments . length ; i ++ ) { var source = null != arguments [ i ] ? arguments [ i ] : { } ; i % 2 ? ownKeys ( Object ( source ) , ! 0 ) . forEach ( function ( key ) { ( 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 ; } / *
Copyright 2015 - 2023 The Matrix . org Foundation C . I . C .
Licensed under the Apache License , Version 2.0 ( the "License" ) ;
you may not use this file except in compliance with the License .
You may obtain a copy of the License at
http : //www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing , software
distributed under the License is distributed on an "AS IS" BASIS ,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND , either express or implied .
See the License for the specific language governing permissions and
limitations under the License .
* /
2026-09-12 23:57:45 -04:00
// 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.
2026-09-13 14:14:38 -04:00
const KNOWN _SAFE _ROOM _VERSION = "10" ;
exports . KNOWN _SAFE _ROOM _VERSION = KNOWN _SAFE _ROOM _VERSION ;
2026-09-12 23:57:45 -04:00
const SAFE _ROOM _VERSIONS = [ "1" , "2" , "3" , "4" , "5" , "6" , "7" , "8" , "9" , "10" ] ;
// When inserting a visibility event affecting event `eventId`, we
// need to scan through existing visibility events for `eventId`.
// In theory, this could take an unlimited amount of time if:
//
// - the visibility event was sent by a moderator; and
// - `eventId` already has many visibility changes (usually, it should
// be 2 or less); and
// - for some reason, the visibility changes are received out of order
// (usually, this shouldn't happen at all).
//
// For this reason, we limit the number of events to scan through,
// expecting that a broken visibility change for a single event in
// an extremely uncommon case (possibly a DoS) is a small
// price to pay to keep matrix-js-sdk responsive.
const MAX _NUMBER _OF _VISIBILITY _EVENTS _TO _SCAN _THROUGH = 30 ;
2026-09-13 14:14:38 -04:00
let NotificationCountType = /*#__PURE__*/ function ( NotificationCountType ) {
2026-09-12 23:57:45 -04:00
NotificationCountType [ "Highlight" ] = "highlight" ;
NotificationCountType [ "Total" ] = "total" ;
return NotificationCountType ;
} ( { } ) ;
2026-09-13 14:14:38 -04:00
exports . NotificationCountType = NotificationCountType ;
let RoomEvent = /*#__PURE__*/ function ( RoomEvent ) {
2026-09-12 23:57:45 -04:00
RoomEvent [ "MyMembership" ] = "Room.myMembership" ;
RoomEvent [ "Tags" ] = "Room.tags" ;
RoomEvent [ "AccountData" ] = "Room.accountData" ;
RoomEvent [ "Receipt" ] = "Room.receipt" ;
RoomEvent [ "Name" ] = "Room.name" ;
RoomEvent [ "Redaction" ] = "Room.redaction" ;
RoomEvent [ "RedactionCancelled" ] = "Room.redactionCancelled" ;
RoomEvent [ "LocalEchoUpdated" ] = "Room.localEchoUpdated" ;
RoomEvent [ "Timeline" ] = "Room.timeline" ;
RoomEvent [ "TimelineReset" ] = "Room.timelineReset" ;
RoomEvent [ "TimelineRefresh" ] = "Room.TimelineRefresh" ;
RoomEvent [ "OldStateUpdated" ] = "Room.OldStateUpdated" ;
RoomEvent [ "CurrentStateUpdated" ] = "Room.CurrentStateUpdated" ;
RoomEvent [ "HistoryImportedWithinTimeline" ] = "Room.historyImportedWithinTimeline" ;
RoomEvent [ "UnreadNotifications" ] = "Room.UnreadNotifications" ;
return RoomEvent ;
} ( { } ) ;
2026-09-13 14:14:38 -04:00
exports . RoomEvent = RoomEvent ;
class Room extends _readReceipt . ReadReceipt {
2026-09-12 23:57:45 -04:00
/ * *
* 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 / s y n c .
*
* < 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.
2026-09-13 14:14:38 -04:00
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 ( ) ) ;
2026-09-12 23:57:45 -04:00
// Pending in-flight requests { string: MatrixEvent }
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "notificationCounts" , { } ) ;
( 0 , _defineProperty2 . default ) ( this , "threadNotifications" , new Map ( ) ) ;
( 0 , _defineProperty2 . default ) ( this , "cachedThreadReadReceipts" , new Map ( ) ) ;
2026-09-12 23:57:45 -04:00
// Useful to know at what point the current user has started using threads in this room
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "oldestThreadedReceiptTs" , Infinity ) ;
2026-09-12 23:57:45 -04:00
/ * *
* A record of the latest unthread receipts per user
* This is useful in determining whether a user has read a thread or not
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "unthreadedReceipts" , new Map ( ) ) ;
( 0 , _defineProperty2 . default ) ( this , "timelineSets" , void 0 ) ;
( 0 , _defineProperty2 . default ) ( this , "polls" , new Map ( ) ) ;
2026-09-12 23:57:45 -04:00
/ * *
* Empty array if the timeline sets have not been initialised . After initialisation :
* 0 : All threads
* 1 : Threads the current user has participated in
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "threadsTimelineSets" , [ ] ) ;
2026-09-12 23:57:45 -04:00
// any filtered timeline sets we're maintaining for this room
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "filteredTimelineSets" , { } ) ;
2026-09-12 23:57:45 -04:00
// filter_id: timelineSet
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "timelineNeedsRefresh" , false ) ;
( 0 , _defineProperty2 . default ) ( this , "pendingEventList" , void 0 ) ;
2026-09-12 23:57:45 -04:00
// read by megolm via getter; boolean value - null indicates "use global value"
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "blacklistUnverifiedDevices" , void 0 ) ;
( 0 , _defineProperty2 . default ) ( this , "selfMembership" , void 0 ) ;
( 0 , _defineProperty2 . default ) ( this , "summaryHeroes" , null ) ;
2026-09-12 23:57:45 -04:00
// flags to stop logspam about missing m.room.create events
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "getTypeWarning" , false ) ;
( 0 , _defineProperty2 . default ) ( this , "getVersionWarning" , false ) ;
( 0 , _defineProperty2 . default ) ( this , "membersPromise" , void 0 ) ;
2026-09-12 23:57:45 -04:00
// XXX: These should be read-only
/ * *
* The human - readable display name for this room .
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "name" , void 0 ) ;
2026-09-12 23:57:45 -04:00
/ * *
* The un - homoglyphed name for this room .
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "normalizedName" , void 0 ) ;
2026-09-12 23:57:45 -04:00
/ * *
* 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 } } `
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "tags" , { } ) ;
2026-09-12 23:57:45 -04:00
// $tagName: { $metadata: $value }
/ * *
* accountData Dict of per - room account _data events ; the keys are the
* event type and the values are the events .
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "accountData" , new Map ( ) ) ;
2026-09-12 23:57:45 -04:00
// $eventType: $event
/ * *
* The room summary .
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "summary" , null ) ;
/ * *
* The live event timeline for this room , with the oldest event at index 0.
*
* @ deprecated Present for backwards compatibility .
* Use getLiveTimeline ( ) . getEvents ( ) instead
* /
( 0 , _defineProperty2 . default ) ( this , "timeline" , void 0 ) ;
2026-09-12 23:57:45 -04:00
/ * *
* oldState The state of the room at the time of the oldest event in the live timeline .
*
* @ deprecated Present for backwards compatibility .
* Use getLiveTimeline ( ) . getState ( EventTimeline . BACKWARDS ) instead
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "oldState" , void 0 ) ;
2026-09-12 23:57:45 -04:00
/ * *
* currentState The state of the room at the time of the newest event in the timeline .
*
* @ deprecated Present for backwards compatibility .
* Use getLiveTimeline ( ) . getState ( EventTimeline . FORWARDS ) instead .
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "currentState" , void 0 ) ;
( 0 , _defineProperty2 . default ) ( this , "relations" , new _relationsContainer . RelationsContainer ( this . client , this ) ) ;
2026-09-12 23:57:45 -04:00
/ * *
* A collection of events known by the client
* This is not a comprehensive list of the threads that exist in this room
* /
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "threads" , new Map ( ) ) ;
/ * *
* @ deprecated This value is unreliable . It may not contain the last thread .
* Use { @ link Room . getLastThread } instead .
* /
( 0 , _defineProperty2 . default ) ( this , "lastThread" , void 0 ) ;
2026-09-12 23:57:45 -04:00
/ * *
* 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
* /
2026-09-13 14:14:38 -04:00
( 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 ) => {
2026-09-12 23:57:45 -04:00
if ( thread . length ) {
2026-09-13 14:14:38 -04:00
var _this$threadsTimeline ;
this . updateThreadRootEvent ( ( _this$threadsTimeline = this . threadsTimelineSets ) === null || _this$threadsTimeline === void 0 ? void 0 : _this$threadsTimeline [ 0 ] , thread , toStartOfTimeline , recreateEvent ) ;
2026-09-12 23:57:45 -04:00
if ( thread . hasCurrentUserParticipated ) {
2026-09-13 14:14:38 -04:00
var _this$threadsTimeline2 ;
this . updateThreadRootEvent ( ( _this$threadsTimeline2 = this . threadsTimelineSets ) === null || _this$threadsTimeline2 === void 0 ? void 0 : _this$threadsTimeline2 [ 1 ] , thread , toStartOfTimeline , recreateEvent ) ;
2026-09-12 23:57:45 -04:00
}
}
} ) ;
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "updateThreadRootEvent" , ( timelineSet , thread , toStartOfTimeline , recreateEvent ) => {
2026-09-12 23:57:45 -04:00
if ( timelineSet && thread . rootEvent ) {
if ( recreateEvent ) {
timelineSet . removeEvent ( thread . id ) ;
}
2026-09-13 14:14:38 -04:00
if ( _thread . Thread . hasServerSideSupport ) {
2026-09-12 23:57:45 -04:00
timelineSet . addLiveEvent ( thread . rootEvent , {
2026-09-13 14:14:38 -04:00
duplicateStrategy : _eventTimelineSet . DuplicateStrategy . Replace ,
2026-09-12 23:57:45 -04:00
fromCache : false ,
2026-09-13 14:14:38 -04:00
roomState : this . currentState
2026-09-12 23:57:45 -04:00
} ) ;
} else {
timelineSet . addEventToTimeline ( thread . rootEvent , timelineSet . getLiveTimeline ( ) , {
2026-09-13 14:14:38 -04:00
toStartOfTimeline
2026-09-12 23:57:45 -04:00
} ) ;
}
}
} ) ;
2026-09-13 14:14:38 -04:00
( 0 , _defineProperty2 . default ) ( this , "applyRedaction" , event => {
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
redactedEvent . makeRedacted ( event ) ;
2026-09-12 23:57:45 -04:00
2026-09-13 14:14:38 -04:00
// 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 ) ;
}
2026-09-12 23:57:45 -04:00
}
2026-09-13 14:14:38 -04:00
// 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.
2026-09-12 23:57:45 -04:00
}
} ) ;
this . setMaxListeners ( 100 ) ;
2026-09-13 14:14:38 -04:00
this . reEmitter = new _ReEmitter . TypedReEmitter ( this ) ;
opts . pendingEventOrdering = opts . pendingEventOrdering || _client . PendingEventOrdering . Chronological ;
2026-09-12 23:57:45 -04:00
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.
2026-09-13 14:14:38 -04:00
this . timelineSets = [ new _eventTimelineSet . EventTimelineSet ( this , opts ) ] ;
2026-09-12 23:57:45 -04:00
this . reEmitter . reEmit ( this . getUnfilteredTimelineSet ( ) , [ RoomEvent . Timeline , RoomEvent . TimelineReset ] ) ;
this . fixUpLegacyTimelineFields ( ) ;
2026-09-13 14:14:38 -04:00
if ( this . opts . pendingEventOrdering === _client . PendingEventOrdering . Detached ) {
2026-09-12 23:57:45 -04:00
this . pendingEventList = [ ] ;
this . client . store . getPendingEvents ( this . roomId ) . then ( events => {
const mapper = this . client . getEventMapper ( {
2026-09-13 14:14:38 -04:00
toDevice : false ,
2026-09-12 23:57:45 -04:00
decrypt : false
} ) ;
events . forEach ( async serializedEvent => {
const event = mapper ( serializedEvent ) ;
await client . decryptEventIfNeeded ( event ) ;
2026-09-13 14:14:38 -04:00
event . setStatus ( _eventStatus . EventStatus . NOT _SENT ) ;
2026-09-12 23:57:45 -04:00
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 ( ) {
2026-09-13 14:14:38 -04:00
var _this$client ;
2026-09-12 23:57:45 -04:00
if ( this . threadTimelineSetsPromise ) {
return this . threadTimelineSetsPromise ;
}
2026-09-13 14:14:38 -04:00
if ( ( _this$client = this . client ) !== null && _this$client !== void 0 && _this$client . supportsThreads ( ) ) {
2026-09-12 23:57:45 -04:00
try {
2026-09-13 14:14:38 -04:00
this . threadTimelineSetsPromise = Promise . all ( [ this . createThreadTimelineSet ( ) , this . createThreadTimelineSet ( _thread . ThreadFilterType . My ) ] ) ;
2026-09-12 23:57:45 -04:00
const timelineSets = await this . threadTimelineSetsPromise ;
this . threadsTimelineSets [ 0 ] = timelineSets [ 0 ] ;
this . threadsTimelineSets [ 1 ] = timelineSets [ 1 ] ;
return timelineSets ;
2026-09-13 14:14:38 -04:00
} catch ( e ) {
2026-09-12 23:57:45 -04:00
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 ( ) {
2026-09-13 14:14:38 -04:00
if ( ! this . client . isCryptoEnabled ( ) ) return ;
2026-09-12 23:57:45 -04:00
const readReceiptEventId = this . getEventReadUpTo ( this . client . getUserId ( ) , true ) ;
const events = this . getLiveTimeline ( ) . getEvents ( ) ;
const readReceiptTimelineIndex = events . findIndex ( matrixEvent => {
return matrixEvent . event . event _id === readReceiptEventId ;
} ) ;
2026-09-13 14:14:38 -04:00
const decryptionPromises = events . slice ( readReceiptTimelineIndex ) . reverse ( ) . map ( event => this . client . decryptEventIfNeeded ( event , {
isRetry : true
} ) ) ;
2026-09-12 23:57:45 -04:00
await Promise . allSettled ( decryptionPromises ) ;
}
/ * *
* Bulk decrypt events in a room
*
* @ returns Signals when all events have been decrypted
* /
async decryptAllEvents ( ) {
2026-09-13 14:14:38 -04:00
if ( ! this . client . isCryptoEnabled ( ) ) return ;
2026-09-12 23:57:45 -04:00
const decryptionPromises = this . getUnfilteredTimelineSet ( ) . getLiveTimeline ( ) . getEvents ( ) . slice ( 0 ) // copy before reversing
2026-09-13 14:14:38 -04:00
. reverse ( ) . map ( event => this . client . decryptEventIfNeeded ( event , {
isRetry : true
} ) ) ;
2026-09-12 23:57:45 -04:00
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 ( ) {
2026-09-13 14:14:38 -04:00
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 ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Gets the version of the room
2026-09-13 14:14:38 -04:00
* @ returns The version of the room , or null if it could not be determined
2026-09-12 23:57:45 -04:00
* /
getVersion ( ) {
2026-09-13 14:14:38 -04:00
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 ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ( ) {
2026-09-13 14:14:38 -04:00
const capabilities = await this . client . getCapabilities ( ) ;
2026-09-12 23:57:45 -04:00
let versionCap = capabilities [ "m.room_versions" ] ;
if ( ! versionCap ) {
versionCap = {
default : KNOWN _SAFE _ROOM _VERSION ,
available : { }
} ;
for ( const safeVer of SAFE _ROOM _VERSIONS ) {
2026-09-13 14:14:38 -04:00
versionCap . available [ safeVer ] = _client . RoomVersionStability . Stable ;
2026-09-12 23:57:45 -04:00
}
}
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.
2026-09-13 14:14:38 -04:00
_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" ] ;
2026-09-12 23:57:45 -04:00
if ( ! versionCap ) {
2026-09-13 14:14:38 -04:00
_logger . logger . warn ( "No room version capability - assuming upgrade required." ) ;
2026-09-12 23:57:45 -04:00
return result ;
} else {
result = this . checkVersionAgainstCapability ( versionCap ) ;
}
}
return result ;
}
checkVersionAgainstCapability ( versionCap ) {
const currentVersion = this . getVersion ( ) ;
2026-09-13 14:14:38 -04:00
_logger . logger . log ( ` [ ${ this . roomId } ] Current version: ${ currentVersion } ` ) ;
_logger . logger . log ( ` [ ${ this . roomId } ] Version capability: ` , versionCap ) ;
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
_logger . logger . warn ( ` URGENT upgrade required on ${ this . roomId } ` ) ;
2026-09-12 23:57:45 -04:00
} else {
2026-09-13 14:14:38 -04:00
_logger . logger . warn ( ` Non-urgent upgrade required on ${ this . roomId } ` ) ;
2026-09-12 23:57:45 -04:00
}
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 ) {
2026-09-13 14:14:38 -04:00
return this . currentState . maySendStateEvent ( _event2 . EventType . RoomTombstone , userId ) ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ) ;
}
2026-09-13 14:14:38 -04:00
const removed = ( 0 , _utils . removeElement ) ( this . pendingEventList , function ( ev ) {
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
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 ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Get a specific event from the pending event list , if configured , null otherwise .
*
* @ param eventId - The event ID to check for .
* /
getPendingEvent ( eventId ) {
2026-09-13 14:14:38 -04:00
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 ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Get the live unfiltered timeline for this room .
*
* @ returns live timeline
* /
getLiveTimeline ( ) {
return this . getUnfilteredTimelineSet ( ) . getLiveTimeline ( ) ;
}
/ * *
* Get the timestamp of the last message in the room
*
* @ returns the timestamp of the last message in the room
* /
getLastActiveTimestamp ( ) {
const timeline = this . getLiveTimeline ( ) ;
const events = timeline . getEvents ( ) ;
if ( events . length ) {
const lastEvent = events [ events . length - 1 ] ;
return lastEvent . getTs ( ) ;
} else {
return Number . MIN _SAFE _INTEGER ;
}
}
/ * *
* Returns the last live event of this room .
* "last" means latest timestamp .
* Instead of using timestamps , it would be better to do the comparison based on the order of the homeserver DAG .
* Unfortunately , this information is currently not available in the client .
* See { @ link https : //github.com/matrix-org/matrix-js-sdk/issues/3325}.
* "live of this room" means from all live timelines : the room and the threads .
*
* @ returns MatrixEvent if there is a last event ; else undefined .
* /
getLastLiveEvent ( ) {
2026-09-13 14:14:38 -04:00
var _lastRoomEvent$getTs , _lastThreadEvent$getT ;
2026-09-12 23:57:45 -04:00
const roomEvents = this . getLiveTimeline ( ) . getEvents ( ) ;
const lastRoomEvent = roomEvents [ roomEvents . length - 1 ] ;
const lastThread = this . getLastThread ( ) ;
if ( ! lastThread ) return lastRoomEvent ;
const lastThreadEvent = lastThread . events [ lastThread . events . length - 1 ] ;
2026-09-13 14:14:38 -04:00
return ( ( _lastRoomEvent$getTs = lastRoomEvent === null || lastRoomEvent === void 0 ? void 0 : lastRoomEvent . getTs ( ) ) !== null && _lastRoomEvent$getTs !== void 0 ? _lastRoomEvent$getTs : 0 ) > ( ( _lastThreadEvent$getT = lastThreadEvent === null || lastThreadEvent === void 0 ? void 0 : lastThreadEvent . getTs ( ) ) !== null && _lastThreadEvent$getT !== void 0 ? _lastThreadEvent$getT : 0 ) ? lastRoomEvent : lastThreadEvent ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Returns the last thread of this room .
* "last" means latest timestamp of the last thread event .
* Instead of using timestamps , it would be better to do the comparison based on the order of the homeserver DAG .
* Unfortunately , this information is currently not available in the client .
* See { @ link https : //github.com/matrix-org/matrix-js-sdk/issues/3325}.
*
* @ returns the thread with the most recent event in its live time line . undefined if there is no thread .
* /
getLastThread ( ) {
return this . getThreads ( ) . reduce ( ( lastThread , thread ) => {
2026-09-13 14:14:38 -04:00
var _threadEvent$getTs , _lastThreadEvent$getT2 ;
2026-09-12 23:57:45 -04:00
if ( ! lastThread ) return thread ;
const threadEvent = thread . events [ thread . events . length - 1 ] ;
const lastThreadEvent = lastThread . events [ lastThread . events . length - 1 ] ;
2026-09-13 14:14:38 -04:00
if ( ( ( _threadEvent$getTs = threadEvent === null || threadEvent === void 0 ? void 0 : threadEvent . getTs ( ) ) !== null && _threadEvent$getTs !== void 0 ? _threadEvent$getTs : 0 ) >= ( ( _lastThreadEvent$getT2 = lastThreadEvent === null || lastThreadEvent === void 0 ? void 0 : lastThreadEvent . getTs ( ) ) !== null && _lastThreadEvent$getT2 !== void 0 ? _lastThreadEvent$getT2 : 0 ) ) {
2026-09-12 23:57:45 -04:00
// Last message of current thread is newer → new last thread.
// Equal also means newer, because it was added to the thread map later.
return thread ;
}
return lastThread ;
} , undefined ) ;
}
/ * *
2026-09-13 14:14:38 -04:00
* @ returns the membership type ( join | leave | invite ) for the logged in user
2026-09-12 23:57:45 -04:00
* /
getMyMembership ( ) {
2026-09-13 14:14:38 -04:00
var _this$selfMembership ;
return ( _this$selfMembership = this . selfMembership ) !== null && _this$selfMembership !== void 0 ? _this$selfMembership : "leave" ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ( ) ;
}
2026-09-13 14:14:38 -04:00
if ( this . selfMembership === "invite" ) {
2026-09-12 23:57:45 -04:00
// fall back to summary information
const memberCount = this . getInvitedAndJoinedMemberCount ( ) ;
if ( memberCount === 2 ) {
2026-09-13 14:14:38 -04:00
var _this$summaryHeroes ;
return ( _this$summaryHeroes = this . summaryHeroes ) === null || _this$summaryHeroes === void 0 ? void 0 : _this$summaryHeroes [ 0 ] ;
2026-09-12 23:57:45 -04:00
}
}
}
/ * *
* 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
2026-09-13 14:14:38 -04:00
if ( Array . isArray ( this . summaryHeroes ) && this . summaryHeroes . length ) {
return this . summaryHeroes [ 0 ] ;
2026-09-12 23:57:45 -04:00
}
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 ( ) {
2026-09-13 14:14:38 -04:00
const memberCount = this . getInvitedAndJoinedMemberCount ( ) ;
if ( memberCount > 2 ) {
return ;
}
const hasHeroes = Array . isArray ( this . summaryHeroes ) && this . summaryHeroes . length ;
2026-09-12 23:57:45 -04:00
if ( hasHeroes ) {
2026-09-13 14:14:38 -04:00
const availableMember = this . summaryHeroes . map ( userId => {
return this . getMember ( userId ) ;
2026-09-12 23:57:45 -04:00
} ) . find ( member => ! ! member ) ;
if ( availableMember ) {
return availableMember ;
}
}
2026-09-13 14:14:38 -04:00
const members = this . currentState . getMembers ( ) ;
// could be different than memberCount
// as this includes left members
if ( members . length <= 2 ) {
const availableMember = members . find ( m => {
2026-09-12 23:57:45 -04:00
return m . userId !== this . myUserId ;
} ) ;
if ( availableMember ) {
return availableMember ;
}
}
2026-09-13 14:14:38 -04:00
// if all else fails, try falling back to a user,
// and create a one-off member for it
2026-09-12 23:57:45 -04:00
if ( hasHeroes ) {
2026-09-13 14:14:38 -04:00
const availableUser = this . summaryHeroes . map ( userId => {
return this . client . getUser ( userId ) ;
2026-09-12 23:57:45 -04:00
} ) . find ( user => ! ! user ) ;
if ( availableUser ) {
2026-09-13 14:14:38 -04:00
const member = new _roomMember . RoomMember ( this . roomId , availableUser . userId ) ;
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
if ( membership === "leave" ) {
2026-09-12 23:57:45 -04:00
this . cleanupAfterLeaving ( ) ;
}
this . emit ( RoomEvent . MyMembership , this , membership , prevMembership ) ;
}
}
async loadMembersFromServer ( ) {
const lastSyncToken = this . client . store . getSyncToken ( ) ;
2026-09-13 14:14:38 -04:00
const response = await this . client . members ( this . roomId , undefined , "leave" , lastSyncToken !== null && lastSyncToken !== void 0 ? lastSyncToken : undefined ) ;
2026-09-12 23:57:45 -04:00
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.
2026-09-13 14:14:38 -04:00
if ( rawMembersEvents === null || this . client . isCryptoEnabled ( ) && this . client . isRoomEncrypted ( this . roomId ) ) {
2026-09-12 23:57:45 -04:00
fromServer = true ;
rawMembersEvents = await this . loadMembersFromServer ( ) ;
2026-09-13 14:14:38 -04:00
_logger . logger . log ( ` LL: got ${ rawMembersEvents . length } ` + ` members from server for room ${ this . roomId } ` ) ;
2026-09-12 23:57:45 -04:00
}
2026-09-13 14:14:38 -04:00
const memberEvents = rawMembersEvents . filter ( _utils . noUnsafeEventProps ) . map ( this . client . getEventMapper ( ) ) ;
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
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 } ` ) ;
2026-09-12 23:57:45 -04:00
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 => {
2026-09-13 14:14:38 -04:00
_logger . logger . log ( "LL: storing OOB room members failed, oh well" , err ) ;
2026-09-12 23:57:45 -04:00
} ) ;
}
} ) . catch ( err => {
// as this is not awaited anywhere,
// at least show the error in the console
2026-09-13 14:14:38 -04:00
_logger . logger . error ( err ) ;
2026-09-12 23:57:45 -04:00
} ) ;
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 => {
2026-09-13 14:14:38 -04:00
_logger . logger . error ( ` error after clearing loaded members from ` + ` room ${ this . roomId } after leaving ` ) ;
_logger . logger . log ( err ) ;
2026-09-12 23:57:45 -04:00
} ) ;
}
/ * *
* 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 ( ) ;
2026-09-13 14:14:38 -04:00
const forwardPaginationToken = liveTimelineBefore . getPaginationToken ( _eventTimeline . EventTimeline . FORWARDS ) ;
const backwardPaginationToken = liveTimelineBefore . getPaginationToken ( _eventTimeline . EventTimeline . BACKWARDS ) ;
2026-09-12 23:57:45 -04:00
const eventsBefore = liveTimelineBefore . getEvents ( ) ;
const mostRecentEventInTimeline = eventsBefore [ eventsBefore . length - 1 ] ;
2026-09-13 14:14:38 -04:00
_logger . logger . log ( ` [refreshLiveTimeline for ${ this . roomId } ] at ` + ` mostRecentEventInTimeline= ${ mostRecentEventInTimeline && mostRecentEventInTimeline . getId ( ) } ` + ` liveTimelineBefore= ${ liveTimelineBefore . toString ( ) } ` + ` forwardPaginationToken= ${ forwardPaginationToken } ` + ` backwardPaginationToken= ${ backwardPaginationToken } ` ) ;
2026-09-12 23:57:45 -04:00
// Get the main TimelineSet
const timelineSet = this . getUnfilteredTimelineSet ( ) ;
2026-09-13 14:14:38 -04:00
let newTimeline ;
2026-09-12 23:57:45 -04:00
// 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 ( ) ;
2026-09-13 14:14:38 -04:00
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 ` ) ;
2026-09-12 23:57:45 -04:00
// 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.
2026-09-13 14:14:38 -04:00
newTimeline . setPaginationToken ( forwardPaginationToken , _eventTimeline . EventTimeline . FORWARDS ) ;
2026-09-12 23:57:45 -04:00
// 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 {
2026-09-13 14:14:38 -04:00
_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. ` ) ;
2026-09-12 23:57:45 -04:00
}
// 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 ) {
2026-09-13 14:14:38 -04:00
timelineSet . resetLiveTimeline ( backPaginationToken !== null && backPaginationToken !== void 0 ? backPaginationToken : undefined , forwardPaginationToken !== null && forwardPaginationToken !== void 0 ? forwardPaginationToken : undefined ) ;
2026-09-12 23:57:45 -04:00
}
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 ;
2026-09-13 14:14:38 -04:00
// maintain this.timeline as a reference to the live timeline,
// and this.oldState and this.currentState as references to the
2026-09-12 23:57:45 -04:00
// state at the start and end of that timeline. These are more
// for backwards-compatibility than anything else.
2026-09-13 14:14:38 -04:00
this . timeline = this . getLiveTimeline ( ) . getEvents ( ) ;
this . oldState = this . getLiveTimeline ( ) . getState ( _eventTimeline . EventTimeline . BACKWARDS ) ;
this . currentState = this . getLiveTimeline ( ) . getState ( _eventTimeline . EventTimeline . FORWARDS ) ;
2026-09-12 23:57:45 -04:00
// 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?
2026-09-13 14:14:38 -04:00
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 ] ) ;
2026-09-12 23:57:45 -04:00
}
}
2026-09-13 14:14:38 -04:00
/ * *
* 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 ;
2026-09-12 23:57:45 -04:00
}
2026-09-13 14:14:38 -04:00
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 ;
2026-09-12 23:57:45 -04:00
}
}
2026-09-13 14:14:38 -04:00
return false ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ( ) ) {
2026-09-13 14:14:38 -04:00
var _threadNotification$t ;
count += ( _threadNotification$t = threadNotification [ type ] ) !== null && _threadNotification$t !== void 0 ? _threadNotification$t : 0 ;
2026-09-12 23:57:45 -04:00
}
return count ;
}
/ * *
* Get the notification for the event context ( room or thread timeline )
* /
getUnreadCountForEventContext ( type = NotificationCountType . Total , event ) {
2026-09-13 14:14:38 -04:00
var _ref ;
2026-09-12 23:57:45 -04:00
const isThreadEvent = ! ! event . threadRootId && ! event . isThreadRoot ;
2026-09-13 14:14:38 -04:00
return ( _ref = isThreadEvent ? this . getThreadUnreadNotificationCount ( event . threadRootId , type ) : this . getRoomUnreadNotificationCount ( type ) ) !== null && _ref !== void 0 ? _ref : 0 ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ) {
2026-09-13 14:14:38 -04:00
var _this$notificationCou ;
return ( _this$notificationCou = this . notificationCounts [ type ] ) !== null && _this$notificationCou !== void 0 ? _this$notificationCou : 0 ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ) {
2026-09-13 14:14:38 -04:00
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 ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Checks if the current room has unread thread notifications
* @ returns
* /
hasThreadUnreadNotification ( ) {
for ( const notification of this . threadNotifications . values ( ) ) {
2026-09-13 14:14:38 -04:00
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 ) {
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
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
} , {
2026-09-12 23:57:45 -04:00
[ type ] : count
2026-09-13 14:14:38 -04:00
} ) ;
2026-09-12 23:57:45 -04:00
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 ( ) ) {
2026-09-13 14:14:38 -04:00
var _threadNotification$h , _threadNotification$t2 ;
if ( ( ( _threadNotification$h = threadNotification . highlight ) !== null && _threadNotification$h !== void 0 ? _threadNotification$h : 0 ) > 0 ) {
2026-09-12 23:57:45 -04:00
return NotificationCountType . Highlight ;
2026-09-13 14:14:38 -04:00
} else if ( ( ( _threadNotification$t2 = threadNotification . total ) !== null && _threadNotification$t2 !== void 0 ? _threadNotification$t2 : 0 ) > 0 && ! type ) {
2026-09-12 23:57:45 -04:00
type = NotificationCountType . Total ;
}
}
return type ;
}
/ * *
2026-09-13 14:14:38 -04:00
* Resets the thread notifications for this room
2026-09-12 23:57:45 -04:00
* /
2026-09-13 14:14:38 -04:00
resetThreadUnreadNotificationCount ( notificationsToKeep ) {
if ( notificationsToKeep ) {
for ( const [ threadId ] of this . threadNotifications ) {
if ( ! notificationsToKeep . includes ( threadId ) ) {
this . threadNotifications . delete ( threadId ) ;
2026-09-12 23:57:45 -04:00
}
}
2026-09-13 14:14:38 -04:00
} else {
this . threadNotifications . clear ( ) ;
2026-09-12 23:57:45 -04:00
}
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 ) {
2026-09-13 14:14:38 -04:00
const heroes = summary [ "m.heroes" ] ;
2026-09-12 23:57:45 -04:00
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 ) ) {
2026-09-13 14:14:38 -04:00
// 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 ;
2026-09-12 23:57:45 -04:00
} ) ;
}
}
/ * *
* 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 .
* /
2026-09-13 14:14:38 -04:00
getAvatarUrl ( baseUrl , width , height , resizeMethod , allowDefault = true ) {
const roomAvatarEvent = this . currentState . getStateEvents ( _event2 . EventType . RoomAvatar , "" ) ;
if ( ! roomAvatarEvent && ! allowDefault ) {
2026-09-12 23:57:45 -04:00
return null ;
}
2026-09-13 14:14:38 -04:00
const mainUrl = roomAvatarEvent ? roomAvatarEvent . getContent ( ) . url : null ;
2026-09-12 23:57:45 -04:00
if ( mainUrl ) {
2026-09-13 14:14:38 -04:00
return ( 0 , _contentRepo . getHttpUriForMxc ) ( baseUrl , mainUrl , width , height , resizeMethod ) ;
2026-09-12 23:57:45 -04:00
}
return null ;
}
/ * *
* Get the mxc avatar url for the room , if one was set .
* @ returns the mxc avatar url or falsy
* /
getMxcAvatarUrl ( ) {
2026-09-13 14:14:38 -04:00
var _this$currentState$ge ;
return ( ( _this$currentState$ge = this . currentState . getStateEvents ( _event2 . EventType . RoomAvatar , "" ) ) === null || _this$currentState$ge === void 0 || ( _this$currentState$ge = _this$currentState$ge . getContent ( ) ) === null || _this$currentState$ge === void 0 ? void 0 : _this$currentState$ge . url ) || null ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ( ) {
2026-09-13 14:14:38 -04:00
const canonicalAlias = this . currentState . getStateEvents ( _event2 . EventType . RoomCanonicalAlias , "" ) ;
if ( canonicalAlias ) {
return canonicalAlias . getContent ( ) . alias || null ;
}
return null ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Get this room ' s alternative aliases
* @ returns The room ' s alternative aliases , or an empty array
* /
getAltAliases ( ) {
2026-09-13 14:14:38 -04:00
const canonicalAlias = this . currentState . getStateEvents ( _event2 . EventType . RoomCanonicalAlias , "" ) ;
if ( canonicalAlias ) {
return canonicalAlias . getContent ( ) . alt _aliases || [ ] ;
2026-09-12 23:57:45 -04:00
}
return [ ] ;
}
/ * *
* Add events to a timeline
*
* < p > Will fire "Room.timeline" for each event added .
*
* @ param events - A list of events to add .
*
* @ param toStartOfTimeline - True to add these events to the start
* ( oldest ) instead of the end ( newest ) of the timeline . If true , the oldest
* event will be the < b > last < / b > e l e m e n t o f ' e v e n t s ' .
*
* @ param timeline - timeline to
* add events to .
*
* @ param paginationToken - token for the next batch of events
*
* @ remarks
* Fires { @ link RoomEvent . Timeline }
* /
2026-09-13 14:14:38 -04:00
addEventsToTimeline ( events , toStartOfTimeline , timeline , paginationToken ) {
timeline . getTimelineSet ( ) . addEventsToTimeline ( events , toStartOfTimeline , timeline , paginationToken ) ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ) {
2026-09-13 14:14:38 -04:00
var _this$threads$get ;
return ( _this$threads$get = this . threads . get ( eventId ) ) !== null && _this$threads$get !== void 0 ? _this$threads$get : null ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ( ) {
2026-09-13 14:14:38 -04:00
return this . getMembersWithMembership ( "join" ) ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ( ) ;
2026-09-13 14:14:38 -04:00
let members = this . getMembersWithMembership ( "join" ) ;
2026-09-12 23:57:45 -04:00
if ( this . shouldEncryptForInvitedMembers ( ) ) {
2026-09-13 14:14:38 -04:00
members = members . concat ( this . getMembersWithMembership ( "invite" ) ) ;
2026-09-12 23:57:45 -04:00
}
return members ;
}
/ * *
* Determine whether we should encrypt messages for invited users in this room
* @ returns if we should encrypt messages for invited users
* /
shouldEncryptForInvitedMembers ( ) {
2026-09-13 14:14:38 -04:00
var _ev$getContent ;
const ev = this . currentState . getStateEvents ( _event2 . EventType . RoomHistoryVisibility , "" ) ;
return ( ev === null || ev === void 0 || ( _ev$getContent = ev . getContent ( ) ) === null || _ev$getContent === void 0 ? void 0 : _ev$getContent . history _visibility ) !== "joined" ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ) ;
2026-09-13 14:14:38 -04:00
const timelineSet = new _eventTimelineSet . EventTimelineSet ( this , opts ) ;
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
timelineSet . addLiveEvent ( event ) ;
2026-09-12 23:57:45 -04:00
} ) ;
// find the earliest unfiltered timeline
let timeline = unfilteredLiveTimeline ;
2026-09-13 14:14:38 -04:00
while ( timeline . getNeighbouringTimeline ( _eventTimeline . EventTimeline . BACKWARDS ) ) {
timeline = timeline . getNeighbouringTimeline ( _eventTimeline . EventTimeline . BACKWARDS ) ;
2026-09-12 23:57:45 -04:00
}
2026-09-13 14:14:38 -04:00
timelineSet . getLiveTimeline ( ) . setPaginationToken ( timeline . getPaginationToken ( _eventTimeline . EventTimeline . BACKWARDS ) , _eventTimeline . EventTimeline . BACKWARDS ) ;
2026-09-12 23:57:45 -04:00
} else if ( useSyncEvents ) {
2026-09-13 14:14:38 -04:00
const livePaginationToken = unfilteredLiveTimeline . getPaginationToken ( _eventTimeline . Direction . Forward ) ;
timelineSet . getLiveTimeline ( ) . setPaginationToken ( livePaginationToken , _eventTimeline . Direction . Backward ) ;
2026-09-12 23:57:45 -04:00
}
// 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 ;
}
2026-09-13 14:14:38 -04:00
async getThreadListFilter ( filterType = _thread . ThreadFilterType . All ) {
2026-09-12 23:57:45 -04:00
const myUserId = this . client . getUserId ( ) ;
2026-09-13 14:14:38 -04:00
const filter = new _filter . Filter ( myUserId ) ;
2026-09-12 23:57:45 -04:00
const definition = {
room : {
timeline : {
2026-09-13 14:14:38 -04:00
[ _thread . FILTER _RELATED _BY _REL _TYPES . name ] : [ _thread . THREAD _RELATION _TYPE . name ]
2026-09-12 23:57:45 -04:00
}
}
} ;
2026-09-13 14:14:38 -04:00
if ( filterType === _thread . ThreadFilterType . My ) {
definition . room . timeline [ _thread . FILTER _RELATED _BY _SENDERS . name ] = [ myUserId ] ;
2026-09-12 23:57:45 -04:00
}
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 ;
2026-09-13 14:14:38 -04:00
if ( _thread . Thread . hasServerSideListSupport ) {
timelineSet = new _eventTimelineSet . EventTimelineSet ( this , _objectSpread ( _objectSpread ( { } , this . opts ) , { } , {
2026-09-12 23:57:45 -04:00
pendingEvents : false
2026-09-13 14:14:38 -04:00
} ) , undefined , undefined , filterType !== null && filterType !== void 0 ? filterType : _thread . ThreadFilterType . All ) ;
2026-09-12 23:57:45 -04:00
this . reEmitter . reEmit ( timelineSet , [ RoomEvent . Timeline , RoomEvent . TimelineReset ] ) ;
2026-09-13 14:14:38 -04:00
} else if ( _thread . Thread . hasServerSideSupport ) {
2026-09-12 23:57:45 -04:00
const filter = await this . getThreadListFilter ( filterType ) ;
timelineSet = this . getOrCreateFilteredTimelineSet ( filter , {
prepopulateTimeline : false ,
useSyncEvents : false ,
pendingEvents : false
} ) ;
} else {
2026-09-13 14:14:38 -04:00
timelineSet = new _eventTimelineSet . EventTimelineSet ( this , {
2026-09-12 23:57:45 -04:00
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 ( ) ;
} ) ;
2026-09-13 14:14:38 -04:00
if ( filterType !== _thread . ThreadFilterType . My || currentUserParticipated ) {
2026-09-12 23:57:45 -04:00
timelineSet . getLiveTimeline ( ) . addEvent ( thread . rootEvent , {
2026-09-13 14:14:38 -04:00
toStartOfTimeline : false
2026-09-12 23:57:45 -04:00
} ) ;
}
} ) ;
}
return timelineSet ;
}
/ * *
* Takes the given thread root events and creates threads for them .
* /
processThreadRoots ( events , toStartOfTimeline ) {
for ( const rootEvent of events ) {
2026-09-13 14:14:38 -04:00
_eventTimeline . EventTimeline . setEventMetadata ( rootEvent , this . currentState , toStartOfTimeline ) ;
2026-09-12 23:57:45 -04:00
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 ;
}
2026-09-13 14:14:38 -04:00
if ( _thread . Thread . hasServerSideListSupport ) {
await Promise . all ( [ this . fetchRoomThreadList ( _thread . ThreadFilterType . All ) , this . fetchRoomThreadList ( _thread . ThreadFilterType . My ) ] ) ;
2026-09-12 23:57:45 -04:00
} else {
const allThreadsFilter = await this . getThreadListFilter ( ) ;
const {
chunk : events
2026-09-13 14:14:38 -04:00
} = await this . client . createMessagesRequest ( this . roomId , "" , Number . MAX _SAFE _INTEGER , _eventTimeline . Direction . Backward , allThreadsFilter ) ;
2026-09-12 23:57:45 -04:00
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
* /
2026-09-13 14:14:38 -04:00
const threadAMetadata = eventA . getServerAggregatedRelation ( _thread . THREAD _RELATION _TYPE . name ) ;
const threadBMetadata = eventB . getServerAggregatedRelation ( _thread . THREAD _RELATION _TYPE . name ) ;
2026-09-12 23:57:45 -04:00
return threadAMetadata . latest _event . origin _server _ts - threadBMetadata . latest _event . origin _server _ts ;
} ) ;
let latestMyThreadsRootEvent ;
2026-09-13 14:14:38 -04:00
const roomState = this . getLiveTimeline ( ) . getState ( _eventTimeline . EventTimeline . FORWARDS ) ;
2026-09-12 23:57:45 -04:00
for ( const rootEvent of threadRoots ) {
2026-09-13 14:14:38 -04:00
var _this$threadsTimeline3 ;
2026-09-12 23:57:45 -04:00
const opts = {
2026-09-13 14:14:38 -04:00
duplicateStrategy : _eventTimelineSet . DuplicateStrategy . Ignore ,
2026-09-12 23:57:45 -04:00
fromCache : false ,
roomState
} ;
2026-09-13 14:14:38 -04:00
( _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 ) ;
2026-09-12 23:57:45 -04:00
latestMyThreadsRootEvent = rootEvent ;
}
}
this . processThreadRoots ( threadRoots , true ) ;
this . client . decryptEventIfNeeded ( threadRoots [ threadRoots . length - 1 ] ) ;
if ( latestMyThreadsRootEvent ) {
this . client . decryptEventIfNeeded ( latestMyThreadsRootEvent ) ;
}
}
2026-09-13 14:14:38 -04:00
this . on ( _thread . ThreadEvent . NewReply , this . onThreadReply ) ;
this . on ( _thread . ThreadEvent . Delete , this . onThreadDelete ) ;
2026-09-12 23:57:45 -04:00
this . threadsReady = true ;
}
/ * *
2026-09-13 14:14:38 -04:00
* Calls { @ link processPollEvent } for a list of events .
2026-09-12 23:57:45 -04:00
*
* @ param events - List of events
* /
async processPollEvents ( events ) {
for ( const event of events ) {
try {
// Continue if the event is a clear text, non-poll event.
2026-09-13 14:14:38 -04:00
if ( ! event . isEncrypted ( ) && ! ( 0 , _poll . isPollEvent ) ( event ) ) continue ;
2026-09-12 23:57:45 -04:00
/ * *
* Try to decrypt the event . Promise resolution does not guarantee a successful decryption .
* Retry is handled in { @ link processPollEvent } .
* /
await this . client . decryptEventIfNeeded ( event ) ;
this . processPollEvent ( event ) ;
} catch ( err ) {
2026-09-13 14:14:38 -04:00
_logger . logger . warn ( "Error processing poll event" , event . getId ( ) , err ) ;
2026-09-12 23:57:45 -04:00
}
}
}
/ * *
* Processes poll events :
* If the event has a decryption failure , it will listen for decryption and tries again .
* If it is a poll start event ( ` m.poll.start ` ) ,
* it creates and stores a Poll model and emits a PollEvent . New event .
* If the event is related to a poll , it will add it to the poll .
* Noop for other cases .
*
* @ param event - Event that could be a poll event
* /
async processPollEvent ( event ) {
if ( event . isDecryptionFailure ( ) ) {
2026-09-13 14:14:38 -04:00
event . once ( _event . MatrixEventEvent . Decrypted , maybeDecryptedEvent => {
2026-09-12 23:57:45 -04:00
this . processPollEvent ( maybeDecryptedEvent ) ;
} ) ;
return ;
}
2026-09-13 14:14:38 -04:00
if ( _matrixEventsSdk . M _POLL _START . matches ( event . getType ( ) ) ) {
2026-09-12 23:57:45 -04:00
try {
2026-09-13 14:14:38 -04:00
const poll = new _poll . Poll ( event , this . client , this ) ;
2026-09-12 23:57:45 -04:00
this . polls . set ( event . getId ( ) , poll ) ;
2026-09-13 14:14:38 -04:00
this . emit ( _poll . PollEvent . New , poll ) ;
2026-09-12 23:57:45 -04:00
// remove the poll when redacted
2026-09-13 14:14:38 -04:00
event . once ( _event . MatrixEventEvent . BeforeRedaction , redactedEvent => {
2026-09-12 23:57:45 -04:00
this . polls . delete ( redactedEvent . getId ( ) ) ;
} ) ;
2026-09-13 14:14:38 -04:00
} catch { }
2026-09-12 23:57:45 -04:00
// poll creation can fail for malformed poll start events
return ;
}
const relationEventId = event . relationEventId ;
if ( relationEventId && this . polls . has ( relationEventId ) ) {
const poll = this . polls . get ( relationEventId ) ;
2026-09-13 14:14:38 -04:00
poll === null || poll === void 0 ? void 0 : poll . onNewRelation ( event ) ;
2026-09-12 23:57:45 -04:00
}
}
/ * *
* Fetch a single page of threadlist messages for the specific thread filter
* @ internal
* /
async fetchRoomThreadList ( filter ) {
if ( this . threadsTimelineSets . length === 0 ) return ;
2026-09-13 14:14:38 -04:00
const timelineSet = filter === _thread . ThreadFilterType . My ? this . threadsTimelineSets [ 1 ] : this . threadsTimelineSets [ 0 ] ;
2026-09-12 23:57:45 -04:00
const {
chunk : events ,
end
2026-09-13 14:14:38 -04:00
} = 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 ) ;
2026-09-12 23:57:45 -04:00
if ( ! events . length ) return ;
const matrixEvents = events . map ( this . client . getEventMapper ( ) ) ;
this . processThreadRoots ( matrixEvents , true ) ;
2026-09-13 14:14:38 -04:00
const roomState = this . getLiveTimeline ( ) . getState ( _eventTimeline . EventTimeline . FORWARDS ) ;
2026-09-12 23:57:45 -04:00
for ( const rootEvent of matrixEvents ) {
timelineSet . addLiveEvent ( rootEvent , {
2026-09-13 14:14:38 -04:00
duplicateStrategy : _eventTimelineSet . DuplicateStrategy . Replace ,
2026-09-12 23:57:45 -04:00
fromCache : false ,
2026-09-13 14:14:38 -04:00
roomState
2026-09-12 23:57:45 -04:00
} ) ;
}
}
2026-09-13 14:14:38 -04:00
onThreadReply ( 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 || ( _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 ) ;
}
}
2026-09-12 23:57:45 -04:00
/ * *
* 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 ) ;
}
}
/ * *
* Determine which timeline ( s ) a given event should live in
* Thread roots live in both the main timeline and their corresponding thread timeline
* Relations , redactions , replies to thread relation events live only in the thread timeline
* Relations ( other than m . thread ) , redactions , replies to a thread root live only in the main timeline
* Relations , redactions , replies where the parent cannot be found live in no timelines but should be aggregated regardless .
* Otherwise , the event lives in the main timeline only .
* /
eventShouldLiveIn ( event , events , roots ) {
2026-09-13 14:14:38 -04:00
var _this$client2 ;
if ( ! ( ( _this$client2 = this . client ) !== null && _this$client2 !== void 0 && _this$client2 . supportsThreads ( ) ) ) {
2026-09-12 23:57:45 -04:00
return {
shouldLiveInRoom : true ,
shouldLiveInThread : false
} ;
}
// A thread root is the only event shown in both timelines
2026-09-13 14:14:38 -04:00
if ( event . isThreadRoot || roots !== null && roots !== void 0 && roots . has ( event . getId ( ) ) ) {
2026-09-12 23:57:45 -04:00
return {
shouldLiveInRoom : true ,
shouldLiveInThread : true ,
threadId : event . getId ( )
} ;
}
2026-09-13 14:14:38 -04:00
const isThreadRelation = event . isRelation ( _thread . THREAD _RELATION _TYPE . name ) ;
2026-09-12 23:57:45 -04:00
const parentEventId = event . getAssociatedId ( ) ;
const threadRootId = event . threadRootId ;
// Where the parent is the thread root and this is a non-thread relation this should live only in the main timeline
2026-09-13 14:14:38 -04:00
if ( ! ! parentEventId && ! isThreadRelation && ( threadRootId === parentEventId || roots !== null && roots !== void 0 && roots . has ( parentEventId ) ) ) {
2026-09-12 23:57:45 -04:00
return {
shouldLiveInRoom : true ,
shouldLiveInThread : false
} ;
}
let parentEvent ;
if ( parentEventId ) {
2026-09-13 14:14:38 -04:00
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 ) ;
2026-09-12 23:57:45 -04:00
}
// Treat non-thread-relations, redactions, and replies as extensions of their parents so evaluate parentEvent instead
if ( parentEvent && ! isThreadRelation ) {
return this . eventShouldLiveIn ( parentEvent , events , roots ) ;
}
// A thread relation (1st and 2nd order) is always only shown in a thread
if ( threadRootId != undefined ) {
return {
shouldLiveInRoom : false ,
shouldLiveInThread : true ,
threadId : threadRootId
} ;
}
// Due to replies not being typical relations and being used as fallbacks for threads relations
// If we bypass the if case above then we know we are not a thread, so if we are still a reply
// then we know that we must be in the main timeline. Same goes if we have no associated parent event.
if ( ! parentEventId || ! ! event . replyEventId ) {
return {
shouldLiveInRoom : true ,
shouldLiveInThread : false
} ;
}
// We've exhausted all scenarios,
// we cannot assume that it lives in the main timeline as this may be a relation for an unknown thread
// adding the event in the wrong timeline causes stuck notifications and can break ability to send read receipts
return {
shouldLiveInRoom : false ,
shouldLiveInThread : false
} ;
}
findThreadForEvent ( event ) {
if ( ! event ) return null ;
const {
threadId
} = this . eventShouldLiveIn ( event ) ;
return threadId ? this . getThread ( threadId ) : null ;
}
addThreadedEvents ( threadId , events , toStartOfTimeline = false ) {
const thread = this . getThread ( threadId ) ;
if ( thread ) {
thread . addEvents ( events , toStartOfTimeline ) ;
} else {
2026-09-13 14:14:38 -04:00
var _this$findEventById2 ;
const rootEvent = ( _this$findEventById2 = this . findEventById ( threadId ) ) !== null && _this$findEventById2 !== void 0 ? _this$findEventById2 : events . find ( e => e . getId ( ) === threadId ) ;
2026-09-12 23:57:45 -04:00
this . createThread ( threadId , rootEvent , events , toStartOfTimeline ) ;
}
}
/ * *
* Adds events to a thread ' s timeline . Will fire "Thread.update"
* /
processThreadedEvents ( events , toStartOfTimeline ) {
2026-09-13 14:14:38 -04:00
events . forEach ( this . applyRedaction ) ;
2026-09-12 23:57:45 -04:00
const eventsByThread = { } ;
for ( const event of events ) {
2026-09-13 14:14:38 -04:00
var _eventsByThread ;
2026-09-12 23:57:45 -04:00
const {
threadId ,
shouldLiveInThread
} = this . eventShouldLiveIn ( event ) ;
if ( shouldLiveInThread && ! eventsByThread [ threadId ] ) {
eventsByThread [ threadId ] = [ ] ;
}
2026-09-13 14:14:38 -04:00
( _eventsByThread = eventsByThread [ threadId ] ) === null || _eventsByThread === void 0 ? void 0 : _eventsByThread . push ( event ) ;
2026-09-12 23:57:45 -04:00
}
Object . entries ( eventsByThread ) . map ( ( [ threadId , threadEvents ] ) => this . addThreadedEvents ( threadId , threadEvents , toStartOfTimeline ) ) ;
}
createThread ( threadId , rootEvent , events = [ ] , toStartOfTimeline ) {
2026-09-13 14:14:38 -04:00
var _this$cachedThreadRea , _this$lastThread , _this$lastThread$root ;
2026-09-12 23:57:45 -04:00
if ( this . threads . has ( threadId ) ) {
return this . threads . get ( threadId ) ;
}
if ( rootEvent ) {
const relatedEvents = this . relations . getAllChildEventsForEvent ( rootEvent . getId ( ) ) ;
2026-09-13 14:14:38 -04:00
if ( relatedEvents !== null && relatedEvents !== void 0 && relatedEvents . length ) {
2026-09-12 23:57:45 -04:00
// 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`
2026-09-13 14:14:38 -04:00
events = events . concat ( relatedEvents . filter ( e => ! e . isRelation ( _event2 . RelationType . Replace ) ) ) ;
2026-09-12 23:57:45 -04:00
}
}
2026-09-13 14:14:38 -04:00
const thread = new _thread . Thread ( threadId , rootEvent , {
2026-09-12 23:57:45 -04:00
room : this ,
client : this . client ,
pendingEventOrdering : this . opts . pendingEventOrdering ,
2026-09-13 14:14:38 -04:00
receipts : ( _this$cachedThreadRea = this . cachedThreadReadReceipts . get ( threadId ) ) !== null && _this$cachedThreadRea !== void 0 ? _this$cachedThreadRea : [ ]
2026-09-12 23:57:45 -04:00
} ) ;
// 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 ) ;
2026-09-13 14:14:38 -04:00
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 ) {
2026-09-12 23:57:45 -04:00
this . updateThreadRootEvents ( thread , toStartOfTimeline , false ) ;
}
2026-09-13 14:14:38 -04:00
this . emit ( _thread . ThreadEvent . New , thread , toStartOfTimeline ) ;
2026-09-12 23:57:45 -04:00
return thread ;
}
processLiveEvent ( event ) {
2026-09-13 14:14:38 -04:00
this . applyRedaction ( event ) ;
2026-09-12 23:57:45 -04:00
// 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 ( ) ) {
2026-09-13 14:14:38 -04:00
_logger . logger . debug ( "processLiveEvent: found sent event without txn ID: " , tid , event . getId ( ) ) ;
2026-09-12 23:57:45 -04:00
// 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 ,
2026-09-13 14:14:38 -04:00
fromCache
2026-09-12 23:57:45 -04:00
} = addLiveEventOptions ;
// add to our timeline sets
for ( const timelineSet of this . timelineSets ) {
timelineSet . addLiveEvent ( event , {
duplicateStrategy ,
fromCache ,
2026-09-13 14:14:38 -04:00
timelineWasEmpty
2026-09-12 23:57:45 -04:00
} ) ;
}
// 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.
2026-09-13 14:14:38 -04:00
if ( event . sender && event . getType ( ) !== _event2 . EventType . RoomRedaction ) {
this . addReceipt ( ( 0 , _readReceipt . synthesizeReceipt ) ( event . sender . userId , event , _read _receipts . ReceiptType . Read ) , true ) ;
2026-09-12 23:57:45 -04:00
// Any live events from a user could be taken as implicit
// presence information: evidence that they are currently active.
// ...except in a world where we use 'user.currentlyActive' to reduce
// presence spam, this isn't very useful - we'll get a transition when
// they are no longer currently active anyway. So don't bother to
// reset the lastActiveAgo and lastPresenceTs from the RoomState's user.
}
}
/ * *
* Add a pending outgoing event to this room .
*
* < p > The event is added to either the pendingEventList , or the live timeline ,
* depending on the setting of opts . pendingEventOrdering .
*
* < p > This is an internal method , intended for use by MatrixClient .
*
* @ param event - The event to add .
*
* @ param txnId - Transaction id for this outgoing event
*
* @ throws if the event doesn 't have status SENDING, or we aren' t given a
* unique transaction id .
*
* @ remarks
* Fires { @ link RoomEvent . LocalEchoUpdated }
* /
addPendingEvent ( event , txnId ) {
2026-09-13 14:14:38 -04:00
if ( event . status !== _eventStatus . EventStatus . SENDING && event . status !== _eventStatus . EventStatus . NOT _SENT ) {
2026-09-12 23:57:45 -04:00
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.
2026-09-13 14:14:38 -04:00
_eventTimeline . EventTimeline . setEventMetadata ( event , this . getLiveTimeline ( ) . getState ( _eventTimeline . EventTimeline . FORWARDS ) , false ) ;
2026-09-12 23:57:45 -04:00
this . txnToEvent . set ( txnId , event ) ;
if ( this . pendingEventList ) {
2026-09-13 14:14:38 -04:00
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 ) ;
2026-09-12 23:57:45 -04:00
}
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 ) ;
2026-09-13 14:14:38 -04:00
this . emit ( RoomEvent . Redaction , event , this ) ;
2026-09-12 23:57:45 -04:00
}
}
} else {
for ( const timelineSet of this . timelineSets ) {
if ( timelineSet . getFilter ( ) ) {
if ( timelineSet . getFilter ( ) . filterRoomTimeline ( [ event ] ) . length ) {
timelineSet . addEventToTimeline ( event , timelineSet . getLiveTimeline ( ) , {
2026-09-13 14:14:38 -04:00
toStartOfTimeline : false
2026-09-12 23:57:45 -04:00
} ) ;
}
} else {
timelineSet . addEventToTimeline ( event , timelineSet . getLiveTimeline ( ) , {
2026-09-13 14:14:38 -04:00
toStartOfTimeline : false
2026-09-12 23:57:45 -04:00
} ) ;
}
}
}
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
2026-09-13 14:14:38 -04:00
const isEventEncrypted = event . type === _event2 . EventType . RoomMessageEncrypted ;
const isRoomEncrypted = this . client . isRoomEncrypted ( this . roomId ) ;
2026-09-12 23:57:45 -04:00
return isEventEncrypted || ! isRoomEncrypted ;
} ) ;
this . client . store . setPendingEvents ( this . roomId , pendingEvents ) ;
}
}
/ * *
* Used to aggregate the local echo for a relation , and also
* for re - applying a relation after it ' s redaction has been cancelled ,
* as the local echo for the redaction of the relation would have
* un - aggregated the relation . Note that this is different from regular messages ,
* which are just kept detached for their local echo .
*
* Also note that live events are aggregated in the live EventTimelineSet .
* @ param event - the relation event that needs to be aggregated .
* /
aggregateNonLiveRelation ( event ) {
this . relations . aggregateChildEvent ( event ) ;
}
getEventForTxnId ( txnId ) {
return this . txnToEvent . get ( txnId ) ;
}
/ * *
* Deal with the echo of a message we sent .
*
* < p > We move the event to the live timeline if it isn ' t there already , and
* update it .
*
* @ param remoteEvent - The event received from
* / s y n c
* @ 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 ;
2026-09-13 14:14:38 -04:00
_logger . logger . debug ( ` Got remote echo for event ${ oldEventId } -> ${ newEventId } old status ${ oldStatus } ` ) ;
2026-09-12 23:57:45 -04:00
// 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 ;
2026-09-13 14:14:38 -04:00
thread === null || thread === void 0 ? void 0 : thread . setEventMetadata ( localEvent ) ;
thread === null || thread === void 0 ? void 0 : thread . timelineSet . handleRemoteEcho ( localEvent , oldEventId , newEventId ) ;
2026-09-12 23:57:45 -04:00
if ( shouldLiveInRoom ) {
for ( const timelineSet of this . timelineSets ) {
// if it's already in the timeline, update the timeline map. If it's not, add it.
timelineSet . handleRemoteEcho ( localEvent , oldEventId , newEventId ) ;
}
}
this . emit ( RoomEvent . LocalEchoUpdated , localEvent , this , oldEventId , oldStatus ) ;
}
/ * *
* Update the status / event id on a pending event , to reflect its transmission
* progress .
*
* < p > This is an internal method .
*
* @ param event - local echo event
* @ param newStatus - status to assign
* @ param newEventId - new event id to assign . Ignored unless newStatus == EventStatus . SENT .
*
* @ remarks
* Fires { @ link RoomEvent . LocalEchoUpdated }
* /
updatePendingEvent ( event , newStatus , newEventId ) {
2026-09-13 14:14:38 -04:00
_logger . logger . log ( ` setting pendingEvent status to ${ newStatus } in ${ event . getRoomId ( ) } ` + ` event ID ${ event . getId ( ) } -> ${ newEventId } ` ) ;
2026-09-12 23:57:45 -04:00
// if the message was sent, we expect an event id
2026-09-13 14:14:38 -04:00
if ( newStatus == _eventStatus . EventStatus . SENT && ! newEventId ) {
2026-09-12 23:57:45 -04:00
throw new Error ( "updatePendingEvent called with status=SENT, but no new event id" ) ;
}
// SENT races against /sync, so we have to special-case it.
2026-09-13 14:14:38 -04:00
if ( newStatus == _eventStatus . EventStatus . SENT ) {
2026-09-12 23:57:45 -04:00
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 ) ;
2026-09-13 14:14:38 -04:00
const remoteTxnId = remoteEvent === null || remoteEvent === void 0 ? void 0 : remoteEvent . getUnsigned ( ) . transaction _id ;
2026-09-12 23:57:45 -04:00
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 ] ;
2026-09-13 14:14:38 -04:00
if ( ! ( allowed !== null && allowed !== void 0 && allowed . includes ( newStatus ) ) ) {
2026-09-12 23:57:45 -04:00
throw new Error ( ` Invalid EventStatus transition ${ oldStatus } -> ${ newStatus } ` ) ;
}
event . setStatus ( newStatus ) ;
2026-09-13 14:14:38 -04:00
if ( newStatus == _eventStatus . EventStatus . SENT ) {
2026-09-12 23:57:45 -04:00
// update the event id
event . replaceLocalEventId ( newEventId ) ;
const {
shouldLiveInRoom ,
threadId
} = this . eventShouldLiveIn ( event ) ;
const thread = threadId ? this . getThread ( threadId ) : undefined ;
2026-09-13 14:14:38 -04:00
thread === null || thread === void 0 ? void 0 : thread . setEventMetadata ( event ) ;
thread === null || thread === void 0 ? void 0 : thread . timelineSet . replaceEventId ( oldEventId , newEventId ) ;
2026-09-12 23:57:45 -04:00
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 ) ;
}
}
2026-09-13 14:14:38 -04:00
} else if ( newStatus == _eventStatus . EventStatus . CANCELLED ) {
2026-09-12 23:57:45 -04:00
// remove it from the pending event list, or the timeline.
if ( this . pendingEventList ) {
const removedEvent = this . getPendingEvent ( oldEventId ) ;
this . removePendingEvent ( oldEventId ) ;
2026-09-13 14:14:38 -04:00
if ( removedEvent !== null && removedEvent !== void 0 && removedEvent . isRedaction ( ) ) {
2026-09-12 23:57:45 -04:00
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' .
* /
2026-09-13 14:14:38 -04:00
/ * *
* @ deprecated In favor of the overload with ` IAddLiveEventOptions `
* /
async addLiveEvents ( events , duplicateStrategyOrOpts , fromCache = false ) {
let duplicateStrategy = duplicateStrategyOrOpts ;
let timelineWasEmpty = false ;
if ( typeof duplicateStrategyOrOpts === "object" ) {
( {
duplicateStrategy ,
fromCache = false ,
/* roomState, (not used here) */
timelineWasEmpty
} = duplicateStrategyOrOpts ) ;
} else if ( duplicateStrategyOrOpts !== undefined ) {
// Deprecation warning
// FIXME: Remove after 2023-06-01 (technical debt)
_logger . logger . warn ( "Overload deprecated: " + "`Room.addLiveEvents(events, duplicateStrategy?, fromCache?)` " + "is deprecated in favor of the overload with `Room.addLiveEvents(events, IAddLiveEventOptions)`" ) ;
}
2026-09-12 23:57:45 -04:00
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
2026-09-13 14:14:38 -04:00
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 ` ) ;
}
}
2026-09-12 23:57:45 -04:00
const threadRoots = this . findThreadRoots ( events ) ;
const eventsByThread = { } ;
const options = {
duplicateStrategy ,
fromCache ,
2026-09-13 14:14:38 -04:00
timelineWasEmpty
2026-09-12 23:57:45 -04:00
} ;
// List of extra events to check for being parents of any relations encountered
const neighbouringEvents = [ ... events ] ;
for ( const event of events ) {
2026-09-13 14:14:38 -04:00
var _threadId , _eventsByThread2 , _threadId3 ;
2026-09-12 23:57:45 -04:00
// 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
}
}
2026-09-13 14:14:38 -04:00
2026-09-12 23:57:45 -04:00
let {
shouldLiveInRoom ,
shouldLiveInThread ,
2026-09-13 14:14:38 -04:00
threadId
2026-09-12 23:57:45 -04:00
} = this . eventShouldLiveIn ( event , neighbouringEvents , threadRoots ) ;
if ( ! shouldLiveInThread && ! shouldLiveInRoom && event . isRelation ( ) ) {
try {
2026-09-13 14:14:38 -04:00
const parentEvent = new _event . MatrixEvent ( await this . client . fetchRoomEvent ( this . roomId , event . relationEventId ) ) ;
2026-09-12 23:57:45 -04:00
neighbouringEvents . push ( parentEvent ) ;
if ( parentEvent . threadRootId ) {
threadRoots . add ( parentEvent . threadRootId ) ;
const unsigned = event . getUnsigned ( ) ;
2026-09-13 14:14:38 -04:00
unsigned [ _event2 . UNSIGNED _THREAD _ID _FIELD . name ] = parentEvent . threadRootId ;
2026-09-12 23:57:45 -04:00
event . setUnsigned ( unsigned ) ;
}
( {
shouldLiveInRoom ,
shouldLiveInThread ,
2026-09-13 14:14:38 -04:00
threadId
2026-09-12 23:57:45 -04:00
} = this . eventShouldLiveIn ( event , neighbouringEvents , threadRoots ) ) ;
} catch ( e ) {
2026-09-13 14:14:38 -04:00
_logger . logger . error ( "Failed to load parent event of unhandled relation" , e ) ;
2026-09-12 23:57:45 -04:00
}
}
2026-09-13 14:14:38 -04:00
if ( shouldLiveInThread && ! eventsByThread [ ( _threadId = threadId ) !== null && _threadId !== void 0 ? _threadId : "" ] ) {
var _threadId2 ;
eventsByThread [ ( _threadId2 = threadId ) !== null && _threadId2 !== void 0 ? _threadId2 : "" ] = [ ] ;
2026-09-12 23:57:45 -04:00
}
2026-09-13 14:14:38 -04:00
( _eventsByThread2 = eventsByThread [ ( _threadId3 = threadId ) !== null && _threadId3 !== void 0 ? _threadId3 : "" ] ) === null || _eventsByThread2 === void 0 ? void 0 : _eventsByThread2 . push ( event ) ;
2026-09-12 23:57:45 -04:00
if ( shouldLiveInRoom ) {
this . addLiveEvent ( event , options ) ;
} else if ( ! shouldLiveInThread && event . isRelation ( ) ) {
this . relations . aggregateChildEvent ( event ) ;
}
}
Object . entries ( eventsByThread ) . forEach ( ( [ threadId , threadEvents ] ) => {
this . addThreadedEvents ( threadId , threadEvents , false ) ;
} ) ;
}
partitionThreadedEvents ( events ) {
// Indices to the events array, for readability
const ROOM = 0 ;
const THREAD = 1 ;
const UNKNOWN _RELATION = 2 ;
if ( this . client . supportsThreads ( ) ) {
const threadRoots = this . findThreadRoots ( events ) ;
return events . reduce ( ( memo , event ) => {
const {
shouldLiveInRoom ,
shouldLiveInThread ,
threadId
} = this . eventShouldLiveIn ( event , events , threadRoots ) ;
if ( shouldLiveInRoom ) {
memo [ ROOM ] . push ( event ) ;
}
if ( shouldLiveInThread ) {
2026-09-13 14:14:38 -04:00
event . setThreadId ( threadId !== null && threadId !== void 0 ? threadId : "" ) ;
2026-09-12 23:57:45 -04:00
memo [ THREAD ] . push ( event ) ;
}
if ( ! shouldLiveInThread && ! shouldLiveInRoom ) {
memo [ UNKNOWN _RELATION ] . push ( event ) ;
}
return memo ;
} , [ [ ] , [ ] , [ ] ] ) ;
} else {
2026-09-13 14:14:38 -04:00
// When `experimentalThreadSupport` is disabled treat all events as timelineEvents
2026-09-12 23:57:45 -04:00
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 ) {
const threadRootId = event . threadRootId ;
if ( threadRootId != undefined ) {
threadRoots . add ( threadRootId ) ;
}
}
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 => {
2026-09-13 14:14:38 -04:00
var _receipt$thread _id , _this$unthreadedRecei , _this$unthreadedRecei2 ;
2026-09-12 23:57:45 -04:00
const receipt = content [ eventId ] [ receiptType ] [ userId ] ;
2026-09-13 14:14:38 -04:00
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 : "" ) ;
2026-09-12 23:57:45 -04:00
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
2026-09-13 14:14:38 -04:00
// that the user has read that message.
// We can mark the room as read and not wait for the local echo
// from synapse
2026-09-12 23:57:45 -04:00
// This needs to be done after the initial sync as we do not want this
// logic to run whilst the room is being initialised
2026-09-13 14:14:38 -04:00
if ( this . client . isInitialSyncComplete ( ) && userId === this . client . getUserId ( ) ) {
2026-09-12 23:57:45 -04:00
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 {
2026-09-13 14:14:38 -04:00
var _this$cachedThreadRea2 ;
2026-09-12 23:57:45 -04:00
// 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
2026-09-13 14:14:38 -04:00
this . cachedThreadReadReceipts . set ( receipt . thread _id , [ ... ( ( _this$cachedThreadRea2 = this . cachedThreadReadReceipts . get ( receipt . thread _id ) ) !== null && _this$cachedThreadRea2 !== void 0 ? _this$cachedThreadRea2 : [ ] ) , {
2026-09-12 23:57:45 -04:00
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.
2026-09-13 14:14:38 -04:00
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 ) ) {
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
if ( event . getType ( ) === _event2 . EventType . Typing ) {
2026-09-12 23:57:45 -04:00
this . currentState . setTypingEvent ( event ) ;
2026-09-13 14:14:38 -04:00
} else if ( event . getType ( ) === _event2 . EventType . Receipt ) {
2026-09-12 23:57:45 -04:00
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.
2026-09-13 14:14:38 -04:00
const membershipEvent = this . currentState . getStateEvents ( _event2 . EventType . RoomMember , this . myUserId ) ;
2026-09-12 23:57:45 -04:00
if ( membershipEvent ) {
const membership = membershipEvent . getContent ( ) . membership ;
this . updateMyMembership ( membership ) ;
2026-09-13 14:14:38 -04:00
if ( membership === "invite" ) {
2026-09-12 23:57:45 -04:00
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
2026-09-13 14:14:38 -04:00
this . currentState . setStateEvents ( [ new _event . MatrixEvent ( {
2026-09-12 23:57:45 -04:00
type : strippedEvent . type ,
state _key : strippedEvent . state _key ,
content : strippedEvent . content ,
event _id : "$fake" + Date . now ( ) ,
room _id : this . roomId ,
2026-09-13 14:14:38 -04:00
user _id : this . myUserId // technically a lie
2026-09-12 23:57:45 -04:00
} ) ] ) ;
}
} ) ;
}
}
2026-09-13 14:14:38 -04:00
2026-09-12 23:57:45 -04:00
const oldName = this . name ;
this . name = this . calculateRoomName ( this . myUserId ) ;
2026-09-13 14:14:38 -04:00
this . normalizedName = ( 0 , _utils . normalize ) ( this . name ) ;
this . summary = new _roomSummary . RoomSummary ( this . roomId , {
2026-09-12 23:57:45 -04:00
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 ( ) {
2026-09-13 14:14:38 -04:00
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 ) ) ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ) {
2026-09-13 14:14:38 -04:00
let canInvite = this . getMyMembership ( ) === "join" ;
const powerLevelsEvent = this . currentState . getStateEvents ( _event2 . EventType . RoomPowerLevels , "" ) ;
2026-09-12 23:57:45 -04:00
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 ( ) {
2026-09-13 14:14:38 -04:00
const createEvent = this . currentState . getStateEvents ( _event2 . EventType . RoomCreate , "" ) ;
2026-09-12 23:57:45 -04:00
if ( ! createEvent ) {
if ( ! this . getTypeWarning ) {
2026-09-13 14:14:38 -04:00
_logger . logger . warn ( "[getType] Room " + this . roomId + " does not have an m.room.create event" ) ;
2026-09-12 23:57:45 -04:00
this . getTypeWarning = true ;
}
return undefined ;
}
2026-09-13 14:14:38 -04:00
return createEvent . getContent ( ) [ _event2 . RoomCreateTypeField ] ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Returns whether the room is a space - room as defined by MSC1772 .
* @ returns true if the room ' s type is RoomType . Space
* /
isSpaceRoom ( ) {
2026-09-13 14:14:38 -04:00
return this . getType ( ) === _event2 . RoomType . Space ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Returns whether the room is a call - room as defined by MSC3417 .
* @ returns true if the room ' s type is RoomType . UnstableCall
* /
isCallRoom ( ) {
2026-09-13 14:14:38 -04:00
return this . getType ( ) === _event2 . RoomType . UnstableCall ;
2026-09-12 23:57:45 -04:00
}
/ * *
* Returns whether the room is a video room .
* @ returns true if the room ' s type is RoomType . ElementVideo
* /
isElementVideoRoom ( ) {
2026-09-13 14:14:38 -04:00
return this . getType ( ) === _event2 . RoomType . ElementVideo ;
2026-09-12 23:57:45 -04:00
}
/ * *
* 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 ) {
2026-09-13 14:14:38 -04:00
const currentState = this . getLiveTimeline ( ) . getState ( _eventTimeline . EventTimeline . FORWARDS ) ;
2026-09-12 23:57:45 -04:00
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 ) {
2026-09-13 14:14:38 -04:00
// 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 ) {
2026-09-12 23:57:45 -04:00
return this . roomNameGenerator ( {
type : RoomNameType . Actual ,
2026-09-13 14:14:38 -04:00
name : mRoomName . getContent ( ) . name
2026-09-12 23:57:45 -04:00
} ) ;
}
}
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
2026-09-13 14:14:38 -04:00
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 ;
}
2026-09-12 23:57:45 -04:00
2026-09-13 14:14:38 -04:00
// get members that are NOT ourselves and are actually in the room.
2026-09-12 23:57:45 -04:00
let otherNames = [ ] ;
2026-09-13 14:14:38 -04:00
if ( this . summaryHeroes ) {
// if we have a summary, the member state events should be in the room state
this . summaryHeroes . forEach ( userId => {
2026-09-12 23:57:45 -04:00
// filter service members
2026-09-13 14:14:38 -04:00
if ( excludedUserIds . includes ( userId ) ) {
2026-09-12 23:57:45 -04:00
inviteJoinCount -- ;
return ;
}
2026-09-13 14:14:38 -04:00
const member = this . getMember ( userId ) ;
otherNames . push ( member ? member . name : userId ) ;
2026-09-12 23:57:45 -04:00
} ) ;
} else {
let otherMembers = this . currentState . getMembers ( ) . filter ( m => {
2026-09-13 14:14:38 -04:00
return m . userId !== userId && ( m . membership === "invite" || m . membership === "join" ) ;
2026-09-12 23:57:45 -04:00
} ) ;
otherMembers = otherMembers . filter ( ( {
userId
} ) => {
// filter service members
if ( excludedUserIds . includes ( userId ) ) {
inviteJoinCount -- ;
return false ;
}
return true ;
} ) ;
// make sure members have stable order
2026-09-13 14:14:38 -04:00
otherMembers . sort ( ( a , b ) => ( 0 , _utils . compare ) ( a . userId , b . userId ) ) ;
2026-09-12 23:57:45 -04:00
// 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
2026-09-13 14:14:38 -04:00
if ( myMembership == "join" ) {
const thirdPartyInvites = this . currentState . getStateEvents ( _event2 . EventType . RoomThirdPartyInvite ) ;
if ( thirdPartyInvites !== null && thirdPartyInvites !== void 0 && thirdPartyInvites . length ) {
2026-09-12 23:57:45 -04:00
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 => {
2026-09-13 14:14:38 -04:00
return m . userId !== userId && m . membership !== "invite" && m . membership !== "join" ;
2026-09-12 23:57:45 -04:00
} ) . 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 ;
}
2026-09-13 14:14:38 -04:00
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 ) ;
2026-09-12 23:57:45 -04:00
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 ( ) ;
2026-09-13 14:14:38 -04:00
const originalEventId = relation === null || relation === void 0 ? void 0 : relation . event _id ;
2026-09-12 23:57:45 -04:00
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
2026-09-13 14:14:38 -04:00
exports . Room = Room ;
2026-09-12 23:57:45 -04:00
const ALLOWED _TRANSITIONS = {
2026-09-13 14:14:38 -04:00
[ _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 ] : [ ]
2026-09-12 23:57:45 -04:00
} ;
2026-09-13 14:14:38 -04:00
let RoomNameType = /*#__PURE__*/ function ( RoomNameType ) {
2026-09-12 23:57:45 -04:00
RoomNameType [ RoomNameType [ "EmptyRoom" ] = 0 ] = "EmptyRoom" ;
RoomNameType [ RoomNameType [ "Generated" ] = 1 ] = "Generated" ;
RoomNameType [ RoomNameType [ "Actual" ] = 2 ] = "Actual" ;
return RoomNameType ;
} ( { } ) ;
2026-09-13 14:14:38 -04:00
exports . RoomNameType = RoomNameType ;
2026-09-12 23:57:45 -04:00
// 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