diff --git a/apps/desktop/src/routes/home/components/index.ts b/apps/desktop/src/routes/home/components/index.ts deleted file mode 100644 index 0505d67f..00000000 --- a/apps/desktop/src/routes/home/components/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './newsfeed'; -export * from './notification'; diff --git a/apps/desktop/src/routes/home/components/newsfeed.tsx b/apps/desktop/src/routes/home/components/newsfeed.tsx deleted file mode 100644 index 0b70a060..00000000 --- a/apps/desktop/src/routes/home/components/newsfeed.tsx +++ /dev/null @@ -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 NewsfeedWidget() { - const ark = useArk(); - const storage = useStorage(); - const ref = useRef(); - - 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 ; - case NDKKind.Repost: - return ; - default: - return ; - } - }; - - return ( - - } - /> - - - {isLoading ? ( -
- - Loading -
- ) : ( - allEvents.map((item) => renderItem(item)) - )} -
- {hasNextPage ? ( - - ) : null} -
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/home/components/notification.tsx b/apps/desktop/src/routes/home/components/notification.tsx deleted file mode 100644 index 3b9c5136..00000000 --- a/apps/desktop/src/routes/home/components/notification.tsx +++ /dev/null @@ -1,174 +0,0 @@ -import { NoteSkeleton, TextNote, Widget, useArk, useStorage } from "@lume/ark"; -import { - AnnouncementIcon, - ArrowRightCircleIcon, - LoaderIcon, -} from "@lume/icons"; -import { FETCH_LIMIT } from "@lume/utils"; -import { NDKEvent, NDKKind, NDKSubscription } from "@nostr-dev-kit/ndk"; -import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import { sendNativeNotification } from "apps/desktop/src/utils/notification"; -import { useEffect, useMemo } from "react"; -import { VList } from "virtua"; - -export function NotificationWidget() { - const ark = useArk(); - const storage = useStorage(); - const queryClient = useQueryClient(); - - const { status, data, hasNextPage, isFetchingNextPage, fetchNextPage } = - useInfiniteQuery({ - queryKey: ["notification"], - initialPageParam: 0, - queryFn: async ({ - signal, - pageParam, - }: { - signal: AbortSignal; - pageParam: number; - }) => { - const events = await ark.getInfiniteEvents({ - filter: { - kinds: [ - NDKKind.Text, - NDKKind.Repost, - NDKKind.Reaction, - NDKKind.Zap, - ], - "#p": [storage.account.pubkey], - }, - limit: FETCH_LIMIT, - pageParam, - signal, - }); - - return events; - }, - getNextPageParam: (lastPage) => { - const lastEvent = lastPage.at(-1); - if (!lastEvent) return; - return lastEvent.created_at - 1; - }, - refetchOnWindowFocus: false, - refetchOnMount: false, - refetchOnReconnect: false, - staleTime: Infinity, - }); - - const allEvents = useMemo( - () => (data ? data.pages.flatMap((page) => page) : []), - [data], - ); - - const renderEvent = (event: NDKEvent) => { - if (event.pubkey === storage.account.pubkey) return null; - return ; - }; - - useEffect(() => { - let sub: NDKSubscription = undefined; - - if (status === "success" && storage.account) { - const filter = { - kinds: [NDKKind.Text, NDKKind.Repost, NDKKind.Reaction, NDKKind.Zap], - "#p": [storage.account.pubkey], - since: Math.floor(Date.now() / 1000), - }; - - sub = ark.subscribe({ - filter, - closeOnEose: false, - cb: async (event) => { - queryClient.setQueryData( - ["notification"], - (prev: { pageParams: number; pages: Array }) => ({ - ...prev, - pages: [[event], ...prev.pages], - }), - ); - - const profile = await ark.getUserProfile({ pubkey: event.pubkey }); - - switch (event.kind) { - case NDKKind.Text: - return await sendNativeNotification( - `${ - profile.displayName || profile.name - } has replied to your note`, - ); - case NDKKind.Repost: - return await sendNativeNotification( - `${ - profile.displayName || profile.name - } has reposted to your note`, - ); - case NDKKind.Reaction: - return await sendNativeNotification( - `${profile.displayName || profile.name} has reacted ${ - event.content - } to your note`, - ); - case NDKKind.Zap: - return await sendNativeNotification( - `${ - profile.displayName || profile.name - } has zapped to your note`, - ); - default: - break; - } - }, - }); - } - - return () => { - if (sub) sub.stop(); - }; - }, [status]); - - return ( - - } - /> - - - {status === "pending" ? ( - - ) : allEvents.length < 1 ? ( -
-
馃帀
-

- Hmm! Nothing new yet. -

-
- ) : ( - allEvents.map((event) => renderEvent(event)) - )} -
- {hasNextPage ? ( - - ) : null} -
-
-
-
- ); -} diff --git a/apps/desktop/src/routes/home/index.tsx b/apps/desktop/src/routes/home/index.tsx index dc389ef7..9b49aa91 100644 --- a/apps/desktop/src/routes/home/index.tsx +++ b/apps/desktop/src/routes/home/index.tsx @@ -1,5 +1,5 @@ import { NotificationColumn } from "@columns/notification"; -import { TimelineColumn } from "@columns/timeline"; +import { Timeline } from "@columns/timeline"; import { useStorage } from "@lume/ark"; import { LoaderIcon } from "@lume/icons"; import { WidgetProps } from "@lume/types"; @@ -37,12 +37,10 @@ export function HomeScreen() { const renderItem = (widget: WidgetProps) => { switch (widget.kind) { - case WIDGET_KIND.notification: - return ; case WIDGET_KIND.newsfeed: - return ; + return ; default: - return ; + return ; } }; diff --git a/apps/desktop/tailwind.config.js b/apps/desktop/tailwind.config.js index f8afe82a..45f371ba 100644 --- a/apps/desktop/tailwind.config.js +++ b/apps/desktop/tailwind.config.js @@ -3,6 +3,7 @@ import sharedConfig from "@lume/tailwindcss"; const config = { content: [ "./src/**/*.{js,ts,jsx,tsx}", + "../../packages/@columns/**/*{.js,.ts,.jsx,.tsx}", "../../packages/ark/**/*{.js,.ts,.jsx,.tsx}", "../../packages/ui/**/*{.js,.ts,.jsx,.tsx}", "index.html", diff --git a/biome.json b/biome.json index b2d67f6c..db5374c4 100644 --- a/biome.json +++ b/biome.json @@ -11,7 +11,7 @@ "noNonNullAssertion": "warn" }, "correctness": { - "useExhaustiveDependencies": "warn" + "useExhaustiveDependencies": "off" }, "a11y": { "noSvgWithoutTitle": "off" diff --git a/packages/@columns/timeline/package.json b/packages/@columns/timeline/package.json index 22b6db6a..e6603ecb 100644 --- a/packages/@columns/timeline/package.json +++ b/packages/@columns/timeline/package.json @@ -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": { diff --git a/packages/@columns/timeline/src/event.tsx b/packages/@columns/timeline/src/event.tsx new file mode 100644 index 00000000..2aa10da8 --- /dev/null +++ b/packages/@columns/timeline/src/event.tsx @@ -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 ( + +
+ +
+
+ + +
+
+ ); +} diff --git a/packages/@columns/timeline/src/home.tsx b/packages/@columns/timeline/src/home.tsx new file mode 100644 index 00000000..a80b3e11 --- /dev/null +++ b/packages/@columns/timeline/src/home.tsx @@ -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(); + 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 ; + case NDKKind.Repost: + return ; + default: + return ; + } + }; + + 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 ( +
+ + {isLoading ? ( +
+ + Loading +
+ ) : ( + allEvents.map((item) => renderItem(item)) + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+ ); +} diff --git a/packages/@columns/timeline/src/index.ts b/packages/@columns/timeline/src/index.ts deleted file mode 100644 index ab509301..00000000 --- a/packages/@columns/timeline/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./timeline"; diff --git a/packages/@columns/timeline/src/index.tsx b/packages/@columns/timeline/src/index.tsx new file mode 100644 index 00000000..fafd6d2d --- /dev/null +++ b/packages/@columns/timeline/src/index.tsx @@ -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 ( + + } + /> + + } /> + } /> + } /> + + + ); +} diff --git a/packages/@columns/timeline/src/timeline.tsx b/packages/@columns/timeline/src/timeline.tsx deleted file mode 100644 index 13261353..00000000 --- a/packages/@columns/timeline/src/timeline.tsx +++ /dev/null @@ -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(); - - 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 ; - case NDKKind.Repost: - return ; - default: - return ; - } - }; - - return ( - - } - /> - - - {isLoading ? ( -
- - Loading -
- ) : ( - allEvents.map((item) => renderItem(item)) - )} -
- {hasNextPage ? ( - - ) : null} -
-
-
-
- ); -} diff --git a/packages/@columns/timeline/src/user.tsx b/packages/@columns/timeline/src/user.tsx new file mode 100644 index 00000000..d6bbbb09 --- /dev/null +++ b/packages/@columns/timeline/src/user.tsx @@ -0,0 +1,7 @@ +import { useParams } from "react-router-dom"; + +export function UserRoute() { + const { id } = useParams(); + + return
{id}
; +} diff --git a/packages/ark/src/components/column/content.tsx b/packages/ark/src/components/column/content.tsx new file mode 100644 index 00000000..09d2060e --- /dev/null +++ b/packages/ark/src/components/column/content.tsx @@ -0,0 +1,6 @@ +import { ReactNode } from "react"; +import { Routes } from "react-router-dom"; + +export function ColumnContent({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/packages/ark/src/components/column/header.tsx b/packages/ark/src/components/column/header.tsx new file mode 100644 index 00000000..4ff83038 --- /dev/null +++ b/packages/ark/src/components/column/header.tsx @@ -0,0 +1,112 @@ +import { + ArrowLeftIcon, + ArrowRightIcon, + HorizontalDotsIcon, + RefreshIcon, + ThreadIcon, + TrashIcon, +} from "@lume/icons"; +import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; +import { useQueryClient } from "@tanstack/react-query"; +import { ReactNode } from "react"; +import { useWidget } from "../../hooks/useWidget"; + +export function ColumnHeader({ + id, + title, + queryKey, + icon, +}: { + id: string; + title: string; + queryKey?: string[]; + icon?: ReactNode; +}) { + const queryClient = useQueryClient(); + const { removeWidget } = useWidget(); + + const refresh = async () => { + if (queryKey) await queryClient.refetchQueries({ queryKey }); + }; + + const moveLeft = async () => { + removeWidget.mutate(id); + }; + + const moveRight = async () => { + removeWidget.mutate(id); + }; + + const deleteWidget = async () => { + removeWidget.mutate(id); + }; + + return ( +
+
+
+
+ {icon ? icon : } +
{title}
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/packages/ark/src/components/column/index.ts b/packages/ark/src/components/column/index.ts new file mode 100644 index 00000000..aecc1b5a --- /dev/null +++ b/packages/ark/src/components/column/index.ts @@ -0,0 +1,13 @@ +import { Route } from "react-router-dom"; +import { ColumnContent } from "./content"; +import { ColumnHeader } from "./header"; +import { ColumnLiveWidget } from "./live"; +import { ColumnRoot } from "./root"; + +export const Column = { + Root: ColumnRoot, + Live: ColumnLiveWidget, + Header: ColumnHeader, + Content: ColumnContent, + Route: Route, +}; diff --git a/packages/ark/src/components/column/live.tsx b/packages/ark/src/components/column/live.tsx new file mode 100644 index 00000000..e3ecd95d --- /dev/null +++ b/packages/ark/src/components/column/live.tsx @@ -0,0 +1,42 @@ +import { ChevronUpIcon } from "@lume/icons"; +import { NDKEvent, NDKFilter } from "@nostr-dev-kit/ndk"; +import { useEffect, useState } from "react"; +import { useArk } from "../../provider"; + +export function ColumnLiveWidget({ + filter, + onClick, +}: { + filter: NDKFilter; + onClick: () => void; +}) { + const ark = useArk(); + const [events, setEvents] = useState([]); + + useEffect(() => { + const sub = ark.subscribe({ + filter, + closeOnEose: false, + cb: (event: NDKEvent) => setEvents((prev) => [...prev, event]), + }); + + return () => { + if (sub) sub.stop(); + }; + }, []); + + if (!events.length) return null; + + return ( +
+ +
+ ); +} diff --git a/packages/ark/src/components/column/root.tsx b/packages/ark/src/components/column/root.tsx new file mode 100644 index 00000000..bb3d89d9 --- /dev/null +++ b/packages/ark/src/components/column/root.tsx @@ -0,0 +1,37 @@ +import { Resizable } from "re-resizable"; +import { ReactNode, useState } from "react"; +import { MemoryRouter, UNSAFE_LocationContext } from "react-router-dom"; +import { twMerge } from "tailwind-merge"; + +export function ColumnRoot({ + children, + className, +}: { + children: ReactNode; + className?: string; +}) { + const [width, setWidth] = useState(420); + + return ( + + e.preventDefault()} + onResizeStop={(_e, _direction, _ref, d) => { + setWidth((prevWidth) => prevWidth + d.width); + }} + minWidth={420} + maxWidth={600} + className={twMerge( + "relative flex flex-col border-r-2 border-neutral-50 hover:border-neutral-100 dark:border-neutral-950 dark:hover:border-neutral-900", + className, + )} + enable={{ right: true }} + > + + {children} + + + + ); +} diff --git a/packages/ark/src/components/note/builds/reply.tsx b/packages/ark/src/components/note/builds/reply.tsx index f756a715..2b95bb84 100644 --- a/packages/ark/src/components/note/builds/reply.tsx +++ b/packages/ark/src/components/note/builds/reply.tsx @@ -23,10 +23,10 @@ export function Reply({ className="h-14 px-3" /> -
+
{event.replies?.length > 0 ? ( -
+
-
- -
- - - - -
+
+ + + +
))} diff --git a/packages/ark/src/components/note/builds/text.tsx b/packages/ark/src/components/note/builds/text.tsx index 13947b91..f3a9ce32 100644 --- a/packages/ark/src/components/note/builds/text.tsx +++ b/packages/ark/src/components/note/builds/text.tsx @@ -2,12 +2,15 @@ import { NDKEvent } from "@nostr-dev-kit/ndk"; import { Note } from ".."; import { useArk } from "../../../provider"; -export function TextNote({ event }: { event: NDKEvent }) { +export function TextNote({ + event, + className, +}: { event: NDKEvent; className?: string }) { const ark = useArk(); const thread = ark.getEventThread({ tags: event.tags }); return ( - + { + const thread = ark.getEventThread({ tags: data.tags }); + switch (event.kind) { + case NDKKind.Text: + return ( + <> + + + + ); + case NDKKind.Article: + return ; + case 1063: + return ; + default: + return ( + + ); + } + }; + + if (isLoading) { + return
Loading...
; + } + + return ( + + + {renderEventKind(data)} +
+ +
+ + + + +
+
+
+ ); +} diff --git a/packages/ark/src/components/note/index.ts b/packages/ark/src/components/note/index.ts index f7366895..9a0a617d 100644 --- a/packages/ark/src/components/note/index.ts +++ b/packages/ark/src/components/note/index.ts @@ -8,7 +8,7 @@ import { NoteArticleContent } from "./kinds/article"; import { NoteMediaContent } from "./kinds/media"; import { NoteTextContent } from "./kinds/text"; import { NoteMenu } from "./menu"; -import { NoteReplies } from "./reply"; +import { NoteReplyList } from "./reply"; import { NoteRoot } from "./root"; import { NoteThread } from "./thread"; import { NoteUser } from "./user"; @@ -27,12 +27,13 @@ export const Note = { TextContent: NoteTextContent, MediaContent: NoteMediaContent, ArticleContent: NoteArticleContent, - Replies: NoteReplies, + ReplyList: NoteReplyList, }; export * from "./builds/text"; export * from "./builds/repost"; export * from "./builds/skeleton"; +export * from "./builds/thread"; export * from "./preview/image"; export * from "./preview/link"; export * from "./preview/video"; diff --git a/packages/ark/src/components/note/mentions/note.tsx b/packages/ark/src/components/note/mentions/note.tsx index 92e66196..0ebebe21 100644 --- a/packages/ark/src/components/note/mentions/note.tsx +++ b/packages/ark/src/components/note/mentions/note.tsx @@ -1,15 +1,13 @@ -import { WIDGET_KIND } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { memo } from "react"; +import { Link } from "react-router-dom"; import { Note } from ".."; import { useEvent } from "../../../hooks/useEvent"; -import { useWidget } from "../../../hooks/useWidget"; export const MentionNote = memo(function MentionNote({ eventId, }: { eventId: string }) { const { isLoading, isError, data } = useEvent(eventId); - const { addWidget } = useWidget(); const renderKind = (event: NDKEvent) => { switch (event.kind) { @@ -51,19 +49,12 @@ export const MentionNote = memo(function MentionNote({
{renderKind(data)} - +
); diff --git a/packages/ark/src/components/note/reply.tsx b/packages/ark/src/components/note/reply.tsx index 4ed46354..a93af91c 100644 --- a/packages/ark/src/components/note/reply.tsx +++ b/packages/ark/src/components/note/reply.tsx @@ -1,67 +1,68 @@ -import { LoaderIcon } from '@lume/icons'; -import { NDKEventWithReplies } from '@lume/types'; -import { NDKSubscription } from '@nostr-dev-kit/ndk'; -import { useEffect, useState } from 'react'; -import { useArk } from '../../provider'; -import { Reply } from './builds/reply'; +import { LoaderIcon } from "@lume/icons"; +import { NDKEventWithReplies } from "@lume/types"; +import { NDKSubscription } from "@nostr-dev-kit/ndk"; +import { useEffect, useState } from "react"; +import { twMerge } from "tailwind-merge"; +import { useArk } from "../../provider"; +import { Reply } from "./builds/reply"; -export function NoteReplies({ eventId }: { eventId: string }) { - const ark = useArk(); - const [data, setData] = useState(null); +export function NoteReplyList({ + eventId, + title, + className, +}: { eventId: string; title?: string; className?: string }) { + const ark = useArk(); + const [data, setData] = useState(null); - useEffect(() => { - let sub: NDKSubscription; - let isCancelled = false; + useEffect(() => { + let sub: NDKSubscription; + let isCancelled = false; - async function fetchRepliesAndSub() { - const events = await ark.getThreads({ id: eventId }); - if (!isCancelled) { - setData(events); - } - // subscribe for new replies - sub = ark.subscribe({ - filter: { - '#e': [eventId], - since: Math.floor(Date.now() / 1000), - }, - closeOnEose: false, - cb: (event: NDKEventWithReplies) => setData((prev) => [event, ...prev]), - }); - } + async function fetchRepliesAndSub() { + const events = await ark.getThreads({ id: eventId }); + if (!isCancelled) { + setData(events); + } + // subscribe for new replies + sub = ark.subscribe({ + filter: { + "#e": [eventId], + since: Math.floor(Date.now() / 1000), + }, + closeOnEose: false, + cb: (event: NDKEventWithReplies) => setData((prev) => [event, ...prev]), + }); + } - fetchRepliesAndSub(); + fetchRepliesAndSub(); - return () => { - isCancelled = true; - if (sub) sub.stop(); - }; - }, [eventId]); + return () => { + isCancelled = true; + if (sub) sub.stop(); + }; + }, [eventId]); - if (!data) { - return ( -
-
- -
-
- ); - } - - return ( -
-

Replies

- {data?.length === 0 ? ( -
-
-

馃憢

-

- Be the first to Reply! -

-
-
- ) : ( - data.map((event) => ) - )} -
- ); + return ( +
+

{title}

+ {!data ? ( +
+ +
+ ) : data.length === 0 ? ( +
+
+

馃憢

+

+ Be the first to Reply! +

+
+
+ ) : ( + data.map((event) => ( + + )) + )} +
+ ); } diff --git a/packages/ark/src/components/note/root.tsx b/packages/ark/src/components/note/root.tsx index b14a81da..741e7b32 100644 --- a/packages/ark/src/components/note/root.tsx +++ b/packages/ark/src/components/note/root.tsx @@ -1,21 +1,21 @@ -import { ReactNode } from 'react'; -import { twMerge } from 'tailwind-merge'; +import { ReactNode } from "react"; +import { twMerge } from "tailwind-merge"; export function NoteRoot({ - children, - className, + children, + className, }: { - children: ReactNode; - className?: string; + children: ReactNode; + className?: string; }) { - return ( -
- {children} -
- ); + return ( +
+ {children} +
+ ); } diff --git a/packages/ark/src/components/note/thread.tsx b/packages/ark/src/components/note/thread.tsx index 62dca8be..9d651602 100644 --- a/packages/ark/src/components/note/thread.tsx +++ b/packages/ark/src/components/note/thread.tsx @@ -1,38 +1,32 @@ -import { WIDGET_KIND } from '@lume/utils'; -import { twMerge } from 'tailwind-merge'; -import { Note } from '.'; -import { useWidget } from '../../hooks/useWidget'; +import { Link } from "react-router-dom"; +import { twMerge } from "tailwind-merge"; +import { Note } from "."; export function NoteThread({ - thread, - className, + thread, + className, }: { - thread: { rootEventId: string; replyEventId: string }; - className?: string; + thread: { rootEventId: string; replyEventId: string }; + className?: string; }) { - const { addWidget } = useWidget(); + if (!thread) return null; - if (!thread) return null; - - return ( -
-
- {thread.rootEventId ? : null} - {thread.replyEventId ? : null} - -
-
- ); + return ( +
+
+ {thread.rootEventId ? ( + + ) : null} + {thread.replyEventId ? ( + + ) : null} + + Show full thread + +
+
+ ); } diff --git a/packages/ark/src/components/note/user.tsx b/packages/ark/src/components/note/user.tsx index c8fc5830..18b25044 100644 --- a/packages/ark/src/components/note/user.tsx +++ b/packages/ark/src/components/note/user.tsx @@ -1,173 +1,236 @@ -import { RepostIcon } from '@lume/icons'; -import { displayNpub, formatCreatedAt } from '@lume/utils'; -import * as Avatar from '@radix-ui/react-avatar'; -import { minidenticon } from 'minidenticons'; -import { useMemo } from 'react'; -import { twMerge } from 'tailwind-merge'; -import { useProfile } from '../../hooks/useProfile'; +import { RepostIcon } from "@lume/icons"; +import { displayNpub, formatCreatedAt } from "@lume/utils"; +import * as Avatar from "@radix-ui/react-avatar"; +import { minidenticon } from "minidenticons"; +import { useMemo } from "react"; +import { twMerge } from "tailwind-merge"; +import { useProfile } from "../../hooks/useProfile"; export function NoteUser({ - pubkey, - time, - variant = 'text', - className, + pubkey, + time, + variant = "text", + className, }: { - pubkey: string; - time: number; - variant?: 'text' | 'repost' | 'mention'; - className?: string; + pubkey: string; + time: number; + variant?: "text" | "repost" | "mention" | "thread"; + className?: string; }) { - const createdAt = useMemo(() => formatCreatedAt(time), [time]); - const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]); - const fallbackAvatar = useMemo( - () => `data:image/svg+xml;utf8,${encodeURIComponent(minidenticon(pubkey, 90, 50))}`, - [pubkey] - ); + const createdAt = useMemo(() => formatCreatedAt(time), [time]); + const fallbackName = useMemo(() => displayNpub(pubkey, 16), [pubkey]); + const fallbackAvatar = useMemo( + () => + `data:image/svg+xml;utf8,${encodeURIComponent( + minidenticon(pubkey, 90, 50), + )}`, + [pubkey], + ); - const { isLoading, user } = useProfile(pubkey); + const { isLoading, user } = useProfile(pubkey); - if (variant === 'mention') { - if (isLoading) { - return ( -
- - - -
-
- {fallbackName} -
- - {createdAt} -
-
- ); - } + if (variant === "mention") { + if (isLoading) { + return ( +
+ + + +
+
+ {fallbackName} +
+ + + {createdAt} + +
+
+ ); + } - return ( -
- - - - {pubkey} - - -
-
- {user?.name || user?.display_name || user?.displayName || fallbackName} -
- - {createdAt} -
-
- ); - } + return ( +
+ + + + {pubkey} + + +
+
+ {user?.name || + user?.display_name || + user?.displayName || + fallbackName} +
+ + + {createdAt} + +
+
+ ); + } - if (variant === 'repost') { - if (isLoading) { - return ( -
-
- -
-
-
-
-
-
- ); - } + if (variant === "repost") { + if (isLoading) { + return ( +
+
+ +
+
+
+
+
+
+ ); + } - return ( -
-
- -
-
- - - - {pubkey} - - -
-
- {user?.name || user?.display_name || user?.displayName || fallbackName} -
- reposted -
-
-
- ); - } + return ( +
+
+ +
+
+ + + + {pubkey} + + +
+
+ {user?.name || + user?.display_name || + user?.displayName || + fallbackName} +
+ reposted +
+
+
+ ); + } - if (isLoading) { - return ( -
- - - -
-
- {fallbackName} -
-
-
- ); - } + if (variant === "thread") { + if (isLoading) { + return ( +
+
+
+
+
+
+
+ ); + } - return ( -
- - - - {pubkey} - - -
-
- {user?.name || user?.display_name || user?.displayName || fallbackName} -
-
{createdAt}
-
-
- ); + return ( +
+ + + + {pubkey} + + +
+
+ {user?.name || user?.display_name || user?.displayName || "Anon"} +
+
+ {createdAt} + + {fallbackName} +
+
+
+ ); + } + + if (isLoading) { + return ( +
+ + + +
+
+ {fallbackName} +
+
+
+ ); + } + + return ( +
+ + + + {pubkey} + + +
+
+ {user?.name || + user?.display_name || + user?.displayName || + fallbackName} +
+
+ {createdAt} +
+
+
+ ); } diff --git a/packages/ark/src/components/widget/content.tsx b/packages/ark/src/components/widget/content.tsx deleted file mode 100644 index 3d057b39..00000000 --- a/packages/ark/src/components/widget/content.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ReactNode } from 'react'; - -export function WidgetContent({ children }: { children: ReactNode }) { - return
{children}
; -} diff --git a/packages/ark/src/components/widget/header.tsx b/packages/ark/src/components/widget/header.tsx deleted file mode 100644 index c15223ee..00000000 --- a/packages/ark/src/components/widget/header.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { - ArrowLeftIcon, - ArrowRightIcon, - HorizontalDotsIcon, - RefreshIcon, - ThreadIcon, - TrashIcon, -} from '@lume/icons'; -import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; -import { useQueryClient } from '@tanstack/react-query'; -import { ReactNode } from 'react'; -import { useWidget } from '../../hooks/useWidget'; - -export function WidgetHeader({ - id, - title, - queryKey, - icon, -}: { - id: string; - title: string; - queryKey?: string[]; - icon?: ReactNode; -}) { - const queryClient = useQueryClient(); - const { removeWidget } = useWidget(); - - const refresh = async () => { - if (queryKey) await queryClient.refetchQueries({ queryKey }); - }; - - const moveLeft = async () => { - removeWidget.mutate(id); - }; - - const moveRight = async () => { - removeWidget.mutate(id); - }; - - const deleteWidget = async () => { - removeWidget.mutate(id); - }; - - return ( -
-
-
-
- {icon ? icon : } -
{title}
-
-
-
- - - - - - - - - - - - - - - - - - - - - - -
-
- ); -} diff --git a/packages/ark/src/components/widget/index.ts b/packages/ark/src/components/widget/index.ts deleted file mode 100644 index 6bfb8395..00000000 --- a/packages/ark/src/components/widget/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { WidgetContent } from "./content"; -import { WidgetHeader } from "./header"; -import { WidgetLive } from "./live"; -import { WidgetRoot } from "./root"; - -export const Widget = { - Root: WidgetRoot, - Live: WidgetLive, - Header: WidgetHeader, - Content: WidgetContent, -}; diff --git a/packages/ark/src/components/widget/live.tsx b/packages/ark/src/components/widget/live.tsx deleted file mode 100644 index 394b8a25..00000000 --- a/packages/ark/src/components/widget/live.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { ChevronUpIcon } from '@lume/icons'; -import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk'; -import { useEffect, useState } from 'react'; -import { useArk } from '../../provider'; - -export function WidgetLive({ - filter, - onClick, -}: { - filter: NDKFilter; - onClick: () => void; -}) { - const ark = useArk(); - const [events, setEvents] = useState([]); - - useEffect(() => { - const sub = ark.subscribe({ - filter, - closeOnEose: false, - cb: (event: NDKEvent) => setEvents((prev) => [...prev, event]), - }); - - return () => { - if (sub) sub.stop(); - }; - }, []); - - if (!events.length) return null; - - return ( -
- -
- ); -} diff --git a/packages/ark/src/components/widget/root.tsx b/packages/ark/src/components/widget/root.tsx deleted file mode 100644 index 9e3674f3..00000000 --- a/packages/ark/src/components/widget/root.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Resizable } from "re-resizable"; -import { ReactNode, useState } from "react"; -import { twMerge } from "tailwind-merge"; - -export function WidgetRoot({ - children, - className, -}: { - children: ReactNode; - className?: string; -}) { - const [width, setWidth] = useState(420); - - return ( - e.preventDefault()} - onResizeStop={(_e, _direction, _ref, d) => { - setWidth((prevWidth) => prevWidth + d.width); - }} - minWidth={420} - maxWidth={600} - className={twMerge( - "relative flex flex-col border-r-2 border-neutral-50 hover:border-neutral-100 dark:border-neutral-950 dark:hover:border-neutral-900", - className, - )} - enable={{ right: true }} - > - {children} - - ); -} diff --git a/packages/ark/src/index.ts b/packages/ark/src/index.ts index bd88a9b8..a4d68e34 100644 --- a/packages/ark/src/index.ts +++ b/packages/ark/src/index.ts @@ -1,6 +1,6 @@ export * from "./ark"; export * from "./provider"; -export * from "./components/widget"; +export * from "./components/column"; export * from "./components/note"; export * from "./hooks/useWidget"; export * from "./hooks/useRichContent"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53a1d77c..af0f1f1d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -383,6 +383,9 @@ importers: react: specifier: ^18.2.0 version: 18.2.0 + react-router-dom: + specifier: ^6.21.0 + version: 6.21.0(react-dom@18.2.0)(react@18.2.0) virtua: specifier: ^0.18.0 version: 0.18.0(react-dom@18.2.0)(react@18.2.0)