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 VideoPlayer from '../VideoPlayer'
|
||||||
import WebPreview from '../WebPreview'
|
import WebPreview from '../WebPreview'
|
||||||
|
|
||||||
const Content = memo(
|
const Content = memo(({ event, className }: { event: Event; className?: string }) => {
|
||||||
({
|
const nodes = parseContent(event.content, [
|
||||||
event,
|
EmbeddedImageParser,
|
||||||
className,
|
EmbeddedVideoParser,
|
||||||
size = 'normal'
|
EmbeddedNormalUrlParser,
|
||||||
}: {
|
EmbeddedWebsocketUrlParser,
|
||||||
event: Event
|
EmbeddedEventParser,
|
||||||
className?: string
|
EmbeddedMentionParser,
|
||||||
size?: 'normal' | 'small'
|
EmbeddedHashtagParser,
|
||||||
}) => {
|
EmbeddedEmojiParser
|
||||||
const nodes = parseContent(event.content, [
|
])
|
||||||
EmbeddedImageParser,
|
|
||||||
EmbeddedVideoParser,
|
|
||||||
EmbeddedNormalUrlParser,
|
|
||||||
EmbeddedWebsocketUrlParser,
|
|
||||||
EmbeddedEventParser,
|
|
||||||
EmbeddedMentionParser,
|
|
||||||
EmbeddedHashtagParser,
|
|
||||||
EmbeddedEmojiParser
|
|
||||||
])
|
|
||||||
|
|
||||||
const imageInfos = event.tags
|
const imageInfos = event.tags
|
||||||
.map((tag) => extractImageInfoFromTag(tag))
|
.map((tag) => extractImageInfoFromTag(tag))
|
||||||
.filter(Boolean) as TImageInfo[]
|
.filter(Boolean) as TImageInfo[]
|
||||||
const allImages = nodes
|
const allImages = nodes
|
||||||
.map((node) => {
|
.map((node) => {
|
||||||
if (node.type === 'image') {
|
if (node.type === 'image') {
|
||||||
const imageInfo = imageInfos.find((image) => image.url === node.data)
|
const imageInfo = imageInfos.find((image) => image.url === node.data)
|
||||||
return imageInfo ?? { 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') {
|
if (node.type === 'image' || node.type === 'images') {
|
||||||
const urls = Array.isArray(node.data) ? node.data : [node.data]
|
const start = imageIndex
|
||||||
return urls.map((url) => {
|
const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1)
|
||||||
const imageInfo = imageInfos.find((image) => image.url === url)
|
imageIndex = end
|
||||||
return imageInfo ?? { url }
|
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
|
return null
|
||||||
})
|
})}
|
||||||
.filter(Boolean)
|
{lastNormalUrl && <WebPreview className="mt-2" url={lastNormalUrl} />}
|
||||||
.flat() as TImageInfo[]
|
</div>
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
Content.displayName = 'Content'
|
Content.displayName = 'Content'
|
||||||
export default Content
|
export default Content
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { randomString } from '@/lib/random'
|
import { randomString } from '@/lib/random'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
|
||||||
import modalManager from '@/services/modal-manager.service'
|
import modalManager from '@/services/modal-manager.service'
|
||||||
import { TImageInfo } from '@/types'
|
import { TImageInfo } from '@/types'
|
||||||
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
import { ReactNode, useEffect, useMemo, useState } from 'react'
|
||||||
@@ -14,19 +13,16 @@ export default function ImageGallery({
|
|||||||
className,
|
className,
|
||||||
images,
|
images,
|
||||||
isNsfw = false,
|
isNsfw = false,
|
||||||
size = 'normal',
|
|
||||||
start = 0,
|
start = 0,
|
||||||
end = images.length
|
end = images.length
|
||||||
}: {
|
}: {
|
||||||
className?: string
|
className?: string
|
||||||
images: TImageInfo[]
|
images: TImageInfo[]
|
||||||
isNsfw?: boolean
|
isNsfw?: boolean
|
||||||
size?: 'normal' | 'small'
|
|
||||||
start?: number
|
start?: number
|
||||||
end?: number
|
end?: number
|
||||||
}) {
|
}) {
|
||||||
const id = useMemo(() => `image-gallery-${randomString()}`, [])
|
const id = useMemo(() => `image-gallery-${randomString()}`, [])
|
||||||
const { isSmallScreen } = useScreenSize()
|
|
||||||
const [index, setIndex] = useState(-1)
|
const [index, setIndex] = useState(-1)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -50,34 +46,21 @@ export default function ImageGallery({
|
|||||||
imageContent = (
|
imageContent = (
|
||||||
<Image
|
<Image
|
||||||
key={0}
|
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={{
|
classNames={{
|
||||||
errorPlaceholder: cn('aspect-square', size === 'small' ? 'h-[15vh]' : 'h-[30vh]')
|
errorPlaceholder: 'aspect-square h-[30vh]'
|
||||||
}}
|
}}
|
||||||
image={displayImages[0]}
|
image={displayImages[0]}
|
||||||
onClick={(e) => handlePhotoClick(e, 0)}
|
onClick={(e) => handlePhotoClick(e, 0)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
} else if (size === 'small') {
|
} else if (displayImages.length === 2 || displayImages.length === 4) {
|
||||||
imageContent = (
|
imageContent = (
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-2 gap-2 w-full">
|
||||||
{displayImages.map((image, i) => (
|
{displayImages.map((image, i) => (
|
||||||
<Image
|
<Image
|
||||||
key={i}
|
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)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</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')}
|
|
||||||
image={image}
|
image={image}
|
||||||
onClick={(e) => handlePhotoClick(e, i)}
|
onClick={(e) => handlePhotoClick(e, i)}
|
||||||
/>
|
/>
|
||||||
@@ -90,7 +73,7 @@ export default function ImageGallery({
|
|||||||
{displayImages.map((image, i) => (
|
{displayImages.map((image, i) => (
|
||||||
<Image
|
<Image
|
||||||
key={i}
|
key={i}
|
||||||
className={cn('aspect-square w-full rounded-lg')}
|
className="aspect-square w-full rounded-lg border"
|
||||||
image={image}
|
image={image}
|
||||||
onClick={(e) => handlePhotoClick(e, i)}
|
onClick={(e) => handlePhotoClick(e, i)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { toNote } from '@/lib/link'
|
import { toNote } from '@/lib/link'
|
||||||
import { cn } from '@/lib/utils'
|
|
||||||
import { useSecondaryPage } from '@/PageManager'
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
@@ -35,7 +34,7 @@ export default function MainNoteCard({
|
|||||||
|
|
||||||
const checkHeight = () => {
|
const checkHeight = () => {
|
||||||
const fullHeight = contentEl.scrollHeight
|
const fullHeight = contentEl.scrollHeight
|
||||||
if (fullHeight > 900) {
|
if (fullHeight > 1000) {
|
||||||
setShouldCollapse(true)
|
setShouldCollapse(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,7 +90,7 @@ export default function MainNoteCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!embedded && <NoteStats className={cn('mt-3', embedded ? '' : 'px-4')} event={event} />}
|
{!embedded && <NoteStats className="mt-3 px-4" event={event} />}
|
||||||
</div>
|
</div>
|
||||||
{!embedded && <Separator />}
|
{!embedded && <Separator />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export default function ReplyNote({
|
|||||||
</div>
|
</div>
|
||||||
{parentEvent && (
|
{parentEvent && (
|
||||||
<ParentNotePreview
|
<ParentNotePreview
|
||||||
className="mt-1"
|
className="mt-2"
|
||||||
event={parentEvent}
|
event={parentEvent}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -65,7 +65,7 @@ export default function ReplyNote({
|
|||||||
)}
|
)}
|
||||||
{show ? (
|
{show ? (
|
||||||
<>
|
<>
|
||||||
<Content className="mt-1" event={event} size="small" />
|
<Content className="mt-2" event={event} />
|
||||||
<NoteStats className="mt-2" event={event} variant="reply" />
|
<NoteStats className="mt-2" event={event} variant="reply" />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import NsfwOverlay from '../NsfwOverlay'
|
|||||||
export default function VideoPlayer({
|
export default function VideoPlayer({
|
||||||
src,
|
src,
|
||||||
className,
|
className,
|
||||||
isNsfw = false,
|
isNsfw = false
|
||||||
size = 'normal'
|
|
||||||
}: {
|
}: {
|
||||||
src: string
|
src: string
|
||||||
className?: string
|
className?: string
|
||||||
isNsfw?: boolean
|
isNsfw?: boolean
|
||||||
size?: 'normal' | 'small'
|
|
||||||
}) {
|
}) {
|
||||||
const videoRef = useRef<HTMLVideoElement>(null)
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
@@ -51,7 +49,7 @@ export default function VideoPlayer({
|
|||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
controls
|
controls
|
||||||
playsInline
|
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}
|
src={src}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
onPlay={(event) => {
|
onPlay={(event) => {
|
||||||
|
|||||||
@@ -4,15 +4,7 @@ import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import Image from '../Image'
|
import Image from '../Image'
|
||||||
|
|
||||||
export default function WebPreview({
|
export default function WebPreview({ url, className }: { url: string; className?: string }) {
|
||||||
url,
|
|
||||||
className,
|
|
||||||
size = 'normal'
|
|
||||||
}: {
|
|
||||||
url: string
|
|
||||||
className?: string
|
|
||||||
size?: 'normal' | 'small'
|
|
||||||
}) {
|
|
||||||
const { isSmallScreen } = useScreenSize()
|
const { isSmallScreen } = useScreenSize()
|
||||||
const { title, description, image } = useFetchWebMetadata(url)
|
const { title, description, image } = useFetchWebMetadata(url)
|
||||||
const hostname = useMemo(() => {
|
const hostname = useMemo(() => {
|
||||||
@@ -56,20 +48,14 @@ export default function WebPreview({
|
|||||||
{image && (
|
{image && (
|
||||||
<Image
|
<Image
|
||||||
image={{ url: 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
|
hideIfError
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 w-0 p-2">
|
<div className="flex-1 w-0 p-2">
|
||||||
<div className="text-xs text-muted-foreground">{hostname}</div>
|
<div className="text-xs text-muted-foreground">{hostname}</div>
|
||||||
<div className={`font-semibold ${size === 'normal' ? 'line-clamp-2' : 'line-clamp-1'}`}>
|
<div className="font-semibold line-clamp-2">{title}</div>
|
||||||
{title}
|
<div className="text-xs text-muted-foreground line-clamp-5">{description}</div>
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={`text-xs text-muted-foreground ${size === 'normal' ? 'line-clamp-5' : 'line-clamp-2'}`}
|
|
||||||
>
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user