feat: pinned users event
This commit is contained in:
parent
ad016aba35
commit
7ec4835c61
10 changed files with 303 additions and 136 deletions
|
|
@ -54,25 +54,30 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
|||
}, [publicMutePubkeySet, privateMutePubkeySet])
|
||||
const [changing, setChanging] = useState(false)
|
||||
|
||||
const getPrivateTags = async (muteListEvent: Event) => {
|
||||
if (!muteListEvent.content) return []
|
||||
const getPrivateTags = useCallback(
|
||||
async (muteListEvent: Event) => {
|
||||
if (!muteListEvent.content) return []
|
||||
|
||||
const storedDecryptedTags = await indexedDb.getMuteDecryptedTags(muteListEvent.id)
|
||||
|
||||
if (storedDecryptedTags) {
|
||||
return storedDecryptedTags
|
||||
} else {
|
||||
try {
|
||||
const plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
||||
const storedPlainText = await indexedDb.getDecryptedContent(muteListEvent.id)
|
||||
|
||||
let plainText: string
|
||||
if (storedPlainText) {
|
||||
plainText = storedPlainText
|
||||
} else {
|
||||
plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
||||
await indexedDb.putDecryptedContent(muteListEvent.id, plainText)
|
||||
}
|
||||
|
||||
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||
await indexedDb.putMuteDecryptedTags(muteListEvent.id, privateTags)
|
||||
return privateTags
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt mute list content', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[nip04Decrypt]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const updateMuteTags = async () => {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ type TNostrContext = {
|
|||
favoriteRelaysEvent: Event | null
|
||||
userEmojiListEvent: Event | null
|
||||
pinListEvent: Event | null
|
||||
pinnedUsersEvent: Event | null
|
||||
notificationsSeenAt: number
|
||||
account: TAccountPointer | null
|
||||
accounts: TAccountPointer[]
|
||||
|
|
@ -88,6 +89,7 @@ type TNostrContext = {
|
|||
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
|
||||
updateUserEmojiListEvent: (userEmojiListEvent: Event) => Promise<void>
|
||||
updatePinListEvent: (pinListEvent: Event) => Promise<void>
|
||||
updatePinnedUsersEvent: (pinnedUsersEvent: Event, privateTags?: string[][]) => Promise<void>
|
||||
updateNotificationsSeenAt: (skipPublish?: boolean) => Promise<void>
|
||||
}
|
||||
|
||||
|
|
@ -119,6 +121,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
const [relayList, setRelayList] = useState<TRelayList | null>(null)
|
||||
const [followListEvent, setFollowListEvent] = useState<Event | null>(null)
|
||||
const [muteListEvent, setMuteListEvent] = useState<Event | null>(null)
|
||||
const [pinnedUsersEvent, setPinnedUsersEvent] = useState<Event | null>(null)
|
||||
const [bookmarkListEvent, setBookmarkListEvent] = useState<Event | null>(null)
|
||||
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | null>(null)
|
||||
const [userEmojiListEvent, setUserEmojiListEvent] = useState<Event | null>(null)
|
||||
|
|
@ -195,7 +198,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
storedBookmarkListEvent,
|
||||
storedFavoriteRelaysEvent,
|
||||
storedUserEmojiListEvent,
|
||||
storedPinListEvent
|
||||
storedPinListEvent,
|
||||
storedPinnedUsersEvent
|
||||
] = await Promise.all([
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
|
||||
|
|
@ -204,7 +208,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
indexedDb.getReplaceableEvent(account.pubkey, kinds.BookmarkList),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.UserEmojiList),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Pinlist)
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Pinlist),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.PINNED_USERS)
|
||||
])
|
||||
if (storedRelayListEvent) {
|
||||
setRelayList(getRelayListFromEvent(storedRelayListEvent, storage.getFilterOutOnionRelays()))
|
||||
|
|
@ -231,6 +236,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
if (storedPinListEvent) {
|
||||
setPinListEvent(storedPinListEvent)
|
||||
}
|
||||
if (storedPinnedUsersEvent !== undefined) {
|
||||
setPinnedUsersEvent(storedPinnedUsersEvent)
|
||||
}
|
||||
|
||||
const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
|
||||
kinds: [kinds.RelayList],
|
||||
|
|
@ -254,7 +262,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
ExtendedKind.FAVORITE_RELAYS,
|
||||
ExtendedKind.BLOSSOM_SERVER_LIST,
|
||||
kinds.UserEmojiList,
|
||||
kinds.Pinlist
|
||||
kinds.Pinlist,
|
||||
ExtendedKind.PINNED_USERS
|
||||
],
|
||||
authors: [account.pubkey]
|
||||
},
|
||||
|
|
@ -280,6 +289,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
getReplaceableEventIdentifier(e) === ApplicationDataKey.NOTIFICATIONS_SEEN_AT
|
||||
)
|
||||
const pinnedNotesEvent = sortedEvents.find((e) => e.kind === kinds.Pinlist)
|
||||
const pinnedUsersEvent = sortedEvents.find((e) => e.kind === ExtendedKind.PINNED_USERS)
|
||||
|
||||
if (profileEvent) {
|
||||
const updatedProfileEvent = await indexedDb.putReplaceableEvent(profileEvent)
|
||||
if (updatedProfileEvent.id === profileEvent.id) {
|
||||
|
|
@ -332,6 +343,15 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
setPinListEvent(updatedPinnedNotesEvent)
|
||||
}
|
||||
}
|
||||
if (pinnedUsersEvent) {
|
||||
const updatedPinnedUsersEvent = await indexedDb.putReplaceableEvent(pinnedUsersEvent)
|
||||
if (updatedPinnedUsersEvent.id === pinnedUsersEvent.id) {
|
||||
setPinnedUsersEvent(updatedPinnedUsersEvent)
|
||||
}
|
||||
} else {
|
||||
await indexedDb.putNullReplaceableEvent(account.pubkey, ExtendedKind.PINNED_USERS)
|
||||
setPinnedUsersEvent(null)
|
||||
}
|
||||
|
||||
const notificationsSeenAt = Math.max(
|
||||
notificationsSeenAtEvent?.created_at ?? 0,
|
||||
|
|
@ -726,7 +746,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
const newMuteListEvent = await indexedDb.putReplaceableEvent(muteListEvent)
|
||||
if (newMuteListEvent.id !== muteListEvent.id) return
|
||||
|
||||
await indexedDb.putMuteDecryptedTags(muteListEvent.id, privateTags)
|
||||
await indexedDb.putDecryptedContent(muteListEvent.id, JSON.stringify(privateTags))
|
||||
setMuteListEvent(muteListEvent)
|
||||
}
|
||||
|
||||
|
|
@ -758,6 +778,16 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
setPinListEvent(newPinListEvent)
|
||||
}
|
||||
|
||||
const updatePinnedUsersEvent = async (pinnedUsersEvent: Event, privateTags?: string[][]) => {
|
||||
const newPinnedUsersEvent = await indexedDb.putReplaceableEvent(pinnedUsersEvent)
|
||||
if (newPinnedUsersEvent.id !== pinnedUsersEvent.id) return
|
||||
|
||||
if (privateTags) {
|
||||
await indexedDb.putDecryptedContent(pinnedUsersEvent.id, JSON.stringify(privateTags))
|
||||
}
|
||||
setPinnedUsersEvent(newPinnedUsersEvent)
|
||||
}
|
||||
|
||||
const updateNotificationsSeenAt = async (skipPublish = false) => {
|
||||
if (!account) return
|
||||
|
||||
|
|
@ -794,6 +824,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
favoriteRelaysEvent,
|
||||
userEmojiListEvent,
|
||||
pinListEvent,
|
||||
pinnedUsersEvent,
|
||||
notificationsSeenAt,
|
||||
account,
|
||||
accounts,
|
||||
|
|
@ -823,6 +854,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||
updateFavoriteRelaysEvent,
|
||||
updateUserEmojiListEvent,
|
||||
updatePinListEvent,
|
||||
updatePinnedUsersEvent,
|
||||
updateNotificationsSeenAt
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
171
src/providers/PinnedUsersProvider.tsx
Normal file
171
src/providers/PinnedUsersProvider.tsx
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
import { ExtendedKind } from '@/constants'
|
||||
import { getPubkeysFromPTags } from '@/lib/tag'
|
||||
import indexedDb from '@/services/indexed-db.service'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { z } from 'zod'
|
||||
import { useNostr } from './NostrProvider'
|
||||
|
||||
type TPinnedUsersContext = {
|
||||
pinnedPubkeySet: Set<string>
|
||||
isPinned: (pubkey: string) => boolean
|
||||
pinUser: (pubkey: string) => Promise<void>
|
||||
unpinUser: (pubkey: string) => Promise<void>
|
||||
togglePin: (pubkey: string) => Promise<void>
|
||||
}
|
||||
|
||||
const PinnedUsersContext = createContext<TPinnedUsersContext | undefined>(undefined)
|
||||
|
||||
export const usePinnedUsers = () => {
|
||||
const context = useContext(PinnedUsersContext)
|
||||
if (!context) {
|
||||
throw new Error('usePinnedUsers must be used within a PinnedUsersProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
function createPinnedUsersListDraftEvent(tags: string[][], content = '') {
|
||||
return {
|
||||
kind: ExtendedKind.PINNED_USERS,
|
||||
content,
|
||||
tags,
|
||||
created_at: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
}
|
||||
|
||||
export function PinnedUsersProvider({ children }: { children: React.ReactNode }) {
|
||||
const {
|
||||
pubkey: accountPubkey,
|
||||
pinnedUsersEvent,
|
||||
updatePinnedUsersEvent,
|
||||
publish,
|
||||
nip04Decrypt,
|
||||
nip04Encrypt
|
||||
} = useNostr()
|
||||
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
||||
const pinnedPubkeySet = useMemo(() => {
|
||||
if (!pinnedUsersEvent) return new Set<string>()
|
||||
return new Set(getPubkeysFromPTags(pinnedUsersEvent.tags.concat(privateTags)))
|
||||
}, [pinnedUsersEvent, privateTags])
|
||||
|
||||
useEffect(() => {
|
||||
const updatePrivateTags = async () => {
|
||||
if (!pinnedUsersEvent) {
|
||||
setPrivateTags([])
|
||||
return
|
||||
}
|
||||
|
||||
const privateTags = await getPrivateTags(pinnedUsersEvent).catch(() => {
|
||||
return []
|
||||
})
|
||||
setPrivateTags(privateTags)
|
||||
}
|
||||
updatePrivateTags()
|
||||
}, [pinnedUsersEvent])
|
||||
|
||||
const getPrivateTags = useCallback(
|
||||
async (event: Event) => {
|
||||
if (!event.content) return []
|
||||
|
||||
try {
|
||||
const storedPlainText = await indexedDb.getDecryptedContent(event.id)
|
||||
|
||||
let plainText: string
|
||||
if (storedPlainText) {
|
||||
plainText = storedPlainText
|
||||
} else {
|
||||
plainText = await nip04Decrypt(event.pubkey, event.content)
|
||||
await indexedDb.putDecryptedContent(event.id, plainText)
|
||||
}
|
||||
|
||||
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||
return privateTags
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt pinned users content', error)
|
||||
return []
|
||||
}
|
||||
},
|
||||
[nip04Decrypt]
|
||||
)
|
||||
|
||||
const isPinned = useCallback(
|
||||
(pubkey: string) => {
|
||||
return pinnedPubkeySet.has(pubkey)
|
||||
},
|
||||
[pinnedPubkeySet]
|
||||
)
|
||||
|
||||
const pinUser = useCallback(
|
||||
async (pubkey: string) => {
|
||||
if (!accountPubkey || isPinned(pubkey)) return
|
||||
|
||||
try {
|
||||
const newTags = [...(pinnedUsersEvent?.tags ?? []), ['p', pubkey]]
|
||||
const draftEvent = createPinnedUsersListDraftEvent(newTags, pinnedUsersEvent?.content ?? '')
|
||||
const newEvent = await publish(draftEvent)
|
||||
await updatePinnedUsersEvent(newEvent, privateTags)
|
||||
} catch (error) {
|
||||
console.error('Failed to pin user:', error)
|
||||
}
|
||||
},
|
||||
[accountPubkey, isPinned, pinnedUsersEvent, publish, updatePinnedUsersEvent, privateTags]
|
||||
)
|
||||
|
||||
const unpinUser = useCallback(
|
||||
async (pubkey: string) => {
|
||||
if (!accountPubkey || !pinnedUsersEvent || !isPinned(pubkey)) return
|
||||
|
||||
try {
|
||||
const newTags = pinnedUsersEvent.tags.filter(
|
||||
([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey
|
||||
)
|
||||
const newPrivateTags = privateTags.filter(
|
||||
([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey
|
||||
)
|
||||
let newContent = pinnedUsersEvent.content
|
||||
if (newPrivateTags.length !== privateTags.length) {
|
||||
newContent = await nip04Encrypt(pinnedUsersEvent.pubkey, JSON.stringify(newPrivateTags))
|
||||
}
|
||||
const draftEvent = createPinnedUsersListDraftEvent(newTags, newContent)
|
||||
const newEvent = await publish(draftEvent)
|
||||
await updatePinnedUsersEvent(newEvent, newPrivateTags)
|
||||
} catch (error) {
|
||||
console.error('Failed to unpin user:', error)
|
||||
}
|
||||
},
|
||||
[
|
||||
accountPubkey,
|
||||
isPinned,
|
||||
pinnedUsersEvent,
|
||||
publish,
|
||||
updatePinnedUsersEvent,
|
||||
privateTags,
|
||||
nip04Encrypt
|
||||
]
|
||||
)
|
||||
|
||||
const togglePin = useCallback(
|
||||
async (pubkey: string) => {
|
||||
if (isPinned(pubkey)) {
|
||||
await unpinUser(pubkey)
|
||||
} else {
|
||||
await pinUser(pubkey)
|
||||
}
|
||||
},
|
||||
[isPinned, pinUser, unpinUser]
|
||||
)
|
||||
|
||||
return (
|
||||
<PinnedUsersContext.Provider
|
||||
value={{
|
||||
pinnedPubkeySet,
|
||||
isPinned,
|
||||
pinUser,
|
||||
unpinUser,
|
||||
togglePin
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PinnedUsersContext.Provider>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue