diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.kt b/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.kt
index 820c7326a..1e5462f51 100644
--- a/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.kt
+++ b/android/app/src/main/java/com/mattermost/rnbeta/MainActivity.kt
@@ -3,6 +3,7 @@ package com.mattermost.rnbeta
import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
+import androidx.core.view.WindowCompat
import com.facebook.react.ReactActivityDelegate
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint
import com.facebook.react.defaults.DefaultReactActivityDelegate
@@ -37,6 +38,7 @@ class MainActivity : NavigationActivity() {
setHWKeyboardConnected()
lastOrientation = this.resources.configuration.orientation
foldableObserver.onCreate()
+ WindowCompat.setDecorFitsSystemWindows(window, false)
}
override fun onStart() {
diff --git a/android/app/src/main/res/values-v35/styles.xml b/android/app/src/main/res/values-v35/styles.xml
index decb34cef..45b73c2c3 100644
--- a/android/app/src/main/res/values-v35/styles.xml
+++ b/android/app/src/main/res/values-v35/styles.xml
@@ -7,5 +7,14 @@
- @drawable/rn_edit_text_material
- true
+
+ - @android:color/transparent
+
+
+ - false
+ - false
+
+
+ - shortEdges
diff --git a/app/screens/navigation.ts b/app/screens/navigation.ts
index d8fffaf58..212c74166 100644
--- a/app/screens/navigation.ts
+++ b/app/screens/navigation.ts
@@ -3,8 +3,9 @@
/* eslint-disable max-lines */
+import RNUtils from '@mattermost/rnutils';
import merge from 'deepmerge';
-import {Appearance, DeviceEventEmitter, StatusBar, Platform, Alert, type EmitterSubscription, Keyboard} from 'react-native';
+import {Appearance, DeviceEventEmitter, Platform, Alert, type EmitterSubscription, Keyboard, StatusBar} from 'react-native';
import {type ComponentWillAppearEvent, type ImageResource, type LayoutOrientation, Navigation, type Options, OptionsModalPresentationStyle, type OptionsTopBarButton, type ScreenPoppedEvent, type EventSubscription} from 'react-native-navigation';
import tinyColor from 'tinycolor2';
@@ -36,6 +37,23 @@ let subscriptions: Array | undefined;
export const allOrientations: LayoutOrientation[] = ['sensor', 'sensorLandscape', 'sensorPortrait', 'landscape', 'portrait'];
export const portraitOrientation: LayoutOrientation[] = ['portrait'];
+const loginFlowScreens = new Set([
+ Screens.ONBOARDING,
+ Screens.SERVER,
+ Screens.LOGIN,
+ Screens.SSO,
+ Screens.MFA,
+ Screens.FORGOT_PASSWORD,
+]);
+
+function setNavigationBarColor(screen: AvailableScreens, th?: Theme) {
+ if (Platform.OS === 'android' && Platform.Version >= 34) {
+ const theme = th || getThemeFromState();
+ const color = loginFlowScreens.has(screen) ? theme.sidebarBg : theme.centerChannelBg;
+ RNUtils.setNavigationBarColor(color, tinyColor(color).isLight());
+ }
+}
+
export function registerNavigationListeners() {
subscriptions?.forEach((v) => v.remove());
subscriptions = [
@@ -88,14 +106,19 @@ function onCommandListener(name: string, params: any) {
}
}
- if (NavigationStore.getVisibleScreen() === Screens.HOME) {
+ const screen = NavigationStore.getVisibleScreen();
+ if (screen === Screens.HOME) {
DeviceEventEmitter.emit(Events.TAB_BAR_VISIBLE, true);
}
+
+ setNavigationBarColor(screen);
}
function onPoppedListener({componentId}: ScreenPoppedEvent) {
// screen pop does not trigger registerCommandListener, but does trigger screenPoppedListener
- NavigationStore.removeScreenFromStack(componentId as AvailableScreens);
+ const screen = componentId as AvailableScreens;
+ NavigationStore.removeScreenFromStack(screen);
+ setNavigationBarColor(screen);
}
function onScreenWillAppear(event: ComponentWillAppearEvent) {
@@ -279,6 +302,32 @@ function isScreenRegistered(screen: AvailableScreens) {
return true;
}
+function edgeToEdgeHack(screen: AvailableScreens, theme: Theme) {
+ const isDark = tinyColor(theme.sidebarBg).isDark();
+
+ if (Platform.OS === 'android') {
+ if (Platform.Version >= 34) {
+ const listener = Navigation.events().registerComponentDidAppearListener((event) => {
+ if (event.componentName === screen) {
+ setNavigationBarColor(screen, theme);
+ listener.remove();
+ }
+ });
+ }
+
+ if (Platform.Version >= 36) {
+ return {
+ drawBehind: true,
+ translucent: false,
+ isDark,
+ };
+ }
+ }
+
+ StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
+ return {isDark};
+}
+
export function openToS() {
NavigationStore.setToSOpen(true);
return showOverlay(Screens.TERMS_OF_SERVICE, {}, {overlay: {interceptTouchOutside: true}});
@@ -286,8 +335,7 @@ export function openToS() {
export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}) {
const theme = getThemeFromState();
- const isDark = tinyColor(theme.sidebarBg).isDark();
- StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
+ const edgeToEdge = edgeToEdgeHack(Screens.HOME, theme);
if (!passProps.coldStart && (passProps.launchType === Launch.AddServer || passProps.launchType === Launch.AddServerFromDeepLink)) {
dismissModal({componentId: Screens.SERVER});
@@ -313,6 +361,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
statusBar: {
visible: true,
backgroundColor: theme.sidebarBg,
+ ...edgeToEdge,
},
topBar: {
visible: false,
@@ -337,8 +386,7 @@ export function resetToHome(passProps: LaunchProps = {launchType: Launch.Normal}
export function resetToSelectServer(passProps: LaunchProps) {
const theme = getDefaultThemeByAppearance();
- const isDark = tinyColor(theme.sidebarBg).isDark();
- StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
+ const edgeToEdge = edgeToEdgeHack(Screens.SERVER, theme);
const children = [{
component: {
@@ -356,6 +404,7 @@ export function resetToSelectServer(passProps: LaunchProps) {
statusBar: {
visible: true,
backgroundColor: theme.sidebarBg,
+ ...edgeToEdge,
},
topBar: {
backButton: {
@@ -383,8 +432,7 @@ export function resetToSelectServer(passProps: LaunchProps) {
export function resetToOnboarding(passProps: LaunchProps) {
const theme = getDefaultThemeByAppearance();
- const isDark = tinyColor(theme.sidebarBg).isDark();
- StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
+ const edgeToEdge = edgeToEdgeHack(Screens.ONBOARDING, theme);
const children = [{
component: {
@@ -402,6 +450,7 @@ export function resetToOnboarding(passProps: LaunchProps) {
statusBar: {
visible: true,
backgroundColor: theme.sidebarBg,
+ ...edgeToEdge,
},
topBar: {
backButton: {
@@ -429,8 +478,7 @@ export function resetToOnboarding(passProps: LaunchProps) {
export function resetToTeams() {
const theme = getThemeFromState();
- const isDark = tinyColor(theme.sidebarBg).isDark();
- StatusBar.setBarStyle(isDark ? 'light-content' : 'dark-content');
+ const edgeToEdge = edgeToEdgeHack(Screens.SELECT_TEAM, theme);
return Navigation.setRoot({
root: {
@@ -446,6 +494,7 @@ export function resetToTeams() {
statusBar: {
visible: true,
backgroundColor: theme.sidebarBg,
+ ...edgeToEdge,
},
topBar: {
visible: false,
@@ -472,7 +521,7 @@ export function goToScreen(name: AvailableScreens, title: string, passProps = {}
}
const theme = getThemeFromState();
- const isDark = tinyColor(theme.sidebarBg).isDark();
+ const edgeToEdge = edgeToEdgeHack(name, theme);
const componentId = NavigationStore.getVisibleScreen();
if (!componentId) {
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 = {}
right: {enabled: false},
},
statusBar: {
- style: isDark ? 'light' : 'dark',
+ style: edgeToEdge.isDark ? 'light' : 'dark',
backgroundColor: theme.sidebarBg,
+ drawBehind: edgeToEdge.drawBehind ?? false,
+ translucent: edgeToEdge.translucent ?? false,
},
topBar: {
animate: true,
@@ -608,6 +659,7 @@ export function showModal(name: AvailableScreens, title: string, passProps = {},
}
const theme = getThemeFromState();
+ const edgeToEdge = edgeToEdgeHack(name, theme);
const modalPresentationStyle: OptionsModalPresentationStyle = Platform.OS === 'ios' ? OptionsModalPresentationStyle.pageSheet : OptionsModalPresentationStyle.none;
const defaultOptions: Options = {
modalPresentationStyle,
@@ -617,6 +669,7 @@ export function showModal(name: AvailableScreens, title: string, passProps = {},
statusBar: {
visible: true,
backgroundColor: theme.sidebarBg,
+ ...edgeToEdge,
},
topBar: {
animate: true,
diff --git a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt
index ceafaa653..6d98b58aa 100644
--- a/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt
+++ b/libraries/@mattermost/rnutils/android/src/main/java/com/mattermost/rnutils/RNUtilsModuleImpl.kt
@@ -1,9 +1,12 @@
package com.mattermost.rnutils
import android.app.Activity
+import android.graphics.Color
import android.os.Build
import android.view.WindowManager
+import androidx.core.content.ContextCompat
import androidx.core.net.toUri
+import androidx.core.view.WindowInsetsControllerCompat
import com.facebook.react.bridge.Arguments
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
@@ -13,8 +16,13 @@ import com.mattermost.rnutils.helpers.Notifications
import com.mattermost.rnutils.helpers.RealPathUtil
import com.mattermost.rnutils.helpers.SaveDataTask
import com.mattermost.rnutils.helpers.SplitView
+import androidx.core.graphics.toColorInt
+import com.facebook.react.bridge.LifecycleEventListener
-class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
+class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext): LifecycleEventListener {
+
+ private var lastHex: String? = null
+ private var lastLightIcons: Boolean = true
companion object {
const val NAME = "RNUtils"
@@ -37,8 +45,17 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
setCtx(reactContext)
SplitView.setCtx(reactContext)
Notifications.setCtx(reactContext)
+ reactContext.addLifecycleEventListener(this)
}
+ override fun onHostResume() {
+ val hex = lastHex ?: return
+ val light = lastLightIcons
+ setNavigationBarColor(hex, light)
+ }
+ override fun onHostPause() {}
+ override fun onHostDestroy() {}
+
fun getTypedExportedConstants(): MutableMap {
val map = mutableMapOf()
val group = mutableMapOf()
@@ -213,4 +230,36 @@ class RNUtilsModuleImpl(private val reactContext: ReactApplicationContext) {
currentActivity.window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE)
}
}
+
+ fun setNavigationBarColor(colorHex: String, lightIcons: Boolean) {
+ val currentActivity: Activity = reactContext.currentActivity ?: return
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ return
+ }
+
+ lastHex = colorHex
+ lastLightIcons = lightIcons
+
+ currentActivity.runOnUiThread {
+ try {
+ val w = currentActivity.window
+ w.decorView.post {
+ val parsedColor = colorHex.toColorInt()
+ w.navigationBarColor = parsedColor
+
+ val controller = WindowInsetsControllerCompat(w, w.decorView)
+ controller.isAppearanceLightNavigationBars = lightIcons
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ w.isNavigationBarContrastEnforced = false
+ }
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ w.navigationBarDividerColor = Color.TRANSPARENT
+ }
+ }
+ } catch (e: Exception) {
+ android.util.Log.e("RNUtils", "Error setting navigation bar color: $colorHex", e)
+ }
+ }
+ }
}
diff --git a/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt b/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt
index dc3358b28..a28f9093d 100644
--- a/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt
+++ b/libraries/@mattermost/rnutils/android/src/oldarch/RNUtilsModule.kt
@@ -1,5 +1,6 @@
package com.mattermost.rnutils
+import com.facebook.react.bridge.LifecycleEventListener
import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.bridge.ReactContextBaseJavaModule
@@ -115,4 +116,9 @@ class RNUtilsModule(context: ReactApplicationContext) :
val pathList = paths.toArrayList().map { it.toString() }
implementation.createZipFile(pathList, promise)
}
+
+ @ReactMethod
+ fun setNavigationBarColor(colorHex: String, lightIcons: Boolean) {
+ implementation.setNavigationBarColor(colorHex, lightIcons)
+ }
}
diff --git a/libraries/@mattermost/rnutils/src/NativeRNUtils.ts b/libraries/@mattermost/rnutils/src/NativeRNUtils.ts
index 9451b11dd..ab41c44e9 100644
--- a/libraries/@mattermost/rnutils/src/NativeRNUtils.ts
+++ b/libraries/@mattermost/rnutils/src/NativeRNUtils.ts
@@ -75,6 +75,9 @@ export interface Spec extends TurboModule {
setSoftKeyboardToAdjustNothing(): void;
createZipFile: (paths: string[]) => Promise;
+
+ // Android only
+ setNavigationBarColor: (color: string, lightIcons: boolean) => void;
}
export default TurboModuleRegistry.getEnforcing('RNUtils');