Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Alert, BackHandler } from 'react-native';
import { PersistGate } from 'redux-persist/integration/react';
import { store, persistor } from './store';
import { AppNavigator } from '@/navigation';
import { ThemeProvider } from '@/context';

import i18n from '@/i18n';

Expand Down Expand Up @@ -34,7 +35,9 @@ const Chatwoot = () => {
return (
<Provider store={store}>
<PersistGate loading={null} persistor={persistor}>
<AppNavigator />
<ThemeProvider>
<AppNavigator />
</ThemeProvider>
</PersistGate>
</Provider>
);
Expand Down
101 changes: 101 additions & 0 deletions src/components-next/sheet-components/ThemeList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React from 'react';
import { Pressable, Text, Animated } from 'react-native';

import { TickIcon } from '@/svg-icons';
import { tailwind } from '@/theme';
import { Theme } from '@/types/common/Theme';
import { useHaptic } from '@/utils';
import { Icon } from '@/components-next/common/icon';
import i18n from '@/i18n';

type ThemeListItemType = {
value: Theme;
label: string;
description: string;
};

const THEME_OPTIONS: ThemeListItemType[] = [
{
value: 'light',
label: i18n.t('SETTINGS.THEME.LIGHT'),
description: i18n.t('SETTINGS.THEME.LIGHT_DESCRIPTION'),
},
{
value: 'dark',
label: i18n.t('SETTINGS.THEME.DARK'),
description: i18n.t('SETTINGS.THEME.DARK_DESCRIPTION'),
},
{
value: 'system',
label: i18n.t('SETTINGS.THEME.SYSTEM'),
description: i18n.t('SETTINGS.THEME.SYSTEM_DESCRIPTION'),
},
];

type ThemeCellProps = {
item: ThemeListItemType;
index: number;
currentTheme: Theme;
changeTheme: (theme: Theme) => void;
};

const ThemeCell = ({ item, index, currentTheme, changeTheme }: ThemeCellProps) => {
const hapticSelection = useHaptic();

const handlePress = () => {
hapticSelection?.();
changeTheme(item.value);
};

const isLastItem = index === THEME_OPTIONS.length - 1;
const isSelected = currentTheme === item.value;

return (
<Pressable onPress={handlePress}>
<Animated.View
style={tailwind.style(
'flex flex-row items-center justify-between py-3 px-4',
!isLastItem && 'border-b-[1px] border-blackA-A3',
)}>
<Animated.View style={tailwind.style('flex-1 flex-col')}>
<Text
style={tailwind.style(
'text-base text-gray-950 font-inter-420-20 leading-[21px] tracking-[0.16px]',
)}>
{item.label}
</Text>
<Text style={tailwind.style('text-sm text-gray-700 font-inter-normal-20 mt-0.5')}>
{item.description}
</Text>
</Animated.View>
{isSelected && (
<Animated.View style={tailwind.style('ml-3')}>
<Icon icon={<TickIcon />} size={20} />
</Animated.View>
)}
</Animated.View>
</Pressable>
);
};

export const ThemeList = ({
currentTheme,
changeTheme,
}: {
currentTheme: Theme;
changeTheme: (theme: Theme) => void;
}) => {
return (
<Animated.View style={tailwind.style('flex flex-col')}>
{THEME_OPTIONS.map((item, index) => (
<ThemeCell
key={item.value}
item={item}
index={index}
currentTheme={currentTheme}
changeTheme={changeTheme}
/>
))}
</Animated.View>
);
};
1 change: 1 addition & 0 deletions src/components-next/sheet-components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './AvailabilityStatusList';
export * from './NotificationPreferences';
export * from './SwitchAccount';
export * from './ThemeList';
3 changes: 3 additions & 0 deletions src/context/RefsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface RefsContextType {
filtersModalSheetRef: React.RefObject<BottomSheetModal>;
actionsModalSheetRef: React.RefObject<BottomSheetModal>;
languagesModalSheetRef: React.RefObject<BottomSheetModal>;
themeSheetRef: React.RefObject<BottomSheetModal>;
chatPagerView: React.RefObject<PagerView>;
addLabelSheetRef: React.RefObject<BottomSheetModal>;
macrosListSheetRef: React.RefObject<BottomSheetModal>;
Expand Down Expand Up @@ -40,6 +41,7 @@ const RefsProvider: React.FC<Partial<RefsContextType & { children: React.ReactNo
const filtersModalSheetRef = useRef<BottomSheetModal>(null);
const actionsModalSheetRef = useRef<BottomSheetModal>(null);
const languagesModalSheetRef = useRef<BottomSheetModal>(null);
const themeSheetRef = useRef<BottomSheetModal>(null);
const notificationPreferencesSheetRef = useRef<BottomSheetModal>(null);
const addLabelSheetRef = useRef<BottomSheetModal>(null);
const macrosListSheetRef = useRef<BottomSheetModal>(null);
Expand All @@ -59,6 +61,7 @@ const RefsProvider: React.FC<Partial<RefsContextType & { children: React.ReactNo
filtersModalSheetRef,
actionsModalSheetRef,
languagesModalSheetRef,
themeSheetRef,
notificationPreferencesSheetRef,
chatPagerView,
addLabelSheetRef,
Expand Down
60 changes: 60 additions & 0 deletions src/context/ThemeContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { createContext, useContext, useEffect, ReactNode } from 'react';
import { Appearance, ColorSchemeName } from 'react-native';
import { useAppDispatch, useAppSelector } from '@/hooks';
import { setTheme } from '@/store/settings/settingsSlice';
import { selectTheme } from '@/store/settings/settingsSelectors';
import { tailwind } from '@/theme';
import { Theme } from '@/types/common/Theme';

type ThemeContextType = {
theme: Theme;
colorScheme: ColorSchemeName;
isDark: boolean;
changeTheme: (newTheme: Theme) => void;
};

const ThemeContext = createContext<ThemeContextType | undefined>(undefined);

type ThemeProviderProps = {
children: ReactNode;
};

export const ThemeProvider = ({ children }: ThemeProviderProps) => {
const dispatch = useAppDispatch();
const theme = useAppSelector(selectTheme) || 'system';
const systemColorScheme = Appearance.getColorScheme();

const colorScheme: ColorSchemeName =
theme === 'system' ? systemColorScheme : theme === 'dark' ? 'dark' : 'light';

const isDark = colorScheme === 'dark';

useEffect(() => {
if (theme === 'system') {
const subscription = Appearance.addChangeListener(({ colorScheme: newColorScheme }) => {
tailwind.setColorScheme(newColorScheme);
});
Comment on lines +32 to +36
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge System theme never applied on initial render

When the user selects system (the default) the ThemeProvider branch at lines 32-36 only registers an Appearance listener and never calls tailwind.setColorScheme with the current system scheme, nor does it update context when the system theme changes. With dark mode enabled at the OS level, the app stays on the prior light scheme until a future appearance change triggers a re-render, so the “Follow system” option and default boot on dark devices show incorrect colors.

Useful? React with 👍 / 👎.

return () => subscription.remove();
} else {
tailwind.setColorScheme(colorScheme);
}
}, [theme, colorScheme]);

const changeTheme = (newTheme: Theme) => {
dispatch(setTheme(newTheme));
};

return (
<ThemeContext.Provider value={{ theme, colorScheme, isDark, changeTheme }}>
{children}
</ThemeContext.Provider>
);
};

export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
1 change: 1 addition & 0 deletions src/context/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export * from './RefsContext';
export * from './ConversationListContext';
export * from './InboxListContext';
export * from './useChatWindowContext';
export * from './ThemeContext';
12 changes: 11 additions & 1 deletion src/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -290,14 +290,24 @@
"SUPPORT": "Support",
"SWITCH_ACCOUNT": "Switch Account",
"CHANGE_LANGUAGE": "Change Language",
"CHANGE_THEME": "Appearance",
"NOTIFICATIONS": "Notifications",
"NOTIFICATION_PREFERENCES": "Notification Preferences",
"READ_DOCS": "Read Docs",
"CHAT_WITH_US": "Chat with us",
"LOGOUT": "Logout",
"SUBMIT": "Submit",
"SET_LANGUAGE": "Set Language",
"DEBUG_ACTIONS": "Debug Actions"
"SET_THEME": "Set Appearance",
"DEBUG_ACTIONS": "Debug Actions",
"THEME": {
"LIGHT": "Light",
"LIGHT_DESCRIPTION": "Always use light mode",
"DARK": "Dark",
"DARK_DESCRIPTION": "Always use dark mode",
"SYSTEM": "System",
"SYSTEM_DESCRIPTION": "Follow system settings"
}
},
"NOTIFICATION": {
"HEADER_TITLE": "Notifications",
Expand Down
47 changes: 40 additions & 7 deletions src/screens/settings/SettingsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React, { useCallback, useEffect, useState } from 'react';
import { StatusBar, Text, Platform, Pressable } from 'react-native';
import { StatusBar, Text, Platform, Pressable, Appearance } from 'react-native';
import Animated from 'react-native-reanimated';
// import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context';
import { SafeAreaView } from 'react-native-safe-area-context';
Expand Down Expand Up @@ -35,12 +35,14 @@ import {
NotificationPreferences,
SwitchAccount,
SettingsList,
ThemeList,
} from '@/components-next';
import { UserAvatar } from './components/UserAvatar';

import { LANGUAGES, TAB_BAR_HEIGHT } from '@/constants';
import { useRefsContext } from '@/context';
import { ChatwootIcon, NotificationIcon, SwitchIcon, TranslateIcon } from '@/svg-icons';
import { useTheme } from '@/context';
import { GenericListType } from '@/types';

import { useHaptic } from '@/utils';
Expand Down Expand Up @@ -133,12 +135,14 @@ const SettingsScreen = () => {
const enableAccountSwitch = accounts.length > 1;

const activeLocale = useSelector(selectLocale);
const { theme, changeTheme } = useTheme();
const {
userAvailabilityStatusSheetRef,
languagesModalSheetRef,
notificationPreferencesSheetRef,
switchAccountSheetRef,
debugActionsSheetRef,
themeSheetRef,
} = useRefsContext();

const hapticSelection = useHaptic();
Expand Down Expand Up @@ -192,6 +196,13 @@ const SettingsScreen = () => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeLocale]);

useEffect(() => {
themeSheetRef.current?.dismiss({
overshootClamping: true,
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme]);

const openURL = async () => {
await WebBrowser.openBrowserAsync(HELP_URL);
};
Expand Down Expand Up @@ -237,6 +248,14 @@ const SettingsScreen = () => {
subtitleType: 'light',
onPressListItem: () => languagesModalSheetRef.current?.present(),
},
{
hasChevron: true,
title: i18n.t('SETTINGS.CHANGE_THEME'),
icon: <SwitchIcon />,
subtitle: i18n.t(`SETTINGS.THEME.${theme.toUpperCase()}`),
subtitleType: 'light',
onPressListItem: () => themeSheetRef.current?.present(),
},
{
hasChevron: enableAccountSwitch,
title: i18n.t('SETTINGS.SWITCH_ACCOUNT'),
Expand Down Expand Up @@ -271,11 +290,11 @@ const SettingsScreen = () => {
];

return (
<SafeAreaView style={tailwind.style('flex-1 bg-white font-inter-normal-20')}>
<SafeAreaView style={tailwind.style('flex-1 bg-white dark:bg-grayDark-50 font-inter-normal-20')}>
<StatusBar
translucent
backgroundColor={tailwind.color('bg-white')}
barStyle={'dark-content'}
backgroundColor={tailwind.color('bg-white dark:bg-grayDark-50')}
barStyle={theme === 'dark' || (theme === 'system' && Appearance.getColorScheme() === 'dark') ? 'light-content' : 'dark-content'}
/>
<SettingsHeader />
<Animated.ScrollView
Expand All @@ -290,12 +309,12 @@ const SettingsScreen = () => {
)}></Animated.View>
</Animated.View>
<Animated.View style={tailwind.style('flex flex-col items-center gap-1')}>
<Animated.Text style={tailwind.style('text-[22px] font-inter-580-24 text-gray-950')}>
<Animated.Text style={tailwind.style('text-[22px] font-inter-580-24 text-gray-950 dark:text-grayDark-950')}>
{name}
</Animated.Text>
<Animated.Text
style={tailwind.style(
'text-[15px] font-inter-420-20 leading-[17.25px] text-gray-900',
'text-[15px] font-inter-420-20 leading-[17.25px] text-gray-900 dark:text-grayDark-900',
)}>
{email}
</Animated.Text>
Expand All @@ -318,7 +337,7 @@ const SettingsScreen = () => {
<Pressable
style={tailwind.style('p-4 items-center')}
onLongPress={() => debugActionsSheetRef.current?.present()}>
<Text style={tailwind.style('text-sm text-gray-700 ')}>
<Text style={tailwind.style('text-sm text-gray-700 dark:text-grayDark-700')}>
{`${chatwootInstance} ${appVersionDetails}`}
</Text>
</Pressable>
Expand Down Expand Up @@ -358,6 +377,20 @@ const SettingsScreen = () => {
<LanguageList onChangeLanguage={onChangeLanguage} currentLanguage={activeLocale} />
</BottomSheetScrollView>
</BottomSheetModal>
<BottomSheetModal
ref={themeSheetRef}
backdropComponent={BottomSheetBackdrop}
handleIndicatorStyle={tailwind.style('overflow-hidden bg-blackA-A6 w-8 h-1 rounded-[11px]')}
enablePanDownToClose
animationConfigs={animationConfigs}
handleStyle={tailwind.style('p-0 h-4 pt-[5px]')}
style={tailwind.style('rounded-[26px] overflow-hidden')}
snapPoints={[260]}>
<BottomSheetWrapper>
<BottomSheetHeader headerText={i18n.t('SETTINGS.SET_THEME')} />
<ThemeList currentTheme={theme} changeTheme={changeTheme} />
</BottomSheetWrapper>
</BottomSheetModal>
<BottomSheetModal
ref={notificationPreferencesSheetRef}
backdropComponent={BottomSheetBackdrop}
Expand Down
5 changes: 4 additions & 1 deletion src/store/settings/settingsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ export const settingsSlice = createSlice({
state.localeValue = action.payload;
state.uiFlags.isLocaleSet = true;
},
setTheme: (state, action) => {
state.theme = action.payload;
},
},
extraReducers: builder => {
builder
Expand Down Expand Up @@ -99,5 +102,5 @@ export const settingsSlice = createSlice({
});
},
});
export const { resetSettings, setLocale } = settingsSlice.actions;
export const { resetSettings, setLocale, setTheme } = settingsSlice.actions;
export default settingsSlice.reducer;
1 change: 1 addition & 0 deletions src/theme/tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const chatwootAppColors = {
};

export const twConfig = {
darkMode: 'class',
theme: {
...defaultTheme,
extend: {
Expand Down