diff --git a/package/src/components/AttachmentPicker/AttachmentPicker.tsx b/package/src/components/AttachmentPicker/AttachmentPicker.tsx index 97e45dde6d..5ddf942875 100644 --- a/package/src/components/AttachmentPicker/AttachmentPicker.tsx +++ b/package/src/components/AttachmentPicker/AttachmentPicker.tsx @@ -8,6 +8,8 @@ import { LayoutChangeEvent, } from 'react-native'; +import { runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated'; + import { useBottomSheetSpringConfigs } from '@gorhom/bottom-sheet'; import dayjs from 'dayjs'; import duration from 'dayjs/plugin/duration'; @@ -119,8 +121,36 @@ export const AttachmentPicker = () => { [semantics.backgroundCoreElevation1], ); + const animatedIndex = useSharedValue(currentIndex); + + // This is required to prevent the attachment picker from getting out of sync + // with the rest of the state. While there are more prudent fixes, this is about + // as much as we can do now without refactoring the entire state layer for the + // picker. When we do that, this can be removed completely. + const reactToIndex = useStableCallback((index: number) => { + if (index === -1) { + attachmentPickerStore.setSelectedPicker(undefined); + } + + if (index === 0) { + // TODO: Extend the store to at least accept a default value. + // This in particular is not nice. + attachmentPickerStore.setSelectedPicker('images'); + } + }); + + useAnimatedReaction( + () => animatedIndex.value, + (index, previousIndex) => { + if ((index === 0 || index === -1) && index !== previousIndex) { + runOnJS(reactToIndex)(index); + } + }, + ); + return ( { handleComponent={RenderNull} index={currentIndex} onAnimate={setCurrentIndex} + animatedIndex={animatedIndex} // @ts-ignore ref={ref} snapPoints={snapPoints} diff --git a/package/src/contexts/messageInputContext/MessageInputContext.tsx b/package/src/contexts/messageInputContext/MessageInputContext.tsx index 109c8a1f92..3dfe213f1b 100644 --- a/package/src/contexts/messageInputContext/MessageInputContext.tsx +++ b/package/src/contexts/messageInputContext/MessageInputContext.tsx @@ -7,7 +7,7 @@ import React, { useRef, useState, } from 'react'; -import { Alert, Linking, Platform, TextInput, TextInputProps } from 'react-native'; +import { Alert, Linking, TextInput, TextInputProps } from 'react-native'; import { lookup as lookupMimeType } from 'mime-types'; import { @@ -540,19 +540,9 @@ export const MessageInputProvider = ({ */ const openAttachmentPicker = useCallback(() => { dismissKeyboard(); - const run = () => { - attachmentPickerStore.setSelectedPicker('images'); - openPicker(); - }; - - if (Platform.OS === 'android') { - setTimeout(() => { - run(); - }, 200); - } else { - run(); - } - }, [openPicker, attachmentPickerStore]); + attachmentPickerStore.setSelectedPicker('images'); + openPicker(); + }, [attachmentPickerStore, openPicker]); /** * Function to close the attachment picker if the MediaLibrary is installed. diff --git a/package/src/hooks/useAttachmentPickerBottomSheet.ts b/package/src/hooks/useAttachmentPickerBottomSheet.ts index c03f87cc7a..6841934731 100644 --- a/package/src/hooks/useAttachmentPickerBottomSheet.ts +++ b/package/src/hooks/useAttachmentPickerBottomSheet.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import BottomSheet from '@gorhom/bottom-sheet'; import { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; @@ -6,31 +6,14 @@ import { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types'; /** * This hook is used to manage the state of the attachment picker bottom sheet. * It provides functions to open and close the bottom sheet, as well as a reference to the bottom sheet itself. - * It also handles the cleanup of the timeout used to close the bottom sheet. * The bottom sheet is used to display the attachment picker UI. * The `openPicker` function opens the bottom sheet, and the `closePicker` function closes it. * The `bottomSheetRef` is a reference to the bottom sheet component, which allows for programmatic control of the bottom sheet. - * The `bottomSheetCloseTimeoutRef` is used to store the timeout ID for the close operation, allowing for cleanup if necessary. */ export const useAttachmentPickerBottomSheet = () => { - const bottomSheetCloseTimeoutRef = useRef>(undefined); const bottomSheetRef = useRef(null); - useEffect( - () => - // cleanup the timeout if the component unmounts - () => { - if (bottomSheetCloseTimeoutRef.current) { - clearTimeout(bottomSheetCloseTimeoutRef.current); - } - }, - [], - ); - const openPicker = useCallback((ref: React.RefObject) => { - if (bottomSheetCloseTimeoutRef.current) { - clearTimeout(bottomSheetCloseTimeoutRef.current); - } if (ref.current?.snapToIndex) { ref.current.snapToIndex(0); } else { @@ -40,27 +23,11 @@ export const useAttachmentPickerBottomSheet = () => { const closePicker = useCallback((ref: React.RefObject) => { if (ref.current?.close) { - if (bottomSheetCloseTimeoutRef.current) { - clearTimeout(bottomSheetCloseTimeoutRef.current); - } ref.current.close(); - // Attempt to close the bottomsheet again to circumvent accidental opening on Android. - // Details: This to prevent a race condition where the close function is called during the point when a internal container layout happens within the bottomsheet due to keyboard affecting the layout - // If the container layout measures a shorter height than previous but if the close snapped to the previous height's position, the bottom sheet will show up - // this short delay ensures that close function is always called after a container layout due to keyboard change - // NOTE: this timeout has to be above 500 as the keyboardAnimationDuration is 500 in the bottomsheet library - see src/hooks/useKeyboard.ts there for more details - bottomSheetCloseTimeoutRef.current = setTimeout(() => { - ref.current?.close(); - }, 600); } }, []); - useEffect(() => { - closePicker(bottomSheetRef); - }, [closePicker]); - return { - bottomSheetCloseTimeoutRef, bottomSheetRef, closePicker, openPicker, diff --git a/package/src/hooks/useKeyboardVisibility.ts b/package/src/hooks/useKeyboardVisibility.ts index e013d41241..cffd977b31 100644 --- a/package/src/hooks/useKeyboardVisibility.ts +++ b/package/src/hooks/useKeyboardVisibility.ts @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react'; -import { EventSubscription, Keyboard } from 'react-native'; +import { EventSubscription, Keyboard, Platform } from 'react-native'; import { KeyboardControllerPackage } from '../components/KeyboardCompatibleView/KeyboardControllerAvoidingView'; @@ -24,8 +24,16 @@ export const useKeyboardVisibility = () => { ), ); } else { - listeners.push(Keyboard.addListener('keyboardWillShow', () => setIsKeyboardVisible(true))); - listeners.push(Keyboard.addListener('keyboardWillHide', () => setIsKeyboardVisible(false))); + listeners.push( + Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () => + setIsKeyboardVisible(true), + ), + ); + listeners.push( + Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () => + setIsKeyboardVisible(false), + ), + ); } return () => listeners.forEach((listener) => listener.remove());