Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
3b819a4b72
  1. 9
      src/components/MediaPlayer/LazyMediaTapPlaceholder.tsx
  2. 2
      src/components/MediaPlayer/index.tsx
  3. 26
      src/components/Profile/index.tsx
  4. 2
      src/components/UserAvatar/index.tsx
  5. 2
      src/components/Username/index.tsx
  6. 7
      src/components/VideoPlayer/index.tsx
  7. 7
      src/components/YoutubeEmbeddedPlayer/index.tsx
  8. 2
      src/components/ZapStreamEmbeddedPlayer/index.tsx
  9. 16
      src/hooks/useFetchProfile.tsx
  10. 20
      src/lib/nostr-parser.tsx
  11. 25
      src/lib/profile-navigation-seed.ts
  12. 58
      src/pages/secondary/ProfileEditorPage/index.tsx

9
src/components/MediaPlayer/LazyMediaTapPlaceholder.tsx

@ -58,7 +58,9 @@ function BlurHashLayer({ blurHash, className }: { blurHash: string; className?: @@ -58,7 +58,9 @@ function BlurHashLayer({ blurHash, className }: { blurHash: string; className?:
const frameClass = (kind: 'video' | 'audio', className?: string) =>
cn(
'relative w-full max-w-[400px] shrink-0 self-start overflow-hidden rounded-lg border border-border bg-muted/30 shadow-sm',
// `not-prose`: poster <img> lives inside MarkdownArticle `.prose`; typography adds img margins
// that break `absolute inset-0` layout and show a blurhash band above the still.
'not-prose relative w-full max-w-[400px] shrink-0 self-start overflow-hidden rounded-lg border border-border bg-muted/30 shadow-sm',
kind === 'video' ? 'aspect-video' : 'min-h-[7.5rem] aspect-[21/9]',
className
)
@ -89,7 +91,7 @@ function MediaPlaceholderLayers({ @@ -89,7 +91,7 @@ function MediaPlaceholderLayers({
<img
src={poster}
alt=""
className="absolute inset-0 z-[1] h-full w-full object-cover"
className="absolute inset-0 z-[1] m-0 h-full w-full max-w-none object-cover object-center"
loading="eager"
decoding="async"
/>
@ -187,7 +189,8 @@ export default function LazyMediaTapPlaceholder({ @@ -187,7 +189,8 @@ export default function LazyMediaTapPlaceholder({
className={cn(
// `block` + `p-0` + `leading-none`: native <button> keeps a line-box / padding; with only
// absolutely positioned children that shifts the stack and the play icon looks bottom-heavy.
'group relative block w-full max-w-[400px] shrink-0 self-start overflow-hidden rounded-lg border border-border bg-muted/30 p-0 text-left leading-none shadow-sm outline-none transition-opacity hover:opacity-95 focus-visible:ring-2 focus-visible:ring-ring',
// `not-prose`: see frameClass — poster img must not inherit prose img margins inside notes.
'not-prose group relative block w-full max-w-[400px] shrink-0 self-start overflow-hidden rounded-lg border border-border bg-muted/30 p-0 text-left leading-none shadow-sm outline-none transition-opacity hover:opacity-95 focus-visible:ring-2 focus-visible:ring-ring',
kind === 'video' ? 'aspect-video' : 'min-h-[7.5rem] aspect-[21/9]',
className
)}

2
src/components/MediaPlayer/index.tsx

@ -199,7 +199,7 @@ export default function MediaPlayer({ @@ -199,7 +199,7 @@ export default function MediaPlayer({
'transition-opacity duration-300 ease-out motion-reduce:transition-none'
return (
<div className="relative w-full max-w-[400px] shrink-0 self-start">
<div className="not-prose relative w-full max-w-[400px] shrink-0 self-start">
{!embedPainted ? (
<div className="relative z-10 w-full">
<MediaEmbedBlurFrame

26
src/components/Profile/index.tsx

@ -378,9 +378,9 @@ export default function Profile({ @@ -378,9 +378,9 @@ export default function Profile({
return (
<>
<div>
<div className="relative bg-cover bg-center mb-2">
<Skeleton className="w-full aspect-[3/1] rounded-none" />
<Skeleton className="w-24 h-24 md:w-48 md:h-48 absolute bottom-0 left-3 translate-y-1/2 border-4 border-background rounded-full" />
<div className="relative isolate mb-2 bg-cover bg-center">
<Skeleton className="relative z-0 w-full aspect-[3/1] rounded-none" />
<Skeleton className="absolute bottom-0 left-3 z-20 h-24 w-24 translate-y-1/2 rounded-full border-4 border-background md:h-48 md:w-48" />
</div>
</div>
<div className="px-4">
@ -404,10 +404,16 @@ export default function Profile({ @@ -404,10 +404,16 @@ export default function Profile({
return (
<>
<div>
<div className="relative bg-cover bg-center mb-2">
{/* Avatar first in DOM + higher fetch priority so it loads before the wide banner. */}
<div className="relative isolate mb-2 bg-cover bg-center">
{/* Banner first in paint order; avatar uses higher z-index so it always sits on top. fetchPriority still prefers the pic over the banner. */}
<ProfileBanner
banner={banner}
pubkey={pubkey}
className="relative z-0 w-full overflow-hidden aspect-[3/1]"
imageFetchPriority="low"
/>
{isVideo(avatar ?? '') ? (
<div className="w-24 h-24 md:w-48 md:h-48 absolute left-3 bottom-0 z-10 translate-y-1/2 border-4 border-background overflow-hidden rounded-full bg-muted">
<div className="absolute bottom-0 left-3 z-20 h-24 w-24 translate-y-1/2 overflow-hidden rounded-full border-4 border-background bg-muted md:h-48 md:w-48">
<video
src={avatar}
className="h-full w-full object-cover object-center"
@ -419,7 +425,7 @@ export default function Profile({ @@ -419,7 +425,7 @@ export default function Profile({
/>
</div>
) : (
<Avatar className="w-24 h-24 md:w-48 md:h-48 absolute left-3 bottom-0 z-10 translate-y-1/2 border-4 border-background">
<Avatar className="absolute bottom-0 left-3 z-20 h-24 w-24 translate-y-1/2 border-4 border-background md:h-48 md:w-48">
<AvatarImage
src={avatar}
className="object-cover object-center"
@ -431,12 +437,6 @@ export default function Profile({ @@ -431,12 +437,6 @@ export default function Profile({
</AvatarFallback>
</Avatar>
)}
<ProfileBanner
banner={banner}
pubkey={pubkey}
className="relative z-0 w-full aspect-[3/1]"
imageFetchPriority="low"
/>
</div>
<div className="px-4">
<div className="flex justify-end h-8 gap-2 items-center">

2
src/components/UserAvatar/index.tsx

@ -4,6 +4,7 @@ import { toNostrBuildThumbUrl } from '@/lib/nostr-build' @@ -4,6 +4,7 @@ import { toNostrBuildThumbUrl } from '@/lib/nostr-build'
import { isImage, isMedia, isVideo } from '@/lib/url'
import { generateImageByPubkey, userIdToPubkey } from '@/lib/pubkey'
import { toProfile } from '@/lib/link'
import { seedProfileForNavigation } from '@/lib/profile-navigation-seed'
import { cn } from '@/lib/utils'
import { useSmartProfileNavigationOptional } from '@/PageManager'
import type { TProfile } from '@/types'
@ -254,6 +255,7 @@ export default function UserAvatar({ @@ -254,6 +255,7 @@ export default function UserAvatar({
style={{ position: 'relative', zIndex: 10, isolation: 'isolate', display: 'block' }}
onClick={(e) => {
e.stopPropagation()
if (profile) seedProfileForNavigation(profile)
navigateToProfile(toProfile(displayPubkey))
}}
>

2
src/components/Username/index.tsx

@ -1,6 +1,7 @@ @@ -1,6 +1,7 @@
import { Skeleton } from '@/components/ui/skeleton'
import { useFetchProfile } from '@/hooks'
import { toProfile } from '@/lib/link'
import { seedProfileForNavigation } from '@/lib/profile-navigation-seed'
import { formatPubkey, userIdToPubkey, pubkeyToNpub, formatNpub } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
import { useSmartProfileNavigationOptional } from '@/PageManager'
@ -63,6 +64,7 @@ export default function Username({ @@ -63,6 +64,7 @@ export default function Username({
onClick={(e) => {
e.stopPropagation()
onNavigate?.()
seedProfileForNavigation(profile)
navigateToProfile(toProfile(profilePubkey))
}}
>

7
src/components/VideoPlayer/index.tsx

@ -160,13 +160,16 @@ export default function VideoPlayer({ @@ -160,13 +160,16 @@ export default function VideoPlayer({
setError(true)
}}
>
<div ref={containerRef} className="relative w-full max-w-full overflow-hidden">
<div ref={containerRef} className="not-prose relative w-full max-w-full overflow-hidden">
<video
ref={videoRef}
controls
playsInline
preload={onReady ? (hlsMode === 'hlsjs' ? 'auto' : 'metadata') : 'none'}
className={cn('rounded-lg max-h-[80vh] sm:max-h-[60vh] border w-full h-auto max-w-full', className)}
className={cn(
'm-0 max-w-full rounded-lg max-h-[80vh] sm:max-h-[60vh] border w-full h-auto',
className
)}
src={hlsMode === 'hlsjs' ? undefined : src}
poster={poster}
onClick={(e) => e.stopPropagation()}

7
src/components/YoutubeEmbeddedPlayer/index.tsx

@ -114,7 +114,12 @@ export default function YoutubeEmbeddedPlayer({ @@ -114,7 +114,12 @@ export default function YoutubeEmbeddedPlayer({
return <ExternalLink url={url} />
}
return (
<div className={cn('rounded-lg border overflow-hidden w-full max-w-[400px]', frameClassName)}>
<div
className={cn(
'not-prose rounded-lg border overflow-hidden w-full max-w-[400px]',
frameClassName
)}
>
<div ref={containerRef} className="w-full h-full" />
</div>
)

2
src/components/ZapStreamEmbeddedPlayer/index.tsx

@ -44,7 +44,7 @@ export default function ZapStreamEmbeddedPlayer({ @@ -44,7 +44,7 @@ export default function ZapStreamEmbeddedPlayer({
title="zap.stream"
src={embedSrc}
className={cn(
'rounded-lg border w-full max-w-[400px] aspect-video max-h-[min(70vh,28rem)]',
'not-prose rounded-lg border w-full max-w-[400px] aspect-video max-h-[min(70vh,28rem)]',
className
)}
allow="autoplay; encrypted-media; fullscreen; clipboard-write"

16
src/hooks/useFetchProfile.tsx

@ -1,5 +1,6 @@ @@ -1,5 +1,6 @@
import { PROFILE_FETCH_PROMISE_TIMEOUT_MS } from '@/constants'
import { getProfileFromEvent } from '@/lib/event-metadata'
import { getSeededProfileForNavigation } from '@/lib/profile-navigation-seed'
import { userIdToPubkey } from '@/lib/pubkey'
import { useNostrOptional } from '@/providers/nostr-context'
import { useNoteFeedProfileContext } from '@/providers/NoteFeedProfileContext'
@ -312,6 +313,21 @@ export function useFetchProfile(id?: string, skipCache = false) { @@ -312,6 +313,21 @@ export function useFetchProfile(id?: string, skipCache = false) {
return
}
}
// Userbadge → profile panel: feed row already had this profile, but secondary stack is outside NoteFeedProfileContext.
if (extractedPubkey && !skipCache) {
const fromNavigation = getSeededProfileForNavigation(extractedPubkey)
if (fromNavigation) {
setProfile(fromNavigation)
setPubkey(extractedPubkey)
setIsFetching(false)
setError(null)
processingPubkeyRef.current = extractedPubkey
initializedPubkeysRef.current.add(extractedPubkey)
effectRunCountRef.current.delete(extractedPubkey)
return
}
}
// CRITICAL: Early exit if already processing this exact pubkey - prevents infinite loops
// This check must happen FIRST, before any other logic

20
src/lib/nostr-parser.tsx

@ -539,15 +539,17 @@ function NostrInlineVideo({ mediaUrl, fallbackText }: { mediaUrl: string; fallba @@ -539,15 +539,17 @@ function NostrInlineVideo({ mediaUrl, fallbackText }: { mediaUrl: string; fallba
)
}
return (
<video
src={mediaUrl}
controls
className="max-w-full sm:max-w-[400px] w-full h-auto rounded-lg my-2 block"
preload="metadata"
onError={() => setFailed(true)}
>
Your browser does not support the video tag.
</video>
<div className="not-prose my-2 max-w-full sm:max-w-[400px] w-full">
<video
src={mediaUrl}
controls
className="m-0 max-w-full w-full h-auto rounded-lg block"
preload="metadata"
onError={() => setFailed(true)}
>
Your browser does not support the video tag.
</video>
</div>
)
}

25
src/lib/profile-navigation-seed.ts

@ -0,0 +1,25 @@ @@ -0,0 +1,25 @@
import type { TProfile } from '@/types'
const seeds = new Map<string, TProfile>()
function normPubkey(pubkey: string): string {
return pubkey.toLowerCase()
}
/**
* Call before navigating to `/users/…` from a userbadge (or anywhere we already have a {@link TProfile}).
* The secondary profile panel mounts outside {@link NoteFeedProfileContext}, so `useFetchProfile` would
* otherwise start a cold relay/IDB path even though the feed row already resolved this profile.
*/
export function seedProfileForNavigation(profile: TProfile): void {
if (!profile?.pubkey) return
seeds.set(normPubkey(profile.pubkey), profile)
}
/** Instant paint for `useFetchProfile` when opening the profile route from a seeded navigation. */
export function getSeededProfileForNavigation(pubkey: string): TProfile | undefined {
const pk = normPubkey(pubkey)
const p = seeds.get(pk)
if (p && normPubkey(p.pubkey) === pk) return p
return undefined
}

58
src/pages/secondary/ProfileEditorPage/index.tsx

@ -472,21 +472,43 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { @@ -472,21 +472,43 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
return (
<SecondaryPageLayout ref={ref} index={index} title={profile.username} controls={controls}>
{/* Banner & avatar uploaders */}
<div className="relative bg-cover bg-center mb-2">
{/* Avatar first in DOM + higher fetch priority so it loads before the banner (same as profile view). */}
<div className="relative isolate mb-2 bg-cover bg-center">
{/* Banner under avatar in stacking order; fetchPriority still loads the pic first. */}
<Uploader
onUploadSuccess={onBannerUploadSuccess}
onUploadStart={() => setUploadingBanner(true)}
onUploadEnd={() => setUploadingBanner(false)}
className="relative z-0 w-full cursor-pointer overflow-hidden"
accept={PROFILE_IMAGE_VIDEO_UPLOADER_ACCEPT}
maxCompressedSizeMb={5}
>
<ProfileBanner
banner={banner}
pubkey={account.pubkey}
className="w-full aspect-[3/1]"
imageFetchPriority="low"
/>
<div className="absolute inset-0 flex flex-col items-center justify-center bg-muted/30">
{uploadingBanner ? (
<Skeleton className="size-9 shrink-0 rounded-md" aria-hidden />
) : (
<Upload size={36} />
)}
</div>
</Uploader>
<Uploader
onUploadSuccess={onAvatarUploadSuccess}
onUploadStart={() => setUploadingAvatar(true)}
onUploadEnd={() => setUploadingAvatar(false)}
className="z-10 w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background cursor-pointer rounded-full"
className="absolute bottom-0 left-4 z-20 h-24 w-24 translate-y-1/2 cursor-pointer rounded-full border-4 border-background md:h-48 md:w-48"
accept={PROFILE_IMAGE_VIDEO_UPLOADER_ACCEPT}
maxCompressedSizeMb={2}
>
<div className="w-full h-full overflow-hidden rounded-full bg-muted">
<div className="h-full w-full overflow-hidden rounded-full bg-muted">
{isVideo(avatar) ? (
<video
src={avatar}
className="w-full h-full object-cover object-center"
className="h-full w-full object-cover object-center"
autoPlay
muted
loop
@ -494,7 +516,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { @@ -494,7 +516,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
fetchPriority="high"
/>
) : (
<Avatar className="w-full h-full">
<Avatar className="h-full w-full">
<AvatarImage
src={avatar}
className="object-cover object-center"
@ -507,7 +529,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { @@ -507,7 +529,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
</Avatar>
)}
</div>
<div className="absolute top-0 bg-muted/30 w-full h-full rounded-full flex flex-col justify-center items-center">
<div className="absolute inset-0 flex flex-col items-center justify-center rounded-full bg-muted/30">
{uploadingAvatar ? (
<Skeleton className="size-4 shrink-0 rounded-sm" aria-hidden />
) : (
@ -515,28 +537,6 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { @@ -515,28 +537,6 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
)}
</div>
</Uploader>
<Uploader
onUploadSuccess={onBannerUploadSuccess}
onUploadStart={() => setUploadingBanner(true)}
onUploadEnd={() => setUploadingBanner(false)}
className="relative z-0 w-full cursor-pointer"
accept={PROFILE_IMAGE_VIDEO_UPLOADER_ACCEPT}
maxCompressedSizeMb={5}
>
<ProfileBanner
banner={banner}
pubkey={account.pubkey}
className="w-full aspect-[3/1]"
imageFetchPriority="low"
/>
<div className="absolute top-0 bg-muted/30 w-full h-full flex flex-col justify-center items-center">
{uploadingBanner ? (
<Skeleton className="size-9 shrink-0 rounded-md" aria-hidden />
) : (
<Upload size={36} />
)}
</div>
</Uploader>
</div>
<div className="pt-14 px-4 flex flex-col gap-4">

Loading…
Cancel
Save