Skip to content

Commit 8926a6d

Browse files
erancybersecclaude
andcommitted
feat: fix scroll-up pagination to load older messages from API
- Rewrote chatFetchMessages to accept a page parameter and return { msgs, totalPages } matching the Evolution API page-based response format ({ messages: { total, pages, currentPage, records[] } }) - Added chatFetchOlderFromApi() that increments apiCurrentPage and fetches the next page from the API when local cache is exhausted on scroll-up - Track apiCurrentPage / apiTotalPages / _loadingOlderFromApi on chatState; reset on every chatOpenConversation - historyFullyLoaded is now only set true when the API confirms no more pages exist (totalPages <= 1 or nextPage > totalPages), never based on local message count - Alias-JID fetching (LID/phone dedup) limited to page 1 only to avoid duplicate loads on paginated fetches - Removed noisy debug console.log statements left from earlier investigation - All four callers of chatFetchMessages updated for new return shape Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 650377d commit 8926a6d

File tree

1 file changed

+114
-68
lines changed

1 file changed

+114
-68
lines changed

index.html

Lines changed: 114 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)