diff --git a/assets/images/attachment-not-found.svg b/assets/images/attachment-not-found.svg new file mode 100644 index 000000000000..25da973ce9cb --- /dev/null +++ b/assets/images/attachment-not-found.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index 4de43a763231..5800e92cc4f4 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -4,6 +4,7 @@ import {View} from 'react-native'; import AttachmentView from '@components/Attachments/AttachmentView'; import type {Attachment} from '@components/Attachments/types'; import Button from '@components/Button'; +import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; import Text from '@components/Text'; @@ -83,6 +84,7 @@ function CarouselItem({item, onPress, isFocused, isModalHovered}: CarouselItemPr isHovered={isModalHovered} isFocused={isFocused} duration={item.duration} + fallbackSource={Expensicons.AttachmentNotFound} /> diff --git a/src/components/Attachments/AttachmentView/index.tsx b/src/components/Attachments/AttachmentView/index.tsx index 0af1a86992e7..1281c017308d 100644 --- a/src/components/Attachments/AttachmentView/index.tsx +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -10,6 +10,7 @@ import EReceipt from '@components/EReceipt'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; import {usePlaybackContext} from '@components/VideoPlayerContexts/PlaybackContext'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -127,7 +128,7 @@ function AttachmentView({ const [imageError, setImageError] = useState(false); - useNetwork({onReconnect: () => setImageError(false)}); + const {isOffline} = useNetwork({onReconnect: () => setImageError(false)}); useEffect(() => { FileUtils.getFileResolution(file).then((resolution) => { @@ -226,15 +227,20 @@ function AttachmentView({ if (isFileImage) { if (imageError && (typeof fallbackSource === 'number' || typeof fallbackSource === 'function')) { return ( - + + + + {translate('attachmentView.attachmentNotFound')} + + ); } + let imageSource = imageError && fallbackSource ? (fallbackSource as string) : (source as string); if (isHighResolution) { @@ -268,6 +274,9 @@ function AttachmentView({ isImage={isFileImage} onPress={onPress} onError={() => { + if (isOffline) { + return; + } setImageError(true); }} /> diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx index 43177be408ff..f53e490dd0f9 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx +++ b/src/components/HTMLEngineProvider/HTMLRenderers/ImageRenderer.tsx @@ -1,4 +1,4 @@ -import React, {memo, useState} from 'react'; +import React, {memo} from 'react'; import {useOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; import type {CustomRendererProps, TBlock} from 'react-native-render-html'; @@ -67,7 +67,6 @@ function ImageRenderer({tnode}: ImageRendererProps) { const fileType = FileUtils.getFileType(attachmentSourceAttribute); const fallbackIcon = fileType === CONST.ATTACHMENT_FILE_TYPE.FILE ? Expensicons.Document : Expensicons.GalleryNotFound; - const [hasLoadFailed, setHasLoadFailed] = useState(true); const theme = useTheme(); let fileName = htmlAttribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${isAttachmentOrReceipt ? attachmentSourceAttribute : htmlAttribs.src}`); @@ -86,8 +85,6 @@ function ImageRenderer({tnode}: ImageRendererProps) { imageHeight={imageHeight} isDeleted={isDeleted} altText={alt} - onLoadFailure={() => setHasLoadFailed(true)} - onMeasure={() => setHasLoadFailed(false)} fallbackIconBackground={theme.highlightBG} fallbackIconColor={theme.border} /> @@ -119,7 +116,6 @@ function ImageRenderer({tnode}: ImageRendererProps) { shouldUseHapticsOnLongPress accessibilityRole={CONST.ROLE.BUTTON} accessibilityLabel={translate('accessibilityHints.viewAttachment')} - disabled={hasLoadFailed} > {thumbnailImageComponent} diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index fa531ce34adf..bd4bb64da050 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -8,6 +8,7 @@ import ArrowRight from '@assets/images/arrow-right.svg'; import ArrowUpLong from '@assets/images/arrow-up-long.svg'; import UpArrow from '@assets/images/arrow-up.svg'; import ArrowsUpDown from '@assets/images/arrows-updown.svg'; +import AttachmentNotFound from '@assets/images/attachment-not-found.svg'; import AdminRoomAvatar from '@assets/images/avatars/admin-room.svg'; import AnnounceRoomAvatar from '@assets/images/avatars/announce-room.svg'; import ConciergeAvatar from '@assets/images/avatars/concierge-avatar.svg'; @@ -217,6 +218,7 @@ export { ArrowsUpDown, ArrowUpLong, ArrowDownLong, + AttachmentNotFound, Wrench, BackArrow, Bank, diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index 266ed2eed16a..0bce2fd38432 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -196,8 +196,12 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV document.removeEventListener('mouseup', trackPointerPosition); }; }, [canUseTouchScreen, trackMovement, trackPointerPosition]); - - const isLocalFile = FileUtils.isLocalFile(url); + // isLocalToUserDeviceFile means the file is located on the user device, + // not loaded on the server yet (the user is offline when loading this file in fact) + let isLocalToUserDeviceFile = FileUtils.isLocalFile(url); + if (isLocalToUserDeviceFile && typeof url === 'string' && url.startsWith('/chat-attachments')) { + isLocalToUserDeviceFile = false; + } if (canUseTouchScreen) { return ( @@ -238,8 +242,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV /> - {isLoading && (!isOffline || isLocalFile) && } - {isLoading && !isLocalFile && } + {isLoading && (!isOffline || isLocalToUserDeviceFile) && } + {isLoading && !isLocalToUserDeviceFile && } ); } diff --git a/src/languages/en.ts b/src/languages/en.ts index 7f05ea436837..fa78b563c522 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1970,6 +1970,7 @@ const translations = { afterLinkText: 'to view it.', formLabel: 'View PDF', }, + attachmentNotFound: 'Attachment not found', }, messages: { errorMessageInvalidPhone: `Please enter a valid phone number without brackets or dashes. If you're outside the US, please include your country code (e.g. ${CONST.EXAMPLE_PHONE_NUMBER}).`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 92ffd8728d97..0c682569263f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1990,6 +1990,7 @@ const translations = { afterLinkText: 'para verlo.', formLabel: 'Ver PDF', }, + attachmentNotFound: 'Archivo adjunto no encontrado', }, messages: { errorMessageInvalidPhone: `Por favor, introduce un número de teléfono válido sin paréntesis o guiones. Si reside fuera de Estados Unidos, por favor incluye el prefijo internacional (p. ej. ${CONST.EXAMPLE_PHONE_NUMBER}).`,