refactor(ark): rename widget to column

This commit is contained in:
2023-12-26 13:44:38 +07:00
parent 227c2ddefa
commit e1db873bd5
34 changed files with 800 additions and 892 deletions

View File

@@ -2,7 +2,7 @@
"name": "@columns/timeline",
"version": "0.0.0",
"private": true,
"main": "./src/index.ts",
"main": "./src/index.tsx",
"dependencies": {
"@lume/ark": "workspace:^",
"@lume/icons": "workspace:^",
@@ -10,6 +10,7 @@
"@nostr-dev-kit/ndk": "^2.3.1",
"@tanstack/react-query": "^5.14.2",
"react": "^18.2.0",
"react-router-dom": "^6.21.0",
"virtua": "^0.18.0"
},
"devDependencies": {

View File

@@ -0,0 +1,28 @@
import { Note, ThreadNote } from "@lume/ark";
import { ArrowLeftIcon } from "@lume/icons";
import { useNavigate, useParams } from "react-router-dom";
import { WVList } from "virtua";
export function EventRoute() {
const { id } = useParams();
const navigate = useNavigate();
return (
<WVList className="pb-5 overflow-y-auto">
<div className="h-11 bg-neutral-50 dark:bg-neutral-950 border-b flex items-center px-3 border-neutral-100 dark:border-neutral-900 mb-3">
<button
type="button"
className="inline-flex items-center gap-2.5 text-sm font-medium"
onClick={() => navigate(-1)}
>
<ArrowLeftIcon className="size-4" />
Back
</button>
</div>
<div className="px-3">
<ThreadNote eventId={id} />
<Note.ReplyList eventId={id} title="All replies" className="mt-5" />
</div>
</WVList>
);
}

View File

@@ -0,0 +1,119 @@
import { RepostNote, TextNote, useArk, useStorage } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useEffect, useMemo, useRef } from "react";
import { CacheSnapshot, VList, VListHandle } from "virtua";
export function HomeRoute() {
const ark = useArk();
const storage = useStorage();
const ref = useRef<VListHandle>();
const cacheKey = "newsfeed-vlist";
const [offset, cache] = useMemo(() => {
const serialized = sessionStorage.getItem(cacheKey);
if (!serialized) return [];
return JSON.parse(serialized) as [number, CacheSnapshot];
}, []);
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["newsfeed"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: !storage.account.contacts.length
? [storage.account.pubkey]
: storage.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderItem = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote key={event.id} event={event} className="mt-3" />;
case NDKKind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
useEffect(() => {
if (!ref.current) return;
const handle = ref.current;
if (offset) {
handle.scrollTo(offset);
}
return () => {
sessionStorage.setItem(
cacheKey,
JSON.stringify([handle.scrollOffset, handle.cache]),
);
};
}, []);
return (
<div className="w-full h-full">
<VList ref={ref} cache={cache} overscan={2} className="flex-1 px-3">
{isLoading ? (
<div className="inline-flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5" />
Loading
</div>
) : (
allEvents.map((item) => renderItem(item))
)}
<div className="flex h-16 items-center justify-center">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="h-5 w-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</div>
);
}

View File

@@ -1 +0,0 @@
export * from "./timeline";

View File

@@ -0,0 +1,23 @@
import { Column } from "@lume/ark";
import { TimelineIcon } from "@lume/icons";
import { EventRoute } from "./event";
import { HomeRoute } from "./home";
import { UserRoute } from "./user";
export function Timeline() {
return (
<Column.Root>
<Column.Header
id="9999"
queryKey={["newsfeed"]}
title="Timeline"
icon={<TimelineIcon className="size-4" />}
/>
<Column.Content>
<Column.Route path="/" element={<HomeRoute />} />
<Column.Route path="/events/:id" element={<EventRoute />} />
<Column.Route path="/users/:id" element={<UserRoute />} />
</Column.Content>
</Column.Root>
);
}

View File

@@ -1,104 +0,0 @@
import { RepostNote, TextNote, Widget, useArk, useStorage } from "@lume/ark";
import { ArrowRightCircleIcon, LoaderIcon, TimelineIcon } from "@lume/icons";
import { FETCH_LIMIT } from "@lume/utils";
import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useMemo, useRef } from "react";
import { VList, VListHandle } from "virtua";
export function TimelineColumn() {
const ark = useArk();
const storage = useStorage();
const ref = useRef<VListHandle>();
const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } =
useInfiniteQuery({
queryKey: ["newsfeed"],
initialPageParam: 0,
queryFn: async ({
signal,
pageParam,
}: {
signal: AbortSignal;
pageParam: number;
}) => {
const events = await ark.getInfiniteEvents({
filter: {
kinds: [NDKKind.Text, NDKKind.Repost],
authors: !storage.account.contacts.length
? [storage.account.pubkey]
: storage.account.contacts,
},
limit: FETCH_LIMIT,
pageParam,
signal,
});
return events;
},
getNextPageParam: (lastPage) => {
const lastEvent = lastPage.at(-1);
if (!lastEvent) return;
return lastEvent.created_at - 1;
},
refetchOnWindowFocus: false,
});
const allEvents = useMemo(
() => (data ? data.pages.flatMap((page) => page) : []),
[data],
);
const renderItem = (event: NDKEvent) => {
switch (event.kind) {
case NDKKind.Text:
return <TextNote key={event.id} event={event} />;
case NDKKind.Repost:
return <RepostNote key={event.id} event={event} />;
default:
return <TextNote key={event.id} event={event} />;
}
};
return (
<Widget.Root>
<Widget.Header
id="9999"
queryKey={["newsfeed"]}
title="Timeline"
icon={<TimelineIcon className="h-5 w-5" />}
/>
<Widget.Content>
<VList ref={ref} overscan={2} className="flex-1">
{isLoading ? (
<div className="inline-flex h-16 items-center justify-center gap-2 px-3 py-1.5">
<LoaderIcon className="size-5" />
Loading
</div>
) : (
allEvents.map((item) => renderItem(item))
)}
<div className="flex h-16 items-center justify-center px-3 py-3">
{hasNextPage ? (
<button
type="button"
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
className="inline-flex h-10 w-max items-center justify-center gap-2 rounded-full bg-blue-500 px-6 font-medium text-white hover:bg-blue-600 focus:outline-none"
>
{isFetchingNextPage ? (
<LoaderIcon className="h-5 w-5 animate-spin" />
) : (
<>
<ArrowRightCircleIcon className="h-5 w-5" />
Load more
</>
)}
</button>
) : null}
</div>
</VList>
</Widget.Content>
</Widget.Root>
);
}

View File

@@ -0,0 +1,7 @@
import { useParams } from "react-router-dom";
export function UserRoute() {
const { id } = useParams();
return <div>{id}</div>;
}