|
|
|
@ -1,17 +1,21 @@ |
|
|
|
<script lang="ts"> |
|
|
|
<script lang="ts"> |
|
|
|
import { |
|
|
|
import { |
|
|
|
ClipboardCheckOutline, |
|
|
|
|
|
|
|
ClipboardCleanOutline, |
|
|
|
ClipboardCleanOutline, |
|
|
|
DotsVerticalOutline, |
|
|
|
DotsVerticalOutline, |
|
|
|
EyeOutline, |
|
|
|
EyeOutline, |
|
|
|
ShareNodesOutline |
|
|
|
ShareNodesOutline |
|
|
|
} from "flowbite-svelte-icons"; |
|
|
|
} from "flowbite-svelte-icons"; |
|
|
|
import { Button, Modal, Popover } from "flowbite-svelte"; |
|
|
|
import { Button, Modal, Popover } from "flowbite-svelte"; |
|
|
|
import { standardRelays } from "$lib/consts"; |
|
|
|
import { standardRelays, FeedType } from "$lib/consts"; |
|
|
|
import { neventEncode, naddrEncode } from "$lib/utils"; |
|
|
|
import { neventEncode, naddrEncode } from "$lib/utils"; |
|
|
|
import InlineProfile from "$components/util/InlineProfile.svelte"; |
|
|
|
import InlineProfile from "$components/util/InlineProfile.svelte"; |
|
|
|
|
|
|
|
import { feedType } from "$lib/stores"; |
|
|
|
|
|
|
|
import { inboxRelays, ndkSignedIn } from "$lib/ndk"; |
|
|
|
|
|
|
|
import type { NDKEvent } from "@nostr-dev-kit/ndk"; |
|
|
|
|
|
|
|
import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; |
|
|
|
|
|
|
|
|
|
|
|
let { event } = $props(); |
|
|
|
// Component props |
|
|
|
|
|
|
|
let { event } = $props<{ event: NDKEvent }>(); |
|
|
|
|
|
|
|
|
|
|
|
// Derive metadata from event |
|
|
|
// Derive metadata from event |
|
|
|
let title = $derived(event.tags.find((t: string[]) => t[0] === 'title')?.[1] ?? ''); |
|
|
|
let title = $derived(event.tags.find((t: string[]) => t[0] === 'title')?.[1] ?? ''); |
|
|
|
@ -26,94 +30,81 @@ |
|
|
|
let publisher = $derived(event.tags.find((t: string[]) => t[0] === 'publisher')?.[1] ?? null); |
|
|
|
let publisher = $derived(event.tags.find((t: string[]) => t[0] === 'publisher')?.[1] ?? null); |
|
|
|
let identifier = $derived(event.tags.find((t: string[]) => t[0] === 'identifier')?.[1] ?? null); |
|
|
|
let identifier = $derived(event.tags.find((t: string[]) => t[0] === 'identifier')?.[1] ?? null); |
|
|
|
|
|
|
|
|
|
|
|
let jsonModalOpen: boolean = $state(false); |
|
|
|
// UI state |
|
|
|
let detailsModalOpen: boolean = $state(false); |
|
|
|
let detailsModalOpen: boolean = $state(false); |
|
|
|
let eventIdCopied: boolean = $state(false); |
|
|
|
let isOpen: boolean = $state(false); |
|
|
|
let shareLinkCopied: boolean = $state(false); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let isOpen = $state(false); |
|
|
|
/** |
|
|
|
|
|
|
|
* Selects the appropriate relay set based on user state and feed type |
|
|
|
|
|
|
|
* - Uses user's inbox relays when signed in and viewing personal feed |
|
|
|
|
|
|
|
* - Falls back to standard relays for anonymous users or standard feed |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
let activeRelays = $derived( |
|
|
|
|
|
|
|
(() => { |
|
|
|
|
|
|
|
const isUserFeed = $ndkSignedIn && $feedType === FeedType.UserRelays; |
|
|
|
|
|
|
|
const relays = isUserFeed ? $inboxRelays : standardRelays; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
console.debug("[CardActions] Selected relays:", { |
|
|
|
|
|
|
|
eventId: event.id, |
|
|
|
|
|
|
|
isSignedIn: $ndkSignedIn, |
|
|
|
|
|
|
|
feedType: $feedType, |
|
|
|
|
|
|
|
isUserFeed, |
|
|
|
|
|
|
|
relayCount: relays.length, |
|
|
|
|
|
|
|
relayUrls: relays |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return relays; |
|
|
|
|
|
|
|
})() |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Opens the actions popover menu |
|
|
|
|
|
|
|
*/ |
|
|
|
function openPopover() { |
|
|
|
function openPopover() { |
|
|
|
|
|
|
|
console.debug("[CardActions] Opening menu", { eventId: event.id }); |
|
|
|
isOpen = true; |
|
|
|
isOpen = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Closes the actions popover menu and removes focus |
|
|
|
|
|
|
|
*/ |
|
|
|
function closePopover() { |
|
|
|
function closePopover() { |
|
|
|
|
|
|
|
console.debug("[CardActions] Closing menu", { eventId: event.id }); |
|
|
|
isOpen = false; |
|
|
|
isOpen = false; |
|
|
|
const menu = document.getElementById('dots-' + event.id); |
|
|
|
const menu = document.getElementById('dots-' + event.id); |
|
|
|
if (menu) menu.blur(); |
|
|
|
if (menu) menu.blur(); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
function shareNjump() { |
|
|
|
/** |
|
|
|
const relays: string[] = standardRelays; |
|
|
|
* Gets the appropriate identifier (nevent or naddr) for copying |
|
|
|
|
|
|
|
* @param type - The type of identifier to get ('nevent' or 'naddr') |
|
|
|
try { |
|
|
|
* @returns The encoded identifier string |
|
|
|
const naddr = naddrEncode(event, relays); |
|
|
|
*/ |
|
|
|
console.debug(naddr); |
|
|
|
function getIdentifier(type: 'nevent' | 'naddr'): string { |
|
|
|
navigator.clipboard.writeText(`https://njump.me/${naddr}`); |
|
|
|
const encodeFn = type === 'nevent' ? neventEncode : naddrEncode; |
|
|
|
shareLinkCopied = true; |
|
|
|
return encodeFn(event, activeRelays); |
|
|
|
setTimeout(() => { |
|
|
|
|
|
|
|
shareLinkCopied = false; |
|
|
|
|
|
|
|
}, 4000); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
catch (e) { |
|
|
|
|
|
|
|
console.error('Failed to encode naddr:', e); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function copyEventId() { |
|
|
|
|
|
|
|
console.debug("copyEventID"); |
|
|
|
|
|
|
|
const relays: string[] = standardRelays; |
|
|
|
|
|
|
|
const nevent = neventEncode(event, relays); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
navigator.clipboard.writeText(nevent); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
eventIdCopied = true; |
|
|
|
|
|
|
|
setTimeout(() => { |
|
|
|
|
|
|
|
eventIdCopied = false; |
|
|
|
|
|
|
|
}, 4000); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function viewJson() { |
|
|
|
|
|
|
|
console.debug("viewJSON"); |
|
|
|
|
|
|
|
jsonModalOpen = true; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function viewDetails() { |
|
|
|
|
|
|
|
detailsModalOpen = true; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// --- Custom JSON pretty-printer with naddr hyperlinking --- |
|
|
|
|
|
|
|
/** |
|
|
|
/** |
|
|
|
* Returns HTML for pretty-printed JSON, with naddrs as links to /events?id=naddr1... |
|
|
|
* Opens the event details modal |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
function jsonWithNaddrLinks(obj: any): string { |
|
|
|
function viewDetails() { |
|
|
|
const NADDR_REGEX = /\b(\d{5}:[a-f0-9]{64}:[a-zA-Z0-9._-]+)\b/g; |
|
|
|
console.debug("[CardActions] Opening details modal", { |
|
|
|
function replacer(_key: string, value: any) { |
|
|
|
eventId: event.id, |
|
|
|
return value; |
|
|
|
title: event.title, |
|
|
|
} |
|
|
|
author: event.author |
|
|
|
// Stringify with 2-space indent |
|
|
|
|
|
|
|
let json = JSON.stringify(obj, replacer, 2); |
|
|
|
|
|
|
|
// Replace naddr addresses with links |
|
|
|
|
|
|
|
json = json.replace(NADDR_REGEX, (match) => { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
const [kind, pubkey, dtag] = match.split(":"); |
|
|
|
|
|
|
|
// Compose a fake event for naddrEncode |
|
|
|
|
|
|
|
const fakeEvent = { |
|
|
|
|
|
|
|
kind: parseInt(kind), |
|
|
|
|
|
|
|
pubkey, |
|
|
|
|
|
|
|
tags: [["d", dtag]], |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
const naddr = naddrEncode(fakeEvent as any, standardRelays); |
|
|
|
|
|
|
|
return `<a href='./events?id=${naddr}' class='text-primary-600 underline' target='_blank'>${match}</a>`; |
|
|
|
|
|
|
|
} catch { |
|
|
|
|
|
|
|
return match; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
// Escape < and > for HTML safety, but allow our <a> tags |
|
|
|
detailsModalOpen = true; |
|
|
|
json = json.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'); |
|
|
|
|
|
|
|
json = json.replace(/<a /g, '<a ').replace(/<\/a>/g, '</a>'); |
|
|
|
|
|
|
|
return json; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Log component initialization |
|
|
|
|
|
|
|
console.debug("[CardActions] Initialized", { |
|
|
|
|
|
|
|
eventId: event.id, |
|
|
|
|
|
|
|
kind: event.kind, |
|
|
|
|
|
|
|
pubkey: event.pubkey, |
|
|
|
|
|
|
|
title: event.title, |
|
|
|
|
|
|
|
author: event.author |
|
|
|
|
|
|
|
}); |
|
|
|
</script> |
|
|
|
</script> |
|
|
|
|
|
|
|
|
|
|
|
<div class="group bg-highlight dark:bg-primary-1000 rounded" role="group" onmouseenter={openPopover}> |
|
|
|
<div class="group bg-highlight dark:bg-primary-1000 rounded" role="group" onmouseenter={openPopover}> |
|
|
|
@ -142,22 +133,18 @@ |
|
|
|
</button> |
|
|
|
</button> |
|
|
|
</li> |
|
|
|
</li> |
|
|
|
<li> |
|
|
|
<li> |
|
|
|
<button class='btn-leather w-full text-left' onclick={shareNjump}> |
|
|
|
<CopyToClipboard |
|
|
|
{#if shareLinkCopied} |
|
|
|
displayText="Copy naddr address" |
|
|
|
<ClipboardCheckOutline class="inline mr-2" /> Copied! |
|
|
|
copyText={getIdentifier('naddr')} |
|
|
|
{:else} |
|
|
|
icon={ShareNodesOutline} |
|
|
|
<ShareNodesOutline class="inline mr-2" /> Share via NJump |
|
|
|
/> |
|
|
|
{/if} |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
</li> |
|
|
|
</li> |
|
|
|
<li> |
|
|
|
<li> |
|
|
|
<button class='btn-leather w-full text-left' onclick={copyEventId}> |
|
|
|
<CopyToClipboard |
|
|
|
{#if eventIdCopied} |
|
|
|
displayText="Copy nevent address" |
|
|
|
<ClipboardCheckOutline class="inline mr-2" /> Copied! |
|
|
|
copyText={getIdentifier('nevent')} |
|
|
|
{:else} |
|
|
|
icon={ClipboardCleanOutline} |
|
|
|
<ClipboardCleanOutline class="inline mr-2" /> Copy event ID |
|
|
|
/> |
|
|
|
{/if} |
|
|
|
|
|
|
|
</button> |
|
|
|
|
|
|
|
</li> |
|
|
|
</li> |
|
|
|
</ul> |
|
|
|
</ul> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
@ -214,7 +201,7 @@ |
|
|
|
<h5 class="text-sm">Identifier: {identifier}</h5> |
|
|
|
<h5 class="text-sm">Identifier: {identifier}</h5> |
|
|
|
{/if} |
|
|
|
{/if} |
|
|
|
<a |
|
|
|
<a |
|
|
|
href="/events?id={neventEncode(event, standardRelays)}" |
|
|
|
href="/events?id={neventEncode(event, activeRelays)}" |
|
|
|
class="mt-4 btn-leather text-center text-primary-700 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300 font-semibold" |
|
|
|
class="mt-4 btn-leather text-center text-primary-700 hover:text-primary-800 dark:text-primary-400 dark:hover:text-primary-300 font-semibold" |
|
|
|
> |
|
|
|
> |
|
|
|
View Event Details |
|
|
|
View Event Details |
|
|
|
|