diff --git a/patches/expo-image+3.0.10.patch b/patches/expo-image+3.0.10.patch new file mode 100644 index 0000000000..a518e43ef2 --- /dev/null +++ b/patches/expo-image+3.0.10.patch @@ -0,0 +1,103 @@ +diff --git a/node_modules/expo-image/build/Image.types.d.ts b/node_modules/expo-image/build/Image.types.d.ts +index 022ae48..416504f 100644 +--- a/node_modules/expo-image/build/Image.types.d.ts ++++ b/node_modules/expo-image/build/Image.types.d.ts +@@ -152,6 +152,16 @@ export interface ImageProps extends Omit { + * @default 'normal' + */ + priority?: 'low' | 'normal' | 'high' | null; ++ /** ++ * The loading behavior for the image. Maps to the native HTML `loading` attribute on web. ++ * ++ * - `'lazy'` - Defers loading until the image is near the viewport. ++ * - `'eager'` - Loads the image immediately. ++ * ++ * @default undefined ++ * @platform web ++ */ ++ loading?: 'lazy' | 'eager' | null; + /** + * Determines whether to cache the image and where: on the disk, in the memory or both. + * +diff --git a/node_modules/expo-image/src/ExpoImage.web.tsx b/node_modules/expo-image/src/ExpoImage.web.tsx +index 2a49ff0..1c3de93 100644 +--- a/node_modules/expo-image/src/ExpoImage.web.tsx ++++ b/node_modules/expo-image/src/ExpoImage.web.tsx +@@ -70,6 +70,7 @@ export default function ExpoImage({ + onLoadEnd, + onDisplay, + priority, ++ loading, + blurRadius, + recyclingKey, + style, +@@ -118,6 +119,7 @@ export default function ExpoImage({ + accessibilityLabel={accessibilityLabel ?? alt} + cachePolicy={cachePolicy} + priority={priority} ++ loading={loading} + tintColor={tintColor} + /> + ), +@@ -149,6 +151,7 @@ export default function ExpoImage({ + className={className} + cachePolicy={cachePolicy} + priority={priority} ++ loading={loading} + contentPosition={selectedSource ? contentPosition : { top: '50%', left: '50%' }} + hashPlaceholderContentPosition={contentPosition} + hashPlaceholderStyle={imageHashStyle} +diff --git a/node_modules/expo-image/src/Image.types.ts b/node_modules/expo-image/src/Image.types.ts +index 9dec0e7..61c1621 100644 +--- a/node_modules/expo-image/src/Image.types.ts ++++ b/node_modules/expo-image/src/Image.types.ts +@@ -178,6 +178,17 @@ export interface ImageProps extends Omit { + */ + priority?: 'low' | 'normal' | 'high' | null; + ++ /** ++ * The loading behavior for the image. Maps to the native HTML `loading` attribute on web. ++ * ++ * - `'lazy'` - Defers loading until the image is near the viewport. ++ * - `'eager'` - Loads the image immediately. ++ * ++ * @default undefined ++ * @platform web ++ */ ++ loading?: 'lazy' | 'eager' | null; ++ + /** + * Determines whether to cache the image and where: on the disk, in the memory or both. + * +diff --git a/node_modules/expo-image/src/web/ImageWrapper.tsx b/node_modules/expo-image/src/web/ImageWrapper.tsx +index e8f891d..89a5cb1 100644 +--- a/node_modules/expo-image/src/web/ImageWrapper.tsx ++++ b/node_modules/expo-image/src/web/ImageWrapper.tsx +@@ -30,6 +30,7 @@ const ImageWrapper = React.forwardRef( + contentPosition, + hashPlaceholderContentPosition, + priority, ++ loading, + style, + hashPlaceholderStyle, + tintColor, +@@ -82,6 +83,7 @@ const ImageWrapper = React.forwardRef( + // @ts-ignore + // eslint-disable-next-line react/no-unknown-property + fetchPriority={getFetchPriorityFromImagePriority(priority || 'normal')} ++ loading={loading || undefined} + {...getImageWrapperEventHandler(events, sourceWithHeaders)} + {...getImgPropsFromSource(source)} + {...props} +diff --git a/node_modules/expo-image/src/web/ImageWrapper.types.ts b/node_modules/expo-image/src/web/ImageWrapper.types.ts +index 19bbe2f..179837f 100644 +--- a/node_modules/expo-image/src/web/ImageWrapper.types.ts ++++ b/node_modules/expo-image/src/web/ImageWrapper.types.ts +@@ -29,6 +29,7 @@ export type ImageWrapperProps = { + contentPosition?: ImageContentPositionObject; + hashPlaceholderContentPosition?: ImageContentPositionObject; + priority?: string | null; ++ loading?: 'lazy' | 'eager' | null; + style: CSSProperties; + tintColor?: string | null; + hashPlaceholderStyle?: CSSProperties; diff --git a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx index 392cdd8a10..3f22e13d7a 100644 --- a/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx +++ b/src/components/Post/Embed/ExternalEmbed/ExternalPlayer.tsx @@ -226,6 +226,7 @@ export function ExternalPlayer({ style={[a.flex_1]} source={{uri: link.thumb}} accessibilityIgnoresInvertColors + loading="lazy" /> ) : undefined} diff --git a/src/components/Post/Embed/ImageEmbed.tsx b/src/components/Post/Embed/ImageEmbed.tsx index 700408bdb8..a3f0d46377 100644 --- a/src/components/Post/Embed/ImageEmbed.tsx +++ b/src/components/Post/Embed/ImageEmbed.tsx @@ -10,9 +10,9 @@ import {Image} from 'expo-image' import {useLightboxControls} from '#/state/lightbox' import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' -import {AutoSizedImage} from '#/view/com/util/images/AutoSizedImage' -import {ImageLayoutGrid} from '#/view/com/util/images/ImageLayoutGrid' import {atoms as a} from '#/alf' +import {AutoSizedImage} from '#/components/images/AutoSizedImage' +import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {type EmbedType} from '#/types/bsky/post' import {type CommonProps} from './types' diff --git a/src/components/Post/Embed/VideoEmbed/index.tsx b/src/components/Post/Embed/VideoEmbed/index.tsx index 9de292b4ad..dcc9be5a44 100644 --- a/src/components/Post/Embed/VideoEmbed/index.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.tsx @@ -6,10 +6,10 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' -import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' import {atoms as a} from '#/alf' import {Button} from '#/components/Button' import {useThrottledValue} from '#/components/hooks/useThrottledValue' +import {ConstrainedImage} from '#/components/images/AutoSizedImage' import {PlayButtonIcon} from '#/components/video/PlayButtonIcon' import {VideoEmbedInnerNative} from './VideoEmbedInner/VideoEmbedInnerNative' import * as VideoFallback from './VideoEmbedInner/VideoFallback' diff --git a/src/components/Post/Embed/VideoEmbed/index.web.tsx b/src/components/Post/Embed/VideoEmbed/index.web.tsx index 3de5a99687..b436d7a934 100644 --- a/src/components/Post/Embed/VideoEmbed/index.web.tsx +++ b/src/components/Post/Embed/VideoEmbed/index.web.tsx @@ -13,10 +13,10 @@ import {useLingui} from '@lingui/react' import {isFirefox} from '#/lib/browser' import {ErrorBoundary} from '#/view/com/util/ErrorBoundary' -import {ConstrainedImage} from '#/view/com/util/images/AutoSizedImage' import {atoms as a, useTheme} from '#/alf' import {useIsWithinMessage} from '#/components/dms/MessageContext' import {useFullscreen} from '#/components/hooks/useFullscreen' +import {ConstrainedImage} from '#/components/images/AutoSizedImage' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import { HLSUnsupportedError, diff --git a/src/view/com/util/images/AutoSizedImage.tsx b/src/components/images/AutoSizedImage.tsx similarity index 98% rename from src/view/com/util/images/AutoSizedImage.tsx rename to src/components/images/AutoSizedImage.tsx index 92210e55e7..1a4e3c8417 100644 --- a/src/view/com/util/images/AutoSizedImage.tsx +++ b/src/components/images/AutoSizedImage.tsx @@ -1,4 +1,4 @@ -import React, {useRef} from 'react' +import {useMemo, useRef} from 'react' import {type DimensionValue, Pressable, View} from 'react-native' import Animated, { type AnimatedRef, @@ -34,7 +34,7 @@ export function ConstrainedImage({ * Computed as a % value to apply as `paddingTop`, this basically controls * the height of the image. */ - const outerAspectRatio = React.useMemo(() => { + const outerAspectRatio = useMemo(() => { const ratio = isNative ? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box : Math.min(1 / aspectRatio, 1) // 1:1 bounding box @@ -127,6 +127,7 @@ export function AutoSizedImage({ } } }} + loading="lazy" /> diff --git a/src/view/com/util/images/Gallery.tsx b/src/components/images/Gallery.tsx similarity index 97% rename from src/view/com/util/images/Gallery.tsx rename to src/components/images/Gallery.tsx index 3659e848ec..3a377d3d20 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/components/images/Gallery.tsx @@ -29,7 +29,7 @@ interface Props { viewContext?: PostEmbedViewContext insetBorderStyle?: StyleProp containerRefs: AnimatedRef[] - thumbDimsRef: React.MutableRefObject<(Dimensions | null)[]> + thumbDimsRef: React.RefObject<(Dimensions | null)[]> } export function GalleryItem({ @@ -87,6 +87,7 @@ export function GalleryItem({ height: e.source.height, } }} + loading="lazy" /> diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/components/images/ImageLayoutGrid.tsx similarity index 97% rename from src/view/com/util/images/ImageLayoutGrid.tsx rename to src/components/images/ImageLayoutGrid.tsx index 391302a0c7..54ee1e0121 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/components/images/ImageLayoutGrid.tsx @@ -1,11 +1,11 @@ -import React from 'react' +import {useRef} from 'react' import {type StyleProp, View, type ViewStyle} from 'react-native' import {type AnimatedRef, useAnimatedRef} from 'react-native-reanimated' import {type AppBskyEmbedImages} from '@atproto/api' +import {type Dimensions} from '#/view/com/lightbox/ImageViewing/@types' import {atoms as a, useBreakpoints} from '#/alf' import {PostEmbedViewContext} from '#/components/Post/Embed/types' -import {type Dimensions} from '../../lightbox/ImageViewing/@types' import {GalleryItem} from './Gallery' interface ImageLayoutGridProps { @@ -60,7 +60,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { const containerRef2 = useAnimatedRef() const containerRef3 = useAnimatedRef() const containerRef4 = useAnimatedRef() - const thumbDimsRef = React.useRef<(Dimensions | null)[]>([]) + const thumbDimsRef = useRef<(Dimensions | null)[]>([]) switch (count) { case 2: { diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx index 8a9e51a332..4b0a2d468b 100644 --- a/src/view/com/util/UserAvatar.tsx +++ b/src/view/com/util/UserAvatar.tsx @@ -1,6 +1,6 @@ import {memo, useCallback, useMemo, useState} from 'react' import { - Image, + Image as RNImage, Pressable, type StyleProp, StyleSheet, @@ -8,6 +8,7 @@ import { type ViewStyle, } from 'react-native' import Svg, {Circle, Path, Rect} from 'react-native-svg' +import {Image as ExpoImage} from 'expo-image' import {type ModerationUI} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg, Trans} from '@lingui/macro' @@ -37,7 +38,6 @@ import { } from '#/state/gallery' import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' import {EditImageDialog} from '#/view/com/composer/photos/EditImageDialog' -import {HighPriorityImage} from '#/view/com/util/images/Image' import {atoms as a, tokens, useTheme} from '#/alf' import {Button} from '#/components/Button' import {useDialogControl} from '#/components/Dialog' @@ -289,7 +289,7 @@ let UserAvatar = ({ !((moderation?.blur && isAndroid) /* android crashes with blur */) ? ( {usePlainRNImage ? ( - ) : ( - ( {avatar ? ( - - ) -} diff --git a/src/view/com/util/images/Image.web.tsx b/src/view/com/util/images/Image.web.tsx deleted file mode 100644 index a5200b3ea2..0000000000 --- a/src/view/com/util/images/Image.web.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import {Image} from 'react-native' - -export const HighPriorityImage = Image