Skip to content

Commit 3431f29

Browse files
Properly handle android system bars (#9242) (#9244)
(cherry picked from commit c0eaeef) Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
1 parent c308a9a commit 3431f29

File tree

6 files changed

+136
-14
lines changed

6 files changed

+136
-14
lines changed

android/app/src/main/java/com/mattermost/rnbeta/MainActivity.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.mattermost.rnbeta
33
import android.content.res.Configuration
44
import android.os.Bundle
55
import android.view.KeyEvent
6+
import androidx.core.view.WindowCompat
67
import com.facebook.react.ReactActivityDelegate
78
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
89
import com.facebook.react.defaults.DefaultReactActivityDelegate
@@ -37,6 +38,7 @@ class MainActivity : NavigationActivity() {
3738
setHWKeyboardConnected()
3839
lastOrientation = this.resources.configuration.orientation
3940
foldableObserver.onCreate()
41+
WindowCompat.setDecorFitsSystemWindows(window, false)
4042
}
4143

4244
override fun onStart() {

android/app/src/main/res/values-v35/styles.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,14 @@
77
<item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
88
<item name="android:windowOptOutEdgeToEdgeEnforcement">true</item>
99

10+
11+
<item name="android:navigationBarDividerColor">@android:color/transparent</item>
12+
13+
<!-- Avoid forced contrast paddings on some OEMs -->
14+
<item name="android:enforceStatusBarContrast">false</item>
15+
<item name="android:enforceNavigationBarContrast">false</item>
16+
17+
<!-- Support cutouts -->
18+
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
1019
</style>
1120
</resources>

app/screens/navigation.ts

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@
33

44
/* eslint-disable max-lines */
55

6+
import RNUtils from '@mattermost/rnutils';
67
import merge from 'deepmerge';
7-
import {Appearance, DeviceEventEmitter, StatusBar, Platform, Alert, type EmitterSubscription, Keyboard} from 'react-native';
8+
import {Appearance, DeviceEventEmitter, Platform, Alert, type EmitterSubscription, Keyboard, StatusBar} from 'react-native';
89
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription} from 'react-native-navigation';
910
import tinyColor from 'tinycolor2';
1011

@@ -36,6 +37,23 @@ let subscriptions: Array<EmitterSubscription | EventSubscription> | undefined;
3637
export const allOrientations: LayoutOrientation[] = ['sensor', 'sensorLandscape', 'sensorPortrait', 'landscape', 'portrait'];
3738
export const portraitOrientation: LayoutOrientation[] = ['portrait'];
3839

40+
const loginFlowScreens = new Set<AvailableScreens>([
41+
Screens.ONBOARDING,
42+
Screens.SERVER,
43+
Screens.LOGIN,
44+
Screens.SSO,
45+
Screens.MFA,
46+
Screens.FORGOT_PASSWORD,
47+
]);
48+
49+
function setNavigationBarColor(screen: AvailableScreens, th?: Theme) {
50+
if (Platform.OS === 'android' && Platform.Version >= 34) {
51+
const theme = th || getThemeFromState();
52+
const color = loginFlowScreens.has(screen) ? theme.sidebarBg : theme.centerChannelBg;
53+
RNUtils.setNavigationBarColor(color, tinyColor(color).isLight());
54+
}
55+
}
56+
3957
export function registerNavigationListeners() {
4058
subscriptions?.forEach((v) => v.remove());
4159
subscriptions = [
@@ -88,14 +106,19 @@ function onCommandListener(name: string, params: any) {
88106
}
89107
}
90108

91-
if (NavigationStore.getVisibleScreen() === Screens.HOME) {
109+
const screen = NavigationStore.getVisibleScreen();
110+
if (screen === Screens.HOME) {
92111
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
93112
}
113+
114+
setNavigationBarColor(screen);
94115
}
95116

96117
function onPoppedListener({componentId}: ScreenPoppedEvent) {
97118
// screen pop does not trigger registerCommandListener, but does trigger screenPoppedListener
98-
NavigationStore.removeScreenFromStack(componentId as AvailableScreens);
119+
const screen = componentId as AvailableScreens;
120+
NavigationStore.removeScreenFromStack(screen);
121+
setNavigationBarColor(screen);
99122
}
100123

101124
function onScreenWillAppear(event: ComponentWillAppearEvent) {
@@ -279,15 +302,40 @@ function isScreenRegistered(screen: AvailableScreens) {
279302
return true;
280303
}
281304

305+
function edgeToEdgeHack(screen: AvailableScreens, theme: Theme) {
306+
const isDark = tinyColor(theme.sidebarBg).isDark();
307+
308+
if (Platform.OS === 'android') {
309+
if (Platform.Version >= 34) {
310+
const listener = Navigation.events().registerComponentDidAppearListener((event) => {
311+
if (event.componentName === screen) {
312+
setNavigationBarColor(screen, theme);
313+
listener.remove();
314+
}
315+
});
316+
}
317+
318+
if (Platform.Version >= 36) {
319+
return {
320+
drawBehind: true,
321+
translucent: false,
322+
isDark,
323+
};
324+
}
325+
}
326+
327+
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
328+
return {isDark};
329+
}
330+
282331
export function openToS() {
283332
NavigationStore.setToSOpen(true);
284333
return showOverlay(Screens.TERMS_OF_SERVICE, {}, {overlay: {interceptTouchOutside: true}});
285334
}
286335

287336
export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}) {
288337
const theme = getThemeFromState();
289-
const isDark = tinyColor(theme.sidebarBg).isDark();
290-
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
338+
const edgeToEdge = edgeToEdgeHack(Screens.HOME, theme);
291339

292340
if (!passProps.coldStart && (passProps.launchType === Launch.AddServer || passProps.launchType === Launch.AddServerFromDeepLink)) {
293341
dismissModal({componentId: Screens.SERVER});
@@ -313,6 +361,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
313361
statusBar: {
314362
visible: true,
315363
backgroundColor: theme.sidebarBg,
364+
...edgeToEdge,
316365
},
317366
topBar: {
318367
visible: false,
@@ -337,8 +386,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
337386

338387
export function resetToSelectServer(passProps: LaunchProps) {
339388
const theme = getDefaultThemeByAppearance();
340-
const isDark = tinyColor(theme.sidebarBg).isDark();
341-
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
389+
const edgeToEdge = edgeToEdgeHack(Screens.SERVER, theme);
342390

343391
const children = [{
344392
component: {
@@ -356,6 +404,7 @@ export function resetToSelectServer(passProps: LaunchProps) {
356404
statusBar: {
357405
visible: true,
358406
backgroundColor: theme.sidebarBg,
407+
...edgeToEdge,
359408
},
360409
topBar: {
361410
backButton: {
@@ -383,8 +432,7 @@ export function resetToSelectServer(passProps: LaunchProps) {
383432

384433
export function resetToOnboarding(passProps: LaunchProps) {
385434
const theme = getDefaultThemeByAppearance();
386-
const isDark = tinyColor(theme.sidebarBg).isDark();
387-
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
435+
const edgeToEdge = edgeToEdgeHack(Screens.ONBOARDING, theme);
388436

389437
const children = [{
390438
component: {
@@ -402,6 +450,7 @@ export function resetToOnboarding(passProps: LaunchProps) {
402450
statusBar: {
403451
visible: true,
404452
backgroundColor: theme.sidebarBg,
453+
...edgeToEdge,
405454
},
406455
topBar: {
407456
backButton: {
@@ -429,8 +478,7 @@ export function resetToOnboarding(passProps: LaunchProps) {
429478

430479
export function resetToTeams() {
431480
const theme = getThemeFromState();
432-
const isDark = tinyColor(theme.sidebarBg).isDark();
433-
StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
481+
const edgeToEdge = edgeToEdgeHack(Screens.SELECT_TEAM, theme);
434482

435483
return Navigation.setRoot({
436484
root: {
@@ -446,6 +494,7 @@ export function resetToTeams() {
446494
statusBar: {
447495
visible: true,
448496
backgroundColor: theme.sidebarBg,
497+
...edgeToEdge,
449498
},
450499
topBar: {
451500
visible: false,
@@ -472,7 +521,7 @@ export function goToScreen(name: AvailableScreens, title: string, passProps = {}
472521
}
473522

474523
const theme = getThemeFromState();
475-
const isDark = tinyColor(theme.sidebarBg).isDark();
524+
const edgeToEdge = edgeToEdgeHack(name, theme);
476525
const componentId = NavigationStore.getVisibleScreen();
477526
if (!componentId) {
478527
logError('Trying to go to screen without any screen on the navigation store');
@@ -489,8 +538,10 @@ export function goToScreen(name: AvailableScreens, title: string, passProps = {}
489538
right: {enabled: false},
490539
},
491540
statusBar: {
492-
style: isDark ? 'light' : 'dark',
541+
style: edgeToEdge.isDark ? 'light' : 'dark',
493542
backgroundColor: theme.sidebarBg,
543+
drawBehind: edgeToEdge.drawBehind ?? false,
544+
translucent: edgeToEdge.translucent ?? false,
494545
},
495546
topBar: {
496547
animate: true,
@@ -608,6 +659,7 @@ export function showModal(name: AvailableScreens, title: string, passProps = {},
608659
}
609660

610661
const theme = getThemeFromState();
662+
const edgeToEdge = edgeToEdgeHack(name, theme);
611663
const modalPresentationStyle: OptionsModalPresentationStyle = Platform.OS === 'ios' ? OptionsModalPresentationStyle.pageSheet : OptionsModalPresentationStyle.none;
612664
const defaultOptions: Options = {
613665
modalPresentationStyle,
@@ -617,6 +669,7 @@ export function showModal(name: AvailableScreens, title: string, passProps = {},
617669
statusBar: {
618670
visible: true,
619671
backgroundColor: theme.sidebarBg,
672+
...edgeToEdge,
620673
},
621674
topBar: {
622675
animate: true,

libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package com.mattermost.rnutils
22

33
import android.app.Activity
4+
import android.graphics.Color
45
import android.os.Build
56
import android.view.WindowManager
7+
import androidx.core.content.ContextCompat
68
import androidx.core.net.toUri
9+
import androidx.core.view.WindowInsetsControllerCompat
710
import com.facebook.react.bridge.Arguments
811
import com.facebook.react.bridge.Promise
912
import com.facebook.react.bridge.ReactApplicationContext
@@ -13,8 +16,13 @@ import com.mattermost.rnutils.helpers.Notifications
1316
import com.mattermost.rnutils.helpers.RealPathUtil
1417
import com.mattermost.rnutils.helpers.SaveDataTask
1518
import com.mattermost.rnutils.helpers.SplitView
19+
import androidx.core.graphics.toColorInt
20+
import com.facebook.react.bridge.LifecycleEventListener
1621

17-
class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
22+
class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext): LifecycleEventListener {
23+
24+
private var lastHex: String? = null
25+
private var lastLightIcons: Boolean = true
1826

1927
companion object {
2028
const val NAME = "RNUtils"
@@ -37,7 +45,16 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
3745
setCtx(reactContext)
3846
SplitView.setCtx(reactContext)
3947
Notifications.setCtx(reactContext)
48+
reactContext.addLifecycleEventListener(this)
49+
}
50+
51+
override fun onHostResume() {
52+
val hex = lastHex ?: return
53+
val light = lastLightIcons
54+
setNavigationBarColor(hex, light)
4055
}
56+
override fun onHostPause() {}
57+
override fun onHostDestroy() {}
4158

4259
fun getTypedExportedConstants(): MutableMap<String, Any> {
4360
val map = mutableMapOf<String, Any>()
@@ -213,4 +230,36 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
213230
currentActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
214231
}
215232
}
233+
234+
fun setNavigationBarColor(colorHex: String, lightIcons: Boolean) {
235+
val currentActivity: Activity = reactContext.currentActivity ?: return
236+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
237+
return
238+
}
239+
240+
lastHex = colorHex
241+
lastLightIcons = lightIcons
242+
243+
currentActivity.runOnUiThread {
244+
try {
245+
val w = currentActivity.window
246+
w.decorView.post {
247+
val parsedColor = colorHex.toColorInt()
248+
w.navigationBarColor = parsedColor
249+
250+
val controller = WindowInsetsControllerCompat(w, w.decorView)
251+
controller.isAppearanceLightNavigationBars = lightIcons
252+
253+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
254+
w.isNavigationBarContrastEnforced = false
255+
}
256+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
257+
w.navigationBarDividerColor = Color.TRANSPARENT
258+
}
259+
}
260+
} catch (e: Exception) {
261+
android.util.Log.e("RNUtils", "Error setting navigation bar color: $colorHex", e)
262+
}
263+
}
264+
}
216265
}

libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.mattermost.rnutils
22

3+
import com.facebook.react.bridge.LifecycleEventListener
34
import com.facebook.react.bridge.Promise
45
import com.facebook.react.bridge.ReactApplicationContext
56
import com.facebook.react.bridge.ReactContextBaseJavaModule
@@ -115,4 +116,9 @@ class RNUtilsModule(context: ReactApplicationContext) :
115116
val pathList = paths.toArrayList().map { it.toString() }
116117
implementation.createZipFile(pathList, promise)
117118
}
119+
120+
@ReactMethod
121+
fun setNavigationBarColor(colorHex: String, lightIcons: Boolean) {
122+
implementation.setNavigationBarColor(colorHex, lightIcons)
123+
}
118124
}

libraries/@mattermost/rnutils/src/NativeRNUtils.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ export interface Spec extends TurboModule {
7575
setSoftKeyboardToAdjustNothing(): void;
7676

7777
createZipFile: (paths: string[]) => Promise<string>;
78+
79+
// Android only
80+
setNavigationBarColor: (color: string, lightIcons: boolean) => void;
7881
}
7982

8083
export default TurboModuleRegistry.getEnforcing<Spec>('RNUtils');

0 commit comments

Comments
 (0)