@@ -5510,11 +5510,15 @@ async function updateChat(chat, arrMessages = [], profile = null, fClicked = fal
55105510 continue ;
55115511 }
55125512
5513- // Get the oldest message in the DOM
5513+ // Get the oldest message in the DOM. Match any rendered element
5514+ // that maps to a message — including system events (which render
5515+ // as .msg-inline-timestamp, not .dmsg). Anchoring only to .dmsg
5516+ // would let older prepended messages slip BELOW a system event
5517+ // stranded at the top, pinning it there out of chronological order.
55145518 let oldestMsgElement = null ;
55155519 for ( let i = 0 ; i < domChatMessages . children . length ; i ++ ) {
55165520 const child = domChatMessages . children [ i ] ;
5517- if ( child . classList && child . classList . contains ( 'dmsg' ) ) {
5521+ if ( child . id && chat . messages . some ( m => m . id === child . id ) ) {
55185522 oldestMsgElement = child ;
55195523 break ;
55205524 }
@@ -5553,11 +5557,14 @@ async function updateChat(chat, arrMessages = [], profile = null, fClicked = fal
55535557 // This is a less common case, so we'll do a linear scan
55545558 let inserted = false ;
55555559
5556- // Get the message elements sorted by time (oldest to newest)
5560+ // Get the message elements sorted by time (oldest to newest).
5561+ // Include system events (.msg-inline-timestamp with an id) so a
5562+ // mid-list insert lands in true chronological order relative to
5563+ // them, not just relative to .dmsg rows.
55575564 let messageNodes = [ ] ;
55585565 for ( let i = 0 ; i < domChatMessages . children . length ; i ++ ) {
55595566 const child = domChatMessages . children [ i ] ;
5560- if ( child . id && child . classList && child . classList . contains ( 'dmsg' ) ) {
5567+ if ( child . id ) {
55615568 const childMsg = chat . messages . find ( m => m . id === child . id ) ;
55625569 if ( childMsg ) {
55635570 messageNodes . push ( { element : child , message : childMsg } ) ;
@@ -5710,12 +5717,14 @@ function _dedupeAdjacentDaySeparators() {
57105717 }
57115718 for ( const sep of stale ) sep . remove ( ) ;
57125719
5713- // Re-insert one date divider above each `.dmsg` that starts a new day.
5720+ // Re-insert one date divider above the first day-content element (a
5721+ // `.dmsg` row OR a system event — both carry `dataset.at`) that starts a
5722+ // new day. The "New" divider and stale separators have no `at`, so the
5723+ // Number.isFinite guard skips them.
57145724 let prevAt = null ;
57155725 const inserts = [ ] ;
57165726 for ( const child of domChatMessages . children ) {
5717- if ( ! child . classList ?. contains ( 'dmsg' ) ) continue ;
5718- const at = parseInt ( child . dataset . at , 10 ) ;
5727+ const at = parseInt ( child . dataset ?. at , 10 ) ;
57195728 if ( ! Number . isFinite ( at ) ) continue ;
57205729 if ( prevAt === null || _dmsgIsDifferentDay ( prevAt , at ) ) {
57215730 inserts . push ( { before : child , at } ) ;
@@ -6740,6 +6749,46 @@ function hideEditHistory() {
67406749 * Open a chat with a particular contact
67416750 * @param {string } contact
67426751 */
6752+ // System events (wallpaper/membership changes) are synthesized app-data
6753+ // events stored apart from the message-views pagination (kind 30078,
6754+ // distinguished by a `d` tag, so the kind-filtered message window skips them).
6755+ // To give them the SAME on-demand windowing as messages, the full set is
6756+ // fetched once into this side buffer, then revealed into the message cache
6757+ // only as far back as the loaded message window reaches — and progressively
6758+ // as the user scrolls older messages into view.
6759+ const _systemEventBuffer = new Map ( ) ; // chatId -> sorted array of system-event msg objects
6760+
6761+ /** Reveal buffered system events that fall within the currently-loaded
6762+ * message window into the event cache. Returns the newly-revealed ones so
6763+ * the caller can hand them to updateChat. Dedup-safe via cache.addEvent. */
6764+ function revealSystemEventsInWindow ( chatId ) {
6765+ const buffer = _systemEventBuffer . get ( chatId ) ;
6766+ if ( ! buffer || ! buffer . length ) return [ ] ;
6767+
6768+ // Lower bound of the loaded window = oldest real (non-system) message in
6769+ // the cache. Below that, messages haven't been paged in yet, so their
6770+ // system events stay hidden. Once every message is loaded, the bound drops
6771+ // away and the remaining (oldest) system events reveal too.
6772+ const stats = eventCache . getStats ( chatId ) ;
6773+ let bound = - Infinity ;
6774+ if ( ! stats ?. isFullyLoaded ) {
6775+ const loaded = eventCache . getEvents ( chatId ) || [ ] ;
6776+ let oldestReal = Infinity ;
6777+ for ( const m of loaded ) {
6778+ if ( ! m . system_event && m . at < oldestReal ) oldestReal = m . at ;
6779+ }
6780+ if ( oldestReal !== Infinity ) bound = oldestReal ;
6781+ }
6782+
6783+ const revealed = [ ] ;
6784+ for ( const sm of buffer ) {
6785+ if ( sm . at >= bound && eventCache . addEvent ( chatId , sm ) ) {
6786+ revealed . push ( sm ) ;
6787+ }
6788+ }
6789+ return revealed ;
6790+ }
6791+
67436792async function openChat ( contact ) {
67446793 pushBack ( 'chat' , closeChat ) ;
67456794 // Abandon a wallpaper preview staged in a different chat so its edit
@@ -6854,30 +6903,31 @@ async function openChat(contact) {
68546903 // Merge any historical PIVX payments — helper in pivx.js
68556904 await mergePivxPaymentsIntoChat ( contact , initialMessages ) ;
68566905
6857- // Load system events (member joined/left, etc.) for this chat and merge them
6906+ // Load system events (wallpaper/membership changes). They're fetched in
6907+ // full but buffered — only the ones inside the initially-loaded message
6908+ // window are revealed now; the rest surface as the user scrolls older
6909+ // messages into view (same on-demand windowing as messages).
68586910 try {
68596911 const systemEvents = await invoke ( 'get_system_events' , { conversationId : contact } ) ;
6860- if ( systemEvents && systemEvents . length > 0 ) {
6861- for ( const event of systemEvents ) {
6862- // Check if this event already exists in messages
6863- const existing = initialMessages . find ( m => m . id === event . id ) ;
6864- if ( ! existing ) {
6865- const systemMsg = {
6866- id : event . id ,
6867- at : event . at ,
6868- content : event . content ,
6869- mine : false ,
6870- attachments : [ ] ,
6871- system_event : {
6872- event_type : event . event_type ,
6873- member_npub : event . member_npub ,
6874- }
6875- } ;
6876- // Add to cache (which also adds to initialMessages since they share the same array reference)
6877- eventCache . addEvent ( contact , systemMsg ) ;
6878- }
6879- }
6880- // Re-sort by timestamp after adding system events
6912+ const buffer = ( systemEvents || [ ] )
6913+ . filter ( event => ! initialMessages . find ( m => m . id === event . id ) )
6914+ . map ( event => ( {
6915+ id : event . id ,
6916+ at : event . at ,
6917+ content : event . content ,
6918+ mine : false ,
6919+ attachments : [ ] ,
6920+ system_event : {
6921+ event_type : event . event_type ,
6922+ member_npub : event . member_npub ,
6923+ } ,
6924+ } ) )
6925+ . sort ( ( a , b ) => a . at - b . at ) ;
6926+ _systemEventBuffer . set ( contact , buffer ) ;
6927+ // revealSystemEventsInWindow adds into the cache array, which is
6928+ // aliased to initialMessages — re-sort so the pre-paint renders them
6929+ // in chronological order.
6930+ if ( revealSystemEventsInWindow ( contact ) . length > 0 ) {
68816931 initialMessages . sort ( ( a , b ) => a . at - b . at ) ;
68826932 }
68836933 } catch ( e ) {
0 commit comments