about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx6
-rw-r--r--src/App.web.tsx6
-rw-r--r--src/Splash.tsx4
-rw-r--r--src/alf/themes.ts85
-rw-r--r--src/alf/util/useColorModeTheme.ts54
-rw-r--r--src/lib/ThemeContext.tsx28
-rw-r--r--src/lib/themes.ts13
-rw-r--r--src/state/persisted/legacy.ts1
-rw-r--r--src/state/persisted/schema.ts2
-rw-r--r--src/state/shell/color-mode.tsx70
-rw-r--r--src/state/shell/index.tsx2
-rw-r--r--src/view/screens/Settings.tsx36
-rw-r--r--src/view/screens/Storybook/index.tsx24
13 files changed, 197 insertions, 134 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 41b78fc98..50a80d9fe 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -17,7 +17,6 @@ import {ThemeProvider as Alf} from '#/alf'
 import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
 import {init as initPersistedState} from '#/state/persisted'
 import {listenSessionDropped} from './state/events'
-import {useColorMode} from 'state/shell'
 import {ThemeProvider} from 'lib/ThemeContext'
 import {s} from 'lib/styles'
 import {Shell} from 'view/shell'
@@ -49,10 +48,9 @@ import {useLingui} from '@lingui/react'
 SplashScreen.preventAutoHideAsync()
 
 function InnerApp() {
-  const colorMode = useColorMode()
   const {isInitialLoad, currentAccount} = useSession()
   const {resumeSession} = useSessionApi()
-  const theme = useColorModeTheme(colorMode)
+  const theme = useColorModeTheme()
   const {_} = useLingui()
 
   // init
@@ -75,7 +73,7 @@ function InnerApp() {
             key={currentAccount?.did}>
             <LoggedOutViewProvider>
               <UnreadNotifsProvider>
-                <ThemeProvider theme={colorMode}>
+                <ThemeProvider theme={theme}>
                   {/* All components should be within this provider */}
                   <RootSiblingParent>
                     <GestureHandlerRootView style={s.h100pct}>
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 1efa0567c..9d1bd3506 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -10,7 +10,6 @@ import 'view/icons'
 import {ThemeProvider as Alf} from '#/alf'
 import {useColorModeTheme} from '#/alf/util/useColorModeTheme'
 import {init as initPersistedState} from '#/state/persisted'
-import {useColorMode} from 'state/shell'
 import {Shell} from 'view/shell/index'
 import {ToastContainer} from 'view/com/util/Toast.web'
 import {ThemeProvider} from 'lib/ThemeContext'
@@ -36,8 +35,7 @@ import {Provider as PortalProvider} from '#/components/Portal'
 function InnerApp() {
   const {isInitialLoad, currentAccount} = useSession()
   const {resumeSession} = useSessionApi()
-  const colorMode = useColorMode()
-  const theme = useColorModeTheme(colorMode)
+  const theme = useColorModeTheme()
 
   // init
   useEffect(() => {
@@ -55,7 +53,7 @@ function InnerApp() {
         key={currentAccount?.did}>
         <LoggedOutViewProvider>
           <UnreadNotifsProvider>
-            <ThemeProvider theme={colorMode}>
+            <ThemeProvider theme={theme}>
               {/* All components should be within this provider */}
               <RootSiblingParent>
                 <SafeAreaProvider>
diff --git a/src/Splash.tsx b/src/Splash.tsx
index 99f9a100d..80d0a66e7 100644
--- a/src/Splash.tsx
+++ b/src/Splash.tsx
@@ -21,7 +21,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
 import Svg, {Path, SvgProps} from 'react-native-svg'
 
 import {isAndroid} from '#/platform/detection'
-import {useColorMode} from '#/state/shell'
+import {useThemePrefs} from 'state/shell'
 import {Logotype} from '#/view/icons/Logotype'
 
 // @ts-ignore
@@ -75,7 +75,7 @@ export function Splash(props: React.PropsWithChildren<Props>) {
     isLayoutReady &&
     reduceMotion !== undefined
 
-  const colorMode = useColorMode()
+  const {colorMode} = useThemePrefs()
   const colorScheme = useColorScheme()
   const themeName = colorMode === 'system' ? colorScheme : colorMode
   const isDarkMode = themeName === 'dark'
diff --git a/src/alf/themes.ts b/src/alf/themes.ts
index 7c6b7dab4..b36e782fd 100644
--- a/src/alf/themes.ts
+++ b/src/alf/themes.ts
@@ -71,7 +71,7 @@ export const lightPalette = {
 
 export const darkPalette: Palette = {
   white: tokens.color.gray_0,
-  black: tokens.color.gray_1000,
+  black: tokens.color.trueBlack,
 
   contrast_25: tokens.color.gray_975,
   contrast_50: tokens.color.gray_950,
@@ -130,6 +130,11 @@ export const darkPalette: Palette = {
   negative_975: tokens.color.red_975,
 } as const
 
+export const dimPalette: Palette = {
+  ...darkPalette,
+  black: tokens.color.gray_1000,
+} as const
+
 export const light = {
   name: 'light',
   palette: lightPalette,
@@ -191,70 +196,6 @@ export const light = {
   },
 }
 
-export const dim: Theme = {
-  name: 'dim',
-  palette: darkPalette,
-  atoms: {
-    text: {
-      color: darkPalette.white,
-    },
-    text_contrast_700: {
-      color: darkPalette.contrast_800,
-    },
-    text_contrast_600: {
-      color: darkPalette.contrast_700,
-    },
-    text_contrast_500: {
-      color: darkPalette.contrast_600,
-    },
-    text_contrast_400: {
-      color: darkPalette.contrast_500,
-    },
-    text_inverted: {
-      color: darkPalette.black,
-    },
-    bg: {
-      backgroundColor: darkPalette.contrast_50,
-    },
-    bg_contrast_25: {
-      backgroundColor: darkPalette.contrast_100,
-    },
-    bg_contrast_50: {
-      backgroundColor: darkPalette.contrast_200,
-    },
-    bg_contrast_100: {
-      backgroundColor: darkPalette.contrast_300,
-    },
-    bg_contrast_200: {
-      backgroundColor: darkPalette.contrast_400,
-    },
-    bg_contrast_300: {
-      backgroundColor: darkPalette.contrast_500,
-    },
-    border: {
-      borderColor: darkPalette.contrast_200,
-    },
-    border_contrast: {
-      borderColor: darkPalette.contrast_400,
-    },
-    shadow_sm: {
-      ...atoms.shadow_sm,
-      shadowOpacity: 0.7,
-      shadowColor: tokens.color.trueBlack,
-    },
-    shadow_md: {
-      ...atoms.shadow_md,
-      shadowOpacity: 0.7,
-      shadowColor: tokens.color.trueBlack,
-    },
-    shadow_lg: {
-      ...atoms.shadow_lg,
-      shadowOpacity: 0.7,
-      shadowColor: tokens.color.trueBlack,
-    },
-  },
-}
-
 export const dark: Theme = {
   name: 'dark',
   palette: darkPalette,
@@ -318,3 +259,17 @@ export const dark: Theme = {
     },
   },
 }
+
+export const dim: Theme = {
+  ...dark,
+  name: 'dim',
+  atoms: {
+    ...dark.atoms,
+    text_inverted: {
+      color: dimPalette.black,
+    },
+    bg: {
+      backgroundColor: dimPalette.black,
+    },
+  },
+}
diff --git a/src/alf/util/useColorModeTheme.ts b/src/alf/util/useColorModeTheme.ts
index 79cebc139..49e2ec8f5 100644
--- a/src/alf/util/useColorModeTheme.ts
+++ b/src/alf/util/useColorModeTheme.ts
@@ -1,10 +1,54 @@
+import React from 'react'
 import {useColorScheme} from 'react-native'
 
-import * as persisted from '#/state/persisted'
+import {useThemePrefs} from 'state/shell'
+import {isWeb} from 'platform/detection'
+import {ThemeName, light, dark, dim} from '#/alf/themes'
+import * as SystemUI from 'expo-system-ui'
 
-export function useColorModeTheme(
-  theme: persisted.Schema['colorMode'],
-): 'light' | 'dark' {
+export function useColorModeTheme(): ThemeName {
   const colorScheme = useColorScheme()
-  return (theme === 'system' ? colorScheme : theme) || 'light'
+  const {colorMode, darkTheme} = useThemePrefs()
+
+  return React.useMemo(() => {
+    if (
+      (colorMode === 'system' && colorScheme === 'light') ||
+      colorMode === 'light'
+    ) {
+      updateDocument('light')
+      updateSystemBackground('light')
+      return 'light'
+    } else {
+      const themeName = darkTheme ?? 'dim'
+      updateDocument(themeName)
+      updateSystemBackground(themeName)
+      return themeName
+    }
+  }, [colorMode, darkTheme, colorScheme])
+}
+
+function updateDocument(theme: ThemeName) {
+  // @ts-ignore web only
+  if (isWeb && typeof window !== 'undefined') {
+    // @ts-ignore web only
+    const html = window.document.documentElement
+    // remove any other color mode classes
+    html.className = html.className.replace(/(theme)--\w+/g, '')
+
+    html.classList.add(`theme--${theme}`)
+  }
+}
+
+function updateSystemBackground(theme: ThemeName) {
+  switch (theme) {
+    case 'light':
+      SystemUI.setBackgroundColorAsync(light.atoms.bg.backgroundColor)
+      break
+    case 'dark':
+      SystemUI.setBackgroundColorAsync(dark.atoms.bg.backgroundColor)
+      break
+    case 'dim':
+      SystemUI.setBackgroundColorAsync(dim.atoms.bg.backgroundColor)
+      break
+  }
 }
diff --git a/src/lib/ThemeContext.tsx b/src/lib/ThemeContext.tsx
index 38bd199cb..63e2beeb1 100644
--- a/src/lib/ThemeContext.tsx
+++ b/src/lib/ThemeContext.tsx
@@ -1,11 +1,7 @@
 import React, {ReactNode, createContext, useContext} from 'react'
-import {
-  TextStyle,
-  useColorScheme,
-  ViewStyle,
-  ColorSchemeName,
-} from 'react-native'
-import {darkTheme, defaultTheme} from './themes'
+import {TextStyle, ViewStyle} from 'react-native'
+import {darkTheme, defaultTheme, dimTheme} from './themes'
+import {ThemeName} from '#/alf/themes'
 
 export type ColorScheme = 'light' | 'dark'
 
@@ -84,23 +80,31 @@ export interface Theme {
 
 export interface ThemeProviderProps {
   children?: ReactNode
-  theme?: 'light' | 'dark' | 'system'
+  theme: ThemeName
 }
 
 export const ThemeContext = createContext<Theme>(defaultTheme)
 
 export const useTheme = () => useContext(ThemeContext)
 
-function getTheme(theme: ColorSchemeName) {
-  return theme === 'dark' ? darkTheme : defaultTheme
+function getTheme(theme: ThemeName) {
+  switch (theme) {
+    case 'light':
+      return defaultTheme
+    case 'dim':
+      return dimTheme
+    case 'dark':
+      return darkTheme
+    default:
+      return defaultTheme
+  }
 }
 
 export const ThemeProvider: React.FC<ThemeProviderProps> = ({
   theme,
   children,
 }) => {
-  const colorScheme = useColorScheme()
-  const themeValue = getTheme(theme === 'system' ? colorScheme : theme)
+  const themeValue = getTheme(theme)
 
   return (
     <ThemeContext.Provider value={themeValue}>{children}</ThemeContext.Provider>
diff --git a/src/lib/themes.ts b/src/lib/themes.ts
index 2d4515c77..9a3880b92 100644
--- a/src/lib/themes.ts
+++ b/src/lib/themes.ts
@@ -2,7 +2,7 @@ import {Platform} from 'react-native'
 import type {Theme} from './ThemeContext'
 import {colors} from './styles'
 
-import {darkPalette, lightPalette} from '#/alf/themes'
+import {darkPalette, lightPalette, dimPalette} from '#/alf/themes'
 
 export const defaultTheme: Theme = {
   colorScheme: 'light',
@@ -336,3 +336,14 @@ export const darkTheme: Theme = {
     },
   },
 }
+
+export const dimTheme: Theme = {
+  ...darkTheme,
+  palette: {
+    ...darkTheme.palette,
+    default: {
+      ...darkTheme.palette.default,
+      background: dimPalette.black,
+    },
+  },
+}
diff --git a/src/state/persisted/legacy.ts b/src/state/persisted/legacy.ts
index 6bb75ae86..767faf48f 100644
--- a/src/state/persisted/legacy.ts
+++ b/src/state/persisted/legacy.ts
@@ -69,6 +69,7 @@ const DEPRECATED_ROOT_STATE_STORAGE_KEY = 'root'
 export function transform(legacy: Partial<LegacySchema>): Schema {
   return {
     colorMode: legacy.shell?.colorMode || defaults.colorMode,
+    darkTheme: defaults.darkTheme,
     session: {
       accounts: legacy.session?.accounts || defaults.session.accounts,
       currentAccount:
diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts
index 870e14aaf..ade97ef74 100644
--- a/src/state/persisted/schema.ts
+++ b/src/state/persisted/schema.ts
@@ -18,6 +18,7 @@ export type PersistedAccount = z.infer<typeof accountSchema>
 
 export const schema = z.object({
   colorMode: z.enum(['system', 'light', 'dark']),
+  darkTheme: z.enum(['dim', 'dark']).optional(),
   session: z.object({
     accounts: z.array(accountSchema),
     currentAccount: accountSchema.optional(),
@@ -60,6 +61,7 @@ export type Schema = z.infer<typeof schema>
 
 export const defaults: Schema = {
   colorMode: 'system',
+  darkTheme: 'dim',
   session: {
     accounts: [],
     currentAccount: undefined,
diff --git a/src/state/shell/color-mode.tsx b/src/state/shell/color-mode.tsx
index 192b88314..4f0391aa6 100644
--- a/src/state/shell/color-mode.tsx
+++ b/src/state/shell/color-mode.tsx
@@ -1,57 +1,65 @@
 import React from 'react'
-import {isWeb} from '#/platform/detection'
 import * as persisted from '#/state/persisted'
 
-type StateContext = persisted.Schema['colorMode']
-type SetContext = (v: persisted.Schema['colorMode']) => void
+type StateContext = {
+  colorMode: persisted.Schema['colorMode']
+  darkTheme: persisted.Schema['darkTheme']
+}
+type SetContext = {
+  setColorMode: (v: persisted.Schema['colorMode']) => void
+  setDarkTheme: (v: persisted.Schema['darkTheme']) => void
+}
 
-const stateContext = React.createContext<StateContext>('system')
-const setContext = React.createContext<SetContext>(
-  (_: persisted.Schema['colorMode']) => {},
-)
+const stateContext = React.createContext<StateContext>({
+  colorMode: 'system',
+  darkTheme: 'dark',
+})
+const setContext = React.createContext<SetContext>({} as SetContext)
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
-  const [state, setState] = React.useState(persisted.get('colorMode'))
+  const [colorMode, setColorMode] = React.useState(persisted.get('colorMode'))
+  const [darkTheme, setDarkTheme] = React.useState(persisted.get('darkTheme'))
 
-  const setStateWrapped = React.useCallback(
-    (colorMode: persisted.Schema['colorMode']) => {
-      setState(colorMode)
-      persisted.write('colorMode', colorMode)
-      updateDocument(colorMode)
+  const setColorModeWrapped = React.useCallback(
+    (_colorMode: persisted.Schema['colorMode']) => {
+      setColorMode(_colorMode)
+      persisted.write('colorMode', _colorMode)
     },
-    [setState],
+    [setColorMode],
+  )
+
+  const setDarkThemeWrapped = React.useCallback(
+    (_darkTheme: persisted.Schema['darkTheme']) => {
+      setDarkTheme(_darkTheme)
+      persisted.write('darkTheme', _darkTheme)
+    },
+    [setDarkTheme],
   )
 
   React.useEffect(() => {
-    updateDocument(persisted.get('colorMode')) // set on load
     return persisted.onUpdate(() => {
-      setState(persisted.get('colorMode'))
-      updateDocument(persisted.get('colorMode'))
+      setColorModeWrapped(persisted.get('colorMode'))
+      setDarkThemeWrapped(persisted.get('darkTheme'))
     })
-  }, [setState])
+  }, [setColorModeWrapped, setDarkThemeWrapped])
 
   return (
-    <stateContext.Provider value={state}>
-      <setContext.Provider value={setStateWrapped}>
+    <stateContext.Provider value={{colorMode, darkTheme}}>
+      <setContext.Provider
+        value={{
+          setDarkTheme: setDarkThemeWrapped,
+          setColorMode: setColorModeWrapped,
+        }}>
         {children}
       </setContext.Provider>
     </stateContext.Provider>
   )
 }
 
-export function useColorMode() {
+export function useThemePrefs() {
   return React.useContext(stateContext)
 }
 
-export function useSetColorMode() {
+export function useSetThemePrefs() {
   return React.useContext(setContext)
 }
-
-function updateDocument(colorMode: string) {
-  if (isWeb && typeof window !== 'undefined') {
-    const html = window.document.documentElement
-    // remove any other color mode classes
-    html.className = html.className.replace(/colorMode--\w+/g, '')
-    html.classList.add(`colorMode--${colorMode}`)
-  }
-}
diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx
index 53f05055c..07909c000 100644
--- a/src/state/shell/index.tsx
+++ b/src/state/shell/index.tsx
@@ -14,7 +14,7 @@ export {
   useSetDrawerSwipeDisabled,
 } from './drawer-swipe-disabled'
 export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode'
-export {useColorMode, useSetColorMode} from './color-mode'
+export {useThemePrefs, useSetThemePrefs} from './color-mode'
 export {useOnboardingState, useOnboardingDispatch} from './onboarding'
 export {useComposerState, useComposerControls} from './composer'
 export {useTickEveryMinute} from './tick-every-minute'
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 17e4b45c5..104506576 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -40,8 +40,8 @@ import {RQKEY as RQKEY_PROFILE} from '#/state/queries/profile'
 import {useModalControls} from '#/state/modals'
 import {
   useSetMinimalShellMode,
-  useColorMode,
-  useSetColorMode,
+  useThemePrefs,
+  useSetThemePrefs,
   useOnboardingDispatch,
 } from '#/state/shell'
 import {
@@ -144,8 +144,8 @@ function SettingsAccountCard({account}: {account: SessionAccount}) {
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
 export function SettingsScreen({}: Props) {
   const queryClient = useQueryClient()
-  const colorMode = useColorMode()
-  const setColorMode = useSetColorMode()
+  const {colorMode, darkTheme} = useThemePrefs()
+  const {setColorMode, setDarkTheme} = useSetThemePrefs()
   const pal = usePalette('default')
   const {_} = useLingui()
   const setMinimalShellMode = useSetMinimalShellMode()
@@ -483,8 +483,36 @@ export function SettingsScreen({}: Props) {
             />
           </View>
         </View>
+
         <View style={styles.spacer20} />
 
+        {colorMode !== 'light' && (
+          <>
+            <Text type="xl-bold" style={[pal.text, styles.heading]}>
+              <Trans>Dark Theme</Trans>
+            </Text>
+            <View>
+              <View style={[styles.linkCard, pal.view, styles.selectableBtns]}>
+                <SelectableBtn
+                  selected={!darkTheme || darkTheme === 'dim'}
+                  label={_(msg`Dim`)}
+                  left
+                  onSelect={() => setDarkTheme('dim')}
+                  accessibilityHint={_(msg`Set dark theme to the dim theme`)}
+                />
+                <SelectableBtn
+                  selected={darkTheme === 'dark'}
+                  label={_(msg`Dark`)}
+                  right
+                  onSelect={() => setDarkTheme('dark')}
+                  accessibilityHint={_(msg`Set dark theme to the dark theme`)}
+                />
+              </View>
+            </View>
+            <View style={styles.spacer20} />
+          </>
+        )}
+
         <Text type="xl-bold" style={[pal.text, styles.heading]}>
           <Trans>Basics</Trans>
         </Text>
diff --git a/src/view/screens/Storybook/index.tsx b/src/view/screens/Storybook/index.tsx
index d8898f20e..40929555e 100644
--- a/src/view/screens/Storybook/index.tsx
+++ b/src/view/screens/Storybook/index.tsx
@@ -3,7 +3,7 @@ import {View} from 'react-native'
 import {CenteredView, ScrollView} from '#/view/com/util/Views'
 
 import {atoms as a, useTheme, ThemeProvider} from '#/alf'
-import {useSetColorMode} from '#/state/shell'
+import {useSetThemePrefs} from '#/state/shell'
 import {Button} from '#/components/Button'
 
 import {Theming} from './Theming'
@@ -19,7 +19,7 @@ import {Icons} from './Icons'
 
 export function Storybook() {
   const t = useTheme()
-  const setColorMode = useSetColorMode()
+  const {setColorMode, setDarkTheme} = useSetThemePrefs()
 
   return (
     <ScrollView>
@@ -38,7 +38,7 @@ export function Storybook() {
               variant="solid"
               color="secondary"
               size="small"
-              label='Set theme to "system"'
+              label='Set theme to "light"'
               onPress={() => setColorMode('light')}>
               Light
             </Button>
@@ -46,8 +46,22 @@ export function Storybook() {
               variant="solid"
               color="secondary"
               size="small"
-              label='Set theme to "system"'
-              onPress={() => setColorMode('dark')}>
+              label='Set theme to "dim"'
+              onPress={() => {
+                setColorMode('dark')
+                setDarkTheme('dim')
+              }}>
+              Dim
+            </Button>
+            <Button
+              variant="solid"
+              color="secondary"
+              size="small"
+              label='Set theme to "dark"'
+              onPress={() => {
+                setColorMode('dark')
+                setDarkTheme('dark')
+              }}>
               Dark
             </Button>
           </View>