From b3b790588a099a5b8483039a5c0542bb45eeeacb Mon Sep 17 00:00:00 2001 From: Ren Amamiya <123083837+reyamir@users.noreply.github.com> Date: Mon, 17 Jul 2023 08:54:17 +0700 Subject: [PATCH] add hashtag block --- src/app/space/components/addFeed.tsx | 10 +- src/app/space/components/addImage.tsx | 15 +-- src/app/space/components/blocks/feed.tsx | 10 +- src/app/space/components/blocks/following.tsx | 6 +- src/app/space/components/blocks/hashtag.tsx | 102 ++++++++++++++++++ src/app/space/components/blocks/image.tsx | 4 +- src/app/space/components/blocks/thread.tsx | 4 +- src/app/space/index.tsx | 35 +++--- src/shared/notes/actions.tsx | 4 +- src/shared/notes/content.tsx | 19 ++-- src/shared/notes/hashtag.tsx | 36 +++++++ src/shared/notes/index.tsx | 1 + src/shared/notes/kinds/kind1.tsx | 4 +- src/stores/constants.tsx | 8 ++ src/utils/types.d.ts | 4 +- 15 files changed, 203 insertions(+), 59 deletions(-) create mode 100644 src/app/space/components/blocks/hashtag.tsx create mode 100644 src/shared/notes/hashtag.tsx diff --git a/src/app/space/components/addFeed.tsx b/src/app/space/components/addFeed.tsx index 33ced7ad..aa221100 100644 --- a/src/app/space/components/addFeed.tsx +++ b/src/app/space/components/addFeed.tsx @@ -12,7 +12,7 @@ import { createBlock } from '@libs/storage'; import { CancelIcon, CheckCircleIcon, CommandIcon, LoaderIcon } from '@shared/icons'; -import { DEFAULT_AVATAR } from '@stores/constants'; +import { BLOCK_KINDS, DEFAULT_AVATAR } from '@stores/constants'; import { ADD_FEEDBLOCK_SHORTCUT } from '@stores/shortcuts'; import { useAccount } from '@utils/hooks/useAccount'; @@ -38,7 +38,7 @@ export function AddFeedBlock() { useHotkeys(ADD_FEEDBLOCK_SHORTCUT, () => openModal()); const block = useMutation({ - mutationFn: (data: any) => { + mutationFn: (data: { kind: number; title: string; content: string }) => { return createBlock(data.kind, data.title, data.content); }, onSuccess: () => { @@ -53,7 +53,7 @@ export function AddFeedBlock() { formState: { isDirty, isValid }, } = useForm(); - const onSubmit = (data: any) => { + const onSubmit = (data: { kind: number; title: string; content: string }) => { setLoading(true); selected.forEach((item, index) => { @@ -64,7 +64,7 @@ export function AddFeedBlock() { // insert to database block.mutate({ - kind: 1, + kind: BLOCK_KINDS.feed, title: data.title, content: JSON.stringify(selected), }); @@ -205,7 +205,7 @@ export function AddFeedBlock() { {status === 'loading' ? (

Loading...

) : ( - JSON.parse(account.follows).map((follow) => ( + JSON.parse(account.follows as string).map((follow) => ( { @@ -101,7 +94,7 @@ export function AddImageBlock() { }; const block = useMutation({ - mutationFn: (data: any) => { + mutationFn: (data: { kind: number; title: string; content: string }) => { return createBlock(data.kind, data.title, data.content); }, onSuccess: () => { @@ -109,14 +102,14 @@ export function AddImageBlock() { }, }); - const onSubmit = async (data: any) => { + const onSubmit = async (data: { kind: number; title: string; content: string }) => { setLoading(true); // publish file metedata await publish({ content: data.title, kind: 1063, tags: tags.current }); // mutate - block.mutate({ kind: 0, title: data.title, content: data.content }); + block.mutate({ kind: BLOCK_KINDS.image, title: data.title, content: data.content }); setLoading(false); // reset form diff --git a/src/app/space/components/blocks/feed.tsx b/src/app/space/components/blocks/feed.tsx index 35fd9a1c..3b74d871 100644 --- a/src/app/space/components/blocks/feed.tsx +++ b/src/app/space/components/blocks/feed.tsx @@ -9,11 +9,11 @@ import { NoteKindUnsupport } from '@shared/notes/kinds/unsupport'; import { NoteSkeleton } from '@shared/notes/skeleton'; import { TitleBar } from '@shared/titleBar'; -import { LumeEvent } from '@utils/types'; +import { Block, LumeEvent } from '@utils/types'; const ITEM_PER_PAGE = 10; -export function FeedBlock({ params }: { params: any }) { +export function FeedBlock({ params }: { params: Block }) { const queryClient = useQueryClient(); const { status, data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({ @@ -24,7 +24,7 @@ export function FeedBlock({ params }: { params: any }) { getNextPageParam: (lastPage) => lastPage.nextCursor, }); - const notes = data ? data.pages.flatMap((d: { data: any }) => d.data) : []; + const notes = data ? data.pages.flatMap((d: { data: LumeEvent[] }) => d.data) : []; const parentRef = useRef(); const rowVirtualizer = useVirtualizer({ @@ -161,9 +161,7 @@ export function FeedBlock({ params }: { params: any }) { }px)`, }} > - {rowVirtualizer - .getVirtualItems() - .map((virtualRow) => renderItem(virtualRow.index))} + {itemsVirtualizer.map((virtualRow) => renderItem(virtualRow.index))} )} diff --git a/src/app/space/components/blocks/following.tsx b/src/app/space/components/blocks/following.tsx index aff97bf3..d2cc0676 100644 --- a/src/app/space/components/blocks/following.tsx +++ b/src/app/space/components/blocks/following.tsx @@ -36,7 +36,7 @@ export function FollowingBlock() { getNextPageParam: (lastPage) => lastPage.nextCursor, }); - const notes = data ? data.pages.flatMap((d: { data: any }) => d.data) : []; + const notes = data ? data.pages.flatMap((d: { data: LumeEvent[] }) => d.data) : []; const parentRef = useRef(); const rowVirtualizer = useVirtualizer({ @@ -198,9 +198,7 @@ export function FollowingBlock() { }px)`, }} > - {rowVirtualizer - .getVirtualItems() - .map((virtualRow) => renderItem(virtualRow.index))} + {itemsVirtualizer.map((virtualRow) => renderItem(virtualRow.index))} )} diff --git a/src/app/space/components/blocks/hashtag.tsx b/src/app/space/components/blocks/hashtag.tsx new file mode 100644 index 00000000..5d792b49 --- /dev/null +++ b/src/app/space/components/blocks/hashtag.tsx @@ -0,0 +1,102 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useVirtualizer } from '@tanstack/react-virtual'; +import { useRef } from 'react'; + +import { useNDK } from '@libs/ndk/provider'; +import { removeBlock } from '@libs/storage'; + +import { NoteKind_1, NoteSkeleton } from '@shared/notes'; +import { TitleBar } from '@shared/titleBar'; + +import { nHoursAgo } from '@utils/date'; +import { Block, LumeEvent } from '@utils/types'; + +export function HashtagBlock({ params }: { params: Block }) { + const queryClient = useQueryClient(); + const parentRef = useRef(); + + const { relayUrls, fetcher } = useNDK(); + const { status, data } = useQuery(['hashtag', params.content], async () => { + const events = (await fetcher.fetchAllEvents( + relayUrls, + { kinds: [1], '#t': [params.content] }, + { since: nHoursAgo(48) } + )) as unknown as LumeEvent[]; + return events; + }); + + const block = useMutation({ + mutationFn: (id: string) => { + return removeBlock(id); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['blocks'] }); + }, + }); + + const rowVirtualizer = useVirtualizer({ + count: data ? data.length : 0, + getScrollElement: () => parentRef.current, + estimateSize: () => 400, + }); + + const itemsVirtualizer = rowVirtualizer.getVirtualItems(); + + return ( +
+ block.mutate(params.id)} + /> +
+ {status === 'loading' ? ( +
+
+ +
+
+ ) : itemsVirtualizer.length === 0 ? ( +
+
+
+

+ No new posts about this hashtag in 48 hours ago +

+
+
+
+ ) : ( +
+
+ {itemsVirtualizer.map((virtualRow) => ( +
+ +
+ ))} +
+
+ )} +
+
+ ); +} diff --git a/src/app/space/components/blocks/image.tsx b/src/app/space/components/blocks/image.tsx index 05202221..1e2cc8f2 100644 --- a/src/app/space/components/blocks/image.tsx +++ b/src/app/space/components/blocks/image.tsx @@ -7,7 +7,9 @@ import { Image } from '@shared/image'; import { DEFAULT_AVATAR } from '@stores/constants'; -export function ImageBlock({ params }: { params: any }) { +import { Block } from '@utils/types'; + +export function ImageBlock({ params }: { params: Block }) { const queryClient = useQueryClient(); const block = useMutation({ diff --git a/src/app/space/components/blocks/thread.tsx b/src/app/space/components/blocks/thread.tsx index 5bc68727..a3fe7b06 100644 --- a/src/app/space/components/blocks/thread.tsx +++ b/src/app/space/components/blocks/thread.tsx @@ -5,7 +5,6 @@ import { useLiveThread } from '@app/space/hooks/useLiveThread'; import { getNoteByID, removeBlock } from '@libs/storage'; -import { NoteMetadata } from '@shared/notes/metadata'; import { NoteReplyForm } from '@shared/notes/replies/form'; import { RepliesList } from '@shared/notes/replies/list'; import { NoteSkeleton } from '@shared/notes/skeleton'; @@ -14,8 +13,9 @@ import { User } from '@shared/user'; import { useAccount } from '@utils/hooks/useAccount'; import { parser } from '@utils/parser'; +import { Block } from '@utils/types'; -export function ThreadBlock({ params }: { params: any }) { +export function ThreadBlock({ params }: { params: Block }) { useLiveThread(params.content); const queryClient = useQueryClient(); diff --git a/src/app/space/index.tsx b/src/app/space/index.tsx index 40908966..a8643542 100644 --- a/src/app/space/index.tsx +++ b/src/app/space/index.tsx @@ -1,8 +1,10 @@ import { useQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; import { AddBlock } from '@app/space/components/add'; import { FeedBlock } from '@app/space/components/blocks/feed'; import { FollowingBlock } from '@app/space/components/blocks/following'; +import { HashtagBlock } from '@app/space/components/blocks/hashtag'; import { ImageBlock } from '@app/space/components/blocks/image'; import { ThreadBlock } from '@app/space/components/blocks/thread'; @@ -10,6 +12,8 @@ import { getBlocks } from '@libs/storage'; import { LoaderIcon } from '@shared/icons'; +import { Block } from '@utils/types'; + export function SpaceScreen() { const { status, @@ -28,6 +32,24 @@ export function SpaceScreen() { } ); + const renderBlock = useCallback( + (block: Block) => { + switch (block.kind) { + case 0: + return ; + case 1: + return ; + case 2: + return ; + case 3: + return ; + default: + break; + } + }, + [blocks] + ); + return (
@@ -43,18 +65,7 @@ export function SpaceScreen() {
) : ( - blocks.map((block: { kind: number; id: string }) => { - switch (block.kind) { - case 0: - return ; - case 1: - return ; - case 2: - return ; - default: - break; - } - }) + blocks.map((block: Block) => renderBlock(block)) )} {isFetching && (
diff --git a/src/shared/notes/actions.tsx b/src/shared/notes/actions.tsx index c106917b..9255850d 100644 --- a/src/shared/notes/actions.tsx +++ b/src/shared/notes/actions.tsx @@ -9,6 +9,8 @@ import { NoteReply } from '@shared/notes/actions/reply'; import { NoteRepost } from '@shared/notes/actions/repost'; import { NoteZap } from '@shared/notes/actions/zap'; +import { BLOCK_KINDS } from '@stores/constants'; + export function NoteActions({ id, pubkey }: { id: string; pubkey: string }) { const queryClient = useQueryClient(); @@ -22,7 +24,7 @@ export function NoteActions({ id, pubkey }: { id: string; pubkey: string }) { }); const openThread = (thread: string) => { - block.mutate({ kind: 2, title: 'Thread', content: thread }); + block.mutate({ kind: BLOCK_KINDS.thread, title: 'Thread', content: thread }); }; return ( diff --git a/src/shared/notes/content.tsx b/src/shared/notes/content.tsx index fe728ef3..e46a4990 100644 --- a/src/shared/notes/content.tsx +++ b/src/shared/notes/content.tsx @@ -2,6 +2,7 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { + Hashtag, ImagePreview, LinkPreview, MentionNote, @@ -30,24 +31,16 @@ export function NoteContent({ del: ({ children }) => { const key = children[0] as string; if (key.startsWith('pub')) return ; - if (key.startsWith('tag')) - return ( - - ); + if (key.startsWith('tag')) return ; }, }} > {content.parsed} - {content.images.length > 0 && } - {content.videos.length > 0 && } - {content.links.length > 0 && } - {content.notes.length > 0 && + {content.images?.length > 0 && } + {content.videos?.length > 0 && } + {content.links?.length > 0 && } + {content.notes?.length > 0 && content.notes.map((note: string) => )} ); diff --git a/src/shared/notes/hashtag.tsx b/src/shared/notes/hashtag.tsx new file mode 100644 index 00000000..5b29c425 --- /dev/null +++ b/src/shared/notes/hashtag.tsx @@ -0,0 +1,36 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { createBlock } from '@libs/storage'; + +import { BLOCK_KINDS } from '@stores/constants'; + +export function Hashtag({ tag }: { tag: string }) { + const queryClient = useQueryClient(); + + const block = useMutation({ + mutationFn: (data: { kind: number; title: string; content: string }) => { + return createBlock(data.kind, data.title, data.content); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['blocks'] }); + }, + }); + + const openBlock = () => { + block.mutate({ + kind: BLOCK_KINDS.hashtag, + title: tag, + content: tag.replace('#', ''), + }); + }; + + return ( + + ); +} diff --git a/src/shared/notes/index.tsx b/src/shared/notes/index.tsx index b12f5be9..a9ba8762 100644 --- a/src/shared/notes/index.tsx +++ b/src/shared/notes/index.tsx @@ -21,3 +21,4 @@ export * from './kinds/sub'; export * from './skeleton'; export * from './actions'; export * from './content'; +export * from './hashtag'; diff --git a/src/shared/notes/kinds/kind1.tsx b/src/shared/notes/kinds/kind1.tsx index 13730902..f6bc0c0c 100644 --- a/src/shared/notes/kinds/kind1.tsx +++ b/src/shared/notes/kinds/kind1.tsx @@ -24,11 +24,11 @@ export function NoteKind_1({
- +
{!skipMetadata ? ( - + ) : (
)} diff --git a/src/stores/constants.tsx b/src/stores/constants.tsx index 1673abc4..0c26d95e 100644 --- a/src/stores/constants.tsx +++ b/src/stores/constants.tsx @@ -70,3 +70,11 @@ export const FULL_RELAYS = [ 'wss://relay.nostr.band/all', 'wss://nostr.mutinywallet.com', ]; + +export const BLOCK_KINDS = { + image: 0, + feed: 1, + thread: 2, + hashtag: 3, + exchange_rate: 4, +}; diff --git a/src/utils/types.d.ts b/src/utils/types.d.ts index b2068820..17ff140c 100644 --- a/src/utils/types.d.ts +++ b/src/utils/types.d.ts @@ -1,8 +1,8 @@ import { NDKEvent } from '@nostr-dev-kit/ndk'; export interface LumeEvent extends NDKEvent { - event_id: string; - parent_id: string; + event_id?: string; + parent_id?: string; } export interface Account {