diff --git a/apps/mobile/src/assets/icons.tsx b/apps/mobile/src/assets/icons.tsx index 81ff8227..b1c79a34 100644 --- a/apps/mobile/src/assets/icons.tsx +++ b/apps/mobile/src/assets/icons.tsx @@ -372,6 +372,7 @@ export const RepostIcon: React.FC = (props) => { ); }; + export const LikeIcon: React.FC = (props) => { return ( diff --git a/apps/mobile/src/modules/Post/index.tsx b/apps/mobile/src/modules/Post/index.tsx index 70bcb013..f267492e 100644 --- a/apps/mobile/src/modules/Post/index.tsx +++ b/apps/mobile/src/modules/Post/index.tsx @@ -1,7 +1,7 @@ import {NDKEvent, NDKKind} from '@nostr-dev-kit/ndk'; import {useNavigation} from '@react-navigation/native'; import {useQueryClient} from '@tanstack/react-query'; -import {useProfile, useReact, useReactions, useReplyNotes, useRepost} from 'afk_nostr_sdk'; +import {useProfile, useReact, useReactions, useReplyNotes, useRepost, useBookmark} from 'afk_nostr_sdk'; // import { useAuth } from '../../store/auth'; import {useAuth} from 'afk_nostr_sdk'; import {useMemo, useState} from 'react'; @@ -31,7 +31,6 @@ export type PostProps = { isRepost?:boolean }; - export const Post: React.FC = ({asComment, event, repostedEventProps, isRepost}) => { const repostedEvent = repostedEventProps ?? undefined; @@ -51,6 +50,7 @@ export const Post: React.FC = ({asComment, event, repostedEventProps, const react = useReact(); const queryClient = useQueryClient(); const repostMutation = useRepost({ event }); + const { bookmarkNote } = useBookmark(publicKey); const [menuOpen, setMenuOpen] = useState(false); @@ -134,8 +134,15 @@ export const Post: React.FC = ({asComment, event, repostedEventProps, } }; - const handleBookmarkList = async () => { - showToast({title: 'Bookmark and List coming soon', type: 'info'}); + const handleBookmark = async () => { + if (!event) return; + try { + await bookmarkNote({ event }); + showToast({title: 'Post bookmarked successfully', type: 'success'}); + } catch (error) { + console.error('Bookmark error:', error); + showToast({title: 'Failed to bookmark', type: 'error'}); + } }; const content = event?.content || ''; @@ -287,10 +294,7 @@ export const Post: React.FC = ({asComment, event, repostedEventProps, { - if (!event) return; - handleBookmarkList(); - }} + onPress={handleBookmark} > @@ -319,4 +323,4 @@ export const Post: React.FC = ({asComment, event, repostedEventProps, )} ); -}; \ No newline at end of file +}; diff --git a/packages/afk_nostr_sdk/src/hooks/index.ts b/packages/afk_nostr_sdk/src/hooks/index.ts index d1eca022..cdf4c4a7 100644 --- a/packages/afk_nostr_sdk/src/hooks/index.ts +++ b/packages/afk_nostr_sdk/src/hooks/index.ts @@ -35,4 +35,5 @@ export {useRepost} from './useRepost'; export {useSendPrivateMessage} from './messages/useSendPrivateMessage'; export {useMyGiftWrapMessages} from './messages/useMyGiftWrapMessages'; export {useMyMessagesSent} from './messages/useMyMessagesSent'; +export {useBookmark} from './useBookmark'; diff --git a/packages/afk_nostr_sdk/src/hooks/useBookmark.ts b/packages/afk_nostr_sdk/src/hooks/useBookmark.ts new file mode 100644 index 00000000..36a37a36 --- /dev/null +++ b/packages/afk_nostr_sdk/src/hooks/useBookmark.ts @@ -0,0 +1,106 @@ +import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useNostrContext } from '../context/NostrContext'; + +interface BookmarkParams { + event: NDKEvent; + category?: string; +} + +interface RemoveBookmarkParams { + eventId: string; + category?: string; +} + +export const useBookmark = (userPublicKey: string) => { + const { ndk } = useNostrContext(); + const queryClient = useQueryClient(); + + const bookmarkNote = useMutation({ + mutationKey: ["bookmark", ndk], + mutationFn: async ({ event, category }: BookmarkParams) => { + if (!event) { + throw new Error('No event provided for bookmark'); + } + + const bookmarkEvent = new NDKEvent(ndk); + + if (category) { + bookmarkEvent.kind = NDKKind.BookmarkSet; + bookmarkEvent.tags = [ + ['d', category], + ['e', event.id, event.relay?.url || ''], + ['p', event.pubkey], + ]; + } else { + bookmarkEvent.kind = 10003; + bookmarkEvent.tags = [ + ['e', event.id, event.relay?.url || ''], + ['p', event.pubkey], + ]; + } + + await bookmarkEvent.publish(); + return bookmarkEvent; + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['bookmarks'] }); + }, + onError: (error) => { + console.error('Failed to bookmark note:', error); + }, + }); + + const getBookmarks = useQuery({ + queryKey: ['bookmarks', userPublicKey], + queryFn: async () => { + if (!ndk.signer) { + throw new Error('No signer available'); + } + const filter = { kinds: [10003, 30003], authors: [userPublicKey] }; + const events = await ndk.fetchEvents(filter); + return Array.from(events); + }, + }); + + const removeBookmark = useMutation({ + mutationKey: ["bookmark", ndk], + mutationFn: async ({ eventId, category }: RemoveBookmarkParams) => { + const existingBookmarks = getBookmarks.data; + + if (!existingBookmarks) { + throw new Error('No existing bookmarks found'); + } + + const bookmarkEvent = existingBookmarks.find((event) => { + const isMatchingCategory = category + ? event.tags.some(tag => tag[0] === 'd' && tag[1] === category) + : true; + + return isMatchingCategory && event.tags.some(tag => tag[0] === 'e' && tag[1] === eventId); + }); + + if (bookmarkEvent) { + bookmarkEvent.tags = bookmarkEvent.tags.filter(tag => !(tag[0] === 'e' && tag[1] === eventId)); + if (bookmarkEvent.tags.length > 0) { + await bookmarkEvent.publish(); + } + } else { + throw new Error('Bookmark not found'); + } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['bookmarks', userPublicKey] }); + }, + onError: (error) => { + console.error('Failed to remove bookmark:', error); + }, + }); + + return { + bookmarkNote: bookmarkNote.mutateAsync, + removeBookmark: removeBookmark.mutateAsync, + getBookmarks: getBookmarks.data, + isFetchingBookmarks: getBookmarks.isFetching, + }; +};