diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index fd2f105..7ed067d 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -8,18 +8,30 @@ SidebarWrapper, Heading, CloseButton, + Textarea, + Popover, + P, + Modal, } from "flowbite-svelte"; import { getContext, onDestroy, onMount } from "svelte"; import { CloseOutline, ExclamationCircleOutline, + MessageDotsOutline, + FilePenOutline, + DotsVerticalOutline, + EyeOutline, + EyeSlashOutline, + ClipboardCleanOutline, + TrashBinOutline, } from "flowbite-svelte-icons"; import type { NDKEvent } from "@nostr-dev-kit/ndk"; import PublicationSection from "./PublicationSection.svelte"; import Details from "$components/util/Details.svelte"; + import CopyToClipboard from "$components/util/CopyToClipboard.svelte"; + import { neventEncode, naddrEncode } from "$lib/utils"; import { publicationColumnVisibility } from "$lib/stores"; import BlogHeader from "$components/cards/BlogHeader.svelte"; - import Interactions from "$components/util/Interactions.svelte"; import type { SveltePublicationTree } from "./svelte_publication_tree.svelte"; import TableOfContents from "./TableOfContents.svelte"; import type { TableOfContents as TocType } from "./table_of_contents.svelte"; @@ -27,15 +39,13 @@ import { deleteEvent } from "$lib/services/deletion"; import { getNdkContext, activeOutboxRelays } from "$lib/ndk"; import { goto } from "$app/navigation"; + import { getMatchingTags } from "$lib/utils/nostrUtils"; import HighlightLayer from "./HighlightLayer.svelte"; - import { EyeOutline, EyeSlashOutline } from "flowbite-svelte-icons"; - import HighlightButton from "./HighlightButton.svelte"; import HighlightSelectionHandler from "./HighlightSelectionHandler.svelte"; import CommentLayer from "./CommentLayer.svelte"; - import CommentButton from "./CommentButton.svelte"; import SectionComments from "./SectionComments.svelte"; - import { Textarea, P } from "flowbite-svelte"; import { userStore } from "$lib/stores/userStore"; + import CardActions from "$components/util/CardActions.svelte"; let { rootAddress, publicationType, indexEvent, publicationTree, toc } = $props<{ @@ -62,6 +72,10 @@ let isSubmittingArticleComment = $state(false); let articleCommentError = $state(null); let articleCommentSuccess = $state(false); + + // Publication header actions menu state + let publicationActionsOpen = $state(false); + let detailsModalOpen = $state(false); // Toggle between mock and real data for testing (DEBUG MODE) // Can be controlled via VITE_USE_MOCK_COMMENTS and VITE_USE_MOCK_HIGHLIGHTS environment variables @@ -240,6 +254,11 @@ publicationColumnVisibility.update((v) => ({ ...v, discussion: false })); } + function viewDetails() { + detailsModalOpen = true; + publicationActionsOpen = false; + } + function loadBlog(rootId: string) { // depending on the size of the screen, also toggle discussion visibility publicationColumnVisibility.update((current) => { @@ -445,6 +464,27 @@ if (isLeaf || isBlog) { publicationColumnVisibility.update((v) => ({ ...v, toc: false })); } + + // Set up the intersection observer. + observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if ( + entry.isIntersecting && + !isLoading && + !isDone && + publicationTree + ) { + loadMore(1); + } + }); + }, + { threshold: 0.5 }, + ); + + // AI-NOTE: Removed duplicate loadMore call + // Initial content loading is handled by the $effect that watches publicationTree + // This prevents duplicate loading when both onMount and $effect trigger // Set up the intersection observer. observer = new IntersectionObserver( @@ -467,6 +507,7 @@ // Initial content loading is handled by the $effect that watches publicationTree // This prevents duplicate loading when both onMount and $effect trigger + return () => { observer.disconnect(); }; @@ -484,7 +525,7 @@
@@ -505,14 +546,14 @@ }} /> -
+
-
+
{#if $publicationColumnVisibility.main}
@@ -520,12 +561,150 @@
+ + +
e.stopPropagation()} + onkeydown={(e) => e.stopPropagation()} + > +
(publicationActionsOpen = true)} + > + + + {#if publicationActionsOpen} + (publicationActionsOpen = false)} + > +
+
+
    +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • +
  • + +
  • + {#if $userStore.signedIn && $userStore.pubkey === indexEvent.pubkey} +
  • + +
  • + {/if} +
+
+
+
+ {/if} +
+
{#if publicationDeleted} @@ -557,39 +736,6 @@
- -
-
- - -
-
- - -
-
{#if showArticleCommentUI} @@ -647,6 +793,8 @@ {:else} {@const address = leaf.tagAddress()} + {@const publicationTitle = getMatchingTags(indexEvent, "title")[0]?.[1]} + {@const isFirstSection = i === 0} onPublicationSectionMounted(el, address)} /> {/if} @@ -861,3 +1011,11 @@ bind:comments {useMockComments} /> + + +
+ +
diff --git a/src/lib/components/publications/PublicationSection.svelte b/src/lib/components/publications/PublicationSection.svelte index 54cadaf..c273352 100644 --- a/src/lib/components/publications/PublicationSection.svelte +++ b/src/lib/components/publications/PublicationSection.svelte @@ -26,6 +26,8 @@ ref, allComments = [], commentsVisible = true, + publicationTitle, + isFirstSection = false, }: { address: string; rootAddress: string; @@ -35,6 +37,8 @@ ref: (ref: HTMLElement) => void; allComments?: NDKEvent[]; commentsVisible?: boolean; + publicationTitle?: string; + isFirstSection?: boolean; } = $props(); const asciidoctor: Asciidoctor = getContext("asciidoctor"); @@ -89,17 +93,32 @@ // AI-NOTE: Kind 30023 events contain Markdown content, not AsciiDoc // Use parseAdvancedmarkup for 30023 events, Asciidoctor for 30041/30818 events + let processed: string; if (event?.kind === 30023) { - return await parseAdvancedmarkup(content); + processed = await parseAdvancedmarkup(content); } else { // For 30041 and 30818 events, use Asciidoctor (AsciiDoc) const converted = asciidoctor.convert(content); - const processed = await postProcessAdvancedAsciidoctorHtml( + processed = await postProcessAdvancedAsciidoctorHtml( converted.toString(), ndk, ); - return processed; } + + // Remove redundant h1 title from first section if it matches publication title + if (isFirstSection && publicationTitle && typeof processed === 'string') { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = processed; + const h1Elements = tempDiv.querySelectorAll('h1'); + h1Elements.forEach((h1) => { + if (h1.textContent?.trim() === publicationTitle.trim()) { + h1.remove(); + } + }); + processed = tempDiv.innerHTML; + } + + return processed; }); let previousLeafEvent: NDKEvent | null = $derived.by(() => { @@ -224,7 +243,7 @@ -
+
{:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]} - -
- -
- {#await leafEvent then event} - {#if event} - - {/if} - {/await} -
+ +
{#each divergingBranches as [branch, depth]} {@render sectionHeading( getMatchingTags(branch, "title")[0]?.[1] ?? "", @@ -257,7 +264,21 @@ {/each} {#if leafTitle} {@const leafDepth = leafHierarchy.length - 1} - {@render sectionHeading(leafTitle, leafDepth)} +
+ +
+ {#await leafEvent then event} + {#if event} + + {/if} + {/await} +
+ {@render sectionHeading(leafTitle, leafDepth)} +
{/if} {@render contentParagraph( leafContent.toString(), @@ -267,7 +288,7 @@
-
+
- - {#await leafEvent then event} - {#if event} - - - {/if} - {/await}
+ + {#if depth === 2} + {@const rootEntry = toc.getRootEntry()} + {#if rootEntry} + {@const isVisible = isEntryVisible(rootEntry.address)} + { + const element = document.getElementById(rootEntry.address); + if (element) { + element.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + } + onClose?.(); + }} + > + + + {/if} + {/if} {#each entries as entry, index} {@const address = entry.address} {@const expanded = toc.expandedMap.get(address) ?? false} diff --git a/src/lib/components/util/ArticleNav.svelte b/src/lib/components/util/ArticleNav.svelte index 34d395d..bb19cc6 100644 --- a/src/lib/components/util/ArticleNav.svelte +++ b/src/lib/components/util/ArticleNav.svelte @@ -40,8 +40,10 @@ // Function to toggle column visibility function toggleColumn(column: "toc" | "blog" | "inner" | "discussion") { + console.log("[ArticleNav] toggleColumn called with:", column); publicationColumnVisibility.update((current) => { const newValue = !current[column]; + console.log("[ArticleNav] Toggling", column, "from", current[column], "to", newValue); const updated = { ...current, [column]: newValue }; if (window.innerWidth < 1400 && column === "blog" && newValue) { diff --git a/src/lib/components/util/CardActions.svelte b/src/lib/components/util/CardActions.svelte index 4b30246..11c7bdf 100644 --- a/src/lib/components/util/CardActions.svelte +++ b/src/lib/components/util/CardActions.svelte @@ -26,10 +26,16 @@ import { WebSocketPool } from "$lib/data_structures/websocket_pool"; // Component props - let { event, onDelete, sectionAddress } = $props<{ + let { + event, + onDelete, + sectionAddress, + detailsModalOpen = $bindable(false) + } = $props<{ event: NDKEvent; onDelete?: () => void; sectionAddress?: string; // If provided, shows "Comment on section" option + detailsModalOpen?: boolean; // Bindable prop to control modal from outside }>(); const ndk = getNdkContext(); @@ -72,8 +78,7 @@ event.tags.find((t: string[]) => t[0] === "identifier")?.[1] ?? null, ); - // UI state - let detailsModalOpen: boolean = $state(false); + // UI state - detailsModalOpen is now a bindable prop let isOpen: boolean = $state(false); // Comment modal state @@ -535,7 +540,7 @@

- Index author: {@render userBadge(event.pubkey, author, ndk)} + {event.kind === 30040 ? "Index author" : "Article author"}: {@render userBadge(event.pubkey, author, ndk)}

diff --git a/src/lib/components/util/Details.svelte b/src/lib/components/util/Details.svelte index 56c1a85..c8db27e 100644 --- a/src/lib/components/util/Details.svelte +++ b/src/lib/components/util/Details.svelte @@ -14,10 +14,11 @@ // isModal // - don't show interactions in modal view // - don't show all the details when _not_ in modal view - let { event, isModal = false, onDelete } = $props<{ + let { event, isModal = false, onDelete, hideActions = false } = $props<{ event: any; isModal?: boolean; onDelete?: () => void; + hideActions?: boolean; }>(); let title: string = $derived(getMatchingTags(event, "title")[0]?.[1]); @@ -66,7 +67,7 @@
- {#if !isModal} + {#if !isModal && !hideActions}

+ {:else if !isModal && hideActions} +
+ +

{@render userBadge(event.pubkey, undefined, ndk)}

+
{/if}
- + {title} {/snippet}