diff --git a/__imap.cpp b/__imap.cpp index 2b5edb4..b2506e2 100644 --- a/__imap.cpp +++ b/__imap.cpp @@ -832,7 +832,7 @@ namespace jet { return extractLiteralAt(fetchLine, braceEnd); } - coreutils::MString __imap::listMessages(const std::string &folder, int limit) { + coreutils::MString __imap::listMessages(const std::string &folder, int limit, int offset) { if (!selectFolderIfNeeded(folder)) throw coreutils::Exception("imap: could not select folder " + folder, __FILE__, __LINE__, 502); @@ -843,22 +843,30 @@ namespace jet { throw coreutils::Exception("imap: FETCH failed", __FILE__, __LINE__, 502); std::vector items = parseSummaryLines(lines); + int total = (int)items.size(); - // Keep only the newest `limit` - UIDs are assigned in strictly - // ascending delivery order (see MailFolder.h, BARANTMail, built - // earlier this session), so the tail of this UID-ascending list - // is the newest mail. - if ((int)items.size() > limit) - items.erase(items.begin(), items.end() - limit); - std::reverse(items.begin(), items.end()); + // items is UID-ascending (oldest-first) - UIDs are assigned in + // strictly ascending delivery order (see MailFolder.h, BARANTMail, + // built earlier this session), so the tail is the newest mail. + // `offset` counts back from the newest message (0 = the very + // newest), so the page is the `limit`-wide slice ending `offset` + // messages from the end. + int end = total - offset; + if (end < 0) + end = 0; + int start = end - limit; + if (start < 0) + start = 0; + std::vector page(items.begin() + start, items.begin() + end); + std::reverse(page.begin(), page.end()); - std::string json = "["; - for (size_t i = 0; i < items.size(); ++i) { + std::string json = "{\"total\":" + std::to_string(total) + ",\"messages\":["; + for (size_t i = 0; i < page.size(); ++i) { if (i) json += ","; - json += items[i]; + json += page[i]; } - json += "]"; + json += "]}"; return coreutils::MString(json); } diff --git a/__imap.h b/__imap.h index b04d44b..7d396d9 100644 --- a/__imap.h +++ b/__imap.h @@ -55,7 +55,9 @@ namespace jet { // just write whatever this returns straight into their own `out`, // the same one-call-does-everything shape __sql's query() call // has relative to . - coreutils::MString listMessages(const std::string &folder, int limit); + /// offset counts back from the newest message (0 = the newest + /// page); the returned JSON is {"total":N,"messages":[...]}. + coreutils::MString listMessages(const std::string &folder, int limit, int offset); coreutils::MString searchMessages(const std::string &folder, const std::string &query); coreutils::MString fetchMessage(const std::string &folder, uint32_t uid); coreutils::MString setFlag(const std::string &folder, uint32_t uid, const std::string &flag, bool add); diff --git a/__imaplist.cpp b/__imaplist.cpp index 46acb69..3c4e049 100644 --- a/__imaplist.cpp +++ b/__imaplist.cpp @@ -20,7 +20,12 @@ namespace jet { if (limit <= 0) limit = 50; - out = global.getImapSession(sessionId)->listMessages(folder, limit); + std::string offsetStr = resolveKeyword("offset").str(); + int offset = offsetStr.empty() ? 0 : atoi(offsetStr.c_str()); + if (offset < 0) + offset = 0; + + out = global.getImapSession(sessionId)->listMessages(folder, limit, offset); } } diff --git a/jetc/barant-site/assets/images/email.js b/jetc/barant-site/assets/images/email.js index 9313894..f7ec8cd 100644 --- a/jetc/barant-site/assets/images/email.js +++ b/jetc/barant-site/assets/images/email.js @@ -35,6 +35,14 @@ '' + '' + '' + + '', + pagePrev: + '' + + '' + + '', + pageNext: + '' + + '' + '' }; @@ -49,6 +57,19 @@ // showing search results instead (see performSearch()/clearSearch() // and loadMessageList()'s own branch on this). var currentSearchQuery = null; + // How many newest-first messages /email/list has already skipped - + // 0 is the newest page. Must match LIST_LIMIT server-side (email.jet + // defaults "limit" to 50 too) so a page is exactly one "limit" + // worth of messages. Reset to 0 everywhere currentFolder/currentUid + // already get reset (folder switch, search, account switch) - a + // stale offset from one folder/account applied to another would + // silently skip or duplicate messages. + var currentOffset = 0; + var LIST_LIMIT = 50; + // Total message count the server reported for the CURRENT page's + // folder/query, used only to enable/disable the Next button - + // updated every time updateListCount() runs. + var currentTotal = 0; // The array last handed to renderMessageList() - kept so openMessage() // can flip a single message's .seen flag locally and recompute the // toolbar's unread/total count without a round trip. @@ -70,6 +91,7 @@ return; currentFolder = folder; currentUid = null; + currentOffset = 0; // A folder click always means "show me this folder, plain" - drops // out of search mode even if the folder didn't actually change, // which is exactly how a user would expect to back out of a @@ -129,6 +151,7 @@ '
' + '' + escapeHtml(userEmail) + '' + '' + + '' + '' + '' + '' + @@ -207,6 +230,7 @@ var query = input.value.trim(); currentSearchQuery = query === '' ? null : query; currentUid = null; + currentOffset = 0; document.getElementById('email-reading-pane').innerHTML = '
Select a message to read it.
'; loadMessageList(); @@ -234,10 +258,51 @@ countEl.textContent = '(' + unread + '/' + messages.length + ')'; } - function renderMessageList(messages) { + // total === null means "not paginated" (search results, or nothing + // loaded yet) - hides the whole control rather than showing a + // meaningless "1-0 of null". Otherwise renders the current page's + // 1-based range and enables Prev/Next based on currentOffset/total, + // matching how far a click on either button would actually move. + function updatePaginationControls(total) { + var el = document.getElementById('email-pagination'); + if (!el) + return; + currentTotal = total || 0; + if (total === null || total === 0) { + el.innerHTML = ''; + return; + } + var rangeStart = total === 0 ? 0 : currentOffset + 1; + var rangeEnd = Math.min(currentOffset + LIST_LIMIT, total); + var atStart = currentOffset <= 0; + var atEnd = currentOffset + LIST_LIMIT >= total; + el.innerHTML = + '' + + '' + rangeStart + '–' + rangeEnd + ' of ' + total + '' + + ''; + var prevBtn = document.getElementById('email-page-prev'); + var nextBtn = document.getElementById('email-page-next'); + if (prevBtn) prevBtn.addEventListener('click', function() { + if (currentOffset <= 0) + return; + currentOffset = Math.max(0, currentOffset - LIST_LIMIT); + currentUid = null; + loadMessageList(); + }); + if (nextBtn) nextBtn.addEventListener('click', function() { + if (currentOffset + LIST_LIMIT >= currentTotal) + return; + currentOffset += LIST_LIMIT; + currentUid = null; + loadMessageList(); + }); + } + + function renderMessageList(messages, total) { var list = document.getElementById('email-list'); currentMessages = messages; updateListCount(messages); + updatePaginationControls(total === undefined ? null : total); if (!messages.length) { list.innerHTML = '
No messages.
'; return; @@ -397,10 +462,11 @@ // (see __imap.cpp's shared parseSummaryLines()), so this is the // only place that needs to know search mode even exists - // renderMessageList() below has no idea which one produced what - // it's rendering. + // it's rendering. Search isn't paginated (offset/limit only apply + // to the plain listing) - a search result set is returned in full. var request = currentSearchQuery ? post('/email/search', { folder: currentFolder, query: currentSearchQuery }) - : post('/email/list', { folder: currentFolder }); + : post('/email/list', { folder: currentFolder, offset: currentOffset, limit: LIST_LIMIT }); return Promise.all([ request, skipFade ? true : delay(180) @@ -409,8 +475,10 @@ var res = results[0]; if (!res.ok && res.error) list.innerHTML = '
' + escapeHtml(res.error) + '
'; + else if (currentSearchQuery) + renderMessageList(Array.isArray(res) ? res : [], null); else - renderMessageList(Array.isArray(res) ? res : []); + renderMessageList(Array.isArray(res.messages) ? res.messages : [], res.total || 0); // Every place that reloads the list (connect, folder switch, // refresh, post-trash) is a place the unseen counts next to // each folder name could plausibly have changed too - one @@ -482,6 +550,7 @@ function resetViewForActiveAccount(user) { currentFolder = 'Inbox'; currentUid = null; + currentOffset = 0; currentSearchQuery = null; var searchInput = document.getElementById('email-search-input'); if (searchInput) searchInput.value = ''; diff --git a/jetc/barant-site/src/email.jet b/jetc/barant-site/src/email.jet index 1fa0525..8bc4209 100644 --- a/jetc/barant-site/src/email.jet +++ b/jetc/barant-site/src/email.jet @@ -67,7 +67,7 @@ - + {"ok":false,"error":"$[_imaperr]"}