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

33
src/components/NormalFeed/index.tsx

@ -5,40 +5,45 @@ import { useKindFilter } from '@/providers/KindFilterProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { TFeedSubRequest, TNoteListMode } from '@/types' import { TFeedSubRequest, TNoteListMode } from '@/types'
import { useMemo, useRef, useState } from 'react' import { forwardRef, useMemo, useRef, useState } from 'react'
import KindFilter from '../KindFilter' import KindFilter from '../KindFilter'
import { RefreshButton } from '../RefreshButton' import { RefreshButton } from '../RefreshButton'
export default function NormalFeed({ const NormalFeed = forwardRef<TNoteListRef, {
subRequests,
areAlgoRelays = false,
isMainFeed = false,
showRelayCloseReason = false
}: {
subRequests: TFeedSubRequest[] subRequests: TFeedSubRequest[]
areAlgoRelays?: boolean areAlgoRelays?: boolean
isMainFeed?: boolean isMainFeed?: boolean
showRelayCloseReason?: boolean showRelayCloseReason?: boolean
}) { }>(function NormalFeed({
subRequests,
areAlgoRelays = false,
isMainFeed = false,
showRelayCloseReason = false
}, ref) {
const { hideUntrustedNotes } = useUserTrust() const { hideUntrustedNotes } = useUserTrust()
const { showKinds } = useKindFilter() const { showKinds } = useKindFilter()
const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds) const [temporaryShowKinds, setTemporaryShowKinds] = useState(showKinds)
const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode()) const [listMode, setListMode] = useState<TNoteListMode>(() => storage.getNoteListMode())
const supportTouch = useMemo(() => isTouchDevice(), []) const supportTouch = useMemo(() => isTouchDevice(), [])
const noteListRef = useRef<TNoteListRef>(null) const internalNoteListRef = useRef<TNoteListRef>(null)
const noteListRef = ref || internalNoteListRef
const handleListModeChange = (mode: TNoteListMode) => { const handleListModeChange = (mode: TNoteListMode) => {
setListMode(mode) setListMode(mode)
if (isMainFeed) { if (isMainFeed) {
storage.setNoteListMode(mode) storage.setNoteListMode(mode)
} }
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.scrollToTop('smooth') noteListRef.current?.scrollToTop('smooth')
} }
}
const handleShowKindsChange = (newShowKinds: number[]) => { const handleShowKindsChange = (newShowKinds: number[]) => {
setTemporaryShowKinds(newShowKinds) setTemporaryShowKinds(newShowKinds)
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.scrollToTop() noteListRef.current?.scrollToTop()
} }
}
return ( return (
<> <>
@ -53,7 +58,11 @@ export default function NormalFeed({
}} }}
options={ options={
<> <>
{!supportTouch && <RefreshButton onClick={() => noteListRef.current?.refresh()} />} {!supportTouch && <RefreshButton onClick={() => {
if (noteListRef && typeof noteListRef !== 'function') {
noteListRef.current?.refresh()
}
}} />}
<KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} /> <KindFilter showKinds={temporaryShowKinds} onShowKindsChange={handleShowKindsChange} />
</> </>
} }
@ -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'
import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event' import { getNoteBech32Id, isProtectedEvent, getRootEventHexId } from '@/lib/event'
import { toNjump } from '@/lib/link' import { toNjump } from '@/lib/link'
import { pubkeyToNpub } from '@/lib/pubkey' import { pubkeyToNpub } from '@/lib/pubkey'
import { simplifyUrl } from '@/lib/url' import { normalizeUrl, simplifyUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
@ -53,7 +53,10 @@ export function useMenuActions({
const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays() const { relayUrls: currentBrowsingRelayUrls } = useCurrentRelays()
const { relaySets, favoriteRelays } = useFavoriteRelays() const { relaySets, favoriteRelays } = useFavoriteRelays()
const relayUrls = useMemo(() => { 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]) }, [currentBrowsingRelayUrls, favoriteRelays])
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList() const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeySet } = useMuteList()
const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event]) const isMuted = useMemo(() => mutePubkeySet.has(event.pubkey), [mutePubkeySet, event])

25
src/components/PostEditor/PostContent.tsx

@ -13,7 +13,9 @@ import {
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { isTouchDevice } from '@/lib/utils' import { isTouchDevice } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useFeed } from '@/providers/FeedProvider'
import { useReply } from '@/providers/ReplyProvider' import { useReply } from '@/providers/ReplyProvider'
import { normalizeUrl } from '@/lib/url'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
import { TPollCreateData } from '@/types' import { TPollCreateData } from '@/types'
import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react' import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react'
@ -43,6 +45,7 @@ export default function PostContent({
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { pubkey, publish, checkLogin } = useNostr() const { pubkey, publish, checkLogin } = useNostr()
const { feedInfo } = useFeed()
const { addReplies } = useReply() const { addReplies } = useReply()
const [text, setText] = useState('') const [text, setText] = useState('')
const textareaRef = useRef<TPostTextareaHandle>(null) const textareaRef = useRef<TPostTextareaHandle>(null)
@ -59,6 +62,7 @@ export default function PostContent({
const [publicMessageRecipients, setPublicMessageRecipients] = useState<string[]>([]) const [publicMessageRecipients, setPublicMessageRecipients] = useState<string[]>([])
const [isProtectedEvent, setIsProtectedEvent] = useState(false) const [isProtectedEvent, setIsProtectedEvent] = useState(false)
const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([]) const [additionalRelayUrls, setAdditionalRelayUrls] = useState<string[]>([])
const [userWriteRelays, setUserWriteRelays] = useState<string[]>([])
const [isHighlight, setIsHighlight] = useState(false) const [isHighlight, setIsHighlight] = useState(false)
const [highlightData, setHighlightData] = useState<HighlightData>({ const [highlightData, setHighlightData] = useState<HighlightData>({
sourceType: 'nostr', sourceType: 'nostr',
@ -243,12 +247,30 @@ export default function PostContent({
// console.log('Publishing draft event:', draftEvent) // console.log('Publishing draft event:', draftEvent)
const newEvent = await publish(draftEvent, { const newEvent = await publish(draftEvent, {
specifiedRelayUrls: isProtectedEvent ? additionalRelayUrls : undefined, specifiedRelayUrls: isProtectedEvent ? additionalRelayUrls.filter(url => !userWriteRelays.includes(url)) : undefined,
additionalRelayUrls: isPoll ? pollCreateData.relays : additionalRelayUrls, additionalRelayUrls: isPoll ? pollCreateData.relays : additionalRelayUrls,
minPow minPow
}) })
// console.log('Published event:', newEvent) // 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 // Show publishing feedback
if ((newEvent as any).relayStatuses) { if ((newEvent as any).relayStatuses) {
showPublishingFeedback({ showPublishingFeedback({
@ -470,6 +492,7 @@ export default function PostContent({
<PostRelaySelector <PostRelaySelector
setIsProtectedEvent={setIsProtectedEvent} setIsProtectedEvent={setIsProtectedEvent}
setAdditionalRelayUrls={setAdditionalRelayUrls} setAdditionalRelayUrls={setAdditionalRelayUrls}
setUserWriteRelays={setUserWriteRelays}
parentEvent={parentEvent} parentEvent={parentEvent}
openFrom={openFrom} openFrom={openFrom}
content={text} content={text}

27
src/components/PostEditor/PostRelaySelector.tsx

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

6
src/components/QuoteList/index.tsx

@ -1,5 +1,6 @@
import { FAST_READ_RELAY_URLS, ExtendedKind } from '@/constants' import { FAST_READ_RELAY_URLS, ExtendedKind } from '@/constants'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event' import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { normalizeUrl } from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useUserTrust } from '@/providers/UserTrustProvider' import { useUserTrust } from '@/providers/UserTrustProvider'
import client from '@/services/client.service' import client from '@/services/client.service'
@ -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 // Privacy: Only use user's own relays + defaults, never connect to other users' relays
const userRelays = userRelayList?.read || [] 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( const { closer, timelineKey } = await client.subscribeTimeline(
[ [

24
src/components/Relay/index.tsx

@ -4,8 +4,9 @@ import SearchInput from '@/components/SearchInput'
import { useFetchRelayInfo } from '@/hooks' import { useFetchRelayInfo } from '@/hooks'
import { normalizeUrl } from '@/lib/url' import { normalizeUrl } from '@/lib/url'
import { useCurrentRelays } from '@/providers/CurrentRelaysProvider' import { useCurrentRelays } from '@/providers/CurrentRelaysProvider'
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { TNoteListRef } from '@/components/NoteList'
import NotFound from '../NotFound' import NotFound from '../NotFound'
export default function Relay({ url, className }: { url?: string; className?: string }) { export default function Relay({ url, className }: { url?: string; className?: string }) {
@ -15,6 +16,7 @@ export default function Relay({ url, className }: { url?: string; className?: st
const { relayInfo } = useFetchRelayInfo(normalizedUrl) const { relayInfo } = useFetchRelayInfo(normalizedUrl)
const [searchInput, setSearchInput] = useState('') const [searchInput, setSearchInput] = useState('')
const [debouncedInput, setDebouncedInput] = useState(searchInput) const [debouncedInput, setDebouncedInput] = useState(searchInput)
const noteListRef = useRef<TNoteListRef>(null)
useEffect(() => { useEffect(() => {
if (normalizedUrl) { if (normalizedUrl) {
@ -35,6 +37,25 @@ export default function Relay({ url, className }: { url?: string; className?: st
} }
}, [searchInput]) }, [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) { if (!normalizedUrl) {
return <NotFound /> return <NotFound />
} }
@ -52,6 +73,7 @@ export default function Relay({ url, className }: { url?: string; className?: st
</div> </div>
)} )}
<NormalFeed <NormalFeed
ref={noteListRef}
subRequests={[ subRequests={[
{ urls: [normalizedUrl], filter: debouncedInput ? { search: debouncedInput } : {} } { urls: [normalizedUrl], filter: debouncedInput ? { search: debouncedInput } : {} }
]} ]}

5
src/components/ReplyNoteList/index.tsx

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

10
src/components/SaveRelayDropdownMenu/index.tsx

@ -134,7 +134,10 @@ function RelayItem({ urls }: { urls: string[] }) {
if (isSmallScreen) { if (isSmallScreen) {
return ( return (
<DrawerMenuItem onClick={handleClick} disabled={isLoading}> <DrawerMenuItem
onClick={isLoading ? undefined : handleClick}
className={isLoading ? 'opacity-50 cursor-not-allowed' : ''}
>
{isLoading ? '...' : (saved ? <Check /> : <Plus />)} {isLoading ? '...' : (saved ? <Check /> : <Plus />)}
{isLoading ? t('Loading...') : (saved ? t('Unfavorite') : t('Favorite'))} {isLoading ? t('Loading...') : (saved ? t('Unfavorite') : t('Favorite'))}
</DrawerMenuItem> </DrawerMenuItem>
@ -168,7 +171,10 @@ function RelaySetItem({ set, urls }: { set: TRelaySet; urls: string[] }) {
} else { } else {
updateRelaySet({ updateRelaySet({
...set, ...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'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { prefixNostrAddresses } from '@/lib/nostr-address' import { prefixNostrAddresses } from '@/lib/nostr-address'
import { showPublishingError } from '@/lib/publishing-feedback' import { showPublishingError } from '@/lib/publishing-feedback'
import { simplifyUrl } from '@/lib/url' import { normalizeUrl, simplifyUrl } from '@/lib/url'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { extractHashtagsFromContent, normalizeTopic } from '@/lib/discussion-topics' import { extractHashtagsFromContent, normalizeTopic } from '@/lib/discussion-topics'
import DiscussionContent from '@/components/Note/DiscussionContent' import DiscussionContent from '@/components/Note/DiscussionContent'
@ -105,7 +105,10 @@ export default function CreateThreadDialog({
const relaySet = initialRelay ? relaySets.find(set => set.id === initialRelay) : null const relaySet = initialRelay ? relaySets.find(set => set.id === initialRelay) : null
if (relaySet) { if (relaySet) {
// Include relays from the selected set along with available relays // 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 return availableRelays
}, [availableRelays, relaySets, initialRelay]) }, [availableRelays, relaySets, initialRelay])

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

@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card' import { Card, CardContent } from '@/components/ui/card'
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'
import { DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS } from '@/constants' import { DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS } from '@/constants'
import { normalizeUrl } from '@/lib/url'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { forwardRef, useEffect, useState, useCallback, useMemo } from 'react' import { forwardRef, useEffect, useState, useCallback, useMemo } from 'react'
@ -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([ const allRelays = Array.from(new Set([
...availableRelays, ...availableRelays.map(url => normalizeUrl(url) || url),
...userWriteRelays, ...userWriteRelays.map(url => normalizeUrl(url) || url),
...storedRelaySetRelays, ...storedRelaySetRelays.map(url => normalizeUrl(url) || url),
...FAST_READ_RELAY_URLS ...FAST_READ_RELAY_URLS.map(url => normalizeUrl(url) || url)
])) ]))
setRelayUrls(allRelays) setRelayUrls(allRelays)

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

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

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

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

6
src/providers/FavoriteRelaysProvider.tsx

@ -99,8 +99,12 @@ export function FavoriteRelaysProvider({ children }: { children: React.ReactNode
) )
setRelaySetEvents(storedRelaySetEvents.filter(Boolean) as Event[]) 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( 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], kinds: [kinds.Relaysets],
authors: [pubkey], authors: [pubkey],

7
src/providers/NostrProvider/index.tsx

@ -13,6 +13,7 @@ import {
minePow minePow
} from '@/lib/event' } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { normalizeUrl } from '@/lib/url'
import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey' import { formatPubkey, pubkeyToNpub } from '@/lib/pubkey'
import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback' import { showPublishingFeedback, showSimplePublishSuccess } from '@/lib/publishing-feedback'
import client from '@/services/client.service' import client from '@/services/client.service'
@ -274,7 +275,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
setRelayList(relayList) 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, [ const events = await client.fetchEvents(fetchRelays, [
{ {
kinds: [ kinds: [

33
src/services/client.service.ts

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

Loading…
Cancel
Save