feat: image component

This commit is contained in:
DIYgod 2025-03-10 21:32:19 +08:00
parent f02736ede8
commit fb52f25534
No known key found for this signature in database
3 changed files with 113 additions and 27 deletions

View File

@ -0,0 +1,101 @@
import { createBuildSafeHeaders } from "@follow/utils/src/headers"
import { getImageProxyUrl, IMAGE_PROXY_URL } from "@follow/utils/src/img-proxy"
import type { ImageProps as ExpoImageProps } from "expo-image"
import { Image as ExpoImage } from "expo-image"
import { forwardRef, useCallback, useMemo, useRef, useState } from "react"
import { Pressable, View } from "react-native"
import { proxyEnv } from "@/src/lib/proxy-env"
import { usePreviewImage } from "./PreviewPageProvider"
const buildSafeHeaders = createBuildSafeHeaders(proxyEnv.VITE_WEB_URL, [
IMAGE_PROXY_URL,
proxyEnv.VITE_API_URL,
])
export type ImageProps = Omit<ExpoImageProps, "source"> & {
proxy?: {
width?: number
height?: number
}
enablePreview?: boolean
onPreview?: () => void
source?: {
uri: string
headers?: Record<string, string>
}
blurhash?: string
aspectRatio: number
}
export const Image = forwardRef<ExpoImage, ImageProps>(
({ proxy, source, enablePreview, onPreview, blurhash, aspectRatio, ...rest }, ref) => {
const safeSource: ImageProps["source"] = useMemo(() => {
return source
? {
...source,
headers: {
...buildSafeHeaders({ url: source.uri }),
...source.headers,
},
}
: undefined
}, [source])
const proxiesSafeSource = useMemo(() => {
if (!proxy?.height && !proxy?.width) {
return safeSource
}
return safeSource
? {
...safeSource,
uri: getImageProxyUrl({
url: safeSource.uri,
width: proxy?.width ? proxy?.width * 3 : undefined,
height: proxy?.height ? proxy?.height * 3 : undefined,
}),
}
: undefined
}, [proxy?.height, proxy?.width, safeSource])
const [isError, setIsError] = useState(false)
const onError = useCallback(() => {
setIsError(true)
}, [])
const Wrapper = enablePreview ? Pressable : View
const { openPreview } = usePreviewImage()
const imageRef = useRef<View>(null)
return (
<Wrapper
onPress={() => {
if (enablePreview) {
onPreview?.()
openPreview({
imageRef,
images: [{ source, blurhash, aspectRatio, ...rest }],
// accessoriesElement: Accessory ? <Accessory {...AccessoryProps} /> : undefined,
})
}
}}
>
<View ref={imageRef}>
<ExpoImage
source={isError ? safeSource : proxiesSafeSource}
onError={onError}
placeholder={{ blurhash }}
style={{
aspectRatio,
...(typeof rest.style === "object" && { ...rest.style }),
}}
{...rest}
ref={ref}
/>
</View>
</Wrapper>
)
},
)

View File

@ -1,4 +1,3 @@
import { Image } from "expo-image"
import type { RefObject } from "react"
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
import { Dimensions, Modal, Pressable, useWindowDimensions, View } from "react-native"
@ -16,18 +15,13 @@ import { gentleSpringPreset } from "@/src/constants/spring"
import { CloseCuteReIcon } from "@/src/icons/close_cute_re"
import { PortalHost } from "../portal"
import type { ImageProps } from "./Image"
import { Image } from "./Image"
import { ImageContextMenu } from "./ImageContextMenu"
interface PreviewImageProps {
imageUrl: string
blurhash?: string | undefined
aspectRatio: number
recyclingKey?: string
}
interface OpenPreviewParams {
imageRef: RefObject<View>
images: PreviewImageProps[]
images: ImageProps[]
initialIndex?: number
accessoriesElement?: React.ReactNode
}
@ -49,7 +43,7 @@ export const PreviewImageProvider = ({ children }: { children: React.ReactNode }
const { width, height } = useWindowDimensions()
const [previewModalOpen, setPreviewModalOpen] = useState(false)
const [currentState, setCurrentState] = useState<PreviewImageProps[] | null>(null)
const [currentState, setCurrentState] = useState<ImageProps[] | null>(null)
const [imageRef, setImageRef] = useState<View | null>(null)
const [currentIndex, setCurrentIndex] = useState(0)
const [accessoriesElement, setAccessoriesElement] = useState<React.ReactNode | null>(null)
@ -391,7 +385,7 @@ export const PreviewImageProvider = ({ children }: { children: React.ReactNode }
return (
<PreviewImageContext.Provider value={ctxValue}>
{children}
{currentState && (
{currentState?.[currentIndex] && (
<Modal transparent visible={previewModalOpen}>
<PortalHost>
<Animated.View style={overlayStyle} className="absolute inset-0" />
@ -421,18 +415,8 @@ export const PreviewImageProvider = ({ children }: { children: React.ReactNode }
],
}}
>
<ImageContextMenu imageUrl={currentState[currentIndex]?.imageUrl}>
<Image
recyclingKey={currentState[currentIndex]?.recyclingKey}
source={{ uri: currentState[currentIndex]?.imageUrl }}
className="w-full"
style={{
aspectRatio: currentState[currentIndex]?.aspectRatio,
}}
placeholder={{
blurhash: currentState[currentIndex]?.blurhash,
}}
/>
<ImageContextMenu imageUrl={currentState[currentIndex].source?.uri}>
<Image {...currentState[currentIndex]} className="w-full" />
</ImageContextMenu>
</Animated.View>
</Animated.View>

View File

@ -8,7 +8,7 @@ import { useGeneralSettingKey } from "@/src/atoms/settings/general"
import { UserAvatar } from "@/src/components/ui/avatar/UserAvatar"
import { RelativeDateTime } from "@/src/components/ui/datetime/RelativeDateTime"
import { FeedIcon } from "@/src/components/ui/icon/feed-icon"
import { PreviewImage } from "@/src/components/ui/image/PreviewImage"
import { Image } from "@/src/components/ui/image/Image"
import { ItemPressableStyle } from "@/src/components/ui/pressable/enum"
import { ItemPressable } from "@/src/components/ui/pressable/ItemPressable"
import { gentleSpringPreset } from "@/src/constants/spring"
@ -104,15 +104,16 @@ export function EntrySocialItem({ entryId }: { entryId: string }) {
<View className="ml-10 flex flex-row flex-wrap gap-2">
{media.map((image, idx) => {
return (
<PreviewImage
<Image
key={`${entryId}-${idx}`}
className="ml-2 h-20 rounded-md"
className="ml-2 h-20 w-auto rounded-md"
blurhash={image.blurhash}
imageUrl={image.url}
source={{ uri: image.url }}
aspectRatio={image.width && image.height ? image.width / image.height : 1}
proxy={{
height: 80,
}}
enablePreview
/>
)
})}