Skip to content

Commit 30ed9b8

Browse files
committed
feat: track attachment uploads outside of message composer - in progress
1 parent dd94992 commit 30ed9b8

File tree

17 files changed

+241
-75
lines changed

17 files changed

+241
-75
lines changed

package/src/components/Attachment/FileAttachment.tsx

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useMemo } from 'react';
2-
import { Pressable, StyleProp, StyleSheet, TextStyle, ViewStyle } from 'react-native';
2+
import { Pressable, StyleProp, StyleSheet, TextStyle, View, ViewStyle } from 'react-native';
33

44
import type { Attachment } from 'stream-chat';
55

@@ -16,12 +16,17 @@ import {
1616
useMessagesContext,
1717
} from '../../contexts/messagesContext/MessagesContext';
1818
import { useTheme } from '../../contexts/themeContext/ThemeContext';
19+
import { useIsPendingAttachmentUploading } from '../../hooks/useIsPendingAttachmentUploading';
20+
import type { DefaultAttachmentData } from '../../types/types';
1921

2022
export type FileAttachmentPropsWithContext = Pick<
2123
MessageContextValue,
2224
'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress'
2325
> &
24-
Pick<MessagesContextValue, 'additionalPressableProps' | 'FilePreview'> & {
26+
Pick<
27+
MessagesContextValue,
28+
'additionalPressableProps' | 'FilePreview' | 'ImageUploadingIndicator'
29+
> & {
2530
/** The attachment to render */
2631
attachment: Attachment;
2732
attachmentIconSize?: FileIconProps['size'];
@@ -42,13 +47,17 @@ const FileAttachmentWithContext = (props: FileAttachmentPropsWithContext) => {
4247
attachment,
4348
attachmentIconSize,
4449
FilePreview,
50+
ImageUploadingIndicator,
4551
onLongPress,
4652
onPress,
4753
onPressIn,
4854
preventPress,
4955
styles: stylesProp = styles,
5056
} = props;
5157

58+
const localId = (attachment as DefaultAttachmentData).localId;
59+
const isPendingAttachmentUploading = useIsPendingAttachmentUploading(localId);
60+
5261
const defaultOnPress = () => openUrlSafely(attachment.asset_url);
5362

5463
return (
@@ -86,11 +95,14 @@ const FileAttachmentWithContext = (props: FileAttachmentPropsWithContext) => {
8695
testID='file-attachment'
8796
{...additionalPressableProps}
8897
>
89-
<FilePreview
90-
attachment={attachment}
91-
attachmentIconSize={attachmentIconSize}
92-
styles={stylesProp}
93-
/>
98+
<View style={styles.previewWrap}>
99+
<FilePreview
100+
attachment={attachment}
101+
attachmentIconSize={attachmentIconSize}
102+
styles={stylesProp}
103+
/>
104+
{isPendingAttachmentUploading ? <ImageUploadingIndicator /> : null}
105+
</View>
94106
</Pressable>
95107
);
96108
};
@@ -101,15 +113,21 @@ export type FileAttachmentProps = Partial<Omit<FileAttachmentPropsWithContext, '
101113
export const FileAttachment = (props: FileAttachmentProps) => {
102114
const { FilePreview: PropFilePreview } = props;
103115
const { onLongPress, onPress, onPressIn, preventPress } = useMessageContext();
104-
const { additionalPressableProps, FilePreview: ContextFilePreview } = useMessagesContext();
116+
const {
117+
additionalPressableProps,
118+
FilePreview: ContextFilePreview,
119+
ImageUploadingIndicator: ContextImageUploadingIndicator,
120+
} = useMessagesContext();
105121

106122
const FilePreview = PropFilePreview || ContextFilePreview;
123+
const ImageUploadingIndicator = props.ImageUploadingIndicator || ContextImageUploadingIndicator;
107124

108125
return (
109126
<FileAttachmentWithContext
110127
{...{
111128
additionalPressableProps,
112129
FilePreview,
130+
ImageUploadingIndicator,
113131
onLongPress,
114132
onPress,
115133
onPressIn,
@@ -138,6 +156,9 @@ const useStyles = () => {
138156
? semantics.chatBgAttachmentOutgoing
139157
: semantics.chatBgAttachmentIncoming,
140158
},
159+
previewWrap: {
160+
position: 'relative',
161+
},
141162
});
142163
}, [showBackgroundTransparent, isMyMessage, semantics]);
143164
};

package/src/components/Attachment/Gallery.tsx

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
} from '../../contexts/overlayContext/OverlayContext';
3535
import { useTheme } from '../../contexts/themeContext/ThemeContext';
3636

37+
import { useIsPendingAttachmentUploading } from '../../hooks/useIsPendingAttachmentUploading';
3738
import { useLoadingImage } from '../../hooks/useLoadingImage';
3839
import { useStableCallback } from '../../hooks/useStableCallback';
3940
import { isVideoPlayerAvailable } from '../../native';
@@ -60,6 +61,7 @@ export type GalleryPropsWithContext = Pick<ImageGalleryContextValue, 'imageGalle
6061
| 'VideoThumbnail'
6162
| 'ImageLoadingIndicator'
6263
| 'ImageLoadingFailedIndicator'
64+
| 'ImageUploadingIndicator'
6365
| 'myMessageTheme'
6466
> &
6567
Pick<OverlayContextValue, 'setOverlay'> & {
@@ -74,6 +76,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
7476
imageGalleryStateStore,
7577
ImageLoadingFailedIndicator,
7678
ImageLoadingIndicator,
79+
ImageUploadingIndicator,
7780
images,
7881
message,
7982
onLongPress,
@@ -193,6 +196,7 @@ const GalleryWithContext = (props: GalleryPropsWithContext) => {
193196
imageGalleryStateStore={imageGalleryStateStore}
194197
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
195198
ImageLoadingIndicator={ImageLoadingIndicator}
199+
ImageUploadingIndicator={ImageUploadingIndicator}
196200
imagesAndVideos={imagesAndVideos}
197201
invertedDirections={invertedDirections || false}
198202
key={rowIndex}
@@ -233,6 +237,7 @@ type GalleryThumbnailProps = {
233237
| 'VideoThumbnail'
234238
| 'ImageLoadingIndicator'
235239
| 'ImageLoadingFailedIndicator'
240+
| 'ImageUploadingIndicator'
236241
> &
237242
Pick<ImageGalleryContextValue, 'imageGalleryStateStore'> &
238243
Pick<MessageContextValue, 'onLongPress' | 'onPress' | 'onPressIn' | 'preventPress'> &
@@ -245,6 +250,7 @@ const GalleryThumbnail = ({
245250
imageGalleryStateStore,
246251
ImageLoadingFailedIndicator,
247252
ImageLoadingIndicator,
253+
ImageUploadingIndicator,
248254
imagesAndVideos,
249255
invertedDirections,
250256
message,
@@ -269,6 +275,7 @@ const GalleryThumbnail = ({
269275
} = useTheme();
270276
const { t } = useTranslationContext();
271277
const styles = useStyles();
278+
const isPendingAttachmentUploading = useIsPendingAttachmentUploading(thumbnail.localId);
272279

273280
const openImageViewer = () => {
274281
if (!message) {
@@ -333,6 +340,7 @@ const GalleryThumbnail = ({
333340
>
334341
{thumbnail.type === FileTypes.Video ? (
335342
<VideoThumbnail
343+
isPendingAttachmentUploading={isPendingAttachmentUploading}
336344
style={[styles.image, imageBorderRadius ?? borderRadius, image]}
337345
thumb_url={thumbnail.thumb_url}
338346
/>
@@ -341,6 +349,8 @@ const GalleryThumbnail = ({
341349
borderRadius={imageBorderRadius ?? borderRadius}
342350
ImageLoadingFailedIndicator={ImageLoadingFailedIndicator}
343351
ImageLoadingIndicator={ImageLoadingIndicator}
352+
ImageUploadingIndicator={ImageUploadingIndicator}
353+
isPendingAttachmentUploading={isPendingAttachmentUploading}
344354
thumbnail={thumbnail}
345355
/>
346356
)}
@@ -367,11 +377,19 @@ const GalleryImageThumbnail = ({
367377
borderRadius,
368378
ImageLoadingFailedIndicator,
369379
ImageLoadingIndicator,
380+
ImageUploadingIndicator,
381+
isPendingAttachmentUploading,
370382
thumbnail,
371383
}: Pick<
372384
GalleryThumbnailProps,
373-
'ImageLoadingFailedIndicator' | 'ImageLoadingIndicator' | 'thumbnail' | 'borderRadius'
374-
>) => {
385+
| 'ImageLoadingFailedIndicator'
386+
| 'ImageLoadingIndicator'
387+
| 'ImageUploadingIndicator'
388+
| 'thumbnail'
389+
| 'borderRadius'
390+
> & {
391+
isPendingAttachmentUploading: boolean;
392+
}) => {
375393
const {
376394
isLoadingImage,
377395
isLoadingImageError,
@@ -421,6 +439,7 @@ const GalleryImageThumbnail = ({
421439
uri={thumbnail.url}
422440
/>
423441
{isLoadingImage ? <ImageLoadingIndicator /> : null}
442+
{isPendingAttachmentUploading ? <ImageUploadingIndicator /> : null}
424443
</>
425444
)}
426445
</View>
@@ -499,6 +518,7 @@ export const Gallery = (props: GalleryProps) => {
499518
additionalPressableProps: propAdditionalPressableProps,
500519
ImageLoadingFailedIndicator: PropImageLoadingFailedIndicator,
501520
ImageLoadingIndicator: PropImageLoadingIndicator,
521+
ImageUploadingIndicator: PropImageUploadingIndicator,
502522
images: propImages,
503523
message: propMessage,
504524
myMessageTheme: propMyMessageTheme,
@@ -528,6 +548,7 @@ export const Gallery = (props: GalleryProps) => {
528548
additionalPressableProps: contextAdditionalPressableProps,
529549
ImageLoadingFailedIndicator: ContextImageLoadingFailedIndicator,
530550
ImageLoadingIndicator: ContextImageLoadingIndicator,
551+
ImageUploadingIndicator: ContextImageUploadingIndicator,
531552
myMessageTheme: contextMyMessageTheme,
532553
VideoThumbnail: ContextVideoThumnbnail,
533554
} = useMessagesContext();
@@ -553,6 +574,7 @@ export const Gallery = (props: GalleryProps) => {
553574
const ImageLoadingFailedIndicator =
554575
PropImageLoadingFailedIndicator || ContextImageLoadingFailedIndicator;
555576
const ImageLoadingIndicator = PropImageLoadingIndicator || ContextImageLoadingIndicator;
577+
const ImageUploadingIndicator = PropImageUploadingIndicator || ContextImageUploadingIndicator;
556578
const myMessageTheme = propMyMessageTheme || contextMyMessageTheme;
557579
const messageContentOrder = propMessageContentOrder || contextMessageContentOrder;
558580

@@ -570,6 +592,7 @@ export const Gallery = (props: GalleryProps) => {
570592
imageGalleryStateStore,
571593
ImageLoadingFailedIndicator,
572594
ImageLoadingIndicator,
595+
ImageUploadingIndicator,
573596
images,
574597
message,
575598
myMessageTheme,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
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 = ({ style, ...rest }: ImageUploadingIndicatorProps) => {
9+
const {
10+
theme: { semantics },
11+
} = useTheme();
12+
return (
13+
<View
14+
pointerEvents='none'
15+
style={[StyleSheet.absoluteFillObject, styles.centered, style]}
16+
{...rest}
17+
>
18+
<ActivityIndicator color={semantics.accentPrimary} />
19+
</View>
20+
);
21+
};
22+
23+
const styles = StyleSheet.create({
24+
centered: {
25+
alignItems: 'center',
26+
justifyContent: 'center',
27+
},
28+
});

package/src/components/Attachment/VideoThumbnail.tsx

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,28 @@
11
import React from 'react';
2-
import { ImageBackground, ImageStyle, StyleProp, StyleSheet, ViewStyle } from 'react-native';
2+
import {
3+
ActivityIndicator,
4+
ImageBackground,
5+
ImageStyle,
6+
StyleProp,
7+
StyleSheet,
8+
View,
9+
ViewStyle,
10+
} from 'react-native';
311

412
import { useTheme } from '../../contexts/themeContext/ThemeContext';
13+
import { primitives } from '../../theme';
514
import { VideoPlayIndicator } from '../ui/VideoPlayIndicator';
615

716
const styles = StyleSheet.create({
17+
activityIndicator: {
18+
alignItems: 'flex-start',
19+
justifyContent: 'flex-start',
20+
},
21+
activityIndicatorContainer: {
22+
bottom: primitives.spacingXs,
23+
left: primitives.spacingXs,
24+
position: 'absolute',
25+
},
826
container: {
927
alignItems: 'center',
1028
justifyContent: 'center',
@@ -15,6 +33,8 @@ const styles = StyleSheet.create({
1533

1634
export type VideoThumbnailProps = {
1735
imageStyle?: StyleProp<ImageStyle>;
36+
/** When true, shows upload progress over the thumbnail */
37+
isPendingAttachmentUploading?: boolean;
1838
style?: StyleProp<ViewStyle>;
1939
thumb_url?: string;
2040
};
@@ -25,9 +45,10 @@ export const VideoThumbnail = (props: VideoThumbnailProps) => {
2545
messageItemView: {
2646
videoThumbnail: { container },
2747
},
48+
semantics,
2849
},
2950
} = useTheme();
30-
const { imageStyle, style, thumb_url } = props;
51+
const { imageStyle, isPendingAttachmentUploading, style, thumb_url } = props;
3152
return (
3253
<ImageBackground
3354
accessibilityLabel='Video Thumbnail'
@@ -36,6 +57,11 @@ export const VideoThumbnail = (props: VideoThumbnailProps) => {
3657
style={[styles.container, container, style]}
3758
>
3859
<VideoPlayIndicator size='md' />
60+
{isPendingAttachmentUploading ? (
61+
<View style={[styles.activityIndicatorContainer, styles.activityIndicator]}>
62+
<ActivityIndicator color={semantics.accentPrimary} />
63+
</View>
64+
) : null}
3965
</ImageBackground>
4066
);
4167
};

package/src/components/Attachment/__tests__/Attachment.test.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { generateMessage } from '../../../mock-builders/generator/message';
1616

1717
import { ImageLoadingFailedIndicator } from '../../Attachment/ImageLoadingFailedIndicator';
1818
import { ImageLoadingIndicator } from '../../Attachment/ImageLoadingIndicator';
19+
import { ImageUploadingIndicator } from '../../Attachment/ImageUploadingIndicator';
1920
import { Attachment } from '../Attachment';
2021
import { FilePreview as FilePreviewDefault } from '../FilePreview';
2122

@@ -24,15 +25,20 @@ jest.mock('../../../native.ts', () => ({
2425
isSoundPackageAvailable: jest.fn(() => false),
2526
}));
2627

28+
jest.mock('../../../hooks/useIsPendingAttachmentUploading', () => ({
29+
useIsPendingAttachmentUploading: jest.fn(() => false),
30+
}));
31+
2732
const getAttachmentComponent = (props) => {
2833
const message = generateMessage();
2934
return (
3035
<ThemeProvider>
3136
<MessagesProvider
3237
value={{
38+
FilePreview: FilePreviewDefault,
3339
ImageLoadingFailedIndicator,
3440
ImageLoadingIndicator,
35-
FilePreview: FilePreviewDefault,
41+
ImageUploadingIndicator,
3642
}}
3743
>
3844
<MessageProvider value={{ message }}>

package/src/components/Attachment/__tests__/Giphy.test.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { getTestClientWithUser } from '../../../mock-builders/mock';
2626
import { Streami18n } from '../../../utils/i18n/Streami18n';
2727
import { ImageLoadingFailedIndicator } from '../../Attachment/ImageLoadingFailedIndicator';
2828
import { ImageLoadingIndicator } from '../../Attachment/ImageLoadingIndicator';
29+
import { ImageUploadingIndicator } from '../../Attachment/ImageUploadingIndicator';
2930
import { Channel } from '../../Channel/Channel';
3031
import { Chat } from '../../Chat/Chat';
3132
import { MessageList } from '../../MessageList/MessageList';
@@ -40,7 +41,13 @@ describe('Giphy', () => {
4041
const message = generateMessage();
4142
return (
4243
<ThemeProvider>
43-
<MessagesProvider value={{ ImageLoadingFailedIndicator, ImageLoadingIndicator }}>
44+
<MessagesProvider
45+
value={{
46+
ImageLoadingFailedIndicator,
47+
ImageLoadingIndicator,
48+
ImageUploadingIndicator,
49+
}}
50+
>
4451
<MessageProvider value={{ message, ...messageContextValue }}>
4552
<Giphy {...props} />
4653
</MessageProvider>

package/src/components/Attachment/utils/buildGallery/buildThumbnail.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Attachment } from 'stream-chat';
55
import type { Thumbnail } from './types';
66

77
import { ChatConfigContextValue } from '../../../../contexts/chatConfigContext/ChatConfigContext';
8+
import type { DefaultAttachmentData } from '../../../../types/types';
89

910
import { getResizedImageUrl } from '../../../../utils/getResizedImageUrl';
1011
import { getUrlOfImageAttachment } from '../../../../utils/getUrlOfImageAttachment';
@@ -33,9 +34,11 @@ export function buildThumbnail({
3334
? originalImageHeight + originalImageWidth > height + width
3435
: true;
3536
const imageUrl = getUrlOfImageAttachment(image) as string;
37+
const localId = (image as Attachment & DefaultAttachmentData).localId;
3638

3739
return {
3840
flex,
41+
localId,
3942
resizeMode: resizeMode
4043
? resizeMode
4144
: ((image.original_height && image.original_width ? 'contain' : 'cover') as ImageResizeMode),

0 commit comments

Comments
 (0)