feat: mute
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { createFollowListDraftEvent } from '@/lib/draft-event'
|
||||
import { getFollowingsFromFollowListEvent } from '@/lib/event'
|
||||
import { extractPubkeysFromEventTags } from '@/lib/tag'
|
||||
import client from '@/services/client.service'
|
||||
import storage from '@/services/storage.service'
|
||||
import { Event } from 'nostr-tools'
|
||||
@@ -30,7 +30,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
|
||||
const [followListEvent, setFollowListEvent] = useState<Event | undefined>(undefined)
|
||||
const [isFetching, setIsFetching] = useState(true)
|
||||
const followings = useMemo(
|
||||
() => (followListEvent ? getFollowingsFromFollowListEvent(followListEvent) : []),
|
||||
() => (followListEvent ? extractPubkeysFromEventTags(followListEvent.tags) : []),
|
||||
[followListEvent]
|
||||
)
|
||||
|
||||
@@ -87,7 +87,7 @@ export function FollowListProvider({ children }: { children: React.ReactNode })
|
||||
const getFollowings = async (pubkey: string) => {
|
||||
const followListEvent = storage.getAccountFollowListEvent(pubkey)
|
||||
if (followListEvent) {
|
||||
return getFollowingsFromFollowListEvent(followListEvent)
|
||||
return extractPubkeysFromEventTags(followListEvent.tags)
|
||||
}
|
||||
return await client.fetchFollowings(pubkey)
|
||||
}
|
||||
|
||||
111
src/providers/MuteListProvider.tsx
Normal file
111
src/providers/MuteListProvider.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { createMuteListDraftEvent } from '@/lib/draft-event'
|
||||
import { getLatestEvent } from '@/lib/event'
|
||||
import { extractPubkeysFromEventTags, isSameTag } from '@/lib/tag'
|
||||
import client from '@/services/client.service'
|
||||
import storage from '@/services/storage.service'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import { z } from 'zod'
|
||||
import { useNostr } from './NostrProvider'
|
||||
|
||||
type TMuteListContext = {
|
||||
mutePubkeys: string[]
|
||||
mutePubkey: (pubkey: string) => Promise<void>
|
||||
unmutePubkey: (pubkey: string) => Promise<void>
|
||||
}
|
||||
|
||||
const MuteListContext = createContext<TMuteListContext | undefined>(undefined)
|
||||
|
||||
export const useMuteList = () => {
|
||||
const context = useContext(MuteListContext)
|
||||
if (!context) {
|
||||
throw new Error('useMuteList must be used within a MuteListProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function MuteListProvider({ children }: { children: React.ReactNode }) {
|
||||
const { pubkey: accountPubkey, publish, relayList, nip04Decrypt, nip04Encrypt } = useNostr()
|
||||
const [muteListEvent, setMuteListEvent] = useState<Event | undefined>(undefined)
|
||||
const [tags, setTags] = useState<string[][]>([])
|
||||
const mutePubkeys = useMemo(() => extractPubkeysFromEventTags(tags), [tags])
|
||||
|
||||
useEffect(() => {
|
||||
if (!muteListEvent) {
|
||||
setTags([])
|
||||
return
|
||||
}
|
||||
|
||||
const updateTags = async () => {
|
||||
const tags = muteListEvent.tags
|
||||
if (muteListEvent.content && accountPubkey) {
|
||||
try {
|
||||
const plainText = await nip04Decrypt(accountPubkey, muteListEvent.content)
|
||||
const contentTags = z.array(z.array(z.string())).parse(JSON.parse(plainText))
|
||||
tags.push(...contentTags.filter((tag) => tags.every((t) => !isSameTag(t, tag))))
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
setTags(tags)
|
||||
}
|
||||
updateTags()
|
||||
}, [muteListEvent])
|
||||
|
||||
useEffect(() => {
|
||||
if (!accountPubkey) return
|
||||
|
||||
const init = async () => {
|
||||
setMuteListEvent(undefined)
|
||||
const storedMuteListEvent = storage.getAccountMuteListEvent(accountPubkey)
|
||||
if (storedMuteListEvent) {
|
||||
setMuteListEvent(storedMuteListEvent)
|
||||
}
|
||||
const events = await client.fetchEvents(relayList?.write ?? client.getDefaultRelayUrls(), {
|
||||
kinds: [kinds.Mutelist],
|
||||
authors: [accountPubkey]
|
||||
})
|
||||
const muteEvent = getLatestEvent(events) as Event | undefined
|
||||
if (muteEvent) {
|
||||
setMuteListEvent(muteEvent)
|
||||
}
|
||||
}
|
||||
|
||||
init()
|
||||
}, [accountPubkey])
|
||||
|
||||
const updateMuteListEvent = (event: Event) => {
|
||||
const isNew = storage.setAccountMuteListEvent(event)
|
||||
if (!isNew) return
|
||||
setMuteListEvent(event)
|
||||
}
|
||||
|
||||
const mutePubkey = async (pubkey: string) => {
|
||||
if (!accountPubkey) 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)
|
||||
updateMuteListEvent(newMuteListEvent)
|
||||
}
|
||||
|
||||
const unmutePubkey = async (pubkey: string) => {
|
||||
if (!accountPubkey || !muteListEvent) 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)
|
||||
updateMuteListEvent(newMuteListEvent)
|
||||
}
|
||||
|
||||
return (
|
||||
<MuteListContext.Provider value={{ mutePubkeys, mutePubkey, unmutePubkey }}>
|
||||
{children}
|
||||
</MuteListContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -48,6 +48,20 @@ export class BunkerSigner implements ISigner {
|
||||
})
|
||||
}
|
||||
|
||||
async nip04Encrypt(pubkey: string, plainText: string) {
|
||||
if (!this.signer) {
|
||||
throw new Error('Not logged in')
|
||||
}
|
||||
return await this.signer.nip04Encrypt(pubkey, plainText)
|
||||
}
|
||||
|
||||
async nip04Decrypt(pubkey: string, cipherText: string) {
|
||||
if (!this.signer) {
|
||||
throw new Error('Not logged in')
|
||||
}
|
||||
return await this.signer.nip04Decrypt(pubkey, cipherText)
|
||||
}
|
||||
|
||||
getClientSecretKey() {
|
||||
return bytesToHex(this.clientSecretKey)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,8 @@ type TNostrContext = {
|
||||
publish: (draftEvent: TDraftEvent, additionalRelayUrls?: string[]) => Promise<Event>
|
||||
signHttpAuth: (url: string, method: string) => Promise<string>
|
||||
signEvent: (draftEvent: TDraftEvent) => Promise<Event>
|
||||
nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
|
||||
nip04Decrypt: (pubkey: string, cipherText: string) => Promise<string>
|
||||
checkLogin: <T>(cb?: () => T) => Promise<T | void>
|
||||
getRelayList: (pubkey: string) => Promise<TRelayList>
|
||||
updateRelayListEvent: (relayListEvent: Event) => void
|
||||
@@ -299,6 +301,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
return 'Nostr ' + btoa(JSON.stringify(event))
|
||||
}
|
||||
|
||||
const nip04Encrypt = async (pubkey: string, plainText: string) => {
|
||||
return signer?.nip04Encrypt(pubkey, plainText) ?? ''
|
||||
}
|
||||
|
||||
const nip04Decrypt = async (pubkey: string, cipherText: string) => {
|
||||
return signer?.nip04Decrypt(pubkey, cipherText) ?? ''
|
||||
}
|
||||
|
||||
const checkLogin = async <T,>(cb?: () => T): Promise<T | void> => {
|
||||
if (signer) {
|
||||
return cb && cb()
|
||||
@@ -349,6 +359,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
removeAccount,
|
||||
publish,
|
||||
signHttpAuth,
|
||||
nip04Encrypt,
|
||||
nip04Decrypt,
|
||||
checkLogin,
|
||||
signEvent,
|
||||
getRelayList,
|
||||
|
||||
@@ -23,4 +23,24 @@ export class Nip07Signer implements ISigner {
|
||||
async signEvent(draftEvent: TDraftEvent) {
|
||||
return await this.signer.signEvent(draftEvent)
|
||||
}
|
||||
|
||||
async nip04Encrypt(pubkey: string, plainText: string) {
|
||||
if (!this.signer) {
|
||||
throw new Error('Not logged in')
|
||||
}
|
||||
if (!this.signer.nip04?.encrypt) {
|
||||
throw new Error('The extension you are using does not support nip04 encryption')
|
||||
}
|
||||
return await this.signer.nip04.encrypt(pubkey, plainText)
|
||||
}
|
||||
|
||||
async nip04Decrypt(pubkey: string, cipherText: string) {
|
||||
if (!this.signer) {
|
||||
throw new Error('Not logged in')
|
||||
}
|
||||
if (!this.signer.nip04?.decrypt) {
|
||||
throw new Error('The extension you are using does not support nip04 decryption')
|
||||
}
|
||||
return await this.signer.nip04.decrypt(pubkey, cipherText)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ISigner, TDraftEvent } from '@/types'
|
||||
import { finalizeEvent, getPublicKey as nGetPublicKey, nip19 } from 'nostr-tools'
|
||||
import { finalizeEvent, getPublicKey as nGetPublicKey, nip04, nip19 } from 'nostr-tools'
|
||||
|
||||
export class NsecSigner implements ISigner {
|
||||
private privkey: Uint8Array | null = null
|
||||
@@ -38,4 +38,18 @@ export class NsecSigner implements ISigner {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async nip04Encrypt(pubkey: string, plainText: string) {
|
||||
if (!this.privkey) {
|
||||
throw new Error('Not logged in')
|
||||
}
|
||||
return nip04.encrypt(this.privkey, pubkey, plainText)
|
||||
}
|
||||
|
||||
async nip04Decrypt(pubkey: string, cipherText: string) {
|
||||
if (!this.privkey) {
|
||||
throw new Error('Not logged in')
|
||||
}
|
||||
return nip04.decrypt(this.privkey, pubkey, cipherText)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user