diff --git a/src/components/BookmarkButton/index.tsx b/src/components/BookmarkButton/index.tsx index 6c8bf788..3120f6e5 100644 --- a/src/components/BookmarkButton/index.tsx +++ b/src/components/BookmarkButton/index.tsx @@ -9,16 +9,12 @@ import { Event } from 'nostr-tools' export default function BookmarkButton({ event }: { event: Event }) { const { t } = useTranslation() const { toast } = useToast() - const { pubkey: accountPubkey, checkLogin } = useNostr() - const { bookmarks, addBookmark, removeBookmark } = useBookmarks() + const { pubkey: accountPubkey, bookmarkListEvent, checkLogin } = useNostr() + const { addBookmark, removeBookmark } = useBookmarks() const [updating, setUpdating] = useState(false) - - const eventId = event.id - const eventPubkey = event.pubkey - const isBookmarked = useMemo( - () => bookmarks.some((tag) => tag[0] === 'e' && tag[1] === eventId), - [bookmarks, eventId] + () => bookmarkListEvent?.tags.some((tag) => tag[0] === 'e' && tag[1] === event.id), + [bookmarkListEvent, event] ) if (!accountPubkey) return null @@ -30,11 +26,7 @@ export default function BookmarkButton({ event }: { event: Event }) { setUpdating(true) try { - await addBookmark(eventId, eventPubkey) - toast({ - title: t('Note bookmarked'), - description: t('This note has been added to your bookmarks') - }) + await addBookmark(event) } catch (error) { toast({ title: t('Bookmark failed'), @@ -54,11 +46,7 @@ export default function BookmarkButton({ event }: { event: Event }) { setUpdating(true) try { - await removeBookmark(eventId) - toast({ - title: t('Bookmark removed'), - description: t('This note has been removed from your bookmarks') - }) + await removeBookmark(event) } catch (error) { toast({ title: t('Remove bookmark failed'), @@ -74,8 +62,8 @@ export default function BookmarkButton({ event }: { event: Event }) { return ( ) diff --git a/src/components/BookmarkList/index.tsx b/src/components/BookmarkList/index.tsx new file mode 100644 index 00000000..ecd13411 --- /dev/null +++ b/src/components/BookmarkList/index.tsx @@ -0,0 +1,97 @@ +import { useFetchEvent } from '@/hooks' +import { generateEventIdFromETag } from '@/lib/tag' +import { useNostr } from '@/providers/NostrProvider' +import { kinds } from 'nostr-tools' +import { useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' +import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' + +const SHOW_COUNT = 10 + +export default function BookmarkList() { + const { t } = useTranslation() + const { bookmarkListEvent } = useNostr() + const eventIds = useMemo(() => { + if (!bookmarkListEvent) return [] + + return ( + bookmarkListEvent.tags + .map((tag) => (tag[0] === 'e' ? generateEventIdFromETag(tag) : undefined)) + .filter(Boolean) as `nevent1${string}`[] + ).reverse() + }, [bookmarkListEvent]) + const [showCount, setShowCount] = useState(SHOW_COUNT) + const bottomRef = useRef(null) + + useEffect(() => { + const options = { + root: null, + rootMargin: '10px', + threshold: 0.1 + } + + const loadMore = () => { + if (showCount < eventIds.length) { + setShowCount((prev) => prev + SHOW_COUNT) + } + } + + const observerInstance = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + loadMore() + } + }, options) + + const currentBottomRef = bottomRef.current + + if (currentBottomRef) { + observerInstance.observe(currentBottomRef) + } + + return () => { + if (observerInstance && currentBottomRef) { + observerInstance.unobserve(currentBottomRef) + } + } + }, [showCount, eventIds]) + + if (eventIds.length === 0) { + return ( +
+ {t('no bookmarks found')} +
+ ) + } + + return ( +
+ {eventIds.slice(0, showCount).map((eventId) => ( + + ))} + + {showCount < eventIds.length ? ( +
+ +
+ ) : ( +
+ {t('no more bookmarks')} +
+ )} +
+ ) +} + +function BookmarkedNote({ eventId }: { eventId: string }) { + const { event, isFetching } = useFetchEvent(eventId) + + if (isFetching) { + return + } + + if (!event || event.kind !== kinds.ShortTextNote) { + return null + } + + return +} diff --git a/src/components/BookmarksList/index.tsx b/src/components/BookmarksList/index.tsx deleted file mode 100644 index b0d54a9e..00000000 --- a/src/components/BookmarksList/index.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useFetchEvent } from '@/hooks' -import { useBookmarks } from '@/providers/BookmarksProvider' -import { useEffect, useMemo, useRef, useState } from 'react' -import { useTranslation } from 'react-i18next' -import { generateEventIdFromETag } from '@/lib/tag' -import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard' - -export default function BookmarksList() { - const { t } = useTranslation() - const { bookmarks } = useBookmarks() - const [visibleBookmarks, setVisibleBookmarks] = useState< - { eventId: string; neventId?: string }[] - >([]) - const [loading, setLoading] = useState(true) - const bottomRef = useRef(null) - const SHOW_COUNT = 10 - - const bookmarkItems = useMemo(() => { - return bookmarks - .filter((tag) => tag[0] === 'e') - .map((tag) => ({ - eventId: tag[1], - neventId: generateEventIdFromETag(tag) - })) - .reverse() - }, [bookmarks]) - - useEffect(() => { - setVisibleBookmarks(bookmarkItems.slice(0, SHOW_COUNT)) - setLoading(false) - }, [bookmarkItems]) - - useEffect(() => { - const options = { - root: null, - rootMargin: '10px', - threshold: 0.1 - } - - const loadMore = () => { - if (visibleBookmarks.length < bookmarkItems.length) { - setVisibleBookmarks((prev) => [ - ...prev, - ...bookmarkItems.slice(prev.length, prev.length + SHOW_COUNT) - ]) - } - } - - const observerInstance = new IntersectionObserver((entries) => { - if (entries[0].isIntersecting) { - loadMore() - } - }, options) - - const currentBottomRef = bottomRef.current - - if (currentBottomRef) { - observerInstance.observe(currentBottomRef) - } - - return () => { - if (observerInstance && currentBottomRef) { - observerInstance.unobserve(currentBottomRef) - } - } - }, [visibleBookmarks, bookmarkItems]) - - if (loading) { - return - } - - if (bookmarkItems.length === 0) { - return ( -
- {t('No bookmarks found. Add some by clicking the bookmark icon on notes.')} -
- ) - } - - return ( -
- {visibleBookmarks.map((item) => ( - - ))} - - {visibleBookmarks.length < bookmarkItems.length && ( -
- -
- )} -
- ) -} - -function BookmarkedNote({ eventId, neventId }: { eventId: string; neventId?: string }) { - const { event, isFetching } = useFetchEvent(neventId || eventId) - - if (isFetching) { - return - } - - if (!event) { - return null - } - - return -} diff --git a/src/components/FeedSwitcher/index.tsx b/src/components/FeedSwitcher/index.tsx index 5550386c..8426aa16 100644 --- a/src/components/FeedSwitcher/index.tsx +++ b/src/components/FeedSwitcher/index.tsx @@ -1,20 +1,18 @@ import { toRelaySettings } from '@/lib/link' import { simplifyUrl } from '@/lib/url' import { SecondaryPageLink } from '@/PageManager' -import { useBookmarks } from '@/providers/BookmarksProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFeed } from '@/providers/FeedProvider' import { useNostr } from '@/providers/NostrProvider' +import { BookmarkIcon, UsersRound } from 'lucide-react' import { useTranslation } from 'react-i18next' import RelayIcon from '../RelayIcon' import RelaySetCard from '../RelaySetCard' import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu' -import { BookmarkIcon, UsersRound } from 'lucide-react' export default function FeedSwitcher({ close }: { close?: () => void }) { const { t } = useTranslation() const { pubkey } = useNostr() - const { bookmarks } = useBookmarks() const { relaySets, favoriteRelays } = useFavoriteRelays() const { feedInfo, switchFeed, temporaryRelayUrls } = useFeed() @@ -38,7 +36,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) { )} - {pubkey && bookmarks.length > 0 && ( + {pubkey && ( { diff --git a/src/components/NoteStats/index.tsx b/src/components/NoteStats/index.tsx index 8e3af788..9185b141 100644 --- a/src/components/NoteStats/index.tsx +++ b/src/components/NoteStats/index.tsx @@ -65,9 +65,9 @@ export default function NoteStats({ -
e.stopPropagation()}> +
diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 94c444b5..bd9836fc 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -218,6 +218,11 @@ export default { 'no relays found': 'لم يتم العثور على ريلايات', video: 'فيديو', 'Show n new notes': 'عرض {{n}} ملاحظات جديدة', - YouTabName: 'أنت' + YouTabName: 'أنت', + Bookmark: 'الإشارة المرجعية', + 'Remove bookmark': 'إزالة الإشارة', + 'no bookmarks found': 'لم يتم العثور على إشارات', + 'no more bookmarks': 'لا مزيد من الإشارات', + Bookmarks: 'الإشارات المرجعية' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index c66b71fc..be1d8786 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -222,6 +222,11 @@ export default { 'no relays found': 'Keine Relays gefunden', video: 'Video', 'Show n new notes': 'Zeige {{n}} neue Notizen', - YouTabName: 'Du' + YouTabName: 'Du', + Bookmark: 'Lesezeichen', + 'Remove bookmark': 'Lesezeichen entfernen', + 'no bookmarks found': 'Keine Lesezeichen gefunden', + 'no more bookmarks': 'Keine weiteren Lesezeichen', + Bookmarks: 'Lesezeichen' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index c844bc01..3a5dc63e 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -218,6 +218,11 @@ export default { 'no relays found': 'no relays found', video: 'video', 'Show n new notes': 'Show {{n}} new notes', - YouTabName: 'You' + YouTabName: 'You', + Bookmark: 'Bookmark', + 'Remove bookmark': 'Remove bookmark', + 'no bookmarks found': 'no bookmarks found', + 'no more bookmarks': 'no more bookmarks', + Bookmarks: 'Bookmarks' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index ca20d777..43710206 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -222,6 +222,11 @@ export default { 'no relays found': 'no se encontraron relés', video: 'video', 'Show n new notes': 'Mostrar {{n}} nuevas notas', - YouTabName: 'You' + YouTabName: 'You', + Bookmark: 'Marcador', + 'Remove bookmark': 'Quitar marcador', + 'no bookmarks found': 'No se encontraron marcadores', + 'no more bookmarks': 'No hay más marcadores', + Bookmarks: 'Marcadores' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index cc10200b..45bd18be 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -221,6 +221,11 @@ export default { 'no relays found': 'aucun relais trouvé', video: 'vidéo', 'Show n new notes': 'Afficher {{n}} nouvelles notes', - YouTabName: 'Vous' + YouTabName: 'Vous', + Bookmark: 'Favori', + 'Remove bookmark': 'Retirer le favori', + 'no bookmarks found': 'Aucun favori trouvé', + 'no more bookmarks': 'Plus de favoris', + Bookmarks: 'Favoris' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 485d93db..e2074d85 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -221,6 +221,11 @@ export default { 'no relays found': 'Nessun relay trovato', video: 'video', 'Show n new notes': 'Mostra {{n}} nuove note', - YouTabName: 'Tu' + YouTabName: 'Tu', + Bookmark: 'Segnalibro', + 'Remove bookmark': 'Rimuovi segnalibro', + 'no bookmarks found': 'Nessun segnalibro trovato', + 'no more bookmarks': 'Nessun altro segnalibro', + Bookmarks: 'Segnalibri' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 441b93de..25f1ffdf 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -219,6 +219,11 @@ export default { 'no relays found': 'リレイが見つかりません', video: 'ビデオ', 'Show n new notes': '新しいノートを{{n}}件表示', - YouTabName: 'あなた' + YouTabName: 'あなた', + Bookmark: 'ブックマーク', + 'Remove bookmark': 'ブックマークを削除', + 'no bookmarks found': 'ブックマークが見つかりません', + 'no more bookmarks': 'これ以上ブックマークはありません', + Bookmarks: 'ブックマーク一覧' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index 34e3a18c..7b2bf78e 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -220,6 +220,11 @@ export default { 'no relays found': 'Nie znaleziono transmiterów', video: 'wideo', 'Show n new notes': 'Pokaż {{n}} nowych wpisów', - YouTabName: 'Ty' + YouTabName: 'Ty', + Bookmark: 'Zakładka', + 'Remove bookmark': 'Usuń zakładkę', + 'no bookmarks found': 'Nie znaleziono zakładek', + 'no more bookmarks': 'Brak więcej zakładek', + Bookmarks: 'Zakładki' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index ae0ee1c4..64725449 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -220,6 +220,11 @@ export default { 'no relays found': 'nenhum relé encontrado', video: 'vídeo', 'Show n new notes': 'Ver {{n}} novas notas', - YouTabName: 'Você' + YouTabName: 'Você', + Bookmark: 'Favorito', + 'Remove bookmark': 'Remover favorito', + 'no bookmarks found': 'Nenhum favorito encontrado', + 'no more bookmarks': 'Sem mais favoritos', + Bookmarks: 'Favoritos' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 0c35107f..4750c38c 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -221,6 +221,11 @@ export default { 'no relays found': 'nenhum relé encontrado', video: 'vídeo', 'Show n new notes': 'Mostrar {{n}} novas notas', - YouTabName: 'Você' + YouTabName: 'Você', + Bookmark: 'Favorito', + 'Remove bookmark': 'Remover favorito', + 'no bookmarks found': 'Nenhum favorito encontrado', + 'no more bookmarks': 'Sem mais favoritos', + Bookmarks: 'Favoritos' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 043042dc..3b6aec65 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -222,6 +222,11 @@ export default { 'no relays found': 'ретрансляторы не найдены', video: 'видео', 'Show n new notes': 'Показать {{n}} новых заметок', - YouTabName: 'Вы' + YouTabName: 'Вы', + Bookmark: 'Закладка', + 'Remove bookmark': 'Удалить закладку', + 'no bookmarks found': 'Закладки не найдены', + 'no more bookmarks': 'Больше нет закладок', + Bookmarks: 'Закладки' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 1112d09c..a362efcf 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -219,6 +219,11 @@ export default { 'no relays found': '未找到服务器', video: '视频', 'Show n new notes': '显示 {{n}} 条新笔记', - YouTabName: '与你' + YouTabName: '与你', + Bookmark: '收藏', + 'Remove bookmark': '取消收藏', + 'no bookmarks found': '暂无收藏', + 'no more bookmarks': '到底了', + Bookmarks: '收藏' } } diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index 6ffb084d..a811cc35 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -283,10 +283,10 @@ export function createSeenNotificationsAtDraftEvent(): TDraftEvent { } } -export function createBookmarkDraftEvent(tags: string[][]): TDraftEvent { +export function createBookmarkDraftEvent(tags: string[][], content = ''): TDraftEvent { return { kind: kinds.BookmarkList, - content: '', + content, tags, created_at: dayjs().unix() } diff --git a/src/pages/primary/NoteListPage/index.tsx b/src/pages/primary/NoteListPage/index.tsx index 88e2b313..43288535 100644 --- a/src/pages/primary/NoteListPage/index.tsx +++ b/src/pages/primary/NoteListPage/index.tsx @@ -1,4 +1,4 @@ -import BookmarksList from '@/components/BookmarksList' +import BookmarkList from '@/components/BookmarkList' import NoteList from '@/components/NoteList' import PostEditor from '@/components/PostEditor' import SaveRelayDropdownMenu from '@/components/SaveRelayDropdownMenu' @@ -46,7 +46,7 @@ const NoteListPage = forwardRef((_, ref) => { ) } else { - content = + content = } } else if (isReady) { content = ( diff --git a/src/providers/BookmarksProvider.tsx b/src/providers/BookmarksProvider.tsx index 1935b18d..99469f9e 100644 --- a/src/providers/BookmarksProvider.tsx +++ b/src/providers/BookmarksProvider.tsx @@ -1,12 +1,12 @@ import { createBookmarkDraftEvent } from '@/lib/draft-event' -import { createContext, useContext, useMemo } from 'react' -import { useNostr } from './NostrProvider' import client from '@/services/client.service' +import { createContext, useContext } from 'react' +import { useNostr } from './NostrProvider' +import { Event } from 'nostr-tools' type TBookmarksContext = { - bookmarks: string[][] - addBookmark: (eventId: string, eventPubkey: string, relayHint?: string) => Promise - removeBookmark: (eventId: string) => Promise + addBookmark: (event: Event) => Promise + removeBookmark: (event: Event) => Promise } const BookmarksContext = createContext(undefined) @@ -20,40 +20,34 @@ export const useBookmarks = () => { } export function BookmarksProvider({ children }: { children: React.ReactNode }) { - const { pubkey: accountPubkey, bookmarkListEvent, publish, updateBookmarkListEvent } = useNostr() - const bookmarks = useMemo( - () => (bookmarkListEvent ? bookmarkListEvent.tags : []), - [bookmarkListEvent] - ) + const { pubkey: accountPubkey, publish, updateBookmarkListEvent } = useNostr() - const addBookmark = async (eventId: string, eventPubkey: string, relayHint?: string) => { + const addBookmark = async (event: Event) => { if (!accountPubkey) return - const relayHintToUse = relayHint || client.getEventHint(eventId) - - const newTag = ['e', eventId, relayHintToUse, eventPubkey] - + const bookmarkListEvent = await client.fetchBookmarkListEvent(accountPubkey) const currentTags = bookmarkListEvent?.tags || [] - const isDuplicate = currentTags.some((tag) => tag[0] === 'e' && tag[1] === eventId) + if (currentTags.some((tag) => tag[0] === 'e' && tag[1] === event.id)) return - if (isDuplicate) return - - const newTags = [...currentTags, newTag] - - const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags) + const newBookmarkDraftEvent = createBookmarkDraftEvent( + [...currentTags, ['e', event.id, client.getEventHint(event.id), event.pubkey]], + bookmarkListEvent?.content + ) const newBookmarkEvent = await publish(newBookmarkDraftEvent) await updateBookmarkListEvent(newBookmarkEvent) } - const removeBookmark = async (eventId: string) => { - if (!accountPubkey || !bookmarkListEvent) return + const removeBookmark = async (event: Event) => { + if (!accountPubkey) return - const newTags = bookmarkListEvent.tags.filter((tag) => !(tag[0] === 'e' && tag[1] === eventId)) + const bookmarkListEvent = await client.fetchBookmarkListEvent(accountPubkey) + if (!bookmarkListEvent) return + const newTags = bookmarkListEvent.tags.filter((tag) => !(tag[0] === 'e' && tag[1] === event.id)) if (newTags.length === bookmarkListEvent.tags.length) return - const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags) + const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags, bookmarkListEvent.content) const newBookmarkEvent = await publish(newBookmarkDraftEvent) await updateBookmarkListEvent(newBookmarkEvent) } @@ -61,7 +55,6 @@ export function BookmarksProvider({ children }: { children: React.ReactNode }) { return ( { + const storedBookmarkListEvent = await indexedDb.getReplaceableEvent(pubkey, kinds.BookmarkList) + if (storedBookmarkListEvent) { + return storedBookmarkListEvent + } + + const relayList = await this.fetchRelayList(pubkey) + const events = await this.query(relayList.write.concat(BIG_RELAY_URLS), { + authors: [pubkey], + kinds: [kinds.BookmarkList] + }) + + return events.sort((a, b) => b.created_at - a.created_at)[0] + } + async fetchFollowings(pubkey: string, storeToIndexedDb = false) { const followListEvent = await this.fetchFollowListEvent(pubkey, storeToIndexedDb) return followListEvent ? extractPubkeysFromEventTags(followListEvent.tags) : []