feat: bookmarks (#279)

This commit is contained in:
M.Abubakar
2025-04-18 18:51:36 +05:00
committed by GitHub
parent 085adeb096
commit 7876f26d0c
13 changed files with 390 additions and 15 deletions

View File

@@ -0,0 +1,72 @@
import { createBookmarkDraftEvent } from '@/lib/draft-event'
import { createContext, useContext, useMemo } from 'react'
import { useNostr } from './NostrProvider'
import client from '@/services/client.service'
type TBookmarksContext = {
bookmarks: string[][]
addBookmark: (eventId: string, eventPubkey: string, relayHint?: string) => Promise<void>
removeBookmark: (eventId: string) => Promise<void>
}
const BookmarksContext = createContext<TBookmarksContext | undefined>(undefined)
export const useBookmarks = () => {
const context = useContext(BookmarksContext)
if (!context) {
throw new Error('useBookmarks must be used within a BookmarksProvider')
}
return context
}
export function BookmarksProvider({ children }: { children: React.ReactNode }) {
const { pubkey: accountPubkey, bookmarkListEvent, publish, updateBookmarkListEvent } = useNostr()
const bookmarks = useMemo(
() => (bookmarkListEvent ? bookmarkListEvent.tags : []),
[bookmarkListEvent]
)
const addBookmark = async (eventId: string, eventPubkey: string, relayHint?: string) => {
if (!accountPubkey) return
const relayHintToUse = relayHint || client.getEventHint(eventId)
const newTag = ['e', eventId, relayHintToUse, eventPubkey]
const currentTags = bookmarkListEvent?.tags || []
const isDuplicate = currentTags.some((tag) => tag[0] === 'e' && tag[1] === eventId)
if (isDuplicate) return
const newTags = [...currentTags, newTag]
const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags)
const newBookmarkEvent = await publish(newBookmarkDraftEvent)
await updateBookmarkListEvent(newBookmarkEvent)
}
const removeBookmark = async (eventId: string) => {
if (!accountPubkey || !bookmarkListEvent) return
const newTags = bookmarkListEvent.tags.filter((tag) => !(tag[0] === 'e' && tag[1] === eventId))
if (newTags.length === bookmarkListEvent.tags.length) return
const newBookmarkDraftEvent = createBookmarkDraftEvent(newTags)
const newBookmarkEvent = await publish(newBookmarkDraftEvent)
await updateBookmarkListEvent(newBookmarkEvent)
}
return (
<BookmarksContext.Provider
value={{
bookmarks,
addBookmark,
removeBookmark
}}
>
{children}
</BookmarksContext.Provider>
)
}

View File

@@ -170,6 +170,20 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
})
return setIsReady(true)
}
if (feedType === 'bookmarks') {
if (!options.pubkey) {
return setIsReady(true)
}
const newFeedInfo = { feedType }
setFeedInfo(newFeedInfo)
feedInfoRef.current = newFeedInfo
storage.setFeedInfo(newFeedInfo, pubkey)
setRelayUrls([])
setFilter({})
return setIsReady(true)
}
if (feedType === 'temporary') {
const urls = options.temporaryRelayUrls ?? temporaryRelayUrls
if (!urls.length) {

View File

@@ -33,6 +33,7 @@ type TNostrContext = {
relayList: TRelayList | null
followListEvent?: Event
muteListEvent?: Event
bookmarkListEvent?: Event
favoriteRelaysEvent: Event | null
notificationsSeenAt: number
account: TAccountPointer | null
@@ -60,6 +61,7 @@ type TNostrContext = {
updateProfileEvent: (profileEvent: Event) => Promise<void>
updateFollowListEvent: (followListEvent: Event) => Promise<void>
updateMuteListEvent: (muteListEvent: Event, tags: string[][]) => Promise<void>
updateBookmarkListEvent: (bookmarkListEvent: Event) => Promise<void>
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
updateNotificationsSeenAt: () => Promise<void>
}
@@ -87,6 +89,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const [relayList, setRelayList] = useState<TRelayList | null>(null)
const [followListEvent, setFollowListEvent] = useState<Event | undefined>(undefined)
const [muteListEvent, setMuteListEvent] = useState<Event | undefined>(undefined)
const [bookmarkListEvent, setBookmarkListEvent] = useState<Event | undefined>(undefined)
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | null>(null)
const [notificationsSeenAt, setNotificationsSeenAt] = useState(-1)
const [isInitialized, setIsInitialized] = useState(false)
@@ -149,12 +152,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
storedProfileEvent,
storedFollowListEvent,
storedMuteListEvent,
storedBookmarkListEvent,
storedFavoriteRelaysEvent
] = await Promise.all([
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
indexedDb.getReplaceableEvent(account.pubkey, kinds.Contacts),
indexedDb.getReplaceableEvent(account.pubkey, kinds.Mutelist),
indexedDb.getReplaceableEvent(account.pubkey, kinds.BookmarkList),
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS)
])
if (storedRelayListEvent) {
@@ -172,6 +177,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
if (storedMuteListEvent) {
setMuteListEvent(storedMuteListEvent)
}
if (storedBookmarkListEvent) {
setBookmarkListEvent(storedBookmarkListEvent)
}
if (storedFavoriteRelaysEvent) {
setFavoriteRelaysEvent(storedFavoriteRelaysEvent)
}
@@ -190,7 +198,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const events = await client.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 4), [
{
kinds: [kinds.Metadata, kinds.Contacts, kinds.Mutelist, ExtendedKind.FAVORITE_RELAYS],
kinds: [
kinds.Metadata,
kinds.Contacts,
kinds.Mutelist,
kinds.BookmarkList,
ExtendedKind.FAVORITE_RELAYS
],
authors: [account.pubkey]
},
{
@@ -203,6 +217,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata)
const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts)
const muteListEvent = sortedEvents.find((e) => e.kind === kinds.Mutelist)
const bookmarkListEvent = sortedEvents.find((e) => e.kind === kinds.BookmarkList)
const favoriteRelaysEvent = sortedEvents.find((e) => e.kind === ExtendedKind.FAVORITE_RELAYS)
const notificationsSeenAtEvent = sortedEvents.find(
(e) =>
@@ -227,6 +242,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
setMuteListEvent(muteListEvent)
await indexedDb.putReplaceableEvent(muteListEvent)
}
if (bookmarkListEvent) {
setBookmarkListEvent(bookmarkListEvent)
await indexedDb.putReplaceableEvent(bookmarkListEvent)
}
if (favoriteRelaysEvent) {
setFavoriteRelaysEvent(favoriteRelaysEvent)
await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
@@ -563,6 +582,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
setMuteListEvent(muteListEvent)
}
const updateBookmarkListEvent = async (bookmarkListEvent: Event) => {
const newBookmarkListEvent = await indexedDb.putReplaceableEvent(bookmarkListEvent)
if (newBookmarkListEvent.id !== bookmarkListEvent.id) return
setBookmarkListEvent(newBookmarkListEvent)
}
const updateFavoriteRelaysEvent = async (favoriteRelaysEvent: Event) => {
const newFavoriteRelaysEvent = await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
if (newFavoriteRelaysEvent.id !== favoriteRelaysEvent.id) return
@@ -591,6 +617,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
relayList,
followListEvent,
muteListEvent,
bookmarkListEvent,
favoriteRelaysEvent,
notificationsSeenAt,
account,
@@ -617,6 +644,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
updateProfileEvent,
updateFollowListEvent,
updateMuteListEvent,
updateBookmarkListEvent,
updateFavoriteRelaysEvent,
updateNotificationsSeenAt
}}