Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change high resolution image with preview #44807

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
751f540
Update version to 9.0.2-1
OSBotify Jun 26, 2024
d7542be
Merge pull request #44444 from software-mansion-labs/fix/link-to-with…
luacmartins Jun 26, 2024
98e5373
Update version to 9.0.2-2
OSBotify Jun 26, 2024
20f13e8
Merge pull request #44505 from Expensify/yuwen-fixSplitSelection
yuwenmemon Jun 26, 2024
d6c5cc5
Update version to 9.0.2-3
OSBotify Jun 27, 2024
75d4ce9
Merge pull request #44508 from etCoderDysto/fixHold
carlosmiceli Jun 26, 2024
7076b54
Change big image preview to thumbnail
wildan-m Jun 27, 2024
012de0a
Move hi Res image caption to attachment modal, change local upload be…
wildan-m Jun 28, 2024
8832aad
fix isHiResImage logic
wildan-m Jun 28, 2024
26d983a
change isUsedInAttachmentModal to isUploaded for more accurate result
wildan-m Jun 28, 2024
3cdd20b
Add info icon to hi res image text
wildan-m Jun 28, 2024
aa6b459
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Jul 2, 2024
04472fe
Refactor, remove unnecessary code
wildan-m Jul 2, 2024
f48e435
Remove redundant log, resolve typescript error
wildan-m Jul 3, 2024
440fa48
Add attachmentImageResized localization
wildan-m Jul 3, 2024
baf2452
prettier, refactor
wildan-m Jul 3, 2024
79bcfb5
replace rn text to custom text, remove unnecessary code, fix lint error.
wildan-m Jul 3, 2024
72e0409
Fix bug multiple attachments
wildan-m Jul 3, 2024
a8d61d9
Fix crash on ios when tapping arrow on hi res to standard resolution …
wildan-m Jul 4, 2024
6dafdc6
Run prettier, fix lint
wildan-m Jul 4, 2024
c67aa8a
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Jul 4, 2024
1acae72
lint
wildan-m Jul 4, 2024
8ca81d8
Add comment to explain why using isAttachmentCarouselScrolling to del…
wildan-m Jul 4, 2024
f5bc3da
add attachmentTooLarge text
wildan-m Jul 4, 2024
7208d5f
restore safeAreaPaddingBottomStyle
wildan-m Jul 5, 2024
28f74b1
Refine setIsHighResolutionImage, DRY, remove unnecessary code
wildan-m Jul 5, 2024
b8d9717
revert accidentally commited code
wildan-m Jul 5, 2024
25c372f
revert code xcode files
wildan-m Jul 5, 2024
9a42575
Remove unnecessary code
wildan-m Jul 5, 2024
53eda87
Add comment for previewSource type
wildan-m Jul 5, 2024
314f738
Fix the gap between text and button too wide in native
wildan-m Jul 5, 2024
1e94322
run prettier
wildan-m Jul 5, 2024
a017066
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Jul 5, 2024
208180d
Revise attachmentImageTooLarge spanish translation
wildan-m Jul 8, 2024
3e3edf9
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Jul 8, 2024
4bf5652
prevent unnecessary change, rephrase comment
wildan-m Jul 9, 2024
4daaae2
revert attachment modal
wildan-m Jul 9, 2024
8698b5c
add value to isUploaded, move HighResolutionInfo to AttachmentView
wildan-m Jul 9, 2024
50b7428
Adjust IMAGE_HIGH_RESOLUTION_THRESHOLD
wildan-m Jul 9, 2024
b7e8ec4
Merge
wildan-m Jul 9, 2024
d61979a
remove unnecessary code
wildan-m Jul 9, 2024
1b698e8
remove unnecessary code
wildan-m Jul 9, 2024
8444da1
run prettier
wildan-m Jul 9, 2024
dbd211a
Refactor HighResolutionInfo
wildan-m Jul 9, 2024
66a807b
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Jul 9, 2024
d67066a
Revert unnecessary change
wildan-m Jul 9, 2024
c34b47f
Adjust high resolution info padding
wildan-m Jul 11, 2024
3534200
Merge branch 'main' of https://github.com/wildan-m/App into wildan/fi…
wildan-m Jul 11, 2024
bba81bd
run prettier
wildan-m Jul 11, 2024
78ac528
fix isHighResolutionImage
wildan-m Jul 11, 2024
4c23e80
re-adjust vertical padding highResolutionInfoPadding
wildan-m Jul 11, 2024
fa37c50
fix highresolutioninfo padding
wildan-m Jul 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1203,6 +1203,8 @@ const CONST = {
NOTE: 'n',
},

IMAGE_HIGH_RESOLUTION_THRESHOLD: 7000,

IMAGE_OBJECT_POSITION: {
TOP: 'top',
INITIAL: 'initial',
Expand Down
1 change: 1 addition & 0 deletions src/components/AttachmentModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,7 @@ function AttachmentModal({
fallbackSource={fallbackSource}
isUsedInAttachmentModal
transactionID={transaction?.transactionID}
isUploaded={!isEmptyObject(report)}
/>
</AttachmentCarouselPagerContext.Provider>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr
<View style={[styles.imageModalImageCenterContainer]}>
<AttachmentView
source={item.source}
previewSource={item.previewSource}
file={item.file}
isAuthTokenRequired={item.isAuthTokenRequired}
onPress={onPress}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ type AttachmentCarouselPagerItems = {
/** The source of the image is used to identify each attachment/page in the pager */
source: AttachmentSource;

/** URL to preview-sized attachment that is also used for the thumbnail */
previewSource?: AttachmentSource;

/** The index of the pager item determines the order of the images in the pager */
index: number;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ function AttachmentCarouselPager(
}, [activePage, initialPage]);

/** The `pagerItems` object that passed down to the context. Later used to detect current page, whether it's a single image gallery etc. */
const pagerItems = useMemo(() => items.map((item, index) => ({source: item.source, index, isActive: index === activePageIndex})), [activePageIndex, items]);
const pagerItems = useMemo(
() => items.map((item, index) => ({source: item.source, previewSource: item.previewSource, index, isActive: index === activePageIndex})),
[activePageIndex, items],
);

const extractItemKey = useCallback(
(item: Attachment, index: number) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,17 @@ function extractAttachments(
if (name === 'img' && attribs.src) {
const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src);
const previewSource = tryResolveUrlFromApiRoot(attribs.src);
if (uniqueSources.has(source)) {
return;
}

uniqueSources.add(source);
let fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`);

const width = (attribs['data-expensify-width'] && parseInt(attribs['data-expensify-width'], 10)) || undefined;
const height = (attribs['data-expensify-height'] && parseInt(attribs['data-expensify-height'], 10)) || undefined;

// Public image URLs might lack a file extension in the source URL, without an extension our
// AttachmentView fails to recognize them as images and renders fallback content instead.
// We apply this small hack to add an image extension and ensure AttachmentView renders the image.
Expand All @@ -72,8 +76,9 @@ function extractAttachments(
attachments.unshift({
reportActionID: attribs['data-id'],
source,
previewSource,
isAuthTokenRequired: !!expensifySource,
file: {name: fileName},
file: {name: fileName, width, height},
isReceipt: false,
hasBeenFlagged: attribs['data-flagged'] === 'true',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import Tooltip from '@components/Tooltip';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import type IconAsset from '@src/types/utils/IconAsset';

type DefaultAttachmentViewProps = {
/** The name of the file */
Expand All @@ -21,9 +22,11 @@ type DefaultAttachmentViewProps = {

/** Additional styles for the container */
containerStyles?: StyleProp<ViewStyle>;

icon?: IconAsset;
};

function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles}: DefaultAttachmentViewProps) {
function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = false, shouldShowDownloadIcon, containerStyles, icon}: DefaultAttachmentViewProps) {
const theme = useTheme();
const styles = useThemeStyles();
const {translate} = useLocalize();
Expand All @@ -33,7 +36,7 @@ function DefaultAttachmentView({fileName = '', shouldShowLoadingSpinnerIcon = fa
<View style={styles.mr2}>
<Icon
fill={theme.icon}
src={Expensicons.Paperclip}
src={icon ?? Expensicons.Paperclip}
/>
</View>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import {View} from 'react-native';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';

function HighResolutionInfo({isUploaded}: {isUploaded: boolean}) {
const theme = useTheme();
const styles = useThemeStyles();
const stylesUtils = useStyleUtils();
const {translate} = useLocalize();

return (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.gap2, styles.justifyContentCenter, stylesUtils.getHighResolutionInfoWrapperStyle(isUploaded)]}>
<Icon
src={Expensicons.Info}
height={variables.iconSizeExtraSmall}
width={variables.iconSizeExtraSmall}
fill={theme.icon}
additionalStyles={styles.p1}
/>
<Text style={[styles.textLabelSupporting]}>{isUploaded ? translate('attachmentPicker.attachmentImageResized') : translate('attachmentPicker.attachmentImageTooLarge')}</Text>
</View>
);
}

export default HighResolutionInfo;
87 changes: 64 additions & 23 deletions src/components/Attachments/AttachmentView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {Attachment, AttachmentSource} from '@components/Attachments/types';
import DistanceEReceipt from '@components/DistanceEReceipt';
import EReceipt from '@components/EReceipt';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import ScrollView from '@components/ScrollView';
import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -17,6 +18,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CachedPDFPaths from '@libs/actions/CachedPDFPaths';
import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL';
import * as FileUtils from '@libs/fileDownload/FileUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import type {ColorValue} from '@styles/utils/types';
import variables from '@styles/variables';
Expand All @@ -26,6 +28,7 @@ import AttachmentViewImage from './AttachmentViewImage';
import AttachmentViewPdf from './AttachmentViewPdf';
import AttachmentViewVideo from './AttachmentViewVideo';
import DefaultAttachmentView from './DefaultAttachmentView';
import HighResolutionInfo from './HighResolutionInfo';

type AttachmentViewOnyxProps = {
transaction: OnyxEntry<Transaction>;
Expand Down Expand Up @@ -70,10 +73,14 @@ type AttachmentViewProps = AttachmentViewOnyxProps &

/** Whether the attachment is used as a chat attachment */
isUsedAsChatAttachment?: boolean;

/* Flag indicating whether the attachment has been uploaded. */
isUploaded?: boolean;
};

function AttachmentView({
source,
previewSource,
file,
isAuthTokenRequired,
onPress,
Expand All @@ -92,13 +99,15 @@ function AttachmentView({
isHovered,
duration,
isUsedAsChatAttachment,
isUploaded = true,
}: AttachmentViewProps) {
const {translate} = useLocalize();
const {updateCurrentlyPlayingURL} = usePlaybackContext();
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [loadComplete, setLoadComplete] = useState(false);
const [isHighResolution, setIsHighResolution] = useState<boolean>(false);
const [hasPDFFailedToLoad, setHasPDFFailedToLoad] = useState(false);
const isVideo = (typeof source === 'string' && Str.isVideo(source)) || (file?.name && Str.isVideo(file.name));

Expand All @@ -113,6 +122,12 @@ function AttachmentView({

useNetwork({onReconnect: () => setImageError(false)});

useEffect(() => {
FileUtils.getFileResolution(file).then((resolution) => {
setIsHighResolution(FileUtils.isHighResolutionImage(resolution));
});
}, [file]);

// Handles case where source is a component (ex: SVG) or a number
// Number may represent a SVG or an image
if (typeof source === 'function' || (maybeIcon && typeof source === 'number')) {
Expand Down Expand Up @@ -196,35 +211,61 @@ function AttachmentView({
// For this check we use both source and file.name since temporary file source is a blob
// both PDFs and images will appear as images when pasted into the text field.
// We also check for numeric source since this is how static images (used for preview) are represented in RN.
const isImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
if (isImage || (file?.name && Str.isImage(file.name))) {
if (imageError) {
// AttachmentViewImage can't handle icon fallbacks, so we need to handle it here
if (typeof fallbackSource === 'number' || typeof fallbackSource === 'function') {
const isSourceImage = typeof source === 'number' || (typeof source === 'string' && Str.isImage(source));
const isFileNameImage = file?.name && Str.isImage(file.name);
const isFileImage = isSourceImage || isFileNameImage;

if (isFileImage) {
if (imageError && (typeof fallbackSource === 'number' || typeof fallbackSource === 'function')) {
return (
<Icon
src={fallbackSource}
height={variables.defaultAvatarPreviewSize}
width={variables.defaultAvatarPreviewSize}
additionalStyles={[styles.alignItemsCenter, styles.justifyContentCenter, styles.flex1]}
fill={theme.border}
/>
);
}
let imageSource = imageError && fallbackSource ? (fallbackSource as string) : (source as string);

if (isHighResolution) {
if (!isUploaded) {
return (
<Icon
src={fallbackSource}
height={variables.defaultAvatarPreviewSize}
width={variables.defaultAvatarPreviewSize}
additionalStyles={[styles.alignItemsCenter, styles.justifyContentCenter, styles.flex1]}
fill={theme.border}
/>
<>
<View style={styles.imageModalImageCenterContainer}>
<DefaultAttachmentView
icon={Expensicons.Gallery}
fileName={file?.name}
shouldShowDownloadIcon={shouldShowDownloadIcon}
shouldShowLoadingSpinnerIcon={shouldShowLoadingSpinnerIcon}
containerStyles={containerStyles}
/>
</View>
<HighResolutionInfo isUploaded={isUploaded} />
</>
);
}
imageSource = previewSource?.toString() ?? imageSource;
}

return (
<AttachmentViewImage
url={imageError && fallbackSource ? (fallbackSource as string) : (source as string)}
file={file}
isAuthTokenRequired={isAuthTokenRequired}
loadComplete={loadComplete}
isImage={isImage}
onPress={onPress}
onError={() => {
setImageError(true);
}}
/>
<>
<View style={styles.imageModalImageCenterContainer}>
<AttachmentViewImage
url={imageSource}
file={file}
isAuthTokenRequired={isAuthTokenRequired}
loadComplete={loadComplete}
isImage={isFileImage}
onPress={onPress}
onError={() => {
setImageError(true);
}}
/>
</View>
{isHighResolution && <HighResolutionInfo isUploaded={isUploaded} />}
</>
);
}

Expand Down
3 changes: 3 additions & 0 deletions src/components/Attachments/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ type Attachment = {
/** URL to full-sized attachment, SVG function, or numeric static image on native platforms */
source: AttachmentSource;

/** URL to preview-sized attachment that is also used for the thumbnail */
previewSource?: AttachmentSource;

/** File object can be an instance of File or Object */
file?: FileObject;

Expand Down
2 changes: 1 addition & 1 deletion src/components/Lightbox/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
};
}

const foundPage = attachmentCarouselPagerContext.pagerItems.findIndex((item) => item.source === uri);
const foundPage = attachmentCarouselPagerContext.pagerItems.findIndex((item) => item.source === uri || item.previewSource === uri);
return {
...attachmentCarouselPagerContext,
isUsedInCarousel: !!attachmentCarouselPagerContext.pagerRef,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,8 @@ export default {
notAllowedExtension: 'This file type is not allowed. Please try a different file type.',
folderNotAllowedMessage: 'Uploading a folder is not allowed. Please try a different file.',
protectedPDFNotSupported: 'Password-protected PDF is not supported',
attachmentImageResized: 'This image has been resized for previewing. Download for full resolution.',
attachmentImageTooLarge: 'This image is too large to preview before uploading.',
},
connectionComplete: {
title: 'Connection complete',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,8 @@ export default {
notAllowedExtension: 'Este tipo de archivo no es compatible',
folderNotAllowedMessage: 'Subir una carpeta no está permitido. Prueba con otro archivo.',
protectedPDFNotSupported: 'Los PDFs con contraseña no son compatibles',
attachmentImageResized: 'Se ha cambiado el tamaño de esta imagen para obtener una vista previa. Descargar para resolución completa.',
attachmentImageTooLarge: 'Esta imagen es demasiado grande para obtener una vista previa antes de subirla.',
},
avatarCropModal: {
title: 'Editar foto',
Expand Down
27 changes: 27 additions & 0 deletions src/libs/fileDownload/FileUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import DateUtils from '@libs/DateUtils';
import * as Localize from '@libs/Localize';
import Log from '@libs/Log';
import CONST from '@src/CONST';
import getImageResolution from './getImageResolution';
import type {ReadFileAsync, SplitExtensionFromFileName} from './types';

/**
Expand Down Expand Up @@ -261,6 +262,29 @@ function isLocalFile(receiptUri?: string | number): boolean {
return typeof receiptUri === 'number' || receiptUri?.startsWith('blob:') || receiptUri?.startsWith('file:') || receiptUri?.startsWith('/');
}

function getFileResolution(targetFile: FileObject | undefined): Promise<{width: number; height: number} | null> {
if (!targetFile) {
return Promise.resolve(null);
}

// If the file already has width and height, return them directly
if ('width' in targetFile && 'height' in targetFile) {
return Promise.resolve({width: targetFile.width ?? 0, height: targetFile.height ?? 0});
}

// Otherwise, attempt to get the image resolution
return getImageResolution(targetFile)
.then(({width, height}) => ({width, height}))
.catch((error: Error) => {
Log.hmmm('Failed to get image resolution:', error);
return null;
});
}

function isHighResolutionImage(resolution: {width: number; height: number} | null): boolean {
return resolution !== null && (resolution.width > CONST.IMAGE_HIGH_RESOLUTION_THRESHOLD || resolution.height > CONST.IMAGE_HIGH_RESOLUTION_THRESHOLD);
}

export {
showGeneralErrorAlert,
showSuccessAlert,
Expand All @@ -275,4 +299,7 @@ export {
base64ToFile,
isLocalFile,
validateImageForCorruption,
isImage,
getFileResolution,
isHighResolutionImage,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// eslint-disable-next-line no-restricted-imports
import spacing from '@styles/utils/spacing';
import type GetHighResolutionInfoWrapperStyle from './types';

const getHighResolutionInfoWrapperStyle: GetHighResolutionInfoWrapperStyle = (isUploaded) => ({
...spacing.ph8,
...spacing.pt5,
...(isUploaded ? spacing.pb5 : spacing.mbn1),
});

export default getHighResolutionInfoWrapperStyle;
11 changes: 11 additions & 0 deletions src/styles/utils/getHighResolutionInfoWrapperStyle/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// eslint-disable-next-line no-restricted-imports
import spacing from '@styles/utils/spacing';
import type GetHighResolutionInfoWrapperStyle from './types';

const getHighResolutionInfoWrapperStyle: GetHighResolutionInfoWrapperStyle = (isUploaded) => ({
...spacing.ph5,
...spacing.pt5,
...(isUploaded ? spacing.pb5 : spacing.mbn1),
});

export default getHighResolutionInfoWrapperStyle;
Loading
Loading