Browse Source

feat: optimize display effect when image loading fails

imwald
codytseng 1 year ago
parent
commit
41d46b1a13
  1. 35
      src/components/Image/index.tsx
  2. 5
      src/components/ImageCarousel/index.tsx
  3. 15
      src/components/ImageGallery/index.tsx
  4. 6
      src/components/NoteCard/GroupMetadataCard.tsx
  5. 3
      src/components/NoteCard/LiveEventCard.tsx
  6. 3
      src/components/NoteCard/LongFormArticleCard.tsx
  7. 26
      src/components/NoteList/index.tsx
  8. 2
      src/components/PictureNoteCard/index.tsx
  9. 3
      src/components/ProfileBanner/index.tsx
  10. 5
      src/components/WebPreview/index.tsx
  11. 2
      src/pages/secondary/ProfileEditorPage/index.tsx
  12. 2
      src/pages/secondary/ProfilePage/index.tsx

35
src/components/Image/index.tsx

@ -2,20 +2,29 @@ import { Skeleton } from '@/components/ui/skeleton' @@ -2,20 +2,29 @@ import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'
import { TImageInfo } from '@/types'
import { decode } from 'blurhash'
import { ImageOff } from 'lucide-react'
import { HTMLAttributes, useEffect, useState } from 'react'
export default function Image({
image: { url, blurHash },
alt,
className = '',
classNames = {},
hideIfError = false,
...props
}: HTMLAttributes<HTMLDivElement> & {
classNames?: {
wrapper?: string
errorPlaceholder?: string
}
image: TImageInfo
alt?: string
hideIfError?: boolean
}) {
const [isLoading, setIsLoading] = useState(true)
const [displayBlurHash, setDisplayBlurHash] = useState(true)
const [blurDataUrl, setBlurDataUrl] = useState<string | null>(null)
const [hasError, setHasError] = useState(false)
useEffect(() => {
if (blurHash) {
@ -36,14 +45,17 @@ export default function Image({ @@ -36,14 +45,17 @@ export default function Image({
}
}, [blurHash])
if (hideIfError && hasError) return null
return (
<div className={cn('relative', className)} {...props}>
{isLoading && <Skeleton className={cn('absolute inset-0', className)} />}
<div className={cn('relative', classNames.wrapper)} {...props}>
{isLoading && <Skeleton className={cn('absolute inset-0 rounded-lg', className)} />}
{!hasError ? (
<img
src={url}
alt={alt}
className={cn(
'object-cover transition-opacity duration-300',
'object-cover transition-opacity duration-300 w-full h-full',
isLoading ? 'opacity-0' : 'opacity-100',
className
)}
@ -51,8 +63,23 @@ export default function Image({ @@ -51,8 +63,23 @@ export default function Image({
setIsLoading(false)
setTimeout(() => setDisplayBlurHash(false), 500)
}}
onError={() => {
setIsLoading(false)
setHasError(true)
}}
/>
{displayBlurHash && blurDataUrl && (
) : (
<div
className={cn(
'object-cover flex flex-col items-center justify-center w-full h-full bg-muted',
className,
classNames.errorPlaceholder
)}
>
<ImageOff />
</div>
)}
{displayBlurHash && blurDataUrl && !hasError && (
<img
src={blurDataUrl}
className={cn('absolute inset-0 object-cover w-full h-full -z-10', className)}

5
src/components/ImageCarousel/index.tsx

@ -48,7 +48,10 @@ export function ImageCarousel({ @@ -48,7 +48,10 @@ export function ImageCarousel({
{images.map((image, index) => (
<CarouselItem key={index} className="xl:basis-2/3 cursor-zoom-in">
<Image
className="xl:rounded-lg"
className="xl:rounded-lg max-h-[75vh]"
classNames={{
errorPlaceholder: 'aspect-square'
}}
image={image}
onClick={(e) => handlePhotoClick(e, index)}
/>

15
src/components/ImageGallery/index.tsx

@ -38,8 +38,11 @@ export default function ImageGallery({ @@ -38,8 +38,11 @@ export default function ImageGallery({
className={cn(
'rounded-lg',
!disableLightbox ? 'cursor-auto' : '',
size === 'small' ? 'h-[15vh]' : 'h-[30vh]'
size === 'small' ? 'max-h-[15vh]' : 'max-h-[30vh]'
)}
classNames={{
errorPlaceholder: cn('aspect-square', size === 'small' ? 'h-[15vh]' : 'h-[30vh]')
}}
image={images[0]}
onClick={(e) => handlePhotoClick(e, 0)}
/>
@ -50,7 +53,7 @@ export default function ImageGallery({ @@ -50,7 +53,7 @@ export default function ImageGallery({
{images.map((image, i) => (
<Image
key={i}
className={cn('rounded-lg aspect-square w-full', !disableLightbox ? 'cursor-auto' : '')}
className={cn('aspect-square w-full rounded-lg', !disableLightbox ? 'cursor-auto' : '')}
image={image}
onClick={(e) => handlePhotoClick(e, i)}
/>
@ -63,7 +66,7 @@ export default function ImageGallery({ @@ -63,7 +66,7 @@ export default function ImageGallery({
{images.map((image, i) => (
<Image
key={i}
className={cn('rounded-lg aspect-square w-full', !disableLightbox ? 'cursor-auto' : '')}
className={cn('aspect-square w-full rounded-lg', !disableLightbox ? 'cursor-auto' : '')}
image={image}
onClick={(e) => handlePhotoClick(e, i)}
/>
@ -72,11 +75,11 @@ export default function ImageGallery({ @@ -72,11 +75,11 @@ export default function ImageGallery({
)
} else {
imageContent = (
<div className="grid grid-cols-3 gap-2">
<div className="grid grid-cols-3 gap-2 w-full">
{images.map((image, i) => (
<Image
key={i}
className={cn('rounded-lg aspect-square w-full', !disableLightbox ? 'cursor-auto' : '')}
className={cn('aspect-square w-full rounded-lg', !disableLightbox ? 'cursor-auto' : '')}
image={image}
onClick={(e) => handlePhotoClick(e, i)}
/>
@ -86,7 +89,7 @@ export default function ImageGallery({ @@ -86,7 +89,7 @@ export default function ImageGallery({
}
return (
<div className={cn('relative w-fit max-w-full', className)}>
<div className={cn('relative', images.length === 1 ? 'w-fit max-w-full' : 'w-full', className)}>
{imageContent}
{index >= 0 &&
!disableLightbox &&

6
src/components/NoteCard/GroupMetadataCard.tsx

@ -93,7 +93,11 @@ export default function GroupMetadataCard({ @@ -93,7 +93,11 @@ export default function GroupMetadataCard({
</div>
<div className="flex gap-2 items-start mt-2">
{metadata.picture && (
<Image image={{ url: metadata.picture }} className="h-32 aspect-square rounded-lg" />
<Image
image={{ url: metadata.picture }}
className="h-32 aspect-square rounded-lg"
hideIfError
/>
)}
<div className="flex-1 w-0 space-y-1">
<div className="text-xl font-semibold line-clamp-1">{metadata.name}</div>

3
src/components/NoteCard/LiveEventCard.tsx

@ -120,6 +120,7 @@ export default function LiveEventCard({ @@ -120,6 +120,7 @@ export default function LiveEventCard({
<Image
image={{ url: metadata.image }}
className="w-full aspect-video object-cover rounded-lg"
hideIfError
/>
)}
<div className="space-y-1">
@ -148,7 +149,7 @@ export default function LiveEventCard({ @@ -148,7 +149,7 @@ export default function LiveEventCard({
</div>
</div>
{metadata.image && (
<Image image={{ url: metadata.image }} className="h-36 max-w-44 rounded-lg" />
<Image image={{ url: metadata.image }} className="h-36 max-w-44 rounded-lg" hideIfError />
)}
</div>
{!embedded && <Separator />}

3
src/components/NoteCard/LongFormArticleCard.tsx

@ -113,6 +113,7 @@ export default function LongFormArticleCard({ @@ -113,6 +113,7 @@ export default function LongFormArticleCard({
<Image
image={{ url: metadata.image }}
className="w-full aspect-video object-cover rounded-lg"
hideIfError
/>
)}
<div className="space-y-1">
@ -141,7 +142,7 @@ export default function LongFormArticleCard({ @@ -141,7 +142,7 @@ export default function LongFormArticleCard({
</div>
</div>
{metadata.image && (
<Image image={{ url: metadata.image }} className="h-36 max-w-48 rounded-lg" />
<Image image={{ url: metadata.image }} className="rounded-lg h-36 max-w-48" hideIfError />
)}
</div>
{!embedded && <Separator />}

26
src/components/NoteList/index.tsx

@ -9,8 +9,8 @@ import { useMuteList } from '@/providers/MuteListProvider' @@ -9,8 +9,8 @@ import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
import relayInfoService from '@/services/relay-info.service'
import storage from '@/services/local-storage.service'
import relayInfoService from '@/services/relay-info.service'
import { TNoteListMode } from '@/types'
import dayjs from 'dayjs'
import { Event, Filter, kinds } from 'nostr-tools'
@ -326,30 +326,10 @@ function PictureNoteCardMasonry({ @@ -326,30 +326,10 @@ function PictureNoteCardMasonry({
}
function LoadingSkeleton({ isPictures }: { isPictures: boolean }) {
const { isLargeScreen } = useScreenSize()
const { t } = useTranslation()
if (isPictures) {
return (
<div
className={cn(
'px-2 sm:px-4 grid',
isLargeScreen ? 'grid-cols-3 gap-4' : 'grid-cols-2 gap-2'
)}
>
{[...Array(isLargeScreen ? 3 : 2)].map((_, i) => (
<div key={i}>
<Skeleton className="rounded-lg w-full aspect-[6/8]" />
<div className="p-2">
<Skeleton className="w-32 h-5" />
<div className="flex items-center gap-2 mt-2">
<Skeleton className="w-5 h-5 rounded-full" />
<Skeleton className="w-16 h-3" />
</div>
</div>
</div>
))}
</div>
)
return <div className="text-center text-sm text-muted-foreground">{t('loading...')}</div>
}
return (

2
src/components/PictureNoteCard/index.tsx

@ -38,7 +38,7 @@ export default function PictureNoteCard({ @@ -38,7 +38,7 @@ export default function PictureNoteCard({
return (
<div className={cn('cursor-pointer relative', className)} onClick={() => push(toNote(event))}>
<Image className="rounded-lg w-full aspect-[6/8]" image={images[0]} />
<Image className="w-full aspect-[6/8] rounded-lg" image={images[0]} />
{images.length > 1 && (
<div className="absolute top-2 right-2 bg-background/50 rounded-full p-2">
<Images size={16} />

3
src/components/ProfileBanner/index.tsx

@ -1,4 +1,5 @@ @@ -1,4 +1,5 @@
import { generateImageByPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
import { useEffect, useMemo, useState } from 'react'
import Image from '../Image'
@ -26,7 +27,7 @@ export default function ProfileBanner({ @@ -26,7 +27,7 @@ export default function ProfileBanner({
<Image
image={{ url: bannerUrl }}
alt={`${pubkey} banner`}
className={className}
className={cn('rounded-lg', className)}
onError={() => setBannerUrl(defaultBanner)}
/>
)

5
src/components/WebPreview/index.tsx

@ -30,7 +30,7 @@ export default function WebPreview({ @@ -30,7 +30,7 @@ export default function WebPreview({
if (isSmallScreen && image) {
return (
<div className="rounded-lg border mt-2">
<Image image={{ url: image }} className="rounded-t-lg w-full h-44" />
<Image image={{ url: image }} className="w-full h-44 rounded-t-lg" hideIfError />
<div className="bg-muted p-2 w-full rounded-b-lg">
<div className="text-xs text-muted-foreground">{hostname}</div>
<div className="font-semibold line-clamp-1">{title}</div>
@ -50,7 +50,8 @@ export default function WebPreview({ @@ -50,7 +50,8 @@ export default function WebPreview({
{image && (
<Image
image={{ url: image }}
className={`rounded-l-lg ${size === 'normal' ? 'h-44' : 'h-24'}`}
className={`rounded-lg ${size === 'normal' ? 'h-44' : 'h-24'}`}
hideIfError
/>
)}
<div className="flex-1 w-0 p-2">

2
src/pages/secondary/ProfileEditorPage/index.tsx

@ -109,7 +109,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => { @@ -109,7 +109,7 @@ const ProfileEditorPage = forwardRef(({ index }: { index?: number }, ref) => {
<ProfileBanner
banner={banner}
pubkey={account.pubkey}
className="w-full aspect-video object-cover rounded-lg"
className="w-full aspect-video object-cover"
/>
<div className="absolute top-0 bg-muted/30 w-full h-full rounded-lg flex flex-col justify-center items-center">
{uploadingBanner ? (

2
src/pages/secondary/ProfilePage/index.tsx

@ -82,7 +82,7 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number }, @@ -82,7 +82,7 @@ const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number },
<ProfileBanner
banner={banner}
pubkey={pubkey}
className="w-full aspect-video object-cover rounded-lg"
className="w-full aspect-video object-cover"
/>
<Avatar className="w-24 h-24 absolute bottom-0 left-4 translate-y-1/2 border-4 border-background">
<AvatarImage src={avatar} className="object-cover object-center" />

Loading…
Cancel
Save