Browse Source

make relay-blocking more persistent

imwald
Silberengel 5 months ago
parent
commit
94424c101c
  1. 6
      src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx
  2. 17
      src/components/FavoriteRelaysSetting/RelayItem.tsx
  3. 9
      src/components/FeedSwitcher/index.tsx
  4. 3
      src/i18n/locales/en.ts
  5. 16
      src/lib/event-metadata.ts
  6. 6
      src/providers/FavoriteRelaysProvider.tsx
  7. 34
      src/providers/FeedProvider.tsx
  8. 39
      src/providers/NostrProvider/index.tsx
  9. 8
      src/services/indexed-db.service.ts

6
src/components/FavoriteRelaysSetting/FavoriteRelayList.tsx

@ -20,7 +20,9 @@ import RelayItem from './RelayItem' @@ -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() { @@ -53,7 +55,7 @@ export default function FavoriteRelayList() {
<SortableContext items={favoriteRelays} strategy={verticalListSortingStrategy}>
<div className="grid gap-2">
{favoriteRelays.map((relay) => (
<RelayItem key={relay} relay={relay} />
<RelayItem key={relay} relay={relay} isBlocked={blockedRelays.includes(relay)} />
))}
</div>
</SortableContext>

17
src/components/FavoriteRelaysSetting/RelayItem.tsx

@ -3,10 +3,12 @@ import { useSecondaryPage } from '@/PageManager' @@ -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 }) { @@ -20,7 +22,7 @@ export default function RelayItem({ relay }: { relay: string }) {
return (
<div
className="relative group clickable flex gap-2 border rounded-lg p-2 pr-2.5 items-center justify-between select-none"
className={`relative group clickable flex gap-2 border rounded-lg p-2 pr-2.5 items-center justify-between select-none ${isBlocked ? 'opacity-60' : ''}`}
ref={setNodeRef}
style={style}
onClick={() => push(toRelay(relay))}
@ -33,9 +35,16 @@ export default function RelayItem({ relay }: { relay: string }) { @@ -33,9 +35,16 @@ export default function RelayItem({ relay }: { relay: string }) {
>
<GripVertical className="size-4 text-muted-foreground" />
</div>
<div className="flex gap-2 items-center flex-1">
<div className="flex gap-2 items-center flex-1 min-w-0">
<RelayIcon url={relay} />
<div className="flex-1 w-0 truncate font-semibold">{relay}</div>
<div className="flex items-center gap-2 flex-1 min-w-0">
<div className="flex-1 truncate font-semibold">{relay}</div>
{isBlocked && (
<span className="text-xs text-muted-foreground whitespace-nowrap">
({t('blocked')})
</span>
)}
</div>
</div>
</div>
<SaveRelayDropdownMenu urls={[relay]} />

9
src/components/FeedSwitcher/index.tsx

@ -12,9 +12,12 @@ import RelaySetCard from '../RelaySetCard' @@ -12,9 +12,12 @@ 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 (
<div className="space-y-2">
{pubkey && (
@ -53,7 +56,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) { @@ -53,7 +56,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
</FeedSwitcherItem>
)}
{favoriteRelays.length > 0 && (
{visibleRelays.length > 0 && (
<FeedSwitcherItem
isActive={feedInfo.feedType === 'all-favorites'}
onClick={() => {
@ -94,7 +97,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) { @@ -94,7 +97,7 @@ export default function FeedSwitcher({ close }: { close?: () => void }) {
}}
/>
))}
{favoriteRelays.map((relay) => (
{visibleRelays.map((relay) => (
<FeedSwitcherItem
key={relay}
isActive={feedInfo.feedType === 'relay' && feedInfo.id === relay}

3
src/i18n/locales/en.ts

@ -151,6 +151,9 @@ export default { @@ -151,6 +151,9 @@ export default {
Muted: 'Muted',
Unmute: 'Unmute',
'Unmute user': 'Unmute user',
Block: 'Block',
Unblock: 'Unblock',
blocked: 'blocked',
'Append n relays': 'Append {{n}} relays',
Append: 'Append',
'Select relays to append': 'Select relays to append',

16
src/lib/event-metadata.ts

@ -9,19 +9,26 @@ import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } fro @@ -9,19 +9,26 @@ import { generateBech32IdFromATag, generateBech32IdFromETag, tagNameEquals } fro
import { isWebsocketUrl, normalizeHttpUrl, normalizeUrl } from './url'
import { isTorBrowser } from './utils'
export function getRelayListFromEvent(event?: Event | null) {
export function getRelayListFromEvent(event?: Event | null, blockedRelays?: string[]) {
if (!event) {
return { write: BIG_RELAY_URLS, read: BIG_RELAY_URLS, originalRelays: [] }
}
const torBrowserDetected = isTorBrowser()
const relayList = { write: [], read: [], originalRelays: [] } as TRelayList
// Normalize blocked relays for comparison
const normalizedBlockedRelays = (blockedRelays || []).map(url => 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) { @@ -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) {

6
src/providers/FavoriteRelaysProvider.tsx

@ -92,6 +92,8 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -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 @@ -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

34
src/providers/FeedProvider.tsx

@ -31,7 +31,7 @@ export const useFeed = () => { @@ -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<string[]>([])
const [isReady, setIsReady] = useState(false)
const [feedInfo, setFeedInfo] = useState<TFeedInfo>({
@ -46,9 +46,11 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { @@ -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 }) { @@ -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 }) { @@ -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,
@ -107,6 +116,13 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { @@ -107,6 +116,13 @@ export function FeedProvider({ children }: { children: React.ReactNode }) {
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)
feedInfoRef.current = newFeedInfo
@ -132,7 +148,7 @@ export function FeedProvider({ children }: { children: React.ReactNode }) { @@ -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 }) { @@ -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

39
src/providers/NostrProvider/index.tsx

@ -232,6 +232,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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 }) { @@ -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) {

8
src/services/indexed-db.service.ts

@ -21,6 +21,7 @@ const StoreNames = { @@ -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 { @@ -43,7 +44,7 @@ class IndexedDbService {
init(): Promise<void> {
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 { @@ -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 { @@ -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:

Loading…
Cancel
Save