Skip to content

Commit db99dc7

Browse files
authored
fix: android attachment picker timing coordination (#3539)
## 🎯 Goal This PR stabilizes attachment picker and keyboard behavior on both Android and iOS by simplifying the picker open/close flow and removing timing based workarounds. The main changes are: - removed the delayed keyboard close/open coordination path - removed `forceClose()` and the extra delayed `close()` fallback - kept picker state changes aligned with bottom sheet state - added a small handoff lock to prevent rapid picker/keyboard toggles from desynchronizing the composer and picker Net result: picker/keyboard transitions are more reliable, the Android flicker/stuck states are resolved, and rapid toggling no longer leaves the composer and attachment picker out of sync. This is now finally stable and correct on Android. ## 🛠 Implementation details <!-- Provide a description of the implementation --> ## 🎨 UI Changes <!-- Add relevant screenshots --> <details> <summary>iOS</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> <details> <summary>Android</summary> <table> <thead> <tr> <td>Before</td> <td>After</td> </tr> </thead> <tbody> <tr> <td> <!--<img src="" /> --> </td> <td> <!--<img src="" /> --> </td> </tr> </tbody> </table> </details> ## 🧪 Testing <!-- Explain how this change can be tested (or why it can't be tested) --> ## ☑️ Checklist - [ ] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required) - [ ] PR targets the `develop` branch - [ ] Documentation is updated - [ ] New code is tested in main example apps, including all possible scenarios - [ ] SampleApp iOS and Android - [ ] Expo iOS and Android
1 parent 5cdf7bc commit db99dc7

4 files changed

Lines changed: 47 additions & 51 deletions

File tree

package/src/components/AttachmentPicker/AttachmentPicker.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
LayoutChangeEvent,
99
} from 'react-native';
1010

11+
import { runOnJS, useAnimatedReaction, useSharedValue } from 'react-native-reanimated';
12+
1113
import { useBottomSheetSpringConfigs } from '@gorhom/bottom-sheet';
1214
import dayjs from 'dayjs';
1315
import duration from 'dayjs/plugin/duration';
@@ -119,15 +121,44 @@ export const AttachmentPicker = () => {
119121
[semantics.backgroundCoreElevation1],
120122
);
121123

124+
const animatedIndex = useSharedValue(currentIndex);
125+
126+
// This is required to prevent the attachment picker from getting out of sync
127+
// with the rest of the state. While there are more prudent fixes, this is about
128+
// as much as we can do now without refactoring the entire state layer for the
129+
// picker. When we do that, this can be removed completely.
130+
const reactToIndex = useStableCallback((index: number) => {
131+
if (index === -1) {
132+
attachmentPickerStore.setSelectedPicker(undefined);
133+
}
134+
135+
if (index === 0) {
136+
// TODO: Extend the store to at least accept a default value.
137+
// This in particular is not nice.
138+
attachmentPickerStore.setSelectedPicker('images');
139+
}
140+
});
141+
142+
useAnimatedReaction(
143+
() => animatedIndex.value,
144+
(index, previousIndex) => {
145+
if ((index === 0 || index === -1) && index !== previousIndex) {
146+
runOnJS(reactToIndex)(index);
147+
}
148+
},
149+
);
150+
122151
return (
123152
<BottomSheet
153+
android_keyboardInputMode='adjustResize'
124154
backgroundStyle={backgroundStyle}
125155
enablePanDownToClose={false}
126156
enableContentPanningGesture={false}
127157
enableDynamicSizing={false}
128158
handleComponent={RenderNull}
129159
index={currentIndex}
130160
onAnimate={setCurrentIndex}
161+
animatedIndex={animatedIndex}
131162
// @ts-ignore
132163
ref={ref}
133164
snapPoints={snapPoints}

package/src/contexts/messageInputContext/MessageInputContext.tsx

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import React, {
77
useRef,
88
useState,
99
} from 'react';
10-
import { Alert, Linking, Platform, TextInput, TextInputProps } from 'react-native';
10+
import { Alert, Linking, TextInput, TextInputProps } from 'react-native';
1111

1212
import { lookup as lookupMimeType } from 'mime-types';
1313
import {
@@ -540,19 +540,9 @@ export const MessageInputProvider = ({
540540
*/
541541
const openAttachmentPicker = useCallback(() => {
542542
dismissKeyboard();
543-
const run = () => {
544-
attachmentPickerStore.setSelectedPicker('images');
545-
openPicker();
546-
};
547-
548-
if (Platform.OS === 'android') {
549-
setTimeout(() => {
550-
run();
551-
}, 200);
552-
} else {
553-
run();
554-
}
555-
}, [openPicker, attachmentPickerStore]);
543+
attachmentPickerStore.setSelectedPicker('images');
544+
openPicker();
545+
}, [attachmentPickerStore, openPicker]);
556546

557547
/**
558548
* Function to close the attachment picker if the MediaLibrary is installed.

package/src/hooks/useAttachmentPickerBottomSheet.ts

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,19 @@
1-
import { useCallback, useEffect, useRef } from 'react';
1+
import { useCallback, useRef } from 'react';
22

33
import BottomSheet from '@gorhom/bottom-sheet';
44
import { BottomSheetMethods } from '@gorhom/bottom-sheet/lib/typescript/types';
55

66
/**
77
* This hook is used to manage the state of the attachment picker bottom sheet.
88
* It provides functions to open and close the bottom sheet, as well as a reference to the bottom sheet itself.
9-
* It also handles the cleanup of the timeout used to close the bottom sheet.
109
* The bottom sheet is used to display the attachment picker UI.
1110
* The `openPicker` function opens the bottom sheet, and the `closePicker` function closes it.
1211
* The `bottomSheetRef` is a reference to the bottom sheet component, which allows for programmatic control of the bottom sheet.
13-
* The `bottomSheetCloseTimeoutRef` is used to store the timeout ID for the close operation, allowing for cleanup if necessary.
1412
*/
1513
export const useAttachmentPickerBottomSheet = () => {
16-
const bottomSheetCloseTimeoutRef = useRef<ReturnType<typeof setTimeout>>(undefined);
1714
const bottomSheetRef = useRef<BottomSheet>(null);
1815

19-
useEffect(
20-
() =>
21-
// cleanup the timeout if the component unmounts
22-
() => {
23-
if (bottomSheetCloseTimeoutRef.current) {
24-
clearTimeout(bottomSheetCloseTimeoutRef.current);
25-
}
26-
},
27-
[],
28-
);
29-
3016
const openPicker = useCallback((ref: React.RefObject<BottomSheetMethods | null>) => {
31-
if (bottomSheetCloseTimeoutRef.current) {
32-
clearTimeout(bottomSheetCloseTimeoutRef.current);
33-
}
3417
if (ref.current?.snapToIndex) {
3518
ref.current.snapToIndex(0);
3619
} else {
@@ -40,27 +23,11 @@ export const useAttachmentPickerBottomSheet = () => {
4023

4124
const closePicker = useCallback((ref: React.RefObject<BottomSheetMethods | null>) => {
4225
if (ref.current?.close) {
43-
if (bottomSheetCloseTimeoutRef.current) {
44-
clearTimeout(bottomSheetCloseTimeoutRef.current);
45-
}
4626
ref.current.close();
47-
// Attempt to close the bottomsheet again to circumvent accidental opening on Android.
48-
// 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
49-
// 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
50-
// this short delay ensures that close function is always called after a container layout due to keyboard change
51-
// 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
52-
bottomSheetCloseTimeoutRef.current = setTimeout(() => {
53-
ref.current?.close();
54-
}, 600);
5527
}
5628
}, []);
5729

58-
useEffect(() => {
59-
closePicker(bottomSheetRef);
60-
}, [closePicker]);
61-
6230
return {
63-
bottomSheetCloseTimeoutRef,
6431
bottomSheetRef,
6532
closePicker,
6633
openPicker,

package/src/hooks/useKeyboardVisibility.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { useEffect, useState } from 'react';
2-
import { EventSubscription, Keyboard } from 'react-native';
2+
import { EventSubscription, Keyboard, Platform } from 'react-native';
33

44
import { KeyboardControllerPackage } from '../components/KeyboardCompatibleView/KeyboardControllerAvoidingView';
55

@@ -24,8 +24,16 @@ export const useKeyboardVisibility = () => {
2424
),
2525
);
2626
} else {
27-
listeners.push(Keyboard.addListener('keyboardWillShow', () => setIsKeyboardVisible(true)));
28-
listeners.push(Keyboard.addListener('keyboardWillHide', () => setIsKeyboardVisible(false)));
27+
listeners.push(
28+
Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow', () =>
29+
setIsKeyboardVisible(true),
30+
),
31+
);
32+
listeners.push(
33+
Keyboard.addListener(Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide', () =>
34+
setIsKeyboardVisible(false),
35+
),
36+
);
2937
}
3038

3139
return () => listeners.forEach((listener) => listener.remove());

0 commit comments

Comments
 (0)