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-i18next": "^13.0.3",
"react-native": "0.72.3", "react-native": "0.72.3",
"react-native-blurhash": "^1.1.11", "react-native-blurhash": "^1.1.11",
"react-native-fast-image": "^8.6.3",
"react-native-mmkv": "^2.10.1", "react-native-mmkv": "^2.10.1",
"react-native-reanimated": "~3.3.0", "react-native-reanimated": "~3.3.0",
"react-native-safe-area-context": "4.6.3", "react-native-safe-area-context": "4.6.3",

View File

@ -18,6 +18,7 @@
"react": "*", "react": "*",
"react-native": "*", "react-native": "*",
"react-native-blurhash": "*", "react-native-blurhash": "*",
"react-native-fast-image": "*",
"react-native-reanimated": "*", "react-native-reanimated": "*",
"react-native-svg": "*", "react-native-svg": "*",
"yoshiki": "*" "yoshiki": "*"
@ -35,6 +36,9 @@
"react-native-blurhash": { "react-native-blurhash": {
"optional": true "optional": true
}, },
"react-native-fast-image": {
"optional": true
},
"react-native-web": { "react-native-web": {
"optional": true "optional": true
} }
@ -45,6 +49,8 @@
"solito": "^4.0.1" "solito": "^4.0.1"
}, },
"optionalDependencies": { "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; fill?: boolean;
as?: ComponentType<{ style?: ViewStyle } & RefAttributes<View>>; as?: ComponentType<{ style?: ViewStyle } & RefAttributes<View>>;
} & Stylable } & Stylable
>(function _Avatar( >(function Avatar(
{ src, alt, size = px(24), color, placeholder, isLoading = false, fill = false, as, ...props }, { src, alt, size = px(24), color, placeholder, isLoading = false, fill = false, as, ...props },
ref, ref,
) { ) {

View File

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

View File

@ -19,9 +19,11 @@
*/ */
import { useState } from "react"; import { useState } from "react";
import { ImageProps, ImageStyle, Platform, View, ViewStyle } from "react-native"; import { FlexStyle, ImageStyle, View, ViewStyle } from "react-native";
import { useYoshiki } from "yoshiki/native"; import FastImage from "react-native-fast-image";
import { YoshikiEnhanced, WithLoading, Props, ImageLayout } from "./base-image"; import { Blurhash } from "react-native-blurhash";
import { percent, useYoshiki } from "yoshiki/native";
import { Props, ImageLayout } from "./base-image";
import { Skeleton } from "../skeleton"; import { Skeleton } from "../skeleton";
export const Image = ({ export const Image = ({
@ -30,6 +32,7 @@ export const Image = ({
alt, alt,
isLoading: forcedLoading = false, isLoading: forcedLoading = false,
layout, layout,
Error,
...props ...props
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => { }: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
@ -45,41 +48,37 @@ export const Image = ({
setOldSource(src); 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 (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
if (!src || state === "errored") if (!src || state === "errored") {
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />; return Error !== undefined ? (
Error
const nativeProps = Platform.select<Partial<ImageProps>>({ ) : (
web: { <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
defaultSource: typeof src === "string" ? { uri: src } : Array.isArray(src) ? src[0] : src, );
}, }
default: {},
});
return ( return (
<View {...css(layout)}> <View {...css([layout, border], props)}>
<Blurhash src={src.high} blurhash={src.blurhash} /> {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> </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 { useLayoutEffect, useState } from "react";
import { ImageStyle, View, ViewStyle } from "react-native"; import { ImageStyle, View, ViewStyle } from "react-native";
import { StyleList, processStyleList } from "yoshiki/src/type"; import { StyleList, processStyleList } from "yoshiki/src/type";
import { useYoshiki as useWebYoshiki } from "yoshiki/web";
import { useYoshiki } from "yoshiki/native"; import { useYoshiki } from "yoshiki/native";
import { Props, ImageLayout } from "./base-image"; import { Props, ImageLayout } from "./base-image";
import { blurHashToDataURL } from "./blurhash-web"; import { blurHashToDataURL } from "./blurhash-web";
@ -41,9 +42,11 @@ export const Image = ({
alt, alt,
isLoading: forcedLoading = false, isLoading: forcedLoading = false,
layout, layout,
Error,
...props ...props
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => { }: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
const { css } = useYoshiki(); const { css } = useYoshiki();
const { css: wCss } = useWebYoshiki();
const [state, setState] = useState<"loading" | "errored" | "finished">( const [state, setState] = useState<"loading" | "errored" | "finished">(
src ? "finished" : "errored", src ? "finished" : "errored",
); );
@ -55,8 +58,13 @@ export const Image = ({
const border = { borderRadius: 6 } satisfies ViewStyle; const border = { borderRadius: 6 } satisfies ViewStyle;
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />; if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
if (!src || state === "errored") if (!src || state === "errored") {
return <View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />; return Error !== undefined ? (
Error
) : (
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
);
}
const blurhash = blurHashToDataURL(src.blurhash); const blurhash = blurHashToDataURL(src.blurhash);
return ( return (
@ -79,10 +87,9 @@ export const Image = ({
width: (layout as any).width, width: (layout as any).width,
height: (layout as any).height, height: (layout as any).height,
aspectRatio: (layout as any).aspectRatio, aspectRatio: (layout as any).aspectRatio,
...border,
}} }}
// Gather classnames from props (to support parent's hover for example). // Gather classnames from props (to support parent's hover for example).
className={extractClassNames(props)} {...wCss({ ...border, borderRadius: "6px" }, { className: extractClassNames(props) })}
> >
<NextImage <NextImage
src={src[quality ?? "high"]} src={src[quality ?? "high"]}

View File

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

View File

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