Browse Source

closes #77 closes #189 closes #133 available actions moved to ... menu, profiles, details

master
Nuša Pukšič 1 year ago
parent
commit
a6edcdfda3
  1. 2
      src/app.css
  2. 60
      src/lib/components/Login.svelte
  3. 101
      src/lib/components/PublicationHeader.svelte
  4. 194
      src/lib/components/util/CardActions.svelte
  5. 34
      src/lib/components/util/CopyToClipboard.svelte
  6. 6
      src/lib/components/util/InlineProfile.svelte
  7. 101
      src/lib/components/util/Profile.svelte
  8. 19
      src/styles/publications.css

2
src/app.css

@ -106,7 +106,7 @@
/* Modal */ /* Modal */
div.modal-leather > div { div.modal-leather > div {
@apply bg-primary-0 dark:bg-primary-1000 border-b-[1px] border-gray-800 dark:border-gray-500; @apply bg-primary-0 dark:bg-primary-1000 border-b-[1px] border-primary-100 dark:border-primary-600;
} }
div.modal-leather > div > h1, div.modal-leather > div > h1,

60
src/lib/components/Login.svelte

@ -2,8 +2,7 @@
import { type NDKUserProfile } from '@nostr-dev-kit/ndk'; import { type NDKUserProfile } from '@nostr-dev-kit/ndk';
import { activePubkey, loginWithExtension, logout, ndkInstance, ndkSignedIn, persistLogin } from '$lib/ndk'; import { activePubkey, loginWithExtension, logout, ndkInstance, ndkSignedIn, persistLogin } from '$lib/ndk';
import { Avatar, Button, Popover, Tooltip } from 'flowbite-svelte'; import { Avatar, Button, Popover, Tooltip } from 'flowbite-svelte';
import { ArrowRightToBracketOutline } from 'flowbite-svelte-icons'; import Profile from "$components/util/Profile.svelte";
import CopyToClipboard from "$components/util/CopyToClipboard.svelte";
let profile = $state<NDKUserProfile | null>(null); let profile = $state<NDKUserProfile | null>(null);
let pfp = $derived(profile?.image); let pfp = $derived(profile?.image);
@ -11,8 +10,6 @@
let tag = $derived(profile?.name); let tag = $derived(profile?.name);
let npub = $state<string | undefined >(undefined); let npub = $state<string | undefined >(undefined);
const externalProfileDestination = 'https://nostree.me/'
let signInFailed = $state<boolean>(false); let signInFailed = $state<boolean>(false);
$effect(() => { $effect(() => {
@ -43,61 +40,13 @@
} }
} }
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> </script>
<div class="m-4">
{#if $ndkSignedIn} {#if $ndkSignedIn}
<Avatar <Profile pubkey={$activePubkey} isNav={true} />
rounded
class='h-6 w-6 m-4 cursor-pointer'
src={pfp}
alt={username}
/>
{#key username || tag}
<Popover
class='popover-leather w-fit'
placement='bottom'
target='avatar'
>
<div class='flex flex-row justify-between space-x-4'>
<div class='flex flex-col'>
<h3 class='text-lg font-bold'>{username}</h3>
<h4 class='text-base'>@{tag}</h4>
<CopyToClipboard displayText={shortenNpub(npub)} copyText={npub} />
<a class='text-indigo-600 underline' href='{externalProfileDestination}{npub}' target='_blank'>View profile</a>
</div>
<div class='flex flex-col justify-center'>
<Button
id='sign-out-button'
class='btn-leather !p-2 hover:text-primary-400 dark:hover:text-primary-500 hover:border-primary-400 dark:hover:border-primary-500'
pill
outline
color='alternative'
onclick={handleSignOutClick}
>
<ArrowRightToBracketOutline class='w-4 h-4 !fill-none dark:!fill-none' />
<Tooltip
class='tooltip-leather w-fit whitespace-nowrap'
triggeredBy='#sign-out-button'
placement='bottom'
>
Sign out
</Tooltip>
</Button>
</div>
</div>
</Popover>
{/key}
{:else} {:else}
<Avatar rounded class='h-6 w-6 m-4 cursor-pointer' id='avatar' /> <Avatar rounded class='h-6 w-6 cursor-pointer' id='avatar' />
<Popover <Popover
class='popover-leather w-fit' class='popover-leather w-fit'
placement='bottom' placement='bottom'
@ -118,3 +67,4 @@
</div> </div>
</Popover> </Popover>
{/if} {/if}
</div>

101
src/lib/components/PublicationHeader.svelte

@ -2,10 +2,11 @@
import { ndkInstance } from '$lib/ndk'; import { ndkInstance } from '$lib/ndk';
import { neventEncode } from '$lib/utils'; import { neventEncode } from '$lib/utils';
import type { NDKEvent } from '@nostr-dev-kit/ndk'; import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { naddrEncode, type AddressPointer } from 'nostr-tools/nip19';
import { standardRelays } from '../consts'; import { standardRelays } from '../consts';
import { Card, Button, Modal, Tooltip } from 'flowbite-svelte'; import { Card, Img } from "flowbite-svelte";
import { ClipboardCheckOutline, ClipboardCleanOutline, CodeOutline, ShareNodesOutline } from 'flowbite-svelte-icons'; import CardActions from "$components/util/CardActions.svelte";
import Profile from "$components/util/Profile.svelte";
import InlineProfile from "$components/util/InlineProfile.svelte";
const { event } = $props<{ event: NDKEvent }>(); const { event } = $props<{ event: NDKEvent }>();
@ -25,83 +26,41 @@
let title: string = $derived(event.getMatchingTags('title')[0]?.[1]); let title: string = $derived(event.getMatchingTags('title')[0]?.[1]);
let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown'); let author: string = $derived(event.getMatchingTags('author')[0]?.[1] ?? 'unknown');
let version: string = $derived(event.getMatchingTags('version')[0]?.[1] ?? '1'); let version: string = $derived(event.getMatchingTags('version')[0]?.[1] ?? '1');
let image: string = $derived(event.getMatchingTags('image')[0]?.[1] ?? null);
let eventIdCopied: boolean = $state(false); let authorPubkey: string = $derived(event.getMatchingTags('p')[0]?.[1] ?? null);
let jsonModalOpen: boolean = $state(false);
let shareLinkCopied: boolean = $state(false);
function copyEventId() {
console.debug("copyEventID");
const relays: string[] = standardRelays;
const nevent = neventEncode(event, relays);
navigator.clipboard.writeText(nevent);
eventIdCopied = true;
}
function viewJson() {
console.debug("viewJSON");
jsonModalOpen = true;
}
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;
}
else {
console.log('dTag is undefined');
}
}
</script> </script>
{#if title != null && href != null} {#if title != null && href != null}
<Card class='ArticleBox card-leather w-lg'> <Card class='ArticleBox card-leather w-lg flex flex-row space-x-2'>
<div class='flex flex-col space-y-8'> {#if image != null}
<a href="/{href}" class='flex flex-col space-y-2'> <div class="col justify-center align-middle">
<h2 class='text-lg font-bold'>{title}</h2> <Img src={image} class="rounded max-h-36 max-w-36"/>
<h3 class='text-base font-normal'>by {author}</h3> </div>
{#if version != '1'}
<h3 class='text-base font-normal'>version: {version}</h3>
{/if} {/if}
</a> <div class='col flex flex-row flex-grow space-x-4'>
<div class='w-full flex space-x-2 justify-end'> <div class="flex flex-col flex-grow">
<Button class='btn-leather' size='sm' onclick={shareNjump}><ShareNodesOutline /></Button> <a href="/{href}" class='flex flex-col space-y-2'>
<Tooltip class='tooltip-leather' type='auto' placement='top' on:show={() => shareLinkCopied = false}> <h2 class='text-lg font-bold line-clamp-2' title="{title}">{title}</h2>
{#if shareLinkCopied} <h3 class='text-base font-normal'>
<ClipboardCheckOutline /> by
{#if authorPubkey != null}
<InlineProfile pubkey={authorPubkey} title={author} />
{:else} {:else}
Share via NJump {author}
{/if} {/if}
</Tooltip> </h3>
<Button class='btn-leather' size='sm' outline onclick={copyEventId}><ClipboardCleanOutline /></Button> {#if version != '1'}
<Tooltip class='tooltip-leather' type='auto' placement='top' on:show={() => eventIdCopied = false}> <h3 class='text-base font-thin'>version: {version}</h3>
{#if eventIdCopied}
<ClipboardCheckOutline />
{:else}
Copy event ID
{/if} {/if}
</Tooltip> </a>
<Button class='btn-leather' size='sm' outline onclick={viewJson}><CodeOutline /></Button> </div>
<Tooltip class='tooltip-leather' type='auto' placement='top'>View JSON</Tooltip> <div class="flex flex-col justify-between items-center">
<CardActions event={event} />
<div class="group mt-16">
<Profile pubkey={event.pubkey} />
</div>
</div> </div>
</div> </div>
<Modal class='modal-leather' title='Event JSON' bind:open={jsonModalOpen} autoclose outsideclose size='sm'>
<code>{JSON.stringify(event.rawEvent())}</code>
</Modal>
</Card> </Card>
{/if} {/if}

194
src/lib/components/util/CardActions.svelte

@ -0,0 +1,194 @@
<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 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 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>
<li>
<a href="" role="button" class='btn-leather' onclick={viewDetails}>
<EyeOutline class="inline mr-2" /> View details
</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">
{#if type !== null}
<h5 class="text-sm">Publication type: {type}</h5>
{/if}
{#if language !== null}
<h5 class="text-sm">Language: {language}</h5>
{/if}
</div>
</Modal>
</div>

34
src/lib/components/util/CopyToClipboard.svelte

@ -1,29 +1,27 @@
<script lang='ts'> <script lang='ts'>
import { Tooltip } from 'flowbite-svelte'; import { ClipboardCheckOutline, ClipboardCleanOutline } from "flowbite-svelte-icons";
import { FileCopyOutline } from 'flowbite-svelte-icons';
export let displayText = ""; // Shown in UI let { displayText, copyText = displayText} = $props();
export let copyText = displayText; // Copied to clipboard (default to same if not provided)
let copied: boolean = $state(false);
async function copyToClipboard() { async function copyToClipboard() {
try { try {
await navigator.clipboard.writeText(copyText); await navigator.clipboard.writeText(copyText);
copied = true;
setTimeout(() => {
copied = false;
}, 4000);
} catch (err) { } catch (err) {
console.error("Failed to copy: ", err); console.error("Failed to copy: ", err);
} }
} }
</script> </script>
<div role='tooltip'> <a role="button" class='btn-leather text-nowrap' onclick={copyToClipboard}>
<span> {#if copied}
{displayText} <button class='btn-leather' id='copy-button' onclick={copyToClipboard}><FileCopyOutline class='w-4 h-4 !fill-none dark:!fill-none' /></button> <ClipboardCheckOutline class="!fill-none dark:!fill-none inline mr-1" /> Copied!
</span> {:else}
<Tooltip <ClipboardCleanOutline class="!fill-none dark:!fill-none inline mr-1" /> {displayText}
class='tooltip-leather' {/if}
triggeredBy='#copy-button' </a>
trigger='click'
placement='bottom'
type="auto"
>
Copied!
</Tooltip>
</div>

6
src/lib/components/util/InlineProfile.svelte

@ -3,9 +3,9 @@
import { type NDKUserProfile } from '@nostr-dev-kit/ndk'; import { type NDKUserProfile } from '@nostr-dev-kit/ndk';
import { ndkInstance } from '$lib/ndk'; import { ndkInstance } from '$lib/ndk';
let { pubkey } = $props(); let { pubkey, title = null } = $props();
const externalProfileDestination = 'https://nostree.me/' const externalProfileDestination = 'https://njump.me/'
let loading = $state(true); let loading = $state(true);
let npub = $state(''); let npub = $state('');
@ -43,7 +43,7 @@
class='h-6 w-6 mx-1 cursor-pointer inline' class='h-6 w-6 mx-1 cursor-pointer inline'
src={pfp} src={pfp}
alt={username} /> alt={username} />
<a class='text-indigo-600 underline' href='{externalProfileDestination}{npub}' target='_blank'>{username}</a> <a class='underline' href='{externalProfileDestination}{npub}' title={title ?? username} target='_blank'>{username}</a>
{:else} {:else}
<span>Not found</span> <span>Not found</span>
{/if} {/if}

101
src/lib/components/util/Profile.svelte

@ -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>

19
src/styles/publications.css

@ -90,7 +90,7 @@
.videoblock .title, .videoblock .title,
.olist .title, .olist .title,
.ulist .title { .ulist .title {
@apply my-2; @apply my-2 font-thin text-lg;
} }
.note-leather li p { .note-leather li p {
@ -115,6 +115,10 @@
@apply text-sm; @apply text-sm;
} }
.leading-normal.first-letter\:text-7xl .quoteblock {
min-height: 108px;
}
/* admonition */ /* admonition */
.note-leather .admonitionblock .title { .note-leather .admonitionblock .title {
@apply font-semibold; @apply font-semibold;
@ -128,6 +132,10 @@
@apply flex flex-col; @apply flex flex-col;
} }
.note-leather .admonitionblock p:has(code) {
@apply my-3;
}
.note-leather .admonitionblock { .note-leather .admonitionblock {
@apply rounded overflow-hidden border; @apply rounded overflow-hidden border;
} }
@ -212,4 +220,13 @@
.videoblock .content video { .videoblock .content video {
@apply w-full h-full; @apply w-full h-full;
} }
/* audio */
.audioblock .content {
@apply my-3;
}
.audioblock .content audio {
@apply w-full;
}
} }
Loading…
Cancel
Save