From e6b4738f980428bdef453665fea24323119ff633 Mon Sep 17 00:00:00 2001 From: limina1 Date: Thu, 6 Nov 2025 10:51:14 -0500 Subject: [PATCH] Integrate highlights, comments, and delete functionality into UI --- src/lib/components/cards/BlogHeader.svelte | 37 +- .../publications/Publication.svelte | 315 +++++++++++++- .../publications/PublicationHeader.svelte | 33 +- .../publications/PublicationSection.svelte | 168 ++++++-- src/lib/components/util/CardActions.svelte | 390 +++++++++++++++++- src/lib/components/util/Details.svelte | 11 +- src/lib/utils/asciidoc_ast_parser.ts | 127 ++++-- src/lib/utils/publication_tree_factory.ts | 46 ++- src/lib/utils/publication_tree_processor.ts | 153 +++++-- 9 files changed, 1173 insertions(+), 107 deletions(-) diff --git a/src/lib/components/cards/BlogHeader.svelte b/src/lib/components/cards/BlogHeader.svelte index 3df794a..80de2e7 100644 --- a/src/lib/components/cards/BlogHeader.svelte +++ b/src/lib/components/cards/BlogHeader.svelte @@ -10,7 +10,8 @@ import LazyImage from "$components/util/LazyImage.svelte"; import { generateDarkPastelColor } from "$lib/utils/image_utils"; import { getNdkContext } from "$lib/ndk"; - + import { deleteEvent } from "$lib/services/deletion"; + const { rootId, event, @@ -25,6 +26,38 @@ const ndk = getNdkContext(); + /** + * Handle deletion of this blog article + */ + async function handleDelete() { + const confirmed = confirm( + "Are you sure you want to delete this article? This action will publish a deletion request to all relays." + ); + + if (!confirmed) return; + + try { + await deleteEvent({ + eventAddress: event.tagAddress(), + eventKind: event.kind, + reason: "User deleted article", + onSuccess: (deletionEventId) => { + console.log("[BlogHeader] Deletion event published:", deletionEventId); + // Call onBlogUpdate if provided to refresh the list + if (onBlogUpdate) { + onBlogUpdate(); + } + }, + onError: (error) => { + console.error("[BlogHeader] Deletion failed:", error); + alert(`Failed to delete article: ${error}`); + }, + }, ndk); + } catch (error) { + console.error("[BlogHeader] Deletion error:", error); + } + } + let title: string = $derived(event.getMatchingTags("title")[0]?.[1]); let author: string = $derived( getMatchingTags(event, "author")[0]?.[1] ?? "unknown", @@ -106,7 +139,7 @@
- +
diff --git a/src/lib/components/publications/Publication.svelte b/src/lib/components/publications/Publication.svelte index 37df63b..7aed69d 100644 --- a/src/lib/components/publications/Publication.svelte +++ b/src/lib/components/publications/Publication.svelte @@ -24,6 +24,17 @@ import TableOfContents from "./TableOfContents.svelte"; import type { TableOfContents as TocType } from "./table_of_contents.svelte"; import ArticleNav from "$components/util/ArticleNav.svelte"; + import { deleteEvent } from "$lib/services/deletion"; + import { getNdkContext, activeOutboxRelays } from "$lib/ndk"; + import { goto } from "$app/navigation"; + 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 { Textarea, P } from "flowbite-svelte"; + import { userStore } from "$lib/stores/userStore"; let { rootAddress, publicationType, indexEvent, publicationTree, toc } = $props<{ rootAddress: string; @@ -33,6 +44,59 @@ toc: TocType; }>(); + const ndk = getNdkContext(); + + // Highlight layer state + let highlightsVisible = $state(false); + let highlightLayerRef: any = null; + let publicationContentRef: HTMLElement | null = $state(null); + + // Comment layer state + let commentsVisible = $state(true); + let comments = $state([]); + let commentLayerRef: any = null; + let showArticleCommentUI = $state(false); + let articleCommentContent = $state(""); + let isSubmittingArticleComment = $state(false); + let articleCommentError = $state(null); + let articleCommentSuccess = $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 + let useMockComments = $state(import.meta.env.VITE_USE_MOCK_COMMENTS === "true"); + let useMockHighlights = $state(import.meta.env.VITE_USE_MOCK_HIGHLIGHTS === "true"); + + // Log initial state for debugging + console.log('[Publication] Mock data initialized:', { + useMockComments, + useMockHighlights, + envVars: { + VITE_USE_MOCK_COMMENTS: import.meta.env.VITE_USE_MOCK_COMMENTS, + VITE_USE_MOCK_HIGHLIGHTS: import.meta.env.VITE_USE_MOCK_HIGHLIGHTS, + } + }); + + // Derive all event IDs and addresses for highlight fetching + let allEventIds = $derived.by(() => { + const ids = [indexEvent.id]; + leaves.forEach(leaf => { + if (leaf?.id) ids.push(leaf.id); + }); + return ids; + }); + + let allEventAddresses = $derived.by(() => { + const addresses = [rootAddress]; + leaves.forEach(leaf => { + if (leaf) { + const addr = leaf.tagAddress(); + if (addr) addresses.push(addr); + } + }); + return addresses; + }); + + // #region Loading let leaves = $state>([]); let isLoading = $state(false); @@ -41,6 +105,8 @@ let activeAddress = $state(null); let loadedAddresses = $state>(new Set()); let hasInitialized = $state(false); + let highlightModeActive = $state(false); + let publicationDeleted = $state(false); let observer: IntersectionObserver; @@ -184,6 +250,121 @@ return currentBlog && currentBlogEvent && window.innerWidth < 1140; } + function toggleHighlights() { + highlightsVisible = !highlightsVisible; + } + + function toggleComments() { + commentsVisible = !commentsVisible; + } + + function handleCommentPosted() { + console.log("[Publication] Comment posted, refreshing comment layer"); + // Refresh the comment layer after a short delay to allow relay indexing + setTimeout(() => { + if (commentLayerRef) { + commentLayerRef.refresh(); + } + }, 500); + } + + async function submitArticleComment() { + if (!articleCommentContent.trim()) { + articleCommentError = "Comment cannot be empty"; + return; + } + + isSubmittingArticleComment = true; + articleCommentError = null; + articleCommentSuccess = false; + + try { + // Parse the root address to get event details + const parts = rootAddress.split(":"); + if (parts.length !== 3) { + throw new Error("Invalid address format"); + } + + const [kindStr, authorPubkey, dTag] = parts; + const kind = parseInt(kindStr); + + // Create comment event (kind 1111) + const commentEvent = new (await import("@nostr-dev-kit/ndk")).NDKEvent(ndk); + commentEvent.kind = 1111; + commentEvent.content = articleCommentContent; + + // Get relay hint + const relayHint = $activeOutboxRelays[0] || ""; + + // Add tags following NIP-22 + commentEvent.tags = [ + ["A", rootAddress, relayHint, authorPubkey], + ["K", kind.toString()], + ["P", authorPubkey, relayHint], + ["a", rootAddress, relayHint], + ["k", kind.toString()], + ["p", authorPubkey, relayHint], + ]; + + // Sign and publish + await commentEvent.sign(); + await commentEvent.publish(); + + console.log("[Publication] Article comment published:", commentEvent.id); + + articleCommentSuccess = true; + articleCommentContent = ""; + + // Close UI and refresh after delay + setTimeout(() => { + showArticleCommentUI = false; + articleCommentSuccess = false; + handleCommentPosted(); + }, 1500); + + } catch (err) { + console.error("[Publication] Error posting article comment:", err); + articleCommentError = err instanceof Error ? err.message : "Failed to post comment"; + } finally { + isSubmittingArticleComment = false; + } + } + + /** + * Handles deletion of the entire publication + */ + async function handleDeletePublication() { + const confirmed = confirm( + "Are you sure you want to delete this entire publication? This action will publish a deletion request to all relays." + ); + + if (!confirmed) return; + + try { + await deleteEvent({ + eventAddress: indexEvent.tagAddress(), + eventKind: indexEvent.kind, + reason: "User deleted publication", + onSuccess: (deletionEventId) => { + console.log("[Publication] Deletion event published:", deletionEventId); + publicationDeleted = true; + + // Redirect after 2 seconds + setTimeout(() => { + goto("/publications"); + }, 2000); + }, + onError: (error) => { + console.error("[Publication] Failed to delete publication:", error); + alert(`Failed to delete publication: ${error}`); + }, + }); + } catch (error) { + console.error("[Publication] Error deleting publication:", error); + alert(`Error: ${error}`); + } + } + // #endregion /** @@ -249,6 +430,13 @@ }; }); + // Setup highlight layer container reference + $effect(() => { + if (publicationContentRef && highlightLayerRef) { + highlightLayerRef.setContainer(publicationContentRef); + } + }); + // #endregion @@ -260,6 +448,23 @@ rootId={indexEvent.id} indexEvent={indexEvent} /> + + + + { + highlightModeActive = false; + // Refresh highlights after a short delay to allow relay indexing + setTimeout(() => { + if (highlightLayerRef) { + console.log("[Publication] Refreshing highlights after creation"); + highlightLayerRef.refresh(); + } + }, 500); + }} + />
@@ -299,12 +504,95 @@ {#if $publicationColumnVisibility.main} -
+
-
+
+
+ + {#if publicationDeleted} + + + Publication deleted. Redirecting to publications page... + + {/if} + + +
+
+ + +
+
+ + +
+ + + {#if showArticleCommentUI} +
+
+

Comment on Article

+ +