FavoriteRelaysProvider.tsx raw
1 import { DEPLOYMENT } from '@/constants'
2 import {
3 FavoriteRelays,
4 RelaySet,
5 tryToFavoriteRelays,
6 tryToRelaySet,
7 fromRelaySetToLegacy,
8 Pubkey,
9 RelayUrl
10 } from '@/domain'
11 import client from '@/services/client.service'
12 import indexedDb from '@/services/indexed-db.service'
13 import storage from '@/services/local-storage.service'
14 import { TRelaySet } from '@/types'
15 import { Event, kinds } from 'nostr-tools'
16 import { createContext, useContext, useEffect, useMemo, useState } from 'react'
17 import { useNostr } from './NostrProvider'
18
19 type TFavoriteRelaysContext = {
20 favoriteRelays: string[]
21 addFavoriteRelays: (relayUrls: string[]) => Promise<void>
22 deleteFavoriteRelays: (relayUrls: string[]) => Promise<void>
23 reorderFavoriteRelays: (reorderedRelays: string[]) => Promise<void>
24 relaySets: TRelaySet[]
25 createRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void>
26 addRelaySets: (newRelaySetEvents: Event[]) => Promise<void>
27 deleteRelaySet: (id: string) => Promise<void>
28 updateRelaySet: (newSet: TRelaySet) => Promise<void>
29 reorderRelaySets: (reorderedSets: TRelaySet[]) => Promise<void>
30 }
31
32 const FavoriteRelaysContext = createContext<TFavoriteRelaysContext | undefined>(undefined)
33
34 export const useFavoriteRelays = () => {
35 const context = useContext(FavoriteRelaysContext)
36 if (!context) {
37 throw new Error('useFavoriteRelays must be used within a FavoriteRelaysProvider')
38 }
39 return context
40 }
41
42 export function FavoriteRelaysProvider({ children }: { children: React.ReactNode }) {
43 const { favoriteRelaysEvent, updateFavoriteRelaysEvent, pubkey, relayList, publish } = useNostr()
44 const [relaySetEvents, setRelaySetEvents] = useState<Event[]>([])
45
46 // Create domain FavoriteRelays from event and relay set events
47 const favoriteRelaysAggregate = useMemo(() => {
48 if (!favoriteRelaysEvent || !pubkey) return null
49 return tryToFavoriteRelays(favoriteRelaysEvent, relaySetEvents)
50 }, [favoriteRelaysEvent, relaySetEvents, pubkey])
51
52 // Legacy compatibility: expose relays as string[] for existing consumers
53 const favoriteRelays = useMemo(() => {
54 if (!favoriteRelaysAggregate) {
55 // Fall back to storage-based relay sets
56 const storedRelaySets = storage.getRelaySets()
57 const relays: string[] = []
58 storedRelaySets.forEach(({ relayUrls }) => {
59 relayUrls.forEach((url) => {
60 if (!relays.includes(url)) {
61 relays.push(url)
62 }
63 })
64 })
65 // If still empty and a default relay is configured, use it
66 if (relays.length === 0 && DEPLOYMENT.defaultRelay) {
67 relays.push(DEPLOYMENT.defaultRelay)
68 }
69 return relays
70 }
71 return favoriteRelaysAggregate.getRelayUrls()
72 }, [favoriteRelaysAggregate])
73
74 // Legacy compatibility: expose relay sets as TRelaySet[] for existing consumers
75 const relaySets = useMemo((): TRelaySet[] => {
76 if (!favoriteRelaysAggregate || !pubkey) return []
77 return favoriteRelaysAggregate.getSets().map((set) => fromRelaySetToLegacy(set, pubkey))
78 }, [favoriteRelaysAggregate, pubkey])
79
80 // Initialize relay sets from event
81 useEffect(() => {
82 if (!favoriteRelaysEvent || !pubkey) {
83 setRelaySetEvents([])
84 return
85 }
86
87 const init = async () => {
88 // Extract relay set IDs from event
89 const relaySetIds: string[] = []
90 favoriteRelaysEvent.tags.forEach(([tagName, tagValue]) => {
91 if (tagName === 'a' && tagValue) {
92 const [kind, author, relaySetId] = tagValue.split(':')
93 if (kind !== kinds.Relaysets.toString()) return
94 if (author !== pubkey) return // TODO: support others relay sets
95 if (!relaySetId || relaySetIds.includes(relaySetId)) return
96 relaySetIds.push(relaySetId)
97 }
98 })
99
100 if (!relaySetIds.length) {
101 setRelaySetEvents([])
102 return
103 }
104
105 // Load from cache first
106 const storedRelaySetEvents = await Promise.all(
107 relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id))
108 )
109 setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[])
110
111 // Fetch latest from relays
112 const newRelaySetEvents = await client.fetchEvents(
113 (relayList?.write ?? []).concat(client.currentRelays).slice(0, 5),
114 {
115 kinds: [kinds.Relaysets],
116 authors: [pubkey],
117 '#d': relaySetIds
118 }
119 )
120
121 // Deduplicate by keeping latest version
122 const relaySetEventMap = new Map<string, Event>()
123 newRelaySetEvents.forEach((event) => {
124 const d = event.tags.find((t) => t[0] === 'd')?.[1]
125 if (!d) return
126 const old = relaySetEventMap.get(d)
127 if (!old || old.created_at < event.created_at) {
128 relaySetEventMap.set(d, event)
129 }
130 })
131
132 // Maintain order from relay set IDs
133 const uniqueNewRelaySetEvents = relaySetIds
134 .map((id, index) => relaySetEventMap.get(id) || storedRelaySetEvents[index])
135 .filter(Boolean) as Event[]
136
137 setRelaySetEvents(uniqueNewRelaySetEvents)
138
139 // Cache the events
140 await Promise.all(
141 uniqueNewRelaySetEvents.map((event) => indexedDb.putReplaceableEvent(event))
142 )
143 }
144 init()
145 }, [favoriteRelaysEvent, pubkey, relayList?.write])
146
147 const addFavoriteRelays = async (relayUrls: string[]) => {
148 if (!pubkey) return
149
150 const ownerPubkey = Pubkey.fromHex(pubkey)
151 const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey)
152
153 // Use domain aggregate to add relays
154 const changes = relayUrls
155 .map((url) => currentAggregate.addRelayUrl(url))
156 .filter((c) => c && c.type !== 'no_change')
157
158 if (changes.length === 0) return
159
160 // Publish the updated favorite relays
161 const draftEvent = currentAggregate.toDraftEvent(pubkey)
162 const newFavoriteRelaysEvent = await publish(draftEvent)
163 updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
164 }
165
166 const deleteFavoriteRelays = async (relayUrls: string[]) => {
167 if (!pubkey || !favoriteRelaysAggregate) return
168
169 // Use domain aggregate to remove relays
170 const changes = relayUrls
171 .map((url) => {
172 const relay = RelayUrl.tryCreate(url)
173 return relay ? favoriteRelaysAggregate.removeRelay(relay) : null
174 })
175 .filter((c) => c && c.type !== 'no_change')
176
177 if (changes.length === 0) return
178
179 // Publish the updated favorite relays
180 const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey)
181 const newFavoriteRelaysEvent = await publish(draftEvent)
182 updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
183 }
184
185 const createRelaySet = async (relaySetName: string, relayUrls: string[] = []) => {
186 if (!pubkey) return
187
188 // Create relay set using domain aggregate
189 const newRelaySet = RelaySet.createWithRelays(relaySetName, relayUrls)
190
191 // Publish the relay set event
192 const relaySetDraftEvent = newRelaySet.toDraftEvent()
193 const newRelaySetEvent = await publish(relaySetDraftEvent)
194 await indexedDb.putReplaceableEvent(newRelaySetEvent)
195
196 // Add the set to favorites
197 const ownerPubkey = Pubkey.fromHex(pubkey)
198 const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey)
199 currentAggregate.addSet(newRelaySet)
200
201 // Publish the updated favorite relays
202 const favoriteRelaysDraftEvent = currentAggregate.toDraftEvent(pubkey)
203 const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
204 updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
205 }
206
207 const addRelaySets = async (newRelaySetEvents: Event[]) => {
208 if (!pubkey) return
209
210 const ownerPubkey = Pubkey.fromHex(pubkey)
211 const currentAggregate = favoriteRelaysAggregate ?? FavoriteRelays.empty(ownerPubkey)
212
213 // Convert events to domain objects and add them
214 for (const event of newRelaySetEvents) {
215 const relaySet = tryToRelaySet(event)
216 if (relaySet) {
217 currentAggregate.addSet(relaySet)
218 }
219 }
220
221 // Publish the updated favorite relays
222 const favoriteRelaysDraftEvent = currentAggregate.toDraftEvent(pubkey)
223 const newFavoriteRelaysEvent = await publish(favoriteRelaysDraftEvent)
224 updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
225 }
226
227 const deleteRelaySet = async (id: string) => {
228 if (!pubkey || !favoriteRelaysAggregate) return
229
230 const change = favoriteRelaysAggregate.removeSet(id)
231 if (change.type === 'no_change') return
232
233 // Publish the updated favorite relays
234 const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey)
235 const newFavoriteRelaysEvent = await publish(draftEvent)
236 updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
237 }
238
239 const updateRelaySet = async (newSet: TRelaySet) => {
240 if (!pubkey) return
241
242 // Create domain object from legacy format and publish
243 const relaySet = RelaySet.createWithRelays(newSet.name, newSet.relayUrls, newSet.id)
244 const draftEvent = relaySet.toDraftEvent()
245 const newRelaySetEvent = await publish(draftEvent)
246 await indexedDb.putReplaceableEvent(newRelaySetEvent)
247
248 // Update the local relay set events
249 setRelaySetEvents((prev) => {
250 return prev.map((event) => {
251 const d = event.tags.find((t) => t[0] === 'd')?.[1]
252 if (d === newSet.id) {
253 return newRelaySetEvent
254 }
255 return event
256 })
257 })
258 }
259
260 const reorderFavoriteRelays = async (reorderedRelays: string[]) => {
261 if (!pubkey || !favoriteRelaysAggregate) return
262
263 // Reorder using domain aggregate
264 const relayUrls = reorderedRelays
265 .map((url) => RelayUrl.tryCreate(url))
266 .filter((r): r is RelayUrl => r !== null)
267 favoriteRelaysAggregate.reorderRelays(relayUrls)
268
269 // Publish the updated favorite relays
270 const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey)
271 const newFavoriteRelaysEvent = await publish(draftEvent)
272 updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
273 }
274
275 const reorderRelaySets = async (reorderedSets: TRelaySet[]) => {
276 if (!pubkey || !favoriteRelaysAggregate) return
277
278 // Convert to domain objects and reorder
279 const domainSets = reorderedSets
280 .map((s) => favoriteRelaysAggregate.getSet(s.id))
281 .filter((s): s is RelaySet => s !== undefined)
282 favoriteRelaysAggregate.reorderSets(domainSets)
283
284 // Publish the updated favorite relays
285 const draftEvent = favoriteRelaysAggregate.toDraftEvent(pubkey)
286 const newFavoriteRelaysEvent = await publish(draftEvent)
287 updateFavoriteRelaysEvent(newFavoriteRelaysEvent)
288 }
289
290 return (
291 <FavoriteRelaysContext.Provider
292 value={{
293 favoriteRelays,
294 addFavoriteRelays,
295 deleteFavoriteRelays,
296 reorderFavoriteRelays,
297 relaySets,
298 createRelaySet,
299 addRelaySets,
300 deleteRelaySet,
301 updateRelaySet,
302 reorderRelaySets
303 }}
304 >
305 {children}
306 </FavoriteRelaysContext.Provider>
307 )
308 }
309