Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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