import React, { useEffect, useMemo, useState } from 'react'; import { BackHandler, Dimensions, ImageBackground, Keyboard, Platform, StatusBar, StyleSheet, View, } from 'react-native'; import BottomSheet, { BottomSheetFlatList, BottomSheetHandleProps, TouchableOpacity, } from '@gorhom/bottom-sheet'; import { useAttachmentPickerContext } from '../../contexts/attachmentPickerContext/AttachmentPickerContext'; import { useTheme } from '../../contexts/themeContext/ThemeContext'; import { Asset, getPhotos } from '../../native'; import { vh, vw } from '../../utils/utils'; import type { AttachmentPickerErrorProps } from './components/AttachmentPickerError'; const styles = StyleSheet.create({ container: { flexGrow: 1, }, overlay: { alignItems: 'flex-end', flex: 1, }, }); const screenHeight = vh(100); const fullScreenHeight = Dimensions.get('screen').height; type AttachmentImageProps = { ImageOverlaySelectedComponent: React.ComponentType; onPress: () => void; selected: boolean; uri: string; numberOfAttachmentPickerImageColumns?: number; }; const AttachmentImage: React.FC = (props) => { const { ImageOverlaySelectedComponent, numberOfAttachmentPickerImageColumns, onPress, selected, uri, } = props; const { theme: { attachmentPicker: { image, imageOverlay }, colors: { overlay }, }, } = useTheme(); const size = vw(100) / (numberOfAttachmentPickerImageColumns || 3) - 2; return ( {selected && ( )} ); }; const renderImage = ({ item, }: { item: { asset: Asset; ImageOverlaySelectedComponent: React.ComponentType; maxNumberOfFiles: number; selected: boolean; setSelectedImages: React.Dispatch>; numberOfAttachmentPickerImageColumns?: number; }; }) => { const { asset, ImageOverlaySelectedComponent, maxNumberOfFiles, numberOfAttachmentPickerImageColumns, selected, setSelectedImages, } = item; const onPress = () => { if (selected) { setSelectedImages((images) => images.filter((image) => image.uri !== asset.uri)); } else { setSelectedImages((images) => { if (images.length >= maxNumberOfFiles) { return images; } return [...images, asset]; }); } }; return ( ); }; export type AttachmentPickerProps = { /** * Custom UI component to render [draggable handle](https://github.com/GetStream/stream-chat-react-native/blob/master/screenshots/docs/1.png) of attachment picker. * * **Default** [AttachmentPickerBottomSheetHandle](https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/AttachmentPicker/components/AttachmentPickerBottomSheetHandle.tsx) */ AttachmentPickerBottomSheetHandle: React.FC; /** * Custom UI component to render error component while opening attachment picker. * * **Default** [AttachmentPickerError](https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/AttachmentPicker/components/AttachmentPickerError.tsx) */ AttachmentPickerError: React.ComponentType; /** * Custom UI component to render error image for attachment picker * * **Default** [AttachmentPickerErrorImage](https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/AttachmentPicker/components/AttachmentPickerErrorImage.tsx) */ AttachmentPickerErrorImage: React.ComponentType; /** * Custom UI component to render overlay component, that shows up on top of [selected image](https://github.com/GetStream/stream-chat-react-native/blob/master/screenshots/docs/1.png) (with tick mark) * * **Default** [ImageOverlaySelectedComponent](https://github.com/GetStream/stream-chat-react-native/blob/master/src/components/AttachmentPicker/components/ImageOverlaySelectedComponent.tsx) */ ImageOverlaySelectedComponent: React.ComponentType; attachmentPickerBottomSheetHandleHeight?: number; attachmentPickerBottomSheetHeight?: number; attachmentPickerErrorButtonText?: string; attachmentPickerErrorText?: string; numberOfAttachmentImagesToLoadPerCall?: number; numberOfAttachmentPickerImageColumns?: number; translucentStatusBar?: boolean; }; export const AttachmentPicker = React.forwardRef( (props: AttachmentPickerProps, ref: React.ForwardedRef) => { const { AttachmentPickerBottomSheetHandle, attachmentPickerBottomSheetHandleHeight, attachmentPickerBottomSheetHeight, AttachmentPickerError, attachmentPickerErrorButtonText, AttachmentPickerErrorImage, attachmentPickerErrorText, ImageOverlaySelectedComponent, numberOfAttachmentImagesToLoadPerCall, numberOfAttachmentPickerImageColumns, translucentStatusBar, } = props; const { theme: { attachmentPicker: { bottomSheetContentContainer }, colors: { white }, }, } = useTheme(); const { closePicker, maxNumberOfFiles, selectedImages, selectedPicker, setSelectedImages, setSelectedPicker, topInset, } = useAttachmentPickerContext(); const [currentIndex, setCurrentIndex] = useState(-1); const [endCursor, setEndCursor] = useState(); const [photoError, setPhotoError] = useState(false); const [hasNextPage, setHasNextPage] = useState(true); const [loadingPhotos, setLoadingPhotos] = useState(false); const [photos, setPhotos] = useState([]); const hideAttachmentPicker = () => { setSelectedPicker(undefined); if ((ref as React.MutableRefObject)?.current) { (ref as React.MutableRefObject).current.close(); } }; const getMorePhotos = async () => { if (hasNextPage && !loadingPhotos && currentIndex > -1 && selectedPicker === 'images') { setLoadingPhotos(true); try { const results = await getPhotos({ after: endCursor, first: numberOfAttachmentImagesToLoadPerCall ?? 60, }); if (endCursor) { setPhotos([...photos, ...results.assets]); } else { setPhotos(results.assets); } setEndCursor(results.endCursor); setHasNextPage(results.hasNextPage || false); } catch (error) { console.log(error); setPhotoError(true); } setLoadingPhotos(false); } }; useEffect(() => { const backAction = () => { if (selectedPicker) { setSelectedPicker(undefined); closePicker(); return true; } return false; }; const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction); return () => backHandler.remove(); }, [selectedPicker]); useEffect(() => { if (Platform.OS === 'ios') { Keyboard.addListener('keyboardWillShow', hideAttachmentPicker); } else { Keyboard.addListener('keyboardDidShow', hideAttachmentPicker); } return () => { if (Platform.OS === 'ios') { Keyboard.removeListener('keyboardWillShow', hideAttachmentPicker); } else { Keyboard.removeListener('keyboardDidShow', hideAttachmentPicker); } }; }, []); useEffect(() => { if (currentIndex < 0) { setSelectedPicker(undefined); if (!loadingPhotos) { setEndCursor(undefined); setHasNextPage(true); } } }, [currentIndex]); useEffect(() => { if ( selectedPicker === 'images' && endCursor === undefined && currentIndex > -1 && !loadingPhotos ) { setPhotoError(false); getMorePhotos(); } }, [currentIndex, selectedPicker]); const selectedPhotos = photos.map((asset) => ({ asset, ImageOverlaySelectedComponent, maxNumberOfFiles, numberOfAttachmentPickerImageColumns, selected: selectedImages.some((image) => image.uri === asset.uri), setSelectedImages, })); const handleHeight = attachmentPickerBottomSheetHandleHeight || 20; /** * This is to handle issues with Android measurements coming back incorrect. * If the StatusBar height is perfectly 1/2 of the difference between the two * dimensions for screen and window, it is incorrect and we need to account for * this. If you use a translucent header bar more adjustments are needed. */ const statusBarHeight = StatusBar.currentHeight ?? 0; const bottomBarHeight = fullScreenHeight - screenHeight - statusBarHeight; const androidBottomBarHeightAdjustment = Platform.OS === 'android' ? bottomBarHeight === statusBarHeight ? translucentStatusBar ? 0 : StatusBar.currentHeight ?? 0 : translucentStatusBar ? bottomBarHeight > statusBarHeight ? -bottomBarHeight + statusBarHeight : bottomBarHeight > 0 ? -statusBarHeight : 0 : bottomBarHeight > 0 ? 0 : statusBarHeight : 0; /** * Snap points changing cause a rerender of the position, * this is an issue if you are calling close on the bottom sheet. */ const snapPoints = useMemo( () => [ attachmentPickerBottomSheetHeight ?? 308 + (fullScreenHeight - screenHeight + androidBottomBarHeightAdjustment) - handleHeight, fullScreenHeight - topInset - handleHeight, ], [ androidBottomBarHeightAdjustment, attachmentPickerBottomSheetHeight, fullScreenHeight, handleHeight, screenHeight, topInset, ], ); return ( <> setCurrentIndex(index)} ref={ref} snapPoints={snapPoints} > item.asset.uri} numColumns={numberOfAttachmentPickerImageColumns ?? 3} onEndReached={getMorePhotos} renderItem={renderImage} /> {selectedPicker === 'images' && photoError && ( )} ); }, ); AttachmentPicker.displayName = 'AttachmentPicker';