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}).`,