From aec967e5729469ea2860aa6aca45e4876eb233f4 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Mon, 20 Jan 2025 14:29:03 +0330 Subject: [PATCH 1/6] Add support for nevent type in NoteContent and update parseChunks utility --- .../components/note-content/index.tsx | 23 +++++++++++++++++-- .../components/note-content/utils/index.ts | 9 +++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/templates/nostribe/src/features/note-widget/components/note-content/index.tsx b/templates/nostribe/src/features/note-widget/components/note-content/index.tsx index b70bf41..d8cae4c 100644 --- a/templates/nostribe/src/features/note-widget/components/note-content/index.tsx +++ b/templates/nostribe/src/features/note-widget/components/note-content/index.tsx @@ -1,5 +1,6 @@ -import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk'; +import { EventPointer, NDKEvent, NDKUser } from '@nostr-dev-kit/ndk'; import { useRealtimeProfile } from 'nostr-hooks'; +import { neventEncode } from 'nostr-tools/nip19'; import { memo, useMemo } from 'react'; import { Link } from 'react-router-dom'; @@ -19,7 +20,6 @@ export const NoteContent = memo( switch (chunk.type) { case 'text': case 'naddr': - case 'nevent': return ( {chunk.content} @@ -60,6 +60,25 @@ export const NoteContent = memo( {chunk.content} ); + case 'nevent': + if (!inView) { + return null; + } + + const parsedEvent = JSON.parse(chunk.content) as EventPointer; + if (parsedEvent.kind === 1) { + return ( +
+ +
+ ); + } else { + return ( + + {`nostr:${neventEncode(parsedEvent)}`} + + ); + } case 'note': if (inView) { return ( diff --git a/templates/nostribe/src/features/note-widget/components/note-content/utils/index.ts b/templates/nostribe/src/features/note-widget/components/note-content/utils/index.ts index e33504e..bd2c570 100644 --- a/templates/nostribe/src/features/note-widget/components/note-content/utils/index.ts +++ b/templates/nostribe/src/features/note-widget/components/note-content/utils/index.ts @@ -45,7 +45,6 @@ export const parseChunks = (content: string): Chunk[] => { const decoded = decode(match[0].substring(6)); switch (decoded.type) { case 'naddr': - case 'nevent': matches.push({ type: decoded.type, content: match[0], @@ -53,6 +52,14 @@ export const parseChunks = (content: string): Chunk[] => { fullLength: match[0].length, }); break; + case 'nevent': + matches.push({ + type: decoded.type, + content: JSON.stringify(decoded.data), + index: match.index, + fullLength: match[0].length, + }); + break; case 'npub': case 'note': matches.push({ From 15813370d2b87d334cf5eff320bb78cf535f2eca Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Mon, 20 Jan 2025 14:52:46 +0330 Subject: [PATCH 2/6] Add Open option to note header menu with icon --- .../note-widget/components/note-header/index.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/templates/nostribe/src/features/note-widget/components/note-header/index.tsx b/templates/nostribe/src/features/note-widget/components/note-header/index.tsx index 6f8def4..49edb90 100644 --- a/templates/nostribe/src/features/note-widget/components/note-header/index.tsx +++ b/templates/nostribe/src/features/note-widget/components/note-header/index.tsx @@ -1,6 +1,14 @@ import { NDKEvent, NDKUser } from '@nostr-dev-kit/ndk'; import { formatDistanceToNowStrict } from 'date-fns'; -import { EllipsisIcon, FileJsonIcon, HeartIcon, LinkIcon, TagIcon, TextIcon } from 'lucide-react'; +import { + EllipsisIcon, + FileJsonIcon, + HeartIcon, + LinkIcon, + SquareArrowOutUpRight, + TagIcon, + TextIcon, +} from 'lucide-react'; import { Avatar, AvatarFallback, AvatarImage } from '@/shared/components/ui/avatar'; import { Button } from '@/shared/components/ui/button'; @@ -59,6 +67,11 @@ export const NoteHeader = ({ event }: { event: NDKEvent }) => { + navigate(`/note/${nevent}`)}> + + Open + + { // TODO From 55d061a6cc85c201b0ec117d9ed3fd20463d134b Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Tue, 21 Jan 2025 11:52:48 +0330 Subject: [PATCH 3/6] Add notifications page and update routing for notifications and messages --- templates/nostribe/src/pages/index.tsx | 26 +++++++++++++++---- .../src/pages/notifications/index.tsx | 9 +++++++ 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 templates/nostribe/src/pages/notifications/index.tsx diff --git a/templates/nostribe/src/pages/index.tsx b/templates/nostribe/src/pages/index.tsx index 11ed7d9..1299ee4 100644 --- a/templates/nostribe/src/pages/index.tsx +++ b/templates/nostribe/src/pages/index.tsx @@ -102,7 +102,7 @@ const Layout = () => {
@@ -176,6 +176,15 @@ const Layout = () => {
+ +
+ +
+ +
@@ -185,20 +194,20 @@ const Layout = () => {
- +
- +
@@ -283,6 +292,7 @@ const HomePage = () => import('./home'); const NotePage = () => import('./note'); const ProfilePage = () => import('./profile'); const MessagesPage = () => import('./messages'); +const NotificationsPage = () => import('./notifications'); export const router = createBrowserRouter([ { @@ -319,6 +329,12 @@ export const router = createBrowserRouter([ return { Component: (await MessagesPage()).MessagesPage }; }, }, + { + path: '/notifications', + async lazy() { + return { Component: (await NotificationsPage()).NotificationsPage }; + }, + }, ], }, ]); diff --git a/templates/nostribe/src/pages/notifications/index.tsx b/templates/nostribe/src/pages/notifications/index.tsx new file mode 100644 index 0000000..86d5b0f --- /dev/null +++ b/templates/nostribe/src/pages/notifications/index.tsx @@ -0,0 +1,9 @@ +import { NotificationsWidget } from '@/features/notifications-widget'; + +export const NotificationsPage = () => { + return ( +
+ +
+ ); +}; From 137b54aff9a4848175dd385cb44abe436509281e Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Tue, 21 Jan 2025 12:41:06 +0330 Subject: [PATCH 4/6] Add notifications widget components for mentions, replies, reactions, reposts, and zaps --- .../notifications-widget/components/index.ts | 4 + .../components/mentions-and-replies/index.tsx | 19 ++++ .../components/reactions/index.tsx | 83 ++++++++++++++ .../components/reposts/index.tsx | 82 ++++++++++++++ .../components/zaps/index.tsx | 89 +++++++++++++++ .../features/notifications-widget/index.tsx | 102 ++++++++++++++++++ 6 files changed, 379 insertions(+) create mode 100644 templates/nostribe/src/features/notifications-widget/components/index.ts create mode 100644 templates/nostribe/src/features/notifications-widget/components/mentions-and-replies/index.tsx create mode 100644 templates/nostribe/src/features/notifications-widget/components/reactions/index.tsx create mode 100644 templates/nostribe/src/features/notifications-widget/components/reposts/index.tsx create mode 100644 templates/nostribe/src/features/notifications-widget/components/zaps/index.tsx create mode 100644 templates/nostribe/src/features/notifications-widget/index.tsx diff --git a/templates/nostribe/src/features/notifications-widget/components/index.ts b/templates/nostribe/src/features/notifications-widget/components/index.ts new file mode 100644 index 0000000..cadc503 --- /dev/null +++ b/templates/nostribe/src/features/notifications-widget/components/index.ts @@ -0,0 +1,4 @@ +export * from './mentions-and-replies'; +export * from './reactions'; +export * from './reposts'; +export * from './zaps'; diff --git a/templates/nostribe/src/features/notifications-widget/components/mentions-and-replies/index.tsx b/templates/nostribe/src/features/notifications-widget/components/mentions-and-replies/index.tsx new file mode 100644 index 0000000..7139b93 --- /dev/null +++ b/templates/nostribe/src/features/notifications-widget/components/mentions-and-replies/index.tsx @@ -0,0 +1,19 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk'; + +import { NoteByNoteId } from '@/features/note-widget'; + +export const MentionsAndReplies = ({ + mentionsAndReplies, +}: { + mentionsAndReplies: NDKEvent[] | undefined; +}) => { + return mentionsAndReplies?.map((event) => ( + <> +
+
+ +
+
+ + )); +}; diff --git a/templates/nostribe/src/features/notifications-widget/components/reactions/index.tsx b/templates/nostribe/src/features/notifications-widget/components/reactions/index.tsx new file mode 100644 index 0000000..d630c40 --- /dev/null +++ b/templates/nostribe/src/features/notifications-widget/components/reactions/index.tsx @@ -0,0 +1,83 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { ThumbsUpIcon } from 'lucide-react'; +import { useRealtimeProfile } from 'nostr-hooks'; +import { memo, useMemo } from 'react'; +import { Link } from 'react-router-dom'; + +import { Avatar, AvatarImage } from '@/shared/components/ui/avatar'; + +import { Spinner } from '@/shared/components/spinner'; + +import { ellipsis } from '@/shared/utils'; + +import { NoteByNoteId } from '@/features/note-widget'; + +type CategorizedReactions = Map; + +export const Reactions = memo(({ reactions }: { reactions: NDKEvent[] | undefined }) => { + const categorizedReactions: CategorizedReactions = useMemo(() => { + const categorizedReactions = new Map(); + + reactions?.forEach((reaction) => { + const eTags = reaction.getMatchingTags('e'); + if (eTags.length > 0) { + const eTag = eTags[eTags.length - 1]; + if (eTag.length > 0) { + const eventId = eTag[1]; + const reactions = categorizedReactions.get(eventId) || []; + reactions.push(reaction); + categorizedReactions.set(eventId, reactions); + } + } + }); + + return categorizedReactions; + }, [reactions]); + + if (reactions === undefined) { + return ; + } + + if (reactions.length === 0) { + return
No reactions yet
; + } + + return ( + <> + {Array.from(categorizedReactions.keys()).map((eventId) => ( +
+
+
+ + + {categorizedReactions + .get(eventId) + ?.map((reaction) => )} +
+ + +
+
+ ))} + + ); +}); + +const Reaction = memo(({ reaction }: { reaction: NDKEvent }) => { + const { profile } = useRealtimeProfile(reaction.pubkey); + + return ( + +
+ + + + +

+ {ellipsis(profile?.name?.toString() || reaction.author.npub, 20)} + {reaction.content === '+' ? '👍' : reaction.content} +

+
+ + ); +}); diff --git a/templates/nostribe/src/features/notifications-widget/components/reposts/index.tsx b/templates/nostribe/src/features/notifications-widget/components/reposts/index.tsx new file mode 100644 index 0000000..f99fa7c --- /dev/null +++ b/templates/nostribe/src/features/notifications-widget/components/reposts/index.tsx @@ -0,0 +1,82 @@ +import { NDKEvent } from '@nostr-dev-kit/ndk'; +import { Repeat2Icon } from 'lucide-react'; +import { useRealtimeProfile } from 'nostr-hooks'; +import { memo, useMemo } from 'react'; +import { Link } from 'react-router-dom'; + +import { Avatar, AvatarImage } from '@/shared/components/ui/avatar'; + +import { Spinner } from '@/shared/components/spinner'; + +import { ellipsis } from '@/shared/utils'; + +import { NoteByNoteId } from '@/features/note-widget'; + +type CategorizedReposts = Map; + +export const Reposts = memo(({ reposts }: { reposts: NDKEvent[] | undefined }) => { + const categorizedReposts: CategorizedReposts = useMemo(() => { + const categorizedReposts = new Map(); + + reposts?.forEach((repost) => { + const eTags = repost.getMatchingTags('e'); + if (eTags.length > 0) { + const eTag = eTags[eTags.length - 1]; + if (eTag.length > 0) { + const eventId = eTag[1]; + const reposts = categorizedReposts.get(eventId) || []; + reposts.push(repost); + categorizedReposts.set(eventId, reposts); + } + } + }); + + return categorizedReposts; + }, [reposts]); + + if (reposts === undefined) { + return ; + } + + if (reposts.length === 0) { + return
No reposts yet
; + } + + return ( + <> + {Array.from(categorizedReposts.keys()).map((eventId) => ( +
+
+
+ + + {categorizedReposts + .get(eventId) + ?.map((repost) => )} +
+ + +
+
+ ))} + + ); +}); + +const Repost = memo(({ repost }: { repost: NDKEvent }) => { + const { profile } = useRealtimeProfile(repost.pubkey); + + return ( + +
+ + + + +

+ {ellipsis(profile?.name?.toString() || repost.author.npub, 20)} +

+
+ + ); +}); diff --git a/templates/nostribe/src/features/notifications-widget/components/zaps/index.tsx b/templates/nostribe/src/features/notifications-widget/components/zaps/index.tsx new file mode 100644 index 0000000..215be68 --- /dev/null +++ b/templates/nostribe/src/features/notifications-widget/components/zaps/index.tsx @@ -0,0 +1,89 @@ +import { NDKEvent, NDKUser, zapInvoiceFromEvent } from '@nostr-dev-kit/ndk'; +import { ZapIcon } from 'lucide-react'; +import { useRealtimeProfile } from 'nostr-hooks'; +import { memo, useMemo } from 'react'; +import { Link } from 'react-router-dom'; + +import { Avatar, AvatarImage } from '@/shared/components/ui/avatar'; + +import { Spinner } from '@/shared/components/spinner'; + +import { ellipsis } from '@/shared/utils'; + +import { NoteByNoteId } from '@/features/note-widget'; + +type CategorizedZaps = Map; + +export const Zaps = memo(({ zaps }: { zaps: NDKEvent[] | undefined }) => { + const categorizedZaps: CategorizedZaps = useMemo(() => { + const categorizedZaps = new Map(); + + zaps?.forEach((zap) => { + const eTags = zap.getMatchingTags('e'); + if (eTags.length > 0) { + const eTag = eTags[eTags.length - 1]; + if (eTag.length > 0) { + const eventId = eTag[1]; + const zaps = categorizedZaps.get(eventId) || []; + zaps.push(zap); + categorizedZaps.set(eventId, zaps); + } + } + }); + + return categorizedZaps; + }, [zaps]); + + if (zaps === undefined) { + return ; + } + + if (zaps.length === 0) { + return
No zaps yet
; + } + + return ( + <> + {Array.from(categorizedZaps.keys()).map((eventId) => ( +
+
+
+ + + {categorizedZaps.get(eventId)?.map((zap) => )} +
+ + +
+
+ ))} + + ); +}); + +const Zap = memo( + ({ zap }: { zap: NDKEvent }) => { + const invoice = zapInvoiceFromEvent(zap); + const { profile } = useRealtimeProfile(invoice?.zappee); + const npub = useMemo( + () => (invoice && invoice.zapper ? new NDKUser({ pubkey: invoice.zapper }).npub : ''), + [invoice?.zapper], + ); + + return ( + +
+ + + + +

+ {(invoice?.amount || 0) / 1000} sats + from {ellipsis(profile?.name ? profile.name.toString() : npub, 20)} +

+
+ + ); + }, + (prevProps, nextProps) => prevProps.zap.id === nextProps.zap.id, +); diff --git a/templates/nostribe/src/features/notifications-widget/index.tsx b/templates/nostribe/src/features/notifications-widget/index.tsx new file mode 100644 index 0000000..518d5ee --- /dev/null +++ b/templates/nostribe/src/features/notifications-widget/index.tsx @@ -0,0 +1,102 @@ +import { useActiveUser, useSubscription } from 'nostr-hooks'; +import { memo, useEffect, useMemo } from 'react'; + +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/shared/components/ui/tabs'; + +import { MentionsAndReplies, Reactions, Reposts, Zaps } from './components'; + +export const NotificationsWidget = memo(() => { + const { activeUser } = useActiveUser(); + + const subId = activeUser ? `notifications-${activeUser.pubkey}` : undefined; + + const { createSubscription, events } = useSubscription(subId); + + useEffect(() => { + activeUser && + createSubscription({ + filters: [{ kinds: [1, 6, 7, 9735], '#p': [activeUser.pubkey], limit: 100 }], + }); + }, [activeUser, createSubscription]); + + const mentionsAndReplies = useMemo( + () => + events + ?.filter((event) => event.kind === 1 && event.pubkey !== activeUser?.pubkey) + .sort((a, b) => b.created_at! - a.created_at!), + [events, activeUser?.pubkey], + ); + const reposts = useMemo( + () => + events + ?.filter((event) => event.kind === 6 && event.pubkey !== activeUser?.pubkey) + .sort((a, b) => b.created_at! - a.created_at!), + [events, activeUser?.pubkey], + ); + const reactions = useMemo( + () => + events + ?.filter((event) => event.kind === 7 && event.pubkey !== activeUser?.pubkey) + .sort((a, b) => b.created_at! - a.created_at!), + [events, activeUser?.pubkey], + ); + const zaps = useMemo( + () => + events + ?.filter((event) => event.kind === 9735 && event.pubkey !== activeUser?.pubkey) + .sort((a, b) => b.created_at! - a.created_at!), + [events, activeUser?.pubkey], + ); + const all = useMemo( + () => + [ + ...(mentionsAndReplies || []), + ...(reposts || []), + ...(reactions || []), + ...(zaps || []), + ].sort((a, b) => b.created_at! - a.created_at!), + [mentionsAndReplies, reposts, reactions, zaps], + ); + + return ( +
+ + + All + Mentions and Replies + Reposts + Reactions + Zaps + + + {all?.map((event) => { + switch (event.kind) { + case 1: + return ; + case 6: + return ; + case 7: + return ; + case 9735: + return ; + default: + return null; + } + })} + + + + + + + + + + + + + + +
+ ); +}); From 7d17394cc740df658a652e0bfd403e0dc8493306 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Tue, 21 Jan 2025 12:41:24 +0330 Subject: [PATCH 5/6] Bump Nostribe version to 0.0.6 in package.json --- templates/nostribe/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/nostribe/package.json b/templates/nostribe/package.json index aa455d5..62d20a4 100644 --- a/templates/nostribe/package.json +++ b/templates/nostribe/package.json @@ -1,7 +1,7 @@ { "name": "nostribe", "private": true, - "version": "0.0.5", + "version": "0.0.6", "type": "module", "scripts": { "dev": "vite", From 50e63efd725bcd4eaf9a0409e28de6c6d6c11a53 Mon Sep 17 00:00:00 2001 From: sepehr-safari Date: Tue, 21 Jan 2025 12:41:29 +0330 Subject: [PATCH 6/6] Bump version to 0.7.5 in package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7dbe144..242e212 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-osty", - "version": "0.7.4", + "version": "0.7.5", "type": "module", "license": "MIT", "author": "Sepehr Safari",