style: resize images and videos for better visuals

This commit is contained in:
codytseng
2025-05-08 22:34:33 +08:00
parent ee6780dc69
commit aa24ad83e5
6 changed files with 106 additions and 169 deletions

View File

@@ -27,128 +27,99 @@ import ImageGallery from '../ImageGallery'
import VideoPlayer from '../VideoPlayer'
import WebPreview from '../WebPreview'
const Content = memo(
({
event,
className,
size = 'normal'
}: {
event: Event
className?: string
size?: 'normal' | 'small'
}) => {
const nodes = parseContent(event.content, [
EmbeddedImageParser,
EmbeddedVideoParser,
EmbeddedNormalUrlParser,
EmbeddedWebsocketUrlParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedHashtagParser,
EmbeddedEmojiParser
])
const Content = memo(({ event, className }: { event: Event; className?: string }) => {
const nodes = parseContent(event.content, [
EmbeddedImageParser,
EmbeddedVideoParser,
EmbeddedNormalUrlParser,
EmbeddedWebsocketUrlParser,
EmbeddedEventParser,
EmbeddedMentionParser,
EmbeddedHashtagParser,
EmbeddedEmojiParser
])
const imageInfos = event.tags
.map((tag) => extractImageInfoFromTag(tag))
.filter(Boolean) as TImageInfo[]
const allImages = nodes
.map((node) => {
if (node.type === 'image') {
const imageInfo = imageInfos.find((image) => image.url === node.data)
return imageInfo ?? { url: node.data }
const imageInfos = event.tags
.map((tag) => extractImageInfoFromTag(tag))
.filter(Boolean) as TImageInfo[]
const allImages = nodes
.map((node) => {
if (node.type === 'image') {
const imageInfo = imageInfos.find((image) => image.url === node.data)
return imageInfo ?? { url: node.data }
}
if (node.type === 'images') {
const urls = Array.isArray(node.data) ? node.data : [node.data]
return urls.map((url) => {
const imageInfo = imageInfos.find((image) => image.url === url)
return imageInfo ?? { url }
})
}
return null
})
.filter(Boolean)
.flat() as TImageInfo[]
let imageIndex = 0
const emojiInfos = extractEmojiInfosFromTags(event.tags)
const lastNormalUrlNode = nodes.findLast((node) => node.type === 'url')
const lastNormalUrl =
typeof lastNormalUrlNode?.data === 'string' ? lastNormalUrlNode.data : undefined
return (
<div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
}
if (node.type === 'images') {
const urls = Array.isArray(node.data) ? node.data : [node.data]
return urls.map((url) => {
const imageInfo = imageInfos.find((image) => image.url === url)
return imageInfo ?? { url }
})
if (node.type === 'image' || node.type === 'images') {
const start = imageIndex
const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1)
imageIndex = end
return (
<ImageGallery
className="mt-2"
key={index}
images={allImages}
isNsfw={isNsfwEvent(event)}
start={start}
end={end}
/>
)
}
if (node.type === 'video') {
return (
<VideoPlayer className="mt-2" key={index} src={node.data} isNsfw={isNsfwEvent(event)} />
)
}
if (node.type === 'url') {
return <EmbeddedNormalUrl url={node.data} key={index} />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl url={node.data} key={index} />
}
if (node.type === 'event') {
const id = node.data.split(':')[1]
return <EmbeddedNote key={index} noteId={id} className="mt-2" />
}
if (node.type === 'mention') {
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'hashtag') {
return <EmbeddedHashtag hashtag={node.data} key={index} />
}
if (node.type === 'emoji') {
const shortcode = node.data.split(':')[1]
const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
if (!emoji) return node.data
return <Emoji emoji={emoji} key={index} className="size-4" />
}
return null
})
.filter(Boolean)
.flat() as TImageInfo[]
let imageIndex = 0
const emojiInfos = extractEmojiInfosFromTags(event.tags)
const lastNormalUrlNode = nodes.findLast((node) => node.type === 'url')
const lastNormalUrl =
typeof lastNormalUrlNode?.data === 'string' ? lastNormalUrlNode.data : undefined
return (
<div className={cn('text-wrap break-words whitespace-pre-wrap', className)}>
{nodes.map((node, index) => {
if (node.type === 'text') {
return node.data
}
if (node.type === 'image' || node.type === 'images') {
const start = imageIndex
const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1)
imageIndex = end
return (
<ImageGallery
className={`${size === 'small' ? 'mt-1' : 'mt-2'}`}
key={index}
images={allImages}
isNsfw={isNsfwEvent(event)}
size={size}
start={start}
end={end}
/>
)
}
if (node.type === 'video') {
return (
<VideoPlayer
className={size === 'small' ? 'mt-1' : 'mt-2'}
key={index}
src={node.data}
isNsfw={isNsfwEvent(event)}
size={size}
/>
)
}
if (node.type === 'url') {
return <EmbeddedNormalUrl url={node.data} key={index} />
}
if (node.type === 'websocket-url') {
return <EmbeddedWebsocketUrl url={node.data} key={index} />
}
if (node.type === 'event') {
const id = node.data.split(':')[1]
return (
<EmbeddedNote
key={index}
noteId={id}
className={size === 'small' ? 'mt-1' : 'mt-2'}
/>
)
}
if (node.type === 'mention') {
return <EmbeddedMention key={index} userId={node.data.split(':')[1]} />
}
if (node.type === 'hashtag') {
return <EmbeddedHashtag hashtag={node.data} key={index} />
}
if (node.type === 'emoji') {
const shortcode = node.data.split(':')[1]
const emoji = emojiInfos.find((e) => e.shortcode === shortcode)
if (!emoji) return node.data
return <Emoji emoji={emoji} key={index} className="size-4" />
}
return null
})}
{lastNormalUrl && (
<WebPreview
className={size === 'small' ? 'mt-1' : 'mt-2'}
url={lastNormalUrl}
size={size}
/>
)}
</div>
)
}
)
})}
{lastNormalUrl && <WebPreview className="mt-2" url={lastNormalUrl} />}
</div>
)
})
Content.displayName = 'Content'
export default Content

View File

@@ -1,6 +1,5 @@
import { randomString } from '@/lib/random'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import modalManager from '@/services/modal-manager.service'
import { TImageInfo } from '@/types'
import { ReactNode, useEffect, useMemo, useState } from 'react'
@@ -14,19 +13,16 @@ export default function ImageGallery({
className,
images,
isNsfw = false,
size = 'normal',
start = 0,
end = images.length
}: {
className?: string
images: TImageInfo[]
isNsfw?: boolean
size?: 'normal' | 'small'
start?: number
end?: number
}) {
const id = useMemo(() => `image-gallery-${randomString()}`, [])
const { isSmallScreen } = useScreenSize()
const [index, setIndex] = useState(-1)
useEffect(() => {
if (index >= 0) {
@@ -50,34 +46,21 @@ export default function ImageGallery({
imageContent = (
<Image
key={0}
className={cn('rounded-lg', size === 'small' ? 'max-h-[15vh]' : 'max-h-[30vh]')}
className="rounded-lg max-h-[80vh] sm:max-h-[50vh] border"
classNames={{
errorPlaceholder: cn('aspect-square', size === 'small' ? 'h-[15vh]' : 'h-[30vh]')
errorPlaceholder: 'aspect-square h-[30vh]'
}}
image={displayImages[0]}
onClick={(e) => handlePhotoClick(e, 0)}
/>
)
} else if (size === 'small') {
} else if (displayImages.length === 2 || displayImages.length === 4) {
imageContent = (
<div className="grid grid-cols-4 gap-2">
<div className="grid grid-cols-2 gap-2 w-full">
{displayImages.map((image, i) => (
<Image
key={i}
className={cn('aspect-square w-full rounded-lg')}
image={image}
onClick={(e) => handlePhotoClick(e, i)}
/>
))}
</div>
)
} else if (isSmallScreen && (displayImages.length === 2 || displayImages.length === 4)) {
imageContent = (
<div className="grid grid-cols-2 gap-2">
{displayImages.map((image, i) => (
<Image
key={i}
className={cn('aspect-square w-full rounded-lg')}
className="aspect-square w-full rounded-lg border"
image={image}
onClick={(e) => handlePhotoClick(e, i)}
/>
@@ -90,7 +73,7 @@ export default function ImageGallery({
{displayImages.map((image, i) => (
<Image
key={i}
className={cn('aspect-square w-full rounded-lg')}
className="aspect-square w-full rounded-lg border"
image={image}
onClick={(e) => handlePhotoClick(e, i)}
/>

View File

@@ -1,7 +1,6 @@
import { Button } from '@/components/ui/button'
import { Separator } from '@/components/ui/separator'
import { toNote } from '@/lib/link'
import { cn } from '@/lib/utils'
import { useSecondaryPage } from '@/PageManager'
import { Event } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react'
@@ -35,7 +34,7 @@ export default function MainNoteCard({
const checkHeight = () => {
const fullHeight = contentEl.scrollHeight
if (fullHeight > 900) {
if (fullHeight > 1000) {
setShouldCollapse(true)
}
}
@@ -91,7 +90,7 @@ export default function MainNoteCard({
</div>
)}
</div>
{!embedded && <NoteStats className={cn('mt-3', embedded ? '' : 'px-4')} event={event} />}
{!embedded && <NoteStats className="mt-3 px-4" event={event} />}
</div>
{!embedded && <Separator />}
</div>

View File

@@ -55,7 +55,7 @@ export default function ReplyNote({
</div>
{parentEvent && (
<ParentNotePreview
className="mt-1"
className="mt-2"
event={parentEvent}
onClick={(e) => {
e.stopPropagation()
@@ -65,7 +65,7 @@ export default function ReplyNote({
)}
{show ? (
<>
<Content className="mt-1" event={event} size="small" />
<Content className="mt-2" event={event} />
<NoteStats className="mt-2" event={event} variant="reply" />
</>
) : (

View File

@@ -6,13 +6,11 @@ import NsfwOverlay from '../NsfwOverlay'
export default function VideoPlayer({
src,
className,
isNsfw = false,
size = 'normal'
isNsfw = false
}: {
src: string
className?: string
isNsfw?: boolean
size?: 'normal' | 'small'
}) {
const videoRef = useRef<HTMLVideoElement>(null)
const containerRef = useRef<HTMLDivElement>(null)
@@ -51,7 +49,7 @@ export default function VideoPlayer({
ref={videoRef}
controls
playsInline
className={cn('rounded-lg', size === 'small' ? 'max-h-[30vh]' : 'max-h-[50vh]', className)}
className={cn('rounded-lg max-h-[80vh] sm:max-h-[50vh] border', className)}
src={src}
onClick={(e) => e.stopPropagation()}
onPlay={(event) => {

View File

@@ -4,15 +4,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useMemo } from 'react'
import Image from '../Image'
export default function WebPreview({
url,
className,
size = 'normal'
}: {
url: string
className?: string
size?: 'normal' | 'small'
}) {
export default function WebPreview({ url, className }: { url: string; className?: string }) {
const { isSmallScreen } = useScreenSize()
const { title, description, image } = useFetchWebMetadata(url)
const hostname = useMemo(() => {
@@ -56,20 +48,14 @@ export default function WebPreview({
{image && (
<Image
image={{ url: image }}
className={`rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground ${size === 'normal' ? 'h-44' : 'h-24'}`}
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
hideIfError
/>
)}
<div className="flex-1 w-0 p-2">
<div className="text-xs text-muted-foreground">{hostname}</div>
<div className={`font-semibold ${size === 'normal' ? 'line-clamp-2' : 'line-clamp-1'}`}>
{title}
</div>
<div
className={`text-xs text-muted-foreground ${size === 'normal' ? 'line-clamp-5' : 'line-clamp-2'}`}
>
{description}
</div>
<div className="font-semibold line-clamp-2">{title}</div>
<div className="text-xs text-muted-foreground line-clamp-5">{description}</div>
</div>
</div>
)