Skip to content
Merged
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
9 changes: 3 additions & 6 deletions package/src/components/Channel/Channel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ import { ReactionListBottom as ReactionListBottomDefault } from '../Message/Mess
import { ReactionListTop as ReactionListTopDefault } from '../Message/MessageSimple/ReactionList/ReactionListTop';
import { StreamingMessageView as DefaultStreamingMessageView } from '../Message/MessageSimple/StreamingMessageView';
import { AttachButton as AttachButtonDefault } from '../MessageInput/AttachButton';
import { AttachmentUploadPreviewList as AttachmentUploadPreviewDefault } from '../MessageInput/AttachmentUploadPreviewList';
import { CommandsButton as CommandsButtonDefault } from '../MessageInput/CommandsButton';
import { AttachmentUploadProgressIndicator as AttachmentUploadProgressIndicatorDefault } from '../MessageInput/components/AttachmentPreview/AttachmentUploadProgressIndicator';
import { AudioAttachmentUploadPreview as AudioAttachmentUploadPreviewDefault } from '../MessageInput/components/AttachmentPreview/AudioAttachmentUploadPreview';
Expand All @@ -176,8 +177,6 @@ import { CommandInput as CommandInputDefault } from '../MessageInput/components/
import { InputEditingStateHeader as InputEditingStateHeaderDefault } from '../MessageInput/components/InputEditingStateHeader';
import { InputReplyStateHeader as InputReplyStateHeaderDefault } from '../MessageInput/components/InputReplyStateHeader';
import { CooldownTimer as CooldownTimerDefault } from '../MessageInput/CooldownTimer';
import { FileUploadPreview as FileUploadPreviewDefault } from '../MessageInput/FileUploadPreview';
import { ImageUploadPreview as ImageUploadPreviewDefault } from '../MessageInput/ImageUploadPreview';
import { InputButtons as InputButtonsDefault } from '../MessageInput/InputButtons';
import { MoreOptionsButton as MoreOptionsButtonDefault } from '../MessageInput/MoreOptionsButton';
import { SendButton as SendButtonDefault } from '../MessageInput/SendButton';
Expand Down Expand Up @@ -526,6 +525,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
AttachmentPickerError = DefaultAttachmentPickerError,
AttachmentPickerErrorImage = DefaultAttachmentPickerErrorImage,
AttachmentPickerIOSSelectMorePhotos = DefaultAttachmentPickerIOSSelectMorePhotos,
AttachmentUploadPreviewList = AttachmentUploadPreviewDefault,
ImageOverlaySelectedComponent = DefaultImageOverlaySelectedComponent,
attachmentPickerErrorButtonText,
attachmentPickerErrorText,
Expand Down Expand Up @@ -567,7 +567,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
FileAttachmentUploadPreview = FileAttachmentUploadPreviewDefault,
FileAttachmentGroup = FileAttachmentGroupDefault,
FileAttachmentIcon = FileIconDefault,
FileUploadPreview = FileUploadPreviewDefault,
FlatList = NativeHandlers.FlatList,
forceAlignMessages,
Gallery = GalleryDefault,
Expand Down Expand Up @@ -599,7 +598,6 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
ImageLoadingFailedIndicator = ImageLoadingFailedIndicatorDefault,
ImageLoadingIndicator = ImageLoadingIndicatorDefault,
ImageReloadIndicator = ImageReloadIndicatorDefault,
ImageUploadPreview = ImageUploadPreviewDefault,
initialScrollToFirstUnreadMessage = false,
InlineDateSeparator = InlineDateSeparatorDefault,
InlineUnreadIndicator = InlineUnreadIndicatorDefault,
Expand Down Expand Up @@ -1741,6 +1739,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
attachmentPickerBottomSheetHeight,
AttachmentPickerSelectionBar,
attachmentSelectionBarHeight,
AttachmentUploadPreviewList,
AttachmentUploadProgressIndicator,
AudioAttachmentUploadPreview,
AudioRecorder,
Expand All @@ -1764,15 +1763,13 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
editMessage,
FileAttachmentUploadPreview,
FileSelectorIcon,
FileUploadPreview,
handleAttachButtonPress,
hasCameraPicker,
hasCommands: hasCommands ?? !!clientChannelConfig?.commands?.length,
hasFilePicker,
hasImagePicker,
ImageAttachmentUploadPreview,
ImageSelectorIcon,
ImageUploadPreview,
Input,
InputButtons,
InputEditingStateHeader,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const useCreateInputMessageInputContext = ({
attachmentPickerBottomSheetHeight,
AttachmentPickerSelectionBar,
attachmentSelectionBarHeight,
AttachmentUploadPreviewList,
AttachmentUploadProgressIndicator,
AudioAttachmentUploadPreview,
AudioRecorder,
Expand All @@ -37,15 +38,13 @@ export const useCreateInputMessageInputContext = ({
editMessage,
FileAttachmentUploadPreview,
FileSelectorIcon,
FileUploadPreview,
handleAttachButtonPress,
hasCameraPicker,
hasCommands,
hasFilePicker,
hasImagePicker,
ImageAttachmentUploadPreview,
ImageSelectorIcon,
ImageUploadPreview,
Input,
InputButtons,
InputEditingStateHeader,
Expand Down Expand Up @@ -81,6 +80,7 @@ export const useCreateInputMessageInputContext = ({
attachmentPickerBottomSheetHeight,
AttachmentPickerSelectionBar,
attachmentSelectionBarHeight,
AttachmentUploadPreviewList,
AttachmentUploadProgressIndicator,
AudioAttachmentUploadPreview,
AudioRecorder,
Expand All @@ -103,15 +103,13 @@ export const useCreateInputMessageInputContext = ({
editMessage,
FileAttachmentUploadPreview,
FileSelectorIcon,
FileUploadPreview,
handleAttachButtonPress,
hasCameraPicker,
hasCommands,
hasFilePicker,
hasImagePicker,
ImageAttachmentUploadPreview,
ImageSelectorIcon,
ImageUploadPreview,
Input,
InputButtons,
InputEditingStateHeader,
Expand Down
274 changes: 274 additions & 0 deletions package/src/components/MessageInput/AttachmentUploadPreviewList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { FlatList, LayoutChangeEvent, StyleSheet, View } from 'react-native';

import {
isLocalAudioAttachment,
isLocalFileAttachment,
isLocalImageAttachment,
isLocalVoiceRecordingAttachment,
isVideoAttachment,
LocalAttachment,
LocalImageAttachment,
} from 'stream-chat';

import { useAudioPreviewManager } from './hooks/useAudioPreviewManager';

import { useMessageComposer } from '../../contexts';
import { useAttachmentManagerState } from '../../contexts/messageInputContext/hooks/useAttachmentManagerState';
import {
MessageInputContextValue,
useMessageInputContext,
} from '../../contexts/messageInputContext/MessageInputContext';
import { useTheme } from '../../contexts/themeContext/ThemeContext';
import { isSoundPackageAvailable } from '../../native';

const IMAGE_PREVIEW_SIZE = 100;
const FILE_PREVIEW_HEIGHT = 60;

export type AttachmentUploadPreviewListPropsWithContext = Pick<
MessageInputContextValue,
| 'AudioAttachmentUploadPreview'
| 'FileAttachmentUploadPreview'
| 'ImageAttachmentUploadPreview'
| 'VideoAttachmentUploadPreview'
>;

/**
* AttachmentUploadPreviewList
* UI Component to preview the files set for upload
*/
const UnMemoizedAttachmentUploadListPreview = (
props: AttachmentUploadPreviewListPropsWithContext,
) => {
const [flatListWidth, setFlatListWidth] = useState(0);
const flatListRef = useRef<FlatList<LocalAttachment> | null>(null);
const {
AudioAttachmentUploadPreview,
FileAttachmentUploadPreview,
ImageAttachmentUploadPreview,
VideoAttachmentUploadPreview,
} = props;
const { attachmentManager } = useMessageComposer();
const { attachments } = useAttachmentManagerState();
const {
theme: {
colors: { grey_whisper },
messageInput: {
attachmentSeparator,
attachmentUploadPreviewList: { filesFlatList, imagesFlatList, wrapper },
},
},
} = useTheme();

const imageUploads = attachments.filter((attachment) => isLocalImageAttachment(attachment));
const fileUploads = useMemo(() => {
return attachments.filter((attachment) => !isLocalImageAttachment(attachment));
}, [attachments]);
const audioUploads = useMemo(() => {
return fileUploads.filter(
(attachment) =>
isLocalAudioAttachment(attachment) || isLocalVoiceRecordingAttachment(attachment),
);
}, [fileUploads]);

const { audioAttachmentsStateMap, onLoad, onProgress, onPlayPause } =
useAudioPreviewManager(audioUploads);

const renderImageItem = useCallback(
({ item }: { item: LocalImageAttachment }) => {
return (
<ImageAttachmentUploadPreview
attachment={item}
handleRetry={attachmentManager.uploadAttachment}
removeAttachments={attachmentManager.removeAttachments}
/>
);
},
[
ImageAttachmentUploadPreview,
attachmentManager.removeAttachments,
attachmentManager.uploadAttachment,
],
);

const renderFileItem = useCallback(
({ item }: { item: LocalAttachment }) => {
if (isLocalImageAttachment(item)) {
// This is already handled in the `renderImageItem` above, so we return null here to avoid duplication.
return null;
} else if (isLocalVoiceRecordingAttachment(item)) {
return (
<AudioAttachmentUploadPreview
attachment={item}
audioAttachmentConfig={audioAttachmentsStateMap[item.localMetadata.id]}
handleRetry={attachmentManager.uploadAttachment}
onLoad={onLoad}
onPlayPause={onPlayPause}
onProgress={onProgress}
removeAttachments={attachmentManager.removeAttachments}
/>
);
} else if (isLocalAudioAttachment(item)) {
if (isSoundPackageAvailable()) {
return (
<AudioAttachmentUploadPreview
attachment={item}
audioAttachmentConfig={audioAttachmentsStateMap[item.localMetadata.id]}
handleRetry={attachmentManager.uploadAttachment}
onLoad={onLoad}
onPlayPause={onPlayPause}
onProgress={onProgress}
removeAttachments={attachmentManager.removeAttachments}
/>
);
} else {
return (
<FileAttachmentUploadPreview
attachment={item}
flatListWidth={flatListWidth}
handleRetry={attachmentManager.uploadAttachment}
removeAttachments={attachmentManager.removeAttachments}
/>
);
}
} else if (isVideoAttachment(item)) {
return (
<VideoAttachmentUploadPreview
attachment={item}
flatListWidth={flatListWidth}
handleRetry={attachmentManager.uploadAttachment}
removeAttachments={attachmentManager.removeAttachments}
/>
);
} else if (isLocalFileAttachment(item)) {
return (
<FileAttachmentUploadPreview
attachment={item}
flatListWidth={flatListWidth}
handleRetry={attachmentManager.uploadAttachment}
removeAttachments={attachmentManager.removeAttachments}
/>
);
} else return null;
},
[
AudioAttachmentUploadPreview,
FileAttachmentUploadPreview,
VideoAttachmentUploadPreview,
attachmentManager.removeAttachments,
attachmentManager.uploadAttachment,
audioAttachmentsStateMap,
flatListWidth,
onLoad,
onPlayPause,
onProgress,
],
);

useEffect(() => {
if (fileUploads.length && flatListRef.current) {
setTimeout(() => flatListRef.current?.scrollToEnd(), 1);
}
}, [fileUploads.length]);

const onLayout = useCallback(
(event: LayoutChangeEvent) => {
if (flatListRef.current) {
setFlatListWidth(event.nativeEvent.layout.width);
}
},
[flatListRef],
);

if (!attachments.length) {
return null;
}

return (
<View style={[wrapper]}>
{imageUploads.length ? (
<FlatList
data={imageUploads}
getItemLayout={(_, index) => ({
index,
length: IMAGE_PREVIEW_SIZE + 8,
offset: (IMAGE_PREVIEW_SIZE + 8) * index,
})}
horizontal
keyExtractor={(item) => item.localMetadata.id}
renderItem={renderImageItem}
style={[styles.imagesFlatList, imagesFlatList]}
/>
) : null}
{imageUploads.length && fileUploads.length ? (
<View
style={[
styles.attachmentSeparator,
{
borderBottomColor: grey_whisper,
},
attachmentSeparator,
]}
/>
) : null}
{fileUploads.length ? (
<FlatList
data={fileUploads}
getItemLayout={(_, index) => ({
index,
length: FILE_PREVIEW_HEIGHT + 8,
offset: (FILE_PREVIEW_HEIGHT + 8) * index,
})}
keyExtractor={(item) => item.localMetadata.id}
onLayout={onLayout}
ref={flatListRef}
renderItem={renderFileItem}
style={[styles.filesFlatList, filesFlatList]}
testID={'file-upload-preview'}
/>
) : null}
</View>
);
};

export type AttachmentUploadPreviewListProps = Partial<AttachmentUploadPreviewListPropsWithContext>;

const MemoizedAttachmentUploadPreviewListWithContext = React.memo(
UnMemoizedAttachmentUploadListPreview,
);

/**
* AttachmentUploadPreviewList
* UI Component to preview the files set for upload
*/
export const AttachmentUploadPreviewList = (props: AttachmentUploadPreviewListProps) => {
const {
AudioAttachmentUploadPreview,
FileAttachmentUploadPreview,
ImageAttachmentUploadPreview,
VideoAttachmentUploadPreview,
} = useMessageInputContext();
return (
<MemoizedAttachmentUploadPreviewListWithContext
{...{
AudioAttachmentUploadPreview,
FileAttachmentUploadPreview,
ImageAttachmentUploadPreview,
VideoAttachmentUploadPreview,
}}
{...props}
/>
);
};

const styles = StyleSheet.create({
attachmentSeparator: {
borderBottomWidth: 1,
marginBottom: 10,
},
filesFlatList: { marginBottom: 12, maxHeight: FILE_PREVIEW_HEIGHT * 2.5 + 16 },
imagesFlatList: { paddingBottom: 12 },
});

AttachmentUploadPreviewList.displayName =
'AttachmentUploadPreviewList{messageInput{attachmentUploadPreviewList}}';
Loading