mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 10:15:27 -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;
|
let searchTimer = null;
|
||||||
document.getElementById('email-lib-search').addEventListener('input', (e) => {
|
document.getElementById('email-lib-search').addEventListener('input', (e) => {
|
||||||
state._libSearch = e.target.value;
|
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);
|
if (searchTimer) clearTimeout(searchTimer);
|
||||||
searchTimer = setTimeout(_doSearch, 350);
|
searchTimer = setTimeout(_doSearch, 350);
|
||||||
});
|
});
|
||||||
@@ -1592,6 +1596,52 @@ function _crossFolderCandidates() {
|
|||||||
return Array.from(new Set(candidates.filter(Boolean)));
|
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() {
|
async function _doSearch() {
|
||||||
const seq = ++_libSearchSeq;
|
const seq = ++_libSearchSeq;
|
||||||
const q = state._libSearch.trim();
|
const q = state._libSearch.trim();
|
||||||
@@ -1607,17 +1657,19 @@ async function _doSearch() {
|
|||||||
_renderGrid();
|
_renderGrid();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const grid = document.getElementById('email-lib-grid');
|
|
||||||
if (!grid) return;
|
|
||||||
const sp = _renderEmailLoading(grid);
|
|
||||||
const accountAtStart = state._libAccountId || '';
|
const accountAtStart = state._libAccountId || '';
|
||||||
const folderAtStart = state._libFolder || 'INBOX';
|
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 {
|
try {
|
||||||
const accountQS = accountAtStart ? `&account_id=${encodeURIComponent(accountAtStart)}` : '';
|
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 res = await fetch(`${API_BASE}/api/email/search?folder=${encodeURIComponent(folderAtStart)}${accountQS}&q=${encodeURIComponent(q)}&limit=100`);
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
sp.destroy();
|
|
||||||
if (
|
if (
|
||||||
seq !== _libSearchSeq ||
|
seq !== _libSearchSeq ||
|
||||||
q !== state._libSearch.trim() ||
|
q !== state._libSearch.trim() ||
|
||||||
@@ -1631,13 +1683,12 @@ async function _doSearch() {
|
|||||||
const results = data.emails || [];
|
const results = data.emails || [];
|
||||||
_libSearchHadResults = true;
|
_libSearchHadResults = true;
|
||||||
state._libEmails = results; // temporarily replace with search results
|
state._libEmails = results; // temporarily replace with search results
|
||||||
|
state._libTotal = data.total || results.length;
|
||||||
_renderGrid();
|
_renderGrid();
|
||||||
|
|
||||||
const stats = document.getElementById('email-lib-stats');
|
|
||||||
if (stats) stats.textContent = `${data.total || results.length} match${(data.total || results.length) === 1 ? '' : 'es'}`;
|
if (stats) stats.textContent = `${data.total || results.length} match${(data.total || results.length) === 1 ? '' : 'es'}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
sp.destroy();
|
if (stats) stats.textContent = originalStatsText || 'Search failed';
|
||||||
grid.innerHTML = '<div class="email-loading">Search failed</div>';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user