Skip to content

Commit

Permalink
Merge pull request #32063 from margelo/@chrispader/theme-switching-st…
Browse files Browse the repository at this point in the history
…atus-and-scroll-bar

Theme switching: Dynamic StatusBar and scroll bars
  • Loading branch information
grgia authored Nov 30, 2023
2 parents 0931352 + f7df8ad commit 7ebe763
Show file tree
Hide file tree
Showing 25 changed files with 265 additions and 123 deletions.
3 changes: 2 additions & 1 deletion .imgbotconfig
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"ignoredFiles": [
"assets/images/empty-state_background-fade.png" // Caused an issue with colour gradients, https://github.com/Expensify/App/issues/30499
"assets/images/empty-state_background-fade-dark.png", // Caused an issue with colour gradients, https://github.com/Expensify/App/issues/30499
"assets/images/empty-state_background-fade-light.png"
],
"aggressiveCompression": "false"
}
7 changes: 6 additions & 1 deletion src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import Onyx from 'react-native-onyx';
import {PickerStateProvider} from 'react-native-picker-select';
import {SafeAreaProvider} from 'react-native-safe-area-context';
import '../wdyr';
import ColorSchemeWrapper from './components/ColorSchemeWrapper';
import ComposeProviders from './components/ComposeProviders';
import CustomStatusBar from './components/CustomStatusBar';
import CustomStatusBarContextProvider from './components/CustomStatusBar/CustomStatusBarContextProvider';
import ErrorBoundary from './components/ErrorBoundary';
import HTMLEngineProvider from './components/HTMLEngineProvider';
import {LocaleContextProvider} from './components/LocaleContextProvider';
Expand Down Expand Up @@ -66,11 +68,14 @@ function App() {
ThemeProvider,
ThemeStylesProvider,
ThemeIllustrationsProvider,
CustomStatusBarContextProvider,
]}
>
<CustomStatusBar />
<ErrorBoundary errorMessage="NewExpensify crash caught by error boundary">
<Expensify />
<ColorSchemeWrapper>
<Expensify />
</ColorSchemeWrapper>
</ErrorBoundary>
</ComposeProviders>
</GestureHandlerRootView>
Expand Down
8 changes: 8 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -699,6 +699,14 @@ const CONST = {
DARK: 'dark',
SYSTEM: 'system',
},
COLOR_SCHEME: {
LIGHT: 'light',
DARK: 'dark',
},
STATUS_BAR_STYLE: {
LIGHT_CONTENT: 'light-content',
DARK_CONTENT: 'dark-content',
},
TRANSACTION: {
DEFAULT_MERCHANT: 'Request',
UNKNOWN_MERCHANT: 'Unknown Merchant',
Expand Down
5 changes: 5 additions & 0 deletions src/components/ColorSchemeWrapper/index.native.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
function ColorSchemeWrapper({children}: React.PropsWithChildren) {
return children;
}

export default ColorSchemeWrapper;
13 changes: 13 additions & 0 deletions src/components/ColorSchemeWrapper/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from 'react';
import {View} from 'react-native';
import useTheme from '@styles/themes/useTheme';
import useThemeStyles from '@styles/useThemeStyles';

function ColorSchemeWrapper({children}: React.PropsWithChildren): React.ReactElement {
const theme = useTheme();
const themeStyles = useThemeStyles();

return <View style={[themeStyles.flex1, themeStyles.colorSchemeStyle(theme.colorScheme)]}>{children}</View>;
}

export default ColorSchemeWrapper;
11 changes: 11 additions & 0 deletions src/components/CustomStatusBar/CustomStatusBarContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {createContext} from 'react';

type CustomStatusBarContextType = {
isRootStatusBarDisabled: boolean;
disableRootStatusBar: (isDisabled: boolean) => void;
};

const CustomStatusBarContext = createContext<CustomStatusBarContextType>({isRootStatusBarDisabled: false, disableRootStatusBar: () => undefined});

export default CustomStatusBarContext;
export {type CustomStatusBarContextType};
17 changes: 17 additions & 0 deletions src/components/CustomStatusBar/CustomStatusBarContextProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React, {useMemo, useState} from 'react';
import CustomStatusBarContext from './CustomStatusBarContext';

function CustomStatusBarContextProvider({children}: React.PropsWithChildren) {
const [isRootStatusBarDisabled, disableRootStatusBar] = useState(false);
const value = useMemo(
() => ({
isRootStatusBarDisabled,
disableRootStatusBar,
}),
[isRootStatusBarDisabled],
);

return <CustomStatusBarContext.Provider value={value}>{children}</CustomStatusBarContext.Provider>;
}

export default CustomStatusBarContextProvider;
15 changes: 0 additions & 15 deletions src/components/CustomStatusBar/index.android.tsx

This file was deleted.

94 changes: 78 additions & 16 deletions src/components/CustomStatusBar/index.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,91 @@
import React, {useEffect} from 'react';
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
import {EventListenerCallback, NavigationContainerEventMap} from '@react-navigation/native';
import PropTypes from 'prop-types';
import React, {useCallback, useContext, useEffect} from 'react';
import {navigationRef} from '@libs/Navigation/Navigation';
import StatusBar from '@libs/StatusBar';
import useTheme from '@styles/themes/useTheme';
import type CustomStatusBarType from './types';
import CustomStatusBarContext from './CustomStatusBarContext';

type CustomStatusBarProps = {
isNested: boolean;
};

const propTypes = {
/** Whether the CustomStatusBar is nested within another CustomStatusBar.
* A nested CustomStatusBar will disable the "root" CustomStatusBar. */
isNested: PropTypes.bool,
};

type CustomStatusBarType = {
(props: CustomStatusBarProps): React.ReactNode;
displayName: string;
propTypes: typeof propTypes;
};

// eslint-disable-next-line react/function-component-definition
const CustomStatusBar: CustomStatusBarType = () => {
const CustomStatusBar: CustomStatusBarType = ({isNested = false}) => {
const {isRootStatusBarDisabled, disableRootStatusBar} = useContext(CustomStatusBarContext);
const theme = useTheme();

const isDisabled = !isNested && isRootStatusBarDisabled;

useEffect(() => {
Navigation.isNavigationReady().then(() => {
// Set the status bar colour depending on the current route.
// If we don't have any colour defined for a route, fall back to
// appBG color.
const currentRoute = navigationRef.getCurrentRoute();
let currentScreenBackgroundColor = theme.appBG;
if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_BACKGROUND_COLORS) {
currentScreenBackgroundColor = theme.PAGE_BACKGROUND_COLORS[currentRoute.name];
if (isNested) {
disableRootStatusBar(true);
}

return () => {
if (!isNested) {
return;
}
StatusBar.setBarStyle('light-content', true);
StatusBar.setBackgroundColor(currentScreenBackgroundColor);
});
}, [theme.PAGE_BACKGROUND_COLORS, theme.appBG]);
disableRootStatusBar(false);
};
}, [disableRootStatusBar, isNested]);

const updateStatusBarStyle = useCallback<EventListenerCallback<NavigationContainerEventMap, 'state'>>(() => {
if (isDisabled) {
return;
}

// Set the status bar colour depending on the current route.
// If we don't have any colour defined for a route, fall back to
// appBG color.
const currentRoute = navigationRef.getCurrentRoute();

let currentScreenBackgroundColor = theme.appBG;
let statusBarStyle = theme.statusBarStyle;
if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_THEMES) {
const screenTheme = theme.PAGE_THEMES[currentRoute.name];
currentScreenBackgroundColor = screenTheme.backgroundColor;
statusBarStyle = screenTheme.statusBarStyle;
}

StatusBar.setBackgroundColor(currentScreenBackgroundColor, true);
StatusBar.setBarStyle(statusBarStyle, true);
}, [isDisabled, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle]);

useEffect(() => {
navigationRef.addListener('state', updateStatusBarStyle);

return () => navigationRef.removeListener('state', updateStatusBarStyle);
}, [updateStatusBarStyle]);

useEffect(() => {
if (isDisabled) {
return;
}

StatusBar.setBarStyle(theme.statusBarStyle, true);
}, [isDisabled, theme.statusBarStyle]);

if (isDisabled) {
return null;
}

return <StatusBar />;
};

CustomStatusBar.displayName = 'CustomStatusBar';
CustomStatusBar.propTypes = propTypes;

export default CustomStatusBar;
6 changes: 0 additions & 6 deletions src/components/CustomStatusBar/types.ts

This file was deleted.

46 changes: 1 addition & 45 deletions src/libs/Navigation/NavigationRoot.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import {DefaultTheme, getPathFromState, NavigationContainer, NavigationState} from '@react-navigation/native';
import React, {useEffect, useMemo, useRef} from 'react';
import {ColorValue} from 'react-native';
import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated';
import useCurrentReportID from '@hooks/useCurrentReportID';
import useFlipper from '@hooks/useFlipper';
import useWindowDimensions from '@hooks/useWindowDimensions';
import Log from '@libs/Log';
import StatusBar from '@libs/StatusBar';
import useTheme from '@styles/themes/useTheme';
import AppNavigator from './AppNavigator';
import linkingConfig from './linkingConfig';
Expand Down Expand Up @@ -42,8 +39,8 @@ function parseAndLogRoute(state: NavigationState) {

function NavigationRoot({authenticated, onReady}: NavigationRootProps) {
useFlipper(navigationRef);
const theme = useTheme();
const firstRenderRef = useRef(true);
const theme = useTheme();

const currentReportIDValue = useCurrentReportID();
const {isSmallScreenWidth} = useWindowDimensions();
Expand Down Expand Up @@ -82,46 +79,6 @@ function NavigationRoot({authenticated, onReady}: NavigationRootProps) {
navigationRef.resetRoot(navigationRef.getRootState());
}, [isSmallScreenWidth, authenticated]);

const prevStatusBarBackgroundColor = useRef(theme.appBG);
const statusBarBackgroundColor = useRef(theme.appBG);
const statusBarAnimation = useSharedValue(0);

const updateStatusBarBackgroundColor = (color: ColorValue) => StatusBar.setBackgroundColor(color);
useAnimatedReaction(
() => statusBarAnimation.value,
(current, previous) => {
// Do not run if either of the animated value is null
// or previous animated value is greater than or equal to the current one
if (previous === null || current === null || current <= previous) {
return;
}
const color = interpolateColor(statusBarAnimation.value, [0, 1], [prevStatusBarBackgroundColor.current, statusBarBackgroundColor.current]);
runOnJS(updateStatusBarBackgroundColor)(color);
},
);

const animateStatusBarBackgroundColor = () => {
const currentRoute = navigationRef.getCurrentRoute();

const backgroundColorFromRoute =
currentRoute?.params && 'backgroundColor' in currentRoute.params && typeof currentRoute.params.backgroundColor === 'string' && currentRoute.params.backgroundColor;
const backgroundColorFallback = currentRoute?.name ? theme.PAGE_BACKGROUND_COLORS[currentRoute.name] || theme.appBG : theme.appBG;

// It's possible for backgroundColorFromRoute to be empty string, so we must use "||" to fallback to backgroundColorFallback.
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const currentScreenBackgroundColor = backgroundColorFromRoute || backgroundColorFallback;

prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current;
statusBarBackgroundColor.current = currentScreenBackgroundColor;

if (currentScreenBackgroundColor === theme.appBG && prevStatusBarBackgroundColor.current === theme.appBG) {
return;
}

statusBarAnimation.value = 0;
statusBarAnimation.value = withDelay(300, withTiming(1));
};

const handleStateChange = (state: NavigationState | undefined) => {
if (!state) {
return;
Expand All @@ -132,7 +89,6 @@ function NavigationRoot({authenticated, onReady}: NavigationRootProps) {
currentReportIDValue?.updateCurrentReportID(state);
}, 0);
parseAndLogRoute(state);
animateStatusBarBackgroundColor();
};

return (
Expand Down
14 changes: 9 additions & 5 deletions src/libs/StatusBar/index.android.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import StatusBar from './types';

// Only has custom web implementation
StatusBar.getBackgroundColor = () => null;
const setBackgroundColor = StatusBar.setBackgroundColor;

// We override this because it's not used – on Android our app display edge-to-edge.
// Also because Reanimated's interpolateColor gives Android native colors instead of hex strings, causing this to display a warning.
StatusBar.setBackgroundColor = () => null;
let statusBarColor: string | null = null;

StatusBar.getBackgroundColor = () => statusBarColor;

StatusBar.setBackgroundColor = (color, animated = false) => {
statusBarColor = color as string;
setBackgroundColor(color, animated);
};

export default StatusBar;
2 changes: 1 addition & 1 deletion src/pages/TeachersUnite/SaveTheWorldPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function SaveTheWorldPage(props) {
<IllustratedHeaderPageLayout
shouldShowBackButton
title={translate('sidebarScreen.saveTheWorld')}
backgroundColor={theme.PAGE_BACKGROUND_COLORS[SCREENS.SAVE_THE_WORLD.ROOT]}
backgroundColor={theme.PAGE_THEMES[SCREENS.SAVE_THE_WORLD.ROOT].backgroundColor}
onBackButtonPress={() => Navigation.goBack(ROUTES.HOME)}
illustration={LottieAnimations.SaveTheWorld}
>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/InitialSettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ function InitialSettingsPage(props) {
title={translate('common.settings')}
headerContent={headerContent}
headerContainerStyles={[styles.staticHeaderImage, styles.justifyContentCenter]}
backgroundColor={theme.PAGE_BACKGROUND_COLORS[SCREENS.SETTINGS.ROOT]}
backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.ROOT].backgroundColor}
>
<View style={styles.w100}>
{getMenuItems}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/Preferences/PreferencesPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function PreferencesPage(props) {
<IllustratedHeaderPageLayout
title={translate('common.preferences')}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)}
backgroundColor={theme.PAGE_BACKGROUND_COLORS[SCREENS.SETTINGS.PREFERENCES]}
backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.PREFERENCES].backgroundColor}
illustration={LottieAnimations.PreferencesDJ}
>
<View style={styles.mb6}>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/Profile/CustomStatus/StatusPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ function StatusPage({draftStatus, currentUserPersonalDetails}) {
/>
}
headerContainerStyles={[styles.staticHeaderImage]}
backgroundColor={theme.PAGE_BACKGROUND_COLORS[SCREENS.SETTINGS.STATUS]}
backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.STATUS].backgroundColor}
footer={footerComponent}
>
<View style={[styles.mh5, styles.mb5]}>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/Security/SecuritySettingsPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ function SecuritySettingsPage(props) {
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)}
shouldShowBackButton
illustration={LottieAnimations.Safe}
backgroundColor={theme.PAGE_BACKGROUND_COLORS[SCREENS.SETTINGS.SECURITY]}
backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.SECURITY].backgroundColor}
>
<ScrollView contentContainerStyle={[styles.flexGrow1, styles.flexColumn, styles.justifyContentBetween]}>
<View style={[styles.flex1]}>
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/Wallet/ActivatePhysicalCardPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ function ActivatePhysicalCardPage({
<IllustratedHeaderPageLayout
title={translate('activateCardPage.activateCard')}
onBackButtonPress={() => Navigation.navigate(ROUTES.SETTINGS_WALLET_DOMAINCARD.getRoute(domain))}
backgroundColor={theme.PAGE_BACKGROUND_COLORS[SCREENS.SETTINGS.PREFERENCES]}
backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.PREFERENCES].backgroundColor}
illustration={LottieAnimations.Magician}
scrollViewContainerStyles={[styles.mnh100]}
childrenContainerStyles={[styles.flex1]}
Expand Down
2 changes: 1 addition & 1 deletion src/pages/settings/Wallet/WalletEmptyState.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function WalletEmptyState({onAddPaymentMethod}) {
const {translate} = useLocalize();
return (
<IllustratedHeaderPageLayout
backgroundColor={theme.PAGE_BACKGROUND_COLORS[SCREENS.SETTINGS.WALLET]}
backgroundColor={theme.PAGE_THEMES[SCREENS.SETTINGS.WALLET].backgroundColor}
illustration={LottieAnimations.FastMoney}
onBackButtonPress={() => Navigation.goBack(ROUTES.SETTINGS)}
title={translate('common.wallet')}
Expand Down
Loading

0 comments on commit 7ebe763

Please sign in to comment.