Browse Source

automatic refresh after writing to the relay feed

imwald
Silberengel 5 months ago
parent
commit
77e73d63b2
  1. 10
      src/components/Embedded/EmbeddedNote.tsx
  2. 37
      src/components/NormalFeed/index.tsx
  3. 7
      src/components/NoteOptions/useMenuActions.tsx
  4. 25
      src/components/PostEditor/PostContent.tsx
  5. 27
      src/components/PostEditor/PostRelaySelector.tsx
  6. 6
      src/components/QuoteList/index.tsx
  7. 24
      src/components/Relay/index.tsx
  8. 5
      src/components/ReplyNoteList/index.tsx
  9. 10
      src/components/SaveRelayDropdownMenu/index.tsx
  10. 7
      src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx
  11. 11
      src/pages/primary/DiscussionsPage/index.tsx
  12. 6
      src/pages/secondary/NoteListPage/index.tsx
  13. 10
      src/pages/secondary/NotePage/NotFound.tsx
  14. 6
      src/providers/FavoriteRelaysProvider.tsx
  15. 7
      src/providers/NostrProvider/index.tsx
  16. 33
      src/services/client.service.ts

10
src/components/Embedded/EmbeddedNote.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchEvent } from '@/hooks'
import { normalizeUrl } from '@/lib/url'
import { cn } from '@/lib/utils'
import client from '@/services/client.service'
import { useTranslation } from 'react-i18next'
@ -95,7 +96,7 @@ function EmbeddedNoteNotFound({ @@ -95,7 +96,7 @@ function EmbeddedNoteNotFound({
// Calculate which external relays would be tried
useEffect(() => {
const getExternalRelays = async () => {
const relays: string[] = []
let relays: string[] = []
if (!/^[0-9a-f]{64}$/.test(noteId)) {
try {
@ -112,6 +113,9 @@ function EmbeddedNoteNotFound({ @@ -112,6 +113,9 @@ function EmbeddedNoteNotFound({
const authorRelayList = await client.fetchRelayList(data.pubkey)
relays.push(...authorRelayList.write.slice(0, 6))
}
// Normalize and deduplicate relays
relays = relays.map(url => normalizeUrl(url) || url)
relays = Array.from(new Set(relays))
} catch (err) {
console.error('Failed to parse external relays:', err)
}
@ -120,7 +124,9 @@ function EmbeddedNoteNotFound({ @@ -120,7 +124,9 @@ function EmbeddedNoteNotFound({
const seenOn = client.getSeenEventRelayUrls(noteId)
relays.push(...seenOn)
setExternalRelays(Array.from(new Set(relays)))
// Normalize and deduplicate final relay list
const normalizedRelays = relays.map(url => normalizeUrl(url) || url)
setExternalRelays(Array.from(new Set(normalizedRelays)))
}
getExternalRelays()

37
src/components/NormalFeed/index.tsx

@ -5,39 +5,44 @@ import { useKindFilter } from '@/providers/KindFilterProvider' @@ -5,39 +5,44 @@ import { useKindFilter } from '@/providers/KindFilterProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import storage from '@/services/local-storage.service'
import { TFeedSubRequest, TNoteListMode } from '@/types'
import { useMemo, useRef, useState } from 'react'
import { forwardRef, useMemo, useRef, useState } from 'react'
import KindFilter from '../KindFilter'
import { RefreshButton } from '../RefreshButton'
export default function NormalFeed({
subRequests,
areAlgoRelays = false,
isMainFeed = false,
showRelayCloseReason = false
}: {
const NormalFeed = forwardRef<TNoteListRef, {
subRequests: TFeedSubRequest[]
areAlgoRelays?: boolean
isMainFeed?: boolean
showRelayCloseReason?: boolean
}) {
}>(function NormalFeed({
subRequests,
areAlgoRelays = false,
isMainFeed = false,
showRelayCloseReason = false
}, ref) {
const { hideUntrustedNotes } = useUserTrust()
const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
const supportTouch = useMemo(() => isTouchDevice(), [])
const noteListRef = useRef<TNoteListRef>(null)
const internalNoteListRef = useRef<TNoteListRef>(null)
const noteListRef = ref || internalNoteListRef
const handleListModeChange = (mode: TNoteListMode) => {
setListMode(mode)
if (isMainFeed) {
storage.setNoteListMode(mode)
}
noteListRef.current?.scrollToTop('smooth')
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.scrollToTop('smooth')
}
}
const handleShowKindsChange = (newShowKinds: number[]) => {
setTemporaryShowKinds(newShowKinds)
noteListRef.current?.scrollToTop()
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.scrollToTop()
}
}
return (
@ -53,7 +58,11 @@ export default function NormalFeed({ @@ -53,7 +58,11 @@ export default function NormalFeed({
}}
options={
<>
{!supportTouch && <RefreshButton onClick={() => noteListRef.current?.refresh()} />}
{!supportTouch && <RefreshButton onClick={() => {
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.refresh()
}
}} />}
<KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} />
</>
}
@ -69,4 +78,6 @@ export default function NormalFeed({ @@ -69,4 +78,6 @@ export default function NormalFeed({
/>
</>
)
}
})
export default NormalFeed

7
src/components/NoteOptions/useMenuActions.tsx

@ -2,7 +2,7 @@ import { ExtendedKind } from '@/constants' @@ -2,7 +2,7 @@ import { ExtendedKind } from '@/constants'
import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event'
import { toNjump } from '@/lib/link'
import { pubkeyToNpub } from '@/lib/pubkey'
import { simplifyUrl } from '@/lib/url'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useMuteList } from '@/providers/MuteListProvider'
@ -53,7 +53,10 @@ export function useMenuActions({ @@ -53,7 +53,10 @@ export function useMenuActions({
const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays()
const { relaySets, favoriteRelays } = useFavoriteRelays()
const relayUrls = useMemo(() => {
return Array.from(new Set(currentBrowsingRelayUrls.concat(favoriteRelays)))
return Array.from(new Set([
...currentBrowsingRelayUrls.map(url => normalizeUrl(url) || url),
...favoriteRelays.map(url => normalizeUrl(url) || url)
]))
}, [currentBrowsingRelayUrls, favoriteRelays])
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList()
const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event])

25
src/components/PostEditor/PostContent.tsx

@ -13,7 +13,9 @@ import { @@ -13,7 +13,9 @@ import {
import { ExtendedKind } from '@/constants'
import { isTouchDevice } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider'
import { useFeed } from '@/providers/FeedProvider'
import { useReply } from '@/providers/ReplyProvider'
import { normalizeUrl } from '@/lib/url'
import postEditorCache from '@/services/post-editor-cache.service'
import { TPollCreateData } from '@/types'
import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react'
@ -43,6 +45,7 @@ export default function PostContent({ @@ -43,6 +45,7 @@ export default function PostContent({
}) {
const { t } = useTranslation()
const { pubkey, publish, checkLogin } = useNostr()
const { feedInfo } = useFeed()
const { addReplies } = useReply()
const [text, setText] = useState('')
const textareaRef = useRef<TPostTextareaHandle>(null)
@ -59,6 +62,7 @@ export default function PostContent({ @@ -59,6 +62,7 @@ export default function PostContent({
const [publicMessageRecipients, setPublicMessageRecipients] = useState<string[]>([])
const [isProtectedEvent, setIsProtectedEvent] = useState(false)
const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([])
const [userWriteRelays, setUserWriteRelays] = useState<string[]>([])
const [isHighlight, setIsHighlight] = useState(false)
const [highlightData, setHighlightData] = useState<HighlightData>({
sourceType: 'nostr',
@ -243,12 +247,30 @@ export default function PostContent({ @@ -243,12 +247,30 @@ export default function PostContent({
// console.log('Publishing draft event:', draftEvent)
const newEvent = await publish(draftEvent, {
specifiedRelayUrls: isProtectedEvent ? additionalRelayUrls : undefined,
specifiedRelayUrls: isProtectedEvent ? additionalRelayUrls.filter(url => !userWriteRelays.includes(url)) : undefined,
additionalRelayUrls: isPoll ? pollCreateData.relays : additionalRelayUrls,
minPow
})
// console.log('Published event:', newEvent)
// Check if we need to refresh the current relay view
if (feedInfo.feedType === 'relay' && feedInfo.id) {
const currentRelayUrl = normalizeUrl(feedInfo.id)
const publishedRelays = isProtectedEvent
? additionalRelayUrls.filter(url => !userWriteRelays.includes(url))
: additionalRelayUrls
// If we published to the current relay being viewed, trigger a refresh after a short delay
if (publishedRelays.some(url => normalizeUrl(url) === currentRelayUrl)) {
setTimeout(() => {
// Trigger a page refresh by dispatching a custom event that the relay view can listen to
window.dispatchEvent(new CustomEvent('relay-refresh-needed', {
detail: { relayUrl: currentRelayUrl }
}))
}, 1000) // 1 second delay to allow the event to propagate
}
}
// Show publishing feedback
if ((newEvent as any).relayStatuses) {
showPublishingFeedback({
@ -470,6 +492,7 @@ export default function PostContent({ @@ -470,6 +492,7 @@ export default function PostContent({
<PostRelaySelector
setIsProtectedEvent={setIsProtectedEvent}
setAdditionalRelayUrls={setAdditionalRelayUrls}
setUserWriteRelays={setUserWriteRelays}
parentEvent={parentEvent}
openFrom={openFrom}
content={text}

27
src/components/PostEditor/PostRelaySelector.tsx

@ -25,12 +25,14 @@ export default function PostRelaySelector({ @@ -25,12 +25,14 @@ export default function PostRelaySelector({
openFrom,
setIsProtectedEvent,
setAdditionalRelayUrls,
setUserWriteRelays,
content: postContent = ''
}: {
parentEvent?: NostrEvent
openFrom?: string[]
setIsProtectedEvent: Dispatch<SetStateAction<boolean>>
setAdditionalRelayUrls: Dispatch<SetStateAction<string[]>>
setUserWriteRelays?: Dispatch<SetStateAction<string[]>>
content?: string
}) {
const { t } = useTranslation()
@ -52,13 +54,15 @@ export default function PostRelaySelector({ @@ -52,13 +54,15 @@ export default function PostRelaySelector({
// Get all selectable relays (write relays + favorite relays + relays from relay sets + mention relays)
const selectableRelays = useMemo(() => {
const allRelays = Array.from(new Set([
...relayUrls,
...favoriteRelays,
...relaySets.flatMap(set => set.relayUrls),
...mentionRelays
]))
return allRelays
// Normalize all relay URLs before combining them
const normalizedRelays = [
...relayUrls.map(url => normalizeUrl(url) || url),
...favoriteRelays.map(url => normalizeUrl(url) || url),
...relaySets.flatMap(set => set.relayUrls.map(url => normalizeUrl(url) || url)),
...mentionRelays.map(url => normalizeUrl(url) || url)
].filter(Boolean) // Remove any null/undefined values
return Array.from(new Set(normalizedRelays))
}, [relayUrls, favoriteRelays, relaySets, mentionRelays])
const description = useMemo(() => {
@ -171,7 +175,10 @@ export default function PostRelaySelector({ @@ -171,7 +175,10 @@ export default function PostRelaySelector({
// Default to write relays + mention relays for regular replies, or just write relays for other cases
if (isRegularReply) {
// For regular replies, include write relays and mention relays
const defaultRelays = Array.from(new Set([...relayUrls, ...mentionRelays]))
// Normalize URLs before combining to avoid duplicates with/without trailing slashes
const normalizedWriteRelays = relayUrls.map(url => normalizeUrl(url) || url)
const normalizedMentionRelays = mentionRelays.map(url => normalizeUrl(url) || url)
const defaultRelays = Array.from(new Set([...normalizedWriteRelays, ...normalizedMentionRelays]))
console.log('PostRelaySelector: Setting default relays for regular reply:', {
relayUrls,
mentionRelays,
@ -191,7 +198,9 @@ export default function PostRelaySelector({ @@ -191,7 +198,9 @@ export default function PostRelaySelector({
const isProtectedEvent = selectedRelayUrls.length > 0 && !selectedRelayUrls.some(url => relayUrls.includes(url))
setIsProtectedEvent(isProtectedEvent)
setAdditionalRelayUrls(selectedRelayUrls)
}, [selectedRelayUrls, relayUrls, setIsProtectedEvent, setAdditionalRelayUrls])
// Expose user's write relays to parent component
setUserWriteRelays?.(relayUrls)
}, [selectedRelayUrls, relayUrls, setIsProtectedEvent, setAdditionalRelayUrls, setUserWriteRelays])
const handleRelayCheckedChange = useCallback((checked: boolean, url: string) => {
if (checked) {

6
src/components/QuoteList/index.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { FAST_READ_RELAY_URLS, ExtendedKind } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { normalizeUrl } from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service'
@ -31,7 +32,10 @@ export default function QuoteList({ event, className }: { event: Event; classNam @@ -31,7 +32,10 @@ export default function QuoteList({ event, className }: { event: Event; classNam
// Privacy: Only use user's own relays + defaults, never connect to other users' relays
const userRelays = userRelayList?.read || []
const finalRelayUrls = Array.from(new Set(userRelays.concat(FAST_READ_RELAY_URLS)))
const finalRelayUrls = Array.from(new Set([
...userRelays.map(url => normalizeUrl(url) || url),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
const { closer, timelineKey } = await client.subscribeTimeline(
[

24
src/components/Relay/index.tsx

@ -4,8 +4,9 @@ import SearchInput from '@/components/SearchInput' @@ -4,8 +4,9 @@ import SearchInput from '@/components/SearchInput'
import { useFetchRelayInfo } from '@/hooks'
import { normalizeUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useEffect, useMemo, useState } from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { TNoteListRef } from '@/components/NoteList'
import NotFound from '../NotFound'
export default function Relay({ url, className }: { url?: string; className?: string }) {
@ -15,6 +16,7 @@ export default function Relay({ url, className }: { url?: string; className?: st @@ -15,6 +16,7 @@ export default function Relay({ url, className }: { url?: string; className?: st
const { relayInfo } = useFetchRelayInfo(normalizedUrl)
const [searchInput, setSearchInput] = useState('')
const [debouncedInput, setDebouncedInput] = useState(searchInput)
const noteListRef = useRef<TNoteListRef>(null)
useEffect(() => {
if (normalizedUrl) {
@ -35,6 +37,25 @@ export default function Relay({ url, className }: { url?: string; className?: st @@ -35,6 +37,25 @@ export default function Relay({ url, className }: { url?: string; className?: st
}
}, [searchInput])
// Listen for refresh events when user publishes to this relay
useEffect(() => {
if (!normalizedUrl) return
const handleRelayRefresh = (event: CustomEvent) => {
const { relayUrl } = event.detail
if (normalizeUrl(relayUrl) === normalizedUrl) {
// Trigger a refresh of the note list
noteListRef.current?.refresh()
}
}
window.addEventListener('relay-refresh-needed', handleRelayRefresh as EventListener)
return () => {
window.removeEventListener('relay-refresh-needed', handleRelayRefresh as EventListener)
}
}, [normalizedUrl])
if (!normalizedUrl) {
return <NotFound />
}
@ -52,6 +73,7 @@ export default function Relay({ url, className }: { url?: string; className?: st @@ -52,6 +73,7 @@ export default function Relay({ url, className }: { url?: string; className?: st
</div>
)}
<NormalFeed
ref={noteListRef}
subRequests={[
{ urls: [normalizedUrl], filter: debouncedInput ? { search: debouncedInput } : {} }
]}

5
src/components/ReplyNoteList/index.tsx

@ -11,6 +11,7 @@ import { @@ -11,6 +11,7 @@ import {
} from '@/lib/event'
import { toNote } from '@/lib/link'
import { generateBech32IdFromETag, tagNameEquals } from '@/lib/tag'
import { normalizeUrl } from '@/lib/url'
import { useSecondaryPage } from '@/PageManager'
import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useMuteList } from '@/providers/MuteListProvider'
@ -225,8 +226,8 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even @@ -225,8 +226,8 @@ function ReplyNoteList({ index, event, sort = 'oldest' }: { index?: number; even
// Privacy: Only use user's own relays + defaults, never connect to other users' relays
const userRelays = userRelayList?.read || []
const finalRelayUrls = Array.from(new Set([
...FAST_READ_RELAY_URLS, // Fast, well-connected relays
...userRelays // User's mailbox relays
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url), // Fast, well-connected relays
...userRelays.map(url => normalizeUrl(url) || url) // User's mailbox relays
]))
const filters: (Omit<Filter, 'since' | 'until'> & {

10
src/components/SaveRelayDropdownMenu/index.tsx

@ -134,7 +134,10 @@ function RelayItem({ urls }: { urls: string[] }) { @@ -134,7 +134,10 @@ function RelayItem({ urls }: { urls: string[] }) {
if (isSmallScreen) {
return (
<DrawerMenuItem onClick={handleClick} disabled={isLoading}>
<DrawerMenuItem
onClick={isLoading ? undefined : handleClick}
className={isLoading ? 'opacity-50 cursor-not-allowed' : ''}
>
{isLoading ? '...' : (saved ? <Check /> : <Plus />)}
{isLoading ? t('Loading...') : (saved ? t('Unfavorite') : t('Favorite'))}
</DrawerMenuItem>
@ -168,7 +171,10 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) { @@ -168,7 +171,10 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
} else {
updateRelaySet({
...set,
relayUrls: Array.from(new Set([...set.relayUrls, ...urls]))
relayUrls: Array.from(new Set([
...set.relayUrls.map(url => normalizeUrl(url) || url),
...urls.map(url => normalizeUrl(url) || url)
]))
})
}
}

7
src/pages/primary/DiscussionsPage/CreateThreadDialog.tsx

@ -17,7 +17,7 @@ import { TDraftEvent, TRelaySet } from '@/types' @@ -17,7 +17,7 @@ import { TDraftEvent, TRelaySet } from '@/types'
import { NostrEvent } from 'nostr-tools'
import { prefixNostrAddresses } from '@/lib/nostr-address'
import { showPublishingError } from '@/lib/publishing-feedback'
import { simplifyUrl } from '@/lib/url'
import { normalizeUrl, simplifyUrl } from '@/lib/url'
import dayjs from 'dayjs'
import { extractHashtagsFromContent, normalizeTopic } from '@/lib/discussion-topics'
import DiscussionContent from '@/components/Note/DiscussionContent'
@ -105,7 +105,10 @@ export default function CreateThreadDialog({ @@ -105,7 +105,10 @@ export default function CreateThreadDialog({
const relaySet = initialRelay ? relaySets.find(set => set.id === initialRelay) : null
if (relaySet) {
// Include relays from the selected set along with available relays
return Array.from(new Set([...availableRelays, ...relaySet.relayUrls]))
return Array.from(new Set([
...availableRelays.map(url => normalizeUrl(url) || url),
...relaySet.relayUrls.map(url => normalizeUrl(url) || url)
]))
}
return availableRelays
}, [availableRelays, relaySets, initialRelay])

11
src/pages/primary/DiscussionsPage/index.tsx

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button' @@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import { forwardRef, useEffect, useState, useCallback, useMemo } from 'react'
@ -166,12 +167,12 @@ const DiscussionsPage = forwardRef((_, ref) => { @@ -166,12 +167,12 @@ const DiscussionsPage = forwardRef((_, ref) => {
}
}
// Deduplicate and combine all relays: favorite relays, user write relays, stored relay sets, and fast read relays
// Normalize and deduplicate all relays: favorite relays, user write relays, stored relay sets, and fast read relays
const allRelays = Array.from(new Set([
...availableRelays,
...userWriteRelays,
...storedRelaySetRelays,
...FAST_READ_RELAY_URLS
...availableRelays.map(url => normalizeUrl(url) || url),
...userWriteRelays.map(url => normalizeUrl(url) || url),
...storedRelaySetRelays.map(url => normalizeUrl(url) || url),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
setRelayUrls(allRelays)

6
src/pages/secondary/NoteListPage/index.tsx

@ -2,6 +2,7 @@ import { Favicon } from '@/components/Favicon' @@ -2,6 +2,7 @@ import { Favicon } from '@/components/Favicon'
import NormalFeed from '@/components/NormalFeed'
import { Button } from '@/components/ui/button'
import { BIG_RELAY_URLS, SEARCHABLE_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { toProfileList } from '@/lib/link'
import { fetchPubkeysFromDomain, getWellKnownNip05Url } from '@/lib/nip05'
@ -71,7 +72,10 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => { @@ -71,7 +72,10 @@ const NoteListPage = forwardRef(({ index }: { index?: number }, ref) => {
setSubRequests([
{
filter: { '#I': [externalContentId], ...(kinds.length > 0 ? { kinds } : {}) },
urls: BIG_RELAY_URLS.concat(relayList?.write || [])
urls: Array.from(new Set([
...BIG_RELAY_URLS.map(url => normalizeUrl(url) || url),
...(relayList?.write || []).map(url => normalizeUrl(url) || url)
]))
}
])
return

10
src/pages/secondary/NotePage/NotFound.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import ClientSelect from '@/components/ClientSelect'
import { Button } from '@/components/ui/button'
import { normalizeUrl } from '@/lib/url'
import client from '@/services/client.service'
import { AlertCircle, Search } from 'lucide-react'
import { nip19 } from 'nostr-tools'
@ -26,7 +27,7 @@ export default function NotFound({ @@ -26,7 +27,7 @@ export default function NotFound({
// Get all relays that would be tried in tiers 1-3 (already tried)
const alreadyTriedRelays = await client.getAlreadyTriedRelays(bech32Id)
const externalRelays: string[] = []
let externalRelays: string[] = []
// Parse relay hints and author from bech32 ID
if (!/^[0-9a-f]{64}$/.test(bech32Id)) {
@ -44,6 +45,9 @@ export default function NotFound({ @@ -44,6 +45,9 @@ export default function NotFound({
const authorRelayList = await client.fetchRelayList(data.pubkey)
externalRelays.push(...authorRelayList.write.slice(0, 6))
}
// Normalize and deduplicate external relays
externalRelays = externalRelays.map(url => normalizeUrl(url) || url)
externalRelays = Array.from(new Set(externalRelays))
} catch (err) {
console.error('Failed to parse external relays:', err)
}
@ -55,7 +59,9 @@ export default function NotFound({ @@ -55,7 +59,9 @@ export default function NotFound({
// Filter out relays that were already tried in tiers 1-3
const newRelays = externalRelays.filter(relay => !alreadyTriedRelays.includes(relay))
setExternalRelays(Array.from(new Set(newRelays)))
// Normalize and deduplicate final relay list
const normalizedRelays = newRelays.map(url => normalizeUrl(url) || url)
setExternalRelays(Array.from(new Set(normalizedRelays)))
}
getExternalRelays()

6
src/providers/FavoriteRelaysProvider.tsx

@ -99,8 +99,12 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode @@ -99,8 +99,12 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
)
setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[])
const normalizedRelays = [
...(relayList?.write ?? []).map(url => normalizeUrl(url) || url),
...BIG_RELAY_URLS.map(url => normalizeUrl(url) || url)
]
const newRelaySetEvents = await client.fetchEvents(
(relayList?.write ?? []).concat(BIG_RELAY_URLS).slice(0, 5),
Array.from(new Set(normalizedRelays)).slice(0, 5),
{
kinds: [kinds.Relaysets],
authors: [pubkey],

7
src/providers/NostrProvider/index.tsx

@ -13,6 +13,7 @@ import { @@ -13,6 +13,7 @@ import {
minePow
} from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { normalizeUrl } from '@/lib/url'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import client from '@/services/client.service'
@ -274,7 +275,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -274,7 +275,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
setRelayList(relayList)
const fetchRelays = relayList.write.concat(BIG_RELAY_URLS).slice(0, 4)
const normalizedRelays = [
...relayList.write.map(url => normalizeUrl(url) || url),
...BIG_RELAY_URLS.map(url => normalizeUrl(url) || url)
]
const fetchRelays = Array.from(new Set(normalizedRelays)).slice(0, 4)
const events = await client.fetchEvents(fetchRelays, [
{
kinds: [

33
src/services/client.service.ts

@ -144,7 +144,10 @@ class ClientService extends EventTarget { @@ -144,7 +144,10 @@ class ClientService extends EventTarget {
const relayList = this.pubkey ? await this.fetchRelayList(this.pubkey) : { write: [], read: [] }
const senderWriteRelays = relayList?.write.slice(0, 6) ?? []
const recipientReadRelays = Array.from(new Set(_additionalRelayUrls))
relays = senderWriteRelays.concat(recipientReadRelays)
// Normalize and deduplicate the combined relay list
const normalizedSenderRelays = senderWriteRelays.map(url => normalizeUrl(url) || url)
const normalizedRecipientRelays = recipientReadRelays.map(url => normalizeUrl(url) || url)
relays = Array.from(new Set(normalizedSenderRelays.concat(normalizedRecipientRelays)))
}
if (!relays.length) {
@ -1085,20 +1088,20 @@ class ClientService extends EventTarget { @@ -1085,20 +1088,20 @@ class ClientService extends EventTarget {
// Tier 1: User's read relays + fast read relays
const tier1Relays = Array.from(new Set([
...userRelayList.read,
...FAST_READ_RELAY_URLS
...userRelayList.read.map(url => normalizeUrl(url) || url),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
// Tier 2: User's write relays + fast write relays
const tier2Relays = Array.from(new Set([
...userRelayList.write,
...FAST_WRITE_RELAY_URLS
...userRelayList.write.map(url => normalizeUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
// Tier 3: Search relays + big relays
const tier3Relays = Array.from(new Set([
...SEARCHABLE_RELAY_URLS,
...BIG_RELAY_URLS
...SEARCHABLE_RELAY_URLS.map(url => normalizeUrl(url) || url),
...BIG_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
return Array.from(new Set([
@ -1148,7 +1151,9 @@ class ClientService extends EventTarget { @@ -1148,7 +1151,9 @@ class ClientService extends EventTarget {
const seenOn = this.getSeenEventRelayUrls(id)
externalRelays.push(...seenOn)
const uniqueExternalRelays = Array.from(new Set(externalRelays))
// Normalize and deduplicate the combined external relays
const normalizedExternalRelays = externalRelays.map(url => normalizeUrl(url) || url)
const uniqueExternalRelays = Array.from(new Set(normalizedExternalRelays))
if (uniqueExternalRelays.length === 0) {
return undefined
@ -1209,24 +1214,24 @@ class ClientService extends EventTarget { @@ -1209,24 +1214,24 @@ class ClientService extends EventTarget {
// Tier 1: User's read relays + fast read relays (deduplicated)
const tier1Relays = Array.from(new Set([
...userRelayList.read,
...FAST_READ_RELAY_URLS
...userRelayList.read.map(url => normalizeUrl(url) || url),
...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
const tier1Event = await this.tryHarderToFetchEvent(tier1Relays, filter)
if (tier1Event) { return tier1Event }
// Tier 2: User's write relays + fast write relays (deduplicated)
const tier2Relays = Array.from(new Set([
...userRelayList.write,
...FAST_WRITE_RELAY_URLS
...userRelayList.write.map(url => normalizeUrl(url) || url),
...FAST_WRITE_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
const tier2Event = await this.tryHarderToFetchEvent(tier2Relays, filter)
if (tier2Event) { return tier2Event }
// Tier 3: Search relays + big relays (deduplicated)
const tier3Relays = Array.from(new Set([
...SEARCHABLE_RELAY_URLS,
...BIG_RELAY_URLS
...SEARCHABLE_RELAY_URLS.map(url => normalizeUrl(url) || url),
...BIG_RELAY_URLS.map(url => normalizeUrl(url) || url)
]))
const tier3Event = await this.tryHarderToFetchEvent(tier3Relays, filter)
if (tier3Event) { return tier3Event }

Loading…
Cancel
Save