react-native-gifted-chat 3.3.0 → 3.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -120,18 +120,15 @@ Follow the [react-native-reanimated installation guide](https://docs.swmansion.c
120
120
 
121
121
  ```jsx
122
122
  import React, { useState, useCallback, useEffect } from 'react'
123
- import { Platform } from 'react-native'
124
123
  import { GiftedChat } from 'react-native-gifted-chat'
125
- import { useSafeAreaInsets } from 'react-native-safe-area-context'
124
+ import { useHeaderHeight } from '@react-navigation/elements'
126
125
 
127
126
  export function Example() {
128
127
  const [messages, setMessages] = useState([])
129
- const insets = useSafeAreaInsets()
130
128
 
131
- // If you have a tab bar, include its height
132
- const tabbarHeight = 50
133
- const keyboardTopToolbarHeight = Platform.select({ ios: 44, default: 0 })
134
- const keyboardVerticalOffset = insets.bottom + tabbarHeight + keyboardTopToolbarHeight
129
+ // keyboardVerticalOffset = distance from screen top to GiftedChat container
130
+ // useHeaderHeight() returns status bar + navigation header height
131
+ const headerHeight = useHeaderHeight()
135
132
 
136
133
  useEffect(() => {
137
134
  setMessages([
@@ -161,8 +158,7 @@ export function Example() {
161
158
  user={{
162
159
  _id: 1,
163
160
  }}
164
-
165
- keyboardAvoidingViewProps={{ keyboardVerticalOffset }}
161
+ keyboardAvoidingViewProps={{ keyboardVerticalOffset: headerHeight }}
166
162
  />
167
163
  )
168
164
  }
@@ -227,10 +223,42 @@ interface User {
227
223
  - **`keyboardProviderProps`** _(Object)_ - Props to be passed to the [`KeyboardProvider`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/keyboard-provider) for keyboard handling. Default values:
228
224
  - `statusBarTranslucent: true` - Required on Android for correct keyboard height calculation when status bar is translucent (edge-to-edge mode)
229
225
  - `navigationBarTranslucent: true` - Required on Android for correct keyboard height calculation when navigation bar is translucent (edge-to-edge mode)
230
- - **`keyboardAvoidingViewProps`** _(Object)_ - Props to be passed to the [`KeyboardAvoidingView`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-avoiding-view). Use `keyboardVerticalOffset` to account for headers or iOS predictive text bar (~50pt).
226
+ - **`keyboardAvoidingViewProps`** _(Object)_ - Props to be passed to the [`KeyboardAvoidingView`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-avoiding-view). See **keyboardVerticalOffset** below for proper keyboard handling.
231
227
  - **`isAlignedTop`** _(Boolean)_ Controls whether or not the message bubbles appear at the top of the chat (Default is false - bubbles align to bottom)
232
228
  - **`isInverted`** _(Bool)_ - Reverses display order of `messages`; default is `true`
233
229
 
230
+ #### Understanding `keyboardVerticalOffset`
231
+
232
+ The [`keyboardVerticalOffset`](https://kirillzyusko.github.io/react-native-keyboard-controller/docs/api/components/keyboard-avoiding-view#keyboardverticaloffset) tells the KeyboardAvoidingView where its container starts relative to the top of the screen. This is essential when GiftedChat is not positioned at the very top of the screen (e.g., when you have a navigation header).
233
+
234
+ **Default value:** `insets.top` (status bar height from `useSafeAreaInsets()`). This works correctly only when GiftedChat fills the entire screen without a navigation header. If you have a navigation header, you need to pass the correct offset via `keyboardAvoidingViewProps`.
235
+
236
+ **What the value means:** The offset equals the distance (in points) from the top of the screen to the top of your GiftedChat container. This typically includes:
237
+ - Status bar height
238
+ - Navigation header height (on iOS, `useHeaderHeight()` already includes status bar)
239
+
240
+ **How to use:**
241
+
242
+ ```jsx
243
+ import { useHeaderHeight } from '@react-navigation/elements'
244
+
245
+ function ChatScreen() {
246
+ // useHeaderHeight() returns status bar + navigation header height on iOS
247
+ const headerHeight = useHeaderHeight()
248
+
249
+ return (
250
+ <GiftedChat
251
+ keyboardAvoidingViewProps={{ keyboardVerticalOffset: headerHeight }}
252
+ // ... other props
253
+ />
254
+ )
255
+ }
256
+ ```
257
+
258
+ > **Note:** `useHeaderHeight()` requires your chat component to be rendered inside a proper navigation screen (not conditional rendering). If it returns `0`, ensure your chat screen is a real navigation screen with a visible header.
259
+
260
+ **Why this matters:** Without the correct offset, the keyboard may overlap the input field or leave extra space. The KeyboardAvoidingView uses this value to calculate how much to shift the content when the keyboard appears.
261
+
234
262
  ### Text Input & Composer
235
263
 
236
264
  - **`text`** _(String)_ - Input text; default is `undefined`, but if specified, it will override GiftedChat's internal state. Useful for managing text state outside of GiftedChat (e.g. with Redux). Don't forget to implement `textInputProps.onChangeText` to update the text state.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-gifted-chat",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "description": "The most complete chat UI for React Native",
5
5
  "keywords": [
6
6
  "android",
@@ -92,11 +92,11 @@
92
92
  "react": "19.1.0",
93
93
  "react-dom": "19.1.0",
94
94
  "react-native": "0.81.5",
95
- "react-native-gesture-handler": "~2.28.0",
96
- "react-native-keyboard-controller": "1.18.5",
97
- "react-native-reanimated": "~4.1.1",
95
+ "react-native-gesture-handler": "~2.30.0",
96
+ "react-native-keyboard-controller": "1.20.6",
97
+ "react-native-reanimated": "~4.2.1",
98
98
  "react-native-safe-area-context": "~5.6.2",
99
- "react-native-worklets": "0.5.1",
99
+ "react-native-worklets": "0.7.2",
100
100
  "react-test-renderer": "19.1.0",
101
101
  "typescript": "^5.9.3"
102
102
  },
@@ -8,7 +8,6 @@ import React, {
8
8
  RefObject,
9
9
  } from 'react'
10
10
  import {
11
- Platform,
12
11
  View,
13
12
  LayoutChangeEvent,
14
13
  useColorScheme,
@@ -21,7 +20,7 @@ import dayjs from 'dayjs'
21
20
  import localizedFormat from 'dayjs/plugin/localizedFormat'
22
21
  import { GestureHandlerRootView, TextInput } from 'react-native-gesture-handler'
23
22
  import { KeyboardAvoidingView, KeyboardProvider } from 'react-native-keyboard-controller'
24
- import { SafeAreaProvider } from 'react-native-safe-area-context'
23
+ import { SafeAreaProvider, useSafeAreaInsets } from 'react-native-safe-area-context'
25
24
  import { TEST_ID } from '../Constant'
26
25
  import { GiftedChatContext } from '../GiftedChatContext'
27
26
  import { InputToolbar } from '../InputToolbar'
@@ -34,23 +33,6 @@ import { GiftedChatProps } from './types'
34
33
 
35
34
  dayjs.extend(localizedFormat)
36
35
 
37
- /**
38
- * Default keyboard vertical offset values (similar to Stream Chat SDK)
39
- * iOS: Compensates for predictive/suggestion text bar (~44-50pt) and headers
40
- * Android: Negative offset to account for navigation bar in edge-to-edge mode
41
- */
42
- const DEFAULT_KEYBOARD_VERTICAL_OFFSET = Platform.select({
43
- ios: 50,
44
- android: 0,
45
- default: 0,
46
- })
47
-
48
- const DEFAULT_KEYBOARD_BEHAVIOR = Platform.select({
49
- ios: 'padding' as const,
50
- android: 'padding' as const,
51
- default: 'padding' as const,
52
- })
53
-
54
36
  function GiftedChat<TMessage extends IMessage = IMessage> (
55
37
  props: GiftedChatProps<TMessage>
56
38
  ) {
@@ -92,6 +74,8 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
92
74
 
93
75
  const actionSheetRef = useRef<ActionSheetProviderRef>(null)
94
76
 
77
+ const insets = useSafeAreaInsets()
78
+
95
79
  const messagesContainerRef = useMemo(
96
80
  () => props.messagesContainerRef || createRef<AnimatedList<TMessage>>(),
97
81
  [props.messagesContainerRef]
@@ -344,23 +328,16 @@ function GiftedChat<TMessage extends IMessage = IMessage> (
344
328
  >
345
329
  {/* @ts-expect-error */}
346
330
  <KeyboardAvoidingView
347
- behavior={DEFAULT_KEYBOARD_BEHAVIOR}
348
- keyboardVerticalOffset={DEFAULT_KEYBOARD_VERTICAL_OFFSET}
331
+ behavior='translate-with-padding'
332
+ keyboardVerticalOffset={insets.top}
349
333
  style={stylesCommon.fill}
350
334
  {...props.keyboardAvoidingViewProps}
351
335
  >
352
- <View style={stylesCommon.fill}>
353
- {isInitialized
354
- ? (
355
- <>
356
- {renderMessages}
357
- {inputToolbarFragment}
358
- </>
359
- )
360
- : (
361
- renderComponentOrElement(renderLoading, {})
362
- )}
336
+ <View style={[stylesCommon.fill, !isInitialized && styles.hidden]}>
337
+ {renderMessages}
338
+ {inputToolbarFragment}
363
339
  </View>
340
+ {!isInitialized && renderComponentOrElement(renderLoading, {})}
364
341
  </KeyboardAvoidingView>
365
342
  </View>
366
343
  </ActionSheetProvider>
@@ -7,4 +7,7 @@ export default StyleSheet.create({
7
7
  contentContainer: {
8
8
  overflow: 'hidden',
9
9
  },
10
+ hidden: {
11
+ opacity: 0,
12
+ },
10
13
  })
@@ -34,8 +34,8 @@ exports[`should render <GiftedChat/> and compare with snapshot 1`] = `
34
34
  testID="GC_WRAPPER"
35
35
  >
36
36
  <View
37
- behavior="padding"
38
- keyboardVerticalOffset={50}
37
+ behavior="translate-with-padding"
38
+ keyboardVerticalOffset={0}
39
39
  style={
40
40
  {
41
41
  "flex": 1,
@@ -44,9 +44,14 @@ exports[`should render <GiftedChat/> and compare with snapshot 1`] = `
44
44
  >
45
45
  <View
46
46
  style={
47
- {
48
- "flex": 1,
49
- }
47
+ [
48
+ {
49
+ "flex": 1,
50
+ },
51
+ {
52
+ "opacity": 0,
53
+ },
54
+ ]
50
55
  }
51
56
  />
52
57
  </View>
@@ -90,8 +95,8 @@ exports[`should render <GiftedChat/> with dark colorScheme and compare with snap
90
95
  testID="GC_WRAPPER"
91
96
  >
92
97
  <View
93
- behavior="padding"
94
- keyboardVerticalOffset={50}
98
+ behavior="translate-with-padding"
99
+ keyboardVerticalOffset={0}
95
100
  style={
96
101
  {
97
102
  "flex": 1,
@@ -100,9 +105,14 @@ exports[`should render <GiftedChat/> with dark colorScheme and compare with snap
100
105
  >
101
106
  <View
102
107
  style={
103
- {
104
- "flex": 1,
105
- }
108
+ [
109
+ {
110
+ "flex": 1,
111
+ },
112
+ {
113
+ "opacity": 0,
114
+ },
115
+ ]
106
116
  }
107
117
  />
108
118
  </View>
@@ -146,8 +156,8 @@ exports[`should render <GiftedChat/> with light colorScheme and compare with sna
146
156
  testID="GC_WRAPPER"
147
157
  >
148
158
  <View
149
- behavior="padding"
150
- keyboardVerticalOffset={50}
159
+ behavior="translate-with-padding"
160
+ keyboardVerticalOffset={0}
151
161
  style={
152
162
  {
153
163
  "flex": 1,
@@ -156,9 +166,14 @@ exports[`should render <GiftedChat/> with light colorScheme and compare with sna
156
166
  >
157
167
  <View
158
168
  style={
159
- {
160
- "flex": 1,
161
- }
169
+ [
170
+ {
171
+ "flex": 1,
172
+ },
173
+ {
174
+ "opacity": 0,
175
+ },
176
+ ]
162
177
  }
163
178
  />
164
179
  </View>
@@ -73,28 +73,6 @@ exports[`should render <InputToolbar /> and compare with snapshot 1`] = `
73
73
  />
74
74
  </View>
75
75
  <View
76
- collapsable={false}
77
- jestAnimatedProps={
78
- {
79
- "value": {},
80
- }
81
- }
82
- jestAnimatedStyle={
83
- {
84
- "value": {
85
- "opacity": 0,
86
- },
87
- }
88
- }
89
- jestInlineStyle={
90
- [
91
- {
92
- "justifyContent": "flex-end",
93
- },
94
- undefined,
95
- ]
96
- }
97
- nativeID="0"
98
76
  pointerEvents="none"
99
77
  style={
100
78
  [
@@ -34,38 +34,6 @@ exports[`MessageImage should render <MessageImage /> and compare with snapshot
34
34
  visible={false}
35
35
  >
36
36
  <View
37
- collapsable={false}
38
- jestAnimatedProps={
39
- {
40
- "value": {},
41
- }
42
- }
43
- jestAnimatedStyle={
44
- {
45
- "value": {
46
- "borderRadius": 40,
47
- "opacity": 0,
48
- "transform": [
49
- {
50
- "scale": 0.9,
51
- },
52
- ],
53
- },
54
- }
55
- }
56
- jestInlineStyle={
57
- [
58
- {
59
- "bottom": 0,
60
- "left": 0,
61
- "position": "absolute",
62
- "right": 0,
63
- "top": 0,
64
- "zIndex": 1000,
65
- },
66
- ]
67
- }
68
- nativeID="0"
69
37
  style={
70
38
  [
71
39
  {
@@ -98,35 +66,6 @@ exports[`MessageImage should render <MessageImage /> and compare with snapshot
98
66
  }
99
67
  >
100
68
  <View
101
- collapsable={false}
102
- jestAnimatedProps={
103
- {
104
- "value": {},
105
- }
106
- }
107
- jestAnimatedStyle={
108
- {
109
- "value": {
110
- "borderRadius": 40,
111
- },
112
- }
113
- }
114
- jestInlineStyle={
115
- [
116
- {
117
- "flex": 1,
118
- },
119
- {
120
- "backgroundColor": "#000",
121
- "overflow": "hidden",
122
- },
123
- {
124
- "paddingBottom": 0,
125
- "paddingTop": 0,
126
- },
127
- ]
128
- }
129
- nativeID="1"
130
69
  style={
131
70
  [
132
71
  {
@@ -222,35 +161,6 @@ exports[`MessageImage should render <MessageImage /> and compare with snapshot
222
161
  }
223
162
  >
224
163
  <View
225
- collapsable={false}
226
- jestAnimatedProps={
227
- {
228
- "value": {},
229
- }
230
- }
231
- jestAnimatedStyle={
232
- {
233
- "value": {
234
- "transform": [
235
- {
236
- "translateX": 0,
237
- },
238
- {
239
- "translateY": 0,
240
- },
241
- {
242
- "scale": 1,
243
- },
244
- ],
245
- },
246
- }
247
- }
248
- jestInlineStyle={
249
- [
250
- undefined,
251
- ]
252
- }
253
- nativeID="2"
254
164
  onLayout={[Function]}
255
165
  style={
256
166
  [
@@ -2,44 +2,17 @@
2
2
 
3
3
  exports[`should render <ReplyPreview /> and compare with snapshot 1`] = `
4
4
  <View
5
- collapsable={false}
6
- jestAnimatedProps={
7
- {
8
- "value": {},
9
- }
10
- }
11
- jestAnimatedStyle={
12
- {
13
- "value": {
14
- "height": 0,
15
- "opacity": 0,
16
- "transform": [
17
- {
18
- "translateY": 10,
19
- },
20
- ],
21
- },
22
- }
23
- }
24
- jestInlineStyle={
25
- [
26
- {
27
- "overflow": "hidden",
28
- },
29
- ]
30
- }
31
- nativeID="0"
32
5
  style={
33
6
  [
34
7
  {
35
8
  "overflow": "hidden",
36
9
  },
37
10
  {
38
- "height": 0,
39
- "opacity": 0,
11
+ "height": undefined,
12
+ "opacity": undefined,
40
13
  "transform": [
41
14
  {
42
- "translateY": 10,
15
+ "translateY": undefined,
43
16
  },
44
17
  ],
45
18
  },
@@ -194,44 +167,17 @@ exports[`should render <ReplyPreview /> and compare with snapshot 1`] = `
194
167
 
195
168
  exports[`should render <ReplyPreview /> with image and compare with snapshot 1`] = `
196
169
  <View
197
- collapsable={false}
198
- jestAnimatedProps={
199
- {
200
- "value": {},
201
- }
202
- }
203
- jestAnimatedStyle={
204
- {
205
- "value": {
206
- "height": 0,
207
- "opacity": 0,
208
- "transform": [
209
- {
210
- "translateY": 10,
211
- },
212
- ],
213
- },
214
- }
215
- }
216
- jestInlineStyle={
217
- [
218
- {
219
- "overflow": "hidden",
220
- },
221
- ]
222
- }
223
- nativeID="1"
224
170
  style={
225
171
  [
226
172
  {
227
173
  "overflow": "hidden",
228
174
  },
229
175
  {
230
- "height": 0,
231
- "opacity": 0,
176
+ "height": undefined,
177
+ "opacity": undefined,
232
178
  "transform": [
233
179
  {
234
- "translateY": 10,
180
+ "translateY": undefined,
235
181
  },
236
182
  ],
237
183
  },
@@ -2,28 +2,6 @@
2
2
 
3
3
  exports[`Send should always render <Send /> and compare with snapshot 1`] = `
4
4
  <View
5
- collapsable={false}
6
- jestAnimatedProps={
7
- {
8
- "value": {},
9
- }
10
- }
11
- jestAnimatedStyle={
12
- {
13
- "value": {
14
- "opacity": 0,
15
- },
16
- }
17
- }
18
- jestInlineStyle={
19
- [
20
- {
21
- "justifyContent": "flex-end",
22
- },
23
- undefined,
24
- ]
25
- }
26
- nativeID="1"
27
5
  pointerEvents="auto"
28
6
  style={
29
7
  [
@@ -73,28 +51,6 @@ exports[`Send should always render <Send /> and compare with snapshot 1`] = `
73
51
 
74
52
  exports[`Send should not render <Send /> and compare with snapshot 1`] = `
75
53
  <View
76
- collapsable={false}
77
- jestAnimatedProps={
78
- {
79
- "value": {},
80
- }
81
- }
82
- jestAnimatedStyle={
83
- {
84
- "value": {
85
- "opacity": 0,
86
- },
87
- }
88
- }
89
- jestInlineStyle={
90
- [
91
- {
92
- "justifyContent": "flex-end",
93
- },
94
- undefined,
95
- ]
96
- }
97
- nativeID="0"
98
54
  pointerEvents="none"
99
55
  style={
100
56
  [
@@ -144,28 +100,6 @@ exports[`Send should not render <Send /> and compare with snapshot 1`] = `
144
100
 
145
101
  exports[`Send should render <Send /> where there is input and compare with snapshot 1`] = `
146
102
  <View
147
- collapsable={false}
148
- jestAnimatedProps={
149
- {
150
- "value": {},
151
- }
152
- }
153
- jestAnimatedStyle={
154
- {
155
- "value": {
156
- "opacity": 0,
157
- },
158
- }
159
- }
160
- jestInlineStyle={
161
- [
162
- {
163
- "justifyContent": "flex-end",
164
- },
165
- undefined,
166
- ]
167
- }
168
- nativeID="2"
169
103
  pointerEvents="auto"
170
104
  style={
171
105
  [
package/src/utils.ts CHANGED
@@ -27,6 +27,11 @@ export function renderComponentOrElement<TProps extends Record<string, any>>(
27
27
  return (component as (props: TProps) => React.ReactNode)(props)
28
28
  }
29
29
 
30
+ // Check for React.memo or React.forwardRef wrapped components
31
+ // These have $$typeof property and should be rendered with createElement
32
+ if (typeof component === 'object' && component !== null && '$$typeof' in component)
33
+ return React.createElement(component as React.ComponentType<TProps>, props as any)
34
+
30
35
  // If it's neither, return it as-is
31
36
  return component
32
37
  }