From bfb7d7915f58001b3450ab79feec7587f169de30 Mon Sep 17 00:00:00 2001
From: Ren Amamiya <123083837+reyamir@users.noreply.github.com>
Date: Sat, 26 Aug 2023 09:45:39 +0700
Subject: [PATCH] update single note screen
---
src/app.tsx | 35 +++--
src/app/auth/components/user.tsx | 6 +-
src/app/error.tsx | 2 +-
src/app/events/index.tsx | 67 ---------
src/app/notes/article.tsx | 122 +++++++++++++++++
src/app/notes/text.tsx | 128 ++++++++++++++++++
src/shared/icons/index.tsx | 1 +
src/shared/icons/share.tsx | 21 +++
src/shared/{appLayout.tsx => layouts/app.tsx} | 0
.../{authLayout.tsx => layouts/auth.tsx} | 0
src/shared/layouts/note.tsx | 10 ++
.../settings.tsx} | 0
src/shared/notes/actions/more.tsx | 2 +-
src/shared/notes/index.tsx | 1 +
src/shared/notes/kinds/article.tsx | 24 ++--
src/shared/notes/kinds/articleDetail.tsx | 37 +++++
src/shared/notes/kinds/text.tsx | 9 +-
src/shared/notes/preview/image.tsx | 5 +-
src/shared/notes/replies/item.tsx | 21 +--
src/shared/notes/replies/list.tsx | 97 +++++++------
src/shared/notes/replies/sub.tsx | 4 +-
src/shared/notes/users/repost.tsx | 5 +-
src/shared/notes/users/thread.tsx | 21 +--
src/shared/user.tsx | 5 +-
src/shared/userProfile.tsx | 4 +-
25 files changed, 455 insertions(+), 172 deletions(-)
delete mode 100644 src/app/events/index.tsx
create mode 100644 src/app/notes/article.tsx
create mode 100644 src/app/notes/text.tsx
create mode 100644 src/shared/icons/share.tsx
rename src/shared/{appLayout.tsx => layouts/app.tsx} (100%)
rename src/shared/{authLayout.tsx => layouts/auth.tsx} (100%)
create mode 100644 src/shared/layouts/note.tsx
rename src/shared/{settingsLayout.tsx => layouts/settings.tsx} (100%)
create mode 100644 src/shared/notes/kinds/articleDetail.tsx
diff --git a/src/app.tsx b/src/app.tsx
index f8e38e47..d42e0247 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -5,10 +5,11 @@ import { AuthImportScreen } from '@app/auth/import';
import { OnboardingScreen } from '@app/auth/onboarding';
import { ErrorScreen } from '@app/error';
-import { AppLayout } from '@shared/appLayout';
-import { AuthLayout } from '@shared/authLayout';
import { LoaderIcon } from '@shared/icons';
-import { SettingsLayout } from '@shared/settingsLayout';
+import { AppLayout } from '@shared/layouts/app';
+import { AuthLayout } from '@shared/layouts/auth';
+import { NoteLayout } from '@shared/layouts/note';
+import { SettingsLayout } from '@shared/layouts/settings';
import { checkActiveAccount } from '@utils/checkActiveAccount';
@@ -54,13 +55,6 @@ const router = createBrowserRouter([
return { Component: SpaceScreen };
},
},
- {
- path: 'events/:id',
- async lazy() {
- const { EventScreen } = await import('@app/events');
- return { Component: EventScreen };
- },
- },
{
path: 'users/:pubkey',
async lazy() {
@@ -84,6 +78,27 @@ const router = createBrowserRouter([
},
],
},
+ {
+ path: '/notes',
+ element: ,
+ errorElement: ,
+ children: [
+ {
+ path: 'text/:id',
+ async lazy() {
+ const { TextNoteScreen } = await import('@app/notes/text');
+ return { Component: TextNoteScreen };
+ },
+ },
+ {
+ path: 'article/:id',
+ async lazy() {
+ const { ArticleNoteScreen } = await import('@app/notes/article');
+ return { Component: ArticleNoteScreen };
+ },
+ },
+ ],
+ },
{
path: '/splashscreen',
errorElement: ,
diff --git a/src/app/auth/components/user.tsx b/src/app/auth/components/user.tsx
index 34b0450a..a79c9dea 100644
--- a/src/app/auth/components/user.tsx
+++ b/src/app/auth/components/user.tsx
@@ -28,11 +28,11 @@ export function User({ pubkey, fallback }: { pubkey: string; fallback?: string }
/>
-
+
{user?.name || user?.display_name || user?.nip05}
-
+
- {user?.nip05?.toLowerCase() || displayNpub(pubkey, 16)}
+ {displayNpub(pubkey, 16)}
diff --git a/src/app/error.tsx b/src/app/error.tsx
index 13d454c5..24ce1ba4 100644
--- a/src/app/error.tsx
+++ b/src/app/error.tsx
@@ -40,7 +40,7 @@ export function ErrorScreen() {
Sorry, an unexpected error has occurred.
-
+
{error.statusText || error.message}
diff --git a/src/app/events/index.tsx b/src/app/events/index.tsx
deleted file mode 100644
index 96a43c37..00000000
--- a/src/app/events/index.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
-import { useParams } from 'react-router-dom';
-
-import { useStorage } from '@libs/storage/provider';
-
-import {
- ArticleNote,
- FileNote,
- NoteActions,
- NoteReplyForm,
- NoteStats,
- TextNote,
- ThreadUser,
- UnknownNote,
-} from '@shared/notes';
-import { RepliesList } from '@shared/notes/replies/list';
-import { NoteSkeleton } from '@shared/notes/skeleton';
-
-import { useEvent } from '@utils/hooks/useEvent';
-
-export function EventScreen() {
- const { id } = useParams();
- const { db } = useStorage();
- const { status, data } = useEvent(id);
-
- const renderKind = (event: NDKEvent) => {
- switch (event.kind) {
- case NDKKind.Text:
- return ;
- case NDKKind.Article:
- return ;
- case 1063:
- return ;
- default:
- return ;
- }
- };
-
- return (
-
-
- {status === 'loading' ? (
-
- ) : (
-
-
-
-
{renderKind(data)}
-
-
-
-
-
-
- )}
-
-
-
-
-
-
- );
-}
diff --git a/src/app/notes/article.tsx b/src/app/notes/article.tsx
new file mode 100644
index 00000000..6b5fd9c9
--- /dev/null
+++ b/src/app/notes/article.tsx
@@ -0,0 +1,122 @@
+import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
+import { writeText } from '@tauri-apps/plugin-clipboard-manager';
+import { nip19 } from 'nostr-tools';
+import { EventPointer } from 'nostr-tools/lib/nip19';
+import { useRef, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+
+import { useStorage } from '@libs/storage/provider';
+
+import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
+import {
+ ArticleDetailNote,
+ NoteActions,
+ NoteReplyForm,
+ NoteStats,
+ ThreadUser,
+ UnknownNote,
+} from '@shared/notes';
+import { RepliesList } from '@shared/notes/replies/list';
+import { NoteSkeleton } from '@shared/notes/skeleton';
+
+import { useEvent } from '@utils/hooks/useEvent';
+
+export function ArticleNoteScreen() {
+ const navigate = useNavigate();
+ const replyRef = useRef(null);
+
+ const { id } = useParams();
+ const { db } = useStorage();
+ const { status, data } = useEvent(id);
+
+ const [isCopy, setIsCopy] = useState(false);
+
+ const share = async () => {
+ await writeText(
+ 'https://nostr.com/' +
+ nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
+ );
+ // update state
+ setIsCopy(true);
+ // reset state after 2 sec
+ setTimeout(() => setIsCopy(false), 2000);
+ };
+
+ const scrollToReply = () => {
+ replyRef.current.scrollIntoView();
+ };
+
+ const renderKind = (event: NDKEvent) => {
+ switch (event.kind) {
+ case NDKKind.Article:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {status === 'loading' ? (
+
+ ) : (
+
+
+
+
{renderKind(data)}
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/notes/text.tsx b/src/app/notes/text.tsx
new file mode 100644
index 00000000..f3dd487f
--- /dev/null
+++ b/src/app/notes/text.tsx
@@ -0,0 +1,128 @@
+import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk';
+import { writeText } from '@tauri-apps/plugin-clipboard-manager';
+import { nip19 } from 'nostr-tools';
+import { EventPointer } from 'nostr-tools/lib/nip19';
+import { useRef, useState } from 'react';
+import { useNavigate, useParams } from 'react-router-dom';
+
+import { useStorage } from '@libs/storage/provider';
+
+import { ArrowLeftIcon, CheckCircleIcon, ReplyIcon, ShareIcon } from '@shared/icons';
+import {
+ ArticleNote,
+ FileNote,
+ NoteActions,
+ NoteReplyForm,
+ NoteStats,
+ TextNote,
+ ThreadUser,
+ UnknownNote,
+} from '@shared/notes';
+import { RepliesList } from '@shared/notes/replies/list';
+import { NoteSkeleton } from '@shared/notes/skeleton';
+
+import { useEvent } from '@utils/hooks/useEvent';
+
+export function TextNoteScreen() {
+ const navigate = useNavigate();
+ const replyRef = useRef(null);
+
+ const { id } = useParams();
+ const { db } = useStorage();
+ const { status, data } = useEvent(id);
+
+ const [isCopy, setIsCopy] = useState(false);
+
+ const share = async () => {
+ await writeText(
+ 'https://nostr.com/' +
+ nip19.neventEncode({ id: data.id, author: data.pubkey } as EventPointer)
+ );
+ // update state
+ setIsCopy(true);
+ // reset state after 2 sec
+ setTimeout(() => setIsCopy(false), 2000);
+ };
+
+ const scrollToReply = () => {
+ replyRef.current.scrollIntoView();
+ };
+
+ const renderKind = (event: NDKEvent) => {
+ switch (event.kind) {
+ case NDKKind.Text:
+ return ;
+ case NDKKind.Article:
+ return ;
+ case 1063:
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {status === 'loading' ? (
+
+ ) : (
+
+
+
+
{renderKind(data)}
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/shared/icons/index.tsx b/src/shared/icons/index.tsx
index d36af3b3..5e61fdf4 100644
--- a/src/shared/icons/index.tsx
+++ b/src/shared/icons/index.tsx
@@ -50,3 +50,4 @@ export * from './horizontalDots';
export * from './arrowRightCircle';
export * from './hashtag';
export * from './file';
+export * from './share';
diff --git a/src/shared/icons/share.tsx b/src/shared/icons/share.tsx
new file mode 100644
index 00000000..424cd6f1
--- /dev/null
+++ b/src/shared/icons/share.tsx
@@ -0,0 +1,21 @@
+import { SVGProps } from 'react';
+
+export function ShareIcon(props: JSX.IntrinsicAttributes & SVGProps) {
+ return (
+
+ );
+}
diff --git a/src/shared/appLayout.tsx b/src/shared/layouts/app.tsx
similarity index 100%
rename from src/shared/appLayout.tsx
rename to src/shared/layouts/app.tsx
diff --git a/src/shared/authLayout.tsx b/src/shared/layouts/auth.tsx
similarity index 100%
rename from src/shared/authLayout.tsx
rename to src/shared/layouts/auth.tsx
diff --git a/src/shared/layouts/note.tsx b/src/shared/layouts/note.tsx
new file mode 100644
index 00000000..a48ac711
--- /dev/null
+++ b/src/shared/layouts/note.tsx
@@ -0,0 +1,10 @@
+import { Outlet } from 'react-router-dom';
+
+export function NoteLayout() {
+ return (
+
+ );
+}
diff --git a/src/shared/settingsLayout.tsx b/src/shared/layouts/settings.tsx
similarity index 100%
rename from src/shared/settingsLayout.tsx
rename to src/shared/layouts/settings.tsx
diff --git a/src/shared/notes/actions/more.tsx b/src/shared/notes/actions/more.tsx
index 0d2806fc..8e40688c 100644
--- a/src/shared/notes/actions/more.tsx
+++ b/src/shared/notes/actions/more.tsx
@@ -48,7 +48,7 @@ export function MoreActions({ id, pubkey }: { id: string; pubkey: string }) {
Open as new screen
diff --git a/src/shared/notes/index.tsx b/src/shared/notes/index.tsx
index f56b704e..0591f2f0 100644
--- a/src/shared/notes/index.tsx
+++ b/src/shared/notes/index.tsx
@@ -14,6 +14,7 @@ export * from './replies/sub';
export * from './kinds/text';
export * from './kinds/file';
export * from './kinds/article';
+export * from './kinds/articleDetail';
export * from './kinds/unknown';
export * from './metadata';
export * from './users/mini';
diff --git a/src/shared/notes/kinds/article.tsx b/src/shared/notes/kinds/article.tsx
index 9d1b4422..0e6b5451 100644
--- a/src/shared/notes/kinds/article.tsx
+++ b/src/shared/notes/kinds/article.tsx
@@ -1,5 +1,6 @@
import { NDKEvent } from '@nostr-dev-kit/ndk';
import { useMemo } from 'react';
+import { Link } from 'react-router-dom';
import { Image } from '@shared/image';
@@ -27,26 +28,31 @@ export function ArticleNote({ event }: { event: NDKEvent }) {
}, [event.id]);
return (
-
+
-
-
+ {metadata.image && (
+
+ )}
+
{metadata.title}
{metadata.summary}
-
{metadata.publishedAt.toString()}
-
+
);
}
diff --git a/src/shared/notes/kinds/articleDetail.tsx b/src/shared/notes/kinds/articleDetail.tsx
new file mode 100644
index 00000000..5309fa19
--- /dev/null
+++ b/src/shared/notes/kinds/articleDetail.tsx
@@ -0,0 +1,37 @@
+import { NDKEvent } from '@nostr-dev-kit/ndk';
+import { useMemo } from 'react';
+import ReactMarkdown from 'react-markdown';
+import { Link } from 'react-router-dom';
+import remarkGfm from 'remark-gfm';
+
+import { Image } from '@shared/image';
+
+export function ArticleDetailNote({ event }: { event: NDKEvent }) {
+ const metadata = useMemo(() => {
+ const title = event.tags.find((tag) => tag[0] === 'title')?.[1];
+ const image = event.tags.find((tag) => tag[0] === 'image')?.[1];
+ const summary = event.tags.find((tag) => tag[0] === 'summary')?.[1];
+
+ let publishedAt: Date | string | number = event.tags.find(
+ (tag) => tag[0] === 'published_at'
+ )?.[1];
+ if (publishedAt) {
+ publishedAt = new Date(parseInt(publishedAt)).toLocaleDateString('en-US');
+ } else {
+ publishedAt = new Date(event.created_at * 1000).toLocaleDateString('en-US');
+ }
+
+ return {
+ title,
+ image,
+ publishedAt,
+ summary,
+ };
+ }, [event.id]);
+
+ return (
+
+ {event.content}
+
+ );
+}
diff --git a/src/shared/notes/kinds/text.tsx b/src/shared/notes/kinds/text.tsx
index 70996a08..76dfe6ea 100644
--- a/src/shared/notes/kinds/text.tsx
+++ b/src/shared/notes/kinds/text.tsx
@@ -26,11 +26,16 @@ export function TextNote({ event }: { event: NDKEvent }) {
del: ({ children }) => {
const key = children[0] as string;
if (typeof key !== 'string') return;
- if (key.startsWith('pub') && key.length > 50 && key.length < 100)
+ if (key.startsWith('pub') && key.length > 50 && key.length < 100) {
return
;
- if (key.startsWith('tag')) return
;
+ }
+ if (key.startsWith('tag')) {
+ return
;
+ }
},
}}
+ disallowedElements={['h1', 'h2', 'h3', 'h4', 'h5', 'h6']}
+ unwrapDisallowed={true}
>
{content?.parsed}
diff --git a/src/shared/notes/preview/image.tsx b/src/shared/notes/preview/image.tsx
index 1ce377f1..38ee59d8 100644
--- a/src/shared/notes/preview/image.tsx
+++ b/src/shared/notes/preview/image.tsx
@@ -12,14 +12,13 @@ export function ImagePreview({ urls, truncate }: { urls: string[]; truncate?: bo
};
return (
-
+
{urls.map((url) => (
-
+
+ {event?.replies?.length > 0 && (
+
+ )}
+
-
+
-
- {event.replies ? (
- event.replies.map((sub) =>
)
- ) : (
-
- )}
-
+ {event.replies ? (
+ event.replies.map((sub) =>
)
+ ) : (
+
+ )}
);
diff --git a/src/shared/notes/replies/list.tsx b/src/shared/notes/replies/list.tsx
index 9543c3fe..3173cce4 100644
--- a/src/shared/notes/replies/list.tsx
+++ b/src/shared/notes/replies/list.tsx
@@ -8,42 +8,51 @@ import { NDKEventWithReplies } from '@utils/types';
export function RepliesList({ id }: { id: string }) {
const { ndk } = useNDK();
- const { status, data } = useQuery(['note-replies', id], async () => {
- const events = await ndk.fetchEvents({
- kinds: [1],
- '#e': [id],
- });
+ const { status, data } = useQuery(
+ ['note-replies', id],
+ async () => {
+ try {
+ const events = await ndk.fetchEvents({
+ kinds: [1],
+ '#e': [id],
+ });
- const array = [...events] as unknown as NDKEventWithReplies[];
+ const array = [...events] as unknown as NDKEventWithReplies[];
- if (array.length > 0) {
- const replies = new Set();
- array.forEach((event) => {
- const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id);
- if (tags.length > 0) {
- tags.forEach((tag) => {
- const rootIndex = array.findIndex((el) => el.id === tag[1]);
- if (rootIndex) {
- const rootEvent = array[rootIndex];
- if (rootEvent.replies) {
- rootEvent.replies.push(event);
- } else {
- rootEvent.replies = [event];
- }
- replies.add(event.id);
+ if (array.length > 0) {
+ const replies = new Set();
+ array.forEach((event) => {
+ const tags = event.tags.filter((el) => el[0] === 'e' && el[1] !== id);
+ if (tags.length > 0) {
+ tags.forEach((tag) => {
+ const rootIndex = array.findIndex((el) => el.id === tag[1]);
+ if (rootIndex !== -1) {
+ const rootEvent = array[rootIndex];
+ if (rootEvent && rootEvent.replies) {
+ rootEvent.replies.push(event);
+ } else {
+ rootEvent.replies = [event];
+ }
+ replies.add(event.id);
+ }
+ });
}
});
+ const cleanEvents = array.filter((ev) => !replies.has(ev.id));
+ return cleanEvents;
}
- });
- const cleanEvents = array.filter((ev) => !replies.has(ev.id));
- return cleanEvents;
- }
- return array;
- });
+
+ return array;
+ } catch (e) {
+ throw new Error(e);
+ }
+ },
+ { enabled: !!ndk }
+ );
if (status === 'loading') {
return (
-
+
@@ -53,19 +62,29 @@ export function RepliesList({ id }: { id: string }) {
);
}
- return (
-
-
-
{data?.length || 0} replies
+ if (status === 'error') {
+ return (
+
+
+
+
Error: failed to get replies
+
+
-
+ );
+ }
+
+ return (
+
+
+ {data?.length || 0} replies
+
+
{data?.length === 0 ? (
-
-
-
-
๐
-
Share your thought on it...
-
+
+
+
๐
+
Share your thought on it...
) : (
diff --git a/src/shared/notes/replies/sub.tsx b/src/shared/notes/replies/sub.tsx
index 6c3f31d8..b2633482 100644
--- a/src/shared/notes/replies/sub.tsx
+++ b/src/shared/notes/replies/sub.tsx
@@ -5,9 +5,9 @@ import { User } from '@shared/user';
export function SubReply({ event }: { event: NDKEvent }) {
return (
-
+
-
+
diff --git a/src/shared/notes/users/repost.tsx b/src/shared/notes/users/repost.tsx
index d2029042..abf2e95f 100644
--- a/src/shared/notes/users/repost.tsx
+++ b/src/shared/notes/users/repost.tsx
@@ -19,10 +19,7 @@ export function RepostUser({ pubkey }: { pubkey: string }) {
/>
- {user?.nip05?.toLowerCase() ||
- user?.name ||
- user?.display_name ||
- shortenKey(pubkey)}
+ {user?.nip05 || user?.name || user?.display_name || shortenKey(pubkey)}
reposted
diff --git a/src/shared/notes/users/thread.tsx b/src/shared/notes/users/thread.tsx
index 0411a34f..f12d9a8c 100644
--- a/src/shared/notes/users/thread.tsx
+++ b/src/shared/notes/users/thread.tsx
@@ -1,4 +1,3 @@
-import { VerticalDotsIcon } from '@shared/icons';
import { Image } from '@shared/image';
import { formatCreatedAt } from '@utils/createdAt';
@@ -16,23 +15,15 @@ export function ThreadUser({ pubkey, time }: { pubkey: string; time: number }) {
return (
-
-
-
- {user?.nip05?.toLowerCase() || user?.name || user?.display_name}
-
-
-
-
+
+
+ {user.display_name || user.name}
+
+
{createdAt}
ยท
{displayNpub(pubkey, 16)}
diff --git a/src/shared/user.tsx b/src/shared/user.tsx
index b2821d16..da8f1d7c 100644
--- a/src/shared/user.tsx
+++ b/src/shared/user.tsx
@@ -79,10 +79,7 @@ export function User({
size === 'small' ? 'max-w-[10rem]' : 'max-w-[15rem]'
)}
>
- {user?.nip05?.toLowerCase() ||
- user?.name ||
- user?.display_name ||
- displayNpub(pubkey, 16)}
+ {user?.nip05 || user?.name || user?.display_name || displayNpub(pubkey, 16)}
ยท
{createdAt}
diff --git a/src/shared/userProfile.tsx b/src/shared/userProfile.tsx
index f4024d36..69ebbb3a 100644
--- a/src/shared/userProfile.tsx
+++ b/src/shared/userProfile.tsx
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Link } from 'react-router-dom';
-import { UserMetadata } from '@app/users/components/stats';
+import { UserStats } from '@app/users/components/stats';
import { Image } from '@shared/image';
@@ -65,7 +65,7 @@ export function UserProfile({ pubkey }: { pubkey: string }) {
{user?.about}
-
+
{status === 'loading' ? (