Skip to content

Commit f4fd6a7

Browse files
authored
fix: restructure file and image upload preview to attachment upload preview list (#3133)
* feat: restructure images and files upload preview * feat: create useAudioPreviewManager * fix: paused bug when audio is uploadesd * fix: restructure useeffect
1 parent d680811 commit f4fd6a7

File tree

14 files changed

+613
-709
lines changed

14 files changed

+613
-709
lines changed

package/src/components/Channel/Channel.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ import { ReactionListBottom as ReactionListBottomDefault } from '../Message/Mess
161161
import { ReactionListTop as ReactionListTopDefault } from '../Message/MessageSimple/ReactionList/ReactionListTop';
162162
import { StreamingMessageView as DefaultStreamingMessageView } from '../Message/MessageSimple/StreamingMessageView';
163163
import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton';
164+
import { AttachmentUploadPreviewList as AttachmentUploadPreviewDefault } from '../MessageInput/AttachmentUploadPreviewList';
164165
import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton';
165166
import { AttachmentUploadProgressIndicator as AttachmentUploadProgressIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
166167
import { AudioAttachmentUploadPreview as AudioAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview';
@@ -176,8 +177,6 @@ import { CommandInput as CommandInputDefault } from '../MessageInput/components/
176177
import { InputEditingStateHeader as InputEditingStateHeaderDefault } from '../MessageInput/components/InputEditingStateHeader';
177178
import { InputReplyStateHeader as InputReplyStateHeaderDefault } from '../MessageInput/components/InputReplyStateHeader';
178179
import { CooldownTimer as CooldownTimerDefault } from '../MessageInput/CooldownTimer';
179-
import { FileUploadPreview as FileUploadPreviewDefault } from '../MessageInput/FileUploadPreview';
180-
import { ImageUploadPreview as ImageUploadPreviewDefault } from '../MessageInput/ImageUploadPreview';
181180
import { InputButtons as InputButtonsDefault } from '../MessageInput/InputButtons';
182181
import { MoreOptionsButton as MoreOptionsButtonDefault } from '../MessageInput/MoreOptionsButton';
183182
import { SendButton as SendButtonDefault } from '../MessageInput/SendButton';
@@ -526,6 +525,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
526525
AttachmentPickerError = DefaultAttachmentPickerError,
527526
AttachmentPickerErrorImage = DefaultAttachmentPickerErrorImage,
528527
AttachmentPickerIOSSelectMorePhotos = DefaultAttachmentPickerIOSSelectMorePhotos,
528+
AttachmentUploadPreviewList = AttachmentUploadPreviewDefault,
529529
ImageOverlaySelectedComponent = DefaultImageOverlaySelectedComponent,
530530
attachmentPickerErrorButtonText,
531531
attachmentPickerErrorText,
@@ -567,7 +567,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
567567
FileAttachmentUploadPreview = FileAttachmentUploadPreviewDefault,
568568
FileAttachmentGroup = FileAttachmentGroupDefault,
569569
FileAttachmentIcon = FileIconDefault,
570-
FileUploadPreview = FileUploadPreviewDefault,
571570
FlatList = NativeHandlers.FlatList,
572571
forceAlignMessages,
573572
Gallery = GalleryDefault,
@@ -599,7 +598,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
599598
ImageLoadingFailedIndicator = ImageLoadingFailedIndicatorDefault,
600599
ImageLoadingIndicator = ImageLoadingIndicatorDefault,
601600
ImageReloadIndicator = ImageReloadIndicatorDefault,
602-
ImageUploadPreview = ImageUploadPreviewDefault,
603601
initialScrollToFirstUnreadMessage = false,
604602
InlineDateSeparator = InlineDateSeparatorDefault,
605603
InlineUnreadIndicator = InlineUnreadIndicatorDefault,
@@ -1741,6 +1739,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
17411739
attachmentPickerBottomSheetHeight,
17421740
AttachmentPickerSelectionBar,
17431741
attachmentSelectionBarHeight,
1742+
AttachmentUploadPreviewList,
17441743
AttachmentUploadProgressIndicator,
17451744
AudioAttachmentUploadPreview,
17461745
AudioRecorder,
@@ -1764,15 +1763,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
17641763
editMessage,
17651764
FileAttachmentUploadPreview,
17661765
FileSelectorIcon,
1767-
FileUploadPreview,
17681766
handleAttachButtonPress,
17691767
hasCameraPicker,
17701768
hasCommands: hasCommands ?? !!clientChannelConfig?.commands?.length,
17711769
hasFilePicker,
17721770
hasImagePicker,
17731771
ImageAttachmentUploadPreview,
17741772
ImageSelectorIcon,
1775-
ImageUploadPreview,
17761773
Input,
17771774
InputButtons,
17781775
InputEditingStateHeader,

package/src/components/Channel/hooks/useCreateInputMessageInputContext.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const useCreateInputMessageInputContext = ({
1414
attachmentPickerBottomSheetHeight,
1515
AttachmentPickerSelectionBar,
1616
attachmentSelectionBarHeight,
17+
AttachmentUploadPreviewList,
1718
AttachmentUploadProgressIndicator,
1819
AudioAttachmentUploadPreview,
1920
AudioRecorder,
@@ -37,15 +38,13 @@ export const useCreateInputMessageInputContext = ({
3738
editMessage,
3839
FileAttachmentUploadPreview,
3940
FileSelectorIcon,
40-
FileUploadPreview,
4141
handleAttachButtonPress,
4242
hasCameraPicker,
4343
hasCommands,
4444
hasFilePicker,
4545
hasImagePicker,
4646
ImageAttachmentUploadPreview,
4747
ImageSelectorIcon,
48-
ImageUploadPreview,
4948
Input,
5049
InputButtons,
5150
InputEditingStateHeader,
@@ -81,6 +80,7 @@ export const useCreateInputMessageInputContext = ({
8180
attachmentPickerBottomSheetHeight,
8281
AttachmentPickerSelectionBar,
8382
attachmentSelectionBarHeight,
83+
AttachmentUploadPreviewList,
8484
AttachmentUploadProgressIndicator,
8585
AudioAttachmentUploadPreview,
8686
AudioRecorder,
@@ -103,15 +103,13 @@ export const useCreateInputMessageInputContext = ({
103103
editMessage,
104104
FileAttachmentUploadPreview,
105105
FileSelectorIcon,
106-
FileUploadPreview,
107106
handleAttachButtonPress,
108107
hasCameraPicker,
109108
hasCommands,
110109
hasFilePicker,
111110
hasImagePicker,
112111
ImageAttachmentUploadPreview,
113112
ImageSelectorIcon,
114-
ImageUploadPreview,
115113
Input,
116114
InputButtons,
117115
InputEditingStateHeader,
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2+
import { FlatList, LayoutChangeEvent, StyleSheet, View } from 'react-native';
3+
4+
import {
5+
isLocalAudioAttachment,
6+
isLocalFileAttachment,
7+
isLocalImageAttachment,
8+
isLocalVoiceRecordingAttachment,
9+
isVideoAttachment,
10+
LocalAttachment,
11+
LocalImageAttachment,
12+
} from 'stream-chat';
13+
14+
import { useAudioPreviewManager } from './hooks/useAudioPreviewManager';
15+
16+
import { useMessageComposer } from '../../contexts';
17+
import { useAttachmentManagerState } from '../../contexts/messageInputContext/hooks/useAttachmentManagerState';
18+
import {
19+
MessageInputContextValue,
20+
useMessageInputContext,
21+
} from '../../contexts/messageInputContext/MessageInputContext';
22+
import { useTheme } from '../../contexts/themeContext/ThemeContext';
23+
import { isSoundPackageAvailable } from '../../native';
24+
25+
const IMAGE_PREVIEW_SIZE = 100;
26+
const FILE_PREVIEW_HEIGHT = 60;
27+
28+
export type AttachmentUploadPreviewListPropsWithContext = Pick<
29+
MessageInputContextValue,
30+
| 'AudioAttachmentUploadPreview'
31+
| 'FileAttachmentUploadPreview'
32+
| 'ImageAttachmentUploadPreview'
33+
| 'VideoAttachmentUploadPreview'
34+
>;
35+
36+
/**
37+
* AttachmentUploadPreviewList
38+
* UI Component to preview the files set for upload
39+
*/
40+
const UnMemoizedAttachmentUploadListPreview = (
41+
props: AttachmentUploadPreviewListPropsWithContext,
42+
) => {
43+
const [flatListWidth, setFlatListWidth] = useState(0);
44+
const flatListRef = useRef<FlatList<LocalAttachment> | null>(null);
45+
const {
46+
AudioAttachmentUploadPreview,
47+
FileAttachmentUploadPreview,
48+
ImageAttachmentUploadPreview,
49+
VideoAttachmentUploadPreview,
50+
} = props;
51+
const { attachmentManager } = useMessageComposer();
52+
const { attachments } = useAttachmentManagerState();
53+
const {
54+
theme: {
55+
colors: { grey_whisper },
56+
messageInput: {
57+
attachmentSeparator,
58+
attachmentUploadPreviewList: { filesFlatList, imagesFlatList, wrapper },
59+
},
60+
},
61+
} = useTheme();
62+
63+
const imageUploads = attachments.filter((attachment) => isLocalImageAttachment(attachment));
64+
const fileUploads = useMemo(() => {
65+
return attachments.filter((attachment) => !isLocalImageAttachment(attachment));
66+
}, [attachments]);
67+
const audioUploads = useMemo(() => {
68+
return fileUploads.filter(
69+
(attachment) =>
70+
isLocalAudioAttachment(attachment) || isLocalVoiceRecordingAttachment(attachment),
71+
);
72+
}, [fileUploads]);
73+
74+
const { audioAttachmentsStateMap, onLoad, onProgress, onPlayPause } =
75+
useAudioPreviewManager(audioUploads);
76+
77+
const renderImageItem = useCallback(
78+
({ item }: { item: LocalImageAttachment }) => {
79+
return (
80+
<ImageAttachmentUploadPreview
81+
attachment={item}
82+
handleRetry={attachmentManager.uploadAttachment}
83+
removeAttachments={attachmentManager.removeAttachments}
84+
/>
85+
);
86+
},
87+
[
88+
ImageAttachmentUploadPreview,
89+
attachmentManager.removeAttachments,
90+
attachmentManager.uploadAttachment,
91+
],
92+
);
93+
94+
const renderFileItem = useCallback(
95+
({ item }: { item: LocalAttachment }) => {
96+
if (isLocalImageAttachment(item)) {
97+
// This is already handled in the `renderImageItem` above, so we return null here to avoid duplication.
98+
return null;
99+
} else if (isLocalVoiceRecordingAttachment(item)) {
100+
return (
101+
<AudioAttachmentUploadPreview
102+
attachment={item}
103+
audioAttachmentConfig={audioAttachmentsStateMap[item.localMetadata.id]}
104+
handleRetry={attachmentManager.uploadAttachment}
105+
onLoad={onLoad}
106+
onPlayPause={onPlayPause}
107+
onProgress={onProgress}
108+
removeAttachments={attachmentManager.removeAttachments}
109+
/>
110+
);
111+
} else if (isLocalAudioAttachment(item)) {
112+
if (isSoundPackageAvailable()) {
113+
return (
114+
<AudioAttachmentUploadPreview
115+
attachment={item}
116+
audioAttachmentConfig={audioAttachmentsStateMap[item.localMetadata.id]}
117+
handleRetry={attachmentManager.uploadAttachment}
118+
onLoad={onLoad}
119+
onPlayPause={onPlayPause}
120+
onProgress={onProgress}
121+
removeAttachments={attachmentManager.removeAttachments}
122+
/>
123+
);
124+
} else {
125+
return (
126+
<FileAttachmentUploadPreview
127+
attachment={item}
128+
flatListWidth={flatListWidth}
129+
handleRetry={attachmentManager.uploadAttachment}
130+
removeAttachments={attachmentManager.removeAttachments}
131+
/>
132+
);
133+
}
134+
} else if (isVideoAttachment(item)) {
135+
return (
136+
<VideoAttachmentUploadPreview
137+
attachment={item}
138+
flatListWidth={flatListWidth}
139+
handleRetry={attachmentManager.uploadAttachment}
140+
removeAttachments={attachmentManager.removeAttachments}
141+
/>
142+
);
143+
} else if (isLocalFileAttachment(item)) {
144+
return (
145+
<FileAttachmentUploadPreview
146+
attachment={item}
147+
flatListWidth={flatListWidth}
148+
handleRetry={attachmentManager.uploadAttachment}
149+
removeAttachments={attachmentManager.removeAttachments}
150+
/>
151+
);
152+
} else return null;
153+
},
154+
[
155+
AudioAttachmentUploadPreview,
156+
FileAttachmentUploadPreview,
157+
VideoAttachmentUploadPreview,
158+
attachmentManager.removeAttachments,
159+
attachmentManager.uploadAttachment,
160+
audioAttachmentsStateMap,
161+
flatListWidth,
162+
onLoad,
163+
onPlayPause,
164+
onProgress,
165+
],
166+
);
167+
168+
useEffect(() => {
169+
if (fileUploads.length && flatListRef.current) {
170+
setTimeout(() => flatListRef.current?.scrollToEnd(), 1);
171+
}
172+
}, [fileUploads.length]);
173+
174+
const onLayout = useCallback(
175+
(event: LayoutChangeEvent) => {
176+
if (flatListRef.current) {
177+
setFlatListWidth(event.nativeEvent.layout.width);
178+
}
179+
},
180+
[flatListRef],
181+
);
182+
183+
if (!attachments.length) {
184+
return null;
185+
}
186+
187+
return (
188+
<View style={[wrapper]}>
189+
{imageUploads.length ? (
190+
<FlatList
191+
data={imageUploads}
192+
getItemLayout={(_, index) => ({
193+
index,
194+
length: IMAGE_PREVIEW_SIZE + 8,
195+
offset: (IMAGE_PREVIEW_SIZE + 8) * index,
196+
})}
197+
horizontal
198+
keyExtractor={(item) => item.localMetadata.id}
199+
renderItem={renderImageItem}
200+
style={[styles.imagesFlatList, imagesFlatList]}
201+
/>
202+
) : null}
203+
{imageUploads.length && fileUploads.length ? (
204+
<View
205+
style={[
206+
styles.attachmentSeparator,
207+
{
208+
borderBottomColor: grey_whisper,
209+
},
210+
attachmentSeparator,
211+
]}
212+
/>
213+
) : null}
214+
{fileUploads.length ? (
215+
<FlatList
216+
data={fileUploads}
217+
getItemLayout={(_, index) => ({
218+
index,
219+
length: FILE_PREVIEW_HEIGHT + 8,
220+
offset: (FILE_PREVIEW_HEIGHT + 8) * index,
221+
})}
222+
keyExtractor={(item) => item.localMetadata.id}
223+
onLayout={onLayout}
224+
ref={flatListRef}
225+
renderItem={renderFileItem}
226+
style={[styles.filesFlatList, filesFlatList]}
227+
testID={'file-upload-preview'}
228+
/>
229+
) : null}
230+
</View>
231+
);
232+
};
233+
234+
export type AttachmentUploadPreviewListProps = Partial<AttachmentUploadPreviewListPropsWithContext>;
235+
236+
const MemoizedAttachmentUploadPreviewListWithContext = React.memo(
237+
UnMemoizedAttachmentUploadListPreview,
238+
);
239+
240+
/**
241+
* AttachmentUploadPreviewList
242+
* UI Component to preview the files set for upload
243+
*/
244+
export const AttachmentUploadPreviewList = (props: AttachmentUploadPreviewListProps) => {
245+
const {
246+
AudioAttachmentUploadPreview,
247+
FileAttachmentUploadPreview,
248+
ImageAttachmentUploadPreview,
249+
VideoAttachmentUploadPreview,
250+
} = useMessageInputContext();
251+
return (
252+
<MemoizedAttachmentUploadPreviewListWithContext
253+
{...{
254+
AudioAttachmentUploadPreview,
255+
FileAttachmentUploadPreview,
256+
ImageAttachmentUploadPreview,
257+
VideoAttachmentUploadPreview,
258+
}}
259+
{...props}
260+
/>
261+
);
262+
};
263+
264+
const styles = StyleSheet.create({
265+
attachmentSeparator: {
266+
borderBottomWidth: 1,
267+
marginBottom: 10,
268+
},
269+
filesFlatList: { marginBottom: 12, maxHeight: FILE_PREVIEW_HEIGHT * 2.5 + 16 },
270+
imagesFlatList: { paddingBottom: 12 },
271+
});
272+
273+
AttachmentUploadPreviewList.displayName =
274+
'AttachmentUploadPreviewList{messageInput{attachmentUploadPreviewList}}';

0 commit comments

Comments
 (0)