From 5606dcb32fd876e98a11857bf964512a23a528d8 Mon Sep 17 00:00:00 2001
From: Ren Amamiya <123083837+reyamir@users.noreply.github.com>
Date: Mon, 17 Jul 2023 13:37:01 +0700
Subject: [PATCH] update replies

---
 src/app/note/index.tsx                     | 15 ++---
 src/app/space/components/blocks/thread.tsx | 46 +++++--------
 src/libs/storage.tsx                       |  4 +-
 src/shared/notes/index.tsx                 |  2 +
 src/shared/notes/mentions/user.tsx         |  2 +-
 src/shared/notes/replies/item.tsx          | 22 +++++--
 src/shared/notes/replies/list.tsx          | 20 ++++--
 src/shared/notes/stats.tsx                 | 76 ++++++++++++++++++++++
 src/shared/notes/users/thread.tsx          | 46 +++++++++++++
 src/utils/shortenKey.tsx                   | 14 ++++
 src/utils/transform.tsx                    |  2 +-
 11 files changed, 193 insertions(+), 56 deletions(-)
 create mode 100644 src/shared/notes/stats.tsx
 create mode 100644 src/shared/notes/users/thread.tsx

diff --git a/src/app/note/index.tsx b/src/app/note/index.tsx
index 20016f4e..bd019934 100644
--- a/src/app/note/index.tsx
+++ b/src/app/note/index.tsx
@@ -1,10 +1,7 @@
-import { useQuery } from '@tanstack/react-query';
 import { useParams } from 'react-router-dom';
 
 import { useLiveThread } from '@app/space/hooks/useLiveThread';
 
-import { getNoteByID } from '@libs/storage';
-
 import { NoteMetadata } from '@shared/notes/metadata';
 import { NoteReplyForm } from '@shared/notes/replies/form';
 import { RepliesList } from '@shared/notes/replies/list';
@@ -12,16 +9,12 @@ import { NoteSkeleton } from '@shared/notes/skeleton';
 import { User } from '@shared/user';
 
 import { useAccount } from '@utils/hooks/useAccount';
-import { parser } from '@utils/parser';
+import { useEvent } from '@utils/hooks/useEvent';
 
 export function NoteScreen() {
   const { id } = useParams();
   const { account } = useAccount();
-  const { status, data } = useQuery(['thread', id], async () => {
-    const res = await getNoteByID(id);
-    res['content'] = parser(res);
-    return res;
-  });
+  const { status, data } = useEvent(id);
 
   useLiveThread(id);
 
@@ -39,7 +32,7 @@ export function NoteScreen() {
             <div className="rounded-md bg-zinc-900 px-5 pt-5">
               <User pubkey={data.pubkey} time={data.created_at} />
               <div className="mt-3">
-                <NoteMetadata id={data.event_id || id} eventPubkey={data.pubkey} />
+                <NoteMetadata id={data.event_id || id} />
               </div>
             </div>
             <div className="mt-3 rounded-md bg-zinc-900">
@@ -48,7 +41,7 @@ export function NoteScreen() {
           </div>
         )}
         <div className="px-3">
-          <RepliesList parent_id={id} />
+          <RepliesList id={id} />
         </div>
       </div>
     </div>
diff --git a/src/app/space/components/blocks/thread.tsx b/src/app/space/components/blocks/thread.tsx
index a3fe7b06..f1e37821 100644
--- a/src/app/space/components/blocks/thread.tsx
+++ b/src/app/space/components/blocks/thread.tsx
@@ -1,31 +1,20 @@
-import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
-import { Link } from 'react-router-dom';
+import { useMutation, useQueryClient } from '@tanstack/react-query';
 
-import { useLiveThread } from '@app/space/hooks/useLiveThread';
+// import { useLiveThread } from '@app/space/hooks/useLiveThread';
+import { removeBlock } from '@libs/storage';
 
-import { getNoteByID, removeBlock } from '@libs/storage';
-
-import { NoteReplyForm } from '@shared/notes/replies/form';
+import { NoteContent, NoteStats, ThreadUser } from '@shared/notes';
 import { RepliesList } from '@shared/notes/replies/list';
 import { NoteSkeleton } from '@shared/notes/skeleton';
 import { TitleBar } from '@shared/titleBar';
-import { User } from '@shared/user';
 
-import { useAccount } from '@utils/hooks/useAccount';
-import { parser } from '@utils/parser';
+import { useEvent } from '@utils/hooks/useEvent';
 import { Block } from '@utils/types';
 
 export function ThreadBlock({ params }: { params: Block }) {
-  useLiveThread(params.content);
-
   const queryClient = useQueryClient();
 
-  const { account } = useAccount();
-  const { status, data } = useQuery(['thread', params.content], async () => {
-    const res = await getNoteByID(params.content);
-    res['content'] = parser(res);
-    return res;
-  });
+  const { status, data } = useEvent(params.content);
 
   const block = useMutation({
     mutationFn: (id: string) => {
@@ -36,33 +25,34 @@ export function ThreadBlock({ params }: { params: Block }) {
     },
   });
 
+  // subscribe to live reply
+  // useLiveThread(params.content);
+
   return (
     <div className="w-[400px] shrink-0 border-r border-zinc-900">
       <TitleBar title={params.title} onClick={() => block.mutate(params.id)} />
       <div className="scrollbar-hide flex h-full w-full flex-col gap-1.5 overflow-y-auto pb-20 pt-1.5">
         {status === 'loading' ? (
           <div className="px-3 py-1.5">
-            <div className="shadow-input rounded-md bg-zinc-900 px-3 py-3 shadow-black/20">
+            <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 py-3">
               <NoteSkeleton />
             </div>
           </div>
         ) : (
           <div className="h-min w-full px-3 py-1.5">
-            <div className="rounded-md bg-zinc-900 px-5 pt-5">
-              <User pubkey={data.pubkey} time={data.created_at} />
-              <div className="mt-3">
-                <Link to={`/app/note/${params.content}`}>Focus</Link>
+            <div className="rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
+              <ThreadUser pubkey={data.pubkey} time={data.created_at} />
+              <div className="mt-2">
+                <NoteContent content={data.content} />
+              </div>
+              <div>
+                <NoteStats id={data.id} />
               </div>
-            </div>
-            <div className="mt-3 rounded-md bg-zinc-900">
-              {account && (
-                <NoteReplyForm rootID={params.content} userPubkey={account.pubkey} />
-              )}
             </div>
           </div>
         )}
         <div className="px-3">
-          <RepliesList parent_id={params.content} />
+          <RepliesList id={params.content} />
         </div>
       </div>
     </div>
diff --git a/src/libs/storage.tsx b/src/libs/storage.tsx
index 0d568fc8..61369494 100644
--- a/src/libs/storage.tsx
+++ b/src/libs/storage.tsx
@@ -169,7 +169,7 @@ export async function createNote(
   event_id: string,
   pubkey: string,
   kind: number,
-  tags: string[],
+  tags: string[][],
   content: string,
   created_at: number
 ) {
@@ -186,7 +186,7 @@ export async function createNote(
 // get note replies
 export async function getReplies(parent_id: string) {
   const db = await connect();
-  const result: any = await db.select(
+  const result: Array<LumeEvent> = await db.select(
     `SELECT * FROM replies WHERE parent_id = "${parent_id}" ORDER BY created_at DESC;`
   );
   return result;
diff --git a/src/shared/notes/index.tsx b/src/shared/notes/index.tsx
index a9ba8762..0f7531e8 100644
--- a/src/shared/notes/index.tsx
+++ b/src/shared/notes/index.tsx
@@ -15,6 +15,7 @@ export * from './kinds/kind1063';
 export * from './metadata';
 export * from './users/mini';
 export * from './users/repost';
+export * from './users/thread';
 export * from './kinds/thread';
 export * from './kinds/repost';
 export * from './kinds/sub';
@@ -22,3 +23,4 @@ export * from './skeleton';
 export * from './actions';
 export * from './content';
 export * from './hashtag';
+export * from './stats';
diff --git a/src/shared/notes/mentions/user.tsx b/src/shared/notes/mentions/user.tsx
index d2e9bd36..c0ddf550 100644
--- a/src/shared/notes/mentions/user.tsx
+++ b/src/shared/notes/mentions/user.tsx
@@ -9,7 +9,7 @@ export function MentionUser({ pubkey }: { pubkey: string }) {
       type="button"
       className="break-words rounded bg-zinc-800 px-2 py-px text-sm font-normal text-blue-400 no-underline hover:bg-zinc-700 hover:text-blue-500"
     >
-      @{user?.name || user?.displayName || shortenKey(pubkey)}
+      {'@' + user?.name || user?.displayName || shortenKey(pubkey)}
     </button>
   );
 }
diff --git a/src/shared/notes/replies/item.tsx b/src/shared/notes/replies/item.tsx
index c6718bd8..98b5d19b 100644
--- a/src/shared/notes/replies/item.tsx
+++ b/src/shared/notes/replies/item.tsx
@@ -1,17 +1,25 @@
-import { NoteMetadata } from '@shared/notes/metadata';
+import { NoteActions, NoteContent } from '@shared/notes';
 import { User } from '@shared/user';
 
 import { parser } from '@utils/parser';
+import { LumeEvent } from '@utils/types';
 
-export function Reply({ data }: { data: any }) {
+export function Reply({ data }: { data: LumeEvent }) {
   const content = parser(data);
 
   return (
-    <div className="mb-3 flex h-min min-h-min w-full select-text flex-col rounded-md bg-zinc-900 px-3 pt-5">
-      <div className="flex flex-col">
-        <User pubkey={data.pubkey} time={data.created_at} />
-        <div className="-mt-[20px] pl-[50px]">
-          <NoteMetadata id={data.event_id} eventPubkey={data.pubkey} />
+    <div className="h-min w-full py-1.5">
+      <div className="relative overflow-hidden rounded-xl border-t border-zinc-800/50 bg-zinc-900 px-3 pt-3">
+        <div className="relative flex flex-col">
+          <User pubkey={data.pubkey} time={data.created_at} />
+          <div className="relative z-20 -mt-6 flex items-start gap-3">
+            <div className="w-11 shrink-0" />
+            <div className="flex-1">
+              <NoteContent content={content} />
+              <NoteActions id={data.event_id || data.id} pubkey={data.pubkey} />
+            </div>
+          </div>
+          <div className="pb-3" />
         </div>
       </div>
     </div>
diff --git a/src/shared/notes/replies/list.tsx b/src/shared/notes/replies/list.tsx
index d27b6622..7be43a9a 100644
--- a/src/shared/notes/replies/list.tsx
+++ b/src/shared/notes/replies/list.tsx
@@ -1,17 +1,25 @@
 import { NDKEvent } from '@nostr-dev-kit/ndk';
 import { useQuery } from '@tanstack/react-query';
 
-import { getReplies } from '@libs/storage';
+import { useNDK } from '@libs/ndk/provider';
 
 import { Reply } from '@shared/notes/replies/item';
 
-export function RepliesList({ parent_id }: { parent_id: string }) {
-  const { status, data } = useQuery(['replies', parent_id], async () => {
-    return await getReplies(parent_id);
+import { LumeEvent } from '@utils/types';
+
+export function RepliesList({ id }: { id: string }) {
+  const { relayUrls, fetcher } = useNDK();
+  const { status, data } = useQuery(['thread', id], async () => {
+    const events = (await fetcher.fetchAllEvents(
+      relayUrls,
+      { kinds: [1], '#e': [id] },
+      { since: 0 }
+    )) as unknown as LumeEvent[];
+    return events;
   });
 
   return (
-    <div className="mt-5">
+    <div className="mt-3">
       <div className="mb-2">
         <h5 className="text-lg font-semibold text-zinc-300">Replies</h5>
       </div>
@@ -28,7 +36,7 @@ export function RepliesList({ parent_id }: { parent_id: string }) {
           </div>
         ) : data.length === 0 ? (
           <div className="px=3">
-            <div className="flex w-full items-center justify-center rounded-md bg-zinc-900">
+            <div className="flex w-full items-center justify-center rounded-xl bg-zinc-900">
               <div className="flex flex-col items-center justify-center gap-2 py-6">
                 <h3 className="text-3xl">๐Ÿ‘‹</h3>
                 <p className="leading-none text-zinc-400">Share your thought on it...</p>
diff --git a/src/shared/notes/stats.tsx b/src/shared/notes/stats.tsx
new file mode 100644
index 00000000..ead08097
--- /dev/null
+++ b/src/shared/notes/stats.tsx
@@ -0,0 +1,76 @@
+import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk';
+import { useQuery } from '@tanstack/react-query';
+import { decode } from 'light-bolt11-decoder';
+
+import { useNDK } from '@libs/ndk/provider';
+
+import { LoaderIcon } from '@shared/icons';
+
+export function NoteStats({ id }: { id: string }) {
+  const { ndk } = useNDK();
+  const { status, data } = useQuery(
+    ['note-stats', id],
+    async () => {
+      let reactions = 0;
+      let reposts = 0;
+      let zaps = 0;
+
+      const filter: NDKFilter = {
+        '#e': [id],
+        kinds: [6, 7, 9735],
+      };
+
+      const events = await ndk.fetchEvents(filter);
+      events.forEach((event: NDKEvent) => {
+        switch (event.kind) {
+          case 6:
+            reposts += 1;
+            break;
+          case 7:
+            reactions += 1;
+            break;
+          case 9735: {
+            const bolt11 = event.tags.find((tag) => tag[0] === 'bolt11')[1];
+            if (bolt11) {
+              const decoded = decode(bolt11);
+              const amount = decoded.sections.find((item) => item.name === 'amount');
+              const sats = amount.value / 1000;
+              zaps += sats;
+            }
+            break;
+          }
+          default:
+            break;
+        }
+      });
+
+      return { reposts, reactions, zaps };
+    },
+    { refetchOnWindowFocus: false, refetchOnReconnect: false }
+  );
+
+  if (status === 'loading') {
+    return (
+      <div className="flex h-11 items-center">
+        <LoaderIcon className="h-4 w-4 animate-spin text-zinc-100" />
+      </div>
+    );
+  }
+
+  return (
+    <div className="flex h-11 items-center gap-3">
+      <p className="inline-flex h-6 items-center justify-center gap-1 rounded bg-zinc-800 px-2 text-sm">
+        {data.reactions}
+        <span className="text-zinc-400">reactions</span>
+      </p>
+      <p className="inline-flex h-6 items-center justify-center gap-1 rounded bg-zinc-800 px-2 text-sm">
+        {data.reposts}
+        <span className="text-zinc-400">reposts</span>
+      </p>
+      <p className="inline-flex h-6 items-center justify-center gap-1 rounded bg-zinc-800 px-2 text-sm">
+        {data.zaps}
+        <span className="text-zinc-400">zaps</span>
+      </p>
+    </div>
+  );
+}
diff --git a/src/shared/notes/users/thread.tsx b/src/shared/notes/users/thread.tsx
new file mode 100644
index 00000000..c7ee4346
--- /dev/null
+++ b/src/shared/notes/users/thread.tsx
@@ -0,0 +1,46 @@
+import { VerticalDotsIcon } from '@shared/icons';
+import { Image } from '@shared/image';
+
+import { DEFAULT_AVATAR } from '@stores/constants';
+
+import { formatCreatedAt } from '@utils/createdAt';
+import { useProfile } from '@utils/hooks/useProfile';
+import { displayNpub } from '@utils/shortenKey';
+
+export function ThreadUser({ pubkey, time }: { pubkey: string; time: number }) {
+  const { status, user } = useProfile(pubkey);
+  const createdAt = formatCreatedAt(time);
+
+  if (status === 'loading') {
+    return <div className="h-4 w-4 animate-pulse rounded bg-zinc-700"></div>;
+  }
+
+  return (
+    <div className="flex items-center gap-3">
+      <Image
+        src={user?.picture || user?.image || DEFAULT_AVATAR}
+        fallback={DEFAULT_AVATAR}
+        alt={pubkey}
+        className="relative z-20 inline-block h-11 w-11 rounded-lg"
+      />
+      <div className="lex flex-1 items-baseline justify-between">
+        <div className="inline-flex w-full items-center justify-between">
+          <h5 className="truncate font-semibold leading-none text-zinc-100">
+            {user?.nip05?.toLowerCase() || user?.name || user?.display_name}
+          </h5>
+          <button
+            type="button"
+            className="inline-flex h-5 w-max items-center justify-center rounded px-1 hover:bg-zinc-800"
+          >
+            <VerticalDotsIcon className="h-4 w-4 rotate-90 transform text-zinc-200" />
+          </button>
+        </div>
+        <div className="inline-flex items-center gap-2">
+          <span className="leading-none text-zinc-500">{createdAt}</span>
+          <span className="leading-none text-zinc-500">ยท</span>
+          <span className="leading-none text-zinc-500">{displayNpub(pubkey, 16)}</span>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/src/utils/shortenKey.tsx b/src/utils/shortenKey.tsx
index 4e90b4f5..cf950fd3 100644
--- a/src/utils/shortenKey.tsx
+++ b/src/utils/shortenKey.tsx
@@ -4,3 +4,17 @@ export function shortenKey(pubkey: string) {
   const npub = nip19.npubEncode(pubkey);
   return npub.substring(0, 16).concat('...');
 }
+
+export function displayNpub(pubkey: string, len: number, separator?: string) {
+  const npub = nip19.npubEncode(pubkey) as string;
+  if (npub.length <= len) return npub;
+
+  separator = separator || ' ... ';
+
+  const sepLen = separator.length,
+    charsToShow = len - sepLen,
+    frontChars = Math.ceil(charsToShow / 2),
+    backChars = Math.floor(charsToShow / 2);
+
+  return npub.substr(0, frontChars) + separator + npub.substr(npub.length - backChars);
+}
diff --git a/src/utils/transform.tsx b/src/utils/transform.tsx
index e4f771a6..7068694f 100644
--- a/src/utils/transform.tsx
+++ b/src/utils/transform.tsx
@@ -47,7 +47,7 @@ export function arrayObjToPureArr(arr: any) {
 }
 
 // get parent id from event tags
-export function getParentID(arr: string[], fallback: string) {
+export function getParentID(arr: string[][], fallback: string) {
   const tags = destr(arr);
   let parentID = fallback;