feat: add try-delete post option

This commit is contained in:
codytseng
2025-08-30 14:21:35 +08:00
parent 905ef99e0e
commit 13527a3ca7
21 changed files with 241 additions and 97 deletions

View File

@@ -4,6 +4,7 @@ import './index.css'
import { Toaster } from '@/components/ui/sonner' import { Toaster } from '@/components/ui/sonner'
import { BookmarksProvider } from '@/providers/BookmarksProvider' import { BookmarksProvider } from '@/providers/BookmarksProvider'
import { ContentPolicyProvider } from '@/providers/ContentPolicyProvider' import { ContentPolicyProvider } from '@/providers/ContentPolicyProvider'
import { DeletedEventProvider } from '@/providers/DeletedEventProvider'
import { FavoriteRelaysProvider } from '@/providers/FavoriteRelaysProvider' import { FavoriteRelaysProvider } from '@/providers/FavoriteRelaysProvider'
import { FeedProvider } from '@/providers/FeedProvider' import { FeedProvider } from '@/providers/FeedProvider'
import { FollowListProvider } from '@/providers/FollowListProvider' import { FollowListProvider } from '@/providers/FollowListProvider'
@@ -24,6 +25,7 @@ export default function App(): JSX.Element {
<ThemeProvider> <ThemeProvider>
<ContentPolicyProvider> <ContentPolicyProvider>
<ScreenSizeProvider> <ScreenSizeProvider>
<DeletedEventProvider>
<NostrProvider> <NostrProvider>
<ZapProvider> <ZapProvider>
<TranslationServiceProvider> <TranslationServiceProvider>
@@ -50,6 +52,7 @@ export default function App(): JSX.Element {
</TranslationServiceProvider> </TranslationServiceProvider>
</ZapProvider> </ZapProvider>
</NostrProvider> </NostrProvider>
</DeletedEventProvider>
</ScreenSizeProvider> </ScreenSizeProvider>
</ContentPolicyProvider> </ContentPolicyProvider>
</ThemeProvider> </ThemeProvider>

View File

@@ -5,6 +5,7 @@ import {
isReplaceableEvent, isReplaceableEvent,
isReplyNoteEvent isReplyNoteEvent
} from '@/lib/event' } from '@/lib/event'
import { useDeletedEvent } from '@/providers/DeletedEventProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
@@ -44,6 +45,7 @@ const NoteList = forwardRef(
const { startLogin } = useNostr() const { startLogin } = useNostr()
const { isUserTrusted } = useUserTrust() const { isUserTrusted } = useUserTrust()
const { mutePubkeys } = useMuteList() const { mutePubkeys } = useMuteList()
const { isEventDeleted } = useDeletedEvent()
const [events, setEvents] = useState<Event[]>([]) const [events, setEvents] = useState<Event[]>([])
const [newEvents, setNewEvents] = useState<Event[]>([]) const [newEvents, setNewEvents] = useState<Event[]>([])
const [hasMore, setHasMore] = useState<boolean>(true) const [hasMore, setHasMore] = useState<boolean>(true)
@@ -58,6 +60,7 @@ const NoteList = forwardRef(
const idSet = new Set<string>() const idSet = new Set<string>()
return events.slice(0, showCount).filter((evt) => { return events.slice(0, showCount).filter((evt) => {
if (isEventDeleted(evt)) return false
if (hideReplies && isReplyNoteEvent(evt)) return false if (hideReplies && isReplyNoteEvent(evt)) return false
if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return false if (hideUntrustedNotes && !isUserTrusted(evt.pubkey)) return false
@@ -68,12 +71,13 @@ const NoteList = forwardRef(
idSet.add(id) idSet.add(id)
return true return true
}) })
}, [events, hideReplies, hideUntrustedNotes, showCount]) }, [events, hideReplies, hideUntrustedNotes, showCount, isEventDeleted])
const filteredNewEvents = useMemo(() => { const filteredNewEvents = useMemo(() => {
const idSet = new Set<string>() const idSet = new Set<string>()
return newEvents.filter((event: Event) => { return newEvents.filter((event: Event) => {
if (isEventDeleted(event)) return false
if (hideReplies && isReplyNoteEvent(event)) return false if (hideReplies && isReplyNoteEvent(event)) return false
if (hideUntrustedNotes && !isUserTrusted(event.pubkey)) return false if (hideUntrustedNotes && !isUserTrusted(event.pubkey)) return false
if (filterMutedNotes && mutePubkeys.includes(event.pubkey)) return false if (filterMutedNotes && mutePubkeys.includes(event.pubkey)) return false
@@ -87,7 +91,7 @@ const NoteList = forwardRef(
idSet.add(id) idSet.add(id)
return true return true
}) })
}, [newEvents, hideReplies, hideUntrustedNotes, filterMutedNotes, mutePubkeys]) }, [newEvents, hideReplies, hideUntrustedNotes, filterMutedNotes, mutePubkeys, isEventDeleted])
const scrollToTop = (behavior: ScrollBehavior = 'instant') => { const scrollToTop = (behavior: ScrollBehavior = 'instant') => {
setTimeout(() => { setTimeout(() => {

View File

@@ -6,7 +6,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Bell, BellOff, Code, Copy, Link, Mail, SatelliteDish, Server } from 'lucide-react' import { Bell, BellOff, Code, Copy, Link, Mail, SatelliteDish, Server, Trash2 } from 'lucide-react'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -45,7 +45,7 @@ export function useMenuActions({
isSmallScreen isSmallScreen
}: UseMenuActionsProps) { }: UseMenuActionsProps) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, relayList } = useNostr() const { pubkey, relayList, attemptDelete } = useNostr()
const { relaySets, favoriteRelays } = useFavoriteRelays() const { relaySets, favoriteRelays } = useFavoriteRelays()
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeys } = useMuteList() const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeys } = useMuteList()
const isMuted = useMemo(() => mutePubkeys.includes(event.pubkey), [mutePubkeys, event]) const isMuted = useMemo(() => mutePubkeys.includes(event.pubkey), [mutePubkeys, event])
@@ -235,6 +235,19 @@ export function useMenuActions({
} }
} }
if (pubkey && event.pubkey === pubkey) {
actions.push({
icon: Trash2,
label: t('Try deleting this note'),
onClick: () => {
closeDrawer()
attemptDelete(event)
},
className: 'text-destructive focus:text-destructive',
separator: true
})
}
return actions return actions
}, [ }, [
t, t,

View File

@@ -367,6 +367,8 @@ export default {
'Remember my choice': 'تذكر اختياري', 'Remember my choice': 'تذكر اختياري',
Apply: 'تطبيق', Apply: 'تطبيق',
Reset: 'إعادة تعيين', Reset: 'إعادة تعيين',
'Share something on this Relay': 'شارك شيئاً على هذا الريلاي' 'Share something on this Relay': 'شارك شيئاً على هذا الريلاي',
'Try deleting this note': 'حاول حذف هذه الملاحظة',
'Deletion request sent to {{count}} relays': 'تم إرسال طلب الحذف إلى {{count}} ريلايات'
} }
} }

View File

@@ -375,6 +375,8 @@ export default {
'Remember my choice': 'Meine Auswahl merken', 'Remember my choice': 'Meine Auswahl merken',
Apply: 'Anwenden', Apply: 'Anwenden',
Reset: 'Zurücksetzen', Reset: 'Zurücksetzen',
'Share something on this Relay': 'Teile etwas auf diesem Relay' 'Share something on this Relay': 'Teile etwas auf diesem Relay',
'Try deleting this note': 'Versuche, diese Notiz zu löschen',
'Deletion request sent to {{count}} relays': 'Löschanfrage an {{count}} Relays gesendet'
} }
} }

View File

@@ -366,6 +366,8 @@ export default {
'Remember my choice': 'Remember my choice', 'Remember my choice': 'Remember my choice',
Apply: 'Apply', Apply: 'Apply',
Reset: 'Reset', Reset: 'Reset',
'Share something on this Relay': 'Share something on this Relay' 'Share something on this Relay': 'Share something on this Relay',
'Try deleting this note': 'Try deleting this note',
'Deletion request sent to {{count}} relays': 'Deletion request sent to {{count}} relays'
} }
} }

View File

@@ -371,6 +371,9 @@ export default {
'Remember my choice': 'Recordar mi elección', 'Remember my choice': 'Recordar mi elección',
Apply: 'Aplicar', Apply: 'Aplicar',
Reset: 'Restablecer', Reset: 'Restablecer',
'Share something on this Relay': 'Comparte algo en este relé' 'Share something on this Relay': 'Comparte algo en este relé',
'Try deleting this note': 'Intenta eliminar esta nota',
'Deletion request sent to {{count}} relays':
'Solicitud de eliminación enviada a {{count}} relés'
} }
} }

View File

@@ -368,6 +368,8 @@ export default {
'Remember my choice': 'انتخاب من را به خاطر بسپار', 'Remember my choice': 'انتخاب من را به خاطر بسپار',
Apply: 'اعمال', Apply: 'اعمال',
Reset: 'بازنشانی', Reset: 'بازنشانی',
'Share something on this Relay': 'در این رله چیزی به اشتراک بگذارید' 'Share something on this Relay': 'در این رله چیزی به اشتراک بگذارید',
'Try deleting this note': 'سعی کنید این یادداشت را حذف کنید',
'Deletion request sent to {{count}} relays': 'درخواست حذف به {{count}} رله ارسال شد'
} }
} }

View File

@@ -373,6 +373,8 @@ export default {
'Remember my choice': 'Se souvenir de mon choix', 'Remember my choice': 'Se souvenir de mon choix',
Apply: 'Appliquer', Apply: 'Appliquer',
Reset: 'Réinitialiser', Reset: 'Réinitialiser',
'Share something on this Relay': 'Partager quelque chose sur ce relais' 'Share something on this Relay': 'Partager quelque chose sur ce relais',
'Try deleting this note': 'Essayez de supprimer cette note',
'Deletion request sent to {{count}} relays': 'Demande de suppression envoyée à {{count}} relais'
} }
} }

View File

@@ -371,6 +371,9 @@ export default {
'Remember my choice': 'Ricorda la mia scelta', 'Remember my choice': 'Ricorda la mia scelta',
Apply: 'Applica', Apply: 'Applica',
Reset: 'Reimposta', Reset: 'Reimposta',
'Share something on this Relay': 'Condividi qualcosa su questo Relay' 'Share something on this Relay': 'Condividi qualcosa su questo Relay',
'Try deleting this note': 'Prova a eliminare questa nota',
'Deletion request sent to {{count}} relays':
'Richiesta di eliminazione inviata a {{count}} relays'
} }
} }

View File

@@ -368,6 +368,9 @@ export default {
'Remember my choice': '選択を記憶', 'Remember my choice': '選択を記憶',
Apply: '適用', Apply: '適用',
Reset: 'リセット', Reset: 'リセット',
'Share something on this Relay': 'このリレーで何かを共有する' 'Share something on this Relay': 'このリレーで何かを共有する',
'Try deleting this note': 'このノートを削除してみてください',
'Deletion request sent to {{count}} relays':
'削除リクエストが{{count}}個のリレーに送信されました'
} }
} }

View File

@@ -368,6 +368,8 @@ export default {
'Remember my choice': '내 선택 기억하기', 'Remember my choice': '내 선택 기억하기',
Apply: '적용', Apply: '적용',
Reset: '초기화', Reset: '초기화',
'Share something on this Relay': '이 릴레이에서 무언가를 공유하세요' 'Share something on this Relay': '이 릴레이에서 무언가를 공유하세요',
'Try deleting this note': '이 노트를 삭제해 보세요',
'Deletion request sent to {{count}} relays': '삭제 요청이 {{count}}개의 릴레이로 전송되었습니다'
} }
} }

View File

@@ -372,6 +372,9 @@ export default {
'Remember my choice': 'Zapamiętaj mój wybór', 'Remember my choice': 'Zapamiętaj mój wybór',
Apply: 'Zastosuj', Apply: 'Zastosuj',
Reset: 'Resetuj', Reset: 'Resetuj',
'Share something on this Relay': 'Udostępnij coś na tym przekaźniku' 'Share something on this Relay': 'Udostępnij coś na tym przekaźniku',
'Try deleting this note': 'Spróbuj usunąć ten wpis',
'Deletion request sent to {{count}} relays':
'Żądanie usunięcia wysłane do {{count}} przekaźników'
} }
} }

View File

@@ -369,6 +369,8 @@ export default {
'Remember my choice': 'Lembrar minha escolha', 'Remember my choice': 'Lembrar minha escolha',
Apply: 'Aplicar', Apply: 'Aplicar',
Reset: 'Redefinir', Reset: 'Redefinir',
'Share something on this Relay': 'Compartilhe algo neste Relay' 'Share something on this Relay': 'Compartilhe algo neste Relay',
'Try deleting this note': 'Tente excluir esta nota',
'Deletion request sent to {{count}} relays': 'Pedido de exclusão enviado para {{count}} relays'
} }
} }

View File

@@ -371,6 +371,9 @@ export default {
'Remember my choice': 'Lembrar a minha escolha', 'Remember my choice': 'Lembrar a minha escolha',
Apply: 'Aplicar', Apply: 'Aplicar',
Reset: 'Repor', Reset: 'Repor',
'Share something on this Relay': 'Partilhe algo neste Relay' 'Share something on this Relay': 'Partilhe algo neste Relay',
'Try deleting this note': 'Tente eliminar esta nota',
'Deletion request sent to {{count}} relays':
'Pedido de eliminação enviado para {{count}} relays'
} }
} }

View File

@@ -372,6 +372,8 @@ export default {
'Remember my choice': 'Запомнить мой выбор', 'Remember my choice': 'Запомнить мой выбор',
Apply: 'Применить', Apply: 'Применить',
Reset: 'Сбросить', Reset: 'Сбросить',
'Share something on this Relay': 'Поделиться чем-то на этом релее' 'Share something on this Relay': 'Поделиться чем-то на этом релее',
'Try deleting this note': 'Попробуйте удалить эту заметку',
'Deletion request sent to {{count}} relays': 'Запрос на удаление отправлен на {{count}} релеев'
} }
} }

View File

@@ -365,6 +365,8 @@ export default {
'Remember my choice': 'จำการเลือกของฉัน', 'Remember my choice': 'จำการเลือกของฉัน',
Apply: 'ใช้', Apply: 'ใช้',
Reset: 'รีเซ็ต', Reset: 'รีเซ็ต',
'Share something on this Relay': 'แชร์บางอย่างบนรีเลย์นี้' 'Share something on this Relay': 'แชร์บางอย่างบนรีเลย์นี้',
'Try deleting this note': 'ลองลบโน้ตนี้ดู',
'Deletion request sent to {{count}} relays': 'คำขอลบถูกส่งไปยังรีเลย์ {{count}} รายการ'
} }
} }

View File

@@ -363,6 +363,8 @@ export default {
'Remember my choice': '记住我的选择', 'Remember my choice': '记住我的选择',
Apply: '应用', Apply: '应用',
Reset: '重置', Reset: '重置',
'Share something on this Relay': '在此服务器上分享点什么' 'Share something on this Relay': '在此服务器上分享点什么',
'Try deleting this note': '尝试删除此笔记',
'Deletion request sent to {{count}} relays': '删除请求已发送到 {{count}} 个服务器'
} }
} }

View File

@@ -416,6 +416,22 @@ export function createPollResponseDraftEvent(
} }
} }
export function createDeletionRequestDraftEvent(event: Event): TDraftEvent {
const tags: string[][] = [buildKTag(event.kind)]
if (isReplaceableEvent(event.kind)) {
tags.push(['a', getReplaceableCoordinateFromEvent(event)])
} else {
tags.push(['e', event.id])
}
return {
kind: kinds.EventDeletion,
content: 'Request for deletion of the event.',
tags,
created_at: dayjs().unix()
}
}
function generateImetaTags(imageUrls: string[]) { function generateImetaTags(imageUrls: string[]) {
return imageUrls return imageUrls
.map((imageUrl) => { .map((imageUrl) => {

View File

@@ -0,0 +1,43 @@
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { NostrEvent } from 'nostr-tools'
import { createContext, useCallback, useContext, useState } from 'react'
type TDeletedEventContext = {
addDeletedEvent: (event: NostrEvent) => void
isEventDeleted: (event: NostrEvent) => boolean
}
const DeletedEventContext = createContext<TDeletedEventContext | undefined>(undefined)
export const useDeletedEvent = () => {
const context = useContext(DeletedEventContext)
if (!context) {
throw new Error('useDeletedEvent must be used within a DeletedEventProvider')
}
return context
}
export function DeletedEventProvider({ children }: { children: React.ReactNode }) {
const [deletedEventKeys, setDeletedEventKeys] = useState<Set<string>>(new Set())
const isEventDeleted = useCallback(
(event: NostrEvent) => {
return deletedEventKeys.has(getKey(event))
},
[deletedEventKeys]
)
const addDeletedEvent = (event: NostrEvent) => {
setDeletedEventKeys((prev) => new Set(prev).add(getKey(event)))
}
return (
<DeletedEventContext.Provider value={{ addDeletedEvent, isEventDeleted }}>
{children}
</DeletedEventContext.Provider>
)
}
function getKey(event: NostrEvent) {
return isReplaceableEvent(event.kind) ? getReplaceableCoordinateFromEvent(event) : event.id
}

View File

@@ -1,12 +1,13 @@
import LoginDialog from '@/components/LoginDialog' import LoginDialog from '@/components/LoginDialog'
import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind } from '@/constants' import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind } from '@/constants'
import { import {
createDeletionRequestDraftEvent,
createFollowListDraftEvent, createFollowListDraftEvent,
createMuteListDraftEvent, createMuteListDraftEvent,
createRelayListDraftEvent, createRelayListDraftEvent,
createSeenNotificationsAtDraftEvent createSeenNotificationsAtDraftEvent
} from '@/lib/draft-event' } from '@/lib/draft-event'
import { getLatestEvent, getReplaceableEventIdentifier } from '@/lib/event' import { getLatestEvent, getReplaceableEventIdentifier, isProtectedEvent } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, isValidPubkey, pubkeyToNpub } from '@/lib/pubkey' import { formatPubkey, isValidPubkey, pubkeyToNpub } from '@/lib/pubkey'
import client from '@/services/client.service' import client from '@/services/client.service'
@@ -28,6 +29,7 @@ import { Nip07Signer } from './nip-07.signer'
import { NostrConnectionSigner } from './nostrConnection.signer' import { NostrConnectionSigner } from './nostrConnection.signer'
import { NpubSigner } from './npub.signer' import { NpubSigner } from './npub.signer'
import { NsecSigner } from './nsec.signer' import { NsecSigner } from './nsec.signer'
import { useDeletedEvent } from '../DeletedEventProvider'
type TPublishOptions = { type TPublishOptions = {
specifiedRelayUrls?: string[] specifiedRelayUrls?: string[]
@@ -62,6 +64,7 @@ type TNostrContext = {
* Default publish the event to current relays, user's write relays and additional relays * Default publish the event to current relays, user's write relays and additional relays
*/ */
publish: (draftEvent: TDraftEvent, options?: TPublishOptions) => Promise<Event> publish: (draftEvent: TDraftEvent, options?: TPublishOptions) => Promise<Event>
attemptDelete: (targetEvent: Event) => Promise<void>
signHttpAuth: (url: string, method: string) => Promise<string> signHttpAuth: (url: string, method: string) => Promise<string>
signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent> signEvent: (draftEvent: TDraftEvent) => Promise<VerifiedEvent>
nip04Encrypt: (pubkey: string, plainText: string) => Promise<string> nip04Encrypt: (pubkey: string, plainText: string) => Promise<string>
@@ -91,6 +94,7 @@ export const useNostr = () => {
export function NostrProvider({ children }: { children: React.ReactNode }) { export function NostrProvider({ children }: { children: React.ReactNode }) {
const { t } = useTranslation() const { t } = useTranslation()
const { addDeletedEvent } = useDeletedEvent()
const [accounts, setAccounts] = useState<TAccountPointer[]>( const [accounts, setAccounts] = useState<TAccountPointer[]>(
storage.getAccounts().map((act) => ({ pubkey: act.pubkey, signerType: act.signerType })) storage.getAccounts().map((act) => ({ pubkey: act.pubkey, signerType: act.signerType }))
) )
@@ -587,10 +591,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
return event as VerifiedEvent return event as VerifiedEvent
} }
const publish = async ( const publish = async (draftEvent: TDraftEvent, options: TPublishOptions = {}) => {
draftEvent: TDraftEvent,
{ specifiedRelayUrls, additionalRelayUrls }: TPublishOptions = {}
) => {
if (!account || !signer || account.signerType === 'npub') { if (!account || !signer || account.signerType === 'npub') {
throw new Error('You need to login first') throw new Error('You need to login first')
} }
@@ -610,58 +611,34 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
} }
const _additionalRelayUrls: string[] = additionalRelayUrls ?? [] const relays = await determineTargetRelays(event, options)
if (
!specifiedRelayUrls?.length &&
![kinds.Contacts, kinds.Mutelist].includes(draftEvent.kind)
) {
const mentions: string[] = []
draftEvent.tags.forEach(([tagName, tagValue]) => {
if (
['p', 'P'].includes(tagName) &&
!!tagValue &&
isValidPubkey(tagValue) &&
!mentions.includes(tagValue)
) {
mentions.push(tagValue)
}
})
if (mentions.length > 0) {
const relayLists = await client.fetchRelayLists(mentions)
relayLists.forEach((relayList) => {
_additionalRelayUrls.push(...relayList.read.slice(0, 4))
})
}
}
if (
[
kinds.RelayList,
kinds.Contacts,
ExtendedKind.FAVORITE_RELAYS,
ExtendedKind.BLOSSOM_SERVER_LIST
].includes(draftEvent.kind)
) {
_additionalRelayUrls.push(...BIG_RELAY_URLS)
}
let relays: string[]
if (specifiedRelayUrls?.length) {
relays = specifiedRelayUrls
} else {
const relayList = await client.fetchRelayList(event.pubkey)
relays = (relayList?.write.slice(0, 10) ?? []).concat(
Array.from(new Set(_additionalRelayUrls)) ?? []
)
}
if (!relays.length) {
relays.push(...BIG_RELAY_URLS)
}
await client.publishEvent(relays, event) await client.publishEvent(relays, event)
return event return event
} }
const attemptDelete = async (targetEvent: Event) => {
if (!signer) {
throw new Error(t('You need to login first'))
}
if (account?.pubkey !== targetEvent.pubkey) {
throw new Error(t('You can only delete your own notes'))
}
const deletionRequest = await signEvent(createDeletionRequestDraftEvent(targetEvent))
const seenOn = client.getSeenEventRelayUrls(targetEvent.id)
const relays = await determineTargetRelays(targetEvent, {
specifiedRelayUrls: isProtectedEvent(targetEvent) ? seenOn : undefined,
additionalRelayUrls: seenOn
})
await client.publishEvent(relays, deletionRequest)
addDeletedEvent(targetEvent)
toast.success(t('Deletion request sent to {{count}} relays', { count: relays.length }))
}
const signHttpAuth = async (url: string, method: string, content = '') => { const signHttpAuth = async (url: string, method: string, content = '') => {
const event = await signEvent({ const event = await signEvent({
content, content,
@@ -779,6 +756,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
npubLogin, npubLogin,
removeAccount, removeAccount,
publish, publish,
attemptDelete,
signHttpAuth, signHttpAuth,
nip04Encrypt, nip04Encrypt,
nip04Decrypt, nip04Decrypt,
@@ -799,3 +777,55 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
</NostrContext.Provider> </NostrContext.Provider>
) )
} }
async function determineTargetRelays(
event: Event,
{ specifiedRelayUrls, additionalRelayUrls }: TPublishOptions = {}
) {
const _additionalRelayUrls: string[] = additionalRelayUrls ?? []
if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) {
const mentions: string[] = []
event.tags.forEach(([tagName, tagValue]) => {
if (
['p', 'P'].includes(tagName) &&
!!tagValue &&
isValidPubkey(tagValue) &&
!mentions.includes(tagValue)
) {
mentions.push(tagValue)
}
})
if (mentions.length > 0) {
const relayLists = await client.fetchRelayLists(mentions)
relayLists.forEach((relayList) => {
_additionalRelayUrls.push(...relayList.read.slice(0, 4))
})
}
}
if (
[
kinds.RelayList,
kinds.Contacts,
ExtendedKind.FAVORITE_RELAYS,
ExtendedKind.BLOSSOM_SERVER_LIST
].includes(event.kind)
) {
_additionalRelayUrls.push(...BIG_RELAY_URLS)
}
let relays: string[]
if (specifiedRelayUrls?.length) {
relays = specifiedRelayUrls
} else {
const relayList = await client.fetchRelayList(event.pubkey)
relays = (relayList?.write.slice(0, 10) ?? []).concat(
Array.from(new Set(_additionalRelayUrls)) ?? []
)
}
if (!relays.length) {
relays.push(...BIG_RELAY_URLS)
}
return relays
}