Browse Source

Merge remote-tracking branch 'origin' into Issue#215-only-contact-page

master
Silberengel 10 months ago
parent
commit
241c1381dc
  1. 6
      src/app.css
  2. 64
      src/lib/components/Publication.svelte
  3. 30
      src/lib/components/PublicationSection.svelte
  4. 49
      src/lib/data_structures/lazy.ts
  5. 176
      src/lib/data_structures/publication_tree.ts
  6. 20
      tailwind.config.cjs

6
src/app.css

@ -336,6 +336,12 @@ @@ -336,6 +336,12 @@
@apply bg-gray-100 dark:bg-gray-900 p-4 rounded-lg;
}
.literalblock {
pre {
@apply text-wrap;
}
}
table {
@apply w-full overflow-x-auto;

64
src/lib/components/Publication.svelte

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
<script lang="ts">
import {
Alert,
Button,
Sidebar,
SidebarGroup,
@ -10,7 +11,7 @@ @@ -10,7 +11,7 @@
Tooltip,
} from "flowbite-svelte";
import { getContext, onMount } from "svelte";
import { BookOutline } from "flowbite-svelte-icons";
import { BookOutline, ExclamationCircleOutline } from "flowbite-svelte-icons";
import { page } from "$app/state";
import type { NDKEvent } from "@nostr-dev-kit/ndk";
import PublicationSection from "./PublicationSection.svelte";
@ -28,8 +29,9 @@ @@ -28,8 +29,9 @@
// TODO: Test load handling.
let leaves = $state<NDKEvent[]>([]);
let leaves = $state<Array<NDKEvent | null>>([]);
let isLoading = $state<boolean>(false);
let isDone = $state<boolean>(false);
let lastElementRef = $state<HTMLElement | null>(null);
let observer: IntersectionObserver;
@ -38,12 +40,15 @@ @@ -38,12 +40,15 @@
isLoading = true;
for (let i = 0; i < count; i++) {
const nextItem = await publicationTree.next();
if (leaves.includes(nextItem.value) || nextItem.done) {
isLoading = false;
return;
const iterResult = await publicationTree.next();
const { done, value } = iterResult;
if (done) {
isDone = true;
break;
}
leaves.push(nextItem.value);
leaves.push(value);
}
isLoading = false;
@ -60,8 +65,13 @@ @@ -60,8 +65,13 @@
return;
}
observer.observe(lastElementRef!);
return () => observer.unobserve(lastElementRef!);
if (isDone) {
observer?.unobserve(lastElementRef!);
return;
}
observer?.observe(lastElementRef!);
return () => observer?.unobserve(lastElementRef!);
});
// #endregion
@ -136,8 +146,8 @@ @@ -136,8 +146,8 @@
// Set up the intersection observer.
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && !isLoading) {
loadMore(4);
if (entry.isIntersecting && !isLoading && !isDone) {
loadMore(1);
}
});
}, { threshold: 0.5 });
@ -153,6 +163,8 @@ @@ -153,6 +163,8 @@
});
</script>
<!-- TODO: Keep track of already-loaded leaves. -->
<!-- TODO: Handle entering mid-document and scrolling up. -->
{#if showTocButton && !showToc}
<!-- <Button
@ -185,13 +197,31 @@ @@ -185,13 +197,31 @@
{/if} -->
<div class="flex flex-col space-y-4 max-w-2xl">
{#each leaves as leaf, i}
<PublicationSection
rootAddress={rootAddress}
leaves={leaves}
address={leaf.tagAddress()}
ref={(el) => setLastElementRef(el, i)}
/>
{#if leaf == null}
<Alert class='flex space-x-2'>
<ExclamationCircleOutline class='w-5 h-5' />
Error loading content. One or more events could not be loaded.
</Alert>
{:else}
<PublicationSection
rootAddress={rootAddress}
leaves={leaves}
address={leaf.tagAddress()}
ref={(el) => setLastElementRef(el, i)}
/>
{/if}
{/each}
<div class="flex justify-center my-4">
{#if isLoading}
<Button disabled color="primary">
Loading...
</Button>
{:else if !isDone}
<Button color="primary" on:click={() => loadMore(1)}>
Show More
</Button>
{/if}
</div>
</div>
<style>

30
src/lib/components/PublicationSection.svelte

@ -14,7 +14,7 @@ @@ -14,7 +14,7 @@
}: {
address: string,
rootAddress: string,
leaves: NDKEvent[],
leaves: Array<NDKEvent | null>,
ref: (ref: HTMLElement) => void,
} = $props();
@ -23,24 +23,38 @@ @@ -23,24 +23,38 @@
let leafEvent: Promise<NDKEvent | null> = $derived.by(async () =>
await publicationTree.getEvent(address));
let rootEvent: Promise<NDKEvent | null> = $derived.by(async () =>
await publicationTree.getEvent(rootAddress));
let publicationType: Promise<string | undefined> = $derived.by(async () =>
(await rootEvent)?.getMatchingTags('type')[0]?.[1]);
let leafHierarchy: Promise<NDKEvent[]> = $derived.by(async () =>
await publicationTree.getHierarchy(address));
let leafTitle: Promise<string | undefined> = $derived.by(async () =>
(await leafEvent)?.getMatchingTags('title')[0]?.[1]);
let leafContent: Promise<string | Document> = $derived.by(async () =>
asciidoctor.convert((await leafEvent)?.content ?? ''));
let previousLeafEvent: NDKEvent | null = $derived.by(() => {
const index = leaves.findIndex(leaf => leaf.tagAddress() === address);
if (index === 0) {
return null;
}
return leaves[index - 1];
let index: number;
let event: NDKEvent | null = null;
let decrement = 1;
do {
index = leaves.findIndex(leaf => leaf?.tagAddress() === address);
if (index === 0) {
return null;
}
event = leaves[index - decrement++];
} while (event == null && index - decrement >= 0);
return event;
});
let previousLeafHierarchy: Promise<NDKEvent[] | null> = $derived.by(async () => {
if (!previousLeafEvent) {
return null;
@ -90,12 +104,10 @@ @@ -90,12 +104,10 @@
});
</script>
<!-- TODO: Correctly handle events that are the start of a content section. -->
<section bind:this={sectionRef} class='publication-leather'>
<section bind:this={sectionRef} class='publication-leather content-visibility-auto'>
{#await Promise.all([leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches])}
<TextPlaceholder size='xxl' />
{:then [leafTitle, leafContent, leafHierarchy, publicationType, divergingBranches]}
<!-- TODO: Ensure we render all headings, not just the first one. -->
{#each divergingBranches as [branch, depth]}
{@render sectionHeading(branch.getMatchingTags('title')[0]?.[1] ?? '', depth)}
{/each}

49
src/lib/data_structures/lazy.ts

@ -1,16 +1,55 @@ @@ -1,16 +1,55 @@
export enum LazyStatus {
Pending,
Resolved,
Error,
}
export class Lazy<T> {
#value?: T;
#value: T | null = null;
#resolver: () => Promise<T>;
#pendingPromise: Promise<T | null> | null = null;
status: LazyStatus;
constructor(resolver: () => Promise<T>) {
this.#resolver = resolver;
this.status = LazyStatus.Pending;
}
async value(): Promise<T> {
if (!this.#value) {
this.#value = await this.#resolver();
/**
* Resolves the lazy object and returns the value.
*
* @returns The resolved value.
*
* @remarks Lazy object resolution is performed as an atomic operation. If a resolution has
* already been requested when this function is invoked, the pending promise from the earlier
* invocation is returned. Thus, all calls to this function before it is resolved will depend on
* a single resolution.
*/
value(): Promise<T | null> {
if (this.status === LazyStatus.Resolved) {
return Promise.resolve(this.#value);
}
if (this.#pendingPromise) {
return this.#pendingPromise;
}
return this.#value;
this.#pendingPromise = this.#resolve();
return this.#pendingPromise;
}
async #resolve(): Promise<T | null> {
try {
this.#value = await this.#resolver();
this.status = LazyStatus.Resolved;
return this.#value;
} catch (error) {
this.status = LazyStatus.Error;
console.error(error);
return null;
} finally {
this.#pendingPromise = null;
}
}
}

176
src/lib/data_structures/publication_tree.ts

@ -4,19 +4,24 @@ import { Lazy } from "./lazy.ts"; @@ -4,19 +4,24 @@ import { Lazy } from "./lazy.ts";
import { findIndexAsync as _findIndexAsync } from '../utils.ts';
enum PublicationTreeNodeType {
Root,
Branch,
Leaf,
}
enum PublicationTreeNodeStatus {
Resolved,
Error,
}
interface PublicationTreeNode {
type: PublicationTreeNodeType;
status: PublicationTreeNodeStatus;
address: string;
parent?: PublicationTreeNode;
children?: Array<Lazy<PublicationTreeNode>>;
}
export class PublicationTree implements AsyncIterable<NDKEvent> {
export class PublicationTree implements AsyncIterable<NDKEvent | null> {
/**
* The root node of the tree.
*/
@ -50,7 +55,8 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -50,7 +55,8 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
constructor(rootEvent: NDKEvent, ndk: NDK) {
const rootAddress = rootEvent.tagAddress();
this.#root = {
type: PublicationTreeNodeType.Root,
type: this.#getNodeType(rootEvent),
status: PublicationTreeNodeStatus.Resolved,
address: rootAddress,
children: [],
};
@ -85,6 +91,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -85,6 +91,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
const node: PublicationTreeNode = {
type: await this.#getNodeType(event),
status: PublicationTreeNodeStatus.Resolved,
address,
parent: parentNode,
children: [],
@ -113,7 +120,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -113,7 +120,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
);
}
await this.#addNode(address, parentNode);
this.#addNode(address, parentNode);
}
/**
@ -135,7 +142,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -135,7 +142,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
* @param address The address of the parent node.
* @returns An array of addresses of any loaded child nodes.
*/
async getChildAddresses(address: string): Promise<string[]> {
async getChildAddresses(address: string): Promise<Array<string | null>> {
const node = await this.#nodes.get(address)?.value();
if (!node) {
throw new Error(`PublicationTree: Node with address ${address} not found.`);
@ -143,7 +150,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -143,7 +150,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
return Promise.all(
node.children?.map(async child =>
(await child.value()).address
(await child.value())?.address ?? null
) ?? []
);
}
@ -206,20 +213,44 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -206,20 +213,44 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
async tryMoveToFirstChild(): Promise<boolean> {
if (!this.target) {
throw new Error("Cursor: Target node is null or undefined.");
console.debug("Cursor: Target node is null or undefined.");
return false;
}
if (this.target.type === PublicationTreeNodeType.Leaf) {
return false;
}
if (this.target.children == null || this.target.children.length === 0) {
return false;
}
this.target = await this.target.children?.at(0)?.value();
return true;
}
async tryMoveToLastChild(): Promise<boolean> {
if (!this.target) {
console.debug("Cursor: Target node is null or undefined.");
return false;
}
if (this.target.type === PublicationTreeNodeType.Leaf) {
return false;
}
if (this.target.children == null || this.target.children.length === 0) {
return false;
}
this.target = (await this.target.children?.at(0)?.value())!;
this.target = await this.target.children?.at(-1)?.value();
return true;
}
async tryMoveToNextSibling(): Promise<boolean> {
if (!this.target) {
throw new Error("Cursor: Target node is null or undefined.");
console.debug("Cursor: Target node is null or undefined.");
return false;
}
const parent = this.target.parent;
@ -229,25 +260,53 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -229,25 +260,53 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
}
const currentIndex = await siblings.findIndexAsync(
async (sibling: Lazy<PublicationTreeNode>) => (await sibling.value()).address === this.target!.address
async (sibling: Lazy<PublicationTreeNode>) => (await sibling.value())?.address === this.target!.address
);
if (currentIndex === -1) {
return false;
}
const nextSibling = (await siblings.at(currentIndex + 1)?.value()) ?? null;
if (!nextSibling) {
if (currentIndex + 1 >= siblings.length) {
return false;
}
this.target = nextSibling;
this.target = await siblings.at(currentIndex + 1)?.value();
return true;
}
async tryMoveToPreviousSibling(): Promise<boolean> {
if (!this.target) {
console.debug("Cursor: Target node is null or undefined.");
return false;
}
const parent = this.target.parent;
const siblings = parent?.children;
if (!siblings) {
return false;
}
const currentIndex = await siblings.findIndexAsync(
async (sibling: Lazy<PublicationTreeNode>) => (await sibling.value())?.address === this.target!.address
);
if (currentIndex === -1) {
return false;
}
if (currentIndex <= 0) {
return false;
}
this.target = await siblings.at(currentIndex - 1)?.value();
return true;
}
tryMoveToParent(): boolean {
if (!this.target) {
throw new Error("Cursor: Target node is null or undefined.");
console.debug("Cursor: Target node is null or undefined.");
return false;
}
const parent = this.target.parent;
@ -264,35 +323,75 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -264,35 +323,75 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
// #region Async Iterator Implementation
[Symbol.asyncIterator](): AsyncIterator<NDKEvent> {
[Symbol.asyncIterator](): AsyncIterator<NDKEvent | null> {
return this;
}
async next(): Promise<IteratorResult<NDKEvent>> {
// TODO: Add `previous()` method.
async next(): Promise<IteratorResult<NDKEvent | null>> {
if (!this.#cursor.target) {
await this.#cursor.tryMoveTo(this.#bookmark);
if (await this.#cursor.tryMoveTo(this.#bookmark)) {
const event = await this.getEvent(this.#cursor.target!.address);
return { done: false, value: event };
}
}
// Based on Raymond Chen's tree traversal algorithm example.
// https://devblogs.microsoft.com/oldnewthing/20200106-00/?p=103300
do {
if (await this.#cursor.tryMoveToFirstChild()) {
continue;
}
if (await this.#cursor.tryMoveToNextSibling()) {
continue;
while (await this.#cursor.tryMoveToFirstChild()) {
continue;
}
if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) {
return { done: false, value: null };
}
const event = await this.getEvent(this.#cursor.target!.address);
return { done: false, value: event };
}
} while (this.#cursor.tryMoveToParent());
if (this.#cursor.tryMoveToParent()) {
continue;
if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) {
return { done: false, value: null };
}
// If we get to this point, we're at the root node (can't move up any more).
return { done: true, value: null };
}
async previous(): Promise<IteratorResult<NDKEvent | null>> {
if (!this.#cursor.target) {
if (await this.#cursor.tryMoveTo(this.#bookmark)) {
const event = await this.getEvent(this.#cursor.target!.address);
return { done: false, value: event };
}
}
// Based on Raymond Chen's tree traversal algorithm example.
// https://devblogs.microsoft.com/oldnewthing/20200106-00/?p=103300
do {
if (await this.#cursor.tryMoveToPreviousSibling()) {
while (await this.#cursor.tryMoveToLastChild()) {
continue;
}
if (this.#cursor.target?.type === PublicationTreeNodeType.Root) {
return { done: true, value: null };
if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) {
return { done: false, value: null };
}
const event = await this.getEvent(this.#cursor.target!.address);
return { done: false, value: event };
}
} while (this.#cursor.target?.type !== PublicationTreeNodeType.Leaf);
} while (this.#cursor.tryMoveToParent());
if (this.#cursor.target!.status === PublicationTreeNodeStatus.Error) {
return { done: false, value: null };
}
const event = await this.getEvent(this.#cursor.target!.address);
return { done: false, value: event! };
return { done: true, value: null };
}
// #endregion
@ -391,9 +490,17 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -391,9 +490,17 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
});
if (!event) {
throw new Error(
console.debug(
`PublicationTree: Event with address ${address} not found.`
);
return {
type: PublicationTreeNodeType.Leaf,
status: PublicationTreeNodeStatus.Error,
address,
parent: parentNode,
children: [],
};
}
this.#events.set(address, event);
@ -401,7 +508,8 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -401,7 +508,8 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
const childAddresses = event.tags.filter(tag => tag[0] === 'a').map(tag => tag[1]);
const node: PublicationTreeNode = {
type: await this.#getNodeType(event),
type: this.#getNodeType(event),
status: PublicationTreeNodeStatus.Resolved,
address,
parent: parentNode,
children: [],
@ -414,11 +522,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> { @@ -414,11 +522,7 @@ export class PublicationTree implements AsyncIterable<NDKEvent> {
return node;
}
async #getNodeType(event: NDKEvent): Promise<PublicationTreeNodeType> {
if (event.tagAddress() === this.#root.address) {
return PublicationTreeNodeType.Root;
}
#getNodeType(event: NDKEvent): PublicationTreeNodeType {
if (event.kind === 30040 && event.tags.some(tag => tag[0] === 'a')) {
return PublicationTreeNodeType.Branch;
}

20
tailwind.config.cjs

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import flowbite from "flowbite/plugin";
import plugin from "tailwindcss/plugin";
/** @type {import('tailwindcss').Config}*/
const config = {
@ -85,6 +86,25 @@ const config = { @@ -85,6 +86,25 @@ const config = {
plugins: [
flowbite(),
plugin(function({ addUtilities, matchUtilities }) {
addUtilities({
'.content-visibility-auto': {
'content-visibility': 'auto',
},
'.contain-size': {
contain: 'size',
},
});
matchUtilities({
'contain-intrinsic-w-*': value => ({
width: value,
}),
'contain-intrinsic-h-*': value => ({
height: value,
})
});
})
],
darkMode: 'class',

Loading…
Cancel
Save