feat: support choosing between public and private mute
This commit is contained in:
@@ -1,14 +1,23 @@
|
||||
import { createMuteListDraftEvent } from '@/lib/draft-event'
|
||||
import { extractPubkeysFromEventTags, isSameTag } from '@/lib/tag'
|
||||
import { extractPubkeysFromEventTags } from '@/lib/tag'
|
||||
import client from '@/services/client.service'
|
||||
import indexedDb from '@/services/indexed-db.service'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import { Event } from 'nostr-tools'
|
||||
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { z } from 'zod'
|
||||
import { useNostr } from './NostrProvider'
|
||||
|
||||
type TMuteListContext = {
|
||||
mutePubkeys: string[]
|
||||
mutePubkey: (pubkey: string) => Promise<void>
|
||||
changing: boolean
|
||||
getMutePubkeys: () => string[]
|
||||
getMuteType: (pubkey: string) => 'public' | 'private' | null
|
||||
mutePubkeyPublicly: (pubkey: string) => Promise<void>
|
||||
mutePubkeyPrivately: (pubkey: string) => Promise<void>
|
||||
unmutePubkey: (pubkey: string) => Promise<void>
|
||||
switchToPublicMute: (pubkey: string) => Promise<void>
|
||||
switchToPrivateMute: (pubkey: string) => Promise<void>
|
||||
}
|
||||
|
||||
const MuteListContext = createContext<TMuteListContext | undefined>(undefined)
|
||||
@@ -31,59 +40,209 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||
nip04Encrypt
|
||||
} = useNostr()
|
||||
const [tags, setTags] = useState<string[][]>([])
|
||||
const mutePubkeys = useMemo(() => extractPubkeysFromEventTags(tags), [tags])
|
||||
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
||||
const publicMutePubkeySet = useMemo(() => new Set(extractPubkeysFromEventTags(tags)), [tags])
|
||||
const privateMutePubkeySet = useMemo(
|
||||
() => new Set(extractPubkeysFromEventTags(privateTags)),
|
||||
[privateTags]
|
||||
)
|
||||
const mutePubkeys = useMemo(() => {
|
||||
return Array.from(
|
||||
new Set([...Array.from(privateMutePubkeySet), ...Array.from(publicMutePubkeySet)])
|
||||
)
|
||||
}, [publicMutePubkeySet, privateMutePubkeySet])
|
||||
const [changing, setChanging] = useState(false)
|
||||
|
||||
const getPrivateTags = 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 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 []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const updateMuteTags = async () => {
|
||||
if (!muteListEvent) return
|
||||
|
||||
const tags = [...muteListEvent.tags]
|
||||
if (muteListEvent.content) {
|
||||
const storedDecryptedTags = await indexedDb.getMuteDecryptedTags(muteListEvent.id)
|
||||
|
||||
if (storedDecryptedTags) {
|
||||
tags.push(...storedDecryptedTags)
|
||||
} else {
|
||||
try {
|
||||
const plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
||||
const contentTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||
await indexedDb.putMuteDecryptedTags(muteListEvent.id, contentTags)
|
||||
tags.push(...contentTags.filter((tag) => tags.every((t) => !isSameTag(t, tag))))
|
||||
} catch (error) {
|
||||
console.error('Failed to decrypt mute list content', error)
|
||||
}
|
||||
}
|
||||
if (!muteListEvent) {
|
||||
setTags([])
|
||||
setPrivateTags([])
|
||||
return
|
||||
}
|
||||
setTags(tags)
|
||||
|
||||
const privateTags = await getPrivateTags(muteListEvent).catch(() => {
|
||||
return []
|
||||
})
|
||||
setPrivateTags(privateTags)
|
||||
setTags(muteListEvent.tags)
|
||||
}
|
||||
updateMuteTags()
|
||||
}, [muteListEvent])
|
||||
|
||||
const mutePubkey = async (pubkey: string) => {
|
||||
if (!accountPubkey) return
|
||||
const getMutePubkeys = () => {
|
||||
return mutePubkeys
|
||||
}
|
||||
|
||||
const newTags = tags.concat([['p', pubkey]])
|
||||
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newTags))
|
||||
const newMuteListDraftEvent = createMuteListDraftEvent(muteListEvent?.tags ?? [], cipherText)
|
||||
const newMuteListEvent = await publish(newMuteListDraftEvent)
|
||||
await updateMuteListEvent(newMuteListEvent, newTags)
|
||||
const getMuteType = useCallback(
|
||||
(pubkey: string): 'public' | 'private' | null => {
|
||||
if (publicMutePubkeySet.has(pubkey)) return 'public'
|
||||
if (privateMutePubkeySet.has(pubkey)) return 'private'
|
||||
return null
|
||||
},
|
||||
[publicMutePubkeySet, privateMutePubkeySet]
|
||||
)
|
||||
|
||||
const publishNewMuteListEvent = async (tags: string[][], content?: string) => {
|
||||
if (dayjs().unix() === muteListEvent?.created_at) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||
}
|
||||
const newMuteListDraftEvent = createMuteListDraftEvent(tags, content)
|
||||
return await publish(newMuteListDraftEvent)
|
||||
}
|
||||
|
||||
const mutePubkeyPublicly = async (pubkey: string) => {
|
||||
if (!accountPubkey || changing) return
|
||||
|
||||
setChanging(true)
|
||||
try {
|
||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||
if (
|
||||
muteListEvent &&
|
||||
muteListEvent.tags.some(([tagName, tagValue]) => tagName === 'p' && tagValue === pubkey)
|
||||
) {
|
||||
return
|
||||
}
|
||||
const newTags = (muteListEvent?.tags ?? []).concat([['p', pubkey]])
|
||||
const newMuteListEvent = await publishNewMuteListEvent(newTags, muteListEvent?.content)
|
||||
const privateTags = await getPrivateTags(newMuteListEvent)
|
||||
await updateMuteListEvent(newMuteListEvent, privateTags)
|
||||
} finally {
|
||||
setChanging(false)
|
||||
}
|
||||
}
|
||||
|
||||
const mutePubkeyPrivately = async (pubkey: string) => {
|
||||
if (!accountPubkey || changing) return
|
||||
|
||||
setChanging(true)
|
||||
try {
|
||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||
const privateTags = muteListEvent ? await getPrivateTags(muteListEvent) : []
|
||||
if (privateTags.some(([tagName, tagValue]) => tagName === 'p' && tagValue === pubkey)) {
|
||||
return
|
||||
}
|
||||
|
||||
const newPrivateTags = privateTags.concat([['p', pubkey]])
|
||||
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
||||
const newMuteListEvent = await publishNewMuteListEvent(muteListEvent?.tags ?? [], cipherText)
|
||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
||||
} finally {
|
||||
setChanging(false)
|
||||
}
|
||||
}
|
||||
|
||||
const unmutePubkey = async (pubkey: string) => {
|
||||
if (!accountPubkey || !muteListEvent) return
|
||||
if (!accountPubkey || changing) return
|
||||
|
||||
const newTags = tags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newTags))
|
||||
const newMuteListDraftEvent = createMuteListDraftEvent(
|
||||
muteListEvent.tags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey),
|
||||
cipherText
|
||||
)
|
||||
const newMuteListEvent = await publish(newMuteListDraftEvent)
|
||||
await updateMuteListEvent(newMuteListEvent, newTags)
|
||||
setChanging(true)
|
||||
try {
|
||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||
if (!muteListEvent) return
|
||||
|
||||
const privateTags = await getPrivateTags(muteListEvent)
|
||||
const newPrivateTags = privateTags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||
let cipherText = muteListEvent.content
|
||||
if (newPrivateTags.length !== privateTags.length) {
|
||||
cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
||||
}
|
||||
|
||||
const newMuteListEvent = await publishNewMuteListEvent(
|
||||
muteListEvent.tags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey),
|
||||
cipherText
|
||||
)
|
||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
||||
} finally {
|
||||
setChanging(false)
|
||||
}
|
||||
}
|
||||
|
||||
const switchToPublicMute = async (pubkey: string) => {
|
||||
if (!accountPubkey || changing) return
|
||||
|
||||
setChanging(true)
|
||||
try {
|
||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||
if (!muteListEvent) return
|
||||
|
||||
const privateTags = await getPrivateTags(muteListEvent)
|
||||
const newPrivateTags = privateTags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||
if (newPrivateTags.length === privateTags.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
||||
const newMuteListEvent = await publishNewMuteListEvent(
|
||||
muteListEvent.tags
|
||||
.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||
.concat([['p', pubkey]]),
|
||||
cipherText
|
||||
)
|
||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
||||
} finally {
|
||||
setChanging(false)
|
||||
}
|
||||
}
|
||||
|
||||
const switchToPrivateMute = async (pubkey: string) => {
|
||||
if (!accountPubkey || changing) return
|
||||
|
||||
setChanging(true)
|
||||
try {
|
||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||
if (!muteListEvent) return
|
||||
|
||||
const newTags = muteListEvent.tags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||
if (newTags.length === muteListEvent.tags.length) {
|
||||
return
|
||||
}
|
||||
|
||||
const privateTags = await getPrivateTags(muteListEvent)
|
||||
const newPrivateTags = privateTags
|
||||
.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
||||
.concat([['p', pubkey]])
|
||||
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
||||
const newMuteListEvent = await publishNewMuteListEvent(newTags, cipherText)
|
||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
||||
} finally {
|
||||
setChanging(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<MuteListContext.Provider value={{ mutePubkeys, mutePubkey, unmutePubkey }}>
|
||||
<MuteListContext.Provider
|
||||
value={{
|
||||
mutePubkeys,
|
||||
changing,
|
||||
getMutePubkeys,
|
||||
getMuteType,
|
||||
mutePubkeyPublicly,
|
||||
mutePubkeyPrivately,
|
||||
unmutePubkey,
|
||||
switchToPublicMute,
|
||||
switchToPrivateMute
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</MuteListContext.Provider>
|
||||
)
|
||||
|
||||
@@ -62,7 +62,7 @@ type TNostrContext = {
|
||||
updateRelayListEvent: (relayListEvent: Event) => Promise<void>
|
||||
updateProfileEvent: (profileEvent: Event) => Promise<void>
|
||||
updateFollowListEvent: (followListEvent: Event) => Promise<void>
|
||||
updateMuteListEvent: (muteListEvent: Event, tags: string[][]) => Promise<void>
|
||||
updateMuteListEvent: (muteListEvent: Event, privateTags: string[][]) => Promise<void>
|
||||
updateBookmarkListEvent: (bookmarkListEvent: Event) => Promise<void>
|
||||
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
|
||||
updateNotificationsSeenAt: () => Promise<void>
|
||||
@@ -614,11 +614,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
client.updateFollowListCache(newFollowListEvent)
|
||||
}
|
||||
|
||||
const updateMuteListEvent = async (muteListEvent: Event, tags: string[][]) => {
|
||||
const updateMuteListEvent = async (muteListEvent: Event, privateTags: string[][]) => {
|
||||
const newMuteListEvent = await indexedDb.putReplaceableEvent(muteListEvent)
|
||||
if (newMuteListEvent.id !== muteListEvent.id) return
|
||||
|
||||
await indexedDb.putMuteDecryptedTags(muteListEvent.id, tags)
|
||||
await indexedDb.putMuteDecryptedTags(muteListEvent.id, privateTags)
|
||||
setMuteListEvent(muteListEvent)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user