diff --git a/apps/desktop/package.json b/apps/desktop/package.json index d9ab8093..15c033b6 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -8,6 +8,7 @@ "build": "vite build" }, "dependencies": { + "@columns/group": "workspace:^", "@columns/hashtag": "workspace:^", "@columns/notification": "workspace:^", "@columns/thread": "workspace:^", diff --git a/apps/desktop/src/routes/home/index.tsx b/apps/desktop/src/routes/home/index.tsx index 52d11966..8fe9e08c 100644 --- a/apps/desktop/src/routes/home/index.tsx +++ b/apps/desktop/src/routes/home/index.tsx @@ -1,3 +1,4 @@ +import { Group } from "@columns/group"; import { Hashtag } from "@columns/hashtag"; import { Thread } from "@columns/thread"; import { Timeline } from "@columns/timeline"; @@ -23,6 +24,8 @@ export function HomeScreen() { return ; case COL_TYPES.hashtag: return ; + case COL_TYPES.group: + return ; default: return ; } diff --git a/packages/@columns/group/package.json b/packages/@columns/group/package.json new file mode 100644 index 00000000..54fe5f88 --- /dev/null +++ b/packages/@columns/group/package.json @@ -0,0 +1,26 @@ +{ + "name": "@columns/group", + "version": "0.0.0", + "private": true, + "main": "./src/index.tsx", + "dependencies": { + "@lume/ark": "workspace:^", + "@lume/icons": "workspace:^", + "@lume/ui": "workspace:^", + "@lume/utils": "workspace:^", + "@nostr-dev-kit/ndk": "^2.3.2", + "@tanstack/react-query": "^5.17.0", + "react": "^18.2.0", + "react-router-dom": "^6.21.1", + "sonner": "^1.3.1", + "virtua": "^0.18.0" + }, + "devDependencies": { + "@lume/tailwindcss": "workspace:^", + "@lume/tsconfig": "workspace:^", + "@lume/types": "workspace:^", + "@types/react": "^18.2.46", + "tailwind": "^4.0.0", + "typescript": "^5.3.3" + } +} diff --git a/packages/@columns/group/src/components/form.tsx b/packages/@columns/group/src/components/form.tsx new file mode 100644 index 00000000..8eaa2940 --- /dev/null +++ b/packages/@columns/group/src/components/form.tsx @@ -0,0 +1,92 @@ +import { useColumnContext, useStorage } from "@lume/ark"; +import { CancelIcon, CheckCircleIcon } from "@lume/icons"; +import { User } from "@lume/ui"; +import { useState } from "react"; + +export function GroupForm({ id }: { id: number }) { + const storage = useStorage(); + const { updateColumn, removeColumn } = useColumnContext(); + + const [title, setTitle] = useState(`Group-${id}`); + const [users, setUsers] = useState>([]); + + // toggle follow state + const toggleUser = (pubkey: string) => { + const arr = users.includes(pubkey) + ? users.filter((i) => i !== pubkey) + : [...users, pubkey]; + setUsers(arr); + }; + + const submit = async () => { + await updateColumn(id, title, JSON.stringify(users)); + }; + + return ( +
+
+
+

Create a new Group

+ +
+
+
+ + setTitle(e.target.value)} + placeholder="Nostrichs..." + className="px-3 rounded-xl border-neutral-200 dark:border-neutral-900 h-11 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" + /> +
+
+
+ + Pick user + + {`${users.length} / ∞`} +
+
+ {storage.account?.contacts?.map((item: string) => ( + + ))} +
+
+
+
+
+
+ +
+
+ ); +} diff --git a/packages/@columns/group/src/home.tsx b/packages/@columns/group/src/home.tsx new file mode 100644 index 00000000..ada62adb --- /dev/null +++ b/packages/@columns/group/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({ + colKey, + content, +}: { colKey: string; content: string }) { + const ark = useArk(); + const ref = useRef(); + const cacheKey = `${colKey}-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: [colKey], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const authors = JSON.parse(content); + const events = await ark.getInfiniteEvents({ + filter: { + kinds: [NDKKind.Text, NDKKind.Repost], + authors: authors, + }, + 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 ? ( +
+ +
+ ) : ( + allEvents.map((item) => renderItem(item)) + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+ ); +} diff --git a/packages/@columns/group/src/index.tsx b/packages/@columns/group/src/index.tsx new file mode 100644 index 00000000..bbbafd16 --- /dev/null +++ b/packages/@columns/group/src/index.tsx @@ -0,0 +1,32 @@ +import { Column } from "@lume/ark"; +import { GroupFeedsIcon } from "@lume/icons"; +import { IColumn } from "@lume/types"; +import { GroupForm } from "./components/form"; +import { HomeRoute } from "./home"; + +export function Group({ column }: { column: IColumn }) { + const colKey = `group-${column.id}`; + const created = !!column.content?.length; + + return ( + + {created ? ( + <> + } + /> + + } + /> + + + ) : ( + + )} + + ); +} diff --git a/packages/@columns/group/tailwind.config.js b/packages/@columns/group/tailwind.config.js new file mode 100644 index 00000000..49c48c7a --- /dev/null +++ b/packages/@columns/group/tailwind.config.js @@ -0,0 +1,8 @@ +import sharedConfig from "@lume/tailwindcss"; + +const config = { + content: ["./src/**/*.{js,ts,jsx,tsx}"], + presets: [sharedConfig], +}; + +export default config; diff --git a/packages/@columns/group/tsconfig.json b/packages/@columns/group/tsconfig.json new file mode 100644 index 00000000..34a32891 --- /dev/null +++ b/packages/@columns/group/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "@lume/tsconfig/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/@columns/hashtag/src/home.tsx b/packages/@columns/hashtag/src/home.tsx index 1f2bfd3c..c4de2b3d 100644 --- a/packages/@columns/hashtag/src/home.tsx +++ b/packages/@columns/hashtag/src/home.tsx @@ -12,7 +12,7 @@ export function HomeRoute({ }: { colKey: string; hashtag: string }) { const ark = useArk(); const ref = useRef(); - const cacheKey = "hashtag-vlist"; + const cacheKey = `${colKey}-vlist`; const [offset, cache] = useMemo(() => { const serialized = sessionStorage.getItem(cacheKey); @@ -84,19 +84,19 @@ export function HomeRoute({ )) )} -
+
{hasNextPage ? ( @@ -125,14 +124,14 @@ export function HomeRoute({ id }: { id: string }) { )} Message @@ -170,24 +169,24 @@ export function HomeRoute({ id }: { id: string }) {
{isLoading ? (
- +
) : ( allEvents.map((item) => renderItem(item)) )} -
+
{hasNextPage ? (