Skip to content

Commit 9712fe9

Browse files
committed
feat: optional support for react-native-keyboard-controller
1 parent 59e62b4 commit 9712fe9

File tree

13 files changed

+224
-35
lines changed

13 files changed

+224
-35
lines changed

examples/SampleApp/ios/Podfile.lock

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2036,6 +2036,65 @@ PODS:
20362036
- ReactCommon/turbomodule/core
20372037
- SocketRocket
20382038
- Yoga
2039+
- react-native-keyboard-controller (1.20.2):
2040+
- boost
2041+
- DoubleConversion
2042+
- fast_float
2043+
- fmt
2044+
- glog
2045+
- hermes-engine
2046+
- RCT-Folly
2047+
- RCT-Folly/Fabric
2048+
- RCTRequired
2049+
- RCTTypeSafety
2050+
- React-Core
2051+
- React-debug
2052+
- React-Fabric
2053+
- React-featureflags
2054+
- React-graphics
2055+
- React-hermes
2056+
- React-ImageManager
2057+
- React-jsi
2058+
- react-native-keyboard-controller/common (= 1.20.2)
2059+
- React-NativeModulesApple
2060+
- React-RCTFabric
2061+
- React-renderercss
2062+
- React-rendererdebug
2063+
- React-utils
2064+
- ReactCodegen
2065+
- ReactCommon/turbomodule/bridging
2066+
- ReactCommon/turbomodule/core
2067+
- SocketRocket
2068+
- Yoga
2069+
- react-native-keyboard-controller/common (1.20.2):
2070+
- boost
2071+
- DoubleConversion
2072+
- fast_float
2073+
- fmt
2074+
- glog
2075+
- hermes-engine
2076+
- RCT-Folly
2077+
- RCT-Folly/Fabric
2078+
- RCTRequired
2079+
- RCTTypeSafety
2080+
- React-Core
2081+
- React-debug
2082+
- React-Fabric
2083+
- React-featureflags
2084+
- React-graphics
2085+
- React-hermes
2086+
- React-ImageManager
2087+
- React-jsi
2088+
- React-NativeModulesApple
2089+
- React-RCTFabric
2090+
- React-renderercss
2091+
- React-rendererdebug
2092+
- React-utils
2093+
- ReactCodegen
2094+
- ReactCommon/turbomodule/bridging
2095+
- ReactCommon/turbomodule/core
2096+
- SocketRocket
2097+
- Yoga
20392098
- react-native-maps (1.20.1):
20402099
- React-Core
20412100
- react-native-netinfo (11.4.1):
@@ -3220,6 +3279,7 @@ DEPENDENCIES:
32203279
- "react-native-document-picker (from `../node_modules/@react-native-documents/picker`)"
32213280
- "react-native-geolocation (from `../node_modules/@react-native-community/geolocation`)"
32223281
- react-native-image-picker (from `../node_modules/react-native-image-picker`)
3282+
- react-native-keyboard-controller (from `../node_modules/react-native-keyboard-controller`)
32233283
- react-native-maps (from `../node_modules/react-native-maps`)
32243284
- "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)"
32253285
- react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`)
@@ -3394,6 +3454,8 @@ EXTERNAL SOURCES:
33943454
:path: "../node_modules/@react-native-community/geolocation"
33953455
react-native-image-picker:
33963456
:path: "../node_modules/react-native-image-picker"
3457+
react-native-keyboard-controller:
3458+
:path: "../node_modules/react-native-keyboard-controller"
33973459
react-native-maps:
33983460
:path: "../node_modules/react-native-maps"
33993461
react-native-netinfo:
@@ -3561,6 +3623,7 @@ SPEC CHECKSUMS:
35613623
react-native-document-picker: c5fa18e9fc47b34cfbab3b0a4447d0df918a5621
35623624
react-native-geolocation: eb39c815c9b58ddc3efb552cafdd4b035e4cf682
35633625
react-native-image-picker: e479ec8884df9d99a62c1f53f2307055ad43ea85
3626+
react-native-keyboard-controller: 6fe65d5d011d88e651d5279396e95e9c1f9458ca
35643627
react-native-maps: ee1e65647460c3d41e778071be5eda10e3da6225
35653628
react-native-netinfo: f0a9899081c185db1de5bb2fdc1c88c202a059ac
35663629
react-native-safe-area-context: 7fd4c2c8023da8e18eaa3424cb49d52f626debee

examples/SampleApp/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"react-native-gesture-handler": "^2.26.0",
4848
"react-native-haptic-feedback": "^2.3.3",
4949
"react-native-image-picker": "^8.2.1",
50+
"react-native-keyboard-controller": "^1.20.2",
5051
"react-native-maps": "1.20.1",
5152
"react-native-nitro-modules": "^0.31.3",
5253
"react-native-nitro-sound": "^0.2.9",

examples/SampleApp/src/screens/ChannelScreen.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useCallback, useEffect, useState } from 'react';
22
import type { LocalMessage, Channel as StreamChatChannel } from 'stream-chat';
3+
import { useHeaderHeight } from '@react-navigation/elements';
34
import { RouteProp, useFocusEffect, useNavigation } from '@react-navigation/native';
45
import {
56
Channel,
@@ -17,7 +18,7 @@ import {
1718
useTranslationContext,
1819
MessageActionsParams,
1920
} from 'stream-chat-react-native';
20-
import { Platform, Pressable, StyleSheet, View } from 'react-native';
21+
import { Pressable, StyleSheet, View } from 'react-native';
2122
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';
2223
import { useSafeAreaInsets } from 'react-native-safe-area-context';
2324

@@ -210,6 +211,7 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
210211
},
211212
[chatClient, colors, t, handleMessageInfo],
212213
);
214+
const headerHeight = useHeaderHeight();
213215

214216
if (!channel || !chatClient) {
215217
return null;
@@ -225,7 +227,7 @@ export const ChannelScreen: React.FC<ChannelScreenProps> = ({
225227
disableTypingIndicator
226228
enforceUniqueReaction
227229
initialScrollToFirstUnreadMessage
228-
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -300}
230+
keyboardVerticalOffset={headerHeight}
229231
messageActions={messageActions}
230232
MessageHeader={MessageReminderHeader}
231233
MessageLocation={MessageLocation}

examples/SampleApp/yarn.lock

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7658,6 +7658,13 @@ react-native-is-edge-to-edge@^1.2.1:
76587658
resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz#64e10851abd9d176cbf2b40562f751622bde3358"
76597659
integrity sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==
76607660

7661+
react-native-keyboard-controller@^1.20.2:
7662+
version "1.20.2"
7663+
resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.20.2.tgz#2953341f48e25fec20dd732241cb8152251fd4d1"
7664+
integrity sha512-3xvPTIfasAbosDxT3Mc6b5Xr/M+yq99ECCM4iGnSAngziIVUZsZuPpfYL7nN1UiN9rQjWKvjdul/jq9E0V1s2w==
7665+
dependencies:
7666+
react-native-is-edge-to-edge "^1.2.1"
7667+
76617668
react-native-lightbox@^0.7.0:
76627669
version "0.7.0"
76637670
resolved "https://registry.yarnpkg.com/react-native-lightbox/-/react-native-lightbox-0.7.0.tgz#e52b4d7fcc141f59d7b23f0180de535e35b20ec9"

package/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
"emoji-mart": ">=5.4.0",
9292
"react-native": ">=0.73.0",
9393
"react-native-gesture-handler": ">=2.18.0",
94+
"react-native-keyboard-controller": ">=1.20.2",
9495
"react-native-reanimated": ">=3.16.0",
9596
"react-native-safe-area-context": ">=5.4.1",
9697
"react-native-svg": ">=15.8.0"
@@ -107,6 +108,9 @@
107108
},
108109
"@emoji-mart/data": {
109110
"optional": true
111+
},
112+
"react-native-keyboard-controller": {
113+
"optional": true
110114
}
111115
},
112116
"devDependencies": {
@@ -154,6 +158,7 @@
154158
"react-native": "0.80.2",
155159
"react-native-builder-bob": "0.40.11",
156160
"react-native-gesture-handler": "^2.26.0",
161+
"react-native-keyboard-controller": "^1.20.2",
157162
"react-native-reanimated": "3.18.0",
158163
"react-native-safe-area-context": "^5.6.1",
159164
"react-native-svg": "15.12.0",

package/src/components/AttachmentPicker/AttachmentPicker.tsx

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2-
import { BackHandler, Keyboard, Platform, StyleSheet } from 'react-native';
2+
import { BackHandler, EmitterSubscription, Keyboard, Platform, StyleSheet } from 'react-native';
33

44
import BottomSheetOriginal from '@gorhom/bottom-sheet';
55
import type { BottomSheetHandleProps } from '@gorhom/bottom-sheet';
@@ -19,6 +19,7 @@ import { NativeHandlers } from '../../native';
1919
import type { File } from '../../types/types';
2020
import { BottomSheet } from '../BottomSheetCompatibility/BottomSheet';
2121
import { BottomSheetFlatList } from '../BottomSheetCompatibility/BottomSheetFlatList';
22+
import { KeyboardControllerPackage } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
2223

2324
dayjs.extend(duration);
2425

@@ -185,20 +186,18 @@ export const AttachmentPicker = React.forwardRef(
185186
}
186187
closePicker();
187188
};
188-
const keyboardShowEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
189-
const keyboardSubscription = Keyboard.addListener(keyboardShowEvent, onKeyboardOpenHandler);
190-
189+
let keyboardSubscription: EmitterSubscription | null = null;
190+
if (KeyboardControllerPackage?.KeyboardEvents) {
191+
keyboardSubscription = KeyboardControllerPackage.KeyboardEvents.addListener(
192+
'keyboardWillShow',
193+
onKeyboardOpenHandler,
194+
);
195+
} else {
196+
const keyboardShowEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
197+
keyboardSubscription = Keyboard.addListener(keyboardShowEvent, onKeyboardOpenHandler);
198+
}
191199
return () => {
192-
// Following if-else condition to avoid deprecated warning coming RN 0.65
193-
if (keyboardSubscription?.remove) {
194-
keyboardSubscription.remove();
195-
return;
196-
}
197-
// @ts-ignore
198-
else if (Keyboard.removeListener) {
199-
// @ts-ignore
200-
Keyboard.removeListener(keyboardShowEvent, onKeyboardOpenHandler);
201-
}
200+
keyboardSubscription?.remove();
202201
};
203202
}, [closePicker, selectedPicker, setSelectedPicker]);
204203

package/src/components/Channel/Channel.tsx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import React, { PropsWithChildren, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2-
import { KeyboardAvoidingViewProps, StyleSheet, Text, View } from 'react-native';
2+
import { StyleSheet, Text, View } from 'react-native';
33

44
import debounce from 'lodash/debounce';
55
import throttle from 'lodash/throttle';
@@ -150,7 +150,10 @@ import {
150150
LoadingErrorProps,
151151
} from '../Indicators/LoadingErrorIndicator';
152152
import { LoadingIndicator as LoadingIndicatorDefault } from '../Indicators/LoadingIndicator';
153-
import { KeyboardCompatibleView as KeyboardCompatibleViewDefault } from '../KeyboardCompatibleView/KeyboardCompatibleView';
153+
import {
154+
KeyboardCompatibleView as KeyboardCompatibleViewDefault,
155+
KeyboardCompatibleViewProps,
156+
} from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
154157
import { Message as MessageDefault } from '../Message/Message';
155158
import { MessageAvatar as MessageAvatarDefault } from '../Message/MessageSimple/MessageAvatar';
156159
import { MessageBlocked as MessageBlockedDefault } from '../Message/MessageSimple/MessageBlocked';
@@ -414,7 +417,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
414417
/**
415418
* Additional props passed to keyboard avoiding view
416419
*/
417-
additionalKeyboardAvoidingViewProps?: Partial<KeyboardAvoidingViewProps>;
420+
additionalKeyboardAvoidingViewProps?: Partial<KeyboardCompatibleViewProps>;
418421
/**
419422
* When true, disables the KeyboardCompatibleView wrapper
420423
*
@@ -470,7 +473,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
470473
* When true, messageList will be scrolled at first unread message, when opened.
471474
*/
472475
initialScrollToFirstUnreadMessage?: boolean;
473-
keyboardBehavior?: KeyboardAvoidingViewProps['behavior'];
476+
keyboardBehavior?: KeyboardCompatibleViewProps['behavior'];
474477
/**
475478
* Custom wrapper component that handles height adjustment of Channel component when keyboard is opened or dismissed
476479
* Default component (accepts the same props): [KeyboardCompatibleView](https://github.com/GetStream/stream-chat-react-native/blob/main/package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx)
@@ -490,7 +493,7 @@ export type ChannelPropsWithContext = Pick<ChannelContextValue, 'channel'> &
490493
* />
491494
* ```
492495
*/
493-
KeyboardCompatibleView?: React.ComponentType<KeyboardAvoidingViewProps>;
496+
KeyboardCompatibleView?: React.ComponentType<KeyboardCompatibleViewProps>;
494497
keyboardVerticalOffset?: number;
495498
/**
496499
* Custom loading error indicator to override the Stream default

package/src/components/ImageGallery/ImageGallery.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import React, { RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
2-
import { Image, ImageStyle, Keyboard, StyleSheet, ViewStyle } from 'react-native';
3-
2+
import { Image, ImageStyle, StyleSheet, ViewStyle } from 'react-native';
43
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
54

65
import Animated, {
@@ -50,6 +49,7 @@ import {
5049
BottomSheetModal,
5150
BottomSheetModalProvider,
5251
} from '../BottomSheetCompatibility/BottomSheetModal';
52+
import { dismissKeyboard } from '../KeyboardCompatibleView/KeyboardControllerAvoidingView';
5353

5454
const MARGIN = 32;
5555

@@ -177,7 +177,7 @@ export const ImageGallery = (props: Props) => {
177177
* Run the fade animation on visible change
178178
*/
179179
useEffect(() => {
180-
Keyboard.dismiss();
180+
dismissKeyboard();
181181
showScreen();
182182
// eslint-disable-next-line react-hooks/exhaustive-deps
183183
}, []);
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
3+
import {
4+
Keyboard,
5+
Platform,
6+
KeyboardAvoidingViewProps as ReactNativeKeyboardAvoidingViewProps,
7+
} from 'react-native';
8+
9+
import {
10+
KeyboardAvoidingView as KeyboardControllerPackageKeyboardAvoidingView,
11+
KeyboardController as KeyboardControllerPackageKeyboardController,
12+
KeyboardEvents,
13+
KeyboardProvider,
14+
} from 'react-native-keyboard-controller';
15+
16+
import { KeyboardCompatibleView as KeyboardCompatibleViewDefault } from './KeyboardCompatibleView';
17+
18+
type ExtraKeyboardControllerProps = {
19+
behavior?: 'translate-with-padding';
20+
};
21+
22+
export type KeyboardCompatibleViewProps = ReactNativeKeyboardAvoidingViewProps &
23+
ExtraKeyboardControllerProps;
24+
25+
let KeyboardControllerPackage:
26+
| {
27+
KeyboardAvoidingView: typeof KeyboardControllerPackageKeyboardAvoidingView;
28+
KeyboardController: typeof KeyboardControllerPackageKeyboardController;
29+
KeyboardProvider: typeof KeyboardProvider;
30+
KeyboardEvents: typeof KeyboardEvents;
31+
}
32+
| undefined;
33+
34+
try {
35+
KeyboardControllerPackage = require('react-native-keyboard-controller');
36+
} catch (e) {
37+
KeyboardControllerPackage = undefined;
38+
}
39+
40+
export const KeyboardCompatibleView = (props: KeyboardCompatibleViewProps) => {
41+
const { behavior = 'translate-with-padding', children, ...rest } = props;
42+
43+
const KeyboardProvider = KeyboardControllerPackage?.KeyboardProvider;
44+
const KeyboardAvoidingView = KeyboardControllerPackage?.KeyboardAvoidingView;
45+
46+
if (KeyboardProvider && KeyboardAvoidingView) {
47+
return (
48+
<KeyboardProvider>
49+
{/* @ts-expect-error - The reason is that react-native-keyboard-controller's KeyboardAvoidingViewProps is a discriminated union, not a simple behavior union so it complains about the `position` value passed. */}
50+
<KeyboardAvoidingView behavior={behavior} {...rest}>
51+
{children}
52+
</KeyboardAvoidingView>
53+
</KeyboardProvider>
54+
);
55+
}
56+
const compatibleBehavior =
57+
behavior === 'translate-with-padding'
58+
? Platform.OS === 'ios'
59+
? 'padding'
60+
: 'position'
61+
: behavior;
62+
63+
return (
64+
<KeyboardCompatibleViewDefault behavior={compatibleBehavior} {...rest}>
65+
{children}
66+
</KeyboardCompatibleViewDefault>
67+
);
68+
};
69+
70+
export const dismissKeyboard = () => {
71+
if (KeyboardControllerPackage?.KeyboardController) {
72+
KeyboardControllerPackage?.KeyboardController.dismiss();
73+
}
74+
Keyboard.dismiss();
75+
};
76+
77+
export { KeyboardControllerPackage };

0 commit comments

Comments
 (0)