mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
Email search: instant local-cache filter + stop blanking the grid
Two pain points: - IMAP server search is genuinely slow. - The grid blanked to a whirlpool on every keystroke, so even fast searches felt dead because you couldn't see your own results. Fix: - _localSearchFilter runs synchronously on every keystroke, filtering the pre-search snapshot by subject / from-name / from-address / snippet so the grid responds immediately. Snapshot is taken on the first non-empty keystroke and restored when the input is cleared. - _doSearch no longer renders the loading-whirlpool spinner into the grid. The local filter already shows useful results; surface 'Searching…' in the stats badge to indicate the server search is in flight. - When server results land, they replace the grid; if the user has already typed past them, the seq guard skips the stale render.
This commit is contained in:
@@ -1071,6 +1071,10 @@ export function openEmailLibrary(opts = {}) {
|
||||
let searchTimer = null;
|
||||
document.getElementById('email-lib-search').addEventListener('input', (e) => {
|
||||
state._libSearch = e.target.value;
|
||||
// Instant local filter so the grid responds on every keystroke even
|
||||
// before the server-side IMAP search lands. The debounced server
|
||||
// search still runs and replaces the grid when it returns.
|
||||
_localSearchFilter(state._libSearch);
|
||||
if (searchTimer) clearTimeout(searchTimer);
|
||||
searchTimer = setTimeout(_doSearch, 350);
|
||||
});
|
||||
@@ -1592,6 +1596,52 @@ function _crossFolderCandidates() {
|
||||
return Array.from(new Set(candidates.filter(Boolean)));
|
||||
}
|
||||
|
||||
// Snapshot of state._libEmails taken right before search starts so we
|
||||
// can both filter locally and restore on clear without re-fetching.
|
||||
let _libPreSearchEmails = null;
|
||||
let _libPreSearchTotal = 0;
|
||||
|
||||
function _matchesQuery(em, q) {
|
||||
const needle = q.toLowerCase();
|
||||
return (
|
||||
String(em.subject || '').toLowerCase().includes(needle) ||
|
||||
String(em.from_name || '').toLowerCase().includes(needle) ||
|
||||
String(em.from_address || '').toLowerCase().includes(needle) ||
|
||||
String(em.snippet || em.preview || '').toLowerCase().includes(needle)
|
||||
);
|
||||
}
|
||||
|
||||
// Instant client-side filter — fires on every keystroke. Filters the
|
||||
// pre-search snapshot so the user sees something immediately even
|
||||
// though the server search hasn't returned yet.
|
||||
function _localSearchFilter(query) {
|
||||
const q = (query || '').trim();
|
||||
// First non-empty keystroke after an empty search: take the snapshot.
|
||||
if (q.length >= 1 && !_libPreSearchEmails) {
|
||||
_libPreSearchEmails = (state._libEmails || []).slice();
|
||||
_libPreSearchTotal = state._libTotal;
|
||||
}
|
||||
if (q.length === 0) {
|
||||
// Cleared — restore.
|
||||
if (_libPreSearchEmails) {
|
||||
state._libEmails = _libPreSearchEmails;
|
||||
state._libTotal = _libPreSearchTotal;
|
||||
_libPreSearchEmails = null;
|
||||
_libPreSearchTotal = 0;
|
||||
}
|
||||
_renderGrid();
|
||||
return;
|
||||
}
|
||||
if (q.length < 2) {
|
||||
_renderGrid();
|
||||
return;
|
||||
}
|
||||
const source = _libPreSearchEmails || state._libEmails || [];
|
||||
const filtered = source.filter(em => _matchesQuery(em, q));
|
||||
state._libEmails = filtered;
|
||||
_renderGrid();
|
||||
}
|
||||
|
||||
async function _doSearch() {
|
||||
const seq = ++_libSearchSeq;
|
||||
const q = state._libSearch.trim();
|
||||
@@ -1607,17 +1657,19 @@ async function _doSearch() {
|
||||
_renderGrid();
|
||||
return;
|
||||
}
|
||||
const grid = document.getElementById('email-lib-grid');
|
||||
if (!grid) return;
|
||||
const sp = _renderEmailLoading(grid);
|
||||
const accountAtStart = state._libAccountId || '';
|
||||
const folderAtStart = state._libFolder || 'INBOX';
|
||||
// No grid-blanking spinner — the local filter already painted something
|
||||
// useful. Surface progress in the stats badge instead so the user knows
|
||||
// the server search is still grinding.
|
||||
const stats = document.getElementById('email-lib-stats');
|
||||
const originalStatsText = stats?.textContent || '';
|
||||
if (stats) stats.textContent = 'Searching…';
|
||||
|
||||
try {
|
||||
const accountQS = accountAtStart ? `&account_id=${encodeURIComponent(accountAtStart)}` : '';
|
||||
const res = await fetch(`${API_BASE}/api/email/search?folder=${encodeURIComponent(folderAtStart)}${accountQS}&q=${encodeURIComponent(q)}&limit=100`);
|
||||
const data = await res.json();
|
||||
sp.destroy();
|
||||
if (
|
||||
seq !== _libSearchSeq ||
|
||||
q !== state._libSearch.trim() ||
|
||||
@@ -1631,13 +1683,12 @@ async function _doSearch() {
|
||||
const results = data.emails || [];
|
||||
_libSearchHadResults = true;
|
||||
state._libEmails = results; // temporarily replace with search results
|
||||
state._libTotal = data.total || results.length;
|
||||
_renderGrid();
|
||||
|
||||
const stats = document.getElementById('email-lib-stats');
|
||||
if (stats) stats.textContent = `${data.total || results.length} match${(data.total || results.length) === 1 ? '' : 'es'}`;
|
||||
} catch (e) {
|
||||
sp.destroy();
|
||||
grid.innerHTML = '<div class="email-loading">Search failed</div>';
|
||||
if (stats) stats.textContent = originalStatsText || 'Search failed';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user