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: