10 changed files with 474 additions and 142 deletions
@ -0,0 +1,206 @@ |
|||||||
|
<script lang="ts"> |
||||||
|
import { |
||||||
|
ClipboardCheckOutline, |
||||||
|
ClipboardCleanOutline, |
||||||
|
CodeOutline, |
||||||
|
DotsVerticalOutline, |
||||||
|
EyeOutline, |
||||||
|
ShareNodesOutline |
||||||
|
} from "flowbite-svelte-icons"; |
||||||
|
import { Button, Modal, Popover } from "flowbite-svelte"; |
||||||
|
import { standardRelays } from "$lib/consts"; |
||||||
|
import { neventEncode } from "$lib/utils"; |
||||||
|
import { type AddressPointer, naddrEncode } from "nostr-tools/nip19"; |
||||||
|
import InlineProfile from "$components/util/InlineProfile.svelte"; |
||||||
|
|
||||||
|
let { event } = $props(); |
||||||
|
|
||||||
|
let jsonModalOpen: boolean = $state(false); |
||||||
|
let detailsModalOpen: boolean = $state(false); |
||||||
|
let eventIdCopied: boolean = $state(false); |
||||||
|
let shareLinkCopied: boolean = $state(false); |
||||||
|
let title: string = $derived(event.getMatchingTags('title')[0]?.[1]); |
||||||
|
let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown'); |
||||||
|
let version: string = $derived(event.getMatchingTags('version')[0]?.[1] ?? '1'); |
||||||
|
let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null); |
||||||
|
let originalAuthor: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null); |
||||||
|
let summary: string = $derived(event.getMatchingTags('summary')[0]?.[1] ?? null); |
||||||
|
let type: string = $derived(event.getMatchingTags('type')[0]?.[1] ?? null); |
||||||
|
let language: string = $derived(event.getMatchingTags('l')[0]?.[1] ?? null); |
||||||
|
let source: string = $derived(event.getMatchingTags('source')[0]?.[1] ?? null); |
||||||
|
let publisher: string = $derived(event.getMatchingTags('published_by')[0]?.[1] ?? null); |
||||||
|
let identifier: string = $derived(event.getMatchingTags('i')[0]?.[1] ?? null); |
||||||
|
|
||||||
|
let isOpen = $state(false); |
||||||
|
|
||||||
|
function openPopover() { |
||||||
|
isOpen = true; |
||||||
|
} |
||||||
|
|
||||||
|
function closePopover() { |
||||||
|
isOpen = false; |
||||||
|
const menu = document.getElementById('dots-' + event.id); |
||||||
|
if (menu) menu.blur(); |
||||||
|
} |
||||||
|
|
||||||
|
function shareNjump() { |
||||||
|
const relays: string[] = standardRelays; |
||||||
|
const dTag : string | undefined = event.dTag; |
||||||
|
|
||||||
|
if (typeof dTag === 'string') { |
||||||
|
const opts: AddressPointer = { |
||||||
|
identifier: dTag, |
||||||
|
pubkey: event.pubkey, |
||||||
|
kind: 30040, |
||||||
|
relays |
||||||
|
}; |
||||||
|
const naddr = naddrEncode(opts); |
||||||
|
console.debug(naddr); |
||||||
|
navigator.clipboard.writeText(`https://njump.me/${naddr}`); |
||||||
|
shareLinkCopied = true; |
||||||
|
setTimeout(() => { |
||||||
|
shareLinkCopied = false; |
||||||
|
}, 4000); |
||||||
|
} |
||||||
|
|
||||||
|
else { |
||||||
|
console.log('dTag is undefined'); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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() { |
||||||
|
console.log('Details'); |
||||||
|
detailsModalOpen = true; |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="group" role="group" onmouseenter={openPopover}> |
||||||
|
<!-- Main button --> |
||||||
|
<Button type="button" |
||||||
|
id="dots-{event.id}" |
||||||
|
class=" hover:bg-primary-0 dark:text-highlight dark:hover:bg-primary-800 p-1 dots" color="none" |
||||||
|
data-popover-target="popover-actions"> |
||||||
|
<DotsVerticalOutline class="h-6 w-6"/> |
||||||
|
<span class="sr-only">Open actions menu</span> |
||||||
|
</Button> |
||||||
|
|
||||||
|
{#if isOpen} |
||||||
|
<Popover id="popover-actions" |
||||||
|
placement="bottom" |
||||||
|
trigger="click" |
||||||
|
class='popover-leather w-fit z-10' |
||||||
|
onmouseleave={closePopover} |
||||||
|
> |
||||||
|
<div class='flex flex-row justify-between space-x-4'> |
||||||
|
<div class='flex flex-col text-nowrap'> |
||||||
|
<ul class="space-y-2"> |
||||||
|
<li> |
||||||
|
<a href="" role="button" class='btn-leather' onclick={viewDetails}> |
||||||
|
<EyeOutline class="inline mr-2" /> View details |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li> |
||||||
|
<a role="button" class='btn-leather' onclick={shareNjump}> |
||||||
|
{#if shareLinkCopied} |
||||||
|
<ClipboardCheckOutline class="inline mr-2" /> Copied! |
||||||
|
{:else} |
||||||
|
<ShareNodesOutline class="inline mr-2" /> Share via NJump |
||||||
|
{/if} |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li> |
||||||
|
<a role="button" class='btn-leather' onclick={copyEventId}> |
||||||
|
{#if eventIdCopied} |
||||||
|
<ClipboardCheckOutline class="inline mr-2" /> Copied! |
||||||
|
{:else} |
||||||
|
<ClipboardCleanOutline class="inline mr-2" /> Copy event ID |
||||||
|
{/if} |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
<li> |
||||||
|
<a href="" role="button" class='btn-leather' onclick={viewJson}> |
||||||
|
<CodeOutline class="inline mr-2" /> View JSON |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Popover> |
||||||
|
{/if} |
||||||
|
<!-- Event JSON --> |
||||||
|
<Modal class='modal-leather' title='Event JSON' bind:open={jsonModalOpen} autoclose outsideclose size='sm'> |
||||||
|
<div class="overflow-auto bg-highlight dark:bg-primary-900 text-sm rounded p-1"> |
||||||
|
<code>{JSON.stringify(event.rawEvent())}</code> |
||||||
|
</div> |
||||||
|
</Modal> |
||||||
|
<!-- Event details --> |
||||||
|
<Modal class='modal-leather' title='Publication details' bind:open={detailsModalOpen} autoclose outsideclose size='sm'> |
||||||
|
<div class="flex flex-row space-x-4"> |
||||||
|
{#if image} |
||||||
|
<div class="flex col"> |
||||||
|
<img class="max-w-48" src={image} /> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
<div class="flex flex-col col space-y-5 justify-center align-middle"> |
||||||
|
<h1 class="text-3xl font-bold mt-5">{title}</h1> |
||||||
|
<h2 class="text-base font-bold">by |
||||||
|
{#if originalAuthor !== null} |
||||||
|
<InlineProfile pubkey={originalAuthor} title={author} /> |
||||||
|
{:else} |
||||||
|
{author} |
||||||
|
{/if} |
||||||
|
</h2> |
||||||
|
<h4 class='text-base font-thin mt-2'>Version: {version}</h4> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
{#if summary} |
||||||
|
<div class="flex flex-row "> |
||||||
|
<p class='text-base text-primary-900 dark:text-highlight'>{summary}</p> |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
|
||||||
|
<div class="flex flex-row "> |
||||||
|
<h4 class='text-base font-normal mt-2'>Index author: <InlineProfile pubkey={event.pubkey} /></h4> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="flex flex-col pb-4 space-y-1"> |
||||||
|
{#if source !== null} |
||||||
|
<h5 class="text-sm">Source: <a class="underline" href={source} target="_blank">{source}</a></h5> |
||||||
|
{/if} |
||||||
|
{#if type !== null} |
||||||
|
<h5 class="text-sm">Publication type: {type}</h5> |
||||||
|
{/if} |
||||||
|
{#if language !== null} |
||||||
|
<h5 class="text-sm">Language: {language}</h5> |
||||||
|
{/if} |
||||||
|
{#if publisher !== null} |
||||||
|
<h5 class="text-sm">Published by: {publisher}</h5> |
||||||
|
{/if} |
||||||
|
{#if identifier !== null} |
||||||
|
<h5 class="text-sm">{identifier}</h5> |
||||||
|
{/if} |
||||||
|
|
||||||
|
</div> |
||||||
|
|
||||||
|
</Modal> |
||||||
|
</div> |
||||||
@ -0,0 +1,27 @@ |
|||||||
|
<script lang='ts'> |
||||||
|
import { ClipboardCheckOutline, ClipboardCleanOutline } from "flowbite-svelte-icons"; |
||||||
|
|
||||||
|
let { displayText, copyText = displayText} = $props(); |
||||||
|
|
||||||
|
let copied: boolean = $state(false); |
||||||
|
|
||||||
|
async function copyToClipboard() { |
||||||
|
try { |
||||||
|
await navigator.clipboard.writeText(copyText); |
||||||
|
copied = true; |
||||||
|
setTimeout(() => { |
||||||
|
copied = false; |
||||||
|
}, 4000); |
||||||
|
} catch (err) { |
||||||
|
console.error("Failed to copy: ", err); |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<a role="button" class='btn-leather text-nowrap' onclick={copyToClipboard}> |
||||||
|
{#if copied} |
||||||
|
<ClipboardCheckOutline class="!fill-none dark:!fill-none inline mr-1" /> Copied! |
||||||
|
{:else} |
||||||
|
<ClipboardCleanOutline class="!fill-none dark:!fill-none inline mr-1" /> {displayText} |
||||||
|
{/if} |
||||||
|
</a> |
||||||
@ -0,0 +1,59 @@ |
|||||||
|
<script lang='ts'> |
||||||
|
import { Avatar } from 'flowbite-svelte'; |
||||||
|
import { type NDKUserProfile } from "@nostr-dev-kit/ndk"; |
||||||
|
import { ndkInstance } from '$lib/ndk'; |
||||||
|
|
||||||
|
let { pubkey, title = null } = $props(); |
||||||
|
|
||||||
|
const externalProfileDestination = 'https://njump.me/' |
||||||
|
let loading = $state(true); |
||||||
|
let anon = $state(false); |
||||||
|
let npub = $state(''); |
||||||
|
|
||||||
|
let profile = $state<NDKUserProfile | null>(null); |
||||||
|
let pfp = $derived(profile?.image); |
||||||
|
let username = $derived(profile?.name); |
||||||
|
|
||||||
|
async function fetchUserData(pubkey: string) { |
||||||
|
let user; |
||||||
|
user = $ndkInstance |
||||||
|
.getUser({ pubkey: pubkey ?? undefined }); |
||||||
|
|
||||||
|
npub = user.npub; |
||||||
|
|
||||||
|
user.fetchProfile() |
||||||
|
.then(userProfile => { |
||||||
|
profile = userProfile; |
||||||
|
if (!profile?.name) anon = true; |
||||||
|
loading = false; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
// Fetch data when component mounts |
||||||
|
$effect(() => { |
||||||
|
if (pubkey) { |
||||||
|
fetchUserData(pubkey); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
function shortenNpub(long: string|undefined) { |
||||||
|
if (!long) return ''; |
||||||
|
return long.slice(0, 8) + '…' + long.slice(-4); |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
{#if loading} |
||||||
|
{title ?? '…'} |
||||||
|
{:else if anon } |
||||||
|
<a class='underline' href='{externalProfileDestination}{npub}' title={title ?? npub} target='_blank'>{shortenNpub(npub)}</a> |
||||||
|
{:else if npub } |
||||||
|
<a href='{externalProfileDestination}{npub}' title={title ?? username} target='_blank'> |
||||||
|
<Avatar rounded |
||||||
|
class='h-6 w-6 mx-1 cursor-pointer inline' |
||||||
|
src={pfp} |
||||||
|
alt={username} /> |
||||||
|
<span class='underline'>{username ?? shortenNpub(npub)}</span> |
||||||
|
</a> |
||||||
|
{:else} |
||||||
|
{title ?? pubkey} |
||||||
|
{/if} |
||||||
@ -0,0 +1,101 @@ |
|||||||
|
<script lang='ts'> |
||||||
|
import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; |
||||||
|
import { logout, ndkInstance } from '$lib/ndk'; |
||||||
|
import { ArrowRightToBracketOutline, UserOutline, FileSearchOutline } from "flowbite-svelte-icons"; |
||||||
|
import { Avatar, Popover } from "flowbite-svelte"; |
||||||
|
import type { NDKUserProfile } from "@nostr-dev-kit/ndk"; |
||||||
|
|
||||||
|
const externalProfileDestination = 'https://njump.me/' |
||||||
|
|
||||||
|
let { pubkey, isNav = false } = $props(); |
||||||
|
|
||||||
|
let profile = $state<NDKUserProfile | null>(null); |
||||||
|
let pfp = $derived(profile?.image); |
||||||
|
let username = $derived(profile?.name); |
||||||
|
let tag = $derived(profile?.name); |
||||||
|
let npub = $state<string | undefined >(undefined); |
||||||
|
|
||||||
|
$effect(() => { |
||||||
|
const user = $ndkInstance |
||||||
|
.getUser({ pubkey: pubkey ?? undefined }); |
||||||
|
|
||||||
|
npub = user.npub; |
||||||
|
|
||||||
|
user.fetchProfile() |
||||||
|
.then(userProfile => { |
||||||
|
profile = userProfile; |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
async function handleSignOutClick() { |
||||||
|
logout($ndkInstance.activeUser!); |
||||||
|
profile = null; |
||||||
|
} |
||||||
|
|
||||||
|
function shortenNpub(long: string|undefined) { |
||||||
|
if (!long) return ''; |
||||||
|
return long.slice(0, 8) + '…' + long.slice(-4); |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<div class="relative"> |
||||||
|
{#if profile} |
||||||
|
<div class="group"> |
||||||
|
<Avatar |
||||||
|
rounded |
||||||
|
class='h-6 w-6 cursor-pointer' |
||||||
|
src={pfp} |
||||||
|
alt={username} |
||||||
|
/> |
||||||
|
{#key username || tag} |
||||||
|
<Popover |
||||||
|
target="avatar" |
||||||
|
class='popover-leather w-[180px]' |
||||||
|
trigger='hover' |
||||||
|
> |
||||||
|
<div class='flex flex-row justify-between space-x-4'> |
||||||
|
<div class='flex flex-col'> |
||||||
|
{#if username} |
||||||
|
<h3 class='text-lg font-bold'>{username}</h3> |
||||||
|
{#if isNav}<h4 class='text-base'>@{tag}</h4>{/if} |
||||||
|
{/if} |
||||||
|
<ul class="space-y-2 mt-2"> |
||||||
|
<li> |
||||||
|
<CopyToClipboard displayText={shortenNpub(npub)} copyText={npub} /> |
||||||
|
</li> |
||||||
|
<li> |
||||||
|
<a class='hover:text-primary-400 dark:hover:text-primary-500 text-nowrap mt-3 m-0' href='{externalProfileDestination}{npub}' target='_blank'> |
||||||
|
<UserOutline class='mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none' /><span class='underline'>View profile</span> |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{#if isNav} |
||||||
|
<li> |
||||||
|
<a |
||||||
|
href="" |
||||||
|
id='sign-out-button' |
||||||
|
class='btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500' |
||||||
|
onclick={handleSignOutClick} |
||||||
|
role="button" |
||||||
|
> |
||||||
|
<ArrowRightToBracketOutline class='mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none' /> Sign out |
||||||
|
</a> |
||||||
|
</li> |
||||||
|
{:else} |
||||||
|
<!-- li> |
||||||
|
<a |
||||||
|
href="" |
||||||
|
class='btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500' |
||||||
|
role="button" |
||||||
|
> |
||||||
|
<FileSearchOutline class='mr-1 !h-6 inline !fill-none dark:!fill-none' /> More content |
||||||
|
</a> |
||||||
|
</li --> |
||||||
|
{/if} |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</Popover> |
||||||
|
{/key} |
||||||
|
</div> |
||||||
|
{/if} |
||||||
|
</div> |
||||||
Loading…
Reference in new issue