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

Fix: Chat - Loader is shown on top of image even after image is loaded in attachment carousel #27751

Merged
merged 7 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable es/no-optional-chaining */
import React, {useContext, useEffect, useState} from 'react';
import React, {useContext, useEffect, useRef, useState} from 'react';
import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native';
import PropTypes from 'prop-types';
import Image from '../../../Image';
Expand Down Expand Up @@ -46,13 +46,16 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialI
}, [initialIsActive]);

const [initialActivePageLoad, setInitialActivePageLoad] = useState(isActive);
const [isImageLoading, setIsImageLoading] = useState(true);
const [showFallback, setShowFallback] = useState(isImageLoading);
const isImageLoaded = useRef(null);
const [isImageLoading, setIsImageLoading] = useState(false);
const [isFallbackLoading, setIsFallbackLoading] = useState(false);
const [showFallback, setShowFallback] = useState(true);

// We delay hiding the fallback image while image transformer is still rendering
useEffect(() => {
if (isImageLoading) setShowFallback(true);
if (isImageLoading || showFallback) setShowFallback(true);
else setTimeout(() => setShowFallback(false), 100);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isImageLoading]);

return (
Expand All @@ -73,8 +76,14 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialI
source={{uri: source}}
style={dimensions == null ? undefined : {width: dimensions.imageWidth, height: dimensions.imageHeight}}
isAuthTokenRequired={isAuthTokenRequired}
onLoadStart={() => setIsImageLoading(true)}
onLoadEnd={() => setIsImageLoading(false)}
onLoadStart={() => {
setIsImageLoading(true);
}}
onLoadEnd={() => {
setShowFallback(false);
setIsImageLoading(false);
isImageLoaded.current = true;
}}
onLoad={(evt) => {
const imageWidth = (evt.nativeEvent?.width || 0) / PixelRatio.get();
const imageHeight = (evt.nativeEvent?.height || 0) / PixelRatio.get();
Expand All @@ -100,8 +109,10 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialI
// On the initial render of the active page, the onLoadEnd event is never fired.
// That's why we instead set isImageLoading to false in the onLoad event.
if (initialActivePageLoad) {
setIsImageLoading(false);
setInitialActivePageLoad(false);
setIsImageLoading(false);
setTimeout(() => setShowFallback(false), 100);
isImageLoaded.current = true;
}
}}
/>
Expand All @@ -110,12 +121,20 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialI
)}

{/* Keep rendering the image without gestures as fallback while ImageTransformer is loading the image */}
{(!isActive || showFallback) && (
{(showFallback || !isActive) && (
<ImageWrapper>
<Image
source={{uri: source}}
isAuthTokenRequired={isAuthTokenRequired}
onLoadStart={() => setIsImageLoading(true)}
onLoadStart={() => {
setIsImageLoading(true);
if (isImageLoaded.current) return;
setIsFallbackLoading(true);
}}
onLoadEnd={() => {
if (isImageLoaded.current) return;
setIsFallbackLoading(false);
}}
onLoad={(evt) => {
const imageWidth = evt.nativeEvent.width;
const imageHeight = evt.nativeEvent.height;
Expand All @@ -141,7 +160,7 @@ function AttachmentCarouselPage({source, isAuthTokenRequired, isActive: initialI
)}

{/* Show activity indicator while ImageTransfomer is still loading the image. */}
{isActive && isImageLoading && (
{isActive && isFallbackLoading && !isImageLoaded.current && (
<ActivityIndicator
size="large"
style={StyleSheet.absoluteFill}
Expand Down
33 changes: 30 additions & 3 deletions src/components/Image/index.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite understand, the original issue was happening on native Android, why are we adding changes specifically to non-native Image/index.js?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are also conflicts with main (there were recent changes)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Krishna2323, friendly bump on this in case you've missed it

I don't quite understand, the original issue was happening on native Android, why are we adding changes specifically to non-native Image/index.js?

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useMemo} from 'react';
import React, {useEffect, useMemo, useRef, useState} from 'react';
import {Image as RNImage} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import lodashGet from 'lodash/get';
Expand All @@ -8,7 +8,10 @@ import {defaultProps, imagePropTypes} from './imagePropTypes';
import RESIZE_MODES from './resizeModes';

function Image(props) {
const {source: propsSource, isAuthTokenRequired, onLoad, session} = props;
const {source: propsSource, isAuthTokenRequired, onLoad, session, onLoadStart = () => {}, onLoadEnd = () => {}} = props;

const [isLoading, setIsLoading] = useState(false);
const isLoadedRef = useRef(null);
/**
* Check if the image source is a URL - if so the `encryptedAuthToken` is appended
* to the source.
Expand Down Expand Up @@ -41,14 +44,38 @@ function Image(props) {
});
}, [onLoad, source]);

/** Delay the loader to detect whether the image is being loaded from the cache or the internet. */
useEffect(() => {
if (isLoadedRef.current || !isLoading) {
return;
}
const timeout = _.delay(() => {
if (!isLoading || isLoadedRef.current) {
return;
}
onLoadStart();
}, 200);
return () => clearTimeout(timeout);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isLoading]);

// Omit the props which the underlying RNImage won't use
const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']);
const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired', 'onLoadStart', 'onLoadEnd']);

return (
<RNImage
// eslint-disable-next-line react/jsx-props-no-spreading
{...forwardedProps}
source={source}
onLoadStart={() => setIsLoading(true)}
onLoad={() => {
isLoadedRef.current = true;
}}
onLoadEnd={() => {
setIsLoading(false);
isLoadedRef.current = true;
onLoadEnd();
}}
/>
);
}
Expand Down
26 changes: 2 additions & 24 deletions src/components/ImageWithSizeCalculation.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import _ from 'underscore';
import React, {useState, useRef, useEffect} from 'react';
import React, {useState} from 'react';
import {View} from 'react-native';
import PropTypes from 'prop-types';
import Log from '../libs/Log';
Expand Down Expand Up @@ -39,36 +38,19 @@ const defaultProps = {
*
*/
function ImageWithSizeCalculation(props) {
const isLoadedRef = useRef(null);
const [isImageCached, setIsImageCached] = useState(true);
const [isLoading, setIsLoading] = useState(false);

const onError = () => {
Log.hmmm('Unable to fetch image to calculate size', {url: props.url});
};

const imageLoadedSuccessfully = (event) => {
isLoadedRef.current = true;
props.onMeasure({
width: event.nativeEvent.width,
height: event.nativeEvent.height,
});
};

/** Delay the loader to detect whether the image is being loaded from the cache or the internet. */
useEffect(() => {
if (isLoadedRef.current || !isLoading) {
return;
}
const timeout = _.delay(() => {
if (!isLoading || isLoadedRef.current) {
return;
}
setIsImageCached(false);
}, 200);
return () => clearTimeout(timeout);
}, [isLoading]);

return (
<View style={[styles.w100, styles.h100, props.style]}>
<Image
Expand All @@ -77,19 +59,15 @@ function ImageWithSizeCalculation(props) {
isAuthTokenRequired={props.isAuthTokenRequired}
resizeMode={Image.resizeMode.cover}
onLoadStart={() => {
if (isLoadedRef.current || isLoading) {
return;
}
setIsLoading(true);
}}
onLoadEnd={() => {
setIsLoading(false);
setIsImageCached(true);
}}
onError={onError}
onLoad={imageLoadedSuccessfully}
/>
{isLoading && !isImageCached && <FullscreenLoadingIndicator style={[styles.opacity1, styles.bgTransparent]} />}
{isLoading && <FullscreenLoadingIndicator style={[styles.opacity1, styles.bgTransparent]} />}
</View>
);
}
Expand Down
Loading