import { FavoriteRelays, RelaySet, tryToFavoriteRelays, tryToRelaySet, fromRelaySetToLegacy, Pubkey, RelayUrl } from '@/domain' 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, useMemo, useState } from 'react' import { useNostr } from './NostrProvider' type TFavoriteRelaysContext = { favoriteRelays: string[] addFavoriteRelays: (relayUrls: string[]) => Promise deleteFavoriteRelays: (relayUrls: string[]) => Promise reorderFavoriteRelays: (reorderedRelays: string[]) => Promise relaySets: TRelaySet[] createRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise addRelaySets: (newRelaySetEvents: Event[]) => Promise deleteRelaySet: (id: string) => Promise updateRelaySet: (newSet: TRelaySet) => Promise reorderRelaySets: (reorderedSets: TRelaySet[]) => Promise } const FavoriteRelaysContext = createContext(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, updateFavoriteRelaysEvent, pubkey, relayList, publish } = useNostr() const [relaySetEvents, setRelaySetEvents] = useState([]) // Create domain FavoriteRelays from event and relay set events const favoriteRelaysAggregate = useMemo(() => { if (!favoriteRelaysEvent || !pubkey) return null return tryToFavoriteRelays(favoriteRelaysEvent, relaySetEvents) }, [favoriteRelaysEvent, relaySetEvents, pubkey]) // Legacy compatibility: expose relays as string[] for existing consumers const favoriteRelays = useMemo(() => { if (!favoriteRelaysAggregate) { // Fall back to storage-based relay sets const storedRelaySets = storage.getRelaySets() const relays: string[] = [] storedRelaySets.forEach(({ relayUrls }) => { relayUrls.forEach((url) => { if (!relays.includes(url)) { relays.push(url) } }) }) return relays } return favoriteRelaysAggregate.getRelayUrls() }, [favoriteRelaysAggregate]) // Legacy compatibility: expose relay sets as TRelaySet[] for existing consumers const relaySets = useMemo((): TRelaySet[] => { if (!favoriteRelaysAggregate || !pubkey) return [] return favoriteRelaysAggregate.getSets().map((set) => fromRelaySetToLegacy(set, pubkey)) }, [favoriteRelaysAggregate, pubkey]) // Initialize relay sets from event useEffect(() => { if (!favoriteRelaysEvent || !pubkey) { setRelaySetEvents([]) return } const init = async () => { // Extract relay set IDs from event const relaySetIds: string[] = [] favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => { if (tagName === 'a' && tagValue) { const [kind, author, relaySetId] = tagValue.split(':') if (kind !== kinds.Relaysets.toString()) return if (author !== pubkey) return // TODO: support others relay sets if (!relaySetId || relaySetIds.includes(relaySetId)) return relaySetIds.push(relaySetId) } }) if (!relaySetIds.length) { setRelaySetEvents([]) return } // Load from cache first const storedRelaySetEvents = await Promise.all( relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id)) ) setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[]) // Fetch latest from relays const newRelaySetEvents = await client.fetchEvents( (relayList?.write ?? []).concat(client.currentRelays).slice(0, 5), { kinds: [kinds.Relaysets], authors: [pubkey], '#d': relaySetIds } ) // Deduplicate by keeping latest version const relaySetEventMap = new Map() newRelaySetEvents.forEach((event) => { const d = event.tags.find((t) => t[0] === 'd')?.[1] if (!d) return const old = relaySetEventMap.get(d) if (!old || old.created_at < event.created_at) { relaySetEventMap.set(d, event) } }) // Maintain order from relay set IDs const uniqueNewRelaySetEvents = relaySetIds .map((id, index) => relaySetEventMap.get(id) || storedRelaySetEvents[index]) .filter(Boolean) as Event[] setRelaySetEvents(uniqueNewRelaySetEvents) // Cache the events await Promise.all( uniqueNewRelaySetEvents.map((event) => indexedDb.putReplaceableEvent(event)) ) } init() }, [favoriteRelaysEvent, pubkey, relayList?.write]) const addFavoriteRelays = async (relayUrls: string[]) => { if (!pubkey) return const ownerPubkey = Pubkey.fromHex(pubkey) const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey) // Use domain aggregate to add relays const changes = relayUrls .map((url) => currentAggregate.addRelayUrl(url)) .filter((c) => c && c.type !== 'no_change') if (changes.length === 0) return // Publish the updated favorite relays const draftEvent = currentAggregate.toDraftEvent(pubkey) const newFavoriteRelaysEvent = await publish(draftEvent) updateFavoriteRelaysEvent(newFavoriteRelaysEvent) } const deleteFavoriteRelays = async (relayUrls: string[]) => { if (!pubkey || !favoriteRelaysAggregate) return // Use domain aggregate to remove relays const changes = relayUrls .map((url) => { const relay = RelayUrl.tryCreate(url) return relay ? favoriteRelaysAggregate.removeRelay(relay) : null }) .filter((c) => c && c.type !== 'no_change') if (changes.length === 0) return // Publish the updated favorite relays const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey) const newFavoriteRelaysEvent = await publish(draftEvent) updateFavoriteRelaysEvent(newFavoriteRelaysEvent) } const createRelaySet = async (relaySetName: string, relayUrls: string[] = []) => { if (!pubkey) return // Create relay set using domain aggregate const newRelaySet = RelaySet.createWithRelays(relaySetName, relayUrls) // Publish the relay set event const relaySetDraftEvent = newRelaySet.toDraftEvent() const newRelaySetEvent = await publish(relaySetDraftEvent) await indexedDb.putReplaceableEvent(newRelaySetEvent) // Add the set to favorites const ownerPubkey = Pubkey.fromHex(pubkey) const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey) currentAggregate.addSet(newRelaySet) // Publish the updated favorite relays const favoriteRelaysDraftEvent = currentAggregate.toDraftEvent(pubkey) const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent) updateFavoriteRelaysEvent(newFavoriteRelaysEvent) } const addRelaySets = async (newRelaySetEvents: Event[]) => { if (!pubkey) return const ownerPubkey = Pubkey.fromHex(pubkey) const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey) // Convert events to domain objects and add them for (const event of newRelaySetEvents) { const relaySet = tryToRelaySet(event) if (relaySet) { currentAggregate.addSet(relaySet) } } // Publish the updated favorite relays const favoriteRelaysDraftEvent = currentAggregate.toDraftEvent(pubkey) const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent) updateFavoriteRelaysEvent(newFavoriteRelaysEvent) } const deleteRelaySet = async (id: string) => { if (!pubkey || !favoriteRelaysAggregate) return const change = favoriteRelaysAggregate.removeSet(id) if (change.type === 'no_change') return // Publish the updated favorite relays const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey) const newFavoriteRelaysEvent = await publish(draftEvent) updateFavoriteRelaysEvent(newFavoriteRelaysEvent) } const updateRelaySet = async (newSet: TRelaySet) => { if (!pubkey) return // Create domain object from legacy format and publish const relaySet = RelaySet.createWithRelays(newSet.name, newSet.relayUrls, newSet.id) const draftEvent = relaySet.toDraftEvent() const newRelaySetEvent = await publish(draftEvent) await indexedDb.putReplaceableEvent(newRelaySetEvent) // Update the local relay set events setRelaySetEvents((prev) => { return prev.map((event) => { const d = event.tags.find((t) => t[0] === 'd')?.[1] if (d === newSet.id) { return newRelaySetEvent } return event }) }) } const reorderFavoriteRelays = async (reorderedRelays: string[]) => { if (!pubkey || !favoriteRelaysAggregate) return // Reorder using domain aggregate const relayUrls = reorderedRelays .map((url) => RelayUrl.tryCreate(url)) .filter((r): r is RelayUrl => r !== null) favoriteRelaysAggregate.reorderRelays(relayUrls) // Publish the updated favorite relays const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey) const newFavoriteRelaysEvent = await publish(draftEvent) updateFavoriteRelaysEvent(newFavoriteRelaysEvent) } const reorderRelaySets = async (reorderedSets: TRelaySet[]) => { if (!pubkey || !favoriteRelaysAggregate) return // Convert to domain objects and reorder const domainSets = reorderedSets .map((s) => favoriteRelaysAggregate.getSet(s.id)) .filter((s): s is RelaySet => s !== undefined) favoriteRelaysAggregate.reorderSets(domainSets) // Publish the updated favorite relays const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey) const newFavoriteRelaysEvent = await publish(draftEvent) updateFavoriteRelaysEvent(newFavoriteRelaysEvent) } return ( {children} ) }