@@ -4244,7 +4244,9 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
42444244 }
42454245
42464246 // ── API: Fetch messages for a conversation ───────────────────────────
4247- async function chatFetchMessages ( jid ) {
4247+ // page: 1-based page number (API returns pages of 50, newest-first on page 1)
4248+ // Returns { msgs: parsed[], totalPages: N }
4249+ async function chatFetchMessages ( jid , page = 1 ) {
42484250 try {
42494251 const cfg = getCfg ( ) ;
42504252 // Collect alias JIDs (LID/phone dedup) for fetching messages from both
@@ -4253,93 +4255,82 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
42534255 if ( pri === jid ) aliasJids . add ( sec ) ;
42544256 if ( sec === jid ) aliasJids . add ( pri ) ;
42554257 } ) ;
4256- console . log ( '[Chat] fetchMessages for:' , jid , aliasJids . size > 1 ? `(+aliases: ${ [ ...aliasJids ] . filter ( j => j !== jid ) . join ( ', ' ) } )` : '' ) ;
42574258
42584259 // Try multiple request formats — Evolution API varies across versions
42594260 let data = null ;
4260- let lastErr = null ;
42614261
4262- // Strategy 1: POST with where.key.remoteJid filter
4262+ // Strategy 1: POST with where.key.remoteJid + page
42634263 try {
42644264 data = await chatApiCall ( `/chat/findMessages/${ cfg . instance } ` , {
4265- where : { key : { remoteJid : jid } }
4265+ where : { key : { remoteJid : jid } } , page
42664266 } ) ;
4267- } catch ( e ) { lastErr = e ; console . warn ( '[Chat] findMessages strategy 1 failed:' , e . message ) ; }
4267+ } catch ( e ) { console . warn ( '[Chat] findMessages strategy 1 failed:' , e . message ) ; }
42684268
4269- // Strategy 2: POST with just remoteJid (flat filter)
4269+ // Strategy 2: flat remoteJid filter + page
42704270 if ( ! data || ( typeof data === 'object' && ! Array . isArray ( data ) && ! data . messages && ! data . records ) ) {
42714271 try {
42724272 data = await chatApiCall ( `/chat/findMessages/${ cfg . instance } ` , {
4273- where : { remoteJid : jid }
4273+ where : { remoteJid : jid } , page
42744274 } ) ;
4275- } catch ( e ) { lastErr = e ; console . warn ( '[Chat] findMessages strategy 2 failed:' , e . message ) ; }
4275+ } catch ( e ) { console . warn ( '[Chat] findMessages strategy 2 failed:' , e . message ) ; }
42764276 }
42774277
4278- // Strategy 3: No filter (fetch all, filter client-side)
4278+ // Strategy 3: no filter (fetch all, filter client-side)
42794279 if ( ! data || ( typeof data === 'object' && ! Array . isArray ( data ) && ! data . messages && ! data . records ) ) {
42804280 try {
4281- data = await chatApiCall ( `/chat/findMessages/${ cfg . instance } ` , { } ) ;
4282- } catch ( e ) { lastErr = e ; console . warn ( '[Chat] findMessages strategy 3 failed:' , e . message ) ; }
4281+ data = await chatApiCall ( `/chat/findMessages/${ cfg . instance } ` , { page } ) ;
4282+ } catch ( e ) { console . warn ( '[Chat] findMessages strategy 3 failed:' , e . message ) ; }
42834283 }
42844284
4285- console . log ( '[Chat] findMessages raw response:' , typeof data , JSON . stringify ( data ) . slice ( 0 , 500 ) ) ;
4286-
4287- // Extract message array from various response shapes
4288- let msgs = [ ] ;
4285+ // Extract message array + pagination metadata from various response shapes
4286+ let msgs = [ ] , totalPages = 1 ;
42894287 if ( Array . isArray ( data ) ) {
42904288 msgs = data ;
42914289 } else if ( data && typeof data === 'object' ) {
4292- // Try common nested structures: data.messages, data.records, data.messages.records, data.data
4293- if ( Array . isArray ( data . messages ) ) msgs = data . messages ;
4294- else if ( Array . isArray ( data . records ) ) msgs = data . records ;
4295- else if ( data . messages && Array . isArray ( data . messages . records ) ) msgs = data . messages . records ;
4296- else if ( Array . isArray ( data . data ) ) msgs = data . data ;
4297- else {
4298- // Last resort: find the first array property
4290+ if ( data . messages && Array . isArray ( data . messages . records ) ) {
4291+ msgs = data . messages . records ;
4292+ totalPages = data . messages . pages || 1 ;
4293+ } else if ( Array . isArray ( data . messages ) ) {
4294+ msgs = data . messages ;
4295+ } else if ( Array . isArray ( data . records ) ) {
4296+ msgs = data . records ;
4297+ totalPages = data . pages || 1 ;
4298+ } else if ( Array . isArray ( data . data ) ) {
4299+ msgs = data . data ;
4300+ } else {
42994301 for ( const key of Object . keys ( data ) ) {
4300- if ( Array . isArray ( data [ key ] ) && data [ key ] . length > 0 ) {
4301- console . log ( '[Chat] findMessages: using array from key:' , key , 'length:' , data [ key ] . length ) ;
4302- msgs = data [ key ] ;
4303- break ;
4304- }
4302+ if ( Array . isArray ( data [ key ] ) && data [ key ] . length > 0 ) { msgs = data [ key ] ; break ; }
43054303 }
43064304 }
43074305 }
43084306
4309- console . log ( '[Chat] findMessages extracted msgs count:' , msgs . length ) ;
4310-
4311- // Also fetch messages from alias JIDs (LID/phone dedup)
4312- for ( const aliasJid of aliasJids ) {
4313- if ( aliasJid === jid ) continue ;
4314- try {
4315- const aliasData = await chatApiCall ( `/chat/findMessages/${ cfg . instance } ` , {
4316- where : { key : { remoteJid : aliasJid } }
4317- } ) ;
4318- let aliasMsgs = [ ] ;
4319- if ( Array . isArray ( aliasData ) ) aliasMsgs = aliasData ;
4320- else if ( aliasData ?. messages ?. records ) aliasMsgs = aliasData . messages . records ;
4321- else if ( Array . isArray ( aliasData ?. messages ) ) aliasMsgs = aliasData . messages ;
4322- else if ( Array . isArray ( aliasData ?. records ) ) aliasMsgs = aliasData . records ;
4323- if ( aliasMsgs . length ) {
4324- console . log ( `[Chat] alias ${ aliasJid } : fetched ${ aliasMsgs . length } additional messages` ) ;
4325- msgs = msgs . concat ( aliasMsgs ) ;
4326- }
4327- } catch ( e ) { console . warn ( '[Chat] alias fetch failed for' , aliasJid , e . message ) ; }
4307+ console . log ( `[Chat] findMessages page=${ page } /${ totalPages } records=${ msgs . length } ` ) ;
4308+
4309+ // Also fetch from alias JIDs (LID/phone dedup) on page 1 only
4310+ if ( page === 1 ) {
4311+ for ( const aliasJid of aliasJids ) {
4312+ if ( aliasJid === jid ) continue ;
4313+ try {
4314+ const aliasData = await chatApiCall ( `/chat/findMessages/${ cfg . instance } ` , {
4315+ where : { key : { remoteJid : aliasJid } } , page : 1
4316+ } ) ;
4317+ let aliasMsgs = [ ] ;
4318+ if ( Array . isArray ( aliasData ) ) aliasMsgs = aliasData ;
4319+ else if ( aliasData ?. messages ?. records ) aliasMsgs = aliasData . messages . records ;
4320+ else if ( Array . isArray ( aliasData ?. messages ) ) aliasMsgs = aliasData . messages ;
4321+ else if ( Array . isArray ( aliasData ?. records ) ) aliasMsgs = aliasData . records ;
4322+ if ( aliasMsgs . length ) msgs = msgs . concat ( aliasMsgs ) ;
4323+ } catch ( e ) { console . warn ( '[Chat] alias fetch failed for' , aliasJid , e . message ) ; }
4324+ }
43284325 }
43294326
4330- // Client-side filter — accept messages from primary + alias JIDs
4331- const beforeFilter = msgs . length ;
4332- msgs = msgs . filter ( m => {
4333- const mJid = m . key ?. remoteJid || m . remoteJid || '' ;
4334- return aliasJids . has ( mJid ) ;
4335- } ) ;
4336- console . log ( '[Chat] findMessages: total=' , beforeFilter , 'after jid filter=' , msgs . length , 'aliasJids:' , [ ...aliasJids ] ) ;
4327+ // Client-side filter — keep only messages belonging to this conversation
4328+ msgs = msgs . filter ( m => aliasJids . has ( m . key ?. remoteJid || m . remoteJid || '' ) ) ;
43374329 const parsed = msgs . map ( m => chatParseMessage ( m , jid ) ) . filter ( Boolean ) ;
4338- console . log ( '[Chat] parsed messages:' , parsed . length ) ;
43394330 if ( parsed . length ) await chatDbPutMany ( 'chat_messages' , parsed ) ;
43404331 chatUpdateContactNamesFromMessages ( jid , parsed ) ;
4341- return parsed ;
4342- } catch ( e ) { console . warn ( 'chatFetchMessages error:' , e ) ; return [ ] ; }
4332+ return { msgs : parsed , totalPages } ;
4333+ } catch ( e ) { console . warn ( 'chatFetchMessages error:' , e ) ; return { msgs : [ ] , totalPages : 1 } ; }
43434334 }
43444335
43454336 // ── Update contact names from message pushName data ──────────────────
@@ -4670,7 +4661,7 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
46704661 }
46714662 if ( ! lastMsgKey || ! lastMsgKey . id ) {
46724663 // Fetch messages first to get a valid message ID
4673- const msgs = await chatFetchMessages ( jid ) ;
4664+ const { msgs } = await chatFetchMessages ( jid ) ;
46744665 if ( msgs . length ) {
46754666 const last = msgs [ msgs . length - 1 ] ;
46764667 lastMsgKey = { remoteJid : jid , fromMe : last . fromMe , id : last . id } ;
@@ -4802,8 +4793,11 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
48024793 // ── Open / close conversation ────────────────────────────────────────
48034794 async function chatOpenConversation ( jid ) {
48044795 chatState . activeJid = jid ;
4805- chatState . allFetchedMessages = [ ] ; // full fetched set for this jid
4796+ chatState . allFetchedMessages = [ ] ;
48064797 chatState . historyFullyLoaded = false ;
4798+ chatState . _loadingOlderFromApi = false ;
4799+ chatState . apiCurrentPage = 1 ;
4800+ chatState . apiTotalPages = 1 ;
48074801 // Update UI
48084802 document . getElementById ( 'chat-empty-state' ) . style . display = 'none' ;
48094803 const active = document . getElementById ( 'chat-active' ) ;
@@ -4841,22 +4835,24 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
48414835 chatState . allFetchedMessages = cached ;
48424836 // Show last N messages initially
48434837 chatState . messages = cached . slice ( - CHAT_PAGE_SIZE ) ;
4844- chatState . historyFullyLoaded = cached . length <= CHAT_PAGE_SIZE ;
4838+ chatState . historyFullyLoaded = false ; // API will confirm if truly exhausted
48454839 chatRenderMessages ( ) ;
48464840 chatScrollToBottom ( false ) ;
48474841 }
48484842 } catch ( e ) { }
4849- // Fetch fresh from API
4850- const fresh = await chatFetchMessages ( jid ) ;
4843+ // Fetch fresh from API (page 1 = most recent messages)
4844+ const { msgs : fresh , totalPages } = await chatFetchMessages ( jid , 1 ) ;
4845+ chatState . apiTotalPages = totalPages ;
4846+ chatState . apiCurrentPage = 1 ;
48514847 if ( fresh . length ) {
48524848 const all = fresh . sort ( ( a , b ) => a . timestamp - b . timestamp ) ;
48534849 chatState . allFetchedMessages = all ;
48544850 chatState . messages = all . slice ( - CHAT_PAGE_SIZE ) ;
4855- chatState . historyFullyLoaded = all . length <= CHAT_PAGE_SIZE ;
4851+ chatState . historyFullyLoaded = totalPages <= 1 ; // only 1 page = all loaded
48564852 } else if ( ! chatState . allFetchedMessages . length ) {
48574853 chatState . allFetchedMessages = [ ] ;
48584854 chatState . messages = [ ] ;
4859- chatState . historyFullyLoaded = true ;
4855+ chatState . historyFullyLoaded = true ; // truly empty conversation
48604856 }
48614857 chatRenderMessages ( ) ;
48624858 chatScrollToBottom ( false ) ;
@@ -4882,19 +4878,23 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
48824878 }
48834879
48844880 function chatLoadOlderMessages ( ) {
4885- if ( chatState . historyFullyLoaded ) return ;
4881+ if ( chatState . historyFullyLoaded || chatState . _loadingOlderFromApi ) return ;
48864882 const all = chatState . allFetchedMessages ;
48874883 const currentOldest = chatState . messages . length > 0 ? chatState . messages [ 0 ] . timestamp : Infinity ;
48884884 // Find messages older than what we currently show
48894885 const older = all . filter ( m => m . timestamp < currentOldest ) ;
48904886 if ( ! older . length ) {
4891- chatState . historyFullyLoaded = true ;
4887+ // Local cache exhausted — fetch next page from API
4888+ chatFetchOlderFromApi ( ) ;
48924889 return ;
48934890 }
48944891 // Take the most recent PAGE_SIZE of the older messages
48954892 const page = older . slice ( - CHAT_PAGE_SIZE ) ;
48964893 chatState . messages = [ ...page , ...chatState . messages ] ;
4897- if ( older . length <= CHAT_PAGE_SIZE ) chatState . historyFullyLoaded = true ;
4894+ if ( older . length <= CHAT_PAGE_SIZE ) {
4895+ // Showed all local cache — next scroll will hit API
4896+ // Don't mark fully loaded yet; let the next scroll attempt API fetch
4897+ }
48984898 // Preserve scroll position
48994899 const msgEl = document . getElementById ( 'chat-messages' ) ;
49004900 const prevScrollHeight = msgEl . scrollHeight ;
@@ -4904,11 +4904,57 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
49044904 msgEl . scrollTop = newScrollHeight - prevScrollHeight ;
49054905 }
49064906
4907+ async function chatFetchOlderFromApi ( ) {
4908+ if ( chatState . _loadingOlderFromApi || ! chatState . activeJid ) return ;
4909+ const nextPage = ( chatState . apiCurrentPage || 1 ) + 1 ;
4910+ const totalPages = chatState . apiTotalPages || 1 ;
4911+ if ( nextPage > totalPages ) { chatState . historyFullyLoaded = true ; return ; }
4912+
4913+ chatState . _loadingOlderFromApi = true ;
4914+ const jid = chatState . activeJid ;
4915+ const msgEl = document . getElementById ( 'chat-messages' ) ;
4916+ // Show spinner at top of messages
4917+ const spinner = document . createElement ( 'div' ) ;
4918+ spinner . id = 'chat-older-loading' ;
4919+ spinner . className = 'flex justify-center py-3' ;
4920+ spinner . innerHTML = '<div class="typing-dots"><span></span><span></span><span></span></div>' ;
4921+ if ( msgEl ) msgEl . prepend ( spinner ) ;
4922+ const prevScrollHeight = msgEl ? msgEl . scrollHeight : 0 ;
4923+ try {
4924+ const { msgs : older , totalPages : tp } = await chatFetchMessages ( jid , nextPage ) ;
4925+ chatState . apiCurrentPage = nextPage ;
4926+ chatState . apiTotalPages = tp || totalPages ;
4927+ if ( ! older . length ) {
4928+ chatState . historyFullyLoaded = true ;
4929+ } else {
4930+ const existingIds = new Set ( chatState . allFetchedMessages . map ( m => m . id ) ) ;
4931+ const newMsgs = older . filter ( m => ! existingIds . has ( m . id ) ) ;
4932+ if ( ! newMsgs . length ) {
4933+ chatState . historyFullyLoaded = true ;
4934+ } else {
4935+ chatState . allFetchedMessages = [ ...newMsgs , ...chatState . allFetchedMessages ]
4936+ . sort ( ( a , b ) => a . timestamp - b . timestamp ) ;
4937+ const display = newMsgs . slice ( - CHAT_PAGE_SIZE ) ;
4938+ chatState . messages = [ ...display , ...chatState . messages ] ;
4939+ if ( nextPage >= ( chatState . apiTotalPages || 1 ) ) chatState . historyFullyLoaded = true ;
4940+ chatRenderMessages ( ) ;
4941+ if ( msgEl ) msgEl . scrollTop = msgEl . scrollHeight - prevScrollHeight ;
4942+ }
4943+ }
4944+ } catch ( e ) {
4945+ console . warn ( '[Chat] fetchOlderFromApi error:' , e ) ;
4946+ chatState . historyFullyLoaded = true ;
4947+ }
4948+ document . getElementById ( 'chat-older-loading' ) ?. remove ( ) ;
4949+ chatState . _loadingOlderFromApi = false ;
4950+ }
4951+
49074952 function chatCloseConversation ( ) {
49084953 chatState . activeJid = null ;
49094954 chatState . messages = [ ] ;
49104955 chatState . allFetchedMessages = [ ] ;
49114956 chatState . historyFullyLoaded = false ;
4957+ chatState . _loadingOlderFromApi = false ;
49124958 chatState . replyTo = null ;
49134959 if ( chatState . pollTimer ) { clearInterval ( chatState . pollTimer ) ; chatState . pollTimer = null ; }
49144960 const msgEl = document . getElementById ( 'chat-messages' ) ;
@@ -4923,7 +4969,7 @@ <h3 class="text-sm font-semibold text-gray-800">Privacy Settings</h3>
49234969 async function chatPollMessages ( ) {
49244970 if ( ! chatState . activeJid ) return ;
49254971 const jid = chatState . activeJid ;
4926- const fresh = await chatFetchMessages ( jid ) ;
4972+ const { msgs : fresh } = await chatFetchMessages ( jid , 1 ) ;
49274973 if ( ! fresh . length ) return ;
49284974 const existingIds = new Set ( chatState . messages . map ( m => m . id ) ) ;
49294975 const allIds = new Set ( chatState . allFetchedMessages . map ( m => m . id ) ) ;
0 commit comments