Skip to content

Commit 21ddd9d

Browse files
committed
Merge branch 'develop' into V8
# Conflicts: # examples/SampleApp/src/screens/ThreadScreen.tsx # examples/SampleApp/yarn.lock # package/expo-package/package.json
2 parents 17378ca + 241d1db commit 21ddd9d

File tree

11 files changed

+198
-17
lines changed

11 files changed

+198
-17
lines changed

examples/ExpoMessaging/app.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,8 @@
6262
{
6363
"microphonePermission": "$(PRODUCT_NAME) would like to use your microphone for voice recording."
6464
}
65-
]
65+
],
66+
"./plugins/keyboardInsetMainActivityListener.js"
6667
]
6768
}
6869
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
const { withMainActivity, createRunOncePlugin } = require('@expo/config-plugins');
2+
3+
const requiredImports = [
4+
'import android.os.Build',
5+
'import android.os.Bundle',
6+
'import android.view.View',
7+
'import androidx.core.view.ViewCompat',
8+
'import androidx.core.view.WindowInsetsCompat',
9+
'import androidx.core.view.updatePadding',
10+
];
11+
12+
const customInsetHandler = `
13+
if (Build.VERSION.SDK_INT >= 35) {
14+
val rootView = findViewById<View>(android.R.id.content)
15+
16+
ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
17+
val bars = insets.getInsets(
18+
WindowInsetsCompat.Type.systemBars()
19+
or WindowInsetsCompat.Type.displayCutout()
20+
or WindowInsetsCompat.Type.ime()
21+
)
22+
rootView.updatePadding(
23+
left = bars.left,
24+
top = bars.top,
25+
right = bars.right,
26+
bottom = bars.bottom
27+
)
28+
WindowInsetsCompat.CONSUMED
29+
}
30+
}
31+
`;
32+
33+
const withCustomMainActivity = (config) => {
34+
return withMainActivity(config, (mod) => {
35+
if (mod.modResults.language !== 'kt') {
36+
throw new Error('MainActivity must be written in Kotlin for this plugin.');
37+
}
38+
39+
let contents = mod.modResults.contents;
40+
41+
// Add missing imports
42+
const packageLineMatch = contents.match(/^package\s+[^\n]+\n/);
43+
if (packageLineMatch) {
44+
const packageLine = packageLineMatch[0];
45+
for (const imp of requiredImports) {
46+
if (!contents.includes(imp)) {
47+
contents = contents.replace(packageLine, packageLine + imp + '\n');
48+
}
49+
}
50+
}
51+
52+
// Inject inside onCreate(), right after super.onCreate(null)
53+
// Match the full onCreate method
54+
const onCreateMethodRegex = /override fun onCreate\(savedInstanceState: Bundle\?\) \{([\s\S]*?)^\s*}/m;
55+
56+
// If the method exists and doesn't already contain a custom ViewCompat.setOnApplyWindowInsetsListener, inject it
57+
if (onCreateMethodRegex.test(contents) && !contents.includes('ViewCompat.setOnApplyWindowInsetsListener')) {
58+
contents = contents.replace(onCreateMethodRegex, (match, body) => {
59+
// Inject after super.onCreate(null)
60+
const modifiedBody = body.replace(
61+
/super\.onCreate\(null\);?/,
62+
(superLine) => `${superLine}\n${customInsetHandler}`
63+
);
64+
65+
return `override fun onCreate(savedInstanceState: Bundle?) {\n${modifiedBody}\n}`;
66+
});
67+
}
68+
69+
mod.modResults.contents = contents;
70+
return mod;
71+
});
72+
};
73+
74+
module.exports = createRunOncePlugin(
75+
withCustomMainActivity,
76+
'keyboard-inset-main-activity-listener-plugin',
77+
'0.0.1'
78+
);

examples/SampleApp/android/app/src/main/java/com/sampleapp/MainActivity.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,38 @@ import com.facebook.react.ReactActivityDelegate
55
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
66
import com.facebook.react.defaults.DefaultReactActivityDelegate
77

8+
import android.os.Bundle
9+
import android.os.Build
10+
import android.view.View
11+
import androidx.core.view.ViewCompat
12+
import androidx.core.view.WindowInsetsCompat
13+
import androidx.core.view.updatePadding
14+
815
class MainActivity : ReactActivity() {
16+
17+
override fun onCreate(savedInstanceState: Bundle?) {
18+
super.onCreate(null)
19+
20+
if (Build.VERSION.SDK_INT >= 35) {
21+
val rootView = findViewById<View>(android.R.id.content)
22+
23+
24+
ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
25+
val bars = insets.getInsets(
26+
WindowInsetsCompat.Type.systemBars()
27+
or WindowInsetsCompat.Type.displayCutout()
28+
or WindowInsetsCompat.Type.ime() // adding the ime's height
29+
)
30+
rootView.updatePadding(
31+
left = bars.left,
32+
top = bars.top,
33+
right = bars.right,
34+
bottom = bars.bottom
35+
)
36+
WindowInsetsCompat.CONSUMED
37+
}
38+
}
39+
}
940
/**
1041
* Returns the name of the main component registered from JavaScript. This is used to schedule
1142
* rendering of the component.
@@ -18,4 +49,4 @@ class MainActivity : ReactActivity() {
1849
*/
1950
override fun createReactActivityDelegate(): ReactActivityDelegate =
2051
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
21-
}
52+
}

examples/SampleApp/src/screens/ThreadScreen.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
2-
import { StyleSheet, View } from 'react-native';
1+
import React, { useEffect } from 'react';
2+
import { Platform, StyleSheet, View } from 'react-native';
33
import { SafeAreaView } from 'react-native-safe-area-context';
44
import { Channel, Thread, ThreadType, useTheme, useTypingString } from 'stream-chat-react-native';
55
import { useStateStore } from 'stream-chat-react-native';
@@ -68,7 +68,7 @@ export const ThreadScreen: React.FC<ThreadScreenProps> = ({
6868
audioRecordingEnabled={true}
6969
channel={channel}
7070
enforceUniqueReaction
71-
keyboardVerticalOffset={0}
71+
keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : -300}
7272
thread={thread}
7373
threadList
7474
>

examples/TypeScriptMessaging/android/app/src/main/java/com/typescriptmessaging/MainActivity.kt

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,38 @@ import com.facebook.react.ReactActivityDelegate
55
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
66
import com.facebook.react.defaults.DefaultReactActivityDelegate
77

8+
import android.os.Bundle
9+
import android.os.Build
10+
import android.view.View
11+
import androidx.core.view.ViewCompat
12+
import androidx.core.view.WindowInsetsCompat
13+
import androidx.core.view.updatePadding
14+
815
class MainActivity : ReactActivity() {
16+
17+
override fun onCreate(savedInstanceState: Bundle?) {
18+
super.onCreate(null)
19+
20+
if (Build.VERSION.SDK_INT >= 35) {
21+
val rootView = findViewById<View>(android.R.id.content)
22+
23+
24+
ViewCompat.setOnApplyWindowInsetsListener(rootView) { view, insets ->
25+
val bars = insets.getInsets(
26+
WindowInsetsCompat.Type.systemBars()
27+
or WindowInsetsCompat.Type.displayCutout()
28+
or WindowInsetsCompat.Type.ime() // adding the ime's height
29+
)
30+
rootView.updatePadding(
31+
left = bars.left,
32+
top = bars.top,
33+
right = bars.right,
34+
bottom = bars.bottom
35+
)
36+
WindowInsetsCompat.CONSUMED
37+
}
38+
}
39+
}
940
/**
1041
* Returns the name of the main component registered from JavaScript. This is used to schedule
1142
* rendering of the component.
@@ -18,4 +49,4 @@ class MainActivity : ReactActivity() {
1849
*/
1950
override fun createReactActivityDelegate(): ReactActivityDelegate =
2051
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
21-
}
52+
}

package/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Change Log
22

3+
## [7.2.0](https://github.com/GetStream/stream-chat-react-native/compare/v7.1.2...v7.2.0) (2025-06-23)
4+
5+
6+
### Features
7+
8+
* add support for `expo-audio` ([#3126](https://github.com/GetStream/stream-chat-react-native/issues/3126)) ([e1f555f](https://github.com/GetStream/stream-chat-react-native/commit/e1f555fc4a59bf7514332bf0ba724c49a98288c1))
9+
310
### [7.1.2](https://github.com/GetStream/stream-chat-react-native/compare/v7.1.1...v7.1.2) (2025-06-23)
411

512

package/expo-package/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stream-chat-expo",
33
"description": "The official Expo SDK for Stream Chat, a service for building chat applications",
4-
"version": "7.1.2",
4+
"version": "7.2.0",
55
"author": {
66
"company": "Stream.io Inc",
77
"name": "Stream.io Inc"
@@ -11,7 +11,7 @@
1111
"types": "types/index.d.ts",
1212
"dependencies": {
1313
"mime": "^4.0.7",
14-
"stream-chat-react-native-core": "link:../"
14+
"stream-chat-react-native-core": "7.2.0"
1515
},
1616
"peerDependencies": {
1717
"expo": ">=51.0.0",

package/native-package/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stream-chat-react-native",
33
"description": "The official React Native SDK for Stream Chat, a service for building chat applications",
4-
"version": "7.1.2",
4+
"version": "7.2.0",
55
"homepage": "https://www.npmjs.com/package/stream-chat-react-native",
66
"author": {
77
"company": "Stream.io Inc",
@@ -21,7 +21,7 @@
2121
"dependencies": {
2222
"es6-symbol": "^3.1.3",
2323
"mime": "^4.0.7",
24-
"stream-chat-react-native-core": "7.1.2"
24+
"stream-chat-react-native-core": "7.2.0"
2525
},
2626
"peerDependencies": {
2727
"@react-native-camera-roll/camera-roll": ">=7.8.0",

package/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stream-chat-react-native-core",
33
"description": "The official React Native and Expo components for Stream Chat, a service for building chat applications",
4-
"version": "7.1.2",
4+
"version": "7.2.0",
55
"author": {
66
"company": "Stream.io Inc",
77
"name": "Stream.io Inc"

package/src/components/KeyboardCompatibleView/KeyboardCompatibleView.tsx

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import {
2121

2222
import { KeyboardProvider } from '../../contexts/keyboardContext/KeyboardContext';
2323

24+
type State = {
25+
bottom: number;
26+
};
27+
2428
/**
2529
* View that moves out of the way when the keyboard appears by automatically
2630
* adjusting its height, position, or bottom padding.
@@ -46,6 +50,8 @@ export class KeyboardCompatibleView extends React.Component<
4650
_appStateSubscription: NativeEventSubscription | null = null;
4751
viewRef: React.RefObject<View | null>;
4852
_initialFrameHeight = 0;
53+
_bottom: number = 0;
54+
4955
constructor(props: KeyboardAvoidingViewProps) {
5056
super(props);
5157
this.state = {
@@ -93,28 +99,49 @@ export class KeyboardCompatibleView extends React.Component<
9399
};
94100

95101
_onLayout: (event: LayoutChangeEvent) => void = (event) => {
102+
event.persist();
103+
104+
const oldFrame = this._frame;
96105
this._frame = event.nativeEvent.layout;
97106
if (!this._initialFrameHeight) {
98107
// save the initial frame height, before the keyboard is visible
99108
this._initialFrameHeight = this._frame.height;
100109
}
101110

102-
this._updateBottomIfNecessary();
111+
// update bottom height for the first time or when the height is changed
112+
if (!oldFrame || oldFrame.height !== this._frame.height) {
113+
this._updateBottomIfNecessary();
114+
}
115+
116+
if (this.props.onLayout) {
117+
this.props.onLayout(event);
118+
}
119+
};
120+
121+
// Avoid unnecessary renders if the KeyboardAvoidingView is disabled.
122+
_setBottom = (value: number) => {
123+
const enabled = this.props.enabled ?? true;
124+
this._bottom = value;
125+
if (enabled) {
126+
this.setState({ bottom: value });
127+
}
103128
};
104129

105130
_updateBottomIfNecessary = () => {
106-
if (this._keyboardEvent === null) {
107-
this.setState({ bottom: 0 });
131+
if (this._keyboardEvent == null) {
132+
this._setBottom(0);
108133
return;
109134
}
110135

111136
const { duration, easing, endCoordinates } = this._keyboardEvent;
112137
const height = this._relativeKeyboardHeight(endCoordinates);
113138

114-
if (this.state.bottom === height) {
139+
if (this._bottom === height) {
115140
return;
116141
}
117142

143+
this._setBottom(height);
144+
118145
if (duration && easing) {
119146
LayoutAnimation.configureNext({
120147
// We have to pass the duration equal to minimal accepted duration defined here: RCTLayoutAnimation.m
@@ -125,7 +152,6 @@ export class KeyboardCompatibleView extends React.Component<
125152
},
126153
});
127154
}
128-
this.setState({ bottom: height });
129155
};
130156

131157
_handleAppStateChange = (nextAppState: AppStateStatus) => {
@@ -184,6 +210,13 @@ export class KeyboardCompatibleView extends React.Component<
184210
});
185211
};
186212

213+
componentDidUpdate(_: KeyboardAvoidingViewProps, prevState: State): void {
214+
const enabled = this.props.enabled ?? true;
215+
if (enabled && this._bottom !== prevState.bottom) {
216+
this.setState({ bottom: this._bottom });
217+
}
218+
}
219+
187220
componentDidMount() {
188221
this._appStateSubscription = AppState.addEventListener('change', this._handleAppStateChange);
189222
this.setKeyboardListeners();

0 commit comments

Comments
 (0)