diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..2773f21d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,28 @@ +# Project conventions + +## Themed styles — use the `useThemedStyles` hook + +Components that build a `StyleSheet` from the theme MUST obtain it via +[`useThemedStyles`](hooks/useThemedStyles.ts). Do NOT hand-roll the +`useTheme()` + `useMemo(() => createStyles(theme), [theme])` boilerplate. + +```tsx +// ✅ styles only +const { styles } = useThemedStyles(createStyles); + +// ✅ when the component also reads theme directly (icon colors, insets, etc.) +const { styles, theme } = useThemedStyles(createStyles); + +// ✅ parametric factory — pass extra args after the factory +const { styles } = useThemedStyles(createStyles, disabled); + +// ❌ do not do this +const { theme } = useTheme(); +const styles = useMemo(() => createStyles(theme), [theme]); +``` + +- Keep the `createStyles = (theme: Theme, ...args) => StyleSheet.create({ ... })` + factory at the bottom of the file, unchanged — the hook memoizes it. +- The hook returns `{ styles, theme }`; destructure only what you use. +- `useTheme()` from [context/ThemeContext](context/ThemeContext.tsx) is still fine + for components that need `theme` but build no `StyleSheet`. diff --git a/app/(drawer)/benchmark.tsx b/app/(drawer)/benchmark.tsx index 3f83335a..05731dae 100644 --- a/app/(drawer)/benchmark.tsx +++ b/app/(drawer)/benchmark.tsx @@ -1,16 +1,10 @@ -import React, { - useEffect, - useState, - useCallback, - useRef, - useMemo, -} from 'react'; +import React, { useEffect, useState, useCallback, useRef } from 'react'; import { View, StyleSheet } from 'react-native'; import { useSQLiteContext } from 'expo-sqlite'; import { useLocalSearchParams } from 'expo-router'; import { BottomSheetModal } from '@gorhom/bottom-sheet'; import { useModelStore } from '../../store/modelStore'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { BenchmarkResult, @@ -30,8 +24,7 @@ import useDefaultHeader from '../../hooks/useDefaultHeader'; const BenchmarkScreen = () => { useDefaultHeader(); const bottomSheetModalRef = useRef(null); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const db = useSQLiteContext(); const { getModelById } = useModelStore(); const { modelId } = useLocalSearchParams<{ modelId?: string }>(); diff --git a/app/(drawer)/index.tsx b/app/(drawer)/index.tsx index cc72da46..ac12ceb5 100644 --- a/app/(drawer)/index.tsx +++ b/app/(drawer)/index.tsx @@ -1,10 +1,4 @@ -import React, { - useCallback, - useEffect, - useLayoutEffect, - useMemo, - useRef, -} from 'react'; +import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import { startPhantomChat } from '../../utils/startPhantomChat'; import { router, useFocusEffect, useNavigation } from 'expo-router'; import { configureReanimatedLogger } from 'react-native-reanimated'; @@ -18,7 +12,7 @@ import { LinearGradient } from 'expo-linear-gradient'; import PrimaryButton from '../../components/PrimaryButton'; import TextButton from '../../components/TextButton'; import { Theme } from '../../styles/colors'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { importChatRoom } from '../../database/exportImportRepository'; import { useChatStore } from '../../store/chatStore'; import ModelSelectSheet from '../../components/bottomSheets/ModelSelectSheet'; @@ -49,8 +43,7 @@ export default function App() { headerRight: () => , }); }, [navigation]); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const { addChat } = useChatStore(); const handleSetModel = async (model: Model, replace = false) => { diff --git a/app/(drawer)/model-hub.tsx b/app/(drawer)/model-hub.tsx index e31e608a..134e84e4 100644 --- a/app/(drawer)/model-hub.tsx +++ b/app/(drawer)/model-hub.tsx @@ -12,7 +12,7 @@ import WarningSheet, { WarningSheetData, } from '../../components/bottomSheets/WarningSheet'; import ModelManagementSheet from '../../components/bottomSheets/ModelManagementSheet'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Model } from '../../database/modelRepository'; @@ -32,8 +32,7 @@ import { useConfirm } from '../../hooks/useConfirm'; const ModelHubScreen = () => { useDefaultHeader(); const router = useRouter(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const addModelSheetRef = useRef(null); const wifiWarningSheetRef = useRef | null>( diff --git a/app/(drawer)/settings.tsx b/app/(drawer)/settings.tsx index 0e661d8a..caf5ee9a 100644 --- a/app/(drawer)/settings.tsx +++ b/app/(drawer)/settings.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, View } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; import { useRouter } from 'expo-router'; @@ -6,14 +6,13 @@ import useDefaultHeader from '../../hooks/useDefaultHeader'; import { SettingsRow } from '../../components/settings/SettingsRow'; import EditIcon from '../../assets/icons/edit.svg'; import InfoCircleIcon from '../../assets/icons/info-circle.svg'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; const SettingsScreen = () => { useDefaultHeader(); const router = useRouter(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/app/(modals)/add-local-model.tsx b/app/(modals)/add-local-model.tsx index 20b2eb7d..b9442b70 100644 --- a/app/(modals)/add-local-model.tsx +++ b/app/(modals)/add-local-model.tsx @@ -1,10 +1,10 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { Text, StyleSheet, Alert, View } from 'react-native'; import { useRouter } from 'expo-router'; import { useModelStore } from '../../store/modelStore'; import ModalHeader from '../../components/ModalHeader'; import { fontSizes, fontFamily } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import PrimaryButton from '../../components/PrimaryButton'; import { ScrollView } from 'react-native-gesture-handler'; import UploadInput from '../../components/UploadInput'; @@ -26,8 +26,7 @@ export interface LocalModelFormState { export default function AddLocalModelScreen() { const router = useRouter(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const { addModelToDB } = useModelStore(); const [ diff --git a/app/(modals)/add-remote-model.tsx b/app/(modals)/add-remote-model.tsx index 6eb28ad0..57a202f2 100644 --- a/app/(modals)/add-remote-model.tsx +++ b/app/(modals)/add-remote-model.tsx @@ -1,11 +1,11 @@ -import React, { useMemo, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import { Text, StyleSheet, Alert, Platform, View } from 'react-native'; import { useRouter } from 'expo-router'; import { useModelStore } from '../../store/modelStore'; import ModalHeader from '../../components/ModalHeader'; import TextFieldInput from '../../components/TextFieldInput'; import { fontSizes, fontFamily } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import PrimaryButton from '../../components/PrimaryButton'; import { ScrollView } from 'react-native-gesture-handler'; import Toast from 'react-native-toast-message'; @@ -20,8 +20,7 @@ export interface RemoteModelFormState { export default function AddRemoteModelScreen() { const router = useRouter(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const scrollViewRef = useRef(null); const { addModelToDB } = useModelStore(); diff --git a/app/(modals)/custom-system-prompt.tsx b/app/(modals)/custom-system-prompt.tsx index 8dadec92..6bc18c84 100644 --- a/app/(modals)/custom-system-prompt.tsx +++ b/app/(modals)/custom-system-prompt.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Platform, Text, StyleSheet, View } from 'react-native'; import { useRouter } from 'expo-router'; import { ScrollView } from 'react-native-gesture-handler'; @@ -8,7 +8,7 @@ import PrimaryButton from '../../components/PrimaryButton'; import TextAreaField from '../../components/TextAreaField'; import { CustomKeyboardAvoidingView } from '../../components/CustomKeyboardAvoidingView'; import { fontSizes, fontFamily } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { useSettingsStore } from '../../store/settingsStore'; import { Feedback } from '../../utils/Feedback'; @@ -16,8 +16,7 @@ import { MAX_CUSTOM_SYSTEM_PROMPT_LENGTH } from '../../constants/settings'; export default function CustomSystemPromptScreen() { const router = useRouter(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const customSystemPrompt = useSettingsStore((s) => s.customSystemPrompt); const setCustomSystemPrompt = useSettingsStore( diff --git a/app/(modals)/edit-local-model/[modelId].tsx b/app/(modals)/edit-local-model/[modelId].tsx index d77ae7fe..d51449ba 100644 --- a/app/(modals)/edit-local-model/[modelId].tsx +++ b/app/(modals)/edit-local-model/[modelId].tsx @@ -1,10 +1,10 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { Text, StyleSheet, View, Alert } from 'react-native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useModelStore } from '../../../store/modelStore'; import ModalHeader from '../../../components/ModalHeader'; import { fontSizes, fontFamily } from '../../../styles/fontStyles'; -import { useTheme } from '../../../context/ThemeContext'; +import { useThemedStyles } from '../../../hooks/useThemedStyles'; import PrimaryButton from '../../../components/PrimaryButton'; import { ScrollView } from 'react-native-gesture-handler'; import UploadInput from '../../../components/UploadInput'; @@ -40,8 +40,7 @@ export default function EditLocalModelScreen() { const modelId = parseInt(rawModelId); const router = useRouter(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const { getModelById, editModel } = useModelStore(); const model = getModelById(modelId); diff --git a/app/(modals)/edit-remote-model/[modelId].tsx b/app/(modals)/edit-remote-model/[modelId].tsx index ed7db62b..e8d88ea6 100644 --- a/app/(modals)/edit-remote-model/[modelId].tsx +++ b/app/(modals)/edit-remote-model/[modelId].tsx @@ -1,11 +1,11 @@ -import React, { useMemo, useRef, useState } from 'react'; +import React, { useRef, useState } from 'react'; import { Text, StyleSheet, Alert, Platform, View } from 'react-native'; import { useLocalSearchParams, useRouter } from 'expo-router'; import { useModelStore } from '../../../store/modelStore'; import ModalHeader from '../../../components/ModalHeader'; import TextFieldInput from '../../../components/TextFieldInput'; import { fontSizes, fontFamily } from '../../../styles/fontStyles'; -import { useTheme } from '../../../context/ThemeContext'; +import { useThemedStyles } from '../../../hooks/useThemedStyles'; import PrimaryButton from '../../../components/PrimaryButton'; import { ScrollView } from 'react-native-gesture-handler'; import Toast from 'react-native-toast-message'; @@ -25,8 +25,7 @@ export default function EditRemoteModelScreen() { const scrollViewRef = useRef(null); const router = useRouter(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const { getModelById, editModel } = useModelStore(); const model = getModelById(modelId); diff --git a/app/(modals)/model-family/[family].tsx b/app/(modals)/model-family/[family].tsx index cc8f32b6..b7ace65e 100644 --- a/app/(modals)/model-family/[family].tsx +++ b/app/(modals)/model-family/[family].tsx @@ -6,7 +6,7 @@ import { BottomSheetModal, BottomSheetModalProvider, } from '@gorhom/bottom-sheet'; -import { useTheme } from '../../../context/ThemeContext'; +import { useThemedStyles } from '../../../hooks/useThemedStyles'; import { Theme } from '../../../styles/colors'; import { fontFamily, fontSizes } from '../../../styles/fontStyles'; import { useModelStore } from '../../../store/modelStore'; @@ -22,8 +22,7 @@ import { FAMILY_DESCRIPTIONS } from '../../../constants/family-descriptions'; const FamilyScreen = () => { const router = useRouter(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const { family } = useLocalSearchParams<{ family: string }>(); const familyName = decodeURIComponent(family ?? ''); diff --git a/app/(modals)/onboarding.tsx b/app/(modals)/onboarding.tsx index 93ae2929..243311e2 100644 --- a/app/(modals)/onboarding.tsx +++ b/app/(modals)/onboarding.tsx @@ -1,10 +1,4 @@ -import React, { - useLayoutEffect, - useMemo, - useRef, - useState, - useEffect, -} from 'react'; +import React, { useLayoutEffect, useRef, useState, useEffect } from 'react'; import { Dimensions, Image, @@ -13,7 +7,7 @@ import { View, BackHandler, } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import OnboardingIntroPanel from '../../components/onboarding/OnboardingIntroPanel'; import Animated, { @@ -85,8 +79,7 @@ function OnboardingScreen() { router.replace('/(modals)/select-starting-model'); }; - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const bgSpillProgress = useSharedValue(0); const [stepNumber, setStepNumber] = useState(0); diff --git a/app/(modals)/select-starting-model.tsx b/app/(modals)/select-starting-model.tsx index 12312c6e..a78856ad 100644 --- a/app/(modals)/select-starting-model.tsx +++ b/app/(modals)/select-starting-model.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { StyleSheet, View, Text, ScrollView } from 'react-native'; import { useRouter } from 'expo-router'; import { fontSizes, fontFamily } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { useModelStore } from '../../store/modelStore'; import PrimaryButton from '../../components/PrimaryButton'; import ModelCard from '../../components/model-hub/ModelCard'; @@ -19,8 +19,7 @@ import { getDeviceMemoryGB } from '../../utils/modelCompatibility'; function SelectStartingModelScreen() { const router = useRouter(); const db = useSQLiteContext(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const { initPhantomChat } = useChatStore(); const { setActiveChatId } = useLLMStore(); const { downloadedModels } = useModelStore(); diff --git a/components/AppToast.tsx b/components/AppToast.tsx index cdc3f844..c0886c49 100644 --- a/components/AppToast.tsx +++ b/components/AppToast.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Platform, StyleSheet, @@ -11,13 +11,12 @@ import Toast, { ToastConfig } from 'react-native-toast-message'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Theme } from '../styles/colors'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import CloseIcon from '../assets/icons/close.svg'; import { fontFamily, fontSizes } from '../styles/fontStyles'; const AppToast: React.FC = () => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const insets = useSafeAreaInsets(); const toastConfig: ToastConfig = { diff --git a/components/Chip.tsx b/components/Chip.tsx index f72ac71c..7351698b 100644 --- a/components/Chip.tsx +++ b/components/Chip.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../styles/fontStyles'; import { Theme } from '../styles/colors'; @@ -19,10 +19,11 @@ const Chip = ({ textColor, borderColor, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo( - () => createStyles(theme, backgroundColor, textColor, borderColor), - [theme, backgroundColor, textColor, borderColor] + const { styles } = useThemedStyles( + createStyles, + backgroundColor, + textColor, + borderColor ); return ( diff --git a/components/Divider.tsx b/components/Divider.tsx index db0ce465..7596f75c 100644 --- a/components/Divider.tsx +++ b/components/Divider.tsx @@ -1,11 +1,10 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, StyleSheet } from 'react-native'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { Theme } from '../styles/colors'; export const Divider = () => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ; }; diff --git a/components/EntryButton.tsx b/components/EntryButton.tsx index 00682fc0..1c37df21 100644 --- a/components/EntryButton.tsx +++ b/components/EntryButton.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { TouchableOpacity, Text, @@ -7,7 +7,7 @@ import { TextStyle, GestureResponderEvent, } from 'react-native'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../styles/fontStyles'; import { Theme } from '../styles/colors'; @@ -28,8 +28,7 @@ const EntryButton = ({ style, textStyle, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/components/ModalHeader.tsx b/components/ModalHeader.tsx index d635f6ac..8c6933db 100644 --- a/components/ModalHeader.tsx +++ b/components/ModalHeader.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, TouchableOpacity, @@ -8,7 +8,7 @@ import { } from 'react-native'; import CloseIcon from '../assets/icons/close.svg'; import ArrowLeftIcon from '../assets/icons/arrow-left.svg'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { Theme } from '../styles/colors'; import { fontFamily, fontSizes } from '../styles/fontStyles'; @@ -19,8 +19,7 @@ interface Props { } const ModalHeader = ({ title, onClose, leftIcon = 'close' }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const Icon = leftIcon === 'back' ? ArrowLeftIcon : CloseIcon; const iconSize = leftIcon === 'back' ? 20 : 16; diff --git a/components/NewChatHeaderButton.tsx b/components/NewChatHeaderButton.tsx index 869a1a9b..827ce521 100644 --- a/components/NewChatHeaderButton.tsx +++ b/components/NewChatHeaderButton.tsx @@ -1,8 +1,8 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { TouchableOpacity, StyleSheet } from 'react-native'; import { useSQLiteContext } from 'expo-sqlite'; import ChatIcon from '../assets/icons/chat.svg'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { Theme } from '../styles/colors'; import { startPhantomChat } from '../utils/startPhantomChat'; @@ -12,8 +12,7 @@ interface Props { const NewChatHeaderButton = ({ noOp = false }: Props) => { const db = useSQLiteContext(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const handlePress = () => { if (noOp) return; diff --git a/components/PrimaryButton.tsx b/components/PrimaryButton.tsx index aa7d8ad7..0391cd4c 100644 --- a/components/PrimaryButton.tsx +++ b/components/PrimaryButton.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { TouchableOpacity, Text, @@ -7,7 +7,7 @@ import { TextStyle, GestureResponderEvent, } from 'react-native'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../styles/fontStyles'; import { Theme } from '../styles/colors'; @@ -28,11 +28,7 @@ const PrimaryButton = ({ style, textStyle, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo( - () => createStyles(theme, disabled), - [theme, disabled] - ); + const { styles } = useThemedStyles(createStyles, disabled); return ( { - const { theme } = useTheme(); - const styles = useMemo( - () => createStyles(theme, disabled), - [theme, disabled] - ); + const { styles } = useThemedStyles(createStyles, disabled); return ( { - const { theme } = useTheme(); + const { styles, theme } = useThemedStyles(createStyles); const rotation = useSharedValue(0); - const styles = useMemo(() => createStyles(theme), [theme]); useEffect(() => { rotation.set( diff --git a/components/SplashScreenAnimation.tsx b/components/SplashScreenAnimation.tsx index a72805c1..87a78ea1 100644 --- a/components/SplashScreenAnimation.tsx +++ b/components/SplashScreenAnimation.tsx @@ -1,6 +1,6 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Dimensions, StyleSheet } from 'react-native'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { lightTheme, Theme } from '../styles/colors'; import { SplashScreen } from 'expo-router'; import Animated, { @@ -12,8 +12,7 @@ import Animated, { } from 'react-native-reanimated'; function SplashScreenAnimation() { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const rippleProgress = useSharedValue(0); const [done, setDone] = useState(false); diff --git a/components/TextAreaField.tsx b/components/TextAreaField.tsx index c01400c8..1cb29202 100644 --- a/components/TextAreaField.tsx +++ b/components/TextAreaField.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { TextInput, StyleSheet, @@ -6,7 +6,7 @@ import { Text, TextInputProps, } from 'react-native'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../styles/fontStyles'; import { Theme } from '../styles/colors'; import TextInputBorder from './TextInputBorder'; @@ -26,9 +26,8 @@ const TextAreaField: React.FC = ({ errorMessage, ...props }) => { - const { theme } = useTheme(); + const { styles, theme } = useThemedStyles(createStyles); const [active, setActive] = useState(false); - const styles = useMemo(() => createStyles(theme), [theme]); const error = !!errorMessage; diff --git a/components/TextButton.tsx b/components/TextButton.tsx index 8a81cd1f..98672a49 100644 --- a/components/TextButton.tsx +++ b/components/TextButton.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { TouchableOpacity, Text, @@ -7,7 +7,7 @@ import { TextStyle, GestureResponderEvent, } from 'react-native'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../styles/fontStyles'; import { Theme } from '../styles/colors'; @@ -26,11 +26,7 @@ const TextButton = ({ style, textStyle, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo( - () => createStyles(theme, disabled), - [theme, disabled] - ); + const { styles } = useThemedStyles(createStyles, disabled); return ( = ({ placeholder, ...props }) => { - const { theme } = useTheme(); - const [active, setActive] = useState(false); - - const styles = useMemo( - () => createStyles(theme, !!icon, props.editable !== false), - [theme, icon, props.editable] + const { styles, theme } = useThemedStyles( + createStyles, + !!icon, + props.editable !== false ); + const [active, setActive] = useState(false); return ( diff --git a/components/TextInputBorder.tsx b/components/TextInputBorder.tsx index 3f2c36d1..a34f7277 100644 --- a/components/TextInputBorder.tsx +++ b/components/TextInputBorder.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { useAnimatedStyle, @@ -6,7 +6,7 @@ import Animated, { withTiming, } from 'react-native-reanimated'; import { Theme } from '../styles/colors'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; interface TextInputBorderProps { active: boolean; @@ -20,8 +20,7 @@ const TextInputBorder: React.FC = ({ active, error = false, }) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const showActive = active && !error; const activeOpacity = useSharedValue(showActive ? 1 : 0); diff --git a/components/UploadInput.tsx b/components/UploadInput.tsx index 573aef6a..5fd8eca1 100644 --- a/components/UploadInput.tsx +++ b/components/UploadInput.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, Text, @@ -10,7 +10,7 @@ import * as DocumentPicker from 'expo-document-picker'; import Folder from '../assets/icons/folder.svg'; import TrashIcon from '../assets/icons/trash.svg'; import AttachmentIcon from '../assets/icons/attachment.svg'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../styles/fontStyles'; import { Theme } from '../styles/colors'; @@ -34,11 +34,7 @@ const formatFileSize = (bytes: number | null) => { }; const UploadInput = ({ fileInfo, onChange, disabled = false }: Props) => { - const { theme } = useTheme(); - const styles = useMemo( - () => createStyles(theme, disabled), - [theme, disabled] - ); + const { styles } = useThemedStyles(createStyles, disabled); const handlePickFile = async () => { const result = await DocumentPicker.getDocumentAsync({ diff --git a/components/WhatsNewCard.tsx b/components/WhatsNewCard.tsx index 2b38c7eb..282e7284 100644 --- a/components/WhatsNewCard.tsx +++ b/components/WhatsNewCard.tsx @@ -1,13 +1,12 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; -import { useTheme } from '../context/ThemeContext'; +import { useThemedStyles } from '../hooks/useThemedStyles'; import { Theme } from '../styles/colors'; import { fontFamily, fontSizes, lineHeights } from '../styles/fontStyles'; import { LATEST_RELEASE } from '../constants/latest-release'; const WhatsNewCard = () => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/components/benchmark/BenchmarkDateCard.tsx b/components/benchmark/BenchmarkDateCard.tsx index 684847e5..70b96adb 100644 --- a/components/benchmark/BenchmarkDateCard.tsx +++ b/components/benchmark/BenchmarkDateCard.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; type Props = { @@ -9,8 +9,7 @@ type Props = { }; const BenchmarkDateCard = ({ timestamp }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const displayDate = new Date(timestamp || new Date()).toLocaleDateString( 'en-GB', diff --git a/components/benchmark/BenchmarkHistory.tsx b/components/benchmark/BenchmarkHistory.tsx index 6066ed1f..7d1df752 100644 --- a/components/benchmark/BenchmarkHistory.tsx +++ b/components/benchmark/BenchmarkHistory.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, StyleSheet, Text } from 'react-native'; import { BottomSheetModal } from '@gorhom/bottom-sheet'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { useModelStore } from '../../store/modelStore'; import { BenchmarkResult } from '../../database/benchmarkRepository'; import BenchmarkItem from './BenchmarkItem'; @@ -16,9 +16,8 @@ interface Props { } const BenchmarkHistory = ({ modalRef, benchmarkList }: Props) => { - const { theme } = useTheme(); + const { styles } = useThemedStyles(createStyles); const { getModelById } = useModelStore(); - const styles = useMemo(() => createStyles(theme), [theme]); return ( <> diff --git a/components/benchmark/BenchmarkItem.tsx b/components/benchmark/BenchmarkItem.tsx index f17f5daf..9fd9e11b 100644 --- a/components/benchmark/BenchmarkItem.tsx +++ b/components/benchmark/BenchmarkItem.tsx @@ -1,9 +1,9 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; import { BenchmarkResult } from '../../database/benchmarkRepository'; import BenchmarkIcon from '../../assets/icons/benchmark.svg'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; @@ -13,8 +13,7 @@ interface Props { } const BenchmarkItem = ({ entry, onPress }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const date = new Date(entry.timestamp); const formattedDate = `${date.getDate()} ${date.toLocaleString('default', { diff --git a/components/benchmark/BenchmarkModal.tsx b/components/benchmark/BenchmarkModal.tsx index ae873764..4b0ee7d4 100644 --- a/components/benchmark/BenchmarkModal.tsx +++ b/components/benchmark/BenchmarkModal.tsx @@ -1,10 +1,10 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Modal, View, Text, StyleSheet } from 'react-native'; import ModelCard from '../model-hub/ModelCard'; import SecondaryButton from '../SecondaryButton'; import { SpinningCircleTimer } from '../SpinningCircleTimer'; import { fontSizes, fontFamily } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { Model } from '../../database/modelRepository'; import CheckIcon from '../../assets/icons/check.svg'; @@ -24,8 +24,7 @@ export const BenchmarkModal = ({ timer, selectedModel, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/components/benchmark/DeviceInfoCard.tsx b/components/benchmark/DeviceInfoCard.tsx index 05c5cca2..75916c69 100644 --- a/components/benchmark/DeviceInfoCard.tsx +++ b/components/benchmark/DeviceInfoCard.tsx @@ -1,8 +1,8 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { Divider } from '../Divider'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; interface Props { @@ -14,8 +14,7 @@ interface Props { } const DeviceInfoCard = ({ deviceInfo }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/components/benchmark/ModelSelector.tsx b/components/benchmark/ModelSelector.tsx index 26011d18..f8df37d8 100644 --- a/components/benchmark/ModelSelector.tsx +++ b/components/benchmark/ModelSelector.tsx @@ -1,8 +1,8 @@ -import React, { Dispatch, SetStateAction, useMemo, useRef } from 'react'; +import React, { Dispatch, SetStateAction, useRef } from 'react'; import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; import { Model } from '../../database/modelRepository'; import ModelCard from '../model-hub/ModelCard'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontSizes, fontFamily } from '../../styles/fontStyles'; import ChevronDownIcon from '../../assets/icons/chevron-down.svg'; import ModelSelectSheet from '../bottomSheets/ModelSelectSheet'; @@ -16,8 +16,7 @@ interface Props { export const ModelSelector = ({ model, setSelectedModel }: Props) => { const bottomSheetModalRef = useRef(null); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( <> diff --git a/components/bottomSheets/AddModelSheet.tsx b/components/bottomSheets/AddModelSheet.tsx index a5fdf9a1..d328db12 100644 --- a/components/bottomSheets/AddModelSheet.tsx +++ b/components/bottomSheets/AddModelSheet.tsx @@ -1,11 +1,11 @@ -import React, { RefObject, useCallback, useMemo } from 'react'; +import React, { RefObject, useCallback } from 'react'; import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, } from '@gorhom/bottom-sheet'; import { StyleSheet, Text, View } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; import { router } from 'expo-router'; @@ -18,8 +18,7 @@ interface Props { } const AddModelSheet = ({ bottomSheetModalRef }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const renderBackdrop = useCallback( (props: any) => ( diff --git a/components/bottomSheets/AttachmentSheet.tsx b/components/bottomSheets/AttachmentSheet.tsx index 0555575b..6259474e 100644 --- a/components/bottomSheets/AttachmentSheet.tsx +++ b/components/bottomSheets/AttachmentSheet.tsx @@ -1,11 +1,11 @@ -import React, { RefObject, useMemo } from 'react'; +import React, { RefObject } from 'react'; import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, } from '@gorhom/bottom-sheet'; import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes, lineHeights } from '../../styles/fontStyles'; import Toast from 'react-native-toast-message'; @@ -63,8 +63,7 @@ const AttachmentSheet = ({ onPickDocument, onSheetStateChange, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const handleOption = (action: () => void) => { bottomSheetModalRef.current?.dismiss(); diff --git a/components/bottomSheets/BenchmarkResultSheet.tsx b/components/bottomSheets/BenchmarkResultSheet.tsx index d74b794c..e5cb46eb 100644 --- a/components/bottomSheets/BenchmarkResultSheet.tsx +++ b/components/bottomSheets/BenchmarkResultSheet.tsx @@ -1,10 +1,4 @@ -import React, { - RefObject, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; +import React, { RefObject, useCallback, useEffect, useState } from 'react'; import { BottomSheetModal, BottomSheetBackdrop, @@ -12,7 +6,7 @@ import { } from '@gorhom/bottom-sheet'; import { StyleSheet, Text } from 'react-native'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import DeviceInfo from 'react-native-device-info'; import SecondaryButton from '../SecondaryButton'; @@ -28,8 +22,7 @@ interface Props { } const BenchmarkResultSheet = ({ bottomSheetModalRef, handleDelete }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const [deviceInfo, setDeviceInfo] = useState({ model: '', diff --git a/components/bottomSheets/BottomSheetSearchInput.tsx b/components/bottomSheets/BottomSheetSearchInput.tsx index 74b506b6..da1c90ee 100644 --- a/components/bottomSheets/BottomSheetSearchInput.tsx +++ b/components/bottomSheets/BottomSheetSearchInput.tsx @@ -1,7 +1,7 @@ -import React, { useMemo, useState } from 'react'; +import React, { useState } from 'react'; import { View, StyleSheet, Platform } from 'react-native'; import { BottomSheetTextInput } from '@gorhom/bottom-sheet'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; import SearchIcon from '../../assets/icons/search.svg'; @@ -20,8 +20,7 @@ const BottomSheetSearchInput = ({ placeholder, style, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const [active, setActive] = useState(false); if (Platform.OS !== 'ios') { diff --git a/components/bottomSheets/EmbeddingDownloadSheet.tsx b/components/bottomSheets/EmbeddingDownloadSheet.tsx index 2a7fb8de..8c18dbb4 100644 --- a/components/bottomSheets/EmbeddingDownloadSheet.tsx +++ b/components/bottomSheets/EmbeddingDownloadSheet.tsx @@ -1,4 +1,4 @@ -import React, { RefObject, useCallback, useMemo } from 'react'; +import React, { RefObject, useCallback } from 'react'; import { BottomSheetModal, BottomSheetView, @@ -7,7 +7,7 @@ import { } from '@gorhom/bottom-sheet'; import { StyleSheet, Text, View } from 'react-native'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import PrimaryButton from '../PrimaryButton'; import SecondaryButton from '../SecondaryButton'; @@ -25,8 +25,7 @@ const EmbeddingDownloadSheet = ({ onDownload, onDismiss, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const status = useEmbeddingModelStore((state) => state.status); const progress = useEmbeddingModelStore((state) => state.progress); diff --git a/components/bottomSheets/ModelManagementSheet.tsx b/components/bottomSheets/ModelManagementSheet.tsx index 9954d341..4c735d1a 100644 --- a/components/bottomSheets/ModelManagementSheet.tsx +++ b/components/bottomSheets/ModelManagementSheet.tsx @@ -1,4 +1,4 @@ -import React, { RefObject, useCallback, useMemo, useState } from 'react'; +import React, { RefObject, useCallback, useState } from 'react'; import { BottomSheetModal, BottomSheetView, @@ -6,7 +6,7 @@ import { } from '@gorhom/bottom-sheet'; import { StyleSheet, Text, View } from 'react-native'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import EntryButton from '../EntryButton'; import ModelCard from '../model-hub/ModelCard'; @@ -34,8 +34,7 @@ enum ModalStage { } const ModelManagementSheet = ({ bottomSheetModalRef }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const { removeModel, removeModelFiles } = useModelStore(); const [stage, setStage] = useState(ModalStage.Initial); diff --git a/components/bottomSheets/ModelSelectSheet.tsx b/components/bottomSheets/ModelSelectSheet.tsx index 9db6c0ab..a5fd0512 100644 --- a/components/bottomSheets/ModelSelectSheet.tsx +++ b/components/bottomSheets/ModelSelectSheet.tsx @@ -1,4 +1,4 @@ -import React, { RefObject, useCallback, useMemo, useState } from 'react'; +import React, { RefObject, useCallback, useState } from 'react'; import { BottomSheetModal, BottomSheetFlatList, @@ -10,7 +10,7 @@ import { Easing } from 'react-native-reanimated'; import { router } from 'expo-router'; import { View, StyleSheet, Text, Platform } from 'react-native'; import { useModelStore } from '../../store/modelStore'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; import { Model } from '../../database/modelRepository'; @@ -30,8 +30,7 @@ const ModelSelectSheet = ({ selectModel, onSheetStateChange, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const { downloadedModels } = useModelStore(); const [search, setSearch] = useState(''); const [isFullyOpen, setIsFullyOpen] = useState(false); diff --git a/components/bottomSheets/WarningSheet.tsx b/components/bottomSheets/WarningSheet.tsx index 1c484163..cf96ba51 100644 --- a/components/bottomSheets/WarningSheet.tsx +++ b/components/bottomSheets/WarningSheet.tsx @@ -1,4 +1,4 @@ -import React, { RefObject, useCallback, useMemo } from 'react'; +import React, { RefObject, useCallback } from 'react'; import { BottomSheetModal, BottomSheetView, @@ -6,7 +6,7 @@ import { } from '@gorhom/bottom-sheet'; import { StyleSheet, Text, View } from 'react-native'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import PrimaryButton from '../PrimaryButton'; import SecondaryButton from '../SecondaryButton'; @@ -24,8 +24,7 @@ interface Props { } const WarningSheet = ({ bottomSheetModalRef, onDismiss }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const renderBackdrop = useCallback( (props: any) => ( diff --git a/components/chat-screen/AnimatedChatLoading.tsx b/components/chat-screen/AnimatedChatLoading.tsx index c15a4099..0cfb08a2 100644 --- a/components/chat-screen/AnimatedChatLoading.tsx +++ b/components/chat-screen/AnimatedChatLoading.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; import { StyleSheet } from 'react-native'; import Animated, { Easing, @@ -7,7 +7,7 @@ import Animated, { withRepeat, withTiming, } from 'react-native-reanimated'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; @@ -18,8 +18,7 @@ import { fontFamily, fontSizes } from '../../styles/fontStyles'; * first token arrives causes no layout shift. */ const AnimatedChatLoading = () => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const opacity = useSharedValue(0.4); diff --git a/components/chat-screen/AttachmentThumbnail.tsx b/components/chat-screen/AttachmentThumbnail.tsx index e126c12c..1add5fa7 100644 --- a/components/chat-screen/AttachmentThumbnail.tsx +++ b/components/chat-screen/AttachmentThumbnail.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react'; +import React, { useEffect } from 'react'; import { View, Image, @@ -13,7 +13,7 @@ import Animated, { useSharedValue, withTiming, } from 'react-native-reanimated'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import CloseIcon from '../../assets/icons/close.svg'; @@ -27,8 +27,7 @@ interface Props { } const AttachmentThumbnail = ({ attachment, onRemove }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const fill = useSharedValue(0); useEffect(() => { diff --git a/components/chat-screen/BranchMarker.tsx b/components/chat-screen/BranchMarker.tsx index c6615cff..902411e7 100644 --- a/components/chat-screen/BranchMarker.tsx +++ b/components/chat-screen/BranchMarker.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import type { ChatBranchMarker } from '../../database/chatRepository'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes, lineHeights } from '../../styles/fontStyles'; import ForkIcon from '../../assets/icons/fork.svg'; @@ -12,8 +12,7 @@ type BranchMarkerProps = { }; export default function BranchMarker({ marker, onPress }: BranchMarkerProps) { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/components/chat-screen/ChatBar.tsx b/components/chat-screen/ChatBar.tsx index 6115281d..3806f5e9 100644 --- a/components/chat-screen/ChatBar.tsx +++ b/components/chat-screen/ChatBar.tsx @@ -27,7 +27,7 @@ import EmbeddingDownloadSheet from '../bottomSheets/EmbeddingDownloadSheet'; import { useAttachment, Attachment } from '../../hooks/useAttachment'; import { Model } from '../../database/modelRepository'; import { fontFamily, fontSizes, lineHeights } from '../../styles/fontStyles'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { useLLMStore } from '../../store/llmStore'; import RotateLeft from '../../assets/icons/rotate_left.svg'; import { Theme } from '../../styles/colors'; @@ -83,8 +83,7 @@ const ChatBar = ({ hasMessages, onAttachmentSheetStateChange, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const containerStyle = useMemo( () => [styles.container, { paddingBottom: theme.insets.bottom + 16 }], [styles.container, theme.insets.bottom] diff --git a/components/chat-screen/ChatBarActions.tsx b/components/chat-screen/ChatBarActions.tsx index d37bc494..089003a9 100644 --- a/components/chat-screen/ChatBarActions.tsx +++ b/components/chat-screen/ChatBarActions.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontSizes, lineHeights } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; import SendIcon from '../../assets/icons/send_icon.svg'; @@ -40,8 +40,7 @@ const ChatBarActions = ({ thinkingEnabled = false, onThinkingToggle, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const isResponding = isGenerating || isProcessingPrompt; const isAttachmentBlocked = isResponding || isLoadingAttachment; diff --git a/components/chat-screen/ChatScreen.tsx b/components/chat-screen/ChatScreen.tsx index 1bfa2879..89238a4f 100644 --- a/components/chat-screen/ChatScreen.tsx +++ b/components/chat-screen/ChatScreen.tsx @@ -20,7 +20,7 @@ import type { MessagesHandle, UserMessageActionMenuState } from './Messages'; import { useLLMStore } from '../../store/llmStore'; import { useChatStore } from '../../store/chatStore'; import { useModelStore } from '../../store/modelStore'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Chat, ChatSettings, @@ -103,8 +103,7 @@ export default function ChatScreen({ setPhantomChatSettings, } = useChatStore(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const headerHeight = useHeaderHeight(); const keyboardLift = useKeyboardLift(); diff --git a/components/chat-screen/ChatSpeechInput.tsx b/components/chat-screen/ChatSpeechInput.tsx index 06001a9a..64a2f2c2 100644 --- a/components/chat-screen/ChatSpeechInput.tsx +++ b/components/chat-screen/ChatSpeechInput.tsx @@ -1,13 +1,7 @@ -import React, { - useEffect, - useLayoutEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { Theme } from '../../styles/colors'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontSizes, lineHeights } from '../../styles/fontStyles'; import { useSpeechInput } from '../../hooks/useSpeechInput'; import CircleButton from '../CircleButton'; @@ -35,8 +29,7 @@ const ChatSpeechInput: React.FC = ({ onSubmit: onSubmitProp, onCancel: onCancelProp, }) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const recordingAttemptedRef = useRef(false); const recordingStartTimeRef = useRef(0); @@ -248,8 +241,7 @@ const RecordingInfo: React.FC<{ /** in seconds */ duration: number; }> = ({ active, duration }) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const minutes = Math.floor(duration / 60); const seconds = duration % 60; @@ -265,8 +257,7 @@ const RecordingInfo: React.FC<{ }; const ScrollableTranscript: React.FC<{ text: string }> = ({ text }) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/components/chat-screen/ChatTitle.tsx b/components/chat-screen/ChatTitle.tsx index 5de8ff96..7228a4f8 100644 --- a/components/chat-screen/ChatTitle.tsx +++ b/components/chat-screen/ChatTitle.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useRef } from 'react'; import { Text, StyleSheet, @@ -6,7 +6,7 @@ import { Pressable, useWindowDimensions, } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; import ChevronDown from '../../assets/icons/chevron-down.svg'; @@ -26,8 +26,7 @@ const ChatTitle = ({ showChevron = false, onBottomMeasured, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const containerRef = useRef(null); const lastReportedBottom = useRef(null); const handleLayout = useCallback(() => { diff --git a/components/chat-screen/ChatTitleMenuSheet.tsx b/components/chat-screen/ChatTitleMenuSheet.tsx index 6b1ca7f7..d8f1f591 100644 --- a/components/chat-screen/ChatTitleMenuSheet.tsx +++ b/components/chat-screen/ChatTitleMenuSheet.tsx @@ -1,11 +1,11 @@ -import React, { RefObject, useMemo } from 'react'; +import React, { RefObject } from 'react'; import { BottomSheetModal, BottomSheetView, BottomSheetBackdrop, } from '@gorhom/bottom-sheet'; import { View, Text, TouchableOpacity, StyleSheet } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes, lineHeights } from '../../styles/fontStyles'; import EditIcon from '../../assets/icons/edit.svg'; @@ -60,8 +60,7 @@ const ChatTitleMenuSheet = ({ onDelete, onDismiss, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const handleOption = (action: () => void) => { bottomSheetModalRef.current?.dismiss(); diff --git a/components/chat-screen/ImageLightbox.tsx b/components/chat-screen/ImageLightbox.tsx index 8d96c017..f8a188eb 100644 --- a/components/chat-screen/ImageLightbox.tsx +++ b/components/chat-screen/ImageLightbox.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Modal, StatusBar, @@ -8,7 +8,7 @@ import { } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import CloseIcon from '../../assets/icons/close.svg'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; interface Props { @@ -19,8 +19,7 @@ interface Props { const ImageLightbox = ({ uri, visible, onClose }: Props) => { const insets = useSafeAreaInsets(); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const isGenerating = useLLMStore((state) => state.isGenerating); const isProcessingPrompt = useLLMStore((state) => state.isProcessingPrompt); const [lightboxVisible, setLightboxVisible] = useState(false); diff --git a/components/chat-screen/Messages.tsx b/components/chat-screen/Messages.tsx index 65174402..9e60c452 100644 --- a/components/chat-screen/Messages.tsx +++ b/components/chat-screen/Messages.tsx @@ -39,7 +39,7 @@ import { SourceDocument, type ChatBranchMarker, } from '../../database/chatRepository'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { Feedback } from '../../utils/Feedback'; import ChevronDown from '../../assets/icons/chevron-down.svg'; @@ -190,8 +190,7 @@ const Messages = ({ onUserActionMenuChange, ref, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const scrollRef = useRef(null); const isAtBottomRef = useRef(true); const [showScrollButton, setShowScrollButton] = useState(false); diff --git a/components/chat-screen/PromptSuggestions.tsx b/components/chat-screen/PromptSuggestions.tsx index 06072cf3..d7005372 100644 --- a/components/chat-screen/PromptSuggestions.tsx +++ b/components/chat-screen/PromptSuggestions.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { ScrollView, TouchableOpacity } from 'react-native-gesture-handler'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes, lineHeights } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; import { @@ -14,8 +14,7 @@ interface Props { } const PromptSuggestions = ({ onSelectPrompt }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const handlePromptPress = (prompt: string) => { onSelectPrompt(prompt); diff --git a/components/chat-screen/RecordingAnimation.tsx b/components/chat-screen/RecordingAnimation.tsx index ce4f35ac..c4ff2d38 100644 --- a/components/chat-screen/RecordingAnimation.tsx +++ b/components/chat-screen/RecordingAnimation.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useImperativeHandle, useMemo } from 'react'; +import React, { useCallback, useImperativeHandle } from 'react'; import { StyleSheet, View } from 'react-native'; import Animated, { Easing, @@ -7,7 +7,7 @@ import Animated, { withTiming, } from 'react-native-reanimated'; import Svg, { Path } from 'react-native-svg'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; const AnimatedPath = Animated.createAnimatedComponent(Path); @@ -32,8 +32,7 @@ const BAR_GAP = 3; const BAR_OFFSET = BAR_WIDTH + BAR_GAP; const RecordingAnimation: React.FC = ({ width, height, ref }) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const barsPerWindow = Math.floor(width / BAR_OFFSET); const windowWidth = barsPerWindow * BAR_OFFSET; diff --git a/components/chat-screen/RenameChatModal.tsx b/components/chat-screen/RenameChatModal.tsx index 6cc0ca07..088a8a8c 100644 --- a/components/chat-screen/RenameChatModal.tsx +++ b/components/chat-screen/RenameChatModal.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Modal, View, @@ -8,7 +8,7 @@ import { Pressable, } from 'react-native'; import { KeyboardAvoidingView } from 'react-native-keyboard-controller'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; @@ -30,8 +30,7 @@ const RenameChatModal = ({ onCancel, onSubmit, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const [value, setValue] = useState(stripTrailingEllipsis(initialTitle)); useEffect(() => { diff --git a/components/chat-screen/SourcesSheet.tsx b/components/chat-screen/SourcesSheet.tsx index 7425b6c2..772e3efa 100644 --- a/components/chat-screen/SourcesSheet.tsx +++ b/components/chat-screen/SourcesSheet.tsx @@ -25,7 +25,7 @@ import { type BottomSheetScrollViewMethods, } from '@gorhom/bottom-sheet'; import { KeyboardEvents } from 'react-native-keyboard-controller'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { radius, @@ -153,8 +153,7 @@ interface SourcesSheetPayload { } const SourcesSheet = forwardRef((_props, ref) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const { height: screenHeight } = useWindowDimensions(); const sheetRef = useRef(null); diff --git a/components/chat-screen/ThinkingBlock.tsx b/components/chat-screen/ThinkingBlock.tsx index a40c0e82..b8ddd7db 100644 --- a/components/chat-screen/ThinkingBlock.tsx +++ b/components/chat-screen/ThinkingBlock.tsx @@ -1,6 +1,6 @@ -import React, { memo, useMemo, useState } from 'react'; +import React, { memo, useState } from 'react'; import { View, StyleSheet, Text, TouchableOpacity } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; import MarkdownComponent from './MarkdownComponent'; @@ -18,8 +18,7 @@ interface Props { const ThinkingBlock = memo( ({ content, isComplete = true, inProgress }: Props) => { const [expanded, setExpanded] = useState(!isComplete); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const toggleExpanded = () => { if (inProgress) return; diff --git a/components/chat-screen/UserMessageActionMenu.tsx b/components/chat-screen/UserMessageActionMenu.tsx index 39e2efc3..686af33a 100644 --- a/components/chat-screen/UserMessageActionMenu.tsx +++ b/components/chat-screen/UserMessageActionMenu.tsx @@ -1,9 +1,9 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import CopyIcon from '../../assets/icons/copy.svg'; import { fontFamily, fontSizes, lineHeights } from '../../styles/fontStyles'; import { SvgComponent } from '../../utils/SvgComponent'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; type MenuItemProps = { @@ -13,8 +13,7 @@ type MenuItemProps = { }; const MenuItem = ({ label, icon: Icon, onPress }: MenuItemProps) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/components/drawer/CustomDrawer.tsx b/components/drawer/CustomDrawer.tsx index e161d595..789b4d69 100644 --- a/components/drawer/CustomDrawer.tsx +++ b/components/drawer/CustomDrawer.tsx @@ -1,13 +1,7 @@ -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { View, StyleSheet, TextInput, Keyboard } from 'react-native'; import { ScrollView } from 'react-native-gesture-handler'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import DrawerMenu from './DrawerMenu'; import { DrawerSearchOverlay } from './DrawerSearchOverlay'; @@ -20,8 +14,7 @@ import { const CustomDrawer = ({ navigation }: DrawerContentComponentProps) => { const isFirstRender = useRef(true); - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const status = useDrawerStatus(); const [searching, setSearching] = useState(false); diff --git a/components/drawer/DrawerEmptyState.tsx b/components/drawer/DrawerEmptyState.tsx index 85b00eb1..2654351b 100644 --- a/components/drawer/DrawerEmptyState.tsx +++ b/components/drawer/DrawerEmptyState.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { useSQLiteContext } from 'expo-sqlite'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { useLLMStore } from '../../store/llmStore'; import { startPhantomChat } from '../../utils/startPhantomChat'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; @@ -15,8 +15,7 @@ interface Props { } export const DrawerEmptyState = ({ onNavigate }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const db = useSQLiteContext(); const { interrupt } = useLLMStore(); diff --git a/components/drawer/DrawerItem.tsx b/components/drawer/DrawerItem.tsx index 3dcc073c..ad97c8bb 100644 --- a/components/drawer/DrawerItem.tsx +++ b/components/drawer/DrawerItem.tsx @@ -1,7 +1,7 @@ -import React, { memo, useMemo } from 'react'; +import React, { memo } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { Pressable } from 'react-native-gesture-handler'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; @@ -25,8 +25,7 @@ export const DrawerItem = memo( testID, hugContent, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const router = useRouter(); const pathname = usePathname(); diff --git a/components/drawer/DrawerNavSection.tsx b/components/drawer/DrawerNavSection.tsx index ad0e4e86..bfba32dc 100644 --- a/components/drawer/DrawerNavSection.tsx +++ b/components/drawer/DrawerNavSection.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import { useRouter, usePathname } from 'expo-router'; import Animated, { @@ -10,7 +10,7 @@ import Animated, { withTiming, } from 'react-native-reanimated'; import { useSQLiteContext } from 'expo-sqlite'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { useLLMStore } from '../../store/llmStore'; import { startPhantomChat } from '../../utils/startPhantomChat'; import { Theme } from '../../styles/colors'; @@ -40,8 +40,7 @@ export const DrawerNavSection = ({ onMeasured, onNavigate, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const router = useRouter(); const pathname = usePathname(); diff --git a/components/drawer/DrawerSearchOverlay.tsx b/components/drawer/DrawerSearchOverlay.tsx index d75e18cd..f70eb817 100644 --- a/components/drawer/DrawerSearchOverlay.tsx +++ b/components/drawer/DrawerSearchOverlay.tsx @@ -1,4 +1,4 @@ -import React, { RefObject, useCallback, useMemo } from 'react'; +import React, { RefObject, useCallback } from 'react'; import { Modal, StyleSheet, @@ -17,8 +17,8 @@ import { } from 'react-native-gesture-handler'; import { BottomSheetModalProvider } from '@gorhom/bottom-sheet'; import { useReanimatedKeyboardAnimation } from 'react-native-keyboard-controller'; -import { useTheme } from '../../context/ThemeContext'; import { Theme } from '../../styles/colors'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import DrawerMenu from './DrawerMenu'; import { useSearchOverlayAnimation } from './useSearchOverlayAnimation'; import { @@ -57,8 +57,7 @@ export const DrawerSearchOverlay = ({ inputRef, scrollOffsetRef, }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); const { width: screenWidth } = useWindowDimensions(); const { mounted, progress, contentProgress, backdropOpacity, startExpand } = diff --git a/components/drawer/DrawerToggleButton.tsx b/components/drawer/DrawerToggleButton.tsx index 29531f7c..21215f23 100644 --- a/components/drawer/DrawerToggleButton.tsx +++ b/components/drawer/DrawerToggleButton.tsx @@ -1,15 +1,14 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { TouchableOpacity, StyleSheet } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import Menu from '../../assets/icons/menu.svg'; import { useNavigation } from 'expo-router'; import { DrawerContentComponentProps } from '@react-navigation/drawer'; const DrawerToggleButton = () => { - const { theme } = useTheme(); const navigation: DrawerContentComponentProps['navigation'] = useNavigation(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const { width: screenWidth } = useWindowDimensions(); const collapsedTitleWidth = diff --git a/components/model-hub/FamilyCard.tsx b/components/model-hub/FamilyCard.tsx index 8578c6d9..4124a73c 100644 --- a/components/model-hub/FamilyCard.tsx +++ b/components/model-hub/FamilyCard.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { TouchableOpacity } from 'react-native-gesture-handler'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { ModelFamily } from '../../utils/modelFamily'; @@ -14,8 +14,7 @@ interface Props { } const FamilyCard = ({ family, onPress }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); const downloadedCount = family.models.filter((m) => m.isDownloaded).length; const variantCount = family.models.length; diff --git a/components/model-hub/FloatingActionButton.tsx b/components/model-hub/FloatingActionButton.tsx index 32b3ed96..adb070a4 100644 --- a/components/model-hub/FloatingActionButton.tsx +++ b/components/model-hub/FloatingActionButton.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, TouchableOpacity } from 'react-native'; import PlusIcon from '../../assets/icons/plus.svg'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; interface Props { @@ -10,11 +10,7 @@ interface Props { } const FloatingActionButton = ({ onPress, disabled = false }: Props) => { - const { theme } = useTheme(); - const styles = useMemo( - () => createStyles(theme, disabled), - [theme, disabled] - ); + const { styles } = useThemedStyles(createStyles, disabled); return ( { - const { theme } = useTheme(); - const styles = useMemo( - () => createStyles(theme, selected), - [theme, selected] - ); + const { styles, theme } = useThemedStyles(createStyles, selected); const { downloadStates, downloadModel, cancelDownload, removeModelFiles } = useModelStore(); diff --git a/components/model-hub/ModelHubTabs.tsx b/components/model-hub/ModelHubTabs.tsx index ec4c2aef..7d1dd63e 100644 --- a/components/model-hub/ModelHubTabs.tsx +++ b/components/model-hub/ModelHubTabs.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, Text, TouchableOpacity, View } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; @@ -18,8 +18,7 @@ const TABS: { key: ModelHubTab; label: string }[] = [ ]; const ModelHubTabs = ({ value, onChange }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( diff --git a/components/model-hub/SortingTag.tsx b/components/model-hub/SortingTag.tsx index 9ce5118e..e358d2b4 100644 --- a/components/model-hub/SortingTag.tsx +++ b/components/model-hub/SortingTag.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { Text, StyleSheet, TouchableOpacity, View } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontSizes, fontFamily } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; import CheckIcon from '../../assets/icons/check.svg'; @@ -12,11 +12,7 @@ interface Props { } const SortingTag = ({ text, selected, onPress }: Props) => { - const { theme } = useTheme(); - const styles = useMemo( - () => createStyles(theme, selected), - [theme, selected] - ); + const { styles } = useThemedStyles(createStyles, selected); return ( diff --git a/components/onboarding/CloseButton.tsx b/components/onboarding/CloseButton.tsx index f64c414a..ff640e41 100644 --- a/components/onboarding/CloseButton.tsx +++ b/components/onboarding/CloseButton.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, TouchableOpacity } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import CloseIcon from '../../assets/icons/cross-small.svg'; @@ -9,8 +9,7 @@ interface Props { } function CloseButton({ onPress }: Props) { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); return ( diff --git a/components/onboarding/OnboardingIntroPanel.tsx b/components/onboarding/OnboardingIntroPanel.tsx index 923023da..ac28169a 100644 --- a/components/onboarding/OnboardingIntroPanel.tsx +++ b/components/onboarding/OnboardingIntroPanel.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import PrimaryButton from '../PrimaryButton'; import TextLogo from '../../assets/text_logo.svg'; @@ -11,8 +11,7 @@ interface Props { } function OnboardingIntroPanel({ onPressStart }: Props) { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); return ( diff --git a/components/onboarding/OnboardingStepPanel.tsx b/components/onboarding/OnboardingStepPanel.tsx index dc1dd563..873e68de 100644 --- a/components/onboarding/OnboardingStepPanel.tsx +++ b/components/onboarding/OnboardingStepPanel.tsx @@ -1,6 +1,6 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, Text, View } from 'react-native'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { Theme } from '../../styles/colors'; import { fontFamily, fontSizes, lineHeights } from '../../styles/fontStyles'; import PrimaryButton from '../PrimaryButton'; @@ -26,8 +26,7 @@ function OnboardingStepPanel({ onNextPress, buttonPrimary, }: OnboardingStepPanelProps) { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles, theme } = useThemedStyles(createStyles); return ( diff --git a/components/settings/SettingsRow.tsx b/components/settings/SettingsRow.tsx index e0b0ba07..a8621cb6 100644 --- a/components/settings/SettingsRow.tsx +++ b/components/settings/SettingsRow.tsx @@ -1,7 +1,7 @@ -import React, { useMemo } from 'react'; +import React from 'react'; import { StyleSheet, Text } from 'react-native'; import { Pressable } from 'react-native-gesture-handler'; -import { useTheme } from '../../context/ThemeContext'; +import { useThemedStyles } from '../../hooks/useThemedStyles'; import { fontFamily, fontSizes } from '../../styles/fontStyles'; import { Theme } from '../../styles/colors'; @@ -12,8 +12,7 @@ interface Props { } export const SettingsRow = ({ label, icon, onPress }: Props) => { - const { theme } = useTheme(); - const styles = useMemo(() => createStyles(theme), [theme]); + const { styles } = useThemedStyles(createStyles); return ( createStyles(theme), [theme]); + * + * boilerplate. Extra args cover parametric factories, e.g. + * `useThemedStyles(createStyles, disabled)`. + */ +export function useThemedStyles( + factory: (theme: Theme, ...args: Args) => T, + ...args: Args +): { styles: T; theme: Theme } { + const { theme } = useTheme(); + const styles = useMemo( + () => factory(theme, ...args), + // eslint-disable-next-line react-hooks/exhaustive-deps + [theme, factory, ...args] + ); + return { styles, theme }; +}