>(
return (
Promise
+ deleteFavoriteRelays: (relayUrls: string[]) => Promise
+ relaySets: TRelaySet[]
+ addRelaySet: (relaySetName: string, relayUrls?: string[]) => Promise
+ deleteRelaySet: (id: string) => Promise
+ updateRelaySet: (newSet: TRelaySet) => Promise
+}
+
+const FavoriteRelaysContext = createContext(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([])
+ const [relaySetEvents, setRelaySetEvents] = useState([])
+ const [relaySets, setRelaySets] = useState([])
+
+ 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()
+ 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 (
+
+ {children}
+
+ )
+}
diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx
index 496e343..cf3f254 100644
--- a/src/providers/FeedProvider.tsx
+++ b/src/providers/FeedProvider.tsx
@@ -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
}
@@ -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(storage.getFeedType())
- const [feedType, setFeedType] = useState(feedTypeRef.current)
+ const { relaySets, favoriteRelays } = useFavoriteRelays()
const [relayUrls, setRelayUrls] = useState([])
const [temporaryRelayUrls, setTemporaryRelayUrls] = useState([])
const [filter, setFilter] = useState({})
const [isReady, setIsReady] = useState(false)
- const [activeRelaySetId, setActiveRelaySetId] = useState(
- storage.getActiveRelaySetId()
- )
+ const [feedInfo, setFeedInfo] = useState({
+ feedType: 'relay',
+ id: DEFAULT_FAVORITE_RELAYS[0]
+ })
+ const feedInfoRef = useRef(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 (feedInfoRef.current.feedType === 'temporary') {
+ return
+ }
- if (feedTypeRef.current === 'relays') {
- return await switchFeed('relays', { activeRelaySetId })
+ 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 (
diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx
index 43af286..f9938e3 100644
--- a/src/providers/NostrProvider/index.tsx
+++ b/src/providers/NostrProvider/index.tsx
@@ -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
updateFollowListEvent: (followListEvent: Event) => Promise
updateMuteListEvent: (muteListEvent: Event, tags: string[][]) => Promise
+ updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise
}
const NostrContext = createContext(undefined)
@@ -80,6 +82,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const [relayList, setRelayList] = useState(null)
const [followListEvent, setFollowListEvent] = useState(undefined)
const [muteListEvent, setMuteListEvent] = useState(undefined)
+ const [favoriteRelaysEvent, setFavoriteRelaysEvent] = useState(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 (
{children}
diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx
index 3d37528..21c57fa 100644
--- a/src/providers/NotificationProvider.tsx
+++ b/src/providers/NotificationProvider.tsx
@@ -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
diff --git a/src/providers/RelaySetsProvider.tsx b/src/providers/RelaySetsProvider.tsx
deleted file mode 100644
index d79a43e..0000000
--- a/src/providers/RelaySetsProvider.tsx
+++ /dev/null
@@ -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(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(() => 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 (
-
- {children}
-
- )
-}
diff --git a/src/services/client.service.ts b/src/services/client.service.ts
index 8fb23e2..228f31e 100644
--- a/src/services/client.service.ts
+++ b/src/services/client.service.ts
@@ -90,10 +90,6 @@ class ClientService extends EventTarget {
await indexedDb.iterateProfileEvents((profileEvent) => this.addUsernameToIndex(profileEvent))
}
- listConnectionStatus() {
- return this.pool.listConnectionStatus()
- }
-
setCurrentRelayUrls(urls: string[]) {
this.currentRelayUrls = urls
}
diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts
index a2c8805..9768351 100644
--- a/src/services/indexed-db.service.ts
+++ b/src/services/indexed-db.service.ts
@@ -1,3 +1,4 @@
+import { ExtendedKind } from '@/constants'
import { tagNameEquals } from '@/lib/tag'
import { Event, kinds } from 'nostr-tools'
@@ -13,7 +14,9 @@ const StoreNames = {
FOLLOW_LIST_EVENTS: 'followListEvents',
MUTE_LIST_EVENTS: 'muteListEvents',
MUTE_DECRYPTED_TAGS: 'muteDecryptedTags',
- RELAY_INFO_EVENTS: 'relayInfoEvents'
+ RELAY_INFO_EVENTS: 'relayInfoEvents',
+ FAVORITE_RELAYS: 'favoriteRelays',
+ RELAY_SETS: 'relaySets'
}
class IndexedDbService {
@@ -32,7 +35,7 @@ class IndexedDbService {
init(): Promise {
if (!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
- const request = window.indexedDB.open('jumble', 2)
+ const request = window.indexedDB.open('jumble', 3)
request.onerror = (event) => {
reject(event)
@@ -63,6 +66,12 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.RELAY_INFO_EVENTS)) {
db.createObjectStore(StoreNames.RELAY_INFO_EVENTS, { keyPath: 'key' })
}
+ if (!db.objectStoreNames.contains(StoreNames.FAVORITE_RELAYS)) {
+ db.createObjectStore(StoreNames.FAVORITE_RELAYS, { keyPath: 'key' })
+ }
+ if (!db.objectStoreNames.contains(StoreNames.RELAY_SETS)) {
+ db.createObjectStore(StoreNames.RELAY_SETS, { keyPath: 'key' })
+ }
this.db = db
}
})
@@ -84,14 +93,15 @@ class IndexedDbService {
const transaction = this.db.transaction(storeName, 'readwrite')
const store = transaction.objectStore(storeName)
- const getRequest = store.get(event.pubkey)
+ const key = this.getReplaceableEventKey(event)
+ const getRequest = store.get(key)
getRequest.onsuccess = () => {
const oldValue = getRequest.result as TValue | undefined
if (oldValue && oldValue.value.created_at >= event.created_at) {
transaction.commit()
return resolve(oldValue.value)
}
- const putRequest = store.put(this.formatValue(event.pubkey, event))
+ const putRequest = store.put(this.formatValue(key, event))
putRequest.onsuccess = () => {
transaction.commit()
resolve(event)
@@ -110,7 +120,7 @@ class IndexedDbService {
})
}
- async getReplaceableEvent(pubkey: string, kind: number): Promise {
+ async getReplaceableEvent(pubkey: string, kind: number, d?: string): Promise {
const storeName = this.getStoreNameByKind(kind)
if (!storeName) {
return Promise.reject('store name not found')
@@ -122,7 +132,8 @@ class IndexedDbService {
}
const transaction = this.db.transaction(storeName, 'readonly')
const store = transaction.objectStore(storeName)
- const request = store.get(pubkey)
+ const key = d === undefined ? pubkey : `${pubkey}:${d}`
+ const request = store.get(key)
request.onsuccess = () => {
transaction.commit()
@@ -298,6 +309,18 @@ class IndexedDbService {
})
}
+ private getReplaceableEventKey(event: Event): string {
+ if (
+ [kinds.Metadata, kinds.Contacts].includes(event.kind) ||
+ (event.kind >= 10000 && event.kind < 20000)
+ ) {
+ return event.pubkey
+ }
+
+ const [, d] = event.tags.find(tagNameEquals('d')) ?? []
+ return `${event.pubkey}:${d ?? ''}`
+ }
+
private getStoreNameByKind(kind: number): string | undefined {
switch (kind) {
case kinds.Metadata:
@@ -308,6 +331,10 @@ class IndexedDbService {
return StoreNames.FOLLOW_LIST_EVENTS
case kinds.Mutelist:
return StoreNames.MUTE_LIST_EVENTS
+ case kinds.Relaysets:
+ return StoreNames.RELAY_SETS
+ case ExtendedKind.FAVORITE_RELAYS:
+ return StoreNames.FAVORITE_RELAYS
default:
return undefined
}
diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts
index 3b079a1..9361ab9 100644
--- a/src/services/local-storage.service.ts
+++ b/src/services/local-storage.service.ts
@@ -4,41 +4,16 @@ import { randomString } from '@/lib/random'
import {
TAccount,
TAccountPointer,
- TFeedType,
+ TFeedInfo,
TNoteListMode,
TRelaySet,
TThemeSetting
} from '@/types'
-const DEFAULT_RELAY_SETS: TRelaySet[] = [
- {
- id: randomString(),
- name: 'Safer Global',
- relayUrls: ['wss://nostr.wine/', 'wss://pyramid.fiatjaf.com/']
- },
- {
- id: randomString(),
- name: 'Short Notes',
- relayUrls: ['wss://140.f7z.io/']
- },
- {
- id: randomString(),
- name: 'News',
- relayUrls: ['wss://news.utxo.one/']
- },
- {
- id: randomString(),
- name: 'Algo',
- relayUrls: ['wss://algo.utxo.one']
- }
-]
-
class LocalStorageService {
static instance: LocalStorageService
private relaySets: TRelaySet[] = []
- private activeRelaySetId: string | null = null
- private feedType: TFeedType = 'relays'
private themeSetting: TThemeSetting = 'system'
private accounts: TAccount[] = []
private currentAccount: TAccount | null = null
@@ -47,6 +22,7 @@ class LocalStorageService {
private defaultZapSats: number = 21
private defaultZapComment: string = 'Zap!'
private quickZap: boolean = false
+ private accountFeedInfoMap: Record = {}
constructor() {
if (!LocalStorageService.instance) {
@@ -63,12 +39,6 @@ class LocalStorageService {
this.accounts = accountsStr ? JSON.parse(accountsStr) : []
const currentAccountStr = window.localStorage.getItem(StorageKey.CURRENT_ACCOUNT)
this.currentAccount = currentAccountStr ? JSON.parse(currentAccountStr) : null
- const feedType = window.localStorage.getItem(StorageKey.FEED_TYPE)
- if (feedType && ['following', 'relays'].includes(feedType)) {
- this.feedType = feedType as 'following' | 'relays'
- } else {
- this.feedType = 'relays'
- }
const noteListModeStr = window.localStorage.getItem(StorageKey.NOTE_LIST_MODE)
this.noteListMode =
noteListModeStr && ['posts', 'postsAndReplies', 'pictures'].includes(noteListModeStr)
@@ -93,16 +63,12 @@ class LocalStorageService {
})
}
if (!relaySets.length) {
- relaySets = DEFAULT_RELAY_SETS
+ relaySets = []
}
- const activeRelaySetId = relaySets[0].id
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(relaySets))
- window.localStorage.setItem(StorageKey.ACTIVE_RELAY_SET_ID, activeRelaySetId)
this.relaySets = relaySets
- this.activeRelaySetId = activeRelaySetId
} else {
this.relaySets = JSON.parse(relaySetsStr)
- this.activeRelaySetId = window.localStorage.getItem(StorageKey.ACTIVE_RELAY_SET_ID) ?? null
}
const defaultZapSatsStr = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_SATS)
@@ -115,12 +81,18 @@ class LocalStorageService {
this.defaultZapComment = window.localStorage.getItem(StorageKey.DEFAULT_ZAP_COMMENT) ?? 'Zap!'
this.quickZap = window.localStorage.getItem(StorageKey.QUICK_ZAP) === 'true'
+ const accountFeedInfoMapStr =
+ window.localStorage.getItem(StorageKey.ACCOUNT_FEED_INFO_MAP) ?? '{}'
+ this.accountFeedInfoMap = JSON.parse(accountFeedInfoMapStr)
+
// Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_RELAY_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_LIST_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_MUTE_DECRYPTED_TAGS_MAP)
+ window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
+ window.localStorage.removeItem(StorageKey.FEED_TYPE)
}
getRelaySets() {
@@ -132,28 +104,6 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.RELAY_SETS, JSON.stringify(this.relaySets))
}
- getActiveRelaySetId() {
- return this.activeRelaySetId
- }
-
- setActiveRelaySetId(id: string | null) {
- this.activeRelaySetId = id
- if (id) {
- window.localStorage.setItem(StorageKey.ACTIVE_RELAY_SET_ID, id)
- } else {
- window.localStorage.removeItem(StorageKey.ACTIVE_RELAY_SET_ID)
- }
- }
-
- getFeedType() {
- return this.feedType
- }
-
- setFeedType(feedType: TFeedType) {
- this.feedType = feedType
- window.localStorage.setItem(StorageKey.FEED_TYPE, this.feedType)
- }
-
getThemeSetting() {
return this.themeSetting
}
@@ -260,6 +210,18 @@ class LocalStorageService {
JSON.stringify(this.lastReadNotificationTimeMap)
)
}
+
+ getFeedInfo(pubkey: string) {
+ return this.accountFeedInfoMap[pubkey]
+ }
+
+ setFeedInfo(info: TFeedInfo, pubkey?: string | null) {
+ this.accountFeedInfoMap[pubkey ?? 'default'] = info
+ window.localStorage.setItem(
+ StorageKey.ACCOUNT_FEED_INFO_MAP,
+ JSON.stringify(this.accountFeedInfoMap)
+ )
+ }
}
const instance = new LocalStorageService()
diff --git a/src/types.ts b/src/types.ts
index 14345a5..9d436df 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -94,7 +94,8 @@ export type TAccount = {
export type TAccountPointer = Pick
-export type TFeedType = 'following' | 'relays' | 'temporary'
+export type TFeedType = 'following' | 'relays' | 'relay' | 'temporary'
+export type TFeedInfo = { feedType: TFeedType; id?: string }
export type TLanguage = 'en' | 'zh' | 'pl'