Browse Source

bug-fixes for performance

imwald
Silberengel 3 weeks ago
parent
commit
24b87496f9
  1. 10
      src/components/Embedded/EmbeddedNote.tsx
  2. 231
      src/components/FavoriteRelaysActiveStrip/index.tsx
  3. 15
      src/components/Image/index.tsx
  4. 10
      src/components/Note/MarkdownArticle/MarkdownArticle.tsx
  5. 5
      src/components/Note/index.tsx
  6. 5
      src/components/NoteCard/MainNoteCard.tsx
  7. 1
      src/components/VideoPlayer/index.tsx
  8. 15
      src/lib/tag.ts
  9. 1
      src/types/index.d.ts

10
src/components/Embedded/EmbeddedNote.tsx

@ -70,11 +70,13 @@ function canSearchOnExternalRelays(noteId: string): boolean { @@ -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({ @@ -99,6 +101,7 @@ export function EmbeddedNote({
noteId={noteId}
className={className}
containingEvent={containingEvent}
showFull={showFull}
/>
)
}
@ -160,11 +163,13 @@ function EmbeddedNoteInvalid({ @@ -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<Event | undefined>(undefined)
@ -254,6 +259,7 @@ function EmbeddedNoteContent({ @@ -254,6 +259,7 @@ function EmbeddedNoteContent({
className={cn('w-full', className)}
event={finalEvent}
embedded
showFull={showFull}
originalNoteId={noteId}
/>
</div>

231
src/components/FavoriteRelaysActiveStrip/index.tsx

@ -1,12 +1,7 @@ @@ -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' @@ -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 @@ -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 = (
<div className="flex w-full min-w-0 max-w-full flex-row flex-wrap items-center gap-y-1 pl-0.5">
{slice.map((pk, i) => (
<HoverCard key={pk} openDelay={180} closeDelay={80}>
<HoverCardTrigger asChild>
<div
className={cn(
'relative shrink-0 rounded-full ring-2 ring-background transition-[z-index] duration-150',
i > 0 && AVATAR_OVERLAP
)}
style={{ zIndex: i + 1 }}
>
<UserAvatar userId={pk} size={avatarSize} />
</div>
</HoverCardTrigger>
<HoverCardContent side="top" className="w-auto max-w-[min(18rem,calc(100vw-2rem))] py-2 px-3">
<SimpleUsername userId={pk} showAt className="text-sm font-medium" />
</HoverCardContent>
</HoverCard>
))}
{extra > 0 ? (
<div
className={cn(
'relative z-[20] flex h-7 min-w-7 shrink-0 items-center justify-center rounded-full bg-muted px-1.5 text-xs font-medium text-muted-foreground ring-2 ring-background',
slice.length > 0 && AVATAR_OVERLAP
)}
title={String(extra)}
>
+{extra > 99 ? '99+' : extra}
</div>
) : null}
</div>
)
return (
<div className={cn('flex w-full min-w-0 max-w-full flex-1 items-start', rowClassName)}>
{row}
</div>
)
}
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 = (
<div className="flex shrink-0 flex-col gap-1">
<span className={cn('tabular-nums', labelClassName)}>
{t('Relay pulse follows', { count: followCount })}
</span>
{onOpenFollowsNotes && mobileBar ? (
<Button
variant="ghost"
size="icon"
className="size-6 shrink-0 self-start"
aria-label={t('See the newest notes from your follows')}
title={t('See the newest notes from your follows')}
onClick={onOpenFollowsNotes}
>
<FileText className="size-3.5" />
</Button>
) : null}
</div>
)
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 (
<div className={cn('flex min-w-0 flex-col gap-2', stackClassName)}>
<div className={cn('flex min-w-0 flex-col gap-1.5', stackClassName)}>
{followCount > 0 ? (
<div
className={
mobileBar ? groupRowClass : sidebarSectionClass
}
>
{mobileBar ? (
<span className="flex min-w-0 shrink-0 items-center gap-1">
<span className={cn('tabular-nums', labelClassName)}>
{t('Relay pulse follows', { count: followCount })}
</span>
{onOpenFollowsNotes ? (
<Button
variant="ghost"
size="icon"
className="size-6 shrink-0"
aria-label={t('See the newest notes from your follows')}
title={t('See the newest notes from your follows')}
onClick={onOpenFollowsNotes}
>
<FileText className="size-3.5" />
</Button>
) : null}
</span>
) : (
followsLabelBlock
)}
<OverlappingAvatars
pubkeys={followPubkeysForAvatars}
max={maxFollow}
avatarSize={avatarSize}
rowClassName={mobileBar ? undefined : 'justify-start'}
/>
<div className={groupRowClass}>
<span className={cn('tabular-nums', labelClassName)}>
{t('Relay pulse follows', { count: followCount })}
</span>
{onOpenFollowsNotes ? (
<Button
variant="ghost"
size="icon"
className={cn('shrink-0', mobileBar ? 'size-6' : 'size-5')}
aria-label={t('See the newest notes from your follows')}
title={t('See the newest notes from your follows')}
onClick={onOpenFollowsNotes}
>
<FileText className={mobileBar ? 'size-3.5' : 'size-3'} />
</Button>
) : null}
</div>
) : null}
{otherCount > 0 ? (
<div className={mobileBar ? groupRowClass : sidebarSectionClass}>
<span className={cn('min-w-0 shrink-0 tabular-nums', labelClassName)}>
{t('Relay pulse others', { count: otherCount })}
</span>
<OverlappingAvatars
pubkeys={otherPubkeysForAvatars}
max={maxOther}
avatarSize={avatarSize}
rowClassName={mobileBar ? undefined : 'justify-start'}
/>
</div>
<span className={cn('min-w-0 tabular-nums', labelClassName)}>
{t('Relay pulse others', { count: otherCount })}
</span>
) : null}
</div>
)
@ -216,34 +89,15 @@ export function FavoriteRelaysActiveStripMobileBar({ className }: { className?: @@ -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?: @@ -288,7 +142,7 @@ export function FavoriteRelaysActiveStripMobileBar({ className }: { className?:
className
)}
>
<div className="flex w-full min-w-0 flex-col gap-3">
<div className="flex w-full min-w-0 flex-col gap-1.5">
<div className="flex min-w-0 max-w-full items-center justify-between gap-2">
<div className="flex min-w-0 shrink items-center gap-2">
<p className="text-xs font-medium leading-tight text-foreground">{t('Relay pulse')}</p>
@ -300,15 +154,10 @@ export function FavoriteRelaysActiveStripMobileBar({ className }: { className?: @@ -300,15 +154,10 @@ export function FavoriteRelaysActiveStripMobileBar({ className }: { className?:
</p>
) : null}
</div>
<ActiveAvatarGroups
<ActiveCountGroups
variant="mobileBar"
followPubkeysForAvatars={followPubkeysForAvatars}
otherPubkeysForAvatars={otherPubkeysForAvatars}
followCount={followCount}
otherCount={otherCount}
maxFollow={MOBILE_MAX_FOLLOW}
maxOther={MOBILE_MAX_OTHER}
avatarSize="small"
labelClassName="text-[0.7rem] font-medium text-muted-foreground"
stackClassName="w-full min-w-0 max-w-full"
onOpenFollowsNotes={pubkey ? () => navigate('follows-latest') : undefined}
@ -323,34 +172,15 @@ export function FavoriteRelaysActiveStripSidebar({ className }: { className?: st @@ -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 @@ -437,14 +267,9 @@ export function FavoriteRelaysActiveStripSidebar({ className }: { className?: st
) : null}
</div>
<div className="max-xl:flex max-xl:justify-center">
<ActiveAvatarGroups
followPubkeysForAvatars={followPubkeysForAvatars}
otherPubkeysForAvatars={otherPubkeysForAvatars}
<ActiveCountGroups
followCount={followCount}
otherCount={otherCount}
maxFollow={SIDEBAR_MAX_FOLLOW}
maxOther={SIDEBAR_MAX_OTHER}
avatarSize="xSmall"
labelClassName="text-[0.6rem] font-medium text-muted-foreground xl:px-1"
stackClassName="w-full max-xl:items-center"
onOpenFollowsNotes={pubkey ? () => navigate('follows-latest') : undefined}

15
src/components/Image/index.tsx

@ -22,8 +22,14 @@ function wrapperReserveStyle( @@ -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({ @@ -167,6 +173,11 @@ export default function Image({
)}
/>
)}
{!revealed && holdUntilClick && fileSizeBytes != null && (
<span className="absolute bottom-2 right-2 z-20 rounded-full bg-black/60 px-2 py-0.5 text-[11px] font-medium text-white/90 backdrop-blur-sm select-none pointer-events-none">
{formatFileSize(fileSizeBytes)}
</span>
)}
</span>
)}
{!showErrorState && revealed && (
@ -176,7 +187,7 @@ export default function Image({ @@ -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}

10
src/components/Note/MarkdownArticle/MarkdownArticle.tsx

@ -2423,7 +2423,7 @@ export function parseMarkdownContentLegacy( @@ -2423,7 +2423,7 @@ export function parseMarkdownContentLegacy(
// Embedded events should be block-level and fill width
parts.push(
<div key={`nostr-${patternIdx}`} className="w-full my-2">
<EmbeddedNote noteId={bech32Id} />
<EmbeddedNote noteId={bech32Id} showFull={!lazyMedia} />
</div>
)
}
@ -3200,7 +3200,7 @@ function parseMarkdownContentMarked( @@ -3200,7 +3200,7 @@ function parseMarkdownContentMarked(
}
return (
<div key={`${key}-nostr-event`} className="w-full my-2">
<EmbeddedNote noteId={bech32Id} containingEvent={containingEvent} />
<EmbeddedNote noteId={bech32Id} containingEvent={containingEvent} showFull={!lazyMedia} />
</div>
)
}
@ -3341,7 +3341,7 @@ function parseMarkdownContentMarked( @@ -3341,7 +3341,7 @@ function parseMarkdownContentMarked(
}
return (
<div key={`${key}-line-event-${lineIdx}`} className="w-full my-2">
<EmbeddedNote noteId={bech32Id} containingEvent={containingEvent} />
<EmbeddedNote noteId={bech32Id} containingEvent={containingEvent} showFull={!lazyMedia} />
</div>
)
}
@ -3386,7 +3386,7 @@ function parseMarkdownContentMarked( @@ -3386,7 +3386,7 @@ function parseMarkdownContentMarked(
} else {
nodes.push(
<div key={`${key}-nostr-raw-event-${segmentIdx++}`} className="w-full my-2">
<EmbeddedNote noteId={bech32Id} containingEvent={containingEvent} />
<EmbeddedNote noteId={bech32Id} containingEvent={containingEvent} showFull={!lazyMedia} />
</div>
)
}
@ -3553,7 +3553,7 @@ function parseMarkdownContentMarked( @@ -3553,7 +3553,7 @@ function parseMarkdownContentMarked(
} else {
nodes.push(
<div key={`${key}-nostr-inline-event-${idx}`} className="w-full my-2">
<EmbeddedNote noteId={bech32} containingEvent={containingEvent} />
<EmbeddedNote noteId={bech32} containingEvent={containingEvent} showFull={!lazyMedia} />
</div>
)
}

5
src/components/Note/index.tsx

@ -129,6 +129,7 @@ export default function Note({ @@ -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<string>()
@ -213,12 +214,12 @@ export default function Note({ @@ -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

5
src/components/NoteCard/MainNoteCard.tsx

@ -21,7 +21,8 @@ export default function MainNoteCard({ @@ -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({ @@ -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({ @@ -93,6 +95,7 @@ export default function MainNoteCard({
disableClick={true}
hideParentNotePreview={hideParentNotePreview}
zapPollVoteHighlightOption={zapPollVoteHighlightOption}
showFull={showFull}
/>
</Collapsible>
{showNoteStatsRow ? (

1
src/components/VideoPlayer/index.tsx

@ -62,6 +62,7 @@ export default function VideoPlayer({ src, className, poster }: { src: string; c @@ -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}

15
src/lib/tag.ts

@ -229,6 +229,21 @@ export function getImetaInfoFromImetaTag(tag: string[], pubkey?: string): TImeta @@ -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
}

1
src/types/index.d.ts vendored

@ -177,6 +177,7 @@ export type TImetaInfo = { @@ -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 = {

Loading…
Cancel
Save