feat: sync relay sets
This commit is contained in:
@@ -1,9 +1,20 @@
|
||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||
import client from '@/services/client.service'
|
||||
import storage from '@/services/storage.service'
|
||||
import { TFeedType } from '@/types'
|
||||
import { createContext, useContext, useState } from 'react'
|
||||
import { Filter } from 'nostr-tools'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useNostr } from './NostrProvider'
|
||||
import { useRelaySets } from './RelaySetsProvider'
|
||||
|
||||
type TFeedContext = {
|
||||
feedType: TFeedType
|
||||
setFeedType: (feedType: TFeedType) => void
|
||||
relayUrls: string[]
|
||||
temporaryRelayUrls: string[]
|
||||
filter: Filter
|
||||
isReady: boolean
|
||||
activeRelaySetId: string | null
|
||||
switchFeed: (feedType: TFeedType, options?: { activeRelaySetId?: string }) => Promise<void>
|
||||
}
|
||||
|
||||
const FeedContext = createContext<TFeedContext | undefined>(undefined)
|
||||
@@ -17,7 +28,113 @@ export const useFeed = () => {
|
||||
}
|
||||
|
||||
export function FeedProvider({ children }: { children: React.ReactNode }) {
|
||||
const { pubkey } = useNostr()
|
||||
const { relaySets } = useRelaySets()
|
||||
const [feedType, setFeedType] = useState<TFeedType>('relays')
|
||||
const [relayUrls, setRelayUrls] = useState<string[]>([])
|
||||
const [temporaryRelayUrls, setTemporaryRelayUrls] = useState<string[]>([])
|
||||
const [filter, setFilter] = useState<Filter>({})
|
||||
const [isReady, setIsReady] = useState(false)
|
||||
const [activeRelaySetId, setActiveRelaySetId] = useState<string | null>(() =>
|
||||
storage.getActiveRelaySetId()
|
||||
)
|
||||
|
||||
return <FeedContext.Provider value={{ feedType, setFeedType }}>{children}</FeedContext.Provider>
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
// temporary relay urls from query params
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const tempRelays = searchParams
|
||||
.getAll('r')
|
||||
.map((url) =>
|
||||
!url.startsWith('ws://') && !url.startsWith('wss://') ? `wss://${url}` : url
|
||||
)
|
||||
.filter((url) => isWebsocketUrl(url))
|
||||
.map((url) => normalizeUrl(url))
|
||||
if (tempRelays.length) {
|
||||
setTemporaryRelayUrls(tempRelays)
|
||||
return await switchFeed('temporary')
|
||||
}
|
||||
|
||||
await switchFeed('relays', { activeRelaySetId })
|
||||
}
|
||||
|
||||
init()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (feedType !== 'following') return
|
||||
|
||||
switchFeed('following')
|
||||
}, [pubkey])
|
||||
|
||||
useEffect(() => {
|
||||
if (feedType !== 'relays') return
|
||||
|
||||
const relaySet = relaySets.find((set) => set.id === activeRelaySetId)
|
||||
if (!relaySet) return
|
||||
|
||||
setRelayUrls(relaySet.relayUrls)
|
||||
}, [relaySets])
|
||||
|
||||
const switchFeed = async (
|
||||
feedType: TFeedType,
|
||||
options: { activeRelaySetId?: string | null } = {}
|
||||
) => {
|
||||
setIsReady(false)
|
||||
if (feedType === 'relays') {
|
||||
const relaySetId = options.activeRelaySetId ?? (relaySets.length > 0 ? relaySets[0].id : null)
|
||||
if (!relaySetId) return
|
||||
|
||||
const relaySet =
|
||||
relaySets.find((set) => set.id === options.activeRelaySetId) ??
|
||||
(relaySets.length > 0 ? relaySets[0] : null)
|
||||
if (relaySet) {
|
||||
setFeedType(feedType)
|
||||
setRelayUrls(relaySet.relayUrls)
|
||||
setActiveRelaySetId(relaySet.id)
|
||||
setFilter({})
|
||||
setIsReady(true)
|
||||
storage.setActiveRelaySetId(relaySet.id)
|
||||
}
|
||||
return
|
||||
}
|
||||
if (feedType === 'following') {
|
||||
if (!pubkey) return
|
||||
setFeedType(feedType)
|
||||
setActiveRelaySetId(null)
|
||||
const [relayList, followings] = await Promise.all([
|
||||
client.fetchRelayList(pubkey),
|
||||
client.fetchFollowings(pubkey)
|
||||
])
|
||||
setRelayUrls(relayList.read.slice(0, 4))
|
||||
setFilter({ authors: followings.includes(pubkey) ? followings : [...followings, pubkey] })
|
||||
setIsReady(true)
|
||||
return
|
||||
}
|
||||
if (feedType === 'temporary') {
|
||||
setFeedType(feedType)
|
||||
setRelayUrls(temporaryRelayUrls)
|
||||
setActiveRelaySetId(null)
|
||||
setFilter({})
|
||||
setIsReady(true)
|
||||
return
|
||||
}
|
||||
setIsReady(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<FeedContext.Provider
|
||||
value={{
|
||||
feedType,
|
||||
relayUrls,
|
||||
temporaryRelayUrls,
|
||||
filter,
|
||||
isReady,
|
||||
activeRelaySetId,
|
||||
switchFeed
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FeedContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { ISigner, TAccount, TAccountPointer, TDraftEvent, TRelayList } from '@/t
|
||||
import dayjs from 'dayjs'
|
||||
import { Event, kinds } from 'nostr-tools'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useRelaySettings } from '../RelaySettingsProvider'
|
||||
import { BunkerSigner } from './bunker.signer'
|
||||
import { Nip07Signer } from './nip-07.signer'
|
||||
import { NsecSigner } from './nsec.signer'
|
||||
@@ -47,7 +46,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
const [account, setAccount] = useState<TAccountPointer | null>(null)
|
||||
const [signer, setSigner] = useState<ISigner | null>(null)
|
||||
const [openLoginDialog, setOpenLoginDialog] = useState(false)
|
||||
const { relayUrls: currentRelayUrls } = useRelaySettings()
|
||||
const { relayList, isFetching: isFetchingRelayList } = useFetchRelayList(account?.pubkey)
|
||||
const { followings, isFetching: isFetchingFollowings } = useFetchFollowings(account?.pubkey)
|
||||
|
||||
@@ -196,10 +194,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const publish = async (draftEvent: TDraftEvent, additionalRelayUrls: string[] = []) => {
|
||||
const event = await signEvent(draftEvent)
|
||||
await client.publishEvent(
|
||||
relayList.write.concat(additionalRelayUrls).concat(currentRelayUrls),
|
||||
event
|
||||
)
|
||||
await client.publishEvent(relayList.write.concat(additionalRelayUrls), event)
|
||||
return event
|
||||
}
|
||||
|
||||
|
||||
80
src/providers/RelaySetsProvider.tsx
Normal file
80
src/providers/RelaySetsProvider.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
import { randomString } from '@/lib/random'
|
||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||
import storage from '@/services/storage.service'
|
||||
import { TRelaySet } from '@/types'
|
||||
import { createContext, useContext, useEffect, useState } from 'react'
|
||||
|
||||
type TRelaySetsContext = {
|
||||
relaySets: TRelaySet[]
|
||||
addRelaySet: (relaySetName: string, relayUrls?: string[]) => string
|
||||
deleteRelaySet: (id: string) => void
|
||||
updateRelaySet: (newSet: TRelaySet) => void
|
||||
mergeRelaySets: (newSets: TRelaySet[]) => void
|
||||
}
|
||||
|
||||
const RelaySetsContext = createContext<TRelaySetsContext | undefined>(undefined)
|
||||
|
||||
export const useRelaySets = () => {
|
||||
const context = useContext(RelaySetsContext)
|
||||
if (!context) {
|
||||
throw new Error('useRelaySets must be used within a RelaySetsProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function RelaySetsProvider({ children }: { children: React.ReactNode }) {
|
||||
const [relaySets, setRelaySets] = useState<TRelaySet[]>(() => storage.getRelaySets())
|
||||
|
||||
useEffect(() => {
|
||||
storage.setRelaySets(relaySets)
|
||||
}, [relaySets])
|
||||
|
||||
const deleteRelaySet = (id: string) => {
|
||||
setRelaySets((pre) => pre.filter((set) => set.id !== id))
|
||||
}
|
||||
|
||||
const updateRelaySet = (newSet: TRelaySet) => {
|
||||
setRelaySets((pre) => {
|
||||
return pre.map((set) => (set.id === newSet.id ? newSet : set))
|
||||
})
|
||||
}
|
||||
|
||||
const addRelaySet = (relaySetName: string, relayUrls: string[] = []) => {
|
||||
const normalizedUrls = relayUrls
|
||||
.filter((url) => isWebsocketUrl(url))
|
||||
.map((url) => normalizeUrl(url))
|
||||
const id = randomString()
|
||||
setRelaySets((pre) => {
|
||||
return [
|
||||
...pre,
|
||||
{
|
||||
id,
|
||||
name: relaySetName,
|
||||
relayUrls: normalizedUrls
|
||||
}
|
||||
]
|
||||
})
|
||||
return id
|
||||
}
|
||||
|
||||
const mergeRelaySets = (newSets: TRelaySet[]) => {
|
||||
setRelaySets((pre) => {
|
||||
const newIds = newSets.map((set) => set.id)
|
||||
return pre.filter((set) => !newIds.includes(set.id)).concat(newSets)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<RelaySetsContext.Provider
|
||||
value={{
|
||||
relaySets: relaySets,
|
||||
addRelaySet,
|
||||
deleteRelaySet,
|
||||
updateRelaySet,
|
||||
mergeRelaySets
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RelaySetsContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
import { SEARCHABLE_RELAY_URLS } from '@/constants'
|
||||
import { checkAlgoRelay, checkSearchRelay } from '@/lib/relay'
|
||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||
import client from '@/services/client.service'
|
||||
import storage from '@/services/storage.service'
|
||||
import { TRelayGroup } from '@/types'
|
||||
import { createContext, Dispatch, useContext, useEffect, useState } from 'react'
|
||||
import { useFeed } from './FeedProvider'
|
||||
|
||||
type TRelaySettingsContext = {
|
||||
relayGroups: TRelayGroup[]
|
||||
temporaryRelayUrls: string[]
|
||||
relayUrls: string[]
|
||||
searchableRelayUrls: string[]
|
||||
areAlgoRelays: boolean
|
||||
switchRelayGroup: (groupName: string) => void
|
||||
renameRelayGroup: (oldGroupName: string, newGroupName: string) => string | null
|
||||
deleteRelayGroup: (groupName: string) => void
|
||||
addRelayGroup: (groupName: string, relayUrls?: string[]) => string | null
|
||||
updateRelayGroupRelayUrls: (groupName: string, relayUrls: string[]) => void
|
||||
setTemporaryRelayUrls: Dispatch<string[]>
|
||||
}
|
||||
|
||||
const RelaySettingsContext = createContext<TRelaySettingsContext | undefined>(undefined)
|
||||
|
||||
export const useRelaySettings = () => {
|
||||
const context = useContext(RelaySettingsContext)
|
||||
if (!context) {
|
||||
throw new Error('useRelaySettings must be used within a RelaySettingsProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
|
||||
export function RelaySettingsProvider({ children }: { children: React.ReactNode }) {
|
||||
const { setFeedType } = useFeed()
|
||||
const [relayGroups, setRelayGroups] = useState<TRelayGroup[]>([])
|
||||
const [temporaryRelayUrls, setTemporaryRelayUrls] = useState<string[]>([])
|
||||
const [relayUrls, setRelayUrls] = useState<string[]>(
|
||||
temporaryRelayUrls.length
|
||||
? temporaryRelayUrls
|
||||
: (relayGroups.find((group) => group.isActive)?.relayUrls ?? [])
|
||||
)
|
||||
const [searchableRelayUrls, setSearchableRelayUrls] = useState<string[]>(SEARCHABLE_RELAY_URLS)
|
||||
const [areAlgoRelays, setAreAlgoRelays] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const tempRelays = searchParams
|
||||
.getAll('r')
|
||||
.map((url) => (url.startsWith('wss://') || url.startsWith('ws://') ? url : `wss://${url}`))
|
||||
.filter((url) => isWebsocketUrl(url))
|
||||
.map((url) => normalizeUrl(url))
|
||||
if (tempRelays.length) {
|
||||
setTemporaryRelayUrls(tempRelays)
|
||||
setFeedType('relays')
|
||||
}
|
||||
const storedGroups = storage.getRelayGroups()
|
||||
setRelayGroups(storedGroups)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const handler = async () => {
|
||||
const newRelayUrls = temporaryRelayUrls.length
|
||||
? temporaryRelayUrls
|
||||
: (relayGroups.find((group) => group.isActive)?.relayUrls ?? [])
|
||||
|
||||
if (JSON.stringify(relayUrls) !== JSON.stringify(newRelayUrls)) {
|
||||
setRelayUrls(newRelayUrls)
|
||||
}
|
||||
const relayInfos = await client.fetchRelayInfos(newRelayUrls)
|
||||
const searchableRelayUrls = newRelayUrls.filter((_, index) =>
|
||||
checkSearchRelay(relayInfos[index])
|
||||
)
|
||||
setSearchableRelayUrls(
|
||||
searchableRelayUrls.length ? searchableRelayUrls : SEARCHABLE_RELAY_URLS
|
||||
)
|
||||
const nonAlgoRelayUrls = newRelayUrls.filter((_, index) => !checkAlgoRelay(relayInfos[index]))
|
||||
setAreAlgoRelays(newRelayUrls.length > 0 && nonAlgoRelayUrls.length === 0)
|
||||
client.setCurrentRelayUrls(nonAlgoRelayUrls)
|
||||
}
|
||||
handler()
|
||||
}, [relayGroups, temporaryRelayUrls, relayUrls])
|
||||
|
||||
const updateGroups = (fn: (pre: TRelayGroup[]) => TRelayGroup[]) => {
|
||||
let newGroups = relayGroups
|
||||
setRelayGroups((pre) => {
|
||||
newGroups = fn(pre)
|
||||
return newGroups
|
||||
})
|
||||
storage.setRelayGroups(newGroups)
|
||||
}
|
||||
|
||||
const switchRelayGroup = (groupName: string) => {
|
||||
updateGroups((pre) =>
|
||||
pre.map((group) => ({
|
||||
...group,
|
||||
isActive: group.groupName === groupName
|
||||
}))
|
||||
)
|
||||
setFeedType('relays')
|
||||
setTemporaryRelayUrls([])
|
||||
}
|
||||
|
||||
const deleteRelayGroup = (groupName: string) => {
|
||||
updateGroups((pre) => pre.filter((group) => group.groupName !== groupName))
|
||||
}
|
||||
|
||||
const updateRelayGroupRelayUrls = (groupName: string, relayUrls: string[]) => {
|
||||
updateGroups((pre) =>
|
||||
pre.map((group) => ({
|
||||
...group,
|
||||
relayUrls: group.groupName === groupName ? relayUrls : group.relayUrls
|
||||
}))
|
||||
)
|
||||
}
|
||||
|
||||
const renameRelayGroup = (oldGroupName: string, newGroupName: string) => {
|
||||
if (newGroupName === '') {
|
||||
return null
|
||||
}
|
||||
if (oldGroupName === newGroupName) {
|
||||
return null
|
||||
}
|
||||
updateGroups((pre) => {
|
||||
if (pre.some((group) => group.groupName === newGroupName)) {
|
||||
return pre
|
||||
}
|
||||
return pre.map((group) => ({
|
||||
...group,
|
||||
groupName: group.groupName === oldGroupName ? newGroupName : group.groupName
|
||||
}))
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
const addRelayGroup = (groupName: string, relayUrls: string[] = []) => {
|
||||
if (groupName === '') {
|
||||
return null
|
||||
}
|
||||
const normalizedUrls = relayUrls
|
||||
.filter((url) => isWebsocketUrl(url))
|
||||
.map((url) => normalizeUrl(url))
|
||||
updateGroups((pre) => {
|
||||
if (pre.some((group) => group.groupName === groupName)) {
|
||||
return pre
|
||||
}
|
||||
return [
|
||||
...pre,
|
||||
{
|
||||
groupName,
|
||||
relayUrls: normalizedUrls,
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<RelaySettingsContext.Provider
|
||||
value={{
|
||||
relayGroups,
|
||||
temporaryRelayUrls,
|
||||
relayUrls,
|
||||
searchableRelayUrls,
|
||||
areAlgoRelays,
|
||||
switchRelayGroup,
|
||||
renameRelayGroup,
|
||||
deleteRelayGroup,
|
||||
addRelayGroup,
|
||||
updateRelayGroupRelayUrls,
|
||||
setTemporaryRelayUrls
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RelaySettingsContext.Provider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user