diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx
index f697142c556e..fb6a8e911e87 100644
--- a/src/components/AttachmentModal.tsx
+++ b/src/components/AttachmentModal.tsx
@@ -456,7 +456,7 @@ function AttachmentModal({
}
const context = useMemo(
() => ({
- pagerItems: [],
+ pagerItems: [{source: sourceForAttachmentView, index: 0, isActive: true}],
activePage: 0,
pagerRef: undefined,
isPagerScrolling: nope,
@@ -465,7 +465,7 @@ function AttachmentModal({
onScaleChanged: () => {},
onSwipeDown: closeModal,
}),
- [closeModal, nope],
+ [closeModal, nope, sourceForAttachmentView],
);
return (
diff --git a/src/components/AttachmentOfflineIndicator.tsx b/src/components/AttachmentOfflineIndicator.tsx
new file mode 100644
index 000000000000..d425e6f18e0e
--- /dev/null
+++ b/src/components/AttachmentOfflineIndicator.tsx
@@ -0,0 +1,57 @@
+import React, {useEffect, useState} from 'react';
+import {View} from 'react-native';
+import useLocalize from '@hooks/useLocalize';
+import useNetwork from '@hooks/useNetwork';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import variables from '@styles/variables';
+import Icon from './Icon';
+import * as Expensicons from './Icon/Expensicons';
+import Text from './Text';
+
+type AttachmentOfflineIndicatorProps = {
+ /** Whether the offline indicator is displayed for the attachment preview. */
+ isPreview?: boolean;
+};
+
+function AttachmentOfflineIndicator({isPreview = false}: AttachmentOfflineIndicatorProps) {
+ const theme = useTheme();
+ const styles = useThemeStyles();
+ const {isOffline} = useNetwork();
+ const {translate} = useLocalize();
+
+ // We don't want to show the offline indicator when the attachment is a cached one, so
+ // we delay the display by 200 ms to ensure it is not a cached one.
+ const [onCacheDelay, setOnCacheDelay] = useState(true);
+
+ useEffect(() => {
+ const timeout = setTimeout(() => setOnCacheDelay(false), 200);
+
+ return () => clearTimeout(timeout);
+ }, []);
+
+ if (!isOffline || onCacheDelay) {
+ return null;
+ }
+
+ return (
+
+
+ {!isPreview && (
+
+ {translate('common.youAppearToBeOffline')}
+ {translate('common.attachementWillBeAvailableOnceBackOnline')}
+
+ )}
+
+ );
+}
+
+AttachmentOfflineIndicator.displayName = 'AttachmentOfflineIndicator';
+
+export default AttachmentOfflineIndicator;
diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx
index 5d09e7abf41d..f08941ef7d77 100644
--- a/src/components/ImageView/index.tsx
+++ b/src/components/ImageView/index.tsx
@@ -2,11 +2,13 @@ import type {SyntheticEvent} from 'react';
import React, {useCallback, useEffect, useRef, useState} from 'react';
import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native';
import {View} from 'react-native';
+import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator';
import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import Image from '@components/Image';
import RESIZE_MODES from '@components/Image/resizeModes';
import type {ImageOnLoadEvent} from '@components/Image/types';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
+import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
@@ -33,6 +35,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
const [imgHeight, setImgHeight] = useState(0);
const [zoomScale, setZoomScale] = useState(0);
const [zoomDelta, setZoomDelta] = useState();
+ const {isOffline} = useNetwork();
const scrollableRef = useRef(null);
const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen();
@@ -210,7 +213,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
onLoad={imageLoad}
onError={onError}
/>
- {(isLoading || zoomScale === 0) && }
+ {((isLoading && !isOffline) || (!isLoading && zoomScale === 0)) && }
+ {isLoading && }
);
}
@@ -243,7 +247,8 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
/>
- {isLoading && }
+ {isLoading && !isOffline && }
+ {isLoading && }
);
}
diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx
index eac5c676370b..3d940103715d 100644
--- a/src/components/ImageWithSizeCalculation.tsx
+++ b/src/components/ImageWithSizeCalculation.tsx
@@ -6,6 +6,7 @@ import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import Log from '@libs/Log';
import CONST from '@src/CONST';
+import AttachmentOfflineIndicator from './AttachmentOfflineIndicator';
import FullscreenLoadingIndicator from './FullscreenLoadingIndicator';
import Image from './Image';
import RESIZE_MODES from './Image/resizeModes';
@@ -108,7 +109,8 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT
onLoad={imageLoadedSuccessfully}
objectPosition={objectPosition}
/>
- {isLoading && !isImageCached && }
+ {isLoading && !isImageCached && !isOffline && }
+ {isLoading && !isImageCached && }
);
}
diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx
index 86a52c2baf6c..0be0171eaa9a 100644
--- a/src/components/Lightbox/index.tsx
+++ b/src/components/Lightbox/index.tsx
@@ -2,12 +2,14 @@ import React, {useCallback, useContext, useEffect, useMemo, useState} from 'reac
import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native';
import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
import {useSharedValue} from 'react-native-reanimated';
+import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator';
import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext';
import Image from '@components/Image';
import type {ImageOnLoadEvent} from '@components/Image/types';
import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas';
import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from '@components/MultiGestureCanvas/types';
import {getCanvasFitScale} from '@components/MultiGestureCanvas/utils';
+import useNetwork from '@hooks/useNetwork';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import NUMBER_OF_CONCURRENT_LIGHTBOXES from './numberOfConcurrentLightboxes';
@@ -47,6 +49,7 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
* we need to create a shared value that can be used in the render function.
*/
const isPagerScrollingFallback = useSharedValue(false);
+ const {isOffline} = useNetwork();
const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext);
const {
@@ -219,8 +222,8 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
style={[contentSize ?? styles.invisibleImage]}
isAuthTokenRequired={isAuthTokenRequired}
onError={onError}
- onLoad={updateContentSize}
- onLoadEnd={() => {
+ onLoad={(e) => {
+ updateContentSize(e);
setLightboxImageLoaded(true);
}}
/>
@@ -236,19 +239,22 @@ function Lightbox({isAuthTokenRequired = false, uri, onScaleChanged: onScaleChan
resizeMode="contain"
style={[fallbackSize ?? styles.invisibleImage]}
isAuthTokenRequired={isAuthTokenRequired}
- onLoad={updateContentSize}
- onLoadEnd={() => setFallbackImageLoaded(true)}
+ onLoad={(e) => {
+ updateContentSize(e);
+ setFallbackImageLoaded(true);
+ }}
/>
)}
{/* Show activity indicator while the lightbox is still loading the image. */}
- {isLoading && (
+ {isLoading && !isOffline && (
)}
+ {isLoading && }
>
)}
diff --git a/src/components/VideoPlayer/BaseVideoPlayer.tsx b/src/components/VideoPlayer/BaseVideoPlayer.tsx
index f74c8753f3d1..462911968000 100644
--- a/src/components/VideoPlayer/BaseVideoPlayer.tsx
+++ b/src/components/VideoPlayer/BaseVideoPlayer.tsx
@@ -5,6 +5,7 @@ import type {MutableRefObject} from 'react';
import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react';
import type {GestureResponderEvent} from 'react-native';
import {View} from 'react-native';
+import AttachmentOfflineIndicator from '@components/AttachmentOfflineIndicator';
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import Hoverable from '@components/Hoverable';
import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback';
@@ -45,6 +46,7 @@ function BaseVideoPlayer({
// user hovers the mouse over the carousel arrows, but this UI bug feels much less troublesome for now.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isVideoHovered = false,
+ isPreview,
}: VideoPlayerProps) {
const styles = useThemeStyles();
const {
@@ -352,9 +354,8 @@ function BaseVideoPlayer({
)}
-
- {(isLoading || isBuffering) && }
-
+ {((isLoading && !isOffline) || isBuffering) && }
+ {isLoading && !isBuffering && }
{controlsStatus !== CONST.VIDEO_PLAYER.CONTROLS_STATUS.HIDE && !isLoading && (isPopoverVisible || isHovered || canUseTouchScreen) && (
;
shouldPlay?: boolean;
+ isPreview?: boolean;
};
export type {VideoPlayerProps, VideoWithOnFullScreenUpdate};
diff --git a/src/components/VideoPlayerPreview/index.tsx b/src/components/VideoPlayerPreview/index.tsx
index cf93ca2462c4..6c170a91f640 100644
--- a/src/components/VideoPlayerPreview/index.tsx
+++ b/src/components/VideoPlayerPreview/index.tsx
@@ -85,6 +85,7 @@ function VideoPlayerPreview({videoUrl, thumbnailUrl, reportID, fileName, videoDi
videoDuration={videoDuration}
shouldUseSmallVideoControls
style={[styles.w100, styles.h100]}
+ isPreview
videoPlayerStyle={styles.videoPlayerPreview}
/>
diff --git a/src/languages/en.ts b/src/languages/en.ts
index c174c2182ef2..9aea8de06748 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -271,6 +271,7 @@ export default {
conciergeHelp: 'Please reach out to Concierge for help.',
youAppearToBeOffline: 'You appear to be offline.',
thisFeatureRequiresInternet: 'This feature requires an active internet connection to be used.',
+ attachementWillBeAvailableOnceBackOnline: 'Attachment will become available once back online.',
areYouSure: 'Are you sure?',
verify: 'Verify',
yesContinue: 'Yes, continue',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index e9e1ed6eb815..ba1f85069801 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -261,6 +261,7 @@ export default {
conciergeHelp: 'Por favor, contacta con Concierge para obtener ayuda.',
youAppearToBeOffline: 'Parece que estás desconectado.',
thisFeatureRequiresInternet: 'Esta función requiere una conexión a Internet activa para ser utilizada.',
+ attachementWillBeAvailableOnceBackOnline: 'El archivo adjunto estará disponible cuando vuelvas a estar en línea.',
areYouSure: '¿Estás seguro?',
verify: 'Verifique',
yesContinue: 'Sí, continuar',