Skip to content

Commit cf50c26

Browse files
committed
feat: add attachments uploading state when the attachments are uploaded in a pending state
1 parent 543f74d commit cf50c26

File tree

8 files changed

+302
-66
lines changed

8 files changed

+302
-66
lines changed

package/src/components/Attachment/FileAttachment.tsx

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
1-
import React, { useMemo } from 'react';
2-
import { Pressable, StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native';
1+
import React, { useCallback, useMemo } from 'react';
2+
import {
3+
ActivityIndicator,
4+
Pressable,
5+
StyleProp,
6+
StyleSheet,
7+
TextStyle,
8+
ViewStyle,
9+
} from 'react-native';
310

411
import type { Attachment } from 'stream-chat';
512

@@ -16,6 +23,8 @@ import {
1623
useMessagesContext,
1724
} from '../../contexts/messagesContext/MessagesContext';
1825
import { useTheme } from '../../contexts/themeContext/ThemeContext';
26+
import { useStateStore } from '../../hooks/useStateStore';
27+
import type { PendingAttachmentsLoadingState } from '../../state-store/pending-attachments-loading-state';
1928

2029
export type FileAttachmentPropsWithContext = Pick<
2130
MessageContextValue,
@@ -32,10 +41,18 @@ export type FileAttachmentPropsWithContext = Pick<
3241
size: StyleProp<TextStyle>;
3342
title: StyleProp<TextStyle>;
3443
}>;
44+
/**
45+
* Whether the attachment is currently being uploaded.
46+
* This is used to show a loading indicator in the file attachment.
47+
*/
48+
isPendingAttachmentLoading: boolean;
3549
};
3650

3751
const FileAttachmentWithContext = (props: FileAttachmentPropsWithContext) => {
3852
const styles = useStyles();
53+
const {
54+
theme: { semantics },
55+
} = useTheme();
3956

4057
const {
4158
additionalPressableProps,
@@ -46,10 +63,18 @@ const FileAttachmentWithContext = (props: FileAttachmentPropsWithContext) => {
4663
onPressIn,
4764
preventPress,
4865
styles: stylesProp = styles,
66+
isPendingAttachmentLoading,
4967
} = props;
5068

5169
const defaultOnPress = () => openUrlSafely(attachment.asset_url);
5270

71+
const renderIndicator = useMemo(() => {
72+
if (isPendingAttachmentLoading) {
73+
return <ActivityIndicator color={semantics.accentPrimary} style={styles.activityIndicator} />;
74+
}
75+
return null;
76+
}, [isPendingAttachmentLoading, semantics.accentPrimary, styles.activityIndicator]);
77+
5378
return (
5479
<Pressable
5580
disabled={preventPress}
@@ -88,6 +113,7 @@ const FileAttachmentWithContext = (props: FileAttachmentPropsWithContext) => {
88113
<FilePreview
89114
attachment={attachment}
90115
attachmentIconSize={attachmentIconSize}
116+
indicator={renderIndicator}
91117
styles={stylesProp}
92118
/>
93119
</Pressable>
@@ -98,8 +124,25 @@ export type FileAttachmentProps = Partial<Omit<FileAttachmentPropsWithContext, '
98124
Pick<FileAttachmentPropsWithContext, 'attachment'>;
99125

100126
export const FileAttachment = (props: FileAttachmentProps) => {
101-
const { onLongPress, onPress, onPressIn, preventPress } = useMessageContext();
102-
const { additionalPressableProps, FileAttachmentIcon = FileIconDefault } = useMessagesContext();
127+
const { attachment } = props;
128+
const { onLongPress, onPress, onPressIn, preventPress, message } = useMessageContext();
129+
const {
130+
additionalPressableProps,
131+
FileAttachmentIcon = FileIconDefault,
132+
pendingAttachmentsLoadingStore,
133+
} = useMessagesContext();
134+
135+
const attachmentId = `${message.id}-${attachment.originalFile?.uri}`;
136+
const selector = useCallback(
137+
(state: PendingAttachmentsLoadingState) => ({
138+
isPendingAttachmentLoading: state.pendingAttachmentsLoading[attachmentId] ?? false,
139+
}),
140+
[attachmentId],
141+
);
142+
const { isPendingAttachmentLoading } = useStateStore(
143+
pendingAttachmentsLoadingStore.store,
144+
selector,
145+
) ?? { isPendingAttachmentLoading: false };
103146

104147
return (
105148
<FileAttachmentWithContext
@@ -110,6 +153,7 @@ export const FileAttachment = (props: FileAttachmentProps) => {
110153
onPress,
111154
onPressIn,
112155
preventPress,
156+
isPendingAttachmentLoading,
113157
}}
114158
{...props}
115159
/>
@@ -134,6 +178,10 @@ const useStyles = () => {
134178
? semantics.chatBgAttachmentOutgoing
135179
: semantics.chatBgAttachmentIncoming,
136180
},
181+
activityIndicator: {
182+
alignItems: 'flex-start',
183+
justifyContent: 'flex-start',
184+
},
137185
});
138186
}, [showBackgroundTransparent, isMyMessage, semantics]);
139187
};

package/src/components/Attachment/Gallery.tsx

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo } from 'react';
1+
import React, { useCallback, useMemo } from 'react';
22
import { Pressable, StyleSheet, Text, View } from 'react-native';
33

44
import type { Attachment, LocalMessage } from 'stream-chat';
@@ -34,8 +34,10 @@ import {
3434
} from '../../contexts/overlayContext/OverlayContext';
3535
import { useTheme } from '../../contexts/themeContext/ThemeContext';
3636

37+
import { useStateStore } from '../../hooks';
3738
import { useLoadingImage } from '../../hooks/useLoadingImage';
3839
import { isVideoPlayerAvailable } from '../../native';
40+
import { PendingAttachmentsLoadingState } from '../../state-store/pending-attachments-loading-state';
3941
import { primitives } from '../../theme';
4042
import { FileTypes } from '../../types/types';
4143
import { getUrlWithoutParams } from '../../utils/utils';
@@ -60,7 +62,9 @@ export type GalleryPropsWithContext = Pick<ImageGalleryContextValue, 'imageGalle
6062
| 'ImageLoadingIndicator'
6163
| 'ImageLoadingFailedIndicator'
6264
| 'ImageReloadIndicator'
65+
| 'ImageUploadingIndicator'
6366
| 'myMessageTheme'
67+
| 'pendingAttachmentsLoadingStore'
6468
> &
6569
Pick<OverlayContextValue, 'setOverlay'> & {
6670
channelId: string | undefined;
@@ -75,6 +79,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
7579
ImageLoadingFailedIndicator,
7680
ImageLoadingIndicator,
7781
ImageReloadIndicator,
82+
ImageUploadingIndicator,
7883
images,
7984
message,
8085
onLongPress,
@@ -85,6 +90,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
8590
videos,
8691
VideoThumbnail,
8792
messageHasOnlyOneImage = false,
93+
pendingAttachmentsLoadingStore,
8894
} = props;
8995

9096
const { resizableCDNHosts } = useChatConfigContext();
@@ -195,6 +201,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
195201
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
196202
ImageLoadingIndicator={ImageLoadingIndicator}
197203
ImageReloadIndicator={ImageReloadIndicator}
204+
ImageUploadingIndicator={ImageUploadingIndicator}
198205
imagesAndVideos={imagesAndVideos}
199206
invertedDirections={invertedDirections || false}
200207
key={rowIndex}
@@ -209,6 +216,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
209216
setOverlay={setOverlay}
210217
thumbnail={thumbnail}
211218
VideoThumbnail={VideoThumbnail}
219+
pendingAttachmentsLoadingStore={pendingAttachmentsLoadingStore}
212220
/>
213221
);
214222
})}
@@ -236,6 +244,8 @@ type GalleryThumbnailProps = {
236244
| 'ImageLoadingIndicator'
237245
| 'ImageLoadingFailedIndicator'
238246
| 'ImageReloadIndicator'
247+
| 'ImageUploadingIndicator'
248+
| 'pendingAttachmentsLoadingStore'
239249
> &
240250
Pick<ImageGalleryContextValue, 'imageGalleryStateStore'> &
241251
Pick<MessageContextValue, 'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress'> &
@@ -249,6 +259,7 @@ const GalleryThumbnail = ({
249259
ImageLoadingFailedIndicator,
250260
ImageLoadingIndicator,
251261
ImageReloadIndicator,
262+
ImageUploadingIndicator,
252263
imagesAndVideos,
253264
invertedDirections,
254265
message,
@@ -262,6 +273,7 @@ const GalleryThumbnail = ({
262273
setOverlay,
263274
thumbnail,
264275
VideoThumbnail,
276+
pendingAttachmentsLoadingStore,
265277
}: GalleryThumbnailProps) => {
266278
const {
267279
theme: {
@@ -274,6 +286,18 @@ const GalleryThumbnail = ({
274286
const { t } = useTranslationContext();
275287
const styles = useStyles();
276288

289+
const attachmentId = `${message.id}-${thumbnail.url}`;
290+
const selector = useCallback(
291+
(state: PendingAttachmentsLoadingState) => ({
292+
isPendingAttachmentLoading: state.pendingAttachmentsLoading[attachmentId] ?? false,
293+
}),
294+
[attachmentId],
295+
);
296+
const { isPendingAttachmentLoading } = useStateStore(
297+
pendingAttachmentsLoadingStore.store,
298+
selector,
299+
) ?? { isPendingAttachmentLoading: false };
300+
277301
const openImageViewer = () => {
278302
if (!message) {
279303
return;
@@ -346,14 +370,17 @@ const GalleryThumbnail = ({
346370
<VideoThumbnail
347371
style={[styles.image, imageBorderRadius ?? borderRadius, image]}
348372
thumb_url={thumbnail.thumb_url}
373+
isPendingAttachmentLoading={isPendingAttachmentLoading}
349374
/>
350375
) : (
351376
<GalleryImageThumbnail
352377
borderRadius={imageBorderRadius ?? borderRadius}
353378
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
354379
ImageLoadingIndicator={ImageLoadingIndicator}
355380
ImageReloadIndicator={ImageReloadIndicator}
381+
ImageUploadingIndicator={ImageUploadingIndicator}
356382
thumbnail={thumbnail}
383+
isPendingAttachmentLoading={isPendingAttachmentLoading}
357384
/>
358385
)}
359386
{colIndex === numOfColumns - 1 && rowIndex === numOfRows - 1 && imagesAndVideos.length > 4 ? (
@@ -381,14 +408,23 @@ const GalleryImageThumbnail = ({
381408
ImageLoadingIndicator,
382409
ImageReloadIndicator,
383410
thumbnail,
411+
isPendingAttachmentLoading,
412+
ImageUploadingIndicator,
384413
}: Pick<
385414
GalleryThumbnailProps,
386415
| 'ImageLoadingFailedIndicator'
387416
| 'ImageLoadingIndicator'
388417
| 'ImageReloadIndicator'
418+
| 'ImageUploadingIndicator'
389419
| 'thumbnail'
390420
| 'borderRadius'
391-
>) => {
421+
> & {
422+
/**
423+
* Whether the attachment is currently being uploaded.
424+
* This is used to show a loading indicator in the thumbnail.
425+
*/
426+
isPendingAttachmentLoading: boolean;
427+
}) => {
392428
const {
393429
isLoadingImage,
394430
isLoadingImageError,
@@ -434,6 +470,11 @@ const GalleryImageThumbnail = ({
434470
<ImageLoadingIndicator style={styles.imageLoadingIndicatorStyle} />
435471
</View>
436472
)}
473+
{isPendingAttachmentLoading && (
474+
<View style={styles.imageLoadingIndicatorContainer}>
475+
<ImageUploadingIndicator style={styles.imageLoadingIndicatorStyle} />
476+
</View>
477+
)}
437478
</>
438479
)}
439480
</View>
@@ -513,6 +554,7 @@ export const Gallery = (props: GalleryProps) => {
513554
ImageLoadingFailedIndicator: PropImageLoadingFailedIndicator,
514555
ImageLoadingIndicator: PropImageLoadingIndicator,
515556
ImageReloadIndicator: PropImageReloadIndicator,
557+
ImageUploadingIndicator: PropImageUploadingIndicator,
516558
images: propImages,
517559
message: propMessage,
518560
myMessageTheme: propMyMessageTheme,
@@ -524,6 +566,7 @@ export const Gallery = (props: GalleryProps) => {
524566
videos: propVideos,
525567
VideoThumbnail: PropVideoThumbnail,
526568
messageContentOrder: propMessageContentOrder,
569+
pendingAttachmentsLoadingStore: propPendingAttachmentsLoadingStore,
527570
} = props;
528571

529572
const { imageGalleryStateStore } = useImageGalleryContext();
@@ -543,8 +586,10 @@ export const Gallery = (props: GalleryProps) => {
543586
ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator,
544587
ImageLoadingIndicator: ContextImageLoadingIndicator,
545588
ImageReloadIndicator: ContextImageReloadIndicator,
589+
ImageUploadingIndicator: ContextImageUploadingIndicator,
546590
myMessageTheme: contextMyMessageTheme,
547591
VideoThumbnail: ContextVideoThumnbnail,
592+
pendingAttachmentsLoadingStore: contextPendingAttachmentsLoadingStore,
548593
} = useMessagesContext();
549594
const { setOverlay: contextSetOverlay } = useOverlayContext();
550595

@@ -568,8 +613,11 @@ export const Gallery = (props: GalleryProps) => {
568613
PropImageLoadingFailedIndicator || ContextImageLoadingFailedIndicator;
569614
const ImageLoadingIndicator = PropImageLoadingIndicator || ContextImageLoadingIndicator;
570615
const ImageReloadIndicator = PropImageReloadIndicator || ContextImageReloadIndicator;
616+
const ImageUploadingIndicator = PropImageUploadingIndicator || ContextImageUploadingIndicator;
571617
const myMessageTheme = propMyMessageTheme || contextMyMessageTheme;
572618
const messageContentOrder = propMessageContentOrder || contextMessageContentOrder;
619+
const pendingAttachmentsLoadingStore =
620+
propPendingAttachmentsLoadingStore || contextPendingAttachmentsLoadingStore;
573621

574622
const messageHasOnlyOneImage =
575623
messageContentOrder?.length === 1 &&
@@ -586,6 +634,7 @@ export const Gallery = (props: GalleryProps) => {
586634
ImageLoadingFailedIndicator,
587635
ImageLoadingIndicator,
588636
ImageReloadIndicator,
637+
ImageUploadingIndicator,
589638
images,
590639
message,
591640
myMessageTheme,
@@ -598,6 +647,7 @@ export const Gallery = (props: GalleryProps) => {
598647
VideoThumbnail,
599648
messageHasOnlyOneImage,
600649
messageContentOrder,
650+
pendingAttachmentsLoadingStore,
601651
}}
602652
/>
603653
);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import React from 'react';
2+
import { ActivityIndicator, StyleSheet, View, ViewProps } from 'react-native';
3+
4+
import { useTheme } from '../../contexts/themeContext/ThemeContext';
5+
6+
export type ImageUploadingIndicatorProps = ViewProps;
7+
8+
export const ImageUploadingIndicator = (props: ImageUploadingIndicatorProps) => {
9+
const {
10+
theme: {
11+
messageSimple: {
12+
loadingIndicator: { container },
13+
},
14+
semantics,
15+
},
16+
} = useTheme();
17+
const { style, ...rest } = props;
18+
return (
19+
<View
20+
{...rest}
21+
accessibilityHint='image-uploading'
22+
style={[StyleSheet.absoluteFillObject, container, style]}
23+
>
24+
<ActivityIndicator color={semantics.accentPrimary} />
25+
</View>
26+
);
27+
};

0 commit comments

Comments
 (0)