Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
119 changes: 74 additions & 45 deletions package/src/components/Attachment/Gallery.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import React, { useMemo } from 'react';
import { Pressable, StyleSheet, Text, View } from 'react-native';
import React, { useEffect, useMemo, useRef } from 'react';
import {
ImageErrorEventData,
NativeSyntheticEvent,
Pressable,
StyleSheet,
Text,
View,
} from 'react-native';

import type { Attachment, LocalMessage } from 'stream-chat';

Expand Down Expand Up @@ -35,6 +42,7 @@ import {
import { useTheme } from '../../contexts/themeContext/ThemeContext';

import { useLoadingImage } from '../../hooks/useLoadingImage';
import { useStableCallback } from '../../hooks/useStableCallback';
import { isVideoPlayerAvailable } from '../../native';
import { primitives } from '../../theme';
import { FileTypes } from '../../types/types';
Expand All @@ -59,22 +67,22 @@ export type GalleryPropsWithContext = Pick<ImageGalleryContextValue, 'imageGalle
| 'VideoThumbnail'
| 'ImageLoadingIndicator'
| 'ImageLoadingFailedIndicator'
| 'ImageReloadIndicator'
| 'myMessageTheme'
> &
Pick<OverlayContextValue, 'setOverlay'> & {
channelId: string | undefined;
messageHasOnlyOneImage: boolean;
};

const IMAGE_LOADING_INDICATOR_DELAY_MS = 120;

const GalleryWithContext = (props: GalleryPropsWithContext) => {
const {
additionalPressableProps,
alignment,
imageGalleryStateStore,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
ImageReloadIndicator,
images,
message,
onLongPress,
Expand Down Expand Up @@ -148,8 +156,8 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
images.length !== 1
? { width: gridWidth, height: gridHeight }
: {
height,
width,
minHeight: height,
minWidth: width,
},
galleryContainer,
]}
Expand Down Expand Up @@ -194,7 +202,6 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
imageGalleryStateStore={imageGalleryStateStore}
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
ImageLoadingIndicator={ImageLoadingIndicator}
ImageReloadIndicator={ImageReloadIndicator}
imagesAndVideos={imagesAndVideos}
invertedDirections={invertedDirections || false}
key={rowIndex}
Expand Down Expand Up @@ -235,7 +242,6 @@ type GalleryThumbnailProps = {
| 'VideoThumbnail'
| 'ImageLoadingIndicator'
| 'ImageLoadingFailedIndicator'
| 'ImageReloadIndicator'
> &
Pick<ImageGalleryContextValue, 'imageGalleryStateStore'> &
Pick<MessageContextValue, 'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress'> &
Expand All @@ -248,7 +254,6 @@ const GalleryThumbnail = ({
imageGalleryStateStore,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
ImageReloadIndicator,
imagesAndVideos,
invertedDirections,
message,
Expand Down Expand Up @@ -352,7 +357,6 @@ const GalleryThumbnail = ({
borderRadius={imageBorderRadius ?? borderRadius}
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
ImageLoadingIndicator={ImageLoadingIndicator}
ImageReloadIndicator={ImageReloadIndicator}
thumbnail={thumbnail}
/>
)}
Expand All @@ -379,15 +383,10 @@ const GalleryImageThumbnail = ({
borderRadius,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
ImageReloadIndicator,
thumbnail,
}: Pick<
GalleryThumbnailProps,
| 'ImageLoadingFailedIndicator'
| 'ImageLoadingIndicator'
| 'ImageReloadIndicator'
| 'thumbnail'
| 'borderRadius'
'ImageLoadingFailedIndicator' | 'ImageLoadingIndicator' | 'thumbnail' | 'borderRadius'
>) => {
const {
isLoadingImage,
Expand All @@ -396,6 +395,7 @@ const GalleryImageThumbnail = ({
setLoadingImage,
setLoadingImageError,
} = useLoadingImage();
const loadingIndicatorTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const {
theme: {
Expand All @@ -405,35 +405,54 @@ const GalleryImageThumbnail = ({

const styles = useStyles();

const clearLoadingIndicatorTimer = useStableCallback(() => {
if (loadingIndicatorTimerRef.current) {
clearTimeout(loadingIndicatorTimerRef.current);
loadingIndicatorTimerRef.current = null;
}
});

const onLoadStart = useStableCallback(() => {
clearLoadingIndicatorTimer();
setLoadingImageError(false);
loadingIndicatorTimerRef.current = setTimeout(() => {
setLoadingImage(true);
loadingIndicatorTimerRef.current = null;
}, IMAGE_LOADING_INDICATOR_DELAY_MS);
});

const onLoadEnd = useStableCallback(() => {
clearLoadingIndicatorTimer();
setLoadingImage(false);
setLoadingImageError(false);
});

const onError = useStableCallback(
({ nativeEvent: { error } }: NativeSyntheticEvent<ImageErrorEventData>) => {
clearLoadingIndicatorTimer();
console.warn(error);
setLoadingImage(false);
setLoadingImageError(true);
},
);

useEffect(() => clearLoadingIndicatorTimer, [clearLoadingIndicatorTimer]);

return (
<View style={styles.image}>
<View style={[styles.image, borderRadius]}>
{isLoadingImageError ? (
<>
<ImageLoadingFailedIndicator style={styles.imageLoadingErrorIndicatorStyle} />
<ImageReloadIndicator
onReloadImage={onReloadImage}
style={styles.imageReloadContainerStyle}
/>
</>
<ImageLoadingFailedIndicator onReloadImage={onReloadImage} />
) : (
<>
<GalleryImage
onError={({ nativeEvent: { error } }) => {
console.warn(error);
setLoadingImage(false);
setLoadingImageError(true);
}}
onLoadEnd={() => setTimeout(() => setLoadingImage(false), 0)}
onLoadStart={() => setLoadingImage(true)}
onError={onError}
onLoadEnd={onLoadEnd}
onLoadStart={onLoadStart}
resizeMode={thumbnail.resizeMode}
style={[borderRadius, gallery.image]}
style={gallery.image}
uri={thumbnail.url}
/>
{isLoadingImage && (
<View style={styles.imageLoadingIndicatorContainer}>
<ImageLoadingIndicator style={styles.imageLoadingIndicatorStyle} />
</View>
)}
{isLoadingImage ? <ImageLoadingIndicator /> : null}
</>
)}
</View>
Expand Down Expand Up @@ -512,7 +531,6 @@ export const Gallery = (props: GalleryProps) => {
additionalPressableProps: propAdditionalPressableProps,
ImageLoadingFailedIndicator: PropImageLoadingFailedIndicator,
ImageLoadingIndicator: PropImageLoadingIndicator,
ImageReloadIndicator: PropImageReloadIndicator,
images: propImages,
message: propMessage,
myMessageTheme: propMyMessageTheme,
Expand Down Expand Up @@ -542,7 +560,6 @@ export const Gallery = (props: GalleryProps) => {
additionalPressableProps: contextAdditionalPressableProps,
ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator,
ImageLoadingIndicator: ContextImageLoadingIndicator,
ImageReloadIndicator: ContextImageReloadIndicator,
myMessageTheme: contextMyMessageTheme,
VideoThumbnail: ContextVideoThumnbnail,
} = useMessagesContext();
Expand All @@ -567,7 +584,6 @@ export const Gallery = (props: GalleryProps) => {
const ImageLoadingFailedIndicator =
PropImageLoadingFailedIndicator || ContextImageLoadingFailedIndicator;
const ImageLoadingIndicator = PropImageLoadingIndicator || ContextImageLoadingIndicator;
const ImageReloadIndicator = PropImageReloadIndicator || ContextImageReloadIndicator;
const myMessageTheme = propMyMessageTheme || contextMyMessageTheme;
const messageContentOrder = propMessageContentOrder || contextMessageContentOrder;

Expand All @@ -585,7 +601,6 @@ export const Gallery = (props: GalleryProps) => {
imageGalleryStateStore,
ImageLoadingFailedIndicator,
ImageLoadingIndicator,
ImageReloadIndicator,
images,
message,
myMessageTheme,
Expand All @@ -607,6 +622,7 @@ const useStyles = () => {
const {
theme: { semantics },
} = useTheme();
const { isMyMessage } = useMessageContext();
return useMemo(() => {
return StyleSheet.create({
errorTextSize: {
Expand All @@ -626,11 +642,15 @@ const useStyles = () => {
imageContainer: {},
image: {
flex: 1,
backgroundColor: isMyMessage
? semantics.chatBgAttachmentOutgoing
: semantics.chatBgAttachmentIncoming,
overflow: 'hidden',
},
imageLoadingErrorIndicatorStyle: {
bottom: 4,
left: 4,
position: 'absolute',
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
},
imageLoadingIndicatorContainer: {
height: '100%',
Expand Down Expand Up @@ -658,8 +678,17 @@ const useStyles = () => {
lineHeight: primitives.typographyLineHeightRelaxed,
fontWeight: primitives.typographyFontWeightSemiBold,
},
imageLoadingErrorContainer: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
},
imageLoadingErrorWrapper: {
...StyleSheet.absoluteFillObject,
overflow: 'hidden',
},
});
}, [semantics]);
}, [semantics, isMyMessage]);
};

Gallery.displayName = 'Gallery{messageSimple{gallery}}';
11 changes: 8 additions & 3 deletions package/src/components/Attachment/Giphy/GiphyImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,13 @@ const GiphyImageWithContext = (props: GiphyImagePropsWithContext) => {

const { giphy: giphyData, image_url, thumb_url, type } = attachment;

const { isLoadingImage, isLoadingImageError, setLoadingImage, setLoadingImageError } =
useLoadingImage();
const {
isLoadingImage,
isLoadingImageError,
setLoadingImage,
setLoadingImageError,
onReloadImage,
} = useLoadingImage();

const {
theme: {
Expand Down Expand Up @@ -93,7 +98,7 @@ const GiphyImageWithContext = (props: GiphyImagePropsWithContext) => {
/>
{isLoadingImageError && (
<View style={[styles.imageIndicatorContainer, imageIndicatorContainer]}>
<ImageLoadingFailedIndicator />
<ImageLoadingFailedIndicator onReloadImage={onReloadImage} />
</View>
)}
{isLoadingImage && (
Expand Down
85 changes: 37 additions & 48 deletions package/src/components/Attachment/ImageLoadingFailedIndicator.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,45 @@
import React from 'react';
import { StyleSheet, Text, View, ViewProps } from 'react-native';
import React, { useMemo } from 'react';
import { Pressable, StyleSheet, ViewProps } from 'react-native';

import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { useTranslationContext } from '../../contexts/translationContext/TranslationContext';

import { Warning } from '../../icons/Warning';

const WARNING_ICON_SIZE = 14;
import { RetryBadge } from '../ui/Badge/RetryBadge';

export type ImageLoadingFailedIndicatorProps = ViewProps & {
/**
* Callback to reload the image
* @returns Callback to reload the image
*/
onReloadImage: () => void;
};

const styles = StyleSheet.create({
container: {
alignContent: 'center',
alignItems: 'center',
borderRadius: 20,
flexDirection: 'row',
justifyContent: 'center',
},
errorText: {
fontSize: 8,
justifyContent: 'center',
paddingHorizontal: 8,
},
warningIconStyle: {
borderRadius: 24,
marginLeft: 4,
marginTop: 6,
},
});
export const ImageLoadingFailedIndicator = ({
onReloadImage,
}: ImageLoadingFailedIndicatorProps) => {
const styles = useStyles();

export type ImageLoadingFailedIndicatorProps = ViewProps;
return (
<Pressable
accessibilityLabel='Image Loading Error Indicator'
onPress={onReloadImage}
style={styles.imageLoadingErrorIndicatorStyle}
>
<RetryBadge size='lg' />
</Pressable>
);
};

export const ImageLoadingFailedIndicator = (props: ImageLoadingFailedIndicatorProps) => {
const useStyles = () => {
const {
theme: {
colors: { accent_red, overlay, white },
},
theme: { semantics },
} = useTheme();

const { t } = useTranslationContext();

const { style, ...rest } = props;
return (
<View {...rest} accessibilityHint='image-loading-error' style={[style]}>
<View style={[styles.container, { backgroundColor: overlay }]}>
<Warning
height={WARNING_ICON_SIZE}
fill={accent_red}
style={styles.warningIconStyle}
width={WARNING_ICON_SIZE}
/>
<Text style={[styles.errorText, { color: white }]}>{t('Error loading')}</Text>
</View>
</View>
);
return useMemo(() => {
return StyleSheet.create({
imageLoadingErrorIndicatorStyle: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: semantics.backgroundCoreOverlayLight,
},
});
}, [semantics]);
};
Loading
Loading