diff --git a/src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx b/src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
index 16a94a0..edbd25c 100644
--- a/src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
+++ b/src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
@@ -20,7 +20,9 @@ import RelayItem from './RelayItem'
export default function FavoriteRelayList() {
const { t } = useTranslation()
- const { favoriteRelays, reorderFavoriteRelays } = useFavoriteRelays()
+ const { favoriteRelays, blockedRelays, reorderFavoriteRelays } = useFavoriteRelays()
+
+ // Show all relays including blocked ones (they'll be marked visually)
const sensors = useSensors(
useSensor(PointerSensor),
@@ -53,7 +55,7 @@ export default function FavoriteRelayList() {
{favoriteRelays.map((relay) => (
-
+
))}
diff --git a/src/components/FavoriteRelaysSetting/RelayItem.tsx b/src/components/FavoriteRelaysSetting/RelayItem.tsx
index ae57e25..9a01fed 100644
--- a/src/components/FavoriteRelaysSetting/RelayItem.tsx
+++ b/src/components/FavoriteRelaysSetting/RelayItem.tsx
@@ -3,10 +3,12 @@ import { useSecondaryPage } from '@/PageManager'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { GripVertical } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
import RelayIcon from '../RelayIcon'
import SaveRelayDropdownMenu from '../SaveRelayDropdownMenu'
-export default function RelayItem({ relay }: { relay: string }) {
+export default function RelayItem({ relay, isBlocked = false }: { relay: string; isBlocked?: boolean }) {
+ const { t } = useTranslation()
const { push } = useSecondaryPage()
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: relay
@@ -20,7 +22,7 @@ export default function RelayItem({ relay }: { relay: string }) {
return (
push(toRelay(relay))}
@@ -33,9 +35,16 @@ export default function RelayItem({ relay }: { relay: string }) {
>
-
+
-
{relay}
+
+
{relay}
+ {isBlocked && (
+
+ ({t('blocked')})
+
+ )}
+
diff --git a/src/components/FeedSwitcher/index.tsx b/src/components/FeedSwitcher/index.tsx
index d3d3736..3379851 100644
--- a/src/components/FeedSwitcher/index.tsx
+++ b/src/components/FeedSwitcher/index.tsx
@@ -12,8 +12,11 @@ import RelaySetCard from '../RelaySetCard'
export default function FeedSwitcher({ close }: { close?: () => void }) {
const { t } = useTranslation()
const { pubkey } = useNostr()
- const { relaySets, favoriteRelays } = useFavoriteRelays()
+ const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays()
const { feedInfo, switchFeed } = useFeed()
+
+ // Filter out blocked relays for display
+ const visibleRelays = favoriteRelays.filter(relay => !blockedRelays.includes(relay))
return (
@@ -53,7 +56,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
)}
- {favoriteRelays.length > 0 && (
+ {visibleRelays.length > 0 && (
{
@@ -94,7 +97,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
}}
/>
))}
- {favoriteRelays.map((relay) => (
+ {visibleRelays.map((relay) => (
normalizeUrl(url) || url)
+
event.tags.filter(tagNameEquals('r')).forEach(([, url, type]) => {
if (!url || !isWebsocketUrl(url)) return
const normalizedUrl = normalizeUrl(url)
if (!normalizedUrl) return
+
+ // Filter out blocked relays
+ if (normalizedBlockedRelays.includes(normalizedUrl)) return
const scope = type === 'read' ? 'read' : type === 'write' ? 'write' : 'both'
relayList.originalRelays.push({ url: normalizedUrl, scope })
@@ -79,13 +86,18 @@ export function getProfileFromEvent(event: Event) {
}
}
-export function getRelaySetFromEvent(event: Event): TRelaySet {
+export function getRelaySetFromEvent(event: Event, blockedRelays?: string[]): TRelaySet {
const id = getReplaceableEventIdentifier(event)
+
+ // Normalize blocked relays for comparison
+ const normalizedBlockedRelays = (blockedRelays || []).map(url => normalizeUrl(url) || url)
+
const relayUrls = event.tags
.filter(tagNameEquals('relay'))
.map((tag) => tag[1])
.filter((url) => url && isWebsocketUrl(url))
.map((url) => normalizeUrl(url))
+ .filter((url) => !normalizedBlockedRelays.includes(url)) // Filter out blocked relays
let name = event.tags.find(tagNameEquals('title'))?.[1]
if (!name) {
diff --git a/src/providers/FavoriteRelaysProvider.tsx b/src/providers/FavoriteRelaysProvider.tsx
index 16e110c..cf82b94 100644
--- a/src/providers/FavoriteRelaysProvider.tsx
+++ b/src/providers/FavoriteRelaysProvider.tsx
@@ -92,6 +92,8 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
}
})
+ // 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) {
@@ -164,9 +166,9 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
useEffect(() => {
setRelaySets(
- relaySetEvents.map((evt) => getRelaySetFromEvent(evt)).filter(Boolean) as TRelaySet[]
+ relaySetEvents.map((evt) => getRelaySetFromEvent(evt, blockedRelays)).filter(Boolean) as TRelaySet[]
)
- }, [relaySetEvents])
+ }, [relaySetEvents, blockedRelays])
const addFavoriteRelays = async (relayUrls: string[]) => {
const normalizedUrls = relayUrls
diff --git a/src/providers/FeedProvider.tsx b/src/providers/FeedProvider.tsx
index 0df5796..89038eb 100644
--- a/src/providers/FeedProvider.tsx
+++ b/src/providers/FeedProvider.tsx
@@ -31,7 +31,7 @@ export const useFeed = () => {
export function FeedProvider({ children }: { children: React.ReactNode }) {
const { pubkey, isInitialized } = useNostr()
- const { relaySets, favoriteRelays } = useFavoriteRelays()
+ const { relaySets, favoriteRelays, blockedRelays } = useFavoriteRelays()
const [relayUrls, setRelayUrls] = useState([])
const [isReady, setIsReady] = useState(false)
const [feedInfo, setFeedInfo] = useState({
@@ -46,9 +46,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
return
}
+ // Get first visible (non-blocked) favorite relay as default
+ const visibleRelays = favoriteRelays.filter(relay => !blockedRelays.includes(relay))
let feedInfo: TFeedInfo = {
feedType: 'relay',
- id: favoriteRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0]
+ id: visibleRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0]
}
if (pubkey) {
const storedFeedInfo = storage.getFeedInfo(pubkey)
@@ -62,6 +64,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
}
if (feedInfo.feedType === 'relay') {
+ // Check if the stored relay is blocked, if so use first visible relay instead
+ if (feedInfo.id && blockedRelays.includes(feedInfo.id)) {
+ console.log('Stored relay is blocked, using first visible relay instead')
+ feedInfo.id = visibleRelays[0] ?? DEFAULT_FAVORITE_RELAYS[0]
+ }
return await switchFeed('relay', { relay: feedInfo.id })
}
@@ -86,10 +93,12 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
// Update relay URLs when favoriteRelays change and we're in all-favorites mode
useEffect(() => {
if (feedInfo.feedType === 'all-favorites') {
- console.log('Updating relay URLs for all-favorites:', favoriteRelays)
- setRelayUrls(favoriteRelays)
+ // Filter out blocked relays
+ const visibleRelays = favoriteRelays.filter(relay => !blockedRelays.includes(relay))
+ console.log('Updating relay URLs for all-favorites:', visibleRelays)
+ setRelayUrls(visibleRelays)
}
- }, [favoriteRelays, feedInfo.feedType])
+ }, [favoriteRelays, blockedRelays, feedInfo.feedType])
const switchFeed = async (
feedType: TFeedType,
@@ -106,6 +115,13 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
setIsReady(true)
return
}
+
+ // Don't allow selecting a blocked relay as feed
+ if (blockedRelays.includes(normalizedUrl)) {
+ console.warn('Cannot select blocked relay as feed:', normalizedUrl)
+ setIsReady(true)
+ return
+ }
const newFeedInfo = { feedType, id: normalizedUrl }
setFeedInfo(newFeedInfo)
@@ -132,7 +148,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
relaySetId
)
if (storedRelaySetEvent) {
- relaySet = getRelaySetFromEvent(storedRelaySetEvent)
+ relaySet = getRelaySetFromEvent(storedRelaySetEvent, blockedRelays)
}
}
if (relaySet) {
@@ -161,11 +177,13 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
return
}
if (feedType === 'all-favorites') {
- console.log('Switching to all-favorites, favoriteRelays:', favoriteRelays)
+ // Filter out blocked relays
+ const visibleRelays = favoriteRelays.filter(relay => !blockedRelays.includes(relay))
+ console.log('Switching to all-favorites, favoriteRelays:', visibleRelays)
const newFeedInfo = { feedType }
setFeedInfo(newFeedInfo)
feedInfoRef.current = newFeedInfo
- setRelayUrls(favoriteRelays)
+ setRelayUrls(visibleRelays)
storage.setFeedInfo(newFeedInfo, pubkey)
setIsReady(true)
return
diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx
index ac4ee7b..0700875 100644
--- a/src/providers/NostrProvider/index.tsx
+++ b/src/providers/NostrProvider/index.tsx
@@ -232,6 +232,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
storedMuteListEvent,
storedBookmarkListEvent,
storedFavoriteRelaysEvent,
+ storedBlockedRelaysEvent,
storedUserEmojiListEvent
] = await Promise.all([
indexedDb.getReplaceableEvent(account.pubkey, kinds.RelayList),
@@ -240,10 +241,26 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
indexedDb.getReplaceableEvent(account.pubkey, kinds.Mutelist),
indexedDb.getReplaceableEvent(account.pubkey, kinds.BookmarkList),
indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.FAVORITE_RELAYS),
+ indexedDb.getReplaceableEvent(account.pubkey, ExtendedKind.BLOCKED_RELAYS),
indexedDb.getReplaceableEvent(account.pubkey, kinds.UserEmojiList)
])
+
+ // Extract blocked relays from event
+ const blockedRelays: string[] = []
+ if (storedBlockedRelaysEvent) {
+ storedBlockedRelaysEvent.tags.forEach(([tagName, tagValue]) => {
+ if (tagName === 'relay' && tagValue) {
+ const normalizedUrl = normalizeUrl(tagValue)
+ if (normalizedUrl && !blockedRelays.includes(normalizedUrl)) {
+ blockedRelays.push(normalizedUrl)
+ }
+ }
+ })
+ setBlockedRelaysEvent(storedBlockedRelaysEvent)
+ }
+
if (storedRelayListEvent) {
- setRelayList(getRelayListFromEvent(storedRelayListEvent))
+ setRelayList(getRelayListFromEvent(storedRelayListEvent, blockedRelays))
}
if (storedProfileEvent) {
setProfileEvent(storedProfileEvent)
@@ -270,7 +287,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
authors: [account.pubkey]
})
const relayListEvent = getLatestEvent(relayListEvents) ?? storedRelayListEvent
- const relayList = getRelayListFromEvent(relayListEvent)
+ const relayList = getRelayListFromEvent(relayListEvent, blockedRelays)
if (relayListEvent) {
client.updateRelayListCache(relayListEvent)
await indexedDb.putReplaceableEvent(relayListEvent)
@@ -294,6 +311,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
kinds.BookmarkList,
10015, // Interest list
ExtendedKind.FAVORITE_RELAYS,
+ ExtendedKind.BLOCKED_RELAYS,
ExtendedKind.BLOSSOM_SERVER_LIST,
kinds.UserEmojiList
],
@@ -369,6 +387,23 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const updatedBlockedRelaysEvent = await indexedDb.putReplaceableEvent(blockedRelaysEvent)
if (updatedBlockedRelaysEvent.id === blockedRelaysEvent.id) {
setBlockedRelaysEvent(updatedBlockedRelaysEvent)
+
+ // Update blockedRelays array and re-filter relay list
+ const newBlockedRelays: string[] = []
+ updatedBlockedRelaysEvent.tags.forEach(([tagName, tagValue]) => {
+ if (tagName === 'relay' && tagValue) {
+ const normalizedUrl = normalizeUrl(tagValue)
+ if (normalizedUrl && !newBlockedRelays.includes(normalizedUrl)) {
+ newBlockedRelays.push(normalizedUrl)
+ }
+ }
+ })
+
+ // Re-filter relay list with updated blocked relays
+ if (relayListEvent) {
+ const updatedRelayList = getRelayListFromEvent(relayListEvent, newBlockedRelays)
+ setRelayList(updatedRelayList)
+ }
}
}
if (blossomServerListEvent) {
diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts
index 72c6a3e..262da29 100644
--- a/src/services/indexed-db.service.ts
+++ b/src/services/indexed-db.service.ts
@@ -21,6 +21,7 @@ const StoreNames = {
USER_EMOJI_LIST_EVENTS: 'userEmojiListEvents',
EMOJI_SET_EVENTS: 'emojiSetEvents',
FAVORITE_RELAYS: 'favoriteRelays',
+ BLOCKED_RELAYS_EVENTS: 'blockedRelaysEvents',
RELAY_SETS: 'relaySets',
FOLLOWING_FAVORITE_RELAYS: 'followingFavoriteRelays',
RELAY_INFOS: 'relayInfos',
@@ -43,7 +44,7 @@ class IndexedDbService {
init(): Promise {
if (!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => {
- const request = window.indexedDB.open('jumble', 9)
+ const request = window.indexedDB.open('jumble', 10)
request.onerror = (event) => {
reject(event)
@@ -80,6 +81,9 @@ class IndexedDbService {
if (!db.objectStoreNames.contains(StoreNames.FAVORITE_RELAYS)) {
db.createObjectStore(StoreNames.FAVORITE_RELAYS, { keyPath: 'key' })
}
+ if (!db.objectStoreNames.contains(StoreNames.BLOCKED_RELAYS_EVENTS)) {
+ db.createObjectStore(StoreNames.BLOCKED_RELAYS_EVENTS, { keyPath: 'key' })
+ }
if (!db.objectStoreNames.contains(StoreNames.RELAY_SETS)) {
db.createObjectStore(StoreNames.RELAY_SETS, { keyPath: 'key' })
}
@@ -461,6 +465,8 @@ class IndexedDbService {
return StoreNames.RELAY_SETS
case ExtendedKind.FAVORITE_RELAYS:
return StoreNames.FAVORITE_RELAYS
+ case ExtendedKind.BLOCKED_RELAYS:
+ return StoreNames.BLOCKED_RELAYS_EVENTS
case kinds.BookmarkList:
return StoreNames.BOOKMARK_LIST_EVENTS
case kinds.UserEmojiList: