feat: 🌸
This commit is contained in:
parent
74e04e1c7d
commit
e91b2648cc
41 changed files with 756 additions and 92 deletions
|
|
@ -46,7 +46,7 @@ const Content = memo(({ event, className }: { event: Event; className?: string }
|
|||
])
|
||||
|
||||
const imageInfos = event.tags
|
||||
.map((tag) => extractImageInfoFromTag(tag))
|
||||
.map((tag) => extractImageInfoFromTag(tag, event.pubkey))
|
||||
.filter(Boolean) as TImageInfo[]
|
||||
const allImages = nodes
|
||||
.map((node) => {
|
||||
|
|
@ -56,13 +56,15 @@ const Content = memo(({ event, className }: { event: Event; className?: string }
|
|||
return imageInfo
|
||||
}
|
||||
const tag = mediaUpload.getImetaTagByUrl(node.data)
|
||||
return tag ? extractImageInfoFromTag(tag) : { url: node.data }
|
||||
return tag
|
||||
? extractImageInfoFromTag(tag, event.pubkey)
|
||||
: { url: node.data, pubkey: event.pubkey }
|
||||
}
|
||||
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 imageInfo ?? { url, pubkey: event.pubkey }
|
||||
})
|
||||
}
|
||||
return null
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { cn } from '@/lib/utils'
|
||||
import client from '@/services/client.service'
|
||||
import { TImageInfo } from '@/types'
|
||||
import { getHashFromURL } from 'blossom-client-sdk'
|
||||
import { decode } from 'blurhash'
|
||||
import { ImageOff } from 'lucide-react'
|
||||
import { HTMLAttributes, useEffect, useState } from 'react'
|
||||
|
||||
export default function Image({
|
||||
image: { url, blurHash },
|
||||
image: { url, blurHash, pubkey },
|
||||
alt,
|
||||
className = '',
|
||||
classNames = {},
|
||||
|
|
@ -27,6 +29,8 @@ export default function Image({
|
|||
const [displayBlurHash, setDisplayBlurHash] = useState(true)
|
||||
const [blurDataUrl, setBlurDataUrl] = useState<string | null>(null)
|
||||
const [hasError, setHasError] = useState(false)
|
||||
const [imageUrl, setImageUrl] = useState(url)
|
||||
const [tried, setTried] = useState(new Set())
|
||||
|
||||
useEffect(() => {
|
||||
if (blurHash) {
|
||||
|
|
@ -49,12 +53,52 @@ export default function Image({
|
|||
|
||||
if (hideIfError && hasError) return null
|
||||
|
||||
const handleImageError = async () => {
|
||||
let oldImageUrl: URL | undefined
|
||||
let hash: string | null = null
|
||||
try {
|
||||
oldImageUrl = new URL(imageUrl)
|
||||
hash = getHashFromURL(oldImageUrl)
|
||||
} catch (error) {
|
||||
console.error('Invalid image URL:', error)
|
||||
}
|
||||
if (!pubkey || !hash || !oldImageUrl) {
|
||||
setIsLoading(false)
|
||||
setHasError(true)
|
||||
return
|
||||
}
|
||||
|
||||
const ext = oldImageUrl.pathname.match(/\.\w+$/i)
|
||||
setTried((prev) => new Set(prev.add(oldImageUrl.hostname)))
|
||||
|
||||
const blossomServerList = await client.fetchBlossomServerList(pubkey)
|
||||
const urls = blossomServerList
|
||||
.map((server) => {
|
||||
try {
|
||||
return new URL(server)
|
||||
} catch (error) {
|
||||
console.error('Invalid Blossom server URL:', server, error)
|
||||
return undefined
|
||||
}
|
||||
})
|
||||
.filter((url) => !!url && !tried.has(url.hostname))
|
||||
const nextUrl = urls[0]
|
||||
if (!nextUrl) {
|
||||
setIsLoading(false)
|
||||
setHasError(true)
|
||||
return
|
||||
}
|
||||
|
||||
nextUrl.pathname = '/' + hash + ext
|
||||
setImageUrl(nextUrl.toString())
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('relative', classNames.wrapper)} {...props}>
|
||||
{isLoading && <Skeleton className={cn('absolute inset-0 rounded-lg', className)} />}
|
||||
{!hasError ? (
|
||||
<img
|
||||
src={url}
|
||||
src={imageUrl}
|
||||
alt={alt}
|
||||
className={cn(
|
||||
'object-cover transition-opacity duration-300',
|
||||
|
|
@ -66,10 +110,7 @@ export default function Image({
|
|||
setHasError(false)
|
||||
setTimeout(() => setDisplayBlurHash(false), 500)
|
||||
}}
|
||||
onError={() => {
|
||||
setIsLoading(false)
|
||||
setHasError(true)
|
||||
}}
|
||||
onError={handleImageError}
|
||||
/>
|
||||
) : (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default function CommunityDefinition({
|
|||
<div className="flex gap-4">
|
||||
{metadata.image && (
|
||||
<Image
|
||||
image={{ url: metadata.image }}
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="rounded-lg aspect-square object-cover bg-foreground h-20"
|
||||
hideIfError
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export default function GroupMetadata({
|
|||
<div className="flex gap-4">
|
||||
{metadata.picture && (
|
||||
<Image
|
||||
image={{ url: metadata.picture }}
|
||||
image={{ url: metadata.picture, pubkey: event.pubkey }}
|
||||
className="rounded-lg aspect-square object-cover bg-foreground h-20"
|
||||
hideIfError
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export default function LiveEvent({ event, className }: { event: Event; classNam
|
|||
<div className={className}>
|
||||
{metadata.image && (
|
||||
<Image
|
||||
image={{ url: metadata.image }}
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="w-full aspect-video object-cover rounded-lg"
|
||||
hideIfError
|
||||
/>
|
||||
|
|
@ -62,7 +62,7 @@ export default function LiveEvent({ event, className }: { event: Event; classNam
|
|||
<div className="flex gap-4">
|
||||
{metadata.image && (
|
||||
<Image
|
||||
image={{ url: metadata.image }}
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
|
||||
hideIfError
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function LongFormArticle({
|
|||
<div className={className}>
|
||||
{metadata.image && (
|
||||
<Image
|
||||
image={{ url: metadata.image }}
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="w-full aspect-video object-cover rounded-lg"
|
||||
hideIfError
|
||||
/>
|
||||
|
|
@ -57,7 +57,7 @@ export default function LongFormArticle({
|
|||
<div className="flex gap-4">
|
||||
{metadata.image && (
|
||||
<Image
|
||||
image={{ url: metadata.image }}
|
||||
image={{ url: metadata.image, pubkey: event.pubkey }}
|
||||
className="rounded-lg aspect-[4/3] xl:aspect-video object-cover bg-foreground h-44"
|
||||
hideIfError
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export function ReactionNotification({
|
|||
if (emojiUrl) {
|
||||
return (
|
||||
<Image
|
||||
image={{ url: emojiUrl }}
|
||||
image={{ url: emojiUrl, pubkey: notification.pubkey }}
|
||||
alt={emojiName}
|
||||
className="w-6 h-6"
|
||||
classNames={{ errorPlaceholder: 'bg-transparent' }}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export default function ProfileBanner({
|
|||
|
||||
return (
|
||||
<Image
|
||||
image={{ url: bannerUrl }}
|
||||
image={{ url: bannerUrl, pubkey }}
|
||||
alt={`${pubkey} banner`}
|
||||
className={className}
|
||||
onError={() => setBannerUrl(defaultBanner)}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ const buttonVariants = cva(
|
|||
secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',
|
||||
'secondary-2': 'bg-secondary text-secondary-foreground hover:bg-primary',
|
||||
ghost: 'clickable hover:text-accent-foreground',
|
||||
'ghost-destructive': 'cursor-pointer hover:bg-destructive/10 text-destructive',
|
||||
link: 'text-primary underline-offset-4 hover:underline'
|
||||
},
|
||||
size: {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<'input'>>(
|
|||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'flex h-9 w-full rounded-lg border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
className
|
||||
)}
|
||||
ref={ref}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue