feat: add pinned post functionality
This commit is contained in:
parent
9c554da2da
commit
d131026af9
31 changed files with 563 additions and 56 deletions
|
|
@ -55,6 +55,7 @@ type TNostrContext = {
|
|||
bookmarkListEvent: Event | null
|
||||
favoriteRelaysEvent: Event | null
|
||||
userEmojiListEvent: Event | null
|
||||
pinListEvent: Event | null
|
||||
notificationsSeenAt: number
|
||||
account: TAccountPointer | null
|
||||
accounts: TAccountPointer[]
|
||||
|
|
@ -85,6 +86,7 @@ type TNostrContext = {
|
|||
updateMuteListEvent: (muteListEvent: Event, privateTags: string[][]) => Promise<void>
|
||||
updateBookmarkListEvent: (bookmarkListEvent: Event) => Promise<void>
|
||||
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
|
||||
updatePinListEvent: (pinListEvent: Event) => Promise<void>
|
||||
updateNotificationsSeenAt: (skipPublish?: boolean) => Promise<void>
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +121,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
const [bookmarkListEvent, setBookmarkListEvent] = useState<Event | null>(null)
|
||||
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | null>(null)
|
||||
const [userEmojiListEvent, setUserEmojiListEvent] = useState<Event | null>(null)
|
||||
const [pinListEvent, setPinListEvent] = useState<Event | null>(null)
|
||||
const [notificationsSeenAt, setNotificationsSeenAt] = useState(-1)
|
||||
const [isInitialized, setIsInitialized] = useState(false)
|
||||
|
||||
|
|
@ -161,6 +164,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
setFollowListEvent(null)
|
||||
setMuteListEvent(null)
|
||||
setBookmarkListEvent(null)
|
||||
setPinListEvent(null)
|
||||
setNotificationsSeenAt(-1)
|
||||
if (!account) {
|
||||
return
|
||||
|
|
@ -189,7 +193,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
storedMuteListEvent,
|
||||
storedBookmarkListEvent,
|
||||
storedFavoriteRelaysEvent,
|
||||
storedUserEmojiListEvent
|
||||
storedUserEmojiListEvent,
|
||||
storedPinListEvent
|
||||
] = await Promise.all([
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
|
||||
|
|
@ -197,7 +202,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Mutelist),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.BookmarkList),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.UserEmojiList)
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.UserEmojiList),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Pinlist)
|
||||
])
|
||||
if (storedRelayListEvent) {
|
||||
setRelayList(getRelayListFromEvent(storedRelayListEvent))
|
||||
|
|
@ -221,6 +227,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
if (storedUserEmojiListEvent) {
|
||||
setUserEmojiListEvent(storedUserEmojiListEvent)
|
||||
}
|
||||
if (storedPinListEvent) {
|
||||
setPinListEvent(storedPinListEvent)
|
||||
}
|
||||
|
||||
const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
|
||||
kinds: [kinds.RelayList],
|
||||
|
|
@ -243,7 +252,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
kinds.BookmarkList,
|
||||
ExtendedKind.FAVORITE_RELAYS,
|
||||
ExtendedKind.BLOSSOM_SERVER_LIST,
|
||||
kinds.UserEmojiList
|
||||
kinds.UserEmojiList,
|
||||
kinds.Pinlist
|
||||
],
|
||||
authors: [account.pubkey]
|
||||
},
|
||||
|
|
@ -268,6 +278,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
e.kind === kinds.Application &&
|
||||
getReplaceableEventIdentifier(e) === ApplicationDataKey.NOTIFICATIONS_SEEN_AT
|
||||
)
|
||||
const pinnedNotesEvent = sortedEvents.find((e) => e.kind === kinds.Pinlist)
|
||||
if (profileEvent) {
|
||||
const updatedProfileEvent = await indexedDb.putReplaceableEvent(profileEvent)
|
||||
if (updatedProfileEvent.id === profileEvent.id) {
|
||||
|
|
@ -314,6 +325,12 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
setUserEmojiListEvent(updatedUserEmojiListEvent)
|
||||
}
|
||||
}
|
||||
if (pinnedNotesEvent) {
|
||||
const updatedPinnedNotesEvent = await indexedDb.putReplaceableEvent(pinnedNotesEvent)
|
||||
if (updatedPinnedNotesEvent.id === pinnedNotesEvent.id) {
|
||||
setPinListEvent(updatedPinnedNotesEvent)
|
||||
}
|
||||
}
|
||||
|
||||
const notificationsSeenAt = Math.max(
|
||||
notificationsSeenAtEvent?.created_at ?? 0,
|
||||
|
|
@ -726,6 +743,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
setFavoriteRelaysEvent(newFavoriteRelaysEvent)
|
||||
}
|
||||
|
||||
const updatePinListEvent = async (pinListEvent: Event) => {
|
||||
const newPinListEvent = await indexedDb.putReplaceableEvent(pinListEvent)
|
||||
if (newPinListEvent.id !== pinListEvent.id) return
|
||||
|
||||
setPinListEvent(newPinListEvent)
|
||||
}
|
||||
|
||||
const updateNotificationsSeenAt = async (skipPublish = false) => {
|
||||
if (!account) return
|
||||
|
||||
|
|
@ -761,6 +785,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
bookmarkListEvent,
|
||||
favoriteRelaysEvent,
|
||||
userEmojiListEvent,
|
||||
pinListEvent,
|
||||
notificationsSeenAt,
|
||||
account,
|
||||
accounts,
|
||||
|
|
@ -788,6 +813,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
updateMuteListEvent,
|
||||
updateBookmarkListEvent,
|
||||
updateFavoriteRelaysEvent,
|
||||
updatePinListEvent,
|
||||
updateNotificationsSeenAt
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
111
src/providers/PinListProvider.tsx
Normal file
111
src/providers/PinListProvider.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { MAX_PINNED_NOTES } from '@/constants'
|
||||
import { buildETag, createPinListDraftEvent } from '@/lib/draft-event'
|
||||
import { getPinnedEventHexIdSetFromPinListEvent } from '@/lib/event-metadata'
|
||||
import client from '@/services/client.service'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { createContext, useContext, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { toast } from 'sonner'
|
||||
import { useNostr } from './NostrProvider'
|
||||
|
||||
type TPinListContext = {
|
||||
pinnedEventHexIdSet: Set<string>
|
||||
pin: (event: Event) => Promise<void>
|
||||
unpin: (event: Event) => Promise<void>
|
||||
}
|
||||
|
||||
const PinListContext = createContext<TPinListContext | undefined>(undefined)
|
||||
|
||||
export const usePinList = () => {
|
||||
const context = useContext(PinListContext)
|
||||
if (!context) {
|
||||
throw new Error('usePinList must be used within a PinListProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function PinListProvider({ children }: { children: React.ReactNode }) {
|
||||
const { t } = useTranslation()
|
||||
const { pubkey: accountPubkey, pinListEvent, publish, updatePinListEvent } = useNostr()
|
||||
const pinnedEventHexIdSet = useMemo(
|
||||
() => getPinnedEventHexIdSetFromPinListEvent(pinListEvent),
|
||||
[pinListEvent]
|
||||
)
|
||||
|
||||
const pin = async (event: Event) => {
|
||||
if (!accountPubkey) return
|
||||
|
||||
if (event.kind !== kinds.ShortTextNote || event.pubkey !== accountPubkey) return
|
||||
|
||||
const _pin = async () => {
|
||||
const pinListEvent = await client.fetchPinListEvent(accountPubkey)
|
||||
const currentTags = pinListEvent?.tags || []
|
||||
|
||||
if (currentTags.some((tag) => tag[0] === 'e' && tag[1] === event.id)) {
|
||||
return
|
||||
}
|
||||
|
||||
let newTags = [...currentTags, buildETag(event.id, event.pubkey)]
|
||||
const eTagCount = newTags.filter((tag) => tag[0] === 'e').length
|
||||
if (eTagCount > MAX_PINNED_NOTES) {
|
||||
let removed = 0
|
||||
const needRemove = eTagCount - MAX_PINNED_NOTES
|
||||
newTags = newTags.filter((tag) => {
|
||||
if (tag[0] === 'e' && removed < needRemove) {
|
||||
removed += 1
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
const newPinListDraftEvent = createPinListDraftEvent(newTags, pinListEvent?.content)
|
||||
const newPinListEvent = await publish(newPinListDraftEvent)
|
||||
await updatePinListEvent(newPinListEvent)
|
||||
}
|
||||
|
||||
const { unwrap } = toast.promise(_pin, {
|
||||
loading: t('Pinning...'),
|
||||
success: t('Pinned!'),
|
||||
error: (err) => t('Failed to pin: {{error}}', { error: err.message })
|
||||
})
|
||||
await unwrap()
|
||||
}
|
||||
|
||||
const unpin = async (event: Event) => {
|
||||
if (!accountPubkey) return
|
||||
|
||||
if (event.kind !== kinds.ShortTextNote || event.pubkey !== accountPubkey) return
|
||||
|
||||
const _unpin = async () => {
|
||||
const pinListEvent = await client.fetchPinListEvent(accountPubkey)
|
||||
if (!pinListEvent) return
|
||||
|
||||
const newTags = pinListEvent.tags.filter((tag) => tag[0] !== 'e' || tag[1] !== event.id)
|
||||
if (newTags.length === pinListEvent.tags.length) return
|
||||
|
||||
const newPinListDraftEvent = createPinListDraftEvent(newTags, pinListEvent.content)
|
||||
const newPinListEvent = await publish(newPinListDraftEvent)
|
||||
await updatePinListEvent(newPinListEvent)
|
||||
}
|
||||
|
||||
const { unwrap } = toast.promise(_unpin, {
|
||||
loading: t('Unpinning...'),
|
||||
success: t('Unpinned!'),
|
||||
error: (err) => t('Failed to unpin: {{error}}', { error: err.message })
|
||||
})
|
||||
await unwrap()
|
||||
}
|
||||
|
||||
return (
|
||||
<PinListContext.Provider
|
||||
value={{
|
||||
pinnedEventHexIdSet,
|
||||
pin,
|
||||
unpin
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PinListContext.Provider>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue