diff --git a/src/PageManager.tsx b/src/PageManager.tsx
index 34ae472..47c6c1c 100644
--- a/src/PageManager.tsx
+++ b/src/PageManager.tsx
@@ -11,6 +11,7 @@ import WalletPage from '@/pages/secondary/WalletPage'
import PostSettingsPage from '@/pages/secondary/PostSettingsPage'
import GeneralSettingsPage from '@/pages/secondary/GeneralSettingsPage'
import TranslationPage from '@/pages/secondary/TranslationPage'
+import SecondaryProfilePage from '@/pages/secondary/ProfilePage'
import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider'
import { NotificationProvider } from '@/providers/NotificationProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
@@ -158,6 +159,27 @@ export function useSmartRelayNavigation() {
return { navigateToRelay }
}
+// Custom hook for intelligent profile navigation
+export function useSmartProfileNavigation() {
+ const { showRecommendedRelaysPanel } = useUserPreferences()
+ const { push: pushSecondary } = useSecondaryPage()
+ const { setPrimaryNoteView } = usePrimaryNoteView()
+
+ const navigateToProfile = (url: string) => {
+ if (!showRecommendedRelaysPanel) {
+ // When right panel is hidden, show profile in primary area
+ // Extract profile ID from URL (e.g., "/users/npub1..." -> "npub1...")
+ const profileId = url.replace('/users/', '')
+ setPrimaryNoteView(, 'settings')
+ } else {
+ // Normal behavior - use secondary navigation
+ pushSecondary(url)
+ }
+ }
+
+ return { navigateToProfile }
+}
+
// Custom hook for intelligent settings navigation
export function useSmartSettingsNavigation() {
const { showRecommendedRelaysPanel } = useUserPreferences()
diff --git a/src/components/Note/Highlight/index.tsx b/src/components/Note/Highlight/index.tsx
index a6977ac..2693213 100644
--- a/src/components/Note/Highlight/index.tsx
+++ b/src/components/Note/Highlight/index.tsx
@@ -1,4 +1,4 @@
-import { SecondaryPageLink } from '@/PageManager'
+import { useSmartNoteNavigation } from '@/PageManager'
import { Event } from 'nostr-tools'
import { ExternalLink, Highlighter } from 'lucide-react'
import { useTranslation } from 'react-i18next'
@@ -13,6 +13,7 @@ export default function Highlight({
className?: string
}) {
const { t } = useTranslation()
+ const { navigateToNote } = useSmartNoteNavigation()
try {
@@ -132,15 +133,18 @@ export default function Highlight({
) : (
- {
+ e.stopPropagation()
+ navigateToNote(toNote(source.bech32))
+ }}
+ className="text-blue-500 hover:underline font-mono cursor-pointer"
>
{source.type === 'event'
? `note1${source.bech32.substring(5, 13)}...`
: `naddr1${source.bech32.substring(6, 14)}...`
}
-
+
)}
)}
diff --git a/src/components/ProfileCard/index.tsx b/src/components/ProfileCard/index.tsx
index 6d9b631..b9eadc6 100644
--- a/src/components/ProfileCard/index.tsx
+++ b/src/components/ProfileCard/index.tsx
@@ -1,4 +1,9 @@
+import { Button } from '@/components/ui/button'
import { useFetchProfile } from '@/hooks'
+import { toProfile } from '@/lib/link'
+import { useSmartProfileNavigation } from '@/PageManager'
+import { UserRound } from 'lucide-react'
+import { useTranslation } from 'react-i18next'
import FollowButton from '../FollowButton'
import Nip05 from '../Nip05'
import ProfileAbout from '../ProfileAbout'
@@ -7,6 +12,8 @@ import { SimpleUserAvatar } from '../UserAvatar'
export default function ProfileCard({ pubkey }: { pubkey: string }) {
const { profile } = useFetchProfile(pubkey)
const { username, about } = profile || {}
+ const { navigateToProfile } = useSmartProfileNavigation()
+ const { t } = useTranslation()
return (
@@ -24,6 +31,18 @@ export default function ProfileCard({ pubkey }: { pubkey: string }) {
className="text-sm text-wrap break-words w-full overflow-hidden text-ellipsis line-clamp-6"
/>
)}
+
)
}
diff --git a/src/components/UserAvatar/index.tsx b/src/components/UserAvatar/index.tsx
index 187beb6..dd679a0 100644
--- a/src/components/UserAvatar/index.tsx
+++ b/src/components/UserAvatar/index.tsx
@@ -2,10 +2,8 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks'
-import { toProfile } from '@/lib/link'
import { generateImageByPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
-import { SecondaryPageLink } from '@/PageManager'
import { useMemo } from 'react'
import ProfileCard from '../ProfileCard'
@@ -44,15 +42,13 @@ export default function UserAvatar({
return (
-
- e.stopPropagation()}>
-
-
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/src/components/Username/index.tsx b/src/components/Username/index.tsx
index 918ca7d..8ce3f0b 100644
--- a/src/components/Username/index.tsx
+++ b/src/components/Username/index.tsx
@@ -1,9 +1,7 @@
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks'
-import { toProfile } from '@/lib/link'
import { cn } from '@/lib/utils'
-import { SecondaryPageLink } from '@/PageManager'
import ProfileCard from '../ProfileCard'
export default function Username({
@@ -34,15 +32,9 @@ export default function Username({
return (
-
-
e.stopPropagation()}
- >
- {showAt && '@'}
- {username}
-
+
+ {showAt && '@'}
+ {username}
diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts
index 2d4caa8..bf71de4 100644
--- a/src/i18n/locales/en.ts
+++ b/src/i18n/locales/en.ts
@@ -45,6 +45,7 @@ export default {
'Copy event ID': 'Copy event ID',
'Copy user ID': 'Copy user ID',
'View raw event': 'View raw event',
+ 'View full profile': 'View full profile',
Like: 'Like',
'switch to light theme': 'switch to light theme',
'switch to dark theme': 'switch to dark theme',
diff --git a/src/lib/url.ts b/src/lib/url.ts
index 9f92929..be4ff1b 100644
--- a/src/lib/url.ts
+++ b/src/lib/url.ts
@@ -12,7 +12,21 @@ export function normalizeUrl(url: string): string {
url = 'wss://' + url
}
}
+
+ // Parse the URL first to validate it
const p = new URL(url)
+
+ // Check if URL has query parameters or hash fragments that suggest it's not a relay
+ // Relay URLs shouldn't have query params like ?token= or hash fragments
+ const hasQueryParams = url.includes('?')
+ const hasHashFragment = url.includes('#')
+
+ // Block URLs with query params or hash fragments (these are likely not relays)
+ if (hasQueryParams || hasHashFragment) {
+ console.warn('Skipping URL with query/hash (not a relay):', url)
+ return ''
+ }
+
p.pathname = p.pathname.replace(/\/+/g, '/')
if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)
if (p.protocol === 'https:') {
@@ -21,6 +35,12 @@ export function normalizeUrl(url: string): string {
p.protocol = 'ws:'
}
+ // After protocol normalization, validate it's actually a websocket URL
+ if (!isWebsocketUrl(p.toString())) {
+ console.warn('Skipping non-websocket URL:', url)
+ return ''
+ }
+
// Normalize localhost and local network addresses to always use ws:// instead of wss://
// This fixes the common typo where people use wss:// for local relays
if (isLocalNetworkUrl(p.toString())) {
@@ -32,7 +52,15 @@ export function normalizeUrl(url: string): string {
}
p.searchParams.sort()
p.hash = ''
- return p.toString()
+
+ // Final validation: ensure we have a proper websocket URL
+ const finalUrl = p.toString()
+ if (!isWebsocketUrl(finalUrl)) {
+ console.warn('Normalization resulted in invalid websocket URL:', finalUrl)
+ return ''
+ }
+
+ return finalUrl
} catch {
console.error('Invalid URL:', url)
return ''
diff --git a/src/services/relay-selection.service.ts b/src/services/relay-selection.service.ts
index 7da5baf..a005004 100644
--- a/src/services/relay-selection.service.ts
+++ b/src/services/relay-selection.service.ts
@@ -86,9 +86,8 @@ class RelaySelectionService {
if (normalized) {
selectableRelays.add(normalized)
} else {
- // If normalization fails, add the original URL but log a warning
- console.warn('Failed to normalize relay URL:', url)
- selectableRelays.add(url)
+ // If normalization fails or returns empty (invalid URL), skip it
+ console.warn('Skipping invalid relay URL:', url)
}
}