Browse Source

grasp clone urls

master
Silberengel 2 months ago
parent
commit
0091306569
  1. 8
      src/lib/components/layout/PageHeader.svelte
  2. 161
      src/lib/components/write/CreateEventForm.svelte
  3. 34
      src/lib/services/content/git-repo-fetcher.ts
  4. 82
      src/routes/repos/+page.svelte
  5. 50
      src/routes/repos/[naddr]/+page.svelte
  6. 2
      static/changelog.yaml

8
src/lib/components/layout/PageHeader.svelte

@ -43,13 +43,14 @@
</div> </div>
<button <button
class="page-header-button refresh-button" class="page-header-button refresh-button"
class:loading={refreshLoading}
onclick={handleRefresh} onclick={handleRefresh}
disabled={refreshLoading} disabled={refreshLoading}
aria-label="Refresh" aria-label="Refresh"
title="Refresh" title="Refresh"
> >
<Icon name="refresh" size={20} /> <span class:loading={refreshLoading}>
<Icon name="refresh" size={20} />
</span>
</button> </button>
</div> </div>
@ -123,7 +124,8 @@
cursor: not-allowed; cursor: not-allowed;
} }
.page-header-button.loading { .page-header-button span.loading {
display: inline-block;
animation: spin 1s linear infinite; animation: spin 1s linear infinite;
} }

161
src/lib/components/write/CreateEventForm.svelte

@ -70,7 +70,6 @@
let tags = $state<string[][]>(getInitialTags()); let tags = $state<string[][]>(getInitialTags());
let publishing = $state(false); let publishing = $state(false);
let showAdvancedEditor = $state(false); let showAdvancedEditor = $state(false);
let hasGraspList = $state<boolean | null>(null); // null = not checked yet
let richTextEditorRef: { clearUploadedFiles: () => void; getUploadedFiles: () => Array<{ url: string; imetaTag: string[] }> } | null = $state(null); let richTextEditorRef: { clearUploadedFiles: () => void; getUploadedFiles: () => Array<{ url: string; imetaTag: string[] }> } | null = $state(null);
let uploadedFiles: Array<{ url: string; imetaTag: string[] }> = $state([]); let uploadedFiles: Array<{ url: string; imetaTag: string[] }> = $state([]);
@ -107,32 +106,6 @@
} }
}); });
// Check if user has grasp list when creating repo announcement
$effect(() => {
if (effectiveKind === 30617 || effectiveKind === KIND.REPO_ANNOUNCEMENT) {
const currentPubkey = sessionManager.getCurrentPubkey();
if (currentPubkey) {
(async () => {
try {
const graspListEvents = await nostrClient.fetchEvents(
[{ kinds: [10317], authors: [currentPubkey], limit: 1 }],
relayManager.getProfileReadRelays(),
{ useCache: 'cache-first', cacheResults: true }
);
hasGraspList = graspListEvents.length > 0;
} catch (error) {
console.warn('Failed to check for grasp list:', error);
hasGraspList = false; // Assume missing on error
}
})();
} else {
hasGraspList = false;
}
} else {
hasGraspList = null; // Reset when not creating repo announcement
}
});
// Restore draft from IndexedDB if no initial event // Restore draft from IndexedDB if no initial event
$effect(() => { $effect(() => {
if (typeof window === 'undefined' || initialEvent) return; if (typeof window === 'undefined' || initialEvent) return;
@ -211,7 +184,6 @@
const helpText = $derived(kindMetadata.helpText); const helpText = $derived(kindMetadata.helpText);
const isKind30040 = $derived(selectedKind === 30040); const isKind30040 = $derived(selectedKind === 30040);
const isKind10895 = $derived(selectedKind === 10895); const isKind10895 = $derived(selectedKind === 10895);
const allPublishRelays = $derived([...new Set([...config.documentationPublishRelays, ...config.graspRelays])]);
// Clear content for metadata-only kinds (but preserve content when cloning/editing) // Clear content for metadata-only kinds (but preserve content when cloning/editing)
$effect(() => { $effect(() => {
@ -413,9 +385,9 @@
true true
); );
// For repo announcements, also add documentation and GRASP relays // For repo announcements, also add documentation relays
if (effectiveKind === 30617 || effectiveKind === KIND.REPO_ANNOUNCEMENT) { if (effectiveKind === 30617 || effectiveKind === KIND.REPO_ANNOUNCEMENT) {
relays = [...new Set([...relays, ...config.documentationPublishRelays, ...config.graspRelays])]; relays = [...new Set([...relays, ...config.documentationPublishRelays])];
} }
const results = await signAndPublish(eventTemplate, relays); const results = await signAndPublish(eventTemplate, relays);
@ -423,46 +395,6 @@
publicationModalOpen = true; publicationModalOpen = true;
if (results.success.length > 0) { if (results.success.length > 0) {
// For repo announcements, check if we need to create a grasp list
if (effectiveKind === 30617 || effectiveKind === KIND.REPO_ANNOUNCEMENT) {
try {
const currentPubkey = session.pubkey;
const graspListEvents = await nostrClient.fetchEvents(
[{ kinds: [10317], authors: [currentPubkey], limit: 1 }],
relayManager.getProfileReadRelays(),
{ useCache: 'cache-first', cacheResults: true }
);
if (graspListEvents.length === 0) {
// User doesn't have a grasp list, create one
// Only include actual GRASP relays in the g tags (not documentation relays)
// Filter out thecitadel since it's not a GRASP server
const graspRelaysForTags = config.graspRelays.filter(r => !r.includes('thecitadel'));
const graspListEventTemplate: Omit<NostrEvent, 'sig' | 'id'> = {
kind: 10317,
pubkey: currentPubkey,
created_at: Math.floor(Date.now() / 1000),
tags: graspRelaysForTags.map(server => ['g', server]), // Only GRASP relays in g tags
content: ''
};
const signedGraspListEvent = await session.signer(graspListEventTemplate);
await cacheEvent(signedGraspListEvent);
// Publish grasp list to documentation and GRASP relays
// (documentation relay accepts these kinds but isn't a GRASP relay, so not in g tags)
const allGraspRelays = [...new Set([...config.documentationPublishRelays, ...config.graspRelays])];
await signAndPublish(graspListEventTemplate, allGraspRelays);
console.log('Auto-created and published user grasp list (kind 10317)');
}
} catch (error) {
console.warn('Failed to check/create grasp list:', error);
// Non-critical, continue
}
}
content = ''; content = '';
tags = []; tags = [];
uploadedFiles = []; uploadedFiles = [];
@ -477,7 +409,6 @@
if (dTag) { if (dTag) {
try { try {
// Only include documentation relay as relay hint (keeps naddr shorter) // Only include documentation relay as relay hint (keeps naddr shorter)
// Both events are published to documentation and GRASP relays, but we only hint at documentation relay
const relayHints = config.documentationPublishRelays; const relayHints = config.documentationPublishRelays;
const naddr = nip19.naddrEncode({ const naddr = nip19.naddrEncode({
kind: signedEvent.kind, kind: signedEvent.kind,
@ -795,34 +726,6 @@
</div> </div>
<div class="form-actions"> <div class="form-actions">
{#if (effectiveKind === 30617 || effectiveKind === KIND.REPO_ANNOUNCEMENT) && hasGraspList === false}
<div class="grasp-list-notice">
<Icon name="info" size={16} />
<div class="notice-content">
<p class="notice-text">
A User Grasp List (kind 10317) will be created automatically on your behalf.
</p>
<p class="notice-relays">
Events will be published to:
</p>
<ul class="relay-list">
{#each allPublishRelays as relay}
<li>
{relay}
{#if config.documentationPublishRelays.includes(relay)}
<span class="relay-note">(documentation relay, accepts these kinds but not a GRASP relay)</span>
{:else}
<span class="relay-note">(GRASP relay)</span>
{/if}
</li>
{/each}
</ul>
<p class="notice-note">
Note: Only GRASP relays will be included in the g tags of the kind 10317 event.
</p>
</div>
</div>
{/if}
<button <button
class="publish-button" class="publish-button"
onclick={publish} onclick={publish}
@ -1955,64 +1858,4 @@
background: var(--fog-dark-border, #475569); background: var(--fog-dark-border, #475569);
border-color: var(--fog-dark-accent, #94a3b8); border-color: var(--fog-dark-accent, #94a3b8);
} }
.grasp-list-notice {
display: flex;
gap: 0.75rem;
padding: 1rem;
margin-bottom: 1rem;
background: var(--fog-highlight, #f3f4f6);
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
color: var(--fog-text, #1f2937);
}
:global(.dark) .grasp-list-notice {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-border, #374151);
color: var(--fog-dark-text, #f1f5f9);
}
.notice-content {
flex: 1;
}
.notice-text {
margin: 0 0 0.5rem 0;
font-weight: 500;
}
.notice-relays {
margin: 0.5rem 0 0.25rem 0;
font-size: 0.875rem;
}
.relay-list {
margin: 0.25rem 0 0 0;
padding-left: 1.5rem;
font-size: 0.875rem;
list-style: disc;
}
.relay-note {
color: var(--fog-text-light, #52667a);
font-style: italic;
margin-left: 0.5rem;
font-size: 0.8125rem;
}
:global(.dark) .relay-note {
color: var(--fog-dark-text-light, #a8b8d0);
}
.notice-note {
margin: 0.75rem 0 0 0;
font-size: 0.8125rem;
color: var(--fog-text-light, #52667a);
font-style: italic;
}
:global(.dark) .notice-note {
color: var(--fog-dark-text-light, #a8b8d0);
}
</style> </style>

34
src/lib/services/content/git-repo-fetcher.ts

@ -1443,21 +1443,29 @@ export function extractGitUrls(event: { tags: string[][]; content: string }): st
// Check tags for git URLs (including 'clone' tag which is used in NIP-34) // Check tags for git URLs (including 'clone' tag which is used in NIP-34)
for (const tag of event.tags) { for (const tag of event.tags) {
if (tag[0] === 'r' || tag[0] === 'url' || tag[0] === 'git' || tag[0] === 'clone') { if (tag[0] === 'r' || tag[0] === 'url' || tag[0] === 'git' || tag[0] === 'clone') {
const url = tag[1]; // Clone tags can have multiple URLs: ["clone", "url1", "url2", "url3"]
if (!url) continue; // For other tags (r, url, git), we only take the first value (index 1)
const isCloneTag = tag[0] === 'clone';
const startIndex = 1;
const endIndex = isCloneTag ? tag.length : 2;
// Convert SSH URLs to HTTPS for (let i = startIndex; i < endIndex; i++) {
if (url.startsWith('git@')) { const url = tag[i];
const httpsUrl = convertSshToHttps(url); if (!url) continue;
if (httpsUrl) {
urls.push(httpsUrl); // Convert SSH URLs to HTTPS
continue; if (url.startsWith('git@')) {
const httpsUrl = convertSshToHttps(url);
if (httpsUrl) {
urls.push(httpsUrl);
continue;
}
}
// Check if it's a git URL (including GRASP URLs with npub)
if (url.includes('github.com') || url.includes('gitlab.com') || url.includes('gitea') || url.includes('.git') || url.includes('/npub1') || url.startsWith('http')) {
urls.push(url);
} }
}
// Check if it's a git URL (including GRASP URLs with npub)
if (url.includes('github.com') || url.includes('gitlab.com') || url.includes('gitea') || url.includes('.git') || url.includes('/npub1') || url.startsWith('http')) {
urls.push(url);
} }
} }
} }

82
src/routes/repos/+page.svelte

@ -309,24 +309,48 @@
// Helper functions to extract repo data // Helper functions to extract repo data
function getCloneUrls(event: NostrEvent): string[] { function getCloneUrls(event: NostrEvent): string[] {
if (!Array.isArray(event.tags)) return []; if (!Array.isArray(event.tags)) return [];
return event.tags const urls: string[] = [];
.filter(t => Array.isArray(t) && t[0] === 'clone' && t[1])
.map(t => { // Clone tags can have multiple URLs: ["clone", "url1", "url2", "url3"]
const url = String(t[1]); for (const tag of event.tags) {
// Convert SSH URLs to HTTPS if (Array.isArray(tag) && tag[0] === 'clone') {
if (url.startsWith('git@')) { // Extract all URLs from this clone tag (skip index 0 which is "clone")
const httpsUrl = convertSshToHttps(url); for (let i = 1; i < tag.length; i++) {
return httpsUrl || url; // Fallback to original if conversion fails const url = String(tag[i]);
if (url) {
// Convert SSH URLs to HTTPS
if (url.startsWith('git@')) {
const httpsUrl = convertSshToHttps(url);
urls.push(httpsUrl || url); // Fallback to original if conversion fails
} else {
urls.push(url);
}
}
} }
return url; }
}); }
return urls;
} }
function getWebUrls(event: NostrEvent): string[] { function getWebUrls(event: NostrEvent): string[] {
if (!Array.isArray(event.tags)) return []; if (!Array.isArray(event.tags)) return [];
return event.tags const urls: string[] = [];
.filter(t => Array.isArray(t) && t[0] === 'web' && t[1])
.map(t => String(t[1])); // Web tags can have multiple URLs: ["web", "url1", "url2", "url3"]
for (const tag of event.tags) {
if (Array.isArray(tag) && tag[0] === 'web') {
// Extract all URLs from this web tag (skip index 0 which is "web")
for (let i = 1; i < tag.length; i++) {
const url = String(tag[i]);
if (url) {
urls.push(url);
}
}
}
}
return urls;
} }
function getMaintainers(event: NostrEvent): string[] { function getMaintainers(event: NostrEvent): string[] {
@ -795,6 +819,38 @@
cursor: pointer; cursor: pointer;
} }
.publish-repo-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid var(--fog-border, #e5e7eb);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1e293b);
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.publish-repo-btn:hover {
background: var(--fog-highlight, #f1f5f9);
border-color: var(--fog-accent, #94a3b8);
}
:global(.dark) .publish-repo-btn {
background: var(--fog-dark-post, #334155);
border-color: var(--fog-dark-border, #475569);
color: var(--fog-dark-text, #f1f5f9);
}
:global(.dark) .publish-repo-btn:hover {
background: var(--fog-dark-highlight, #475569);
border-color: var(--fog-dark-accent, #64748b);
}
.search-container { .search-container {
max-width: 100%; max-width: 100%;
} }

50
src/routes/repos/[naddr]/+page.svelte

@ -943,24 +943,48 @@
function getWebUrls(): string[] { function getWebUrls(): string[] {
if (!repoEvent || !Array.isArray(repoEvent.tags)) return []; if (!repoEvent || !Array.isArray(repoEvent.tags)) return [];
return repoEvent.tags const urls: string[] = [];
.filter(t => Array.isArray(t) && t[0] === 'web' && t[1])
.map(t => String(t[1])); // Web tags can have multiple URLs: ["web", "url1", "url2", "url3"]
for (const tag of repoEvent.tags) {
if (Array.isArray(tag) && tag[0] === 'web') {
// Extract all URLs from this web tag (skip index 0 which is "web")
for (let i = 1; i < tag.length; i++) {
const url = String(tag[i]);
if (url) {
urls.push(url);
}
}
}
}
return urls;
} }
function getCloneUrls(): string[] { function getCloneUrls(): string[] {
if (!repoEvent || !Array.isArray(repoEvent.tags)) return []; if (!repoEvent || !Array.isArray(repoEvent.tags)) return [];
return repoEvent.tags const urls: string[] = [];
.filter(t => Array.isArray(t) && t[0] === 'clone' && t[1])
.map(t => { // Clone tags can have multiple URLs: ["clone", "url1", "url2", "url3"]
const url = String(t[1]); for (const tag of repoEvent.tags) {
// Convert SSH URLs to HTTPS if (Array.isArray(tag) && tag[0] === 'clone') {
if (url.startsWith('git@')) { // Extract all URLs from this clone tag (skip index 0 which is "clone")
const httpsUrl = convertSshToHttps(url); for (let i = 1; i < tag.length; i++) {
return httpsUrl || url; // Fallback to original if conversion fails const url = String(tag[i]);
if (url) {
// Convert SSH URLs to HTTPS
if (url.startsWith('git@')) {
const httpsUrl = convertSshToHttps(url);
urls.push(httpsUrl || url); // Fallback to original if conversion fails
} else {
urls.push(url);
}
}
} }
return url; }
}); }
return urls;
} }
function getRelays(): string[] { function getRelays(): string[] {

2
static/changelog.yaml

@ -1,6 +1,6 @@
versions: versions:
'0.3.4': '0.3.4':
- 'Added support for publishing to GRASP relays' - 'Bug-fixes'
'0.3.3': '0.3.3':
- 'Added GRASP repository management' - 'Added GRASP repository management'
- 'Support manual creation and editing of User Grasp List (kind 10317) and repo announcements (kind 30617)' - 'Support manual creation and editing of User Grasp List (kind 10317) and repo announcements (kind 30617)'

Loading…
Cancel
Save