|
|
|
|
@ -1,14 +1,23 @@
@@ -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 }) {
@@ -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 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 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 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> |
|
|
|
|
) |
|
|
|
|
|