@ -2,6 +2,7 @@
import {
import {
Alert,
Alert,
Button,
Button,
Card,
Sidebar,
Sidebar,
SidebarGroup,
SidebarGroup,
SidebarItem,
SidebarItem,
@ -9,21 +10,31 @@
Skeleton,
Skeleton,
TextPlaceholder,
TextPlaceholder,
Tooltip,
Tooltip,
Heading,
} from "flowbite-svelte";
} from "flowbite-svelte";
import { getContext , onMount } from "svelte";
import { getContext , onDestroy , onMount } from "svelte";
import { BookOutline , ExclamationCircleOutline } from "flowbite-svelte-icons";
import {
CloseOutline,
BookOutline,
ExclamationCircleOutline,
} from "flowbite-svelte-icons";
import { page } from "$app/state";
import { page } from "$app/state";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import PublicationSection from "./PublicationSection.svelte";
import PublicationSection from "./PublicationSection.svelte";
import type { PublicationTree } from "$lib/data_structures/publication_tree";
import type { PublicationTree } from "$lib/data_structures/publication_tree";
import Details from "$components/util/Details.svelte";
import { publicationColumnVisibility } from "$lib/stores";
import BlogHeader from "$components/blog/BlogHeader.svelte";
import Interactions from "$components/util/Interactions.svelte";
import TocToggle from "$components/util/TocToggle.svelte";
let { rootAddress , publicationType , indexEvent } = $props< {
let { rootAddress , publicationType , indexEvent } = $props< {
rootAddress: string,
rootAddress: string;
publicationType: string,
publicationType: string;
indexEvent: NDKEvent
indexEvent: NDKEvent;
}>();
}>();
const publicationTree = getContext('publicationTree' ) as PublicationTree;
const publicationTree = getContext("publicationTree" ) as PublicationTree;
// #region Loading
// #region Loading
@ -76,135 +87,101 @@
// #endregion
// #endregion
// #region ToC
// region Columns visibility
let currentBlog: null | string = $state(null);
const tocBreakpoint = 1140;
let currentBlogEvent: null | NDKEvent = $state(null);
const isLeaf = $derived(indexEvent.kind === 30041);
let activeHash = $state(page.url.hash);
let currentBlog: null|string = $state(null);
function isInnerActive() {
return currentBlog !== null & & $publicationColumnVisibility.inner;
function isDefaultVisible() {
if (publicationType !== 'blog') {
return true;
} else {
return $publicationColumnVisibility.blog;
}
}
function closeDiscussion() {
publicationColumnVisibility.update((v) => ({ ... v , discussion : false } ));
}
}
function loadBlog(rootId: string) {
function loadBlog(rootId: string) {
// depending on the size of the screen, also toggle blog list visibility
// depending on the size of the screen, also toggle blog list & discussion visibility
publicationColumnVisibility.update((current) => {
const updated = current;
if (window.innerWidth < 1024 ) {
if (window.innerWidth < 1024 ) {
$publicationColumnVisibility.blog = false;
updated.blog = false;
updated.discussion = false;
}
}
$publicationColumnVisibility.inner = true;
updated.inner = true;
return updated;
});
currentBlog = rootId;
currentBlog = rootId;
// set current blog values for publication render
currentBlogEvent =
leaves.find((i) => i.tagAddress() === currentBlog) ?? null;
}
}
< / script >
{ #if $publicationColumnVisibility . details }
function showBlogHeader() {
< div class = "flex flex-col space-y-4 max-w-xl flex-grow-1 p-2 bg-highlight" >
return currentBlog & & currentBlogEvent & & window.innerWidth < 1140 ;
< Details event = { indexEvent } / >
}
< / div >
{ /if }
{ #if isDefaultVisible ()}
< div class = "flex flex-col space-y-4 overflow-auto
{ publicationType === 'blog' ? 'max-w-xl flex-grow-1' : 'max-w-2xl flex-grow-2' }
{ currentBlog !== null ? 'discreet' : '' }
">
< Preview { rootId } { publicationType } index = { 0 } onBlogUpdate= { loadBlog } />
< / div >
{ /if }
{ #if currentBlog !== null && $publicationColumnVisibility . inner }
{ #key currentBlog }
< div class = "flex flex-col space-y-4 max-w-3xl overflow-auto flex-grow-2" >
< Preview rootId = { currentBlog } { publicationType } index= { 0 } />
< / div >
{ /key }
{ /if }
{ #if $publicationColumnVisibility . social }
< div class = "flex flex-col space-y-4 max-w-xl overflow-auto flex-grow-1 bg-highlight" >
< p > Social column< / p >
< / div >
{ /if }
// #endregion
onDestroy(() => {
// reset visibility
publicationColumnVisibility.reset();
});
onMount(() => {
onMount(() => {
// Always check whether the TOC sidebar should be visible.
// Set current columns depending on the publication type
setTocVisibilityOnResize();
const isBlog = publicationType === "blog";
window.addEventListener("hashchange", scrollToElementWithOffset);
publicationColumnVisibility.update((v) => ({
// Also handle the case where the user lands on the page with a hash in the URL
...v,
scrollToElementWithOffset();
main: !isBlog,
window.addEventListener("resize", setTocVisibilityOnResize);
blog: isBlog,
window.addEventListener("click", hideTocOnClick);
}));
if (isLeaf || isBlog) {
publicationColumnVisibility.update((v) => ({ ... v , toc : false } ));
}
// Set up the intersection observer.
// Set up the intersection observer.
observer = new IntersectionObserver((entries) => {
observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !isLoading && !isDone) {
if (entry.isIntersecting && !isLoading && !isDone) {
loadMore(1);
loadMore(1);
}
}
});
});
}, { threshold : 0.5 } );
},
{ threshold : 0.5 } ,
);
loadMore(8);
loadMore(8);
return () => {
return () => {
window.removeEventListener("hashchange", scrollToElementWithOffset);
window.removeEventListener("resize", setTocVisibilityOnResize);
window.removeEventListener("click", hideTocOnClick);
observer.disconnect();
observer.disconnect();
};
};
});
});
< / script >
< / script >
<!-- TODO: Keep track of already - loaded leaves. -->
<!-- Table of contents -->
<!-- TODO: Handle entering mid - document and scrolling up. -->
{ #if publicationType !== "blog" || ! isLeaf }
< TocToggle rootId = { rootAddress } / >
{ #if showTocButton && ! showToc }
<!-- <Button
class="btn-leather fixed top-20 left-4 h-6 w-6"
outline={ true }
on:click={( ev ) => {
showToc = true;
ev.stopPropagation();
}}
>
< BookOutline / >
< / Button >
< Tooltip > Show Table of Contents< / Tooltip > -->
{ /if }
{ /if }
<!-- TODO: Use loader to build ToC. -->
<!-- {#if showToc}
<!-- Default publications -->
< Sidebar class = 'sidebar-leather fixed top-20 left-0 px-4 w-60' { activeHash } >
{ #if $publicationColumnVisibility . main }
< SidebarWrapper >
< div class = "flex flex-col p-4 space-y-4 overflow-auto max-w-2xl flex-grow-2" >
< SidebarGroup class = 'sidebar-group-leather overflow-y-scroll' >
< div
{ #each events as event }
class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border"
< SidebarItem
>
class='sidebar-item-leather'
< Details event = { indexEvent } / >
label={ event . getMatchingTags ( 'title' )[ 0 ][ 1 ]}
< / div >
href={ ` ${ $page . url . pathname } # ${ normalizeHashPath ( event . getMatchingTags ( 'title' )[ 0 ][ 1 ]) } ` }
<!-- Publication -->
/>
{ /each }
< / SidebarGroup >
< / SidebarWrapper >
< / Sidebar >
{ /if } -->
< div class = "flex flex-col space-y-4 max-w-2xl pb-10 px-4 sm:px-6 md:px-8" >
{ #each leaves as leaf , i }
{ #each leaves as leaf , i }
{ #if leaf == null }
{ #if leaf == null }
< Alert class = 'flex space-x-2' >
< Alert class = "flex space-x-2" >
< ExclamationCircleOutline class = 'w-5 h-5' / >
< ExclamationCircleOutline class = "w-5 h-5" / >
Error loading content. One or more events could not be loaded.
Error loading content. One or more events could not be loaded.
< / Alert >
< / Alert >
{ : else }
{ : else }
< PublicationSection
< PublicationSection
rootAddress= { rootAddress }
{ rootAddress }
leaves= { leaves }
{ leaves }
address={ leaf . tagAddress ()}
address={ leaf . tagAddress ()}
ref={( el ) => setLastElementRef ( el , i )}
ref={( el ) => setLastElementRef ( el , i )}
/>
/>
@ -212,21 +189,112 @@
{ /each }
{ /each }
< div class = "flex justify-center my-4" >
< div class = "flex justify-center my-4" >
{ #if isLoading }
{ #if isLoading }
< Button disabled color = "primary" >
< Button disabled color = "primary" > Loading...< / Button >
Loading...
< / Button >
{ :else if ! isDone }
{ :else if ! isDone }
< Button color = "primary" on:click = {() => loadMore ( 1 )} >
< Button color = "primary" on:click = {() => loadMore ( 1 )} > Show More </ Button >
Show More
< / Button >
{ : else }
{ : else }
< p class = "text-gray-500 dark:text-gray-400" > You've reached the end of the publication.< / p >
< p class = "text-gray-500 dark:text-gray-400" >
You've reached the end of the publication.
< / p >
{ /if }
{ /if }
< / div >
< / div >
< / div >
< / div >
{ /if }
< style >
<!-- Blog list -->
:global(.sidebar-group-leather) {
{ #if $publicationColumnVisibility . blog }
max-height: calc(100vh - 8rem);
< div
}
class="flex flex-col p-4 space-y-4 overflow-auto max-w-xl flex-grow-1
< / style >
{ isInnerActive () ? 'discreet' : '' }
"
>
< div
class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border"
>
< Details event = { indexEvent } / >
< / div >
<!-- List blog excerpts -->
{ #each leaves as leaf , i }
< BlogHeader
rootId={ leaf . tagAddress ()}
event={ leaf }
onBlogUpdate={ loadBlog }
active={ ! isInnerActive ()}
/>
{ /each }
< / div >
{ /if }
{ #if isInnerActive ()}
{ #key currentBlog }
< div
class="flex flex-col p-4 max-w-3xl overflow-auto flex-grow-2 max-h-[calc(100vh-146px)] sticky top-[146px]"
>
{ #each leaves as leaf , i }
{ #if leaf . tagAddress () === currentBlog }
< div
class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border"
>
< Details event = { leaf } / >
< / div >
< PublicationSection
{ rootAddress }
{ leaves }
address={ leaf . tagAddress ()}
ref={( el ) => setLastElementRef ( el , i )}
/>
< Card class = "ArticleBox !hidden card-leather min-w-full grid mt-4" >
< Interactions rootId = { currentBlog } / >
< / Card >
{ /if }
{ /each }
< / div >
{ /key }
{ /if }
{ #if $publicationColumnVisibility . discussion }
< Sidebar class = "sidebar-leather right-0 md:!pl-8" >
< SidebarWrapper >
< SidebarGroup class = "sidebar-group-leather" >
< div class = "flex justify-between items-baseline" >
< Heading tag = "h1" class = "h-leather !text-lg" > Discussion< / Heading >
< Button
class="btn-leather hidden sm:flex z-30 !p-1 bg-primary-50 dark:bg-gray-800"
outline
onclick={ closeDiscussion }
>
< CloseOutline / >
< / Button >
< / div >
< div class = "flex flex-col space-y-4" >
<!-- TODO
alternative for other publications and
when blog is not opened, but discussion is opened from the list
-->
{ #if showBlogHeader ()}
< BlogHeader
rootId={ currentBlog }
event={ currentBlogEvent }
onBlogUpdate={ loadBlog }
active={ true }
/>
{ /if }
< div class = "flex flex-col w-full space-y-4" >
< Card class = "ArticleBox card-leather w-full grid max-w-xl" >
< div class = "flex flex-col my-2" >
< span > Unknown< / span >
< span class = "text-gray-500" > 1.1.1970< / span >
< / div >
< div class = "flex flex-col flex-grow space-y-4" >
This is a very intelligent comment placeholder that applies to
all the content equally well.
< / div >
< / Card >
< / div >
< / div >
< / SidebarGroup >
< / SidebarWrapper >
< / Sidebar >
{ /if }