style: resize images and videos for better visuals
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user