diff --git a/package-lock.json b/package-lock.json index 112806b..e5bca0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "aitherboard", - "version": "0.3.2", + "version": "0.3.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "aitherboard", - "version": "0.3.2", + "version": "0.3.3", "license": "MIT", "dependencies": { "@codemirror/autocomplete": "^6.20.0", diff --git a/package.json b/package.json index d4b1c54..25f7525 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aitherboard", - "version": "0.3.2", + "version": "0.3.3", "type": "module", "author": "silberengel@gitcitadel.com", "description": "A decentralized messageboard built on the Nostr protocol.", diff --git a/src/lib/components/EventMenu.svelte b/src/lib/components/EventMenu.svelte index 8e82ab0..79b0822 100644 --- a/src/lib/components/EventMenu.svelte +++ b/src/lib/components/EventMenu.svelte @@ -27,6 +27,7 @@ import { goto } from '$app/navigation'; import Icon from './ui/Icon.svelte'; import { getEventLink } from '../services/event-links.js'; + import { isGraspUrl } from '../services/content/git-repo-fetcher.js'; interface Props { event: NostrEvent; @@ -276,13 +277,111 @@ goto(getEventLink(event)); } - function cloneEvent() { + async function editGraspList() { + const currentPubkey = sessionManager.getCurrentPubkey(); + if (!currentPubkey) { + closeMenu(); + return; + } + + try { + // Fetch user's grasp list (kind 10317) + const graspListEvents = await nostrClient.fetchEvents( + [{ kinds: [10317], authors: [currentPubkey], limit: 1 }], + relayManager.getProfileReadRelays(), + { useCache: 'cache-first', cacheResults: true } + ); + + let graspListData; + if (graspListEvents.length > 0) { + // Edit existing grasp list + const existingEvent = graspListEvents[0]; + graspListData = { + kind: 10317, + content: existingEvent.content || '', + tags: existingEvent.tags || [], + isClone: false + }; + } else { + // Create new grasp list with defaults + const { config } = await import('../services/nostr/config.js'); + const graspRelaysForTags = config.graspRelays.filter(r => !r.includes('thecitadel')); + graspListData = { + kind: 10317, + content: '', + tags: graspRelaysForTags.map(server => ['g', server]), + isClone: false + }; + } + + sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(graspListData)); + closeMenu(); + goto('/write'); + } catch (error) { + console.error('Failed to load grasp list:', error); + closeMenu(); + } + } + + async function cloneEvent() { + let tags = event.tags || []; + + // For repo announcements, add GRASP clone if missing + if (event.kind === 30617 || event.kind === KIND.REPO_ANNOUNCEMENT) { + const hasGraspClone = tags.some(t => + t[0] === 'clone' && t[1] && isGraspUrl(t[1]) + ); + + if (!hasGraspClone) { + // Get user's grasp server preference (kind 10317) or use default + const currentPubkey = sessionManager.getCurrentPubkey(); + if (currentPubkey) { + const dTag = tags.find(t => t[0] === 'd')?.[1] || ''; + if (dTag) { + try { + const npub = nip19.npubEncode(currentPubkey); + + // Try to fetch user's grasp server preference (kind 10317) + let graspServer = ''; + try { + const graspListEvents = await nostrClient.fetchEvents( + [{ kinds: [10317], authors: [currentPubkey], limit: 1 }], + relayManager.getProfileReadRelays(), + { useCache: 'cache-first', cacheResults: true } + ); + if (graspListEvents.length > 0) { + const gTag = graspListEvents[0].tags.find(t => t[0] === 'g'); + if (gTag && gTag[1]) { + graspServer = gTag[1]; // Use first grasp server + } + } + } catch (error) { + console.warn('Failed to fetch grasp server preference:', error); + } + + // Fallback to default if no preference found + if (!graspServer) { + graspServer = 'wss://relay.ngit.dev'; // Default + } + + // Convert ws:// to https:// if needed + const httpsGraspServer = graspServer.replace(/^wss?:\/\//, 'https://').replace(/\/$/, ''); + const graspUrl = `${httpsGraspServer}/${npub}/${dTag}.git`; + tags = [...tags, ['clone', graspUrl]]; + } catch (error) { + console.warn('Failed to create GRASP clone URL:', error); + } + } + } + } + } + // Store event data in sessionStorage for the write page to pick up // Ensure content is preserved (important for kind 0 which has stringified JSON) const cloneData = { kind: event.kind, content: event.content || '', // Explicitly preserve content, even if empty string - tags: event.tags || [], + tags: tags, isClone: true }; sessionStorage.setItem('aitherboard_cloneEvent', JSON.stringify(cloneData)); @@ -300,7 +399,7 @@ let allRelays = relayManager.getAllAvailableRelays(); // Add thread publish relays (includes thread-specific relays) - allRelays = [...allRelays, ...relayManager.getThreadPublishRelays()]; + allRelays = [...allRelays, ...relayManager.getdocumentationPublishRelays()]; // Add file metadata publish relays (includes GIF relays) allRelays = [...allRelays, ...relayManager.getFileMetadataPublishRelays()]; @@ -509,6 +608,12 @@ Edit/Clone this event + {#if (event.kind === 30617 || event.kind === KIND.REPO_ANNOUNCEMENT) && isLoggedIn} + + {/if} {/if} @@ -602,14 +683,16 @@ role="button" tabindex="0" > - {#if getImageUrl(repo)} -
- {getRepoName(repo)} -
- {/if}
-

{getRepoName(repo)}

- Kind {repo.kind} + {#if getImageUrl(repo)} +
+ {getRepoName(repo)} +
+ {/if} +
+

{getRepoName(repo)}

+ Kind {repo.kind} +
Owner: @@ -762,12 +845,15 @@ } .repo-image-container { - width: 100%; - height: 200px; + flex-shrink: 0; + width: 3rem; + height: 3rem; + border-radius: 50%; overflow: hidden; - border-radius: 0.375rem; - margin-bottom: 1rem; + margin-bottom: 0; + margin-right: 0.75rem; background: var(--fog-highlight, #f3f4f6); + border: 2px solid var(--fog-border, #e5e7eb); display: flex; align-items: center; justify-content: center; @@ -775,13 +861,13 @@ :global(.dark) .repo-image-container { background: var(--fog-dark-highlight, #475569); + border-color: var(--fog-dark-border, #374151); } .repo-image { width: 100%; height: 100%; - object-fit: cover; - border-radius: 0.375rem; + object-fit: contain; } :global(.dark) .repo-item { @@ -801,12 +887,19 @@ } .repo-header { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.5rem; + } + + .repo-header-text { + flex: 1; + min-width: 0; display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; - margin-bottom: 0.5rem; - min-width: 0; } .repo-name { diff --git a/src/routes/repos/[naddr]/+page.svelte b/src/routes/repos/[naddr]/+page.svelte index 05970ff..3744c95 100644 --- a/src/routes/repos/[naddr]/+page.svelte +++ b/src/routes/repos/[naddr]/+page.svelte @@ -8,7 +8,7 @@ import { goto } from '$app/navigation'; import type { NostrEvent } from '../../../lib/types/nostr.js'; import { nip19 } from 'nostr-tools'; - import { fetchGitRepo, extractGitUrls, type GitRepoInfo, type GitFile } from '../../../lib/services/content/git-repo-fetcher.js'; + import { fetchGitRepo, extractGitUrls, isGraspUrl, convertSshToHttps, type GitRepoInfo, type GitFile } from '../../../lib/services/content/git-repo-fetcher.js'; import FileExplorer from '../../../lib/components/content/FileExplorer.svelte'; import { marked } from 'marked'; import Asciidoctor from 'asciidoctor'; @@ -108,9 +108,18 @@ try { const gitUrls = extractGitUrls(repoEvent); - if (gitUrls.length > 0) { + // Prioritize GRASP clones if multiple URLs exist + let prioritizedUrls = gitUrls; + if (gitUrls.length > 1) { + const graspUrls = gitUrls.filter(url => isGraspUrl(url)); + const nonGraspUrls = gitUrls.filter(url => !isGraspUrl(url)); + // Put GRASP URLs first + prioritizedUrls = [...graspUrls, ...nonGraspUrls]; + } + + if (prioritizedUrls.length > 0) { // Try each URL until one works - for (const url of gitUrls) { + for (const url of prioritizedUrls) { try { const repo = await fetchGitRepo(url); if (repo) { @@ -119,6 +128,7 @@ } } catch (error) { // Failed to fetch git repo - continue to next URL + console.warn(`Failed to fetch repo from ${url}:`, error); } } } @@ -236,7 +246,7 @@ const events = await nostrClient.fetchEvents( [{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }], relays, - { useCache: true, cacheResults: true } + { useCache: 'cache-first', cacheResults: true } ); @@ -322,9 +332,40 @@ issueEvents.push(...events); } - // Deduplicate and sort + // Deduplicate const uniqueIssues = Array.from(new Map(issueEvents.map(e => [e.id, e])).values()); - issues = uniqueIssues.sort((a, b) => b.created_at - a.created_at); + + // Filter to only include issues that actually match this repo + // Check if issue has 'a' tag matching this repo, or 'e' tag matching repo event ID, or 'r' tag matching repo URLs + const repoATag = dTag ? `${repoEvent.kind}:${repoEvent.pubkey}:${dTag}` : null; + const repoEventId = repoEvent.id; + const matchingIssues = uniqueIssues.filter(issue => { + // Check if issue references this repo via 'a' tag + if (repoATag) { + const aTags = issue.tags.filter(t => t[0] === 'a').map(t => t[1]); + if (aTags.includes(repoATag)) { + return true; + } + } + + // Check if issue references this repo via 'e' tag + const eTags = issue.tags.filter(t => t[0] === 'e').map(t => t[1]); + if (eTags.includes(repoEventId)) { + return true; + } + + // Check if issue references this repo via 'r' tag (git URLs) + const rTags = issue.tags.filter(t => t[0] === 'r').map(t => t[1]); + for (const gitUrl of gitUrls) { + if (rTags.includes(gitUrl)) { + return true; + } + } + + return false; + }); + + issues = matchingIssues.sort((a, b) => b.created_at - a.created_at); loadingIssues = false; // Issues are loaded, show them immediately // Load statuses, comments, and profiles in background (don't wait) @@ -709,7 +750,7 @@ const events = await nostrClient.fetchEvents( [{ kinds: [kind], authors: [pubkey], '#d': [dTag], limit: 1 }], allRelays, - { useCache: true, cacheResults: true } + { useCache: 'cache-first', cacheResults: true } ); if (events.length > 0) { @@ -795,7 +836,15 @@ if (!repoEvent || !Array.isArray(repoEvent.tags)) return []; return repoEvent.tags .filter(t => Array.isArray(t) && t[0] === 'clone' && t[1]) - .map(t => String(t[1])); + .map(t => { + const url = String(t[1]); + // Convert SSH URLs to HTTPS + if (url.startsWith('git@')) { + const httpsUrl = convertSshToHttps(url); + return httpsUrl || url; // Fallback to original if conversion fails + } + return url; + }); } function getRelays(): string[] { @@ -1259,11 +1308,6 @@
{@html renderReadme(gitRepo.readme.content, gitRepo.readme.format)}
- {:else if gitRepoFetchAttempted && !loadingGitRepo && extractGitUrls(repoEvent).some(url => /\/npub1[a-z0-9]+/i.test(url))} -
-

GRASP (Git Repository Access via Secure Protocol) repositories are not yet supported for fetching README files.

-

The repository description is shown in the header above.

-
{:else}

No README found.

@@ -1325,11 +1369,6 @@
{/if}
- {:else if gitRepoFetchAttempted && !loadingGitRepo && extractGitUrls(repoEvent).some(url => /\/npub1[a-z0-9]+/i.test(url))} -
-

GRASP (Git Repository Access via Secure Protocol) repositories are not yet supported for fetching repository data.

-

GRASP uses a different protocol than standard git hosting services and cannot be accessed via their APIs.

-
{:else}

Git repository data not available.

@@ -1550,9 +1589,9 @@ .repo-profile-image-container { flex-shrink: 0; - width: 120px; - height: 120px; - border-radius: 0.5rem; + width: 3rem; + height: 3rem; + border-radius: 50%; overflow: hidden; background: var(--fog-highlight, #f3f4f6); border: 2px solid var(--fog-border, #e5e7eb); @@ -1569,7 +1608,7 @@ .repo-profile-image { width: 100%; height: 100%; - object-fit: cover; + object-fit: contain; } .repo-title-section { diff --git a/static/changelog.yaml b/static/changelog.yaml index d2cf01d..8bd07e3 100644 --- a/static/changelog.yaml +++ b/static/changelog.yaml @@ -1,4 +1,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)' '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 373d191..993d645 100644 --- a/static/healthz.json +++ b/static/healthz.json @@ -1,8 +1,8 @@ { "status": "ok", "service": "aitherboard", - "version": "0.3.2", - "buildTime": "2026-02-14T17:51:24.287Z", + "version": "0.3.3", + "buildTime": "2026-02-15T06:43:22.492Z", "gitCommit": "unknown", - "timestamp": 1771091484287 + "timestamp": 1771137802493 } \ No newline at end of file