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
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> |
|
) |
|
}
|
|
|