From b1d2496f8ee73fed4a3371bbb2901b01b2cd3c48 Mon Sep 17 00:00:00 2001 From: reya Date: Sat, 30 Dec 2023 17:33:04 +0700 Subject: [PATCH] feat(column): add hashtag column --- apps/desktop/package.json | 1 + apps/desktop/src/routes/home/index.tsx | 16 +- apps/desktop/src/routes/new/post.tsx | 4 +- packages/@columns/hashtag/package.json | 26 +++ packages/@columns/hashtag/src/event.tsx | 29 +++ packages/@columns/hashtag/src/home.tsx | 109 +++++++++ packages/@columns/hashtag/src/index.tsx | 30 +++ packages/@columns/hashtag/src/user.tsx | 213 ++++++++++++++++++ packages/@columns/hashtag/tailwind.config.js | 8 + packages/@columns/hashtag/tsconfig.json | 8 + packages/@columns/thread/src/index.tsx | 8 +- packages/@columns/timeline/src/event.tsx | 2 +- packages/@columns/user/src/home.tsx | 2 +- packages/@columns/user/src/index.tsx | 10 +- packages/ark/src/ark.ts | 2 - .../ark/src/components/column/provider.tsx | 4 +- .../ark/src/components/note/buttons/pin.tsx | 4 +- .../src/components/note/mentions/hashtag.tsx | 6 +- .../ark/src/components/note/mentions/user.tsx | 4 +- packages/icons/src/hashtag.tsx | 42 ++-- packages/utils/src/constants.ts | 2 +- pnpm-lock.yaml | 55 +++++ 22 files changed, 533 insertions(+), 52 deletions(-) create mode 100644 packages/@columns/hashtag/package.json create mode 100644 packages/@columns/hashtag/src/event.tsx create mode 100644 packages/@columns/hashtag/src/home.tsx create mode 100644 packages/@columns/hashtag/src/index.tsx create mode 100644 packages/@columns/hashtag/src/user.tsx create mode 100644 packages/@columns/hashtag/tailwind.config.js create mode 100644 packages/@columns/hashtag/tsconfig.json diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 9455ae81..60f81736 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -8,6 +8,7 @@ "build": "vite build" }, "dependencies": { + "@columns/hashtag": "workspace:^", "@columns/notification": "workspace:^", "@columns/thread": "workspace:^", "@columns/timeline": "workspace:^", diff --git a/apps/desktop/src/routes/home/index.tsx b/apps/desktop/src/routes/home/index.tsx index 99c0c3c4..fe586296 100644 --- a/apps/desktop/src/routes/home/index.tsx +++ b/apps/desktop/src/routes/home/index.tsx @@ -1,9 +1,10 @@ +import { Hashtag } from "@columns/hashtag"; import { Thread } from "@columns/thread"; import { Timeline } from "@columns/timeline"; import { User } from "@columns/user"; import { useColumnContext } from "@lume/ark"; import { IColumn } from "@lume/types"; -import { WIDGET_KIND } from "@lume/utils"; +import { COL_TYPES } from "@lume/utils"; import { useRef, useState } from "react"; import { VList, VListHandle } from "virtua"; @@ -14,12 +15,14 @@ export function HomeScreen() { const renderItem = (column: IColumn) => { switch (column.kind) { - case WIDGET_KIND.newsfeed: + case COL_TYPES.newsfeed: return ; - case WIDGET_KIND.thread: - return ; - case WIDGET_KIND.user: - return ; + case COL_TYPES.thread: + return ; + case COL_TYPES.user: + return ; + case COL_TYPES.hashtag: + return ; default: return ; } @@ -64,7 +67,6 @@ export function HomeScreen() { }} > {columns.map((column) => renderItem(column))} -
); diff --git a/apps/desktop/src/routes/new/post.tsx b/apps/desktop/src/routes/new/post.tsx index b87669a1..15332d00 100644 --- a/apps/desktop/src/routes/new/post.tsx +++ b/apps/desktop/src/routes/new/post.tsx @@ -1,6 +1,6 @@ import { MentionNote, useArk, useSuggestion, useWidget } from "@lume/ark"; import { CancelIcon, LoaderIcon } from "@lume/icons"; -import { WIDGET_KIND } from "@lume/utils"; +import { COL_TYPES } from "@lume/utils"; import { NDKKind } from "@nostr-dev-kit/ndk"; import CharacterCount from "@tiptap/extension-character-count"; import Image from "@tiptap/extension-image"; @@ -101,7 +101,7 @@ export function NewPostScreen() { addWidget.mutate({ title: "Thread", content: publish.id, - kind: WIDGET_KIND.thread, + kind: COL_TYPES.thread, }); } diff --git a/packages/@columns/hashtag/package.json b/packages/@columns/hashtag/package.json new file mode 100644 index 00000000..5bfa8a12 --- /dev/null +++ b/packages/@columns/hashtag/package.json @@ -0,0 +1,26 @@ +{ + "name": "@columns/hashtag", + "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.15.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/hashtag/src/event.tsx b/packages/@columns/hashtag/src/event.tsx new file mode 100644 index 00000000..1174c079 --- /dev/null +++ b/packages/@columns/hashtag/src/event.tsx @@ -0,0 +1,29 @@ +import { ThreadNote } from "@lume/ark"; +import { ArrowLeftIcon } from "@lume/icons"; +import { ReplyList } from "@lume/ui"; +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/hashtag/src/home.tsx b/packages/@columns/hashtag/src/home.tsx new file mode 100644 index 00000000..1f2bfd3c --- /dev/null +++ b/packages/@columns/hashtag/src/home.tsx @@ -0,0 +1,109 @@ +import { TextNote, useArk } from "@lume/ark"; +import { ArrowRightCircleIcon, LoaderIcon } from "@lume/icons"; +import { FETCH_LIMIT } from "@lume/utils"; +import { 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, + hashtag, +}: { colKey: string; hashtag: string }) { + const ark = useArk(); + const ref = useRef(); + const cacheKey = "hashtag-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 events = await ark.getInfiniteEvents({ + filter: { + kinds: [NDKKind.Text], + "#t": [hashtag], + }, + 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], + ); + + 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) => ( + + )) + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+ ); +} diff --git a/packages/@columns/hashtag/src/index.tsx b/packages/@columns/hashtag/src/index.tsx new file mode 100644 index 00000000..7f3037f6 --- /dev/null +++ b/packages/@columns/hashtag/src/index.tsx @@ -0,0 +1,30 @@ +import { Column } from "@lume/ark"; +import { HashtagIcon, TimelineIcon } from "@lume/icons"; +import { IColumn } from "@lume/types"; +import { EventRoute } from "./event"; +import { HomeRoute } from "./home"; +import { UserRoute } from "./user"; + +export function Hashtag({ column }: { column: IColumn }) { + const colKey = "hashtag"; + const hashtag = column.content.replace("#", ""); + + return ( + + } + /> + + } + /> + } /> + } /> + + + ); +} diff --git a/packages/@columns/hashtag/src/user.tsx b/packages/@columns/hashtag/src/user.tsx new file mode 100644 index 00000000..8d46f957 --- /dev/null +++ b/packages/@columns/hashtag/src/user.tsx @@ -0,0 +1,213 @@ +import { + RepostNote, + TextNote, + useArk, + useProfile, + useStorage, +} from "@lume/ark"; +import { ArrowLeftIcon, ArrowRightCircleIcon, LoaderIcon } from "@lume/icons"; +import { NIP05 } from "@lume/ui"; +import { FETCH_LIMIT, displayNpub } from "@lume/utils"; +import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; +import { useInfiniteQuery } from "@tanstack/react-query"; +import { useEffect, useMemo, useState } from "react"; +import { Link, useNavigate, useParams } from "react-router-dom"; +import { toast } from "sonner"; +import { WVList } from "virtua"; + +export function UserRoute() { + const ark = useArk(); + const storage = useStorage(); + const navigate = useNavigate(); + + const { id } = useParams(); + const { user } = useProfile(id); + const { data, hasNextPage, isLoading, isFetchingNextPage, fetchNextPage } = + useInfiniteQuery({ + queryKey: ["user-posts", id], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const events = await ark.getInfiniteEvents({ + filter: { + kinds: [NDKKind.Text, NDKKind.Repost], + authors: [id], + }, + 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 [followed, setFollowed] = useState(false); + + const allEvents = useMemo( + () => (data ? data.pages.flatMap((page) => page) : []), + [data], + ); + + const follow = async (pubkey: string) => { + try { + const add = await ark.createContact({ pubkey }); + if (add) { + setFollowed(true); + } else { + toast.success("You already follow this user"); + } + } catch (error) { + console.log(error); + } + }; + + const unfollow = async (pubkey: string) => { + try { + const remove = await ark.deleteContact({ pubkey }); + if (remove) { + setFollowed(false); + } + } catch (error) { + console.log(error); + } + }; + + const renderItem = (event: NDKEvent) => { + switch (event.kind) { + case NDKKind.Text: + return ; + case NDKKind.Repost: + return ; + default: + return ; + } + }; + + useEffect(() => { + if (storage.account.contacts.includes(id)) { + setFollowed(true); + } + }, []); + + return ( + +
+ +
+
+
+
+ {id} +
+ {followed ? ( + + ) : ( + + )} + + Message + +
+
+
+
+
+ {user?.name || + user?.display_name || + user?.displayName || + "Anon"} +
+ {user?.nip05 ? ( + + ) : ( + + {displayNpub(id, 16)} + + )} +
+
+ {user?.about} +
+
+
+
+

+ Latest posts +

+
+ {isLoading ? ( +
+ +
+ ) : ( + allEvents.map((item) => renderItem(item)) + )} +
+ {hasNextPage ? ( + + ) : null} +
+
+
+
+
+ ); +} diff --git a/packages/@columns/hashtag/tailwind.config.js b/packages/@columns/hashtag/tailwind.config.js new file mode 100644 index 00000000..49c48c7a --- /dev/null +++ b/packages/@columns/hashtag/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/hashtag/tsconfig.json b/packages/@columns/hashtag/tsconfig.json new file mode 100644 index 00000000..34a32891 --- /dev/null +++ b/packages/@columns/hashtag/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/thread/src/index.tsx b/packages/@columns/thread/src/index.tsx index a7e0b415..0cbb6150 100644 --- a/packages/@columns/thread/src/index.tsx +++ b/packages/@columns/thread/src/index.tsx @@ -1,15 +1,15 @@ import { Column } from "@lume/ark"; -import { WidgetProps } from "@lume/types"; +import { IColumn } from "@lume/types"; import { EventRoute } from "./event"; import { HomeRoute } from "./home"; import { UserRoute } from "./user"; -export function Thread({ thread }: { thread: WidgetProps }) { +export function Thread({ column }: { column: IColumn }) { return ( - + - } /> + } /> } /> } /> diff --git a/packages/@columns/timeline/src/event.tsx b/packages/@columns/timeline/src/event.tsx index dc9245cf..1174c079 100644 --- a/packages/@columns/timeline/src/event.tsx +++ b/packages/@columns/timeline/src/event.tsx @@ -1,4 +1,4 @@ -import { Note, ThreadNote } from "@lume/ark"; +import { ThreadNote } from "@lume/ark"; import { ArrowLeftIcon } from "@lume/icons"; import { ReplyList } from "@lume/ui"; import { useNavigate, useParams } from "react-router-dom"; diff --git a/packages/@columns/user/src/home.tsx b/packages/@columns/user/src/home.tsx index 30f960e9..f6fe1590 100644 --- a/packages/@columns/user/src/home.tsx +++ b/packages/@columns/user/src/home.tsx @@ -11,7 +11,7 @@ import { FETCH_LIMIT, displayNpub } from "@lume/utils"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import { useInfiniteQuery } from "@tanstack/react-query"; import { useEffect, useMemo, useState } from "react"; -import { Link, useNavigate, useParams } from "react-router-dom"; +import { Link, useNavigate } from "react-router-dom"; import { toast } from "sonner"; import { WVList } from "virtua"; diff --git a/packages/@columns/user/src/index.tsx b/packages/@columns/user/src/index.tsx index e34c21cd..5753b93d 100644 --- a/packages/@columns/user/src/index.tsx +++ b/packages/@columns/user/src/index.tsx @@ -1,20 +1,20 @@ import { Column } from "@lume/ark"; import { UserIcon } from "@lume/icons"; -import { WidgetProps } from "@lume/types"; +import { IColumn } from "@lume/types"; import { EventRoute } from "./event"; import { HomeRoute } from "./home"; import { UserRoute } from "./user"; -export function User({ user }: { user: WidgetProps }) { +export function User({ column }: { column: IColumn }) { return ( } /> - } /> + } /> } /> } /> diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 574cbff6..d3c3313a 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -372,8 +372,6 @@ export class Ark { signal?: AbortSignal; dedup?: boolean; }) { - if (!filter?.authors?.length) return []; - const rootIds = new Set(); const dedupQueue = new Set(); const connectedRelays = this.ndk.pool diff --git a/packages/ark/src/components/column/provider.tsx b/packages/ark/src/components/column/provider.tsx index 6759d587..7f6583e6 100644 --- a/packages/ark/src/components/column/provider.tsx +++ b/packages/ark/src/components/column/provider.tsx @@ -1,5 +1,5 @@ import { IColumn } from "@lume/types"; -import { WIDGET_KIND } from "@lume/utils"; +import { COL_TYPES } from "@lume/utils"; import { NDKEvent } from "@nostr-dev-kit/ndk"; import { ReactNode, @@ -27,7 +27,7 @@ export function ColumnProvider({ children }: { children: ReactNode }) { id: "9999", title: "Newsfeed", content: "", - kind: WIDGET_KIND.newsfeed, + kind: COL_TYPES.newsfeed, }, ]); diff --git a/packages/ark/src/components/note/buttons/pin.tsx b/packages/ark/src/components/note/buttons/pin.tsx index 139af123..7f5b0081 100644 --- a/packages/ark/src/components/note/buttons/pin.tsx +++ b/packages/ark/src/components/note/buttons/pin.tsx @@ -1,5 +1,5 @@ import { PinIcon } from "@lume/icons"; -import { WIDGET_KIND } from "@lume/utils"; +import { COL_TYPES } from "@lume/utils"; import * as Tooltip from "@radix-ui/react-tooltip"; import { useNoteContext } from ".."; import { useColumnContext } from "../../column"; @@ -16,7 +16,7 @@ export function NotePin() { type="button" onClick={() => addColumn({ - kind: WIDGET_KIND.thread, + kind: COL_TYPES.thread, title: "Thread", content: event.id, }) diff --git a/packages/ark/src/components/note/mentions/hashtag.tsx b/packages/ark/src/components/note/mentions/hashtag.tsx index 4bf27022..bfddf5f5 100644 --- a/packages/ark/src/components/note/mentions/hashtag.tsx +++ b/packages/ark/src/components/note/mentions/hashtag.tsx @@ -1,4 +1,4 @@ -import { WIDGET_KIND } from "@lume/utils"; +import { COL_TYPES } from "@lume/utils"; import { useColumnContext } from "../../column"; export function Hashtag({ tag }: { tag: string }) { @@ -9,9 +9,9 @@ export function Hashtag({ tag }: { tag: string }) { type="button" onClick={() => addColumn({ - kind: WIDGET_KIND.hashtag, + kind: COL_TYPES.hashtag, title: tag, - content: tag.replace("#", ""), + content: tag, }) } className="cursor-default break-all text-blue-500 hover:text-blue-600" diff --git a/packages/ark/src/components/note/mentions/user.tsx b/packages/ark/src/components/note/mentions/user.tsx index 3c154acd..c19583c9 100644 --- a/packages/ark/src/components/note/mentions/user.tsx +++ b/packages/ark/src/components/note/mentions/user.tsx @@ -1,4 +1,4 @@ -import { WIDGET_KIND } from "@lume/utils"; +import { COL_TYPES } from "@lume/utils"; import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; import { memo } from "react"; import { Link } from "react-router-dom"; @@ -30,7 +30,7 @@ export const MentionUser = memo(function MentionUser({ type="button" onClick={() => addColumn({ - kind: WIDGET_KIND.user, + kind: COL_TYPES.user, title: user?.name || user?.displayName || "", content: pubkey, }) diff --git a/packages/icons/src/hashtag.tsx b/packages/icons/src/hashtag.tsx index 98dc565c..167afaf5 100644 --- a/packages/icons/src/hashtag.tsx +++ b/packages/icons/src/hashtag.tsx @@ -1,22 +1,24 @@ -import { SVGProps } from 'react'; +import { SVGProps } from "react"; -export function HashtagIcon(props: JSX.IntrinsicAttributes & SVGProps) { - return ( - - - - ); +export function HashtagIcon( + props: JSX.IntrinsicAttributes & SVGProps, +) { + return ( + + + + ); } diff --git a/packages/utils/src/constants.ts b/packages/utils/src/constants.ts index 911c7f49..b1f2a2fc 100644 --- a/packages/utils/src/constants.ts +++ b/packages/utils/src/constants.ts @@ -31,7 +31,7 @@ export const HASHTAGS = [ { hashtag: "#primal" }, ]; -export const WIDGET_KIND = { +export const COL_TYPES = { user: 1, thread: 2, group: 3, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 32c9d7f7..402e9641 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: apps/desktop: dependencies: + '@columns/hashtag': + specifier: workspace:^ + version: link:../../packages/@columns/hashtag '@columns/notification': specifier: workspace:^ version: link:../../packages/@columns/notification @@ -286,6 +289,58 @@ importers: specifier: ^4.2.3 version: 4.2.3(typescript@5.3.3)(vite@4.5.1) + packages/@columns/hashtag: + dependencies: + '@lume/ark': + specifier: workspace:^ + version: link:../../ark + '@lume/icons': + specifier: workspace:^ + version: link:../../icons + '@lume/ui': + specifier: workspace:^ + version: link:../../ui + '@lume/utils': + specifier: workspace:^ + version: link:../../utils + '@nostr-dev-kit/ndk': + specifier: ^2.3.2 + version: 2.3.2(typescript@5.3.3) + '@tanstack/react-query': + specifier: ^5.15.0 + version: 5.15.0(react@18.2.0) + react: + specifier: ^18.2.0 + version: 18.2.0 + react-router-dom: + specifier: ^6.21.1 + version: 6.21.1(react-dom@18.2.0)(react@18.2.0) + sonner: + specifier: ^1.3.1 + version: 1.3.1(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) + devDependencies: + '@lume/tailwindcss': + specifier: workspace:^ + version: link:../../tailwindcss + '@lume/tsconfig': + specifier: workspace:^ + version: link:../../tsconfig + '@lume/types': + specifier: workspace:^ + version: link:../../types + '@types/react': + specifier: ^18.2.46 + version: 18.2.46 + tailwind: + specifier: ^4.0.0 + version: 4.0.0 + typescript: + specifier: ^5.3.3 + version: 5.3.3 + packages/@columns/notification: dependencies: '@lume/ark':