You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

316 lines
11 KiB

import { BIG_RELAY_URLS, DEFAULT_FAVORITE_RELAYS } from '@/constants'
import { createFavoriteRelaysDraftEvent, createBlockedRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
import { getReplaceableEventIdentifier } from '@/lib/event'
import { getRelaySetFromEvent } from '@/lib/event-metadata'
import { randomString } from '@/lib/random'
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
import client from '@/services/client.service'
import indexedDb from '@/services/indexed-db.service'
import storage from '@/services/local-storage.service'
import { TRelaySet } from '@/types'
import { Event, kinds } from 'nostr-tools'
import { createContext, useContext, useEffect, useState } from 'react'
import { useNostr } from './NostrProvider'
type TFavoriteRelaysContext = {
favoriteRelays: string[]
addFavoriteRelays: (relayUrls: string[]) => Promise<void>
deleteFavoriteRelays: (relayUrls: string[]) => Promise<void>
reorderFavoriteRelays: (reorderedRelays: string[]) => Promise<void>
blockedRelays: string[]
addBlockedRelays: (relayUrls: string[]) => Promise<void>
deleteBlockedRelays: (relayUrls: string[]) => Promise<void>
relaySets: TRelaySet[]
createRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void>
addRelaySets: (newRelaySetEvents: Event[]) => Promise<void>
deleteRelaySet: (id: string) => Promise<void>
updateRelaySet: (newSet: TRelaySet) => Promise<void>
reorderRelaySets: (reorderedSets: TRelaySet[]) => Promise<void>
}
const FavoriteRelaysContext = createContext<TFavoriteRelaysContext | undefined>(undefined)
export const useFavoriteRelays = () => {
const context = useContext(FavoriteRelaysContext)
if (!context) {
throw new Error('useFavoriteRelays must be used within a FavoriteRelaysProvider')
}
return context
}
export function FavoriteRelaysProvider({ children }: { children: React.ReactNode }) {
const { favoriteRelaysEvent, blockedRelaysEvent, updateFavoriteRelaysEvent, updateBlockedRelaysEvent, pubkey, relayList, publish } = useNostr()
const [favoriteRelays, setFavoriteRelays] = useState<string[]>([])
const [blockedRelays, setBlockedRelays] = useState<string[]>([])
const [relaySetEvents, setRelaySetEvents] = useState<Event[]>([])
const [relaySets, setRelaySets] = useState<TRelaySet[]>([])
useEffect(() => {
if (!favoriteRelaysEvent) {
// For anonymous users (no login), only use relays from BIG_RELAY_URLS
// Don't load potentially untrusted relays from local storage
const favoriteRelays: string[] = pubkey ? DEFAULT_FAVORITE_RELAYS : BIG_RELAY_URLS.slice()
if (pubkey) {
// Only add stored relay sets if user is logged in
const storedRelaySets = storage.getRelaySets()
storedRelaySets.forEach(({ relayUrls }) => {
relayUrls.forEach((url) => {
if (!favoriteRelays.includes(url)) {
favoriteRelays.push(url)
}
})
})
}
setFavoriteRelays(favoriteRelays)
setRelaySetEvents([])
return
}
const init = async () => {
const relays: string[] = []
const relaySetIds: string[] = []
favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => {
if (!tagValue) return
if (tagName === 'relay') {
const normalizedUrl = normalizeUrl(tagValue)
if (normalizedUrl && !relays.includes(normalizedUrl)) {
relays.push(normalizedUrl)
}
} else if (tagName === 'a') {
const [kind, author, relaySetId] = tagValue.split(':')
if (kind !== kinds.Relaysets.toString()) return
if (!pubkey || author !== pubkey) return // TODO: support others relay sets
if (!relaySetId) return
if (!relaySetIds.includes(relaySetId)) {
relaySetIds.push(relaySetId)
}
}
})
// Keep all favorites in state - don't filter blocked relays here
// Blocked relays are filtered at the relay selection service level
setFavoriteRelays(relays)
if (!pubkey || !relaySetIds.length) {
setRelaySets([])
return
}
const storedRelaySetEvents = await Promise.all(
relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id))
)
setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[])
const normalizedRelays = [
...(relayList?.write ?? []).map(url => normalizeUrl(url) || url),
...BIG_RELAY_URLS.map(url => normalizeUrl(url) || url)
]
const newRelaySetEvents = await client.fetchEvents(
Array.from(new Set(normalizedRelays)).slice(0, 5),
{
kinds: [kinds.Relaysets],
authors: [pubkey],
'#d': relaySetIds
}
)
const relaySetEventMap = new Map<string, Event>()
newRelaySetEvents.forEach((event) => {
const d = getReplaceableEventIdentifier(event)
if (!d) return
const old = relaySetEventMap.get(d)
if (!old || old.created_at < event.created_at) {
relaySetEventMap.set(d, event)
}
})
const uniqueNewRelaySetEvents = relaySetIds
.map((id, index) => {
const event = relaySetEventMap.get(id)
if (event) {
return event
}
return storedRelaySetEvents[index] || null
})
.filter(Boolean) as Event[]
setRelaySetEvents(uniqueNewRelaySetEvents)
await Promise.all(
uniqueNewRelaySetEvents.map((event) => {
return indexedDb.putReplaceableEvent(event)
})
)
}
init()
}, [favoriteRelaysEvent, pubkey])
useEffect(() => {
if (!blockedRelaysEvent) {
setBlockedRelays([])
return
}
const relays: string[] = []
blockedRelaysEvent.tags.forEach(([tagName, tagValue]) => {
if (tagName === 'relay' && tagValue) {
const normalizedUrl = normalizeUrl(tagValue)
if (normalizedUrl && !relays.includes(normalizedUrl)) {
relays.push(normalizedUrl)
}
}
})
setBlockedRelays(relays)
}, [blockedRelaysEvent])
useEffect(() => {
setRelaySets(
relaySetEvents.map((evt) => getRelaySetFromEvent(evt, blockedRelays)).filter(Boolean) as TRelaySet[]
)
}, [relaySetEvents, blockedRelays])
const addFavoriteRelays = async (relayUrls: string[]) => {
const normalizedUrls = relayUrls
.map((relayUrl) => normalizeUrl(relayUrl))
.filter((url) => !!url && !favoriteRelays.includes(url))
if (!normalizedUrls.length) return
const draftEvent = createFavoriteRelaysDraftEvent(
[...favoriteRelays, ...normalizedUrls],
relaySetEvents
)
const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const deleteFavoriteRelays = async (relayUrls: string[]) => {
const normalizedUrls = relayUrls
.map((relayUrl) => normalizeUrl(relayUrl))
.filter((url) => !!url && favoriteRelays.includes(url))
if (!normalizedUrls.length) return
const draftEvent = createFavoriteRelaysDraftEvent(
favoriteRelays.filter((url) => !normalizedUrls.includes(url)),
relaySetEvents
)
const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const createRelaySet = async (relaySetName: string, relayUrls: string[] = []) => {
const normalizedUrls = relayUrls
.map((url) => normalizeUrl(url))
.filter((url) => isWebsocketUrl(url))
const id = randomString()
const relaySetDraftEvent = createRelaySetDraftEvent({
id,
name: relaySetName,
relayUrls: normalizedUrls
})
const newRelaySetEvent = await publish(relaySetDraftEvent)
await indexedDb.putReplaceableEvent(newRelaySetEvent)
const favoriteRelaysDraftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, [
...relaySetEvents,
newRelaySetEvent
])
const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const addRelaySets = async (newRelaySetEvents: Event[]) => {
const favoriteRelaysDraftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, [
...relaySetEvents,
...newRelaySetEvents
])
const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const deleteRelaySet = async (id: string) => {
const newRelaySetEvents = relaySetEvents.filter((event) => {
return getReplaceableEventIdentifier(event) !== id
})
if (newRelaySetEvents.length === relaySetEvents.length) return
const draftEvent = createFavoriteRelaysDraftEvent(favoriteRelays, newRelaySetEvents)
const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const updateRelaySet = async (newSet: TRelaySet) => {
const draftEvent = createRelaySetDraftEvent(newSet)
const newRelaySetEvent = await publish(draftEvent)
await indexedDb.putReplaceableEvent(newRelaySetEvent)
setRelaySetEvents((prev) => {
return prev.map((event) => {
if (getReplaceableEventIdentifier(event) === newSet.id) {
return newRelaySetEvent
}
return event
})
})
}
const reorderFavoriteRelays = async (reorderedRelays: string[]) => {
setFavoriteRelays(reorderedRelays)
const draftEvent = createFavoriteRelaysDraftEvent(reorderedRelays, relaySetEvents)
const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
const addBlockedRelays = async (relayUrls: string[]) => {
const normalizedUrls = relayUrls
.map((relayUrl) => normalizeUrl(relayUrl))
.filter((url) => !!url && !blockedRelays.includes(url))
if (!normalizedUrls.length) return
const newBlockedRelays = [...blockedRelays, ...normalizedUrls]
setBlockedRelays(newBlockedRelays)
const draftEvent = createBlockedRelaysDraftEvent(newBlockedRelays)
const newBlockedRelaysEvent = await publish(draftEvent)
updateBlockedRelaysEvent(newBlockedRelaysEvent)
}
const deleteBlockedRelays = async (relayUrls: string[]) => {
const normalizedUrls = relayUrls.map((relayUrl) => normalizeUrl(relayUrl)).filter(Boolean)
const newBlockedRelays = blockedRelays.filter((relay) => !normalizedUrls.includes(relay))
setBlockedRelays(newBlockedRelays)
const draftEvent = createBlockedRelaysDraftEvent(newBlockedRelays)
const newBlockedRelaysEvent = await publish(draftEvent)
updateBlockedRelaysEvent(newBlockedRelaysEvent)
}
const reorderRelaySets = async (reorderedSets: TRelaySet[]) => {
setRelaySets(reorderedSets)
const draftEvent = createFavoriteRelaysDraftEvent(
favoriteRelays,
reorderedSets.map((set) => set.aTag)
)
const newFavoriteRelaysEvent = await publish(draftEvent)
updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
}
return (
<FavoriteRelaysContext.Provider
value={{
favoriteRelays,
addFavoriteRelays,
deleteFavoriteRelays,
reorderFavoriteRelays,
blockedRelays,
addBlockedRelays,
deleteBlockedRelays,
relaySets,
createRelaySet,
addRelaySets,
deleteRelaySet,
updateRelaySet,
reorderRelaySets
}}
>
{children}
</FavoriteRelaysContext.Provider>
)
}