feat: favorite relays (#250)
This commit is contained in:
226
src/providers/FavoriteRelaysProvider.tsx
Normal file
226
src/providers/FavoriteRelaysProvider.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
import { BIG_RELAY_URLS, DEFAULT_FAVORITE_RELAYS } from '@/constants'
|
||||
import { createFavoriteRelaysDraftEvent, createRelaySetDraftEvent } from '@/lib/draft-event'
|
||||
import { getRelaySetFromRelaySetEvent, getReplaceableEventIdentifier } from '@/lib/event'
|
||||
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>
|
||||
relaySets: TRelaySet[]
|
||||
addRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise<void>
|
||||
deleteRelaySet: (id: string) => Promise<void>
|
||||
updateRelaySet: (newSet: 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, updateFavoriteRelaysEvent, pubkey, relayList, publish } = useNostr()
|
||||
const [favoriteRelays, setFavoriteRelays] = useState<string[]>([])
|
||||
const [relaySetEvents, setRelaySetEvents] = useState<Event[]>([])
|
||||
const [relaySets, setRelaySets] = useState<TRelaySet[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
if (!favoriteRelaysEvent) {
|
||||
const favoriteRelays: string[] = DEFAULT_FAVORITE_RELAYS
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
setFavoriteRelays(relays)
|
||||
|
||||
if (!pubkey) return
|
||||
const relaySetEvents = await Promise.all(
|
||||
relaySetIds.map((id) => indexedDb.getReplaceableEvent(pubkey, kinds.Relaysets, id))
|
||||
)
|
||||
const nonExistingRelaySetIds = relaySetIds.filter((_, index) => {
|
||||
return !relaySetEvents[index]
|
||||
})
|
||||
if (nonExistingRelaySetIds.length) {
|
||||
const newRelaySetEvents = await client.fetchEvents(
|
||||
(relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 5),
|
||||
{
|
||||
kinds: [kinds.Relaysets],
|
||||
authors: [pubkey],
|
||||
'#d': nonExistingRelaySetIds
|
||||
}
|
||||
)
|
||||
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)
|
||||
}
|
||||
})
|
||||
await Promise.all(
|
||||
Array.from(relaySetEventMap.values()).map((event) => {
|
||||
return indexedDb.putReplaceableEvent(event)
|
||||
})
|
||||
)
|
||||
nonExistingRelaySetIds.forEach((id) => {
|
||||
const event = relaySetEventMap.get(id)
|
||||
if (event) {
|
||||
const index = relaySetIds.indexOf(id)
|
||||
if (index !== -1) {
|
||||
relaySetEvents[index] = event
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setRelaySetEvents(relaySetEvents.filter(Boolean) as Event[])
|
||||
}
|
||||
init()
|
||||
}, [favoriteRelaysEvent])
|
||||
|
||||
useEffect(() => {
|
||||
setRelaySets(
|
||||
relaySetEvents.map((evt) => getRelaySetFromRelaySetEvent(evt)).filter(Boolean) as TRelaySet[]
|
||||
)
|
||||
}, [relaySetEvents])
|
||||
|
||||
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 addRelaySet = 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 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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<FavoriteRelaysContext.Provider
|
||||
value={{
|
||||
favoriteRelays,
|
||||
addFavoriteRelays,
|
||||
deleteFavoriteRelays,
|
||||
relaySets,
|
||||
addRelaySet,
|
||||
deleteRelaySet,
|
||||
updateRelaySet
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</FavoriteRelaysContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,24 +1,24 @@
|
||||
import { DEFAULT_FAVORITE_RELAYS } from '@/constants'
|
||||
import { checkAlgoRelay } from '@/lib/relay'
|
||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||
import client from '@/services/client.service'
|
||||
import storage from '@/services/local-storage.service'
|
||||
import relayInfoService from '@/services/relay-info.service'
|
||||
import { TFeedType } from '@/types'
|
||||
import { TFeedInfo, TFeedType } from '@/types'
|
||||
import { Filter } from 'nostr-tools'
|
||||
import { createContext, useContext, useEffect, useRef, useState } from 'react'
|
||||
import { useFavoriteRelays } from './FavoriteRelaysProvider'
|
||||
import { useNostr } from './NostrProvider'
|
||||
import { useRelaySets } from './RelaySetsProvider'
|
||||
|
||||
type TFeedContext = {
|
||||
feedType: TFeedType
|
||||
feedInfo: TFeedInfo
|
||||
relayUrls: string[]
|
||||
temporaryRelayUrls: string[]
|
||||
filter: Filter
|
||||
isReady: boolean
|
||||
activeRelaySetId: string | null
|
||||
switchFeed: (
|
||||
feedType: TFeedType,
|
||||
options?: { activeRelaySetId?: string; pubkey?: string }
|
||||
options?: { activeRelaySetId?: string; pubkey?: string; relay?: string | null }
|
||||
) => Promise<void>
|
||||
}
|
||||
|
||||
@@ -35,16 +35,16 @@ export const useFeed = () => {
|
||||
export function FeedProvider({ children }: { children: React.ReactNode }) {
|
||||
const isFirstRenderRef = useRef(true)
|
||||
const { pubkey } = useNostr()
|
||||
const { relaySets } = useRelaySets()
|
||||
const feedTypeRef = useRef<TFeedType>(storage.getFeedType())
|
||||
const [feedType, setFeedType] = useState<TFeedType>(feedTypeRef.current)
|
||||
const { relaySets, favoriteRelays } = useFavoriteRelays()
|
||||
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()
|
||||
)
|
||||
const [feedInfo, setFeedInfo] = useState<TFeedInfo>({
|
||||
feedType: 'relay',
|
||||
id: DEFAULT_FAVORITE_RELAYS[0]
|
||||
})
|
||||
const feedInfoRef = useRef<TFeedInfo>(feedInfo)
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
@@ -60,13 +60,33 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
||||
if (temporaryRelayUrls.length) {
|
||||
return await switchFeed('temporary', { temporaryRelayUrls })
|
||||
}
|
||||
}
|
||||
|
||||
if (feedTypeRef.current === 'relays') {
|
||||
return await switchFeed('relays', { activeRelaySetId })
|
||||
if (feedInfoRef.current.feedType === 'temporary') {
|
||||
return
|
||||
}
|
||||
|
||||
let feedInfo: TFeedInfo = {
|
||||
feedType: 'relay',
|
||||
id: favoriteRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0]
|
||||
}
|
||||
if (pubkey) {
|
||||
const storedFeedInfo = storage.getFeedInfo(pubkey)
|
||||
if (storedFeedInfo) {
|
||||
feedInfo = storedFeedInfo
|
||||
}
|
||||
}
|
||||
|
||||
if (feedTypeRef.current === 'following' && pubkey) {
|
||||
if (feedInfo.feedType === 'relays') {
|
||||
return await switchFeed('relays', { activeRelaySetId: feedInfo.id })
|
||||
}
|
||||
|
||||
if (feedInfo.feedType === 'relay') {
|
||||
return await switchFeed('relay', { relay: feedInfo.id })
|
||||
}
|
||||
|
||||
// update following feed if pubkey changes
|
||||
if (feedInfo.feedType === 'following' && pubkey) {
|
||||
return await switchFeed('following', { pubkey })
|
||||
}
|
||||
}
|
||||
@@ -80,26 +100,46 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
||||
activeRelaySetId?: string | null
|
||||
temporaryRelayUrls?: string[] | null
|
||||
pubkey?: string | null
|
||||
relay?: string | null
|
||||
} = {}
|
||||
) => {
|
||||
setIsReady(false)
|
||||
if (feedType === 'relay') {
|
||||
const normalizedUrl = normalizeUrl(options.relay ?? '')
|
||||
if (!normalizedUrl || !isWebsocketUrl(normalizedUrl)) {
|
||||
setIsReady(true)
|
||||
return
|
||||
}
|
||||
|
||||
const newFeedInfo = { feedType, id: normalizedUrl }
|
||||
setFeedInfo(newFeedInfo)
|
||||
feedInfoRef.current = newFeedInfo
|
||||
setRelayUrls([normalizedUrl])
|
||||
setFilter({})
|
||||
storage.setFeedInfo(newFeedInfo, pubkey)
|
||||
setIsReady(true)
|
||||
|
||||
const relayInfo = await relayInfoService.getRelayInfo(normalizedUrl)
|
||||
client.setCurrentRelayUrls(checkAlgoRelay(relayInfo) ? [] : [normalizedUrl])
|
||||
return
|
||||
}
|
||||
if (feedType === 'relays') {
|
||||
const relaySetId = options.activeRelaySetId ?? (relaySets.length > 0 ? relaySets[0].id : null)
|
||||
if (!relaySetId) {
|
||||
return setIsReady(true)
|
||||
setIsReady(true)
|
||||
return
|
||||
}
|
||||
|
||||
const relaySet =
|
||||
relaySets.find((set) => set.id === options.activeRelaySetId) ??
|
||||
(relaySets.length > 0 ? relaySets[0] : null)
|
||||
if (relaySet) {
|
||||
feedTypeRef.current = feedType
|
||||
setFeedType(feedType)
|
||||
const newFeedInfo = { feedType, id: relaySet.id }
|
||||
setFeedInfo(newFeedInfo)
|
||||
feedInfoRef.current = newFeedInfo
|
||||
setRelayUrls(relaySet.relayUrls)
|
||||
setActiveRelaySetId(relaySet.id)
|
||||
setFilter({})
|
||||
storage.setActiveRelaySetId(relaySet.id)
|
||||
storage.setFeedType(feedType)
|
||||
storage.setFeedInfo(newFeedInfo, pubkey)
|
||||
setIsReady(true)
|
||||
|
||||
const relayInfos = await relayInfoService.getRelayInfos(relaySet.relayUrls)
|
||||
@@ -107,21 +147,23 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
||||
relaySet.relayUrls.filter((_, i) => !relayInfos[i] || !checkAlgoRelay(relayInfos[i]))
|
||||
)
|
||||
}
|
||||
return setIsReady(true)
|
||||
setIsReady(true)
|
||||
return
|
||||
}
|
||||
if (feedType === 'following') {
|
||||
if (!options.pubkey) {
|
||||
return setIsReady(true)
|
||||
}
|
||||
feedTypeRef.current = feedType
|
||||
setFeedType(feedType)
|
||||
setActiveRelaySetId(null)
|
||||
const newFeedInfo = { feedType }
|
||||
setFeedInfo(newFeedInfo)
|
||||
feedInfoRef.current = newFeedInfo
|
||||
storage.setFeedInfo(newFeedInfo, pubkey)
|
||||
|
||||
const followings = await client.fetchFollowings(options.pubkey, true)
|
||||
setRelayUrls([])
|
||||
setFilter({
|
||||
authors: followings.includes(options.pubkey) ? followings : [...followings, options.pubkey]
|
||||
})
|
||||
storage.setFeedType(feedType)
|
||||
return setIsReady(true)
|
||||
}
|
||||
if (feedType === 'temporary') {
|
||||
@@ -130,11 +172,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
||||
return setIsReady(true)
|
||||
}
|
||||
|
||||
feedTypeRef.current = feedType
|
||||
setFeedType(feedType)
|
||||
const newFeedInfo = { feedType }
|
||||
setFeedInfo(newFeedInfo)
|
||||
feedInfoRef.current = newFeedInfo
|
||||
setTemporaryRelayUrls(urls)
|
||||
setRelayUrls(urls)
|
||||
setActiveRelaySetId(null)
|
||||
setFilter({})
|
||||
setIsReady(true)
|
||||
|
||||
@@ -150,12 +192,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<FeedContext.Provider
|
||||
value={{
|
||||
feedType,
|
||||
feedInfo,
|
||||
relayUrls,
|
||||
temporaryRelayUrls,
|
||||
filter,
|
||||
isReady,
|
||||
activeRelaySetId,
|
||||
switchFeed
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import LoginDialog from '@/components/LoginDialog'
|
||||
import { BIG_RELAY_URLS, COMMENT_EVENT_KIND, PICTURE_EVENT_KIND } from '@/constants'
|
||||
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||
import { useToast } from '@/hooks'
|
||||
import {
|
||||
getLatestEvent,
|
||||
@@ -30,6 +30,7 @@ type TNostrContext = {
|
||||
relayList: TRelayList | null
|
||||
followListEvent?: Event
|
||||
muteListEvent?: Event
|
||||
favoriteRelaysEvent?: Event
|
||||
account: TAccountPointer | null
|
||||
accounts: TAccountPointer[]
|
||||
nsec: string | null
|
||||
@@ -55,6 +56,7 @@ type TNostrContext = {
|
||||
updateProfileEvent: (profileEvent: Event) => Promise<void>
|
||||
updateFollowListEvent: (followListEvent: Event) => Promise<void>
|
||||
updateMuteListEvent: (muteListEvent: Event, tags: string[][]) => Promise<void>
|
||||
updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise<void>
|
||||
}
|
||||
|
||||
const NostrContext = createContext<TNostrContext | undefined>(undefined)
|
||||
@@ -80,6 +82,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
const [relayList, setRelayList] = useState<TRelayList | null>(null)
|
||||
const [followListEvent, setFollowListEvent] = useState<Event | undefined>(undefined)
|
||||
const [muteListEvent, setMuteListEvent] = useState<Event | undefined>(undefined)
|
||||
const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState<Event | undefined>(undefined)
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
@@ -131,13 +134,19 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
} else {
|
||||
setNcryptsec(null)
|
||||
}
|
||||
const [storedRelayListEvent, storedProfileEvent, storedFollowListEvent, storedMuteListEvent] =
|
||||
await Promise.all([
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Contacts),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Mutelist)
|
||||
])
|
||||
const [
|
||||
storedRelayListEvent,
|
||||
storedProfileEvent,
|
||||
storedFollowListEvent,
|
||||
storedMuteListEvent,
|
||||
storedFavoriteRelaysEvent
|
||||
] = await Promise.all([
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Metadata),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Contacts),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, kinds.Mutelist),
|
||||
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS)
|
||||
])
|
||||
if (storedRelayListEvent) {
|
||||
setRelayList(
|
||||
storedRelayListEvent ? getRelayListFromRelayListEvent(storedRelayListEvent) : null
|
||||
@@ -153,6 +162,9 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
if (storedMuteListEvent) {
|
||||
setMuteListEvent(storedMuteListEvent)
|
||||
}
|
||||
if (storedFavoriteRelaysEvent) {
|
||||
setFavoriteRelaysEvent(storedFavoriteRelaysEvent)
|
||||
}
|
||||
|
||||
const relayListEvents = await client.fetchEvents(BIG_RELAY_URLS, {
|
||||
kinds: [kinds.RelayList],
|
||||
@@ -167,13 +179,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
setRelayList(relayList)
|
||||
|
||||
const events = await client.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 4), {
|
||||
kinds: [kinds.Metadata, kinds.Contacts, kinds.Mutelist],
|
||||
kinds: [kinds.Metadata, kinds.Contacts, kinds.Mutelist, ExtendedKind.FAVORITE_RELAYS],
|
||||
authors: [account.pubkey]
|
||||
})
|
||||
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at)
|
||||
const profileEvent = sortedEvents.find((e) => e.kind === kinds.Metadata)
|
||||
const followListEvent = sortedEvents.find((e) => e.kind === kinds.Contacts)
|
||||
const muteListEvent = sortedEvents.find((e) => e.kind === kinds.Mutelist)
|
||||
const favoriteRelaysEvent = sortedEvents.find((e) => e.kind === ExtendedKind.FAVORITE_RELAYS)
|
||||
if (profileEvent) {
|
||||
setProfileEvent(profileEvent)
|
||||
setProfile(getProfileFromProfileEvent(profileEvent))
|
||||
@@ -192,6 +205,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
setMuteListEvent(muteListEvent)
|
||||
await indexedDb.putReplaceableEvent(muteListEvent)
|
||||
}
|
||||
if (favoriteRelaysEvent) {
|
||||
setFavoriteRelaysEvent(favoriteRelaysEvent)
|
||||
await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
|
||||
}
|
||||
|
||||
client.initUserIndexFromFollowings(account.pubkey, controller.signal)
|
||||
return controller
|
||||
@@ -414,8 +431,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
kinds.ShortTextNote,
|
||||
kinds.Reaction,
|
||||
kinds.Repost,
|
||||
COMMENT_EVENT_KIND,
|
||||
PICTURE_EVENT_KIND
|
||||
ExtendedKind.COMMENT,
|
||||
ExtendedKind.PICTURE
|
||||
].includes(draftEvent.kind)
|
||||
) {
|
||||
const mentions: string[] = []
|
||||
@@ -509,6 +526,13 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
setMuteListEvent(muteListEvent)
|
||||
}
|
||||
|
||||
const updateFavoriteRelaysEvent = async (favoriteRelaysEvent: Event) => {
|
||||
const newFavoriteRelaysEvent = await indexedDb.putReplaceableEvent(favoriteRelaysEvent)
|
||||
if (newFavoriteRelaysEvent.id !== favoriteRelaysEvent.id) return
|
||||
|
||||
setFavoriteRelaysEvent(newFavoriteRelaysEvent)
|
||||
}
|
||||
|
||||
return (
|
||||
<NostrContext.Provider
|
||||
value={{
|
||||
@@ -518,6 +542,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
relayList,
|
||||
followListEvent,
|
||||
muteListEvent,
|
||||
favoriteRelaysEvent,
|
||||
account,
|
||||
accounts: storage
|
||||
.getAccounts()
|
||||
@@ -541,7 +566,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
||||
updateRelayListEvent,
|
||||
updateProfileEvent,
|
||||
updateFollowListEvent,
|
||||
updateMuteListEvent
|
||||
updateMuteListEvent,
|
||||
updateFavoriteRelaysEvent
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { BIG_RELAY_URLS, COMMENT_EVENT_KIND } from '@/constants'
|
||||
import { BIG_RELAY_URLS, ExtendedKind } from '@/constants'
|
||||
import { TPrimaryPageName, usePrimaryPage } from '@/PageManager'
|
||||
import client from '@/services/client.service'
|
||||
import storage from '@/services/local-storage.service'
|
||||
@@ -73,7 +73,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
|
||||
{
|
||||
kinds: [
|
||||
kinds.ShortTextNote,
|
||||
COMMENT_EVENT_KIND,
|
||||
ExtendedKind.COMMENT,
|
||||
kinds.Reaction,
|
||||
kinds.Repost,
|
||||
kinds.Zap
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import { randomString } from '@/lib/random'
|
||||
import { isWebsocketUrl, normalizeUrl } from '@/lib/url'
|
||||
import storage from '@/services/local-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,
|
||||
addRelaySet,
|
||||
deleteRelaySet,
|
||||
updateRelaySet,
|
||||
mergeRelaySets
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RelaySetsContext.Provider>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user