|
|
|
|
@ -40,9 +40,15 @@
@@ -40,9 +40,15 @@
|
|
|
|
|
let previewLoading = $state(false); |
|
|
|
|
let previewTimeout: ReturnType<typeof setTimeout> | null = null; |
|
|
|
|
|
|
|
|
|
import { DEFAULT_NOSTR_RELAYS, combineRelays } from '../../lib/config.js'; |
|
|
|
|
// Lookup state |
|
|
|
|
let lookupLoading = $state<{ [key: string]: boolean }>({}); |
|
|
|
|
let lookupError = $state<{ [key: string]: string | null }>({}); |
|
|
|
|
let lookupResults = $state<{ [key: string]: any }>({}); |
|
|
|
|
|
|
|
|
|
import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS, combineRelays } from '../../lib/config.js'; |
|
|
|
|
|
|
|
|
|
const nostrClient = new NostrClient(DEFAULT_NOSTR_RELAYS); |
|
|
|
|
const searchClient = new NostrClient(DEFAULT_NOSTR_SEARCH_RELAYS); |
|
|
|
|
|
|
|
|
|
onMount(() => { |
|
|
|
|
nip07Available = isNIP07Available(); |
|
|
|
|
@ -185,6 +191,313 @@
@@ -185,6 +191,313 @@
|
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Validation functions |
|
|
|
|
function validateCloneUrl(url: string): string | null { |
|
|
|
|
if (!url.trim()) return null; // Empty is OK |
|
|
|
|
if (!isValidUrl(url.trim())) { |
|
|
|
|
return 'Invalid URL format. Must start with http:// or https://'; |
|
|
|
|
} |
|
|
|
|
if (!url.trim().endsWith('.git') && !url.trim().includes('/')) { |
|
|
|
|
return 'Clone URL should end with .git or be a valid repository URL'; |
|
|
|
|
} |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function validateWebUrl(url: string): string | null { |
|
|
|
|
if (!url.trim()) return null; // Empty is OK |
|
|
|
|
if (!isValidUrl(url.trim())) { |
|
|
|
|
return 'Invalid URL format. Must start with http:// or https://'; |
|
|
|
|
} |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function validateMaintainer(maintainer: string): string | null { |
|
|
|
|
if (!maintainer.trim()) return null; // Empty is OK |
|
|
|
|
// Check if it's a valid npub or hex pubkey |
|
|
|
|
try { |
|
|
|
|
if (maintainer.startsWith('npub')) { |
|
|
|
|
nip19.decode(maintainer); |
|
|
|
|
return null; |
|
|
|
|
} else if (maintainer.length === 64 && /^[0-9a-f]+$/i.test(maintainer)) { |
|
|
|
|
return null; // Valid hex pubkey |
|
|
|
|
} else { |
|
|
|
|
return 'Invalid maintainer format. Use npub1... or 64-character hex pubkey'; |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
return 'Invalid maintainer format. Use npub1... or 64-character hex pubkey'; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function validateDocumentation(doc: string): string | null { |
|
|
|
|
if (!doc.trim()) return null; // Empty is OK |
|
|
|
|
// Check if it's in naddr format or 30618:pubkey:identifier format |
|
|
|
|
if (doc.startsWith('naddr')) { |
|
|
|
|
try { |
|
|
|
|
const decoded = nip19.decode(doc); |
|
|
|
|
if (decoded.type === 'naddr') return null; |
|
|
|
|
} catch { |
|
|
|
|
return 'Invalid naddr format'; |
|
|
|
|
} |
|
|
|
|
} else if (/^\d+:[0-9a-f]{64}:[a-zA-Z0-9_-]+$/.test(doc)) { |
|
|
|
|
return null; // Valid kind:pubkey:identifier format |
|
|
|
|
} |
|
|
|
|
return 'Invalid documentation format. Use naddr1... or kind:pubkey:identifier'; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function validateImageUrl(url: string): string | null { |
|
|
|
|
if (!url.trim()) return null; // Empty is OK |
|
|
|
|
if (!isValidUrl(url.trim())) { |
|
|
|
|
return 'Invalid URL format. Must start with http:// or https://'; |
|
|
|
|
} |
|
|
|
|
// Check if it's likely an image URL |
|
|
|
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']; |
|
|
|
|
const lowerUrl = url.toLowerCase(); |
|
|
|
|
if (!imageExtensions.some(ext => lowerUrl.includes(ext)) && !lowerUrl.includes('image') && !lowerUrl.includes('img')) { |
|
|
|
|
return 'Warning: URL does not appear to be an image'; |
|
|
|
|
} |
|
|
|
|
return null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Lookup functions |
|
|
|
|
// Use only default search relays for lookups to avoid connecting to random/unreachable user relays |
|
|
|
|
function getSearchRelays(): string[] { |
|
|
|
|
return DEFAULT_NOSTR_SEARCH_RELAYS; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function lookupRepoAnnouncement(query: string, fieldName: string) { |
|
|
|
|
const lookupKey = `repo-${fieldName}`; |
|
|
|
|
lookupLoading[lookupKey] = true; |
|
|
|
|
lookupError[lookupKey] = null; |
|
|
|
|
lookupResults[lookupKey] = null; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const relays = await getSearchRelays(); |
|
|
|
|
const client = new NostrClient(relays); |
|
|
|
|
|
|
|
|
|
// Try to decode as naddr, nevent, or hex |
|
|
|
|
const decoded = decodeNostrAddress(query.trim()); |
|
|
|
|
let events: NostrEvent[] = []; |
|
|
|
|
|
|
|
|
|
if (decoded) { |
|
|
|
|
if (decoded.type === 'note' && decoded.id) { |
|
|
|
|
events = await client.fetchEvents([{ ids: [decoded.id], limit: 10 }]); |
|
|
|
|
} else if (decoded.type === 'nevent' && decoded.id) { |
|
|
|
|
events = await client.fetchEvents([{ ids: [decoded.id], limit: 10 }]); |
|
|
|
|
} else if (decoded.type === 'naddr' && decoded.pubkey && decoded.kind && decoded.identifier) { |
|
|
|
|
events = await client.fetchEvents([ |
|
|
|
|
{ |
|
|
|
|
kinds: [decoded.kind], |
|
|
|
|
authors: [decoded.pubkey], |
|
|
|
|
'#d': [decoded.identifier], |
|
|
|
|
limit: 10 |
|
|
|
|
} |
|
|
|
|
]); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Also search by name or d-tag if query doesn't look like an address |
|
|
|
|
if (events.length === 0 && !query.startsWith('naddr') && !query.startsWith('nevent') && !/^[0-9a-f]{64}$/i.test(query)) { |
|
|
|
|
const repoEvents = await client.fetchEvents([ |
|
|
|
|
{ |
|
|
|
|
kinds: [KIND.REPO_ANNOUNCEMENT], |
|
|
|
|
limit: 20 |
|
|
|
|
} |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
const searchLower = query.toLowerCase(); |
|
|
|
|
events = repoEvents.filter(event => { |
|
|
|
|
const name = event.tags.find(t => t[0] === 'name')?.[1] || ''; |
|
|
|
|
const dTag = event.tags.find(t => t[0] === 'd')?.[1] || ''; |
|
|
|
|
return name.toLowerCase().includes(searchLower) || dTag.toLowerCase().includes(searchLower); |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (events.length === 0) { |
|
|
|
|
lookupError[lookupKey] = 'No repository announcements found'; |
|
|
|
|
} else { |
|
|
|
|
lookupResults[lookupKey] = events; |
|
|
|
|
} |
|
|
|
|
} catch (err) { |
|
|
|
|
lookupError[lookupKey] = `Lookup failed: ${String(err)}`; |
|
|
|
|
} finally { |
|
|
|
|
lookupLoading[lookupKey] = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function lookupNpub(query: string, fieldName: string, index?: number) { |
|
|
|
|
const lookupKey = index !== undefined ? `npub-${fieldName}-${index}` : `npub-${fieldName}`; |
|
|
|
|
lookupLoading[lookupKey] = true; |
|
|
|
|
lookupError[lookupKey] = null; |
|
|
|
|
lookupResults[lookupKey] = null; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
// Try to decode as npub |
|
|
|
|
let pubkey: string | null = null; |
|
|
|
|
try { |
|
|
|
|
if (query.startsWith('npub')) { |
|
|
|
|
const decoded = nip19.decode(query); |
|
|
|
|
if (decoded.type === 'npub') { |
|
|
|
|
pubkey = decoded.data as string; |
|
|
|
|
} |
|
|
|
|
} else if (query.length === 64 && /^[0-9a-f]+$/i.test(query)) { |
|
|
|
|
pubkey = query; |
|
|
|
|
} |
|
|
|
|
} catch { |
|
|
|
|
// Invalid format |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (!pubkey) { |
|
|
|
|
// Search for npubs by name (requires kind 0 metadata) |
|
|
|
|
const relays = getSearchRelays(); |
|
|
|
|
const client = new NostrClient(relays); |
|
|
|
|
|
|
|
|
|
// Search for profiles |
|
|
|
|
const profileEvents = await client.fetchEvents([ |
|
|
|
|
{ |
|
|
|
|
kinds: [0], // Metadata events |
|
|
|
|
limit: 20 |
|
|
|
|
} |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
const searchLower = query.toLowerCase(); |
|
|
|
|
const matches = profileEvents.filter(event => { |
|
|
|
|
try { |
|
|
|
|
const content = JSON.parse(event.content); |
|
|
|
|
const name = content.name || content.display_name || ''; |
|
|
|
|
return name.toLowerCase().includes(searchLower); |
|
|
|
|
} catch { |
|
|
|
|
return false; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
if (matches.length > 0) { |
|
|
|
|
lookupResults[lookupKey] = matches.map(e => { |
|
|
|
|
try { |
|
|
|
|
const content = JSON.parse(e.content); |
|
|
|
|
return { |
|
|
|
|
pubkey: e.pubkey, |
|
|
|
|
npub: nip19.npubEncode(e.pubkey), |
|
|
|
|
name: content.name || content.display_name || 'Unknown', |
|
|
|
|
about: content.about || '', |
|
|
|
|
picture: content.picture || '' |
|
|
|
|
}; |
|
|
|
|
} catch { |
|
|
|
|
return { |
|
|
|
|
pubkey: e.pubkey, |
|
|
|
|
npub: nip19.npubEncode(e.pubkey), |
|
|
|
|
name: 'Unknown' |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
}); |
|
|
|
|
} else { |
|
|
|
|
lookupError[lookupKey] = 'No profiles found matching the query'; |
|
|
|
|
} |
|
|
|
|
} else { |
|
|
|
|
// Valid pubkey, try to fetch profile |
|
|
|
|
const relays = getSearchRelays(); |
|
|
|
|
const client = new NostrClient(relays); |
|
|
|
|
const profileEvents = await client.fetchEvents([ |
|
|
|
|
{ |
|
|
|
|
kinds: [0], |
|
|
|
|
authors: [pubkey], |
|
|
|
|
limit: 1 |
|
|
|
|
} |
|
|
|
|
]); |
|
|
|
|
|
|
|
|
|
let profileData: any = { |
|
|
|
|
pubkey, |
|
|
|
|
npub: query.startsWith('npub') ? query : nip19.npubEncode(pubkey) |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
if (profileEvents.length > 0) { |
|
|
|
|
try { |
|
|
|
|
const content = JSON.parse(profileEvents[0].content); |
|
|
|
|
profileData.name = content.name || content.display_name || ''; |
|
|
|
|
profileData.about = content.about || ''; |
|
|
|
|
profileData.picture = content.picture || ''; |
|
|
|
|
} catch { |
|
|
|
|
// Invalid JSON, use defaults |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
lookupResults[lookupKey] = [profileData]; |
|
|
|
|
} |
|
|
|
|
} catch (err) { |
|
|
|
|
lookupError[lookupKey] = `Lookup failed: ${String(err)}`; |
|
|
|
|
} finally { |
|
|
|
|
lookupLoading[lookupKey] = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function lookupNevent(query: string, fieldName: string) { |
|
|
|
|
const lookupKey = `nevent-${fieldName}`; |
|
|
|
|
lookupLoading[lookupKey] = true; |
|
|
|
|
lookupError[lookupKey] = null; |
|
|
|
|
lookupResults[lookupKey] = null; |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
|
const relays = await getSearchRelays(); |
|
|
|
|
const client = new NostrClient(relays); |
|
|
|
|
|
|
|
|
|
let events: NostrEvent[] = []; |
|
|
|
|
const decoded = decodeNostrAddress(query.trim()); |
|
|
|
|
|
|
|
|
|
if (decoded && decoded.id) { |
|
|
|
|
events = await client.fetchEvents([{ ids: [decoded.id], limit: 10 }]); |
|
|
|
|
} else if (/^[0-9a-f]{64}$/i.test(query.trim())) { |
|
|
|
|
// Hex event ID |
|
|
|
|
events = await client.fetchEvents([{ ids: [query.trim()], limit: 10 }]); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (events.length === 0) { |
|
|
|
|
lookupError[lookupKey] = 'No events found'; |
|
|
|
|
} else { |
|
|
|
|
lookupResults[lookupKey] = events; |
|
|
|
|
} |
|
|
|
|
} catch (err) { |
|
|
|
|
lookupError[lookupKey] = `Lookup failed: ${String(err)}`; |
|
|
|
|
} finally { |
|
|
|
|
lookupLoading[lookupKey] = false; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function selectRepoResult(result: NostrEvent, fieldName: string) { |
|
|
|
|
if (fieldName === 'existingRepoRef') { |
|
|
|
|
existingRepoRef = result.id; |
|
|
|
|
loadExistingRepo(); |
|
|
|
|
} else if (fieldName === 'forkOriginalRepo') { |
|
|
|
|
// Convert to naddr format if possible |
|
|
|
|
const dTag = result.tags.find(t => t[0] === 'd')?.[1]; |
|
|
|
|
if (dTag) { |
|
|
|
|
try { |
|
|
|
|
const naddr = nip19.naddrEncode({ |
|
|
|
|
pubkey: result.pubkey, |
|
|
|
|
kind: result.kind, |
|
|
|
|
identifier: dTag, |
|
|
|
|
relays: [] |
|
|
|
|
}); |
|
|
|
|
forkOriginalRepo = naddr; |
|
|
|
|
} catch { |
|
|
|
|
forkOriginalRepo = `${result.kind}:${result.pubkey}:${dTag}`; |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
lookupResults[`repo-${fieldName}`] = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function selectNpubResult(result: { pubkey: string; npub: string; name?: string; about?: string; picture?: string }, fieldName: string, index?: number) { |
|
|
|
|
if (fieldName === 'maintainers' && index !== undefined) { |
|
|
|
|
updateMaintainer(index, result.npub); |
|
|
|
|
} |
|
|
|
|
const lookupKey = index !== undefined ? `npub-${fieldName}-${index}` : `npub-${fieldName}`; |
|
|
|
|
lookupResults[lookupKey] = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
function clearLookupResults(key: string) { |
|
|
|
|
lookupResults[key] = null; |
|
|
|
|
lookupError[key] = null; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
async function loadExistingRepo() { |
|
|
|
|
if (!existingRepoRef.trim()) return; |
|
|
|
|
|
|
|
|
|
@ -360,6 +673,61 @@
@@ -360,6 +673,61 @@
|
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Validate all fields |
|
|
|
|
const validationErrors: string[] = []; |
|
|
|
|
|
|
|
|
|
// Validate clone URLs |
|
|
|
|
for (let i = 0; i < cloneUrls.length; i++) { |
|
|
|
|
const urlError = validateCloneUrl(cloneUrls[i]); |
|
|
|
|
if (urlError) { |
|
|
|
|
validationErrors.push(`Clone URL ${i + 1}: ${urlError}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Validate web URLs |
|
|
|
|
for (let i = 0; i < webUrls.length; i++) { |
|
|
|
|
const urlError = validateWebUrl(webUrls[i]); |
|
|
|
|
if (urlError) { |
|
|
|
|
validationErrors.push(`Web URL ${i + 1}: ${urlError}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Validate maintainers |
|
|
|
|
for (let i = 0; i < maintainers.length; i++) { |
|
|
|
|
const maintainerError = validateMaintainer(maintainers[i]); |
|
|
|
|
if (maintainerError) { |
|
|
|
|
validationErrors.push(`Maintainer ${i + 1}: ${maintainerError}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Validate documentation |
|
|
|
|
for (let i = 0; i < documentation.length; i++) { |
|
|
|
|
const docError = validateDocumentation(documentation[i]); |
|
|
|
|
if (docError) { |
|
|
|
|
validationErrors.push(`Documentation ${i + 1}: ${docError}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Validate image URLs |
|
|
|
|
if (imageUrl.trim()) { |
|
|
|
|
const imageError = validateImageUrl(imageUrl); |
|
|
|
|
if (imageError) { |
|
|
|
|
validationErrors.push(`Image URL: ${imageError}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (bannerUrl.trim()) { |
|
|
|
|
const bannerError = validateImageUrl(bannerUrl); |
|
|
|
|
if (bannerError) { |
|
|
|
|
validationErrors.push(`Banner URL: ${bannerError}`); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (validationErrors.length > 0) { |
|
|
|
|
error = 'Validation errors:\n' + validationErrors.join('\n'); |
|
|
|
|
return; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
loading = true; |
|
|
|
|
error = null; |
|
|
|
|
|
|
|
|
|
@ -537,9 +905,12 @@
@@ -537,9 +905,12 @@
|
|
|
|
|
// Sign with NIP-07 |
|
|
|
|
const signedEvent = await signEventWithNIP07(eventTemplate); |
|
|
|
|
|
|
|
|
|
// Get user's inbox/outbox relays (from kind 3 or 10002) |
|
|
|
|
// Get user's inbox/outbox relays (from kind 10002) using full relay set to find newest |
|
|
|
|
const { getUserRelays } = await import('../../lib/services/nostr/user-relays.js'); |
|
|
|
|
const { inbox, outbox } = await getUserRelays(pubkey, nostrClient); |
|
|
|
|
// Use comprehensive relay set to ensure we get the newest kind 10002 event |
|
|
|
|
const fullRelaySet = combineRelays([], [...DEFAULT_NOSTR_SEARCH_RELAYS, ...DEFAULT_NOSTR_RELAYS]); |
|
|
|
|
const fullRelayClient = new NostrClient(fullRelaySet); |
|
|
|
|
const { inbox, outbox } = await getUserRelays(pubkey, fullRelayClient); |
|
|
|
|
|
|
|
|
|
// Combine user's outbox with default relays |
|
|
|
|
const userRelays = combineRelays(outbox); |
|
|
|
|
@ -614,6 +985,15 @@
@@ -614,6 +985,15 @@
|
|
|
|
|
placeholder="hex event ID, nevent1..., or naddr1..." |
|
|
|
|
disabled={loading || loadingExisting} |
|
|
|
|
/> |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
onclick={() => lookupRepoAnnouncement(existingRepoRef || '', 'existingRepoRef')} |
|
|
|
|
disabled={loading || loadingExisting || !existingRepoRef.trim()} |
|
|
|
|
class="lookup-button" |
|
|
|
|
title="Search for repository announcements (supports hex ID, nevent, naddr, or search by name)" |
|
|
|
|
> |
|
|
|
|
{lookupLoading['repo-existingRepoRef'] ? '🔍...' : '🔍'} |
|
|
|
|
</button> |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
onclick={loadExistingRepo} |
|
|
|
|
@ -622,6 +1002,70 @@
@@ -622,6 +1002,70 @@
|
|
|
|
|
{loadingExisting ? 'Loading...' : 'Load'} |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{#if lookupError['repo-existingRepoRef']} |
|
|
|
|
<div class="lookup-error">{lookupError['repo-existingRepoRef']}</div> |
|
|
|
|
{/if} |
|
|
|
|
{#if lookupResults['repo-existingRepoRef']} |
|
|
|
|
<div class="lookup-results"> |
|
|
|
|
<div class="lookup-results-header"> |
|
|
|
|
<span>Found {lookupResults['repo-existingRepoRef'].length} repository announcement(s):</span> |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
onclick={() => clearLookupResults('repo-existingRepoRef')} |
|
|
|
|
class="clear-lookup-button" |
|
|
|
|
title="Clear results" |
|
|
|
|
> |
|
|
|
|
✕ |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{#each lookupResults['repo-existingRepoRef'] as result} |
|
|
|
|
{@const nameTag = result.tags.find((t: string[]) => t[0] === 'name')?.[1]} |
|
|
|
|
{@const dTag = result.tags.find((t: string[]) => t[0] === 'd')?.[1]} |
|
|
|
|
{@const descTag = result.tags.find((t: string[]) => t[0] === 'description')?.[1]} |
|
|
|
|
{@const imageTag = result.tags.find((t: string[]) => t[0] === 'image')?.[1]} |
|
|
|
|
{@const ownerNpub = nip19.npubEncode(result.pubkey)} |
|
|
|
|
{@const tags = result.tags.filter((t: string[]) => t[0] === 't' && t[1] && t[1] !== 'private' && t[1] !== 'fork').map((t: string[]) => t[1])} |
|
|
|
|
<div |
|
|
|
|
class="lookup-result-item repo-result" |
|
|
|
|
role="button" |
|
|
|
|
tabindex="0" |
|
|
|
|
onclick={() => selectRepoResult(result, 'existingRepoRef')} |
|
|
|
|
onkeydown={(e) => { |
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') { |
|
|
|
|
e.preventDefault(); |
|
|
|
|
selectRepoResult(result, 'existingRepoRef'); |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div class="result-header"> |
|
|
|
|
{#if imageTag} |
|
|
|
|
<img src={imageTag} alt="" class="result-image" /> |
|
|
|
|
{/if} |
|
|
|
|
<div class="result-info"> |
|
|
|
|
<strong>{nameTag || dTag || 'Unnamed'}</strong> |
|
|
|
|
{#if dTag} |
|
|
|
|
<small class="d-tag">d-tag: {dTag}</small> |
|
|
|
|
{/if} |
|
|
|
|
{#if descTag} |
|
|
|
|
<p class="result-description">{descTag}</p> |
|
|
|
|
{/if} |
|
|
|
|
<div class="result-meta"> |
|
|
|
|
<small>Owner: {ownerNpub.slice(0, 16)}...</small> |
|
|
|
|
<small>Event: {result.id.slice(0, 16)}...</small> |
|
|
|
|
</div> |
|
|
|
|
{#if tags.length > 0} |
|
|
|
|
<div class="result-tags"> |
|
|
|
|
{#each tags as tag} |
|
|
|
|
<span class="tag-badge">#{tag}</span> |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
<div class="form-group"> |
|
|
|
|
@ -756,6 +1200,15 @@
@@ -756,6 +1200,15 @@
|
|
|
|
|
placeholder="npub1abc... or hex pubkey" |
|
|
|
|
disabled={loading} |
|
|
|
|
/> |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
onclick={() => lookupNpub(maintainer || '', 'maintainers', index)} |
|
|
|
|
disabled={loading || !maintainer.trim()} |
|
|
|
|
class="lookup-button" |
|
|
|
|
title="Lookup npub or search by name" |
|
|
|
|
> |
|
|
|
|
{lookupLoading[`npub-maintainers-${index}`] ? '🔍...' : '🔍'} |
|
|
|
|
</button> |
|
|
|
|
{#if maintainers.length > 1} |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
@ -766,6 +1219,55 @@
@@ -766,6 +1219,55 @@
|
|
|
|
|
</button> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{#if lookupError[`npub-maintainers-${index}`]} |
|
|
|
|
<div class="lookup-error">{lookupError[`npub-maintainers-${index}`]}</div> |
|
|
|
|
{/if} |
|
|
|
|
{#if lookupResults[`npub-maintainers-${index}`]} |
|
|
|
|
<div class="lookup-results"> |
|
|
|
|
<div class="lookup-results-header"> |
|
|
|
|
<span>Found {lookupResults[`npub-maintainers-${index}`].length} profile(s):</span> |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
onclick={() => clearLookupResults(`npub-maintainers-${index}`)} |
|
|
|
|
class="clear-lookup-button" |
|
|
|
|
title="Clear results" |
|
|
|
|
> |
|
|
|
|
✕ |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{#each lookupResults[`npub-maintainers-${index}`] as result} |
|
|
|
|
<div |
|
|
|
|
class="lookup-result-item profile-result" |
|
|
|
|
role="button" |
|
|
|
|
tabindex="0" |
|
|
|
|
onclick={() => selectNpubResult(result, 'maintainers', index)} |
|
|
|
|
onkeydown={(e) => { |
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') { |
|
|
|
|
e.preventDefault(); |
|
|
|
|
selectNpubResult(result, 'maintainers', index); |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div class="result-header"> |
|
|
|
|
{#if result.picture} |
|
|
|
|
<img src={result.picture} alt="" class="result-avatar" /> |
|
|
|
|
{:else} |
|
|
|
|
<div class="result-avatar-placeholder"> |
|
|
|
|
{(result.name || result.npub).slice(0, 2).toUpperCase()} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
<div class="result-info"> |
|
|
|
|
<strong>{result.name || 'Unknown'}</strong> |
|
|
|
|
{#if result.about} |
|
|
|
|
<p class="result-description">{result.about}</p> |
|
|
|
|
{/if} |
|
|
|
|
<small class="npub-display">{result.npub}</small> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
{/each} |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
@ -926,14 +1428,89 @@
@@ -926,14 +1428,89 @@
|
|
|
|
|
• npub/repo format: npub1abc.../repo-name<br/> |
|
|
|
|
• Repository address: 30617:owner-pubkey:repo-name</small> |
|
|
|
|
</label> |
|
|
|
|
<input |
|
|
|
|
id="fork-original-repo" |
|
|
|
|
type="text" |
|
|
|
|
bind:value={forkOriginalRepo} |
|
|
|
|
placeholder="npub1abc.../original-repo or naddr1..." |
|
|
|
|
required={isFork} |
|
|
|
|
disabled={loading} |
|
|
|
|
/> |
|
|
|
|
<div class="input-group"> |
|
|
|
|
<input |
|
|
|
|
id="fork-original-repo" |
|
|
|
|
type="text" |
|
|
|
|
bind:value={forkOriginalRepo} |
|
|
|
|
placeholder="npub1abc.../original-repo or naddr1..." |
|
|
|
|
required={isFork} |
|
|
|
|
disabled={loading} |
|
|
|
|
/> |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
onclick={() => lookupRepoAnnouncement(forkOriginalRepo || '', 'forkOriginalRepo')} |
|
|
|
|
disabled={loading || !forkOriginalRepo.trim()} |
|
|
|
|
class="lookup-button" |
|
|
|
|
title="Search for repository announcements" |
|
|
|
|
> |
|
|
|
|
{lookupLoading['repo-forkOriginalRepo'] ? '🔍...' : '🔍'} |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{#if lookupError['repo-forkOriginalRepo']} |
|
|
|
|
<div class="lookup-error">{lookupError['repo-forkOriginalRepo']}</div> |
|
|
|
|
{/if} |
|
|
|
|
{#if lookupResults['repo-forkOriginalRepo']} |
|
|
|
|
<div class="lookup-results"> |
|
|
|
|
<div class="lookup-results-header"> |
|
|
|
|
<span>Found {lookupResults['repo-forkOriginalRepo'].length} repository announcement(s):</span> |
|
|
|
|
<button |
|
|
|
|
type="button" |
|
|
|
|
onclick={() => clearLookupResults('repo-forkOriginalRepo')} |
|
|
|
|
class="clear-lookup-button" |
|
|
|
|
title="Clear results" |
|
|
|
|
> |
|
|
|
|
✕ |
|
|
|
|
</button> |
|
|
|
|
</div> |
|
|
|
|
{#each lookupResults['repo-forkOriginalRepo'] as result} |
|
|
|
|
{@const nameTag = result.tags.find((t: string[]) => t[0] === 'name')?.[1]} |
|
|
|
|
{@const dTag = result.tags.find((t: string[]) => t[0] === 'd')?.[1]} |
|
|
|
|
{@const descTag = result.tags.find((t: string[]) => t[0] === 'description')?.[1]} |
|
|
|
|
{@const imageTag = result.tags.find((t: string[]) => t[0] === 'image')?.[1]} |
|
|
|
|
{@const ownerNpub = nip19.npubEncode(result.pubkey)} |
|
|
|
|
{@const tags = result.tags.filter((t: string[]) => t[0] === 't' && t[1] && t[1] !== 'private' && t[1] !== 'fork').map((t: string[]) => t[1])} |
|
|
|
|
<div |
|
|
|
|
class="lookup-result-item repo-result" |
|
|
|
|
role="button" |
|
|
|
|
tabindex="0" |
|
|
|
|
onclick={() => selectRepoResult(result, 'forkOriginalRepo')} |
|
|
|
|
onkeydown={(e) => { |
|
|
|
|
if (e.key === 'Enter' || e.key === ' ') { |
|
|
|
|
e.preventDefault(); |
|
|
|
|
selectRepoResult(result, 'forkOriginalRepo'); |
|
|
|
|
} |
|
|
|
|
}} |
|
|
|
|
> |
|
|
|
|
<div class="result-header"> |
|
|
|
|
{#if imageTag} |
|
|
|
|
<img src={imageTag} alt="" class="result-image" /> |
|
|
|
|
{/if} |
|
|
|
|
<div class="result-info"> |
|
|
|
|
<strong>{nameTag || dTag || 'Unnamed'}</strong> |
|
|
|
|
{#if dTag} |
|
|
|
|
<small class="d-tag">d-tag: {dTag}</small> |
|
|
|
|
{/if} |
|
|
|
|
{#if descTag} |
|
|
|
|
<p class="result-description">{descTag}</p> |
|
|
|
|
{/if} |
|
|
|
|
<div class="result-meta"> |
|
|
|
|
<small>Owner: {ownerNpub.slice(0, 16)}...</small> |
|
|
|
|
<small>Event: {result.id.slice(0, 16)}...</small> |
|
|
|
|
</div> |
|
|
|
|
{#if tags.length > 0} |
|
|
|
|
<div class="result-tags"> |
|
|
|
|
{#each tags as tag} |
|
|
|
|
<span class="tag-badge">#{tag}</span> |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
</div> |
|
|
|
|
{/each} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
</div> |
|
|
|
|
{/if} |
|
|
|
|
|
|
|
|
|
|