feat: show conversations in mobile search preview (Closes #803)
This commit is contained in:
1 parent
2406d24cb8
commit
51fe9e5af2
7 files changed
+281
-16
No files matched your search
@@ -546,6 +546,10 @@ textarea { resize: vertical; min-height: 120px; }
|
||||
.search-preview-header { display:flex; align-items:flex-start; justify-content:space-between; gap:10px; }
|
||||
.search-preview-header button, .search-preview-actions button, .search-preview-actions a { min-height:44px; }
|
||||
.search-preview-body { margin:0; white-space:pre-wrap; overflow-wrap:anywhere; }
|
||||
.search-preview-conversation { display:grid; gap:8px; padding-top:8px; border-top:1px solid #2a496e; }
|
||||
.search-preview-conversation h2 { margin:0; font-size:1rem; }
|
||||
.search-preview-comment { min-width:0; padding:10px 0; border-bottom:1px solid #1b2d45; overflow-wrap:anywhere; }
|
||||
.search-preview-conversation button { min-height:44px; width:100%; }
|
||||
.search-preview-navigation { display:grid; grid-template-columns:minmax(0,1fr) auto minmax(0,1fr); align-items:center; gap:8px; }
|
||||
.search-preview-actions { position:sticky; bottom:0; display:grid; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; }
|
||||
.search-preview-actions button, .search-preview-actions a { min-height:44px; box-sizing:border-box; display:flex; align-items:center; justify-content:center; }
|
||||
|
||||
+10
-14
@@ -4322,11 +4322,8 @@
|
||||
commandSearch.setScope(scope);
|
||||
}
|
||||
let searchPreviewDetail = null;
|
||||
function searchPreviewPath(item) {
|
||||
const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
|
||||
return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
|
||||
'/preview?kind=' + encodeURIComponent(item.kind);
|
||||
}
|
||||
const renderSearchConversation = conversation =>
|
||||
renderSearchPreviewConversation(conversation, document, escapeHtml, fmt, renderMarkdown);
|
||||
function renderSearchPreview(state) {
|
||||
const sheet = qs('#search-preview');
|
||||
const status = qs('#search-preview-status');
|
||||
@@ -4358,6 +4355,7 @@
|
||||
qs('#search-preview-title').textContent = state.item.title || 'Work preview';
|
||||
qs('#search-preview-meta').textContent = '';
|
||||
qs('#search-preview-body').textContent = '';
|
||||
renderSearchConversation(null);
|
||||
qs('#open-search-result-gitea').href = safeSearchUrl(state.item.url) || '#';
|
||||
status.textContent = 'Loading preview…';
|
||||
return;
|
||||
@@ -4378,6 +4376,7 @@
|
||||
(detail.labels?.length ? ' · ' + detail.labels.join(', ') : '') +
|
||||
(detail.assignees?.length ? ' · assigned to ' + detail.assignees.join(', ') : '');
|
||||
qs('#search-preview-body').innerHTML = renderMarkdown(detail.body || 'No description provided.');
|
||||
renderSearchConversation(state.conversation);
|
||||
qs('#open-search-result-gitea').href = safeSearchUrl(detail.url) || '#';
|
||||
claimButton.hidden = !(detail.claimable || detail.assigned_to_me);
|
||||
claimButton.textContent = detail.assigned_to_me ? 'Open in My Work' : 'Assign to me';
|
||||
@@ -4409,6 +4408,9 @@
|
||||
}
|
||||
const searchPreview = createSearchPreview({
|
||||
fetchJson: item => fetchReviewJson(searchPreviewPath(item), { headers:{ Accept:'application/json' } }),
|
||||
fetchConversation:(item,page)=>fetchReviewJson(searchPreviewConversationPath(item,page),{
|
||||
headers:{Accept:'application/json'},
|
||||
}),
|
||||
mutate: (detail, action) => fetchReviewJson(
|
||||
'api/v1/repos/' + detail.repository.split('/').map(encodeURIComponent).join('/') +
|
||||
'/issues/' + encodeURIComponent(detail.number) + '/' + action,
|
||||
@@ -4430,15 +4432,9 @@
|
||||
},
|
||||
formatTime: fmt,
|
||||
});
|
||||
function canonicalSearchPreviewUrl() {
|
||||
const { query, preview, scope = currentSearchScope() } = taskOverlayHistory.currentState();
|
||||
const params = new URLSearchParams({
|
||||
search:query || '', preview:preview.kind + ':' + preview.repository + ':' + preview.number,
|
||||
search_kind:scope.kind, search_state:scope.state,
|
||||
});
|
||||
if (scope.repository) params.set('search_repository', scope.repository);
|
||||
return new URL('?' + params, window.location.origin + window.location.pathname).href;
|
||||
}
|
||||
const canonicalSearchPreviewUrl = () => searchPreviewUrl(
|
||||
taskOverlayHistory.currentState(), currentSearchScope(), window.location
|
||||
);
|
||||
function createSearchStart(claim) {
|
||||
return createAssignAndStart({
|
||||
available: createAndStart.available,
|
||||
|
||||
@@ -465,6 +465,13 @@
|
||||
<div id="search-preview-status" class="small" aria-live="polite">Choose a search result.</div>
|
||||
<div id="search-preview-meta" class="row"></div>
|
||||
<div id="search-preview-body" class="search-preview-body markdown-content"></div>
|
||||
<section id="search-preview-conversation" class="search-preview-conversation" aria-labelledby="search-preview-conversation-title">
|
||||
<h2 id="search-preview-conversation-title">Current conversation</h2>
|
||||
<div id="search-preview-comments"></div>
|
||||
<div id="search-preview-conversation-status" class="small" aria-live="polite"></div>
|
||||
<button id="retry-search-preview-conversation" type="button" hidden>Retry conversation</button>
|
||||
<button id="load-older-search-preview-comments" type="button" hidden>Load older messages</button>
|
||||
</section>
|
||||
<div class="search-preview-actions">
|
||||
<nav class="search-preview-navigation" aria-label="Search result navigation" hidden>
|
||||
<button id="previous-search-result" type="button">Previous</button>
|
||||
|
||||
@@ -9,9 +9,57 @@
|
||||
return ['http:', 'https:'].includes(url.protocol) ? url.href : '';
|
||||
} catch (_) { return ''; }
|
||||
};
|
||||
root.searchPreviewUrl = (state, defaultScope, location) => {
|
||||
const { query, preview, scope = defaultScope } = state;
|
||||
const params = new URLSearchParams({
|
||||
search:query || '', preview:preview.kind + ':' + preview.repository + ':' + preview.number,
|
||||
search_kind:scope.kind, search_state:scope.state,
|
||||
});
|
||||
if (scope.repository) params.set('search_repository', scope.repository);
|
||||
return new URL('?' + params, location.origin + location.pathname).href;
|
||||
};
|
||||
root.searchPreviewPath = item => {
|
||||
const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
|
||||
return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
|
||||
'/preview?kind=' + encodeURIComponent(item.kind);
|
||||
};
|
||||
root.searchPreviewConversationPath = (item, page) => {
|
||||
const repository = String(item.repository || '').split('/').map(encodeURIComponent).join('/');
|
||||
const query = new URLSearchParams({ kind:item.kind, limit:'20' });
|
||||
if (page) query.set('page', String(page));
|
||||
return 'api/v1/repos/' + repository + '/issues/' + encodeURIComponent(item.number) +
|
||||
'/preview/conversation?' + query.toString();
|
||||
};
|
||||
root.renderSearchPreviewConversation = (conversation, document, escapeHtml, formatTime, renderMarkdown) => {
|
||||
const comments = document.querySelector('#search-preview-comments');
|
||||
const status = document.querySelector('#search-preview-conversation-status');
|
||||
const retry = document.querySelector('#retry-search-preview-conversation');
|
||||
const older = document.querySelector('#load-older-search-preview-comments');
|
||||
if (!conversation) {
|
||||
comments.textContent = status.textContent = '';
|
||||
retry.hidden = older.hidden = true;
|
||||
return;
|
||||
}
|
||||
comments.innerHTML = (conversation.comments || []).map(comment =>
|
||||
'<article class="search-preview-comment" data-comment-id="' + Number(comment.id || 0) + '">' +
|
||||
'<div class="small">' + escapeHtml(comment.author || 'Unknown author') +
|
||||
(comment.created_at ? ' · ' + escapeHtml(formatTime(comment.created_at)) : '') + '</div>' +
|
||||
'<div class="markdown-content">' + renderMarkdown(comment.body || '') + '</div></article>'
|
||||
).join('');
|
||||
retry.hidden = conversation.status !== 'error';
|
||||
older.hidden = conversation.status !== 'ready' || !conversation.olderPage;
|
||||
older.disabled = conversation.status === 'loading';
|
||||
if (conversation.status === 'loading') status.textContent = conversation.comments?.length
|
||||
? 'Loading older messages…' : 'Loading current conversation…';
|
||||
else if (conversation.status === 'error') status.textContent =
|
||||
'Conversation unavailable. Preview and planning actions still work.';
|
||||
else if (!conversation.comments?.length) status.textContent = 'No conversation yet.';
|
||||
else status.textContent = conversation.comments.length +
|
||||
(conversation.comments.length === 1 ? ' message.' : ' messages.');
|
||||
};
|
||||
}
|
||||
})(typeof globalThis !== 'undefined' ? globalThis : this, function () {
|
||||
return function createSearchPreview({ fetchJson, mutate, share, session, getSession, loadMore, onNavigate, navigationRoot, onState }) {
|
||||
return function createSearchPreview({ fetchJson, fetchConversation, mutate, share, session, getSession, loadMore, onNavigate, navigationRoot, onState }) {
|
||||
if (Array.isArray(session)) {
|
||||
getSession = session[0];
|
||||
loadMore = () => session[1].loadMore();
|
||||
@@ -23,6 +71,7 @@
|
||||
let mutationRequest = null;
|
||||
let shareRequest = null;
|
||||
let moveRequest = null;
|
||||
let conversation = null;
|
||||
|
||||
function sameItem(left, right) {
|
||||
return left && right && left.kind === right.kind && left.repository === right.repository &&
|
||||
@@ -72,16 +121,53 @@
|
||||
return mutationRequest;
|
||||
}
|
||||
|
||||
function loadConversation(detail, requestGeneration, page) {
|
||||
if (typeof fetchConversation !== 'function') return Promise.resolve(null);
|
||||
const previousComments = page && Array.isArray(conversation?.comments)
|
||||
? conversation.comments : [];
|
||||
conversation = { status:'loading', comments:previousComments, olderPage:page ?? null };
|
||||
publish({ status:'ready', item:current, detail, conversation });
|
||||
return fetchConversation(detail, page).then(result => {
|
||||
if (requestGeneration !== generation) return result;
|
||||
const incoming = Array.isArray(result?.comments) ? result.comments : [];
|
||||
const comments = page
|
||||
? [...incoming, ...previousComments].filter((comment, index, all) =>
|
||||
all.findIndex(candidate => candidate?.id === comment?.id) === index)
|
||||
: incoming;
|
||||
conversation = {
|
||||
status:'ready',
|
||||
comments,
|
||||
olderPage:result?.older_page ?? null,
|
||||
};
|
||||
publish({ status:'ready', item:current, detail:current, conversation });
|
||||
return result;
|
||||
}).catch(error => {
|
||||
if (requestGeneration === generation) {
|
||||
conversation = {
|
||||
status:'error', comments:previousComments,
|
||||
olderPage:page ?? conversation?.olderPage ?? null, error,
|
||||
};
|
||||
publish({ status:'ready', item:current, detail:current, conversation });
|
||||
}
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
const api = {
|
||||
open(item) {
|
||||
generation += 1;
|
||||
const requestGeneration = generation;
|
||||
current = { ...item };
|
||||
conversation = null;
|
||||
publish({ status: 'loading', item: current });
|
||||
return fetchJson(current).then(detail => {
|
||||
if (requestGeneration === generation) {
|
||||
current = { ...current, ...detail };
|
||||
publish({ status: 'ready', item: current, detail });
|
||||
if (typeof fetchConversation === 'function') {
|
||||
loadConversation(current, requestGeneration);
|
||||
} else {
|
||||
publish({ status: 'ready', item: current, detail });
|
||||
}
|
||||
}
|
||||
return detail;
|
||||
}).catch(error => {
|
||||
@@ -128,6 +214,14 @@
|
||||
})().finally(() => { moveRequest = null; });
|
||||
return moveRequest;
|
||||
},
|
||||
retryConversation() {
|
||||
if (!current) return Promise.resolve(null);
|
||||
return loadConversation(current, generation);
|
||||
},
|
||||
loadOlderConversation() {
|
||||
if (!current || !conversation?.olderPage) return Promise.resolve(null);
|
||||
return loadConversation(current, generation, conversation.olderPage);
|
||||
},
|
||||
share(url) {
|
||||
if (shareRequest) return shareRequest;
|
||||
if (!current || typeof share !== 'function') return Promise.reject(new Error('Sharing is unavailable.'));
|
||||
@@ -152,6 +246,8 @@
|
||||
if (navigationRoot) {
|
||||
navigationRoot.querySelector('#previous-search-result').addEventListener('click', () => api.previous().catch(() => {}));
|
||||
navigationRoot.querySelector('#next-search-result').addEventListener('click', () => api.next().catch(() => {}));
|
||||
navigationRoot.querySelector('#retry-search-preview-conversation').addEventListener('click', () => api.retryConversation());
|
||||
navigationRoot.querySelector('#load-older-search-preview-comments').addEventListener('click', () => api.loadOlderConversation());
|
||||
}
|
||||
return api;
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user