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