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.title} -
+ {metadata.image && ( + {metadata.title} + )} +
{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) => (
image -
+
+ {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 (
{pubkey} -
-
-
- {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' ? (