feat: image component
This commit is contained in:
parent
f02736ede8
commit
fb52f25534
|
|
@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/>
|
||||
)
|
||||
})}
|
||||
|
|
|
|||
Loading…
Reference in New Issue