diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 5b3439c6d1..9acd090aa7 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -187,6 +187,7 @@ export type ChannelPropsWithContext = Pick & | 'bottomInset' | 'topInset' | 'disableAttachmentPicker' + | 'shouldRenderAttachmentPicker' | 'numberOfAttachmentPickerImageColumns' | 'numberOfAttachmentImagesToLoadPerCall' > @@ -389,6 +390,7 @@ export type ChannelPropsWithContext = Pick & const ChannelWithContext = (props: PropsWithChildren) => { const { disableAttachmentPicker = !isImageMediaLibraryAvailable(), + shouldRenderAttachmentPicker = true, additionalKeyboardAvoidingViewProps, additionalPressableProps, additionalTextInputProps, @@ -1571,6 +1573,7 @@ const ChannelWithContext = (props: PropsWithChildren) = closePicker: handleClosePicker, disableAttachmentPicker, openPicker: handleOpenPicker, + shouldRenderAttachmentPicker, topInset, numberOfAttachmentPickerImageColumns, attachmentPickerBottomSheetHeight, @@ -1583,6 +1586,7 @@ const ChannelWithContext = (props: PropsWithChildren) = handleClosePicker, disableAttachmentPicker, handleOpenPicker, + shouldRenderAttachmentPicker, topInset, numberOfAttachmentPickerImageColumns, attachmentPickerBottomSheetHeight, @@ -1805,7 +1809,7 @@ const ChannelWithContext = (props: PropsWithChildren) = {children} - + {shouldRenderAttachmentPicker ? : null} diff --git a/package/src/components/MessageInput/__tests__/AttachButton.test.tsx b/package/src/components/MessageInput/__tests__/AttachButton.test.tsx index 32be1025f6..4e909f5c90 100644 --- a/package/src/components/MessageInput/__tests__/AttachButton.test.tsx +++ b/package/src/components/MessageInput/__tests__/AttachButton.test.tsx @@ -1,9 +1,12 @@ import React from 'react'; +import { Text } from 'react-native'; + import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native'; import type { Channel as ChannelType, StreamChat } from 'stream-chat'; import { OverlayProvider } from '../../../contexts'; +import { useAttachmentPickerState } from '../../../hooks/useAttachmentPickerState'; import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels'; import * as NativeHandler from '../../../native'; @@ -12,6 +15,11 @@ import { Channel } from '../../Channel/Channel'; import { Chat } from '../../Chat/Chat'; import { AttachButton } from '../components/InputButtons/AttachButton'; +const SelectedPicker = () => { + const { selectedPicker } = useAttachmentPickerState(); + return {selectedPicker ?? 'none'}; +}; + const renderComponent = ({ channelProps, client, @@ -32,6 +40,25 @@ const renderComponent = ({ ); }; +const renderWithSelectedPicker = ({ + channelProps, + client, +}: { + channelProps: Partial; + client: StreamChat; +}) => { + return render( + + + + + + + + , + ); +}; + describe('AttachButton', () => { let client: StreamChat; let channel: ChannelType; @@ -211,4 +238,65 @@ describe('AttachButton', () => { expect(queryByTestId('attachment-picker-list')).toBeNull(); }); }); + + it('should select the image picker when pressed', async () => { + jest.spyOn(NativeHandler, 'isImageMediaLibraryAvailable').mockImplementation(() => true); + + renderWithSelectedPicker({ channelProps: { channel }, client }); + + await waitFor(() => { + expect(screen.queryByTestId('attach-button')).toBeTruthy(); + }); + + act(() => { + fireEvent.press(screen.getByTestId('attach-button')); + }); + + await waitFor(() => { + expect(screen.getByTestId('selected-picker')).toHaveTextContent('images'); + }); + }); + + it('should not select a picker when pressed if shouldRenderAttachmentPicker is false', async () => { + jest.spyOn(NativeHandler, 'isImageMediaLibraryAvailable').mockImplementation(() => true); + + renderWithSelectedPicker({ + channelProps: { channel, shouldRenderAttachmentPicker: false }, + client, + }); + + await waitFor(() => { + expect(screen.queryByTestId('attach-button')).toBeTruthy(); + }); + + act(() => { + fireEvent.press(screen.getByTestId('attach-button')); + }); + + await waitFor(() => { + expect(screen.getByTestId('selected-picker')).toHaveTextContent('none'); + expect(screen.queryByTestId('attachment-picker-list')).toBeNull(); + }); + }); + + it('should render the attachment picker by default', async () => { + renderWithSelectedPicker({ channelProps: { channel }, client }); + + await waitFor(() => { + expect(screen.queryByTestId('upload-file-touchable')).toBeTruthy(); + }); + }); + + it('should not render the attachment picker when shouldRenderAttachmentPicker is false', async () => { + renderWithSelectedPicker({ + channelProps: { channel, shouldRenderAttachmentPicker: false }, + client, + }); + + await waitFor(() => { + expect(screen.queryByTestId('attach-button')).toBeTruthy(); + }); + + expect(screen.queryByTestId('upload-file-touchable')).toBeNull(); + }); }); diff --git a/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx b/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx index 71ccda2f50..a2def88b59 100644 --- a/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx +++ b/package/src/contexts/attachmentPickerContext/AttachmentPickerContext.tsx @@ -33,7 +33,20 @@ export type AttachmentPickerContextValue = Pick< openPicker: () => void; topInset: number; + /** + * When `true`, the native image picker is used instead of the in-app image gallery. + * The attachment picker itself (with its attachment type selection bar) is still rendered. + * To not render the attachment picker at all, set `shouldRenderAttachmentPicker` to `false`. + */ disableAttachmentPicker?: boolean; + /** + * Whether the attachment picker should be rendered at all. Defaults to `true`. + * + * Can be set to `false` if attachment picker is not used. `openAttachmentPicker` then + * does nothing. The attach button is still shown, so it needs to either be hidden it or set + * `handleAttachButtonPress` to override what it does. + */ + shouldRenderAttachmentPicker?: boolean; attachmentPickerStore: AttachmentPickerStore; numberOfAttachmentPickerImageColumns?: number; numberOfAttachmentImagesToLoadPerCall?: number; diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx index 7bf77ec6dd..0d6d5e3efb 100644 --- a/package/src/contexts/messageInputContext/MessageInputContext.tsx +++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx @@ -221,8 +221,13 @@ export const MessageInputProvider = ({ }: PropsWithChildren<{ value: InputMessageInputContextValue; }>) => { - const { closePicker, openPicker, attachmentPickerStore, disableAttachmentPicker } = - useAttachmentPickerContext(); + const { + closePicker, + openPicker, + attachmentPickerStore, + disableAttachmentPicker, + shouldRenderAttachmentPicker = true, + } = useAttachmentPickerContext(); const { client } = useChatContext(); const channelCapabilities = useOwnCapabilitiesContext(); const [audioRecorderManager] = useState(new AudioRecorderManager()); @@ -371,10 +376,14 @@ export const MessageInputProvider = ({ * Function to open the attachment picker if the MediaLibary is installed. */ const openAttachmentPicker = useCallback(() => { + if (!shouldRenderAttachmentPicker) { + // There is no picker to open, and a selected picker would leave space reserved for it. + return; + } dismissKeyboard(); attachmentPickerStore.setSelectedPicker('images'); openPicker(); - }, [attachmentPickerStore, openPicker]); + }, [attachmentPickerStore, openPicker, shouldRenderAttachmentPicker]); /** * Function to close the attachment picker if the MediaLibrary is installed.