Skip to content

Commit

Permalink
Merge pull request Expensify#35041 from dukenv0307/fix/34120
Browse files Browse the repository at this point in the history
Receipt thumbnail should show the top of the receipt
  • Loading branch information
Beamanator authored Apr 22, 2024
2 parents 68e6630 + 7a748f5 commit b1d25e4
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 8 deletions.
5 changes: 5 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1135,6 +1135,11 @@ const CONST = {
JPEG: 'image/jpeg',
},

IMAGE_OBJECT_POSITION: {
TOP: 'top',
INITIAL: 'initial',
},

FILE_TYPE_REGEX: {
// Image MimeTypes allowed by iOS photos app.
IMAGE: /\.(jpg|jpeg|png|webp|gif|tiff|bmp|heic|heif)$/,
Expand Down
37 changes: 34 additions & 3 deletions src/components/Image/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,40 @@
import React, {useMemo} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import {withOnyx} from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import BaseImage from './BaseImage';
import type {ImageOnyxProps, ImageOwnProps, ImageProps} from './types';
import type {ImageOnLoadEvent, ImageOnyxProps, ImageOwnProps, ImageProps} from './types';

function Image({source: propsSource, isAuthTokenRequired = false, session, ...forwardedProps}: ImageProps) {
function Image({source: propsSource, isAuthTokenRequired = false, session, onLoad, objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL, style, ...forwardedProps}: ImageProps) {
const [aspectRatio, setAspectRatio] = useState<string | number | null>(null);
const isObjectPositionTop = objectPosition === CONST.IMAGE_OBJECT_POSITION.TOP;

const updateAspectRatio = useCallback(
(width: number, height: number) => {
if (!isObjectPositionTop) {
return;
}

if (width > height) {
setAspectRatio(1);
return;
}

setAspectRatio(height ? width / height : 'auto');
},
[isObjectPositionTop],
);

const handleLoad = useCallback(
(event: ImageOnLoadEvent) => {
const {width, height} = event.nativeEvent;

onLoad?.(event);

updateAspectRatio(width, height);
},
[onLoad, updateAspectRatio],
);
/**
* Check if the image source is a URL - if so the `encryptedAuthToken` is appended
* to the source.
Expand Down Expand Up @@ -34,6 +63,8 @@ function Image({source: propsSource, isAuthTokenRequired = false, session, ...fo
<BaseImage
// eslint-disable-next-line react/jsx-props-no-spreading
{...forwardedProps}
onLoad={handleLoad}
style={[style, aspectRatio ? {aspectRatio, height: 'auto'} : {}, isObjectPositionTop && !aspectRatio && {opacity: 0}]}
source={source}
/>
);
Expand Down
13 changes: 10 additions & 3 deletions src/components/Image/types.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import type {ImageSource} from 'expo-image';
import type {ImageRequireSource, ImageResizeMode, ImageStyle, ImageURISource, StyleProp} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import type CONST from '@src/CONST';
import type {Session} from '@src/types/onyx';

type ExpoImageSource = ImageSource | number | ImageSource[];

type ImageObjectPosition = ValueOf<typeof CONST.IMAGE_OBJECT_POSITION>;

type ImageOnyxProps = {
/** Session info for the currently logged in user. */
session: OnyxEntry<Session>;
Expand All @@ -23,12 +27,12 @@ type BaseImageProps = {

/** Event for when the image is fully loaded and returns the natural dimensions of the image */
onLoad?: (event: ImageOnLoadEvent) => void;
};

type ImageOwnProps = BaseImageProps & {
/** Styles for the Image */
style?: StyleProp<ImageStyle>;
};

type ImageOwnProps = BaseImageProps & {
/** Should an auth token be included in the image request */
isAuthTokenRequired?: boolean;

Expand All @@ -46,8 +50,11 @@ type ImageOwnProps = BaseImageProps & {

/** Progress events while the image is downloading */
onProgress?: () => void;

/** The object position of image */
objectPosition?: ImageObjectPosition;
};

type ImageProps = ImageOnyxProps & ImageOwnProps;

export type {BaseImageProps, ImageOwnProps, ImageOnyxProps, ImageProps, ExpoImageSource, ImageOnLoadEvent};
export type {BaseImageProps, ImageOwnProps, ImageOnyxProps, ImageProps, ExpoImageSource, ImageOnLoadEvent, ImageObjectPosition};
8 changes: 7 additions & 1 deletion src/components/ImageWithSizeCalculation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import {View} from 'react-native';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import Log from '@libs/Log';
import CONST from '@src/CONST';
import FullscreenLoadingIndicator from './FullscreenLoadingIndicator';
import Image from './Image';
import RESIZE_MODES from './Image/resizeModes';
import type {ImageObjectPosition} from './Image/types';

type OnMeasure = (args: {width: number; height: number}) => void;

Expand All @@ -32,6 +34,9 @@ type ImageWithSizeCalculationProps = {

/** Whether the image requires an authToken */
isAuthTokenRequired: boolean;

/** The object position of image */
objectPosition?: ImageObjectPosition;
};

/**
Expand All @@ -40,7 +45,7 @@ type ImageWithSizeCalculationProps = {
* performing some calculation on a network image after fetching dimensions so
* it can be appropriately resized.
*/
function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthTokenRequired}: ImageWithSizeCalculationProps) {
function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthTokenRequired, objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL}: ImageWithSizeCalculationProps) {
const styles = useThemeStyles();
const isLoadedRef = useRef<boolean | null>(null);
const [isImageCached, setIsImageCached] = useState(true);
Expand Down Expand Up @@ -101,6 +106,7 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT
}}
onError={onError}
onLoad={imageLoadedSuccessfully}
objectPosition={objectPosition}
/>
{isLoading && !isImageCached && <FullscreenLoadingIndicator style={[styles.opacity1, styles.bgTransparent]} />}
</View>
Expand Down
4 changes: 3 additions & 1 deletion src/components/ReceiptImage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import {View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
import EReceiptThumbnail from './EReceiptThumbnail';
import type {IconSize} from './EReceiptThumbnail';
Expand Down Expand Up @@ -115,10 +116,11 @@ function ReceiptImage({
<ThumbnailImage
previewSourceURL={source ?? ''}
style={[styles.w100, styles.h100]}
isAuthTokenRequired
isAuthTokenRequired={isAuthTokenRequired ?? false}
shouldDynamicallyResize={false}
fallbackIcon={fallbackIcon}
fallbackIconSize={fallbackIconSize}
objectPosition={CONST.IMAGE_OBJECT_POSITION.TOP}
/>
);
}
Expand Down
3 changes: 3 additions & 0 deletions src/components/ReportActionItem/ReportActionItemImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,16 @@ function ReportActionItemImage({
source: thumbnailSource,
fallbackIcon: Expensicons.Receipt,
fallbackIconSize: isSingleImage ? variables.iconSizeSuperLarge : variables.iconSizeExtraLarge,
isAuthTokenRequired: true,
};
} else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') {
propsObj = {isPDFThumbnail: true, source: attachmentModalSource};
} else {
propsObj = {
isThumbnail,
...(isThumbnail && {iconSize: (isSingleImage ? 'medium' : 'small') as IconSize, fileExtension}),
shouldUseThumbnailImage: true,
isAuthTokenRequired: false,
source: thumbnail ?? image ?? '',
};
}
Expand Down
7 changes: 7 additions & 0 deletions src/components/ThumbnailImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useThumbnailDimensions from '@hooks/useThumbnailDimensions';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type IconAsset from '@src/types/utils/IconAsset';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import type {ImageObjectPosition} from './Image/types';
import ImageWithSizeCalculation from './ImageWithSizeCalculation';

type ThumbnailImageProps = {
Expand All @@ -35,6 +37,9 @@ type ThumbnailImageProps = {

/** Should the image be resized on load or just fit container */
shouldDynamicallyResize?: boolean;

/** The object position of image */
objectPosition?: ImageObjectPosition;
};

type UpdateImageSizeParams = {
Expand All @@ -51,6 +56,7 @@ function ThumbnailImage({
shouldDynamicallyResize = true,
fallbackIcon = Expensicons.Gallery,
fallbackIconSize = variables.iconSizeSuperLarge,
objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL,
}: ThumbnailImageProps) {
const styles = useThemeStyles();
const theme = useTheme();
Expand Down Expand Up @@ -102,6 +108,7 @@ function ThumbnailImage({
onMeasure={updateImageSize}
onLoadFailure={() => setFailedToLoad(true)}
isAuthTokenRequired={isAuthTokenRequired}
objectPosition={objectPosition}
/>
</View>
</View>
Expand Down

0 comments on commit b1d25e4

Please sign in to comment.