From 24b87496f9d36aba59a9b14b181c3627a30d3671 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Mon, 6 Apr 2026 19:09:31 +0200 Subject: [PATCH] bug-fixes for performance --- src/components/Embedded/EmbeddedNote.tsx | 10 +- .../FavoriteRelaysActiveStrip/index.tsx | 231 +++--------------- src/components/Image/index.tsx | 15 +- .../Note/MarkdownArticle/MarkdownArticle.tsx | 10 +- src/components/Note/index.tsx | 5 +- src/components/NoteCard/MainNoteCard.tsx | 5 +- src/components/VideoPlayer/index.tsx | 1 + src/lib/tag.ts | 15 ++ src/types/index.d.ts | 1 + 9 files changed, 78 insertions(+), 215 deletions(-) diff --git a/src/components/Embedded/EmbeddedNote.tsx b/src/components/Embedded/EmbeddedNote.tsx index 7a8cec73..8d4b6389 100644 --- a/src/components/Embedded/EmbeddedNote.tsx +++ b/src/components/Embedded/EmbeddedNote.tsx @@ -70,11 +70,13 @@ function canSearchOnExternalRelays(noteId: string): boolean { export function EmbeddedNote({ noteId, className, - containingEvent + containingEvent, + showFull = false }: { noteId: string className?: string containingEvent?: Event + showFull?: boolean }) { const suppress = useSuppressEmbeddedNoteId() const embeddedHexId = useMemo(() => hexEventIdFromNoteId(noteId), [noteId]) @@ -99,6 +101,7 @@ export function EmbeddedNote({ noteId={noteId} className={className} containingEvent={containingEvent} + showFull={showFull} /> ) } @@ -160,11 +163,13 @@ function EmbeddedNoteInvalid({ function EmbeddedNoteContent({ noteId, className, - containingEvent + containingEvent, + showFull = false }: { noteId: string className?: string containingEvent?: Event + showFull?: boolean }) { const { event, isFetching } = useFetchEvent(noteId) const [retryEvent, setRetryEvent] = useState(undefined) @@ -254,6 +259,7 @@ function EmbeddedNoteContent({ className={cn('w-full', className)} event={finalEvent} embedded + showFull={showFull} originalNoteId={noteId} /> diff --git a/src/components/FavoriteRelaysActiveStrip/index.tsx b/src/components/FavoriteRelaysActiveStrip/index.tsx index b33bbf76..bb448710 100644 --- a/src/components/FavoriteRelaysActiveStrip/index.tsx +++ b/src/components/FavoriteRelaysActiveStrip/index.tsx @@ -1,12 +1,7 @@ -import UserAvatar from '@/components/UserAvatar' -import { SimpleUsername } from '@/components/Username' import { Button } from '@/components/ui/button' -import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' import { usePrimaryPage } from '@/contexts/primary-page-context' -import { useMuteList } from '@/contexts/mute-list-context' -import { muteSetHas } from '@/lib/mute-set' import { useFavoriteRelaysActivity } from '@/providers/favorite-relays-activity-context' import { RelayPulseActiveNpubsOpenButton } from './RelayPulseActiveNpubsSheet' import type { TFunction } from 'i18next' @@ -14,14 +9,6 @@ import { FileText } from 'lucide-react' import { useEffect, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' -const MOBILE_MAX_FOLLOW = 30 -const MOBILE_MAX_OTHER = 30 -const SIDEBAR_MAX_FOLLOW = 50 -const SIDEBAR_MAX_OTHER = 50 - -/** Slight overlap so faces stay recognizable */ -const AVATAR_OVERLAP = '-ml-1' - function relativePastPhrase(timestampMs: number, t: TFunction): string { const sec = Math.floor((Date.now() - timestampMs) / 1000) if (sec < 45) return t('just now') @@ -46,166 +33,52 @@ function useRelativePastPhrase(timestampMs: number | null, t: TFunction): string }, [timestampMs, t, tick]) } -function OverlappingAvatars({ - pubkeys, - max, - avatarSize, - rowClassName -}: { - pubkeys: string[] - max: number - avatarSize: 'small' | 'xSmall' | 'tiny' - rowClassName?: string -}) { - const slice = pubkeys.slice(0, max) - const extra = pubkeys.length - slice.length - - const row = ( -
- {slice.map((pk, i) => ( - - -
0 && AVATAR_OVERLAP - )} - style={{ zIndex: i + 1 }} - > - -
-
- - - -
- ))} - {extra > 0 ? ( -
0 && AVATAR_OVERLAP - )} - title={String(extra)} - > - +{extra > 99 ? '99+' : extra} -
- ) : null} -
- ) - - return ( -
- {row} -
- ) -} - -function ActiveAvatarGroups({ - followPubkeysForAvatars, - otherPubkeysForAvatars, +function ActiveCountGroups({ followCount, otherCount, - maxFollow, - maxOther, - avatarSize, labelClassName, stackClassName, variant = 'default', onOpenFollowsNotes }: { - /** Subset with kind 0 only (shown as circles); counts use full totals */ - followPubkeysForAvatars: string[] - otherPubkeysForAvatars: string[] followCount: number otherCount: number - maxFollow: number - maxOther: number - avatarSize: 'small' | 'xSmall' | 'tiny' labelClassName: string stackClassName?: string - /** Mobile home: label above avatars + scrollable rows; sidebar/default keeps compact rows on wider mini breakpoints */ variant?: 'default' | 'mobileBar' - /** Opens search page and expands the notes-from-follows section */ onOpenFollowsNotes?: () => void }) { const { t } = useTranslation() const mobileBar = variant === 'mobileBar' const groupRowClass = mobileBar - ? 'flex w-full min-w-0 flex-col gap-1.5' - : 'flex min-w-0 flex-col gap-1 min-[380px]:flex-row min-[380px]:items-center min-[380px]:gap-2' - - const followsLabelBlock = ( -
- - {t('Relay pulse follows', { count: followCount })} - - {onOpenFollowsNotes && mobileBar ? ( - - ) : null} -
- ) - - const sidebarSectionClass = 'flex min-w-0 flex-col gap-1' + ? 'flex w-full min-w-0 items-center gap-1.5' + : 'flex min-w-0 items-center gap-1.5' return ( -
+
{followCount > 0 ? ( -
- {mobileBar ? ( - - - {t('Relay pulse follows', { count: followCount })} - - {onOpenFollowsNotes ? ( - - ) : null} - - ) : ( - followsLabelBlock - )} - +
+ + {t('Relay pulse follows', { count: followCount })} + + {onOpenFollowsNotes ? ( + + ) : null}
) : null} {otherCount > 0 ? ( -
- - {t('Relay pulse others', { count: otherCount })} - - -
+ + {t('Relay pulse others', { count: otherCount })} + ) : null}
) @@ -216,34 +89,15 @@ export function FavoriteRelaysActiveStripMobileBar({ className }: { className?: const { t } = useTranslation() const { navigate } = usePrimaryPage() const { pubkey } = useNostr() - const { mutePubkeySet } = useMuteList() const { - followPubkeys, - otherPubkeys, followCount, otherCount, totalCount, loading, relayActivityReady, - lastFetchedAtMs, - profileKind0ByPubkey + lastFetchedAtMs } = useFavoriteRelaysActivity() - const followPubkeysForAvatars = useMemo( - () => - followPubkeys.filter( - (pk) => profileKind0ByPubkey[pk] && !muteSetHas(mutePubkeySet, pk) - ), - [followPubkeys, profileKind0ByPubkey, mutePubkeySet] - ) - const otherPubkeysForAvatars = useMemo( - () => - otherPubkeys.filter( - (pk) => profileKind0ByPubkey[pk] && !muteSetHas(mutePubkeySet, pk) - ), - [otherPubkeys, profileKind0ByPubkey, mutePubkeySet] - ) - const relativeLabel = useRelativePastPhrase(lastFetchedAtMs, t) if (!relayActivityReady && !loading) { @@ -288,7 +142,7 @@ export function FavoriteRelaysActiveStripMobileBar({ className }: { className?: className )} > -
+

{t('Relay pulse')}

@@ -300,15 +154,10 @@ export function FavoriteRelaysActiveStripMobileBar({ className }: { className?:

) : null}
- navigate('follows-latest') : undefined} @@ -323,34 +172,15 @@ export function FavoriteRelaysActiveStripSidebar({ className }: { className?: st const { t } = useTranslation() const { navigate } = usePrimaryPage() const { pubkey } = useNostr() - const { mutePubkeySet } = useMuteList() const { - followPubkeys, - otherPubkeys, followCount, otherCount, totalCount, loading, relayActivityReady, - lastFetchedAtMs, - profileKind0ByPubkey + lastFetchedAtMs } = useFavoriteRelaysActivity() - const followPubkeysForAvatars = useMemo( - () => - followPubkeys.filter( - (pk) => profileKind0ByPubkey[pk] && !muteSetHas(mutePubkeySet, pk) - ), - [followPubkeys, profileKind0ByPubkey, mutePubkeySet] - ) - const otherPubkeysForAvatars = useMemo( - () => - otherPubkeys.filter( - (pk) => profileKind0ByPubkey[pk] && !muteSetHas(mutePubkeySet, pk) - ), - [otherPubkeys, profileKind0ByPubkey, mutePubkeySet] - ) - const relativeLabel = useRelativePastPhrase(lastFetchedAtMs, t) if (!relayActivityReady && !loading) { @@ -437,14 +267,9 @@ export function FavoriteRelaysActiveStripSidebar({ className }: { className?: st ) : null}
- navigate('follows-latest') : undefined} diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index 4fec46e9..a8f96e46 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -22,8 +22,14 @@ function wrapperReserveStyle( return { minHeight: 'min(30vh, 280px)' } } +function formatFileSize(bytes: number): string { + if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB` + if (bytes >= 1_024) return `${Math.round(bytes / 1_024)} KB` + return `${bytes} B` +} + export default function Image({ - image: { url, blurHash, dim, alt: imetaAlt, fallback }, + image: { url, blurHash, dim, alt: imetaAlt, fallback, size: fileSizeBytes }, alt, className = '', classNames = {}, @@ -167,6 +173,11 @@ export default function Image({ )} /> )} + {!revealed && holdUntilClick && fileSizeBytes != null && ( + + {formatFileSize(fileSizeBytes)} + + )} )} {!showErrorState && revealed && ( @@ -176,7 +187,7 @@ export default function Image({ title={finalAlt || undefined} referrerPolicy="no-referrer" decoding="async" - loading="eager" + loading="lazy" draggable={false} onLoad={handleLoad} onError={handleError} diff --git a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx index 730afca7..3cbbcedd 100644 --- a/src/components/Note/MarkdownArticle/MarkdownArticle.tsx +++ b/src/components/Note/MarkdownArticle/MarkdownArticle.tsx @@ -2423,7 +2423,7 @@ export function parseMarkdownContentLegacy( // Embedded events should be block-level and fill width parts.push(
- +
) } @@ -3200,7 +3200,7 @@ function parseMarkdownContentMarked( } return (
- +
) } @@ -3341,7 +3341,7 @@ function parseMarkdownContentMarked( } return (
- +
) } @@ -3386,7 +3386,7 @@ function parseMarkdownContentMarked( } else { nodes.push(
- +
) } @@ -3553,7 +3553,7 @@ function parseMarkdownContentMarked( } else { nodes.push(
- +
) } diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index df4eee2f..93950741 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -129,6 +129,7 @@ export default function Note({ ) const contentPolicy = useContentPolicyOptional() const defaultShowNsfw = contentPolicy?.defaultShowNsfw ?? true + const autoLoadMedia = contentPolicy?.autoLoadMedia ?? true const [showNsfw, setShowNsfw] = useState(false) const muteList = useMuteListOptional() const mutePubkeySet = muteList?.mutePubkeySet ?? new Set() @@ -213,12 +214,12 @@ export default function Note({ className={className} event={event} hideMetadata={hideMetadata} - lazyMedia={!showFull} + lazyMedia={!showFull || !autoLoadMedia} fullCalendarInvite={fullCalendarInvite} /> ) }, - [event, fullCalendarInvite, showFull] + [event, fullCalendarInvite, showFull, autoLoadMedia] ) let content: React.ReactNode diff --git a/src/components/NoteCard/MainNoteCard.tsx b/src/components/NoteCard/MainNoteCard.tsx index 3796bb18..6754f36f 100644 --- a/src/components/NoteCard/MainNoteCard.tsx +++ b/src/components/NoteCard/MainNoteCard.tsx @@ -21,7 +21,8 @@ export default function MainNoteCard({ pinned = false, hideParentNotePreview = false, zapPollVoteHighlightOption, - bottomNoteLabel + bottomNoteLabel, + showFull = false }: { event: Event className?: string @@ -34,6 +35,7 @@ export default function MainNoteCard({ hideParentNotePreview?: boolean zapPollVoteHighlightOption?: number bottomNoteLabel?: string + showFull?: boolean }) { const { t } = useTranslation() const { navigateToNote } = useSmartNoteNavigationOptional() @@ -93,6 +95,7 @@ export default function MainNoteCard({ disableClick={true} hideParentNotePreview={hideParentNotePreview} zapPollVoteHighlightOption={zapPollVoteHighlightOption} + showFull={showFull} /> {showNoteStatsRow ? ( diff --git a/src/components/VideoPlayer/index.tsx b/src/components/VideoPlayer/index.tsx index 41f24141..3d3e06af 100644 --- a/src/components/VideoPlayer/index.tsx +++ b/src/components/VideoPlayer/index.tsx @@ -62,6 +62,7 @@ export default function VideoPlayer({ src, className, poster }: { src: string; c ref={videoRef} controls playsInline + preload="none" className={cn('rounded-lg max-h-[80vh] sm:max-h-[60vh] border w-full h-auto max-w-full', className)} src={src} poster={poster} diff --git a/src/lib/tag.ts b/src/lib/tag.ts index b3d0e500..47d11d5d 100644 --- a/src/lib/tag.ts +++ b/src/lib/tag.ts @@ -229,6 +229,21 @@ export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImeta if (thumbUrl) { imeta.thumb = cleanUrl(thumbUrl) } + + // Parse file size (bytes) + let fileSize: number | undefined + const sizeItem = tag.find((item) => item.startsWith('size ')) + if (sizeItem) { + fileSize = parseInt(sizeItem.slice(5), 10) + } else { + const sizeIndex = tag.findIndex((item) => item === 'size') + if (sizeIndex !== -1 && sizeIndex + 1 < tag.length) { + fileSize = parseInt(tag[sizeIndex + 1], 10) + } + } + if (fileSize && !isNaN(fileSize)) { + imeta.size = fileSize + } return imeta } diff --git a/src/types/index.d.ts b/src/types/index.d.ts index ce99676f..23af404c 100644 --- a/src/types/index.d.ts +++ b/src/types/index.d.ts @@ -177,6 +177,7 @@ export type TImetaInfo = { fallback?: string[] // Array of fallback URLs image?: string // Poster/thumbnail image URL (for videos) thumb?: string // Thumbnail URL for images + size?: number // File size in bytes (NIP-94) } export type TPublishOptions = {