about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/App.native.tsx7
-rw-r--r--src/state/models/root-store.ts4
-rw-r--r--src/state/models/shell-ui.ts22
-rw-r--r--src/view/shell/mobile/Menu.tsx342
4 files changed, 206 insertions, 169 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index fa523cd81..3cce25548 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -5,12 +5,13 @@ import {RootSiblingParent} from 'react-native-root-siblings'
 import {GestureHandlerRootView} from 'react-native-gesture-handler'
 import SplashScreen from 'react-native-splash-screen'
 import {SafeAreaProvider} from 'react-native-safe-area-context'
+import {observer} from 'mobx-react-lite'
 import {ThemeProvider} from './view/lib/ThemeContext'
 import * as view from './view/index'
 import {RootStoreModel, setupState, RootStoreProvider} from './state'
 import {MobileShell} from './view/shell/mobile'
 
-function App() {
+const App = observer(() => {
   const [rootStore, setRootStore] = useState<RootStoreModel | undefined>(
     undefined,
   )
@@ -41,7 +42,7 @@ function App() {
     <GestureHandlerRootView style={{flex: 1}}>
       <RootSiblingParent>
         <RootStoreProvider value={rootStore}>
-          <ThemeProvider>
+          <ThemeProvider theme={rootStore.shell.darkMode ? 'dark' : 'light'}>
             <SafeAreaProvider>
               <MobileShell />
             </SafeAreaProvider>
@@ -50,6 +51,6 @@ function App() {
       </RootSiblingParent>
     </GestureHandlerRootView>
   )
-}
+})
 
 export default App
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 26f947c56..54578b4a5 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -67,6 +67,7 @@ export class RootStoreModel {
       me: this.me.serialize(),
       nav: this.nav.serialize(),
       onboard: this.onboard.serialize(),
+      shell: this.shell.serialize(),
     }
   }
 
@@ -84,6 +85,9 @@ export class RootStoreModel {
       if (hasProp(v, 'onboard')) {
         this.onboard.hydrate(v.onboard)
       }
+      if (hasProp(v, 'shell')) {
+        this.shell.hydrate(v.shell)
+      }
     }
   }
 
diff --git a/src/state/models/shell-ui.ts b/src/state/models/shell-ui.ts
index 6b3134ff2..52d081680 100644
--- a/src/state/models/shell-ui.ts
+++ b/src/state/models/shell-ui.ts
@@ -1,5 +1,6 @@
 import {makeAutoObservable} from 'mobx'
 import {ProfileViewModel} from './profile-view'
+import {isObj, hasProp} from '../lib/type-guards'
 
 export class ConfirmModal {
   name = 'confirm'
@@ -135,6 +136,7 @@ export interface ComposerOpts {
 }
 
 export class ShellUiModel {
+  darkMode = false
   minimalShellMode = false
   isMainMenuOpen = false
   isModalActive = false
@@ -156,7 +158,25 @@ export class ShellUiModel {
   composerOpts: ComposerOpts | undefined
 
   constructor() {
-    makeAutoObservable(this)
+    makeAutoObservable(this, {serialize: false, hydrate: false})
+  }
+
+  serialize(): unknown {
+    return {
+      darkMode: this.darkMode,
+    }
+  }
+
+  hydrate(v: unknown) {
+    if (isObj(v)) {
+      if (hasProp(v, 'darkMode') && typeof v.darkMode === 'boolean') {
+        this.darkMode = v.darkMode
+      }
+    }
+  }
+
+  setDarkMode(v: boolean) {
+    this.darkMode = v
   }
 
   setMinimalShellMode(v: boolean) {
diff --git a/src/view/shell/mobile/Menu.tsx b/src/view/shell/mobile/Menu.tsx
index 84c3f494e..81f2164da 100644
--- a/src/view/shell/mobile/Menu.tsx
+++ b/src/view/shell/mobile/Menu.tsx
@@ -6,6 +6,7 @@ import {
   View,
   ViewStyle,
 } from 'react-native'
+import {observer} from 'mobx-react-lite'
 import VersionNumber from 'react-native-version-number'
 import {s, colors} from '../../lib/styles'
 import {useStores} from '../../../state'
@@ -18,187 +19,198 @@ import {
 } from '../../lib/icons'
 import {UserAvatar} from '../../com/util/UserAvatar'
 import {Text} from '../../com/util/text/Text'
+import {ToggleButton} from '../../com/util/forms/ToggleButton'
 import {CreateSceneModal} from '../../../state/models/shell-ui'
 import {usePalette} from '../../lib/hooks/usePalette'
 
-export const Menu = ({
-  visible,
-  onClose,
-}: {
-  visible: boolean
-  onClose: () => void
-}) => {
-  const pal = usePalette('default')
-  const store = useStores()
+export const Menu = observer(
+  ({visible, onClose}: {visible: boolean; onClose: () => void}) => {
+    const pal = usePalette('default')
+    const store = useStores()
 
-  useEffect(() => {
-    if (visible) {
-      // trigger a refresh in case memberships have changed recently
-      // TODO this impacts performance, need to find the right time to do this
-      // store.me.refreshMemberships()
-    }
-  }, [store, visible])
+    useEffect(() => {
+      if (visible) {
+        // trigger a refresh in case memberships have changed recently
+        // TODO this impacts performance, need to find the right time to do this
+        // store.me.refreshMemberships()
+      }
+    }, [store, visible])
 
-  // events
-  // =
+    // events
+    // =
 
-  const onNavigate = (url: string) => {
-    onClose()
-    if (url === '/notifications') {
-      store.nav.switchTo(1, true)
-    } else {
-      store.nav.switchTo(0, true)
-      if (url !== '/') {
-        store.nav.navigate(url)
+    const onNavigate = (url: string) => {
+      onClose()
+      if (url === '/notifications') {
+        store.nav.switchTo(1, true)
+      } else {
+        store.nav.switchTo(0, true)
+        if (url !== '/') {
+          store.nav.navigate(url)
+        }
       }
     }
-  }
-  const onPressCreateScene = () => {
-    onClose()
-    store.shell.openModal(new CreateSceneModal())
-  }
+    const onPressCreateScene = () => {
+      onClose()
+      store.shell.openModal(new CreateSceneModal())
+    }
 
-  // rendering
-  // =
+    // rendering
+    // =
 
-  const MenuItem = ({
-    icon,
-    label,
-    count,
-    url,
-    bold,
-    onPress,
-  }: {
-    icon: JSX.Element
-    label: string
-    count?: number
-    url?: string
-    bold?: boolean
-    onPress?: () => void
-  }) => (
-    <TouchableOpacity
-      style={styles.menuItem}
-      onPress={onPress ? onPress : () => onNavigate(url || '/')}>
-      <View style={[styles.menuItemIconWrapper]}>
-        {icon}
-        {count ? (
-          <View style={styles.menuItemCount}>
-            <Text style={styles.menuItemCountLabel}>{count}</Text>
-          </View>
-        ) : undefined}
-      </View>
-      <Text
-        type="h4"
-        style={[
-          pal.text,
-          bold ? styles.menuItemLabelBold : styles.menuItemLabel,
-        ]}
-        numberOfLines={1}>
-        {label}
-      </Text>
-    </TouchableOpacity>
-  )
-
-  return (
-    <View style={[styles.view, pal.view]}>
+    const MenuItem = ({
+      icon,
+      label,
+      count,
+      url,
+      bold,
+      onPress,
+    }: {
+      icon: JSX.Element
+      label: string
+      count?: number
+      url?: string
+      bold?: boolean
+      onPress?: () => void
+    }) => (
       <TouchableOpacity
-        onPress={() => onNavigate(`/profile/${store.me.handle}`)}
-        style={styles.profileCard}>
-        <UserAvatar
-          size={60}
-          displayName={store.me.displayName}
-          handle={store.me.handle}
-          avatar={store.me.avatar}
-        />
-        <View style={s.flex1}>
-          <Text
-            type="h3"
-            style={[pal.text, styles.profileCardDisplayName]}
-            numberOfLines={1}>
-            {store.me.displayName || store.me.handle}
-          </Text>
-          <Text
-            style={[pal.textLight, styles.profileCardHandle]}
-            numberOfLines={1}>
-            @{store.me.handle}
-          </Text>
+        style={styles.menuItem}
+        onPress={onPress ? onPress : () => onNavigate(url || '/')}>
+        <View style={[styles.menuItemIconWrapper]}>
+          {icon}
+          {count ? (
+            <View style={styles.menuItemCount}>
+              <Text style={styles.menuItemCountLabel}>{count}</Text>
+            </View>
+          ) : undefined}
         </View>
-      </TouchableOpacity>
-      <TouchableOpacity
-        style={[styles.searchBtn, pal.btn]}
-        onPress={() => onNavigate('/search')}>
-        <MagnifyingGlassIcon
-          style={pal.text as StyleProp<ViewStyle>}
-          size={25}
-        />
-        <Text type="h4" style={[pal.text, styles.searchBtnLabel]}>
-          Search
+        <Text
+          type="h4"
+          style={[
+            pal.text,
+            bold ? styles.menuItemLabelBold : styles.menuItemLabel,
+          ]}
+          numberOfLines={1}>
+          {label}
         </Text>
       </TouchableOpacity>
-      <View style={[styles.section, pal.border]}>
-        <MenuItem
-          icon={<HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" />}
-          label="Home"
-          url="/"
-        />
-        <MenuItem
-          icon={<BellIcon style={pal.text as StyleProp<ViewStyle>} size="28" />}
-          label="Notifications"
-          url="/notifications"
-          count={store.me.notificationCount}
-        />
-      </View>
-      <View style={[styles.section, pal.border]}>
-        <Text type="h5" style={[pal.text, styles.heading]}>
-          Scenes
-        </Text>
-        {store.me.memberships
-          ? store.me.memberships.memberships.map((membership, i) => (
-              <MenuItem
-                key={i}
-                icon={
-                  <UserAvatar
-                    size={34}
-                    displayName={membership.displayName}
-                    handle={membership.handle}
-                    avatar={membership.avatar}
-                  />
-                }
-                label={membership.displayName || membership.handle}
-                url={`/profile/${membership.handle}`}
+    )
+
+    return (
+      <View style={[styles.view, pal.view]}>
+        <TouchableOpacity
+          onPress={() => onNavigate(`/profile/${store.me.handle}`)}
+          style={styles.profileCard}>
+          <UserAvatar
+            size={60}
+            displayName={store.me.displayName}
+            handle={store.me.handle}
+            avatar={store.me.avatar}
+          />
+          <View style={s.flex1}>
+            <Text
+              type="h3"
+              style={[pal.text, styles.profileCardDisplayName]}
+              numberOfLines={1}>
+              {store.me.displayName || store.me.handle}
+            </Text>
+            <Text
+              style={[pal.textLight, styles.profileCardHandle]}
+              numberOfLines={1}>
+              @{store.me.handle}
+            </Text>
+          </View>
+        </TouchableOpacity>
+        <TouchableOpacity
+          style={[styles.searchBtn, pal.btn]}
+          onPress={() => onNavigate('/search')}>
+          <MagnifyingGlassIcon
+            style={pal.text as StyleProp<ViewStyle>}
+            size={25}
+          />
+          <Text type="h4" style={[pal.text, styles.searchBtnLabel]}>
+            Search
+          </Text>
+        </TouchableOpacity>
+        <View style={[styles.section, pal.border]}>
+          <MenuItem
+            icon={
+              <HomeIcon style={pal.text as StyleProp<ViewStyle>} size="26" />
+            }
+            label="Home"
+            url="/"
+          />
+          <MenuItem
+            icon={
+              <BellIcon style={pal.text as StyleProp<ViewStyle>} size="28" />
+            }
+            label="Notifications"
+            url="/notifications"
+            count={store.me.notificationCount}
+          />
+        </View>
+        <View style={[styles.section, pal.border]}>
+          <Text type="h5" style={[pal.text, styles.heading]}>
+            Scenes
+          </Text>
+          {store.me.memberships
+            ? store.me.memberships.memberships.map((membership, i) => (
+                <MenuItem
+                  key={i}
+                  icon={
+                    <UserAvatar
+                      size={34}
+                      displayName={membership.displayName}
+                      handle={membership.handle}
+                      avatar={membership.avatar}
+                    />
+                  }
+                  label={membership.displayName || membership.handle}
+                  url={`/profile/${membership.handle}`}
+                />
+              ))
+            : undefined}
+        </View>
+        <View style={[styles.section, pal.border]}>
+          <MenuItem
+            icon={
+              <UserGroupIcon
+                style={pal.text as StyleProp<ViewStyle>}
+                size="30"
               />
-            ))
-          : undefined}
-      </View>
-      <View style={[styles.section, pal.border]}>
-        <MenuItem
-          icon={
-            <UserGroupIcon style={pal.text as StyleProp<ViewStyle>} size="30" />
-          }
-          label="Create a scene"
-          onPress={onPressCreateScene}
-        />
-        <MenuItem
-          icon={
-            <CogIcon
-              style={pal.text as StyleProp<ViewStyle>}
-              size="30"
-              strokeWidth={2}
-            />
-          }
-          label="Settings"
-          url="/settings"
-        />
-      </View>
-      <View style={styles.footer}>
-        <Text style={[pal.textLight]}>
-          Build version {VersionNumber.appVersion} ({VersionNumber.buildVersion}
-          )
-        </Text>
+            }
+            label="Create a scene"
+            onPress={onPressCreateScene}
+          />
+          <MenuItem
+            icon={
+              <CogIcon
+                style={pal.text as StyleProp<ViewStyle>}
+                size="30"
+                strokeWidth={2}
+              />
+            }
+            label="Settings"
+            url="/settings"
+          />
+        </View>
+        <View style={[styles.section, pal.border]}>
+          <ToggleButton
+            label="Dark mode"
+            isSelected={store.shell.darkMode}
+            onPress={() => store.shell.setDarkMode(!store.shell.darkMode)}
+          />
+        </View>
+        <View style={styles.footer}>
+          <Text style={[pal.textLight]}>
+            Build version {VersionNumber.appVersion} (
+            {VersionNumber.buildVersion})
+          </Text>
+        </View>
       </View>
-    </View>
-  )
-}
+    )
+  },
+)
 
 const styles = StyleSheet.create({
   view: {