Browse Source

bug-fix collapsable side panels and vite URLs

imwald
Silberengel 5 months ago
parent
commit
de7204a051
  1. 22
      src/PageManager.tsx
  2. 14
      src/components/Note/Highlight/index.tsx
  3. 19
      src/components/ProfileCard/index.tsx
  4. 8
      src/components/UserAvatar/index.tsx
  5. 10
      src/components/Username/index.tsx
  6. 1
      src/i18n/locales/en.ts
  7. 30
      src/lib/url.ts
  8. 5
      src/services/relay-selection.service.ts

22
src/PageManager.tsx

@ -11,6 +11,7 @@ import WalletPage from '@/pages/secondary/WalletPage'
import PostSettingsPage from '@/pages/secondary/PostSettingsPage' import PostSettingsPage from '@/pages/secondary/PostSettingsPage'
import GeneralSettingsPage from '@/pages/secondary/GeneralSettingsPage' import GeneralSettingsPage from '@/pages/secondary/GeneralSettingsPage'
import TranslationPage from '@/pages/secondary/TranslationPage' import TranslationPage from '@/pages/secondary/TranslationPage'
import SecondaryProfilePage from '@/pages/secondary/ProfilePage'
import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider' import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider'
import { NotificationProvider } from '@/providers/NotificationProvider' import { NotificationProvider } from '@/providers/NotificationProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
@ -158,6 +159,27 @@ export function useSmartRelayNavigation() {
return { navigateToRelay } 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(<SecondaryProfilePage id={profileId} index={0} hideTitlebar={true} />, 'settings')
} else {
// Normal behavior - use secondary navigation
pushSecondary(url)
}
}
return { navigateToProfile }
}
// Custom hook for intelligent settings navigation // Custom hook for intelligent settings navigation
export function useSmartSettingsNavigation() { export function useSmartSettingsNavigation() {
const { showRecommendedRelaysPanel } = useUserPreferences() const { showRecommendedRelaysPanel } = useUserPreferences()

14
src/components/Note/Highlight/index.tsx

@ -1,4 +1,4 @@
import { SecondaryPageLink } from '@/PageManager' import { useSmartNoteNavigation } from '@/PageManager'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { ExternalLink, Highlighter } from 'lucide-react' import { ExternalLink, Highlighter } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -13,6 +13,7 @@ export default function Highlight({
className?: string className?: string
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { navigateToNote } = useSmartNoteNavigation()
try { try {
@ -132,15 +133,18 @@ export default function Highlight({
<ExternalLink className="w-3 h-3" /> <ExternalLink className="w-3 h-3" />
</a> </a>
) : ( ) : (
<SecondaryPageLink <span
to={toNote(source.bech32)} onClick={(e) => {
className="text-blue-500 hover:underline font-mono" e.stopPropagation()
navigateToNote(toNote(source.bech32))
}}
className="text-blue-500 hover:underline font-mono cursor-pointer"
> >
{source.type === 'event' {source.type === 'event'
? `note1${source.bech32.substring(5, 13)}...` ? `note1${source.bech32.substring(5, 13)}...`
: `naddr1${source.bech32.substring(6, 14)}...` : `naddr1${source.bech32.substring(6, 14)}...`
} }
</SecondaryPageLink> </span>
)} )}
</div> </div>
)} )}

19
src/components/ProfileCard/index.tsx

@ -1,4 +1,9 @@
import { Button } from '@/components/ui/button'
import { useFetchProfile } from '@/hooks' 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 FollowButton from '../FollowButton'
import Nip05 from '../Nip05' import Nip05 from '../Nip05'
import ProfileAbout from '../ProfileAbout' import ProfileAbout from '../ProfileAbout'
@ -7,6 +12,8 @@ import { SimpleUserAvatar } from '../UserAvatar'
export default function ProfileCard({ pubkey }: { pubkey: string }) { export default function ProfileCard({ pubkey }: { pubkey: string }) {
const { profile } = useFetchProfile(pubkey) const { profile } = useFetchProfile(pubkey)
const { username, about } = profile || {} const { username, about } = profile || {}
const { navigateToProfile } = useSmartProfileNavigation()
const { t } = useTranslation()
return ( return (
<div className="w-full flex flex-col gap-2 not-prose"> <div className="w-full flex flex-col gap-2 not-prose">
@ -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" className="text-sm text-wrap break-words w-full overflow-hidden text-ellipsis line-clamp-6"
/> />
)} )}
<Button
variant="outline"
size="sm"
className="w-full mt-2"
onClick={(e) => {
e.stopPropagation()
navigateToProfile(toProfile(pubkey))
}}
>
<UserRound className="w-4 h-4 mr-2" />
{t('View full profile')}
</Button>
</div> </div>
) )
} }

8
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 { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks' import { useFetchProfile } from '@/hooks'
import { toProfile } from '@/lib/link'
import { generateImageByPubkey } from '@/lib/pubkey' import { generateImageByPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { SecondaryPageLink } from '@/PageManager'
import { useMemo } from 'react' import { useMemo } from 'react'
import ProfileCard from '../ProfileCard' import ProfileCard from '../ProfileCard'
@ -44,15 +42,13 @@ export default function UserAvatar({
return ( return (
<HoverCard> <HoverCard>
<HoverCardTrigger> <HoverCardTrigger asChild>
<SecondaryPageLink to={toProfile(pubkey)} onClick={(e) => e.stopPropagation()}> <Avatar className={cn('shrink-0 cursor-pointer', UserAvatarSizeCnMap[size], className)}>
<Avatar className={cn('shrink-0', UserAvatarSizeCnMap[size], className)}>
<AvatarImage src={avatar} className="object-cover object-center" /> <AvatarImage src={avatar} className="object-cover object-center" />
<AvatarFallback> <AvatarFallback>
<img src={defaultAvatar} alt={pubkey} /> <img src={defaultAvatar} alt={pubkey} />
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
</SecondaryPageLink>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent className="w-72"> <HoverCardContent className="w-72">
<ProfileCard pubkey={pubkey} /> <ProfileCard pubkey={pubkey} />

10
src/components/Username/index.tsx

@ -1,9 +1,7 @@
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks' import { useFetchProfile } from '@/hooks'
import { toProfile } from '@/lib/link'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { SecondaryPageLink } from '@/PageManager'
import ProfileCard from '../ProfileCard' import ProfileCard from '../ProfileCard'
export default function Username({ export default function Username({
@ -34,15 +32,9 @@ export default function Username({
return ( return (
<HoverCard> <HoverCard>
<HoverCardTrigger asChild> <HoverCardTrigger asChild>
<div className={className}> <div className={cn('truncate hover:underline cursor-pointer', className)}>
<SecondaryPageLink
to={toProfile(pubkey)}
className="truncate hover:underline"
onClick={(e) => e.stopPropagation()}
>
{showAt && '@'} {showAt && '@'}
{username} {username}
</SecondaryPageLink>
</div> </div>
</HoverCardTrigger> </HoverCardTrigger>
<HoverCardContent className="w-80"> <HoverCardContent className="w-80">

1
src/i18n/locales/en.ts

@ -45,6 +45,7 @@ export default {
'Copy event ID': 'Copy event ID', 'Copy event ID': 'Copy event ID',
'Copy user ID': 'Copy user ID', 'Copy user ID': 'Copy user ID',
'View raw event': 'View raw event', 'View raw event': 'View raw event',
'View full profile': 'View full profile',
Like: 'Like', Like: 'Like',
'switch to light theme': 'switch to light theme', 'switch to light theme': 'switch to light theme',
'switch to dark theme': 'switch to dark theme', 'switch to dark theme': 'switch to dark theme',

30
src/lib/url.ts

@ -12,7 +12,21 @@ export function normalizeUrl(url: string): string {
url = 'wss://' + url url = 'wss://' + url
} }
} }
// Parse the URL first to validate it
const p = new URL(url) 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, '/') p.pathname = p.pathname.replace(/\/+/g, '/')
if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1) if (p.pathname.endsWith('/')) p.pathname = p.pathname.slice(0, -1)
if (p.protocol === 'https:') { if (p.protocol === 'https:') {
@ -21,6 +35,12 @@ export function normalizeUrl(url: string): string {
p.protocol = 'ws:' 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:// // 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 // This fixes the common typo where people use wss:// for local relays
if (isLocalNetworkUrl(p.toString())) { if (isLocalNetworkUrl(p.toString())) {
@ -32,7 +52,15 @@ export function normalizeUrl(url: string): string {
} }
p.searchParams.sort() p.searchParams.sort()
p.hash = '' 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 { } catch {
console.error('Invalid URL:', url) console.error('Invalid URL:', url)
return '' return ''

5
src/services/relay-selection.service.ts

@ -86,9 +86,8 @@ class RelaySelectionService {
if (normalized) { if (normalized) {
selectableRelays.add(normalized) selectableRelays.add(normalized)
} else { } else {
// If normalization fails, add the original URL but log a warning // If normalization fails or returns empty (invalid URL), skip it
console.warn('Failed to normalize relay URL:', url) console.warn('Skipping invalid relay URL:', url)
selectableRelays.add(url)
} }
} }

Loading…
Cancel
Save