|
|
|
|
@ -418,7 +418,7 @@
@@ -418,7 +418,7 @@
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadIssueStatuses() { |
|
|
|
|
async function loadIssueStatuses(forceNetwork = false) { |
|
|
|
|
if (issues.length === 0) return; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
@ -438,6 +438,7 @@
@@ -438,6 +438,7 @@
|
|
|
|
|
|
|
|
|
|
// Status events are different kinds: 1630 (Open), 1631 (Applied/Merged/Resolved), 1632 (Closed), 1633 (Draft) |
|
|
|
|
// They have "e" tags pointing to issues with marker "root" |
|
|
|
|
// Use 'relay-first' when forcing network fetch (e.g., after status change) to bypass cached empty results |
|
|
|
|
const statuses = await nostrClient.fetchEvents( |
|
|
|
|
[{ |
|
|
|
|
'#e': issueIds, |
|
|
|
|
@ -445,7 +446,7 @@
@@ -445,7 +446,7 @@
|
|
|
|
|
limit: 200 |
|
|
|
|
}], |
|
|
|
|
relays, |
|
|
|
|
{ useCache: 'cache-first', cacheResults: true } // Prioritize cache |
|
|
|
|
{ useCache: forceNetwork ? 'relay-first' : 'cache-first', cacheResults: true } |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// Process statuses for this batch |
|
|
|
|
@ -486,8 +487,18 @@
@@ -486,8 +487,18 @@
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Merge with existing state to preserve any local updates that haven't propagated yet |
|
|
|
|
// Keep the newer status for each issue |
|
|
|
|
const mergedStatuses = new Map(issueStatuses); |
|
|
|
|
for (const [issueId, newStatus] of statusMap.entries()) { |
|
|
|
|
const existingStatus = mergedStatuses.get(issueId); |
|
|
|
|
if (!existingStatus || newStatus.created_at > existingStatus.created_at) { |
|
|
|
|
mergedStatuses.set(issueId, newStatus); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Update state after each batch so newest issues show statuses first |
|
|
|
|
issueStatuses = new Map(statusMap); |
|
|
|
|
issueStatuses = mergedStatuses; |
|
|
|
|
} |
|
|
|
|
} catch (error) { |
|
|
|
|
// Failed to load issue statuses |
|
|
|
|
@ -552,6 +563,12 @@
@@ -552,6 +563,12 @@
|
|
|
|
|
issueStatuses.set(issueId, signedEvent); |
|
|
|
|
issueStatuses = new Map(issueStatuses); // Trigger reactivity |
|
|
|
|
|
|
|
|
|
// Refresh the view immediately using cache-first to show the newly cached event |
|
|
|
|
// This ensures the cached event is displayed right away |
|
|
|
|
loadIssueStatuses(false).catch(() => { |
|
|
|
|
// Non-critical - status is already updated locally |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
// Publish to relays in background (don't wait for it) |
|
|
|
|
const relays = relayManager.getProfileReadRelays(); |
|
|
|
|
signAndPublish(event, relays).then((result) => { |
|
|
|
|
@ -559,10 +576,13 @@
@@ -559,10 +576,13 @@
|
|
|
|
|
console.warn('Failed to publish status change to some relays:', result.failed); |
|
|
|
|
// Don't show alert - event is cached and will sync eventually |
|
|
|
|
} |
|
|
|
|
// Reload statuses to get any other updates from relays |
|
|
|
|
loadIssueStatuses().catch(() => { |
|
|
|
|
// Optionally reload from network to get any other updates from relays |
|
|
|
|
// Use a small delay to allow the event to propagate |
|
|
|
|
setTimeout(() => { |
|
|
|
|
loadIssueStatuses(true).catch(() => { |
|
|
|
|
// Non-critical - status is already updated locally |
|
|
|
|
}); |
|
|
|
|
}, 1000); |
|
|
|
|
}).catch((error) => { |
|
|
|
|
console.error('Error publishing status change:', error); |
|
|
|
|
// Don't show alert - event is cached and will sync eventually |
|
|
|
|
@ -854,6 +874,15 @@
@@ -854,6 +874,15 @@
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getRepoATag(): string | null { |
|
|
|
|
if (!repoEvent) return null; |
|
|
|
|
const dTag = repoEvent.tags.find(t => Array.isArray(t) && t[0] === 'd')?.[1] || ''; |
|
|
|
|
if (dTag) { |
|
|
|
|
return `${repoEvent.kind}:${repoEvent.pubkey}:${dTag}`; |
|
|
|
|
} |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function getRepoName(): string { |
|
|
|
|
try { |
|
|
|
|
if (!repoEvent) return 'Repository'; |
|
|
|
|
@ -1470,6 +1499,7 @@
@@ -1470,6 +1499,7 @@
|
|
|
|
|
</div> |
|
|
|
|
{:else if issues.length > 0} |
|
|
|
|
<div class="issues-filter"> |
|
|
|
|
<div class="issues-filter-left"> |
|
|
|
|
<label for="status-filter" class="filter-label">Filter by status:</label> |
|
|
|
|
<select |
|
|
|
|
id="status-filter" |
|
|
|
|
@ -1496,6 +1526,14 @@
@@ -1496,6 +1526,14 @@
|
|
|
|
|
{/if} |
|
|
|
|
</span> |
|
|
|
|
</div> |
|
|
|
|
{#if sessionManager.getSession()} |
|
|
|
|
{@const repoATag = getRepoATag()} |
|
|
|
|
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue"> |
|
|
|
|
<Icon name="edit" size={16} /> |
|
|
|
|
<span>New Issue</span> |
|
|
|
|
</a> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
<div class="issues-list"> |
|
|
|
|
{#if filteredIssues.length > 0} |
|
|
|
|
{#each paginatedIssues as issue} |
|
|
|
|
@ -1577,12 +1615,26 @@
@@ -1577,12 +1615,26 @@
|
|
|
|
|
{:else} |
|
|
|
|
<div class="empty-state"> |
|
|
|
|
<p class="text-fog-text dark:text-fog-dark-text">No issues found with status "{statusFilter}".</p> |
|
|
|
|
{#if sessionManager.getSession()} |
|
|
|
|
{@const repoATag = getRepoATag()} |
|
|
|
|
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue"> |
|
|
|
|
<Icon name="edit" size={16} /> |
|
|
|
|
<span>New Issue</span> |
|
|
|
|
</a> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{:else} |
|
|
|
|
<div class="empty-state"> |
|
|
|
|
<p class="text-fog-text dark:text-fog-dark-text">No issues found.</p> |
|
|
|
|
{#if sessionManager.getSession()} |
|
|
|
|
{@const repoATag = getRepoATag()} |
|
|
|
|
<a href="/write?kind={KIND.ISSUE}{repoATag ? `&a=${encodeURIComponent(repoATag)}` : ''}" class="see-more-events-btn-header" title="Create a new issue"> |
|
|
|
|
<Icon name="edit" size={16} /> |
|
|
|
|
<span>New Issue</span> |
|
|
|
|
</a> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
@ -1634,6 +1686,10 @@
@@ -1634,6 +1686,10 @@
|
|
|
|
|
.empty-state { |
|
|
|
|
padding: 2rem; |
|
|
|
|
text-align: center; |
|
|
|
|
display: flex; |
|
|
|
|
flex-direction: column; |
|
|
|
|
align-items: center; |
|
|
|
|
gap: 1rem; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.repo-banner-container { |
|
|
|
|
@ -1986,12 +2042,14 @@
@@ -1986,12 +2042,14 @@
|
|
|
|
|
.issues-filter { |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
justify-content: space-between; |
|
|
|
|
gap: 1rem; |
|
|
|
|
margin-bottom: 1.5rem; |
|
|
|
|
padding: 1rem; |
|
|
|
|
background: var(--fog-highlight, #f3f4f6); |
|
|
|
|
border-radius: 0.5rem; |
|
|
|
|
border: 1px solid var(--fog-border, #e5e7eb); |
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .issues-filter { |
|
|
|
|
@ -1999,6 +2057,14 @@
@@ -1999,6 +2057,14 @@
|
|
|
|
|
border-color: var(--fog-dark-border, #374151); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.issues-filter-left { |
|
|
|
|
display: flex; |
|
|
|
|
align-items: center; |
|
|
|
|
gap: 1rem; |
|
|
|
|
flex-wrap: wrap; |
|
|
|
|
flex: 1; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.filter-label { |
|
|
|
|
font-weight: 500; |
|
|
|
|
color: var(--fog-text, #1f2937); |
|
|
|
|
@ -2046,7 +2112,6 @@
@@ -2046,7 +2112,6 @@
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.filter-count { |
|
|
|
|
margin-left: auto; |
|
|
|
|
font-size: 0.875rem; |
|
|
|
|
color: var(--fog-text-light, #52667a); |
|
|
|
|
} |
|
|
|
|
@ -2055,6 +2120,43 @@
@@ -2055,6 +2120,43 @@
|
|
|
|
|
color: var(--fog-dark-text-light, #a8b8d0); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.see-more-events-btn-header { |
|
|
|
|
padding: 0.5rem 1rem; |
|
|
|
|
border: 1px solid var(--fog-border, #cbd5e1); |
|
|
|
|
border-radius: 4px; |
|
|
|
|
background: var(--fog-post, #ffffff); |
|
|
|
|
color: var(--fog-text, #1e293b); |
|
|
|
|
cursor: pointer; |
|
|
|
|
transition: all 0.2s; |
|
|
|
|
white-space: nowrap; |
|
|
|
|
text-decoration: none; |
|
|
|
|
display: inline-flex; |
|
|
|
|
align-items: center; |
|
|
|
|
gap: 0.5rem; |
|
|
|
|
flex-shrink: 0; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.see-more-events-btn-header:hover:not(:disabled) { |
|
|
|
|
background: var(--fog-highlight, #f1f5f9); |
|
|
|
|
border-color: var(--fog-accent, #94a3b8); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.see-more-events-btn-header:disabled { |
|
|
|
|
opacity: 0.5; |
|
|
|
|
cursor: not-allowed; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .see-more-events-btn-header { |
|
|
|
|
background: var(--fog-dark-post, #334155); |
|
|
|
|
border-color: var(--fog-dark-border, #475569); |
|
|
|
|
color: var(--fog-dark-text, #f1f5f9); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
:global(.dark) .see-more-events-btn-header:hover:not(:disabled) { |
|
|
|
|
background: var(--fog-dark-highlight, #475569); |
|
|
|
|
border-color: var(--fog-dark-accent, #64748b); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
.issues-list { |
|
|
|
|
display: flex; |
|
|
|
|
flex-direction: column; |
|
|
|
|
|