From 8ef73f7a6228233c02e99890657cd5d8ebd401ad Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 15 Feb 2026 13:48:07 +0100 Subject: [PATCH] support issues --- src/routes/repos/[naddr]/+page.svelte | 168 +++++++++++++++++++++----- src/routes/write/+page.svelte | 12 +- static/changelog.yaml | 1 + static/healthz.json | 4 +- 4 files changed, 147 insertions(+), 38 deletions(-) diff --git a/src/routes/repos/[naddr]/+page.svelte b/src/routes/repos/[naddr]/+page.svelte index 1a3e498..f6d83f5 100644 --- a/src/routes/repos/[naddr]/+page.svelte +++ b/src/routes/repos/[naddr]/+page.svelte @@ -418,7 +418,7 @@ } } - async function loadIssueStatuses() { + async function loadIssueStatuses(forceNetwork = false) { if (issues.length === 0) return; try { @@ -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 @@ 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 @@ } } + // 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 @@ 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 @@ 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(() => { - // Non-critical - status is already updated locally - }); + // 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 @@ } } + 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,31 +1499,40 @@ {:else if issues.length > 0}
- - - - {#if statusFilter} - Showing {filteredIssues.length} of {issues.length} issues - {:else} - {issues.length} {issues.length === 1 ? 'issue' : 'issues'} - {/if} - {#if loadingIssueData} - (loading details...) - {/if} - +
+ + + + {#if statusFilter} + Showing {filteredIssues.length} of {issues.length} issues + {:else} + {issues.length} {issues.length === 1 ? 'issue' : 'issues'} + {/if} + {#if loadingIssueData} + (loading details...) + {/if} + +
+ {#if sessionManager.getSession()} + {@const repoATag = getRepoATag()} + + + New Issue + + {/if}
{#if filteredIssues.length > 0} @@ -1577,12 +1615,26 @@ {:else}

No issues found with status "{statusFilter}".

+ {#if sessionManager.getSession()} + {@const repoATag = getRepoATag()} + + + New Issue + + {/if}
{/if}
{:else}

No issues found.

+ {#if sessionManager.getSession()} + {@const repoATag = getRepoATag()} + + + New Issue + + {/if}
{/if} @@ -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 @@ .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 @@ 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 @@ } .filter-count { - margin-left: auto; font-size: 0.875rem; color: var(--fog-text-light, #52667a); } @@ -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; diff --git a/src/routes/write/+page.svelte b/src/routes/write/+page.svelte index cc76f24..797e4cb 100644 --- a/src/routes/write/+page.svelte +++ b/src/routes/write/+page.svelte @@ -9,8 +9,9 @@ import type { NostrEvent } from '../../lib/types/nostr.js'; - // Read kind from URL synchronously so it's available on first render + // Read kind and a-tag from URL synchronously so it's available on first render const kindParam = $derived($page.url.searchParams.get('kind')); + const aTagParam = $derived($page.url.searchParams.get('a')); let initialEvent = $state(null); let isCloneMode = $state(false); @@ -65,18 +66,23 @@ } } - // Set initial kind from URL if available (only if no event from sessionStorage) + // Set initial kind and tags from URL if available (only if no event from sessionStorage) $effect(() => { if (kindParam && !initialEvent) { const kind = parseInt(kindParam, 10); if (!isNaN(kind)) { + const tags: string[][] = []; + // Add a-tag if provided (for repo references in issues) + if (aTagParam) { + tags.push(['a', aTagParam]); + } initialEvent = { id: '', pubkey: '', created_at: Math.floor(Date.now() / 1000), kind: kind, content: '', - tags: [], + tags: tags, sig: '' }; } diff --git a/static/changelog.yaml b/static/changelog.yaml index 8bd07e3..c1a5e31 100644 --- a/static/changelog.yaml +++ b/static/changelog.yaml @@ -2,6 +2,7 @@ versions: '0.3.3': - 'Added GRASP repository management' - 'Support manual creation and editing of User Grasp List (kind 10317) and repo announcements (kind 30617)' + - 'Support creation of issues in repositories' '0.3.2': - 'Expanded /repos to handle GitLab, Gitea, and OneDev repositories' - 'Added back and refresh buttons to all pages' diff --git a/static/healthz.json b/static/healthz.json index 993d645..8ebb0e7 100644 --- a/static/healthz.json +++ b/static/healthz.json @@ -2,7 +2,7 @@ "status": "ok", "service": "aitherboard", "version": "0.3.3", - "buildTime": "2026-02-15T06:43:22.492Z", + "buildTime": "2026-02-15T12:37:12.663Z", "gitCommit": "unknown", - "timestamp": 1771137802493 + "timestamp": 1771159032663 } \ No newline at end of file