refactor: use domain objects for FollowList and MuteList providers
- Refactor FollowListProvider to use domain FollowList class - Refactor MuteListProvider to use domain MuteList class - Add hide untrusted interactions/notifications settings to GeneralSettingsPage - Maintain backward compatibility with existing Set<string> interface 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -30,7 +30,14 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
mediaAutoLoadPolicy,
|
mediaAutoLoadPolicy,
|
||||||
setMediaAutoLoadPolicy
|
setMediaAutoLoadPolicy
|
||||||
} = useContentPolicy()
|
} = useContentPolicy()
|
||||||
const { hideUntrustedNotes, updateHideUntrustedNotes } = useUserTrust()
|
const {
|
||||||
|
hideUntrustedNotes,
|
||||||
|
updateHideUntrustedNotes,
|
||||||
|
hideUntrustedInteractions,
|
||||||
|
updateHideUntrustedInteractions,
|
||||||
|
hideUntrustedNotifications,
|
||||||
|
updateHideUntrustedNotifications
|
||||||
|
} = useUserTrust()
|
||||||
const { quickReaction, updateQuickReaction, quickReactionEmoji, updateQuickReactionEmoji } =
|
const { quickReaction, updateQuickReaction, quickReactionEmoji, updateQuickReactionEmoji } =
|
||||||
useUserPreferences()
|
useUserPreferences()
|
||||||
|
|
||||||
@@ -99,6 +106,26 @@ const GeneralSettingsPage = forwardRef(({ index }: { index?: number }, ref) => {
|
|||||||
onCheckedChange={updateHideUntrustedNotes}
|
onCheckedChange={updateHideUntrustedNotes}
|
||||||
/>
|
/>
|
||||||
</SettingItem>
|
</SettingItem>
|
||||||
|
<SettingItem>
|
||||||
|
<Label htmlFor="hide-untrusted-interactions" className="text-base font-normal">
|
||||||
|
{t('Hide untrusted interactions')}
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id="hide-untrusted-interactions"
|
||||||
|
checked={hideUntrustedInteractions}
|
||||||
|
onCheckedChange={updateHideUntrustedInteractions}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
|
<SettingItem>
|
||||||
|
<Label htmlFor="hide-untrusted-notifications" className="text-base font-normal">
|
||||||
|
{t('Hide untrusted notifications')}
|
||||||
|
</Label>
|
||||||
|
<Switch
|
||||||
|
id="hide-untrusted-notifications"
|
||||||
|
checked={hideUntrustedNotifications}
|
||||||
|
onCheckedChange={updateHideUntrustedNotifications}
|
||||||
|
/>
|
||||||
|
</SettingItem>
|
||||||
<SettingItem>
|
<SettingItem>
|
||||||
<Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
|
<Label htmlFor="hide-content-mentioning-muted-users" className="text-base font-normal">
|
||||||
{t('Hide content mentioning muted users')}
|
{t('Hide content mentioning muted users')}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import { createFollowListDraftEvent } from '@/lib/draft-event'
|
import {
|
||||||
import { getPubkeysFromPTags } from '@/lib/tag'
|
FollowList,
|
||||||
|
tryToFollowList,
|
||||||
|
fromFollowListToHexSet,
|
||||||
|
Pubkey,
|
||||||
|
CannotFollowSelfError
|
||||||
|
} from '@/domain'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import { createContext, useContext, useMemo } from 'react'
|
import { createContext, useContext, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -7,6 +12,7 @@ import { useNostr } from './NostrProvider'
|
|||||||
|
|
||||||
type TFollowListContext = {
|
type TFollowListContext = {
|
||||||
followingSet: Set<string>
|
followingSet: Set<string>
|
||||||
|
followList: FollowList | null
|
||||||
follow: (pubkey: string) => Promise<void>
|
follow: (pubkey: string) => Promise<void>
|
||||||
unfollow: (pubkey: string) => Promise<void>
|
unfollow: (pubkey: string) => Promise<void>
|
||||||
}
|
}
|
||||||
@@ -24,41 +30,73 @@ export const useFollowList = () => {
|
|||||||
export function FollowListProvider({ children }: { children: React.ReactNode }) {
|
export function FollowListProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { pubkey: accountPubkey, followListEvent, publish, updateFollowListEvent } = useNostr()
|
const { pubkey: accountPubkey, followListEvent, publish, updateFollowListEvent } = useNostr()
|
||||||
const followingSet = useMemo(
|
|
||||||
() => new Set(followListEvent ? getPubkeysFromPTags(followListEvent.tags) : []),
|
// Create domain FollowList from event
|
||||||
|
const followList = useMemo(
|
||||||
|
() => tryToFollowList(followListEvent),
|
||||||
[followListEvent]
|
[followListEvent]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Legacy compatibility: expose as Set<string> for existing consumers
|
||||||
|
const followingSet = useMemo(
|
||||||
|
() => (followList ? fromFollowListToHexSet(followList) : new Set<string>()),
|
||||||
|
[followList]
|
||||||
|
)
|
||||||
|
|
||||||
const follow = async (pubkey: string) => {
|
const follow = async (pubkey: string) => {
|
||||||
if (!accountPubkey) return
|
if (!accountPubkey) return
|
||||||
|
|
||||||
const followListEvent = await client.fetchFollowListEvent(accountPubkey)
|
// Fetch latest follow list event
|
||||||
if (!followListEvent) {
|
const latestEvent = await client.fetchFollowListEvent(accountPubkey)
|
||||||
|
if (!latestEvent) {
|
||||||
const result = confirm(t('FollowListNotFoundConfirmation'))
|
const result = confirm(t('FollowListNotFoundConfirmation'))
|
||||||
|
if (!result) return
|
||||||
|
}
|
||||||
|
|
||||||
if (!result) {
|
// Create or update FollowList using domain object
|
||||||
|
const ownerPubkey = Pubkey.fromHex(accountPubkey)
|
||||||
|
const currentFollowList = latestEvent
|
||||||
|
? FollowList.fromEvent(latestEvent)
|
||||||
|
: FollowList.empty(ownerPubkey)
|
||||||
|
|
||||||
|
// Use domain logic for following
|
||||||
|
const targetPubkey = Pubkey.tryFromString(pubkey)
|
||||||
|
if (!targetPubkey) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const change = currentFollowList.follow(targetPubkey)
|
||||||
|
if (change.type === 'no_change') return
|
||||||
|
|
||||||
|
// Publish the updated follow list
|
||||||
|
const draftEvent = currentFollowList.toDraftEvent()
|
||||||
|
const newFollowListEvent = await publish(draftEvent)
|
||||||
|
await updateFollowListEvent(newFollowListEvent)
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof CannotFollowSelfError) {
|
||||||
|
// Silently ignore self-follow attempts
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
const newFollowListDraftEvent = createFollowListDraftEvent(
|
|
||||||
(followListEvent?.tags ?? []).concat([['p', pubkey]]),
|
|
||||||
followListEvent?.content
|
|
||||||
)
|
|
||||||
const newFollowListEvent = await publish(newFollowListDraftEvent)
|
|
||||||
await updateFollowListEvent(newFollowListEvent)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unfollow = async (pubkey: string) => {
|
const unfollow = async (pubkey: string) => {
|
||||||
if (!accountPubkey) return
|
if (!accountPubkey) return
|
||||||
|
|
||||||
const followListEvent = await client.fetchFollowListEvent(accountPubkey)
|
const latestEvent = await client.fetchFollowListEvent(accountPubkey)
|
||||||
if (!followListEvent) return
|
if (!latestEvent) return
|
||||||
|
|
||||||
const newFollowListDraftEvent = createFollowListDraftEvent(
|
// Use domain object for unfollowing
|
||||||
followListEvent.tags.filter(([tagName, tagValue]) => tagName !== 'p' || tagValue !== pubkey),
|
const currentFollowList = FollowList.fromEvent(latestEvent)
|
||||||
followListEvent.content
|
const targetPubkey = Pubkey.tryFromString(pubkey)
|
||||||
)
|
if (!targetPubkey) return
|
||||||
const newFollowListEvent = await publish(newFollowListDraftEvent)
|
|
||||||
|
const change = currentFollowList.unfollow(targetPubkey)
|
||||||
|
if (change.type === 'no_change') return
|
||||||
|
|
||||||
|
// Publish the updated follow list
|
||||||
|
const draftEvent = currentFollowList.toDraftEvent()
|
||||||
|
const newFollowListEvent = await publish(draftEvent)
|
||||||
await updateFollowListEvent(newFollowListEvent)
|
await updateFollowListEvent(newFollowListEvent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +104,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
|
|||||||
<FollowListContext.Provider
|
<FollowListContext.Provider
|
||||||
value={{
|
value={{
|
||||||
followingSet,
|
followingSet,
|
||||||
|
followList,
|
||||||
follow,
|
follow,
|
||||||
unfollow
|
unfollow
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { createMuteListDraftEvent } from '@/lib/draft-event'
|
import {
|
||||||
import { getPubkeysFromPTags } from '@/lib/tag'
|
MuteList,
|
||||||
|
tryToMuteList,
|
||||||
|
fromMuteListToHexSet,
|
||||||
|
Pubkey,
|
||||||
|
CannotMuteSelfError,
|
||||||
|
MuteVisibility
|
||||||
|
} from '@/domain'
|
||||||
import client from '@/services/client.service'
|
import client from '@/services/client.service'
|
||||||
import indexedDb from '@/services/indexed-db.service'
|
import indexedDb from '@/services/indexed-db.service'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
@@ -12,9 +18,10 @@ import { useNostr } from './NostrProvider'
|
|||||||
|
|
||||||
type TMuteListContext = {
|
type TMuteListContext = {
|
||||||
mutePubkeySet: Set<string>
|
mutePubkeySet: Set<string>
|
||||||
|
muteList: MuteList | null
|
||||||
changing: boolean
|
changing: boolean
|
||||||
getMutePubkeys: () => string[]
|
getMutePubkeys: () => string[]
|
||||||
getMuteType: (pubkey: string) => 'public' | 'private' | null
|
getMuteType: (pubkey: string) => MuteVisibility | null
|
||||||
mutePubkeyPublicly: (pubkey: string) => Promise<void>
|
mutePubkeyPublicly: (pubkey: string) => Promise<void>
|
||||||
mutePubkeyPrivately: (pubkey: string) => Promise<void>
|
mutePubkeyPrivately: (pubkey: string) => Promise<void>
|
||||||
unmutePubkey: (pubkey: string) => Promise<void>
|
unmutePubkey: (pubkey: string) => Promise<void>
|
||||||
@@ -42,35 +49,27 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
|||||||
nip04Decrypt,
|
nip04Decrypt,
|
||||||
nip04Encrypt
|
nip04Encrypt
|
||||||
} = useNostr()
|
} = useNostr()
|
||||||
const [tags, setTags] = useState<string[][]>([])
|
|
||||||
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
const [privateTags, setPrivateTags] = useState<string[][]>([])
|
||||||
const publicMutePubkeySet = useMemo(() => new Set(getPubkeysFromPTags(tags)), [tags])
|
|
||||||
const privateMutePubkeySet = useMemo(
|
|
||||||
() => new Set(getPubkeysFromPTags(privateTags)),
|
|
||||||
[privateTags]
|
|
||||||
)
|
|
||||||
const mutePubkeySet = useMemo(() => {
|
|
||||||
return new Set([...Array.from(privateMutePubkeySet), ...Array.from(publicMutePubkeySet)])
|
|
||||||
}, [publicMutePubkeySet, privateMutePubkeySet])
|
|
||||||
const [changing, setChanging] = useState(false)
|
const [changing, setChanging] = useState(false)
|
||||||
|
|
||||||
|
// Decrypt private tags from mute list event
|
||||||
const getPrivateTags = useCallback(
|
const getPrivateTags = useCallback(
|
||||||
async (muteListEvent: Event) => {
|
async (event: Event) => {
|
||||||
if (!muteListEvent.content) return []
|
if (!event.content) return []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const storedPlainText = await indexedDb.getDecryptedContent(muteListEvent.id)
|
const storedPlainText = await indexedDb.getDecryptedContent(event.id)
|
||||||
|
|
||||||
let plainText: string
|
let plainText: string
|
||||||
if (storedPlainText) {
|
if (storedPlainText) {
|
||||||
plainText = storedPlainText
|
plainText = storedPlainText
|
||||||
} else {
|
} else {
|
||||||
plainText = await nip04Decrypt(muteListEvent.pubkey, muteListEvent.content)
|
plainText = await nip04Decrypt(event.pubkey, event.content)
|
||||||
await indexedDb.putDecryptedContent(muteListEvent.id, plainText)
|
await indexedDb.putDecryptedContent(event.id, plainText)
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
const tags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||||
return privateTags
|
return tags
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to decrypt mute list content', error)
|
console.error('Failed to decrypt mute list content', error)
|
||||||
return []
|
return []
|
||||||
@@ -79,73 +78,92 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
|||||||
[nip04Decrypt]
|
[nip04Decrypt]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Update private tags when mute list event changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const updateMuteTags = async () => {
|
const updatePrivateTags = async () => {
|
||||||
if (!muteListEvent) {
|
if (!muteListEvent) {
|
||||||
setTags([])
|
|
||||||
setPrivateTags([])
|
setPrivateTags([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateTags = await getPrivateTags(muteListEvent).catch(() => {
|
const tags = await getPrivateTags(muteListEvent).catch(() => [])
|
||||||
return []
|
setPrivateTags(tags)
|
||||||
})
|
|
||||||
setPrivateTags(privateTags)
|
|
||||||
setTags(muteListEvent.tags)
|
|
||||||
}
|
}
|
||||||
updateMuteTags()
|
updatePrivateTags()
|
||||||
}, [muteListEvent])
|
}, [muteListEvent, getPrivateTags])
|
||||||
|
|
||||||
const getMutePubkeys = () => {
|
// Create domain MuteList from event and decrypted private tags
|
||||||
return Array.from(mutePubkeySet)
|
const muteList = useMemo(
|
||||||
}
|
() => tryToMuteList(muteListEvent, privateTags),
|
||||||
|
[muteListEvent, privateTags]
|
||||||
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) => {
|
// Legacy compatibility: expose as Set<string> for existing consumers
|
||||||
|
const mutePubkeySet = useMemo(
|
||||||
|
() => (muteList ? fromMuteListToHexSet(muteList) : new Set<string>()),
|
||||||
|
[muteList]
|
||||||
|
)
|
||||||
|
|
||||||
|
const getMutePubkeys = useCallback(() => {
|
||||||
|
return Array.from(mutePubkeySet)
|
||||||
|
}, [mutePubkeySet])
|
||||||
|
|
||||||
|
const getMuteType = useCallback(
|
||||||
|
(pubkey: string): MuteVisibility | null => {
|
||||||
|
if (!muteList) return null
|
||||||
|
const pk = Pubkey.tryFromString(pubkey)
|
||||||
|
return pk ? muteList.getMuteVisibility(pk) : null
|
||||||
|
},
|
||||||
|
[muteList]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Publish updated mute list with rate limiting
|
||||||
|
const publishMuteList = async (updatedMuteList: MuteList, encryptedContent: string) => {
|
||||||
if (dayjs().unix() === muteListEvent?.created_at) {
|
if (dayjs().unix() === muteListEvent?.created_at) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1000))
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
||||||
}
|
}
|
||||||
const newMuteListDraftEvent = createMuteListDraftEvent(tags, content)
|
|
||||||
const event = await publish(newMuteListDraftEvent)
|
const draftEvent = updatedMuteList.toDraftEvent(encryptedContent)
|
||||||
|
const event = await publish(draftEvent)
|
||||||
toast.success(t('Successfully updated mute list'))
|
toast.success(t('Successfully updated mute list'))
|
||||||
return event
|
return event
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkMuteListEvent = (muteListEvent: Event | null) => {
|
|
||||||
if (!muteListEvent) {
|
|
||||||
const result = confirm(t('MuteListNotFoundConfirmation'))
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
throw new Error('Mute list not found')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const mutePubkeyPublicly = async (pubkey: string) => {
|
const mutePubkeyPublicly = async (pubkey: string) => {
|
||||||
if (!accountPubkey || changing) return
|
if (!accountPubkey || changing) return
|
||||||
|
|
||||||
setChanging(true)
|
setChanging(true)
|
||||||
try {
|
try {
|
||||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
const latestEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||||
checkMuteListEvent(muteListEvent)
|
if (!latestEvent) {
|
||||||
if (
|
const result = confirm(t('MuteListNotFoundConfirmation'))
|
||||||
muteListEvent &&
|
if (!result) return
|
||||||
muteListEvent.tags.some(([tagName, tagValue]) => tagName === 'p' && tagValue === pubkey)
|
}
|
||||||
) {
|
|
||||||
return
|
const ownerPubkey = Pubkey.fromHex(accountPubkey)
|
||||||
|
const decryptedPrivateTags = latestEvent ? await getPrivateTags(latestEvent) : []
|
||||||
|
const currentMuteList = latestEvent
|
||||||
|
? MuteList.fromEvent(latestEvent, decryptedPrivateTags)
|
||||||
|
: MuteList.empty(ownerPubkey)
|
||||||
|
|
||||||
|
const targetPubkey = Pubkey.tryFromString(pubkey)
|
||||||
|
if (!targetPubkey) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const change = currentMuteList.mutePublicly(targetPubkey)
|
||||||
|
if (change.type === 'no_change') return
|
||||||
|
|
||||||
|
// Encrypt private tags if there are any
|
||||||
|
const encryptedContent = currentMuteList.hasPrivateMutes()
|
||||||
|
? await nip04Encrypt(accountPubkey, JSON.stringify(currentMuteList.toPrivateTags()))
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const newEvent = await publishMuteList(currentMuteList, encryptedContent)
|
||||||
|
await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof CannotMuteSelfError) return
|
||||||
|
throw error
|
||||||
}
|
}
|
||||||
const newTags = (muteListEvent?.tags ?? []).concat([['p', pubkey]])
|
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(newTags, muteListEvent?.content)
|
|
||||||
const privateTags = await getPrivateTags(newMuteListEvent)
|
|
||||||
await updateMuteListEvent(newMuteListEvent, privateTags)
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t('Failed to mute user publicly') + ': ' + (error as Error).message)
|
toast.error(t('Failed to mute user publicly') + ': ' + (error as Error).message)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -158,17 +176,37 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
setChanging(true)
|
setChanging(true)
|
||||||
try {
|
try {
|
||||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
const latestEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||||
checkMuteListEvent(muteListEvent)
|
if (!latestEvent) {
|
||||||
const privateTags = muteListEvent ? await getPrivateTags(muteListEvent) : []
|
const result = confirm(t('MuteListNotFoundConfirmation'))
|
||||||
if (privateTags.some(([tagName, tagValue]) => tagName === 'p' && tagValue === pubkey)) {
|
if (!result) return
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPrivateTags = privateTags.concat([['p', pubkey]])
|
const ownerPubkey = Pubkey.fromHex(accountPubkey)
|
||||||
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
const decryptedPrivateTags = latestEvent ? await getPrivateTags(latestEvent) : []
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(muteListEvent?.tags ?? [], cipherText)
|
const currentMuteList = latestEvent
|
||||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
? MuteList.fromEvent(latestEvent, decryptedPrivateTags)
|
||||||
|
: MuteList.empty(ownerPubkey)
|
||||||
|
|
||||||
|
const targetPubkey = Pubkey.tryFromString(pubkey)
|
||||||
|
if (!targetPubkey) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const change = currentMuteList.mutePrivately(targetPubkey)
|
||||||
|
if (change.type === 'no_change') return
|
||||||
|
|
||||||
|
// Always encrypt when adding private mutes
|
||||||
|
const encryptedContent = await nip04Encrypt(
|
||||||
|
accountPubkey,
|
||||||
|
JSON.stringify(currentMuteList.toPrivateTags())
|
||||||
|
)
|
||||||
|
|
||||||
|
const newEvent = await publishMuteList(currentMuteList, encryptedContent)
|
||||||
|
await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof CannotMuteSelfError) return
|
||||||
|
throw error
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(t('Failed to mute user privately') + ': ' + (error as Error).message)
|
toast.error(t('Failed to mute user privately') + ': ' + (error as Error).message)
|
||||||
} finally {
|
} finally {
|
||||||
@@ -181,21 +219,25 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
setChanging(true)
|
setChanging(true)
|
||||||
try {
|
try {
|
||||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
const latestEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||||
if (!muteListEvent) return
|
if (!latestEvent) return
|
||||||
|
|
||||||
const privateTags = await getPrivateTags(muteListEvent)
|
const decryptedPrivateTags = await getPrivateTags(latestEvent)
|
||||||
const newPrivateTags = privateTags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
const currentMuteList = MuteList.fromEvent(latestEvent, decryptedPrivateTags)
|
||||||
let cipherText = muteListEvent.content
|
|
||||||
if (newPrivateTags.length !== privateTags.length) {
|
|
||||||
cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
|
||||||
}
|
|
||||||
|
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(
|
const targetPubkey = Pubkey.tryFromString(pubkey)
|
||||||
muteListEvent.tags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey),
|
if (!targetPubkey) return
|
||||||
cipherText
|
|
||||||
)
|
const change = currentMuteList.unmute(targetPubkey)
|
||||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
if (change.type === 'no_change') return
|
||||||
|
|
||||||
|
// Re-encrypt if there are still private mutes
|
||||||
|
const encryptedContent = currentMuteList.hasPrivateMutes()
|
||||||
|
? await nip04Encrypt(accountPubkey, JSON.stringify(currentMuteList.toPrivateTags()))
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const newEvent = await publishMuteList(currentMuteList, encryptedContent)
|
||||||
|
await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
|
||||||
} finally {
|
} finally {
|
||||||
setChanging(false)
|
setChanging(false)
|
||||||
}
|
}
|
||||||
@@ -206,23 +248,25 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
setChanging(true)
|
setChanging(true)
|
||||||
try {
|
try {
|
||||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
const latestEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||||
if (!muteListEvent) return
|
if (!latestEvent) return
|
||||||
|
|
||||||
const privateTags = await getPrivateTags(muteListEvent)
|
const decryptedPrivateTags = await getPrivateTags(latestEvent)
|
||||||
const newPrivateTags = privateTags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
const currentMuteList = MuteList.fromEvent(latestEvent, decryptedPrivateTags)
|
||||||
if (newPrivateTags.length === privateTags.length) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
const targetPubkey = Pubkey.tryFromString(pubkey)
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(
|
if (!targetPubkey) return
|
||||||
muteListEvent.tags
|
|
||||||
.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
const change = currentMuteList.switchToPublic(targetPubkey)
|
||||||
.concat([['p', pubkey]]),
|
if (change.type === 'no_change') return
|
||||||
cipherText
|
|
||||||
)
|
// Re-encrypt private tags
|
||||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
const encryptedContent = currentMuteList.hasPrivateMutes()
|
||||||
|
? await nip04Encrypt(accountPubkey, JSON.stringify(currentMuteList.toPrivateTags()))
|
||||||
|
: ''
|
||||||
|
|
||||||
|
const newEvent = await publishMuteList(currentMuteList, encryptedContent)
|
||||||
|
await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
|
||||||
} finally {
|
} finally {
|
||||||
setChanging(false)
|
setChanging(false)
|
||||||
}
|
}
|
||||||
@@ -233,21 +277,26 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
setChanging(true)
|
setChanging(true)
|
||||||
try {
|
try {
|
||||||
const muteListEvent = await client.fetchMuteListEvent(accountPubkey)
|
const latestEvent = await client.fetchMuteListEvent(accountPubkey)
|
||||||
if (!muteListEvent) return
|
if (!latestEvent) return
|
||||||
|
|
||||||
const newTags = muteListEvent.tags.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
const decryptedPrivateTags = await getPrivateTags(latestEvent)
|
||||||
if (newTags.length === muteListEvent.tags.length) {
|
const currentMuteList = MuteList.fromEvent(latestEvent, decryptedPrivateTags)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const privateTags = await getPrivateTags(muteListEvent)
|
const targetPubkey = Pubkey.tryFromString(pubkey)
|
||||||
const newPrivateTags = privateTags
|
if (!targetPubkey) return
|
||||||
.filter((tag) => tag[0] !== 'p' || tag[1] !== pubkey)
|
|
||||||
.concat([['p', pubkey]])
|
const change = currentMuteList.switchToPrivate(targetPubkey)
|
||||||
const cipherText = await nip04Encrypt(accountPubkey, JSON.stringify(newPrivateTags))
|
if (change.type === 'no_change') return
|
||||||
const newMuteListEvent = await publishNewMuteListEvent(newTags, cipherText)
|
|
||||||
await updateMuteListEvent(newMuteListEvent, newPrivateTags)
|
// Encrypt the updated private tags
|
||||||
|
const encryptedContent = await nip04Encrypt(
|
||||||
|
accountPubkey,
|
||||||
|
JSON.stringify(currentMuteList.toPrivateTags())
|
||||||
|
)
|
||||||
|
|
||||||
|
const newEvent = await publishMuteList(currentMuteList, encryptedContent)
|
||||||
|
await updateMuteListEvent(newEvent, currentMuteList.toPrivateTags())
|
||||||
} finally {
|
} finally {
|
||||||
setChanging(false)
|
setChanging(false)
|
||||||
}
|
}
|
||||||
@@ -257,6 +306,7 @@ export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
|||||||
<MuteListContext.Provider
|
<MuteListContext.Provider
|
||||||
value={{
|
value={{
|
||||||
mutePubkeySet,
|
mutePubkeySet,
|
||||||
|
muteList,
|
||||||
changing,
|
changing,
|
||||||
getMutePubkeys,
|
getMutePubkeys,
|
||||||
getMuteType,
|
getMuteType,
|
||||||
|
|||||||
Reference in New Issue
Block a user