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

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

@ -70,7 +70,6 @@ @@ -70,7 +70,6 @@
let tags = $state<string[][]>(getInitialTags());
let publishing = $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 uploadedFiles: Array<{ url: string; imetaTag: string[] }> = $state([]);
@ -107,32 +106,6 @@ @@ -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
$effect(() => {
if (typeof window === 'undefined' || initialEvent) return;
@ -211,7 +184,6 @@ @@ -211,7 +184,6 @@
const helpText = $derived(kindMetadata.helpText);
const isKind30040 = $derived(selectedKind === 30040);
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)
$effect(() => {
@ -413,9 +385,9 @@ @@ -413,9 +385,9 @@
true
);
// For repo announcements, also add documentation and GRASP relays
// For repo announcements, also add documentation relays
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);
@ -423,46 +395,6 @@ @@ -423,46 +395,6 @@
publicationModalOpen = true;
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 = '';
tags = [];
uploadedFiles = [];
@ -477,7 +409,6 @@ @@ -477,7 +409,6 @@
if (dTag) {
try {
// 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 naddr = nip19.naddrEncode({
kind: signedEvent.kind,
@ -795,34 +726,6 @@ @@ -795,34 +726,6 @@
</div>
<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
class="publish-button"
onclick={publish}
@ -1955,64 +1858,4 @@ @@ -1955,64 +1858,4 @@
background: var(--fog-dark-border, #475569);
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>

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

@ -1443,21 +1443,29 @@ export function extractGitUrls(event: { tags: string[][]; content: string }): st @@ -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)
for (const tag of event.tags) {
if (tag[0] === 'r' || tag[0] === 'url' || tag[0] === 'git' || tag[0] === 'clone') {
const url = tag[1];
if (!url) continue;
// Convert SSH URLs to HTTPS
if (url.startsWith('git@')) {
const httpsUrl = convertSshToHttps(url);
if (httpsUrl) {
urls.push(httpsUrl);
continue;
// Clone tags can have multiple URLs: ["clone", "url1", "url2", "url3"]
// 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;
for (let i = startIndex; i < endIndex; i++) {
const url = tag[i];
if (!url) continue;
// Convert SSH URLs to HTTPS
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 @@ @@ -309,24 +309,48 @@
// Helper functions to extract repo data
function getCloneUrls(event: NostrEvent): string[] {
if (!Array.isArray(event.tags)) return [];
return event.tags
.filter(t => Array.isArray(t) && t[0] === 'clone' && 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
const urls: string[] = [];
// Clone tags can have multiple URLs: ["clone", "url1", "url2", "url3"]
for (const tag of event.tags) {
if (Array.isArray(tag) && tag[0] === 'clone') {
// Extract all URLs from this clone tag (skip index 0 which is "clone")
for (let i = 1; i < tag.length; i++) {
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[] {
if (!Array.isArray(event.tags)) return [];
return event.tags
.filter(t => Array.isArray(t) && t[0] === 'web' && t[1])
.map(t => String(t[1]));
const urls: string[] = [];
// 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[] {
@ -795,6 +819,38 @@ @@ -795,6 +819,38 @@
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 {
max-width: 100%;
}

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

@ -943,24 +943,48 @@ @@ -943,24 +943,48 @@
function getWebUrls(): string[] {
if (!repoEvent || !Array.isArray(repoEvent.tags)) return [];
return repoEvent.tags
.filter(t => Array.isArray(t) && t[0] === 'web' && t[1])
.map(t => String(t[1]));
const urls: string[] = [];
// 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[] {
if (!repoEvent || !Array.isArray(repoEvent.tags)) return [];
return repoEvent.tags
.filter(t => Array.isArray(t) && t[0] === 'clone' && 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
const urls: string[] = [];
// Clone tags can have multiple URLs: ["clone", "url1", "url2", "url3"]
for (const tag of repoEvent.tags) {
if (Array.isArray(tag) && tag[0] === 'clone') {
// Extract all URLs from this clone tag (skip index 0 which is "clone")
for (let i = 1; i < tag.length; i++) {
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[] {

2
static/changelog.yaml

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

Loading…
Cancel
Save