feat: paginate balanced mobile search results (Closes #783)
CI / lint (pull_request) Successful in 1m34s
CI / build-release (pull_request) Successful in 5s
CI / release-candidate (pull_request) Has been skipped

This commit is contained in:
timmy committed 2026-08-14 00:04:06 +00:00
1 parent fa3e5e3bf0
commit 7b736bc4c6
8 files changed
+149 -45

No files matched your search

+41 -17
View File
@@ -22,6 +22,38 @@
let timer = null;
let generation = 0;
let activeController = null;
let state = { status: 'idle', query: '', items: [], more: false, next: 1 };
function publish(next) {
state = next;
onState(next);
}
async function requestPage(query, page, current, append) {
const requestController = new AbortController();
activeController = requestController;
try {
const result = await search(query, requestController.signal, page);
const partial = result.partial === true;
const incoming = result.items || result;
const combined = append ? state.items.concat(incoming) : incoming;
const items = [...new Map((combined || []).map(item =>
[`${item.kind}:${item.repository}:${item.number}`, item]
)).values()];
if (current === generation) publish({
status: 'ready', query, items, partial,
more: !!result.has_more,
next: result.next_page,
});
} catch (error) {
if (error && error.name === 'AbortError') return;
if (current === generation) publish(append
? { ...state, status: 'ready' }
: { status: 'error', query, items: [], error });
} finally {
if (activeController === requestController) activeController = null;
}
}
return {
setQuery(value) {
@@ -32,25 +64,17 @@
if (activeController !== null) activeController.abort();
activeController = null;
if (query.length < 2) {
onState({ status: 'idle', query, items: [] });
publish({ status: 'idle', query, items: [], more: false, next: 1 });
return;
}
onState({ status: 'loading', query, items: [] });
timer = setTimeout(async () => {
const requestController = new AbortController();
activeController = requestController;
try {
const result = await search(query, requestController.signal);
const items = Array.isArray(result) ? result : result.items;
const partial = !Array.isArray(result) && result.partial === true;
if (current === generation) onState({ status: 'ready', query, items, partial });
} catch (error) {
if (error && error.name === 'AbortError') return;
if (current === generation) onState({ status: 'error', query, items: [], error });
} finally {
if (activeController === requestController) activeController = null;
}
}, delay);
publish({ status: 'loading', query, items: [], more: false, next: 1 });
timer = setTimeout(() => requestPage(query, 1, current, false), delay);
},
loadMore() {
if (state.status !== 'ready' || !state.more || activeController) return;
const current = generation;
const page = state.next;
requestPage(state.query, page, current, true);
},
};
};
+2 -1
View File
@@ -119,6 +119,7 @@ textarea { resize: vertical; min-height: 120px; }
.cmd-item:hover, .cmd-item.selected { background: #10233a; outline:1px solid #31577f; }
.cmd-meta { color:#93a4b8; font-size:12px; text-align:right; }
.cmd-status { padding:10px; color:#93a4b8; font-size:13px; }
.cmd-group { padding:8px 10px 3px; color:#60a5fa; font-size:11px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; }
#whiteboard-modal, #markdown-modal { position: fixed; inset: 0; background: rgba(5,12,21,.55); display: none; align-items: center; justify-content: center; z-index: 40; backdrop-filter: blur(6px); }
#whiteboard-modal.open, #markdown-modal.open { display: flex; }
@@ -681,7 +682,7 @@ textarea { resize: vertical; min-height: 120px; }
#cmd-palette { left:0; top:var(--search-viewport-top,0px); transform:none; width:100%; height:var(--search-viewport-height,100dvh); border:0; border-radius:0; padding:12px; padding-bottom:calc(12px + env(safe-area-inset-bottom)); z-index:46; }
#cmd-palette.open { display:flex; flex-direction:column; }
.cmd-palette-header { display:flex; flex:0 0 auto; min-height:44px; }
#close-command-palette { min-height:44px; }
#close-command-palette, #cmd-load-more { min-height:44px; }
#cmd-input { flex:0 0 auto; min-height:44px; }
#cmd-results { flex:1; min-height:0; overflow-y:auto; max-height:none; overscroll-behavior:contain; padding-bottom:env(safe-area-inset-bottom); }
.create-issue-panel { width:100%; border-left:0; padding:14px; }
+9 -15
View File
@@ -4236,7 +4236,6 @@
function openModal(id) { qs('#' + id).classList.add('open'); }
function closeModal(id) { qs('#' + id).classList.remove('open'); }
/* Creative ambient background */
(function bg(){
const c=qs('#bg'),ctx=c.getContext('2d');
let w,h,time=0;
@@ -4258,7 +4257,6 @@
draw();
})();
/* Whiteboard */
function initWhiteboard() {
const canvas = qs('#wb'), ctx = canvas.getContext('2d');
let drawing = false;
@@ -4273,34 +4271,28 @@
qs('#wb-save').addEventListener('click', () => { const a=document.createElement('a'); a.href=canvas.toDataURL(); a.download='whiteboard.png'; a.click(); });
}
/* Markdown */
qs('#md-input').addEventListener('input', renderMD);
function renderMD() {
const raw = qs('#md-input').value || '';
qs('#md-preview').innerHTML = '<pre>' + escapeHtml(raw) + '</pre><div style="margin-top:8px;">' + renderMarkdown(raw) + '</div>';
}
/* Commands */
const commands = [
{ name: 'Open whiteboard', run: () => { openModal('whiteboard-modal'); initWhiteboard(); } },
{ name: 'Open markdown widget', run: () => { qs('#md-input').focus(); } },
{ name: 'Refresh now', run: load },
{ name: 'Scroll issues', run: () => qs('#work').scrollIntoView({ behavior:'smooth', block:'start' }) },
{ name: 'Whiteboard', run: () => { openModal('whiteboard-modal'); initWhiteboard(); } },
{ name: 'Markdown', run: () => { qs('#md-input').focus(); } },
{ name: 'Refresh', run: load },
];
let commandSearchState = { status:'idle', query:'', items:[] };
let commandItems = [];
let commandSelection = -1;
async function searchGlobalWork(query, signal) {
const response = await fetch('api/v1/search?q=' + encodeURIComponent(query) + '&limit=10', {
async function searchGlobalWork(query, signal, page = 1) {
const response = await fetch('api/v1/search?q=' + encodeURIComponent(query) + '&limit=10&page=' + page, {
headers: { Accept:'application/json' },
signal,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) throw new Error(payload.error || 'Search is temporarily unavailable.');
return {
items: Array.isArray(payload.items) ? payload.items : [],
partial: payload.partial === true,
};
return payload;
}
const commandSearch = filterCommands.createGlobalSearchController({
search: searchGlobalWork,
@@ -4458,6 +4450,7 @@
}
function renderCommands(filter) {
const el = qs('#cmd-results');
const loadMore = qs('#cmd-load-more');
const local = filterCommands(commands, filter).map(command => ({ command }));
const remote = commandSearchState.query === String(filter || '').trim()
? commandSearchState.items.map(result => ({ result })) : [];
@@ -4476,6 +4469,7 @@
else if (commandSearchState.partial) html += '<div class="cmd-status">Some results are temporarily unavailable.</div>';
else if (String(filter || '').trim().length >= 2 && !remote.length) html += '<div class="cmd-status">No matching issues or pull requests.</div>';
el.innerHTML = html;
loadMore.hidden = !commandSearchState.more;
el.querySelectorAll('.cmd-item').forEach((item) => {
item.addEventListener('click', () => runCommandItem(commandItems[Number(item.dataset.idx)]));
});
@@ -4550,6 +4544,7 @@
taskOverlayHistory.start();
qs('#open-palette').addEventListener('click', openCommandPalette);
qs('#close-command-palette').addEventListener('click', () => taskOverlayHistory.close());
qs('#cmd-load-more').addEventListener('click', () => commandSearch.loadMore());
qs('#cmd-input').addEventListener('input', (e) => {
commandSelection = -1;
taskOverlayHistory.update({ query:e.target.value });
@@ -6541,7 +6536,6 @@
if (!document.hidden) deviceSetup?.render();
});
/* Widgets */
function widgetTick() { const el=qs('#widget-clock'); if(el) el.textContent = fmt(new Date()); }
setInterval(widgetTick, 1000);
})();
+1
View File
@@ -430,6 +430,7 @@
</div>
<input id="cmd-input" type="text" role="combobox" aria-autocomplete="list" aria-controls="cmd-results" aria-expanded="false" placeholder="Search commands, issues, and pull requests..." />
<div id="cmd-results" class="stack" role="listbox" aria-label="Commands and work search results"></div>
<button id="cmd-load-more" hidden>More results</button>
</div>
<div class="search-preview" id="search-preview" role="dialog" aria-modal="true" aria-labelledby="search-preview-title">