Use react-native fast images and blurhash on mobile

This commit is contained in:
Zoe Roux 2023-08-31 16:06:57 +02:00
parent c06afcd56d
commit 1faf234255
No known key found for this signature in database
8 changed files with 93 additions and 65 deletions

View File

@ -43,6 +43,7 @@
"react-i18next": "^13.0.3",
"react-native": "0.72.3",
"react-native-blurhash": "^1.1.11",
"react-native-fast-image": "^8.6.3",
"react-native-mmkv": "^2.10.1",
"react-native-reanimated": "~3.3.0",
"react-native-safe-area-context": "4.6.3",

View File

@ -18,6 +18,7 @@
"react": "*",
"react-native": "*",
"react-native-blurhash": "*",
"react-native-fast-image": "*",
"react-native-reanimated": "*",
"react-native-svg": "*",
"yoshiki": "*"
@ -35,6 +36,9 @@
"react-native-blurhash": {
"optional": true
},
"react-native-fast-image": {
"optional": true
},
"react-native-web": {
"optional": true
}
@ -45,6 +49,8 @@
"solito": "^4.0.1"
},
"optionalDependencies": {
"blurhash": "^2.0.5"
"blurhash": "^2.0.5",
"react-native-blurhash": "^1.1.11",
"react-native-fast-image": "^8.6.3"
}
}

View File

@ -55,7 +55,7 @@ export const Avatar = forwardRef<
fill?: boolean;
as?: ComponentType<{ style?: ViewStyle } & RefAttributes<View>>;
} & Stylable
>(function _Avatar(
>(function Avatar(
{ src, alt, size = px(24), color, placeholder, isLoading = false, fill = false, as, ...props },
ref,
) {

View File

@ -19,10 +19,8 @@
*/
import { KyooImage } from "@kyoo/models";
import {
Image as Img,
ImageStyle,
} from "react-native";
import { ReactElement } from "react";
import { ImageStyle } from "react-native";
import { YoshikiStyle } from "yoshiki/src/type";
export type YoshikiEnhanced<Style> = Style extends any
@ -31,12 +29,13 @@ export type YoshikiEnhanced<Style> = Style extends any
}
: never;
export type WithLoading<T> = (T & { isLoading?: boolean }) | (Partial<T> & { isLoading: true });
type WithLoading<T> = (T & { isLoading?: false }) | (Partial<T> & { isLoading: true });
export type Props = WithLoading<{
src?: KyooImage | null;
alt: string;
quality: "low" | "medium" | "high";
alt: string;
Error?: ReactElement | null;
}>;
export type ImageLayout = YoshikiEnhanced<

View File

@ -19,9 +19,11 @@
*/
import { useState } from "react";
import { ImageProps, ImageStyle, Platform, View, ViewStyle } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { YoshikiEnhanced, WithLoading, Props, ImageLayout } from "./base-image";
import { FlexStyle, ImageStyle, View, ViewStyle } from "react-native";
import FastImage from "react-native-fast-image";
import { Blurhash } from "react-native-blurhash";
import { percent, useYoshiki } from "yoshiki/native";
import { Props, ImageLayout } from "./base-image";
import { Skeleton } from "../skeleton";
export const Image = ({
@ -30,6 +32,7 @@ export const Image = ({
alt,
isLoading: forcedLoading = false,
layout,
Error,
...props
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
const { css } = useYoshiki();
@ -45,41 +48,37 @@ export const Image = ({
setOldSource(src);
}
const border = { borderRadius: 6 } satisfies ViewStyle;
const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle;
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
if (!src || state === "errored")
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />;
const nativeProps = Platform.select<Partial<ImageProps>>({
web: {
defaultSource: typeof src === "string" ? { uri: src } : Array.isArray(src) ? src[0] : src,
},
default: {},
});
if (!src || state === "errored") {
return Error !== undefined ? (
Error
) : (
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
);
}
return (
<View {...css(layout)}>
<Blurhash src={src.high} blurhash={src.blurhash} />
<View {...css([layout, border], props)}>
{state !== "finished" && (
<Blurhash
blurhash={src.blurhash}
resizeMode="cover"
{...css({ width: percent(100), height: percent(100) })}
/>
)}
<FastImage
source={{ uri: src[quality ?? "high"] }}
accessibilityLabel={alt}
onLoad={() => setState("finished")}
onError={() => setState("errored")}
resizeMode={FastImage.resizeMode.cover}
{...(css({
width: percent(100),
height: percent(100),
}) as { style: FlexStyle })}
/>
</View>
);
// return (
// <Skeleton variant="custom" show={state === "loading"} {...css([layout, border], props)}>
// <Img
// source={{ uri: src[quality || "high"] }}
// accessibilityLabel={alt}
// onLoad={() => setState("finished")}
// onError={() => setState("errored")}
// {...nativeProps}
// {...css([
// {
// width: percent(100),
// height: percent(100),
// resizeMode: "cover",
// },
// ])}
// />
// </Skeleton>
// );
};

View File

@ -21,6 +21,7 @@
import { useLayoutEffect, useState } from "react";
import { ImageStyle, View, ViewStyle } from "react-native";
import { StyleList, processStyleList } from "yoshiki/src/type";
import { useYoshiki as useWebYoshiki } from "yoshiki/web";
import { useYoshiki } from "yoshiki/native";
import { Props, ImageLayout } from "./base-image";
import { blurHashToDataURL } from "./blurhash-web";
@ -41,9 +42,11 @@ export const Image = ({
alt,
isLoading: forcedLoading = false,
layout,
Error,
...props
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
const { css } = useYoshiki();
const { css: wCss } = useWebYoshiki();
const [state, setState] = useState<"loading" | "errored" | "finished">(
src ? "finished" : "errored",
);
@ -55,8 +58,13 @@ export const Image = ({
const border = { borderRadius: 6 } satisfies ViewStyle;
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
if (!src || state === "errored")
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />;
if (!src || state === "errored") {
return Error !== undefined ? (
Error
) : (
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
);
}
const blurhash = blurHashToDataURL(src.blurhash);
return (
@ -79,10 +87,9 @@ export const Image = ({
width: (layout as any).width,
height: (layout as any).height,
aspectRatio: (layout as any).aspectRatio,
...border,
}}
// Gather classnames from props (to support parent's hover for example).
className={extractClassNames(props)}
{...wCss({ ...border, borderRadius: "6px" }, { className: extractClassNames(props) })}
>
<NextImage
src={src[quality ?? "high"]}

View File

@ -19,7 +19,7 @@
*/
import { ImageProps, ImageStyle, Platform, View, ViewProps, ViewStyle } from "react-native";
import { Props, ImageLayout, YoshikiEnhanced } from "./base-image";
import { Props, YoshikiEnhanced } from "./base-image";
import { Image } from "./image";
import { ComponentType, ReactNode, useState } from "react";
import { LinearGradient, LinearGradientProps } from "expo-linear-gradient";
@ -36,12 +36,18 @@ export const Poster = ({
}: Props & { style?: ImageStyle } & {
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
}) => (
<Image isLoading={isLoading} alt={alt} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />
<Image
isLoading={isLoading as any}
alt={alt!}
layout={{ aspectRatio: 2 / 3, ...layout }}
{...props}
/>
);
export const ImageBackground = <AsProps = ViewProps,>({
src,
alt,
quality,
gradient = true,
as,
children,
@ -57,14 +63,6 @@ export const ImageBackground = <AsProps = ViewProps,>({
imageStyle?: YoshikiEnhanced<ImageStyle>;
} & AsProps &
Props) => {
const [isErrored, setErrored] = useState(false);
const nativeProps = Platform.select<Partial<ImageProps>>({
web: {
defaultSource: typeof src === "string" ? { uri: src! } : Array.isArray(src) ? src[0] : src!,
},
default: {},
});
const Container = as ?? View;
return (
<ContrastArea contrastText>
@ -84,16 +82,14 @@ export const ImageBackground = <AsProps = ViewProps,>({
containerStyle,
])}
>
{src && !isErrored && (
{src && (
<Image
source={typeof src === "string" ? { uri: src } : src}
accessibilityLabel={alt}
onError={() => setErrored(true)}
{...nativeProps}
{...css([
{ width: percent(100), height: percent(100), resizeMode: "cover" },
imageStyle,
])}
src={src}
quality={quality}
alt={alt!}
layout={{ width: percent(100), height: percent(100) }}
Error={null}
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as ImageProps)}
/>
)}
{gradient && (

View File

@ -2526,6 +2526,8 @@ __metadata:
"@tanstack/react-query": ^4.32.6
"@types/react": 18.2.0
blurhash: ^2.0.5
react-native-blurhash: ^1.1.11
react-native-fast-image: ^8.6.3
solito: ^4.0.1
typescript: ^5.1.6
peerDependencies:
@ -2538,12 +2540,17 @@ __metadata:
react: "*"
react-native: "*"
react-native-blurhash: "*"
react-native-fast-image: "*"
react-native-reanimated: "*"
react-native-svg: "*"
yoshiki: "*"
dependenciesMeta:
blurhash:
optional: true
react-native-blurhash:
optional: true
react-native-fast-image:
optional: true
peerDependenciesMeta:
"@gorhom/portal":
optional: true
@ -2553,6 +2560,8 @@ __metadata:
optional: true
react-native-blurhash:
optional: true
react-native-fast-image:
optional: true
react-native-web:
optional: true
languageName: unknown
@ -10565,6 +10574,7 @@ __metadata:
react-i18next: ^13.0.3
react-native: 0.72.3
react-native-blurhash: ^1.1.11
react-native-fast-image: ^8.6.3
react-native-mmkv: ^2.10.1
react-native-reanimated: ~3.3.0
react-native-safe-area-context: 4.6.3
@ -11882,6 +11892,16 @@ __metadata:
languageName: node
linkType: hard
"react-native-fast-image@npm:^8.6.3":
version: 8.6.3
resolution: "react-native-fast-image@npm:8.6.3"
peerDependencies:
react: ^17 || ^18
react-native: ">=0.60.0"
checksum: 29289cb6b2eae0983c8922b22e2d9de3be07322bb7991c5def19f95eadefaedb0e308ff0b38cc1d0444e8bd4fe94a7621a99a2d3d9298100bcb60b3144677234
languageName: node
linkType: hard
"react-native-mmkv@npm:^2.10.1":
version: 2.10.1
resolution: "react-native-mmkv@npm:2.10.1"