diff --git a/AGENTS.md b/AGENTS.md
index c4885b6a5ce7..5fef66811aa1 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -9,6 +9,7 @@
- Do not add new exported functions, types, or constants unless they are required outside the file. Prefer file-local helpers for one-off implementation details and tests.
- Do not edit lockfiles by hand. They are generated artifacts. If you cannot regenerate one locally, leave it unchanged.
- Components must not mutate Zustand stores directly with `useXState.setState`, `getState()`-based writes, or similar ad hoc store mutation. If a component needs to affect store state, route it through a store dispatch action or move the state out of the store.
+- When a Zustand store already uses `resetState: Z.defaultReset`, prefer calling `dispatch.resetState()` for full resets instead of manually reassigning each initial field in another dispatch action.
- During refactors, do not delete existing guards, conditionals, or platform/test-specific behavior unless you have proven they are dead and the user asked for that behavior change. Port checks like `androidIsTestDevice` forward into the new code path instead of silently dropping them.
- When addressing PR or review feedback, including bot or lint-style suggestions, do not apply it mechanically. Verify that the reported issue is real in this codebase and that the proposed fix is consistent with repo rules and improves correctness, behavior, or maintainability before making changes.
- When working from a repo plan or checklist such as `PLAN.md`, update the checklist in the same change and mark implemented items done before you finish.
diff --git a/shared/app/index.native.tsx b/shared/app/index.native.tsx
index 3d63b6bf2f38..8a2ed25a4ed9 100644
--- a/shared/app/index.native.tsx
+++ b/shared/app/index.native.tsx
@@ -1,6 +1,7 @@
///
import * as C from '@/constants'
import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
import * as Kb from '@/common-adapters'
import * as React from 'react'
import Main from './main.native'
@@ -35,7 +36,7 @@ const useDarkHookup = () => {
const initedRef = React.useRef(false)
const appStateRef = React.useRef('active')
const setSystemDarkMode = DarkMode.useDarkModeState(s => s.dispatch.setSystemDarkMode)
- const setMobileAppState = useConfigState(s => s.dispatch.setMobileAppState)
+ const setMobileAppState = useShellState(s => s.dispatch.setMobileAppState)
const setSystemSupported = DarkMode.useDarkModeState(s => s.dispatch.setSystemSupported)
const setDarkModePreference = DarkMode.useDarkModeState(s => s.dispatch.setDarkModePreference)
diff --git a/shared/chat/conversation/normal/container.tsx b/shared/chat/conversation/normal/container.tsx
index d066b772b7d7..f79480a258a8 100644
--- a/shared/chat/conversation/normal/container.tsx
+++ b/shared/chat/conversation/normal/container.tsx
@@ -1,6 +1,6 @@
import * as C from '@/constants'
import * as ConvoState from '@/stores/convostate'
-import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
import * as React from 'react'
import Normal from '.'
import * as T from '@/constants/types'
@@ -61,7 +61,7 @@ const useOrangeLine = () => {
// just use the rpc for orange line if we're not active
// if we are active we want to keep whatever state we had so it is maintained
- const active = useConfigState(s => s.active)
+ const active = useShellState(s => s.active)
React.useEffect(() => {
if (!active) {
loadOrangeLine()
@@ -69,7 +69,7 @@ const useOrangeLine = () => {
}, [maxVisibleMsgID, active])
// mobile backgrounded us
- const mobileAppState = useConfigState(s => s.mobileAppState)
+ const mobileAppState = useShellState(s => s.mobileAppState)
const lastMobileAppStateRef = React.useRef(mobileAppState)
React.useEffect(() => {
if (mobileAppState !== lastMobileAppStateRef.current) {
diff --git a/shared/chat/inbox/use-inbox-search.tsx b/shared/chat/inbox/use-inbox-search.tsx
index 7001e66f5412..0c5f7cecdb30 100644
--- a/shared/chat/inbox/use-inbox-search.tsx
+++ b/shared/chat/inbox/use-inbox-search.tsx
@@ -2,7 +2,7 @@ import * as C from '@/constants'
import {ignorePromise} from '@/constants/utils'
import * as T from '@/constants/types'
import logger from '@/logger'
-import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
import {RPCError} from '@/util/errors'
import {isMobile} from '@/constants/platform'
import * as ConvoState from '@/stores/convostate'
@@ -151,7 +151,7 @@ export type InboxSearchController = {
}
export function useInboxSearch(): InboxSearchController {
- const mobileAppState = useConfigState(s => s.mobileAppState)
+ const mobileAppState = useShellState(s => s.mobileAppState)
const [isSearching, setIsSearching] = React.useState(false)
const [searchInfo, setSearchInfo] = React.useState(makeInboxSearchInfo)
const activeSearchIDRef = React.useRef(0)
diff --git a/shared/constants/chat/common.tsx b/shared/constants/chat/common.tsx
index cb3f54b501a3..4e51cdec4016 100644
--- a/shared/constants/chat/common.tsx
+++ b/shared/constants/chat/common.tsx
@@ -1,6 +1,6 @@
import * as T from '../types'
import {getVisibleScreen} from '@/constants/router'
-import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
import {isSplit, threadRouteName} from './layout'
export const explodingModeGregorKeyPrefix = 'exploding:'
@@ -28,7 +28,7 @@ export const isUserActivelyLookingAtThisThread = (conversationIDKey: T.Chat.Conv
(maybeVisibleScreen === undefined ? undefined : maybeVisibleScreen.name) === threadRouteName
}
- const {appFocused, active: userActive} = useConfigState.getState()
+ const {appFocused, active: userActive} = useShellState.getState()
return (
appFocused && // app focused?
diff --git a/shared/constants/init/index.desktop.tsx b/shared/constants/init/index.desktop.tsx
index 6211d65440cb..bc512150fbac 100644
--- a/shared/constants/init/index.desktop.tsx
+++ b/shared/constants/init/index.desktop.tsx
@@ -2,9 +2,9 @@
import * as Chat from '@/stores/chat'
import {ignorePromise} from '@/constants/utils'
import {useConfigState} from '@/stores/config'
-import * as ConfigConstants from '@/stores/config'
import {useDaemonState} from '@/stores/daemon'
import {useFSState} from '@/stores/fs'
+import {openAtLoginKey, useShellState} from '@/stores/shell'
import type * as EngineGen from '@/constants/rpc'
import * as T from '@/constants/types'
import InputMonitor from '@/util/platform-specific/input-monitor.desktop'
@@ -24,7 +24,7 @@ const {activeChanged, requestWindowsStartService} = KB2.functions
const {quitApp, exitApp, setOpenAtLogin} = KB2.functions
const maybePauseVideos = () => {
- const {appFocused} = useConfigState.getState()
+ const {appFocused} = useShellState.getState()
const videos = document.querySelectorAll('video')
const allVideos = Array.from(videos)
@@ -142,16 +142,18 @@ export const initPlatformListener = () => {
for (const unsub of _platformUnsubs) unsub()
_platformUnsubs.length = 0
- _platformUnsubs.push(useConfigState.subscribe((s, old) => {
+ _platformUnsubs.push(useShellState.subscribe((s, old) => {
if (s.appFocused === old.appFocused) return
useFSState.getState().dispatch.onChangedFocus(s.appFocused)
}))
_platformUnsubs.push(useConfigState.subscribe((s, old) => {
if (s.loggedIn !== old.loggedIn) {
- s.dispatch.osNetworkStatusChanged(navigator.onLine, 'notavailable', true)
+ useShellState.getState().dispatch.osNetworkStatusChanged(navigator.onLine, 'notavailable', true)
}
+ }))
+ _platformUnsubs.push(useShellState.subscribe((s, old) => {
if (s.appFocused !== old.appFocused) {
maybePauseVideos()
}
@@ -164,7 +166,7 @@ export const initPlatformListener = () => {
return
} else {
await T.RPCGen.configGuiSetValueRpcPromise({
- path: ConfigConstants.openAtLoginKey,
+ path: openAtLoginKey,
value: {b: openAtLogin, isNull: false},
})
}
@@ -198,7 +200,7 @@ export const initPlatformListener = () => {
if (skipAppFocusActions) {
console.log('Skipping app focus actions!')
} else {
- useConfigState.getState().dispatch.changedFocus(appFocused)
+ useShellState.getState().dispatch.changedFocus(appFocused)
}
}
window.addEventListener('focus', () => handle(true))
@@ -208,16 +210,16 @@ export const initPlatformListener = () => {
const setupReachabilityWatcher = () => {
window.addEventListener('online', () =>
- useConfigState.getState().dispatch.osNetworkStatusChanged(true, 'notavailable')
+ useShellState.getState().dispatch.osNetworkStatusChanged(true, 'notavailable')
)
window.addEventListener('offline', () =>
- useConfigState.getState().dispatch.osNetworkStatusChanged(false, 'notavailable')
+ useShellState.getState().dispatch.osNetworkStatusChanged(false, 'notavailable')
)
}
setupReachabilityWatcher()
if (isLinux) {
- useConfigState.getState().dispatch.initUseNativeFrame()
+ useShellState.getState().dispatch.initUseNativeFrame()
}
const initializeInputMonitor = () => {
@@ -226,7 +228,7 @@ export const initPlatformListener = () => {
if (skipAppFocusActions) {
console.log('Skipping app focus actions!')
} else {
- useConfigState.getState().dispatch.setActive(userActive)
+ useShellState.getState().dispatch.setActive(userActive)
// let node thread save file
activeChanged?.(Date.now(), userActive)
}
diff --git a/shared/constants/init/index.native.tsx b/shared/constants/init/index.native.tsx
index fa978f543c84..77c3c9b002ef 100644
--- a/shared/constants/init/index.native.tsx
+++ b/shared/constants/init/index.native.tsx
@@ -5,6 +5,7 @@ import {useDaemonState} from '@/stores/daemon'
import {useDarkModeState} from '@/stores/darkmode'
import {useFSState} from '@/stores/fs'
import {useRouterState} from '@/stores/router'
+import {useShellState} from '@/stores/shell'
import {useSettingsContactsState} from '@/stores/settings-contacts'
import * as T from '@/constants/types'
import type * as EngineGen from '@/constants/rpc'
@@ -243,7 +244,7 @@ export const onEngineIncoming = (action: EngineGen.Actions) => {
}
export const initPlatformListener = () => {
- useConfigState.subscribe((s, old) => {
+ useShellState.subscribe((s, old) => {
if (s.mobileAppState === old.mobileAppState) return
let appFocused: boolean
let logState: T.RPCGen.MobileAppState
@@ -311,12 +312,16 @@ export const initPlatformListener = () => {
if (s.loggedIn === old.loggedIn) return
const f = async () => {
const {type} = await NetInfo.fetch()
- s.dispatch.osNetworkStatusChanged(type !== NetInfo.NetInfoStateType.none, type, true)
+ useShellState.getState().dispatch.osNetworkStatusChanged(
+ type !== NetInfo.NetInfoStateType.none,
+ type,
+ true
+ )
}
ignorePromise(f())
})
- useConfigState.subscribe((s, old) => {
+ useShellState.subscribe((s, old) => {
if (s.networkStatus === old.networkStatus) return
const type = s.networkStatus?.type
if (!type) return
@@ -330,7 +335,7 @@ export const initPlatformListener = () => {
ignorePromise(f())
})
- useConfigState.subscribe((s, old) => {
+ useShellState.subscribe((s, old) => {
if (s.mobileAppState === old.mobileAppState) return
if (s.mobileAppState === 'active') {
// only reload on foreground
@@ -382,7 +387,7 @@ export const initPlatformListener = () => {
initPushListener()
NetInfo.addEventListener(({type}) => {
- useConfigState.getState().dispatch.osNetworkStatusChanged(type !== NetInfo.NetInfoStateType.none, type)
+ useShellState.getState().dispatch.osNetworkStatusChanged(type !== NetInfo.NetInfoStateType.none, type)
})
const initAudioModes = () => {
diff --git a/shared/constants/init/push-listener.native.tsx b/shared/constants/init/push-listener.native.tsx
index 945d84be9daf..2aae9fbdd69b 100644
--- a/shared/constants/init/push-listener.native.tsx
+++ b/shared/constants/init/push-listener.native.tsx
@@ -15,6 +15,7 @@ import {useConfigState} from '@/stores/config'
import {useCurrentUserState} from '@/stores/current-user'
import {useLogoutState} from '@/stores/logout'
import {usePushState} from '@/stores/push'
+import {useShellState} from '@/stores/shell'
type DataCommon = {
userInteraction: boolean
@@ -172,7 +173,7 @@ const getStartupDetailsFromInitialPush = async () => {
export const initPushListener = () => {
// Permissions
- useConfigState.subscribe((s, old) => {
+ useShellState.subscribe((s, old) => {
if (s.mobileAppState === old.mobileAppState) return
// Only recheck on foreground, not background
if (s.mobileAppState !== 'active') {
diff --git a/shared/constants/init/shared.tsx b/shared/constants/init/shared.tsx
index cc8c7c77a94f..c0c2eb4f99fc 100644
--- a/shared/constants/init/shared.tsx
+++ b/shared/constants/init/shared.tsx
@@ -44,6 +44,7 @@ import {useFSState} from '@/stores/fs'
import {useModalHeaderState} from '@/stores/modal-header'
import {usePeopleState} from '@/stores/people'
import {useProvisionState} from '@/stores/provision'
+import {useShellState} from '@/stores/shell'
import {useSettingsEmailState} from '@/stores/settings-email'
import {useSettingsPhoneState} from '@/stores/settings-phone'
import {useSettingsContactsState} from '@/stores/settings-contacts'
@@ -70,6 +71,39 @@ import {clearSignupDeviceNameDraft} from '@/signup/device-name-draft'
let _emitStartupOnLoadDaemonConnectedOnce: boolean = __DEV__ ? (globalThis.__hmr_startupOnce ?? false) : false
const _sharedUnsubs: Array<() => void> = __DEV__ ? (globalThis.__hmr_sharedUnsubs ??= []) : []
+const getAccountsWaitKey = 'config.getAccounts'
+
+const loadConfiguredAccountsForBootstrap = () => {
+ const configState = useConfigState.getState()
+ if (configState.configuredAccounts.length) {
+ return
+ }
+
+ const version = useDaemonState.getState().handshakeVersion
+ const handshakeWait = !configState.loggedIn
+ const refreshAccounts = configState.dispatch.refreshAccounts
+ const {wait} = useDaemonState.getState().dispatch
+
+ const f = async () => {
+ try {
+ if (handshakeWait) {
+ wait(getAccountsWaitKey, version, true)
+ }
+
+ await refreshAccounts()
+
+ if (handshakeWait && useDaemonState.getState().handshakeWaiters.get(getAccountsWaitKey)) {
+ wait(getAccountsWaitKey, version, false)
+ }
+ } catch {
+ if (handshakeWait && useDaemonState.getState().handshakeWaiters.get(getAccountsWaitKey)) {
+ wait(getAccountsWaitKey, version, false, "Can't get accounts")
+ }
+ }
+ }
+
+ ignorePromise(f())
+}
export const onEngineConnected = () => {
{
@@ -204,22 +238,14 @@ export const initSharedSubscriptions = () => {
clearSignupEmail()
clearSignupDeviceNameDraft()
}
- useDaemonState.getState().dispatch.loadDaemonAccounts(
- s.configuredAccounts.length,
- s.loggedIn,
- useConfigState.getState().dispatch.refreshAccounts
- )
+ loadConfiguredAccountsForBootstrap()
if (!s.loggedInCausedbyStartup) {
ignorePromise(useConfigState.getState().dispatch.refreshAccounts())
}
}
if (s.revokedTrigger !== old.revokedTrigger) {
- useDaemonState.getState().dispatch.loadDaemonAccounts(
- s.configuredAccounts.length,
- s.loggedIn,
- useConfigState.getState().dispatch.refreshAccounts
- )
+ loadConfiguredAccountsForBootstrap()
}
if (s.configuredAccounts !== old.configuredAccounts) {
@@ -232,6 +258,11 @@ export const initSharedSubscriptions = () => {
}
}
+ })
+ )
+
+ _sharedUnsubs.push(
+ useShellState.subscribe((s, old) => {
if (s.active !== old.active) {
const cs = getConvoState(getSelectedConversation())
cs.dispatch.markThreadAsRead()
@@ -244,12 +275,7 @@ export const initSharedSubscriptions = () => {
if (s.handshakeVersion !== old.handshakeVersion) {
useDarkModeState.getState().dispatch.loadDarkPrefs()
useChatState.getState().dispatch.loadStaticConfig()
- const configState = useConfigState.getState()
- s.dispatch.loadDaemonAccounts(
- configState.configuredAccounts.length,
- configState.loggedIn,
- useConfigState.getState().dispatch.refreshAccounts
- )
+ loadConfiguredAccountsForBootstrap()
}
if (s.bootstrapStatus !== old.bootstrapStatus) {
diff --git a/shared/desktop/renderer/main2.desktop.tsx b/shared/desktop/renderer/main2.desktop.tsx
index 23d59b640fb8..c32fab788d33 100644
--- a/shared/desktop/renderer/main2.desktop.tsx
+++ b/shared/desktop/renderer/main2.desktop.tsx
@@ -13,6 +13,7 @@ import {initDesktopStyles} from '@/styles/index.desktop'
import {isWindows} from '@/constants/platform'
import KB2 from '@/util/electron.desktop'
import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
import {setServiceDecoration} from '@/common-adapters/markdown/react'
import ServiceDecoration from '@/common-adapters/markdown/service-decoration'
import {useDarkModeState} from '@/stores/darkmode'
@@ -72,9 +73,9 @@ const setupApp = async () => {
// issuing RPCs on a renderer reload.
await appStartedUp?.()
- useConfigState.getState().dispatch.initNotifySound()
- useConfigState.getState().dispatch.initForceSmallNav()
- useConfigState.getState().dispatch.initOpenAtLogin()
+ useShellState.getState().dispatch.initNotifySound()
+ useShellState.getState().dispatch.initForceSmallNav()
+ useShellState.getState().dispatch.initOpenAtLogin()
useConfigState.getState().dispatch.initAppUpdateLoop()
eng.listenersAreReady()
diff --git a/shared/desktop/renderer/remote-event-handler.desktop.tsx b/shared/desktop/renderer/remote-event-handler.desktop.tsx
index 49f0dbec18b8..9c05a334b794 100644
--- a/shared/desktop/renderer/remote-event-handler.desktop.tsx
+++ b/shared/desktop/renderer/remote-event-handler.desktop.tsx
@@ -13,7 +13,9 @@ import {useChatState} from '@/stores/chat'
import {useConfigState} from '@/stores/config'
import {useFSState} from '@/stores/fs'
import {usePinentryState} from '@/stores/pinentry'
+import {useShellState} from '@/stores/shell'
import {useTrackerState} from '@/stores/tracker'
+import {useUnlockFoldersState} from '@/unlock-folders/store'
import logger from '@/logger'
import {makeUUID} from '@/util/uuid'
import {dumpLogs, showMain} from '@/util/storeless-actions'
@@ -130,13 +132,11 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => {
case RemoteGen.unlockFoldersSubmitPaperKey: {
T.RPCGen.loginPaperKeySubmitRpcPromise({paperPhrase: action.payload.paperKey}, 'unlock-folders:waiting')
.then(() => {
- useConfigState.getState().dispatch.openUnlockFolders([])
+ useUnlockFoldersState.getState().dispatch.close()
})
.catch((e: unknown) => {
if (!(e instanceof RPCError)) return
- useConfigState.setState(s => {
- s.unlockFoldersError = e.desc
- })
+ useUnlockFoldersState.getState().dispatch.setPaperKeyError(e.desc)
})
break
}
@@ -144,7 +144,7 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => {
T.RPCGen.rekeyRekeyStatusFinishRpcPromise()
.then(() => {})
.catch(() => {})
- useConfigState.getState().dispatch.openUnlockFolders([])
+ useUnlockFoldersState.getState().dispatch.close()
break
}
case RemoteGen.stop: {
@@ -191,12 +191,10 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => {
.dispatch.remoteWindowNeedsProps(action.payload.component, action.payload.param)
break
case RemoteGen.updateWindowMaxState:
- useConfigState.setState(s => {
- s.windowState.isMaximized = action.payload.max
- })
+ useShellState.getState().dispatch.setWindowMaximized(action.payload.max)
break
case RemoteGen.updateWindowState:
- useConfigState.getState().dispatch.updateWindowState(action.payload.windowState)
+ useShellState.getState().dispatch.updateWindowState(action.payload.windowState)
break
case RemoteGen.updateWindowShown: {
const win = action.payload.component
diff --git a/shared/router-v2/header/index.desktop.tsx b/shared/router-v2/header/index.desktop.tsx
index 6049728e8984..154468aa4580 100644
--- a/shared/router-v2/header/index.desktop.tsx
+++ b/shared/router-v2/header/index.desktop.tsx
@@ -4,6 +4,7 @@ import * as Platform from '@/constants/platform'
import SyncingFolders from './syncing-folders'
import KB2 from '@/util/electron.desktop'
import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
import type {HeaderBackButtonProps} from '@react-navigation/elements'
import type {NativeStackHeaderProps} from '@react-navigation/native-stack'
@@ -324,9 +325,9 @@ type HeaderProps = Omit s.useNativeFrame)
+ const useNativeFrame = useShellState(s => s.useNativeFrame)
const loggedIn = useConfigState(s => s.loggedIn)
- const isMaximized = useConfigState(s => s.windowState.isMaximized)
+ const isMaximized = useShellState(s => s.windowState.isMaximized)
const {headerMode, title, headerTitle, headerRightActions, subHeader} = _options
const {headerRight, headerTransparent, headerShadowVisible, headerBottomStyle, headerStyle, headerLeft} =
_options
diff --git a/shared/router-v2/tab-bar.desktop.tsx b/shared/router-v2/tab-bar.desktop.tsx
index 7e73ba323fd9..e17ce124643e 100644
--- a/shared/router-v2/tab-bar.desktop.tsx
+++ b/shared/router-v2/tab-bar.desktop.tsx
@@ -18,6 +18,7 @@ import {useTrackerState} from '@/stores/tracker'
import {useFSState} from '@/stores/fs'
import {useNotifState} from '@/stores/notifications'
import {useCurrentUserState} from '@/stores/current-user'
+import {useShellState} from '@/stores/shell'
import {navToProfile} from '@/constants/router'
import {dumpLogs} from '@/util/storeless-actions'
@@ -175,7 +176,7 @@ function TabBar(props: Props) {
Kb.useHotKey(hotKeys, onHotKey)
const onSelectTab = Common.useSubnavTabAction(navigation, state)
- const forceSmallNav = useConfigState(s => s.forceSmallNav)
+ const forceSmallNav = useShellState(s => s.forceSmallNav)
return username ? (
{
}
const UseNativeFrame = () => {
- const {onChangeUseNativeFrame, useNativeFrame} = useConfigState(
+ const {onChangeUseNativeFrame, useNativeFrame} = useShellState(
C.useShallow(s => ({
onChangeUseNativeFrame: s.dispatch.setUseNativeFrame,
useNativeFrame: s.useNativeFrame,
@@ -149,7 +150,7 @@ const Advanced = () => {
loadHasRandomPw: s.dispatch.loadHasRandomPw,
}))
)
- const {onSetOpenAtLogin, openAtLogin} = useConfigState(
+ const {onSetOpenAtLogin, openAtLogin} = useShellState(
C.useShallow(s => ({
onSetOpenAtLogin: s.dispatch.setOpenAtLogin,
openAtLogin: s.openAtLogin,
diff --git a/shared/settings/chat.tsx b/shared/settings/chat.tsx
index f04536ebac34..5f06efc8f2c4 100644
--- a/shared/settings/chat.tsx
+++ b/shared/settings/chat.tsx
@@ -7,6 +7,7 @@ import Group from './group'
import {loadSettings} from './load-settings'
import useNotificationSettings from './notifications/use-notification-settings'
import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
const emptyList = new Array()
@@ -434,7 +435,7 @@ const Links = () => {
}
const Sound = ({allowEdit, groups, toggle}: NotificationSettingsState) => {
- const {onToggleSound, sound} = useConfigState(
+ const {onToggleSound, sound} = useShellState(
C.useShallow(s => ({
onToggleSound: s.dispatch.setNotifySound,
sound: s.notifySound,
diff --git a/shared/settings/display.tsx b/shared/settings/display.tsx
index fb7a35055e41..a4b1ac60ae6a 100644
--- a/shared/settings/display.tsx
+++ b/shared/settings/display.tsx
@@ -3,12 +3,13 @@ import * as Kb from '@/common-adapters'
import * as T from '@/constants/types'
import logger from '@/logger'
import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
import * as DarkMode from '@/stores/darkmode'
const Display = () => {
const allowAnimatedEmojis = useConfigState(s => s.allowAnimatedEmojis)
- const forceSmallNav = useConfigState(s => s.forceSmallNav)
- const setForceSmallNav = useConfigState(s => s.dispatch.setForceSmallNav)
+ const forceSmallNav = useShellState(s => s.forceSmallNav)
+ const setForceSmallNav = useShellState(s => s.dispatch.setForceSmallNav)
const toggleForceSmallNav = () => {
setForceSmallNav(!forceSmallNav)
}
diff --git a/shared/stores/config.tsx b/shared/stores/config.tsx
index 125fb681b58a..d33b5c0b179a 100644
--- a/shared/stores/config.tsx
+++ b/shared/stores/config.tsx
@@ -1,4 +1,3 @@
-import type * as NetInfo from '@react-native-community/netinfo'
import * as T from '@/constants/types'
import {ignorePromise, timeoutPromise} from '@/constants/utils'
import {waitingKeyConfigLogin, waitingKeyConfigLoginAsOther} from '@/constants/strings'
@@ -9,7 +8,7 @@ import isEqual from 'lodash/isEqual'
import logger from '@/logger'
import type {Tab} from '@/constants/tabs'
import {RPCError, convertToError, isErrorTransient, niceError} from '@/util/errors'
-import {defaultUseNativeFrame, isMobile} from '@/constants/platform'
+import {isMobile} from '@/constants/platform'
import {type CommonResponseHandler} from '@/engine/types'
import {invalidPasswordErrorString} from '@/constants/config'
import {navigateAppend} from '@/constants/router'
@@ -17,19 +16,14 @@ import {
onEngineConnected as onEngineConnectedInPlatform,
} from '@/util/storeless-actions'
-export type ConnectionType = NetInfo.NetInfoStateType | 'notavailable'
-
type Store = T.Immutable<{
- active: boolean
allowAnimatedEmojis: boolean
androidShare?:
| {type: T.RPCGen.IncomingShareType.file; urls: Array}
| {type: T.RPCGen.IncomingShareType.text; text: string}
- appFocused: boolean
badgeState?: T.RPCGen.BadgeState
configuredAccounts: Array
defaultUsername: string
- forceSmallNav: boolean
globalError?: Error | RPCError
gregorReachable?: T.RPCGen.Reachable
gregorPushState: Array<{md: T.RPCGregor.Metadata; item: T.RPCGregor.Item}>
@@ -51,10 +45,6 @@ type Store = T.Immutable<{
| 'connectedToDaemonForFirstTime'
| 'reloggedIn'
| 'startupOrReloginButNotInARush'
- mobileAppState: 'active' | 'background' | 'inactive' | 'unknown'
- networkStatus?: {online: boolean; type: ConnectionType; isInit?: boolean}
- notifySound: boolean
- openAtLogin: boolean
outOfDate: T.Config.OutOfDate
remoteWindowNeedsProps: Map>
revokedTrigger: number
@@ -66,36 +56,16 @@ type Store = T.Immutable<{
link: string
tab?: Tab
}
- unlockFoldersDevices: Array<{
- type: T.Devices.DeviceType
- name: string
- deviceID: T.Devices.DeviceID
- }>
- unlockFoldersError: string
- useNativeFrame: boolean
userSwitching: boolean
windowShownCount: Map
- windowState: {
- dockHidden: boolean
- height: number
- isFullScreen: boolean
- isMaximized: boolean
- width: number
- windowHidden: boolean
- x: number
- y: number
- }
}>
const initialStore: Store = {
- active: true,
allowAnimatedEmojis: true,
androidShare: undefined,
- appFocused: true,
badgeState: undefined,
configuredAccounts: [],
defaultUsername: '',
- forceSmallNav: false,
globalError: undefined,
gregorPushState: [],
gregorReachable: undefined,
@@ -112,10 +82,6 @@ const initialStore: Store = {
loggedIn: false,
loggedInCausedbyStartup: false,
loginError: undefined,
- mobileAppState: 'unknown',
- networkStatus: undefined,
- notifySound: false,
- openAtLogin: true,
outOfDate: {
critical: false,
message: '',
@@ -130,32 +96,14 @@ const initialStore: Store = {
link: '',
loaded: false,
},
- unlockFoldersDevices: [],
- unlockFoldersError: '',
- useNativeFrame: defaultUseNativeFrame,
userSwitching: false,
windowShownCount: new Map(),
- windowState: {
- dockHidden: false,
- height: 800,
- isFullScreen: false,
- isMaximized: false,
- width: 600,
- windowHidden: false,
- x: 0,
- y: 0,
- },
}
export type State = Store & {
dispatch: {
- changedFocus: (f: boolean) => void
checkForUpdate: () => void
initAppUpdateLoop: () => void
- initNotifySound: () => void
- initForceSmallNav: () => void
- initOpenAtLogin: () => void
- initUseNativeFrame: () => void
installerRan: () => void
loadIsOnline: () => void
loadOnStart: (phase: State['loadOnStartPhase']) => void
@@ -165,8 +113,6 @@ export type State = Store & {
logoutAndTryToLogInAs: (username: string) => void
onEngineConnected: () => void
onEngineIncoming: (action: EngineGen.Actions) => void
- osNetworkStatusChanged: (online: boolean, type: ConnectionType, isInit?: boolean) => void
- openUnlockFolders: (devices: ReadonlyArray) => void
powerMonitorEvent: (event: string) => void
resetState: (isDebug?: boolean) => void
remoteWindowNeedsProps: (component: string, params: string) => void
@@ -174,37 +120,25 @@ export type State = Store & {
revoke: (deviceName: string, wasCurrentDevice: boolean) => void
refreshAccounts: () => Promise
setAccounts: (a: Store['configuredAccounts']) => void
- setActive: (a: boolean) => void
setAndroidShare: (s: Store['androidShare']) => void
setBadgeState: (b: State['badgeState']) => void
setDefaultUsername: (u: string) => void
- setForceSmallNav: (f: boolean) => void
setGlobalError: (e?: unknown) => void
+ setGregorReachable: (r: Store['gregorReachable']) => void
setHTTPSrvInfo: (address: string, token: string) => void
setIncomingShareUseOriginal: (use: boolean) => void
setJustDeletedSelf: (s: string) => void
setLoggedIn: (l: boolean, causedByStartup: boolean, fromMenubar?: boolean) => void
- setMobileAppState: (nextAppState: 'active' | 'background' | 'inactive') => void
- setNotifySound: (n: boolean) => void
setStartupDetails: (st: Omit) => void
- setOpenAtLogin: (open: boolean) => void
setOutOfDate: (outOfDate: T.Config.OutOfDate) => void
setUpdating: () => void
- setUseNativeFrame: (use: boolean) => void
setUserSwitching: (sw: boolean) => void
toggleRuntimeStats: () => void
updateGregorCategory: (category: string, body: string, dtime?: {offset: number; time: number}) => void
- updateWindowState: (ws: Omit) => void
}
}
-export const openAtLoginKey = 'openAtLogin'
export const useConfigState = Z.createZustand('config', (set, get) => {
- const nativeFrameKey = 'useNativeFrame'
- const notifySoundKey = 'notifySound'
- const forceSmallNavKey = 'ui.forceSmallNav'
- const windowStateKey = 'windowState'
-
const _checkForUpdate = async () => {
try {
const {status, message} = await T.RPCGen.configGetUpdateInfoRpcPromise()
@@ -261,12 +195,6 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
}
const dispatch: State['dispatch'] = {
- changedFocus: f => {
- if (get().appFocused === f) return
- set(s => {
- s.appFocused = f
- })
- },
checkForUpdate: () => {
const f = async () => {
await _checkForUpdate()
@@ -284,58 +212,6 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
}
ignorePromise(f())
},
- initForceSmallNav: () => {
- const f = async () => {
- try {
- const val = await T.RPCGen.configGuiGetValueRpcPromise({path: forceSmallNavKey})
- const forceSmallNav = val.b
- if (typeof forceSmallNav === 'boolean') {
- set(s => {
- s.forceSmallNav = forceSmallNav
- })
- }
- } catch {}
- }
- ignorePromise(f())
- },
- initNotifySound: () => {
- const f = async () => {
- try {
- const val = await T.RPCGen.configGuiGetValueRpcPromise({path: notifySoundKey})
- const notifySound = val.b
- if (typeof notifySound === 'boolean') {
- set(s => {
- s.notifySound = notifySound
- })
- }
- } catch {}
- }
- ignorePromise(f())
- },
- initOpenAtLogin: () => {
- const f = async () => {
- try {
- const val = await T.RPCGen.configGuiGetValueRpcPromise({path: openAtLoginKey})
- const openAtLogin = val.b
- if (typeof openAtLogin === 'boolean') {
- get().dispatch.setOpenAtLogin(openAtLogin)
- }
- } catch {}
- }
- ignorePromise(f())
- },
- initUseNativeFrame: () => {
- const f = async () => {
- try {
- const val = await T.RPCGen.configGuiGetValueRpcPromise({path: nativeFrameKey})
- const useNativeFrame = val.b === undefined || val.b === null ? defaultUseNativeFrame : val.b
- set(s => {
- s.useNativeFrame = useNativeFrame
- })
- } catch {}
- }
- ignorePromise(f())
- },
installerRan: () => {
set(s => {
s.installerRanCount++
@@ -460,7 +336,7 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
const startReachability = async () => {
try {
const reachability = await T.RPCGen.reachabilityStartReachabilityRpcPromise()
- setGregorReachable(reachability.reachable)
+ get().dispatch.setGregorReachable(reachability.reachable)
} catch (err) {
logger.warn('error bootstrapping reachability: ', err)
}
@@ -532,52 +408,12 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
}
case 'keybase.1.reachability.reachabilityChanged':
if (get().loggedIn) {
- setGregorReachable(action.payload.params.reachability.reachable)
+ get().dispatch.setGregorReachable(action.payload.params.reachability.reachable)
}
break
default:
}
},
- openUnlockFolders: devices => {
- set(s => {
- s.unlockFoldersDevices = devices.map(({name, type, deviceID}) => ({
- deviceID,
- name,
- type: T.Devices.stringToDeviceType(type),
- }))
- })
- },
- osNetworkStatusChanged: (online: boolean, type: ConnectionType, isInit?: boolean) => {
- const old = get().networkStatus
- if (old?.online === online && old.type === type && old.isInit === isInit) return
- set(s => {
- if (!s.networkStatus) {
- s.networkStatus = {isInit, online, type}
- } else {
- s.networkStatus.isInit = isInit
- s.networkStatus.online = online
- s.networkStatus.type = type
- }
- })
- const updateGregor = async () => {
- const reachability = await T.RPCGen.reachabilityCheckReachabilityRpcPromise()
- setGregorReachable(reachability.reachable)
- }
- ignorePromise(updateGregor())
-
- const updateFS = async () => {
- if (isInit) return
- try {
- await T.RPCGen.SimpleFSSimpleFSCheckReachabilityRpcPromise()
- } catch (error) {
- if (!(error instanceof RPCError)) {
- return
- }
- logger.warn(`failed to check KBFS reachability: ${error.message}`)
- }
- }
- ignorePromise(updateFS())
- },
powerMonitorEvent: event => {
const f = async () => {
await T.RPCGen.appStatePowerMonitorEventRpcPromise({event})
@@ -624,14 +460,10 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
if (isDebug) return
set(s => ({
...initialStore,
- appFocused: s.appFocused,
configuredAccounts: s.configuredAccounts,
defaultUsername: s.defaultUsername,
dispatch: s.dispatch,
- forceSmallNav: s.forceSmallNav,
- mobileAppState: s.mobileAppState,
startup: {loaded: s.startup.loaded},
- useNativeFrame: s.useNativeFrame,
userSwitching: s.userSwitching,
}))
},
@@ -654,11 +486,6 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
}
})
},
- setActive: a => {
- set(s => {
- s.active = a
- })
- },
setAndroidShare: share => {
set(s => {
s.androidShare = T.castDraft(share)
@@ -675,21 +502,6 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
s.defaultUsername = u
})
},
- setForceSmallNav: force => {
- const f = async () => {
- await T.RPCGen.configGuiSetValueRpcPromise({
- path: forceSmallNavKey,
- value: {
- b: force,
- isNull: false,
- },
- })
- set(s => {
- s.forceSmallNav = force
- })
- }
- ignorePromise(f())
- },
setGlobalError: _e => {
if (_e) {
const e = convertToError(_e)
@@ -707,6 +519,9 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
})
}
},
+ setGregorReachable: r => {
+ setGregorReachable(r)
+ },
setHTTPSrvInfo: (address, token) => {
set(s => {
s.httpSrv.address = address
@@ -758,31 +573,6 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
// hidden and the user can see and respond to the error.
get().dispatch.setUserSwitching(false)
},
- setMobileAppState: nextAppState => {
- if (get().mobileAppState === nextAppState) return
- set(s => {
- s.mobileAppState = nextAppState
- })
- },
- setNotifySound: n => {
- set(s => {
- s.notifySound = n
- })
- ignorePromise(
- T.RPCGen.configGuiSetValueRpcPromise({
- path: notifySoundKey,
- value: {
- b: n,
- isNull: false,
- },
- })
- )
- },
- setOpenAtLogin: open => {
- set(s => {
- s.openAtLogin = open
- })
- },
setOutOfDate: outOfDate => {
set(s => {
Object.assign(s.outOfDate, outOfDate)
@@ -804,20 +594,6 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
s.outOfDate.updating = true
})
},
- setUseNativeFrame: use => {
- set(s => {
- s.useNativeFrame = use
- })
- ignorePromise(
- T.RPCGen.configGuiSetValueRpcPromise({
- path: nativeFrameKey,
- value: {
- b: use,
- isNull: false,
- },
- })
- )
- },
setUserSwitching: sw => {
set(s => {
s.userSwitching = sw
@@ -841,24 +617,6 @@ export const useConfigState = Z.createZustand('config', (set, get) => {
}
ignorePromise(f())
},
- updateWindowState: ws => {
- const old = get().windowState
- const next = {...old, ...ws}
- if (isEqual(old, next)) return
- set(s => {
- s.windowState = next
- })
-
- ignorePromise(
- T.RPCGen.configGuiSetValueRpcPromise({
- path: windowStateKey,
- value: {
- isNull: false,
- s: JSON.stringify(next),
- },
- })
- )
- },
}
return {
...initialStore,
diff --git a/shared/stores/convostate.tsx b/shared/stores/convostate.tsx
index 26f94c26f77c..aa5a351ca0dc 100644
--- a/shared/stores/convostate.tsx
+++ b/shared/stores/convostate.tsx
@@ -51,6 +51,7 @@ import * as Strings from '@/constants/strings'
import {chatStores, convoUIStores} from './convo-registry'
import {useConfigState} from '@/stores/config'
+import {useShellState} from '@/stores/shell'
import {useCurrentUserState} from '@/stores/current-user'
import {useUsersState} from '@/stores/users'
import {getUsernameToShow} from '@/chat/conversation/messages/separator-utils'
@@ -1500,7 +1501,7 @@ const createSlice =
}
const onClose = () => {}
logger.info('invoking NotifyPopup for chat notification')
- const sound = useConfigState.getState().notifySound
+ const sound = useShellState.getState().notifySound
const cleanBody = body.replaceAll(/!>(.*?) Promise
- ) => void
loadDaemonBootstrapStatus: () => Promise
resetState: () => void
setError: (e?: Error) => void
@@ -115,48 +106,6 @@ export const useDaemonState = Z.createZustand('daemon', (set, get) => {
daemonHandshakeDone: () => {
get().dispatch.setState('done')
},
- loadDaemonAccounts: (
- configuredAccountsLength: number,
- loggedIn: boolean,
- refreshAccounts: () => Promise
- ) => {
- const f = async () => {
- const version = get().handshakeVersion
- if (configuredAccountsLength) {
- // bail on already loaded
- return
- }
-
- // did we beat getBootstrapStatus?
- const handshakeWait = !loggedIn
-
- const {wait} = get().dispatch
- try {
- if (handshakeWait) {
- wait(getAccountsWaitKey, version, true)
- }
-
- await refreshAccounts()
-
- if (handshakeWait) {
- // someone dismissed this already?
- const {handshakeWaiters} = get()
- if (handshakeWaiters.get(getAccountsWaitKey)) {
- wait(getAccountsWaitKey, version, false)
- }
- }
- } catch {
- if (handshakeWait) {
- // someone dismissed this already?
- const {handshakeWaiters} = get()
- if (handshakeWaiters.get(getAccountsWaitKey)) {
- wait(getAccountsWaitKey, version, false, "Can't get accounts")
- }
- }
- }
- }
- ignorePromise(f())
- },
// set to true so we reget status when we're reachable again
loadDaemonBootstrapStatus: async () => {
const version = get().handshakeVersion
@@ -179,8 +128,8 @@ export const useDaemonState = Z.createZustand('daemon', (set, get) => {
// if we're logged in act like getAccounts is done already
if (s.loggedIn) {
const {handshakeWaiters} = get()
- if (handshakeWaiters.get(getAccountsWaitKey)) {
- wait(getAccountsWaitKey, version, false)
+ if (handshakeWaiters.get('config.getAccounts')) {
+ wait('config.getAccounts', version, false)
}
}
},
diff --git a/shared/stores/shell.tsx b/shared/stores/shell.tsx
new file mode 100644
index 000000000000..9e421887c46a
--- /dev/null
+++ b/shared/stores/shell.tsx
@@ -0,0 +1,266 @@
+import type * as NetInfo from '@react-native-community/netinfo'
+import * as T from '@/constants/types'
+import {ignorePromise} from '@/constants/utils'
+import * as Z from '@/util/zustand'
+import isEqual from 'lodash/isEqual'
+import logger from '@/logger'
+import {RPCError} from '@/util/errors'
+import {defaultUseNativeFrame} from '@/constants/platform'
+import {useConfigState} from '@/stores/config'
+
+export type ConnectionType = NetInfo.NetInfoStateType | 'notavailable'
+
+type WindowState = T.Immutable<{
+ dockHidden: boolean
+ height: number
+ isFullScreen: boolean
+ isMaximized: boolean
+ width: number
+ windowHidden: boolean
+ x: number
+ y: number
+}>
+
+type Store = T.Immutable<{
+ active: boolean
+ appFocused: boolean
+ forceSmallNav: boolean
+ mobileAppState: 'active' | 'background' | 'inactive' | 'unknown'
+ networkStatus?: {online: boolean; type: ConnectionType; isInit?: boolean}
+ notifySound: boolean
+ openAtLogin: boolean
+ useNativeFrame: boolean
+ windowState: WindowState
+}>
+
+const initialStore: Store = {
+ active: true,
+ appFocused: true,
+ forceSmallNav: false,
+ mobileAppState: 'unknown',
+ networkStatus: undefined,
+ notifySound: false,
+ openAtLogin: true,
+ useNativeFrame: defaultUseNativeFrame,
+ windowState: {
+ dockHidden: false,
+ height: 800,
+ isFullScreen: false,
+ isMaximized: false,
+ width: 600,
+ windowHidden: false,
+ x: 0,
+ y: 0,
+ },
+}
+
+export type State = Store & {
+ dispatch: {
+ changedFocus: (f: boolean) => void
+ initNotifySound: () => void
+ initForceSmallNav: () => void
+ initOpenAtLogin: () => void
+ initUseNativeFrame: () => void
+ osNetworkStatusChanged: (online: boolean, type: ConnectionType, isInit?: boolean) => void
+ resetState: (isDebug?: boolean) => void
+ setActive: (a: boolean) => void
+ setForceSmallNav: (f: boolean) => void
+ setMobileAppState: (nextAppState: 'active' | 'background' | 'inactive') => void
+ setNotifySound: (n: boolean) => void
+ setOpenAtLogin: (open: boolean) => void
+ setUseNativeFrame: (use: boolean) => void
+ setWindowMaximized: (isMaximized: boolean) => void
+ updateWindowState: (ws: Omit) => void
+ }
+}
+
+export const openAtLoginKey = 'openAtLogin'
+
+export const useShellState = Z.createZustand('shell', (set, get) => {
+ const nativeFrameKey = 'useNativeFrame'
+ const notifySoundKey = 'notifySound'
+ const forceSmallNavKey = 'ui.forceSmallNav'
+ const windowStateKey = 'windowState'
+
+ const dispatch: State['dispatch'] = {
+ changedFocus: f => {
+ if (get().appFocused === f) return
+ set(s => {
+ s.appFocused = f
+ })
+ },
+ initForceSmallNav: () => {
+ const f = async () => {
+ try {
+ const val = await T.RPCGen.configGuiGetValueRpcPromise({path: forceSmallNavKey})
+ const forceSmallNav = val.b
+ if (typeof forceSmallNav === 'boolean') {
+ set(s => {
+ s.forceSmallNav = forceSmallNav
+ })
+ }
+ } catch {}
+ }
+ ignorePromise(f())
+ },
+ initNotifySound: () => {
+ const f = async () => {
+ try {
+ const val = await T.RPCGen.configGuiGetValueRpcPromise({path: notifySoundKey})
+ const notifySound = val.b
+ if (typeof notifySound === 'boolean') {
+ set(s => {
+ s.notifySound = notifySound
+ })
+ }
+ } catch {}
+ }
+ ignorePromise(f())
+ },
+ initOpenAtLogin: () => {
+ const f = async () => {
+ try {
+ const val = await T.RPCGen.configGuiGetValueRpcPromise({path: openAtLoginKey})
+ const openAtLogin = val.b
+ if (typeof openAtLogin === 'boolean') {
+ get().dispatch.setOpenAtLogin(openAtLogin)
+ }
+ } catch {}
+ }
+ ignorePromise(f())
+ },
+ initUseNativeFrame: () => {
+ const f = async () => {
+ try {
+ const val = await T.RPCGen.configGuiGetValueRpcPromise({path: nativeFrameKey})
+ const useNativeFrame = val.b === undefined || val.b === null ? defaultUseNativeFrame : val.b
+ set(s => {
+ s.useNativeFrame = useNativeFrame
+ })
+ } catch {}
+ }
+ ignorePromise(f())
+ },
+ osNetworkStatusChanged: (online, type, isInit) => {
+ const old = get().networkStatus
+ if (old?.online === online && old.type === type && old.isInit === isInit) return
+ set(s => {
+ if (!s.networkStatus) {
+ s.networkStatus = {isInit, online, type}
+ } else {
+ s.networkStatus.isInit = isInit
+ s.networkStatus.online = online
+ s.networkStatus.type = type
+ }
+ })
+ const updateGregor = async () => {
+ const reachability = await T.RPCGen.reachabilityCheckReachabilityRpcPromise()
+ useConfigState.getState().dispatch.setGregorReachable(reachability.reachable)
+ }
+ ignorePromise(updateGregor())
+
+ const updateFS = async () => {
+ if (isInit) return
+ try {
+ await T.RPCGen.SimpleFSSimpleFSCheckReachabilityRpcPromise()
+ } catch (error) {
+ if (!(error instanceof RPCError)) {
+ return
+ }
+ logger.warn(`failed to check KBFS reachability: ${error.message}`)
+ }
+ }
+ ignorePromise(updateFS())
+ },
+ // Shell-owned prefs and focus/window state should survive account-level resets.
+ resetState: () => {},
+ setActive: a => {
+ set(s => {
+ s.active = a
+ })
+ },
+ setForceSmallNav: force => {
+ const f = async () => {
+ await T.RPCGen.configGuiSetValueRpcPromise({
+ path: forceSmallNavKey,
+ value: {
+ b: force,
+ isNull: false,
+ },
+ })
+ set(s => {
+ s.forceSmallNav = force
+ })
+ }
+ ignorePromise(f())
+ },
+ setMobileAppState: nextAppState => {
+ if (get().mobileAppState === nextAppState) return
+ set(s => {
+ s.mobileAppState = nextAppState
+ })
+ },
+ setNotifySound: n => {
+ set(s => {
+ s.notifySound = n
+ })
+ ignorePromise(
+ T.RPCGen.configGuiSetValueRpcPromise({
+ path: notifySoundKey,
+ value: {
+ b: n,
+ isNull: false,
+ },
+ })
+ )
+ },
+ setOpenAtLogin: open => {
+ set(s => {
+ s.openAtLogin = open
+ })
+ },
+ setUseNativeFrame: use => {
+ set(s => {
+ s.useNativeFrame = use
+ })
+ ignorePromise(
+ T.RPCGen.configGuiSetValueRpcPromise({
+ path: nativeFrameKey,
+ value: {
+ b: use,
+ isNull: false,
+ },
+ })
+ )
+ },
+ setWindowMaximized: isMaximized => {
+ if (get().windowState.isMaximized === isMaximized) return
+ set(s => {
+ s.windowState.isMaximized = isMaximized
+ })
+ },
+ updateWindowState: ws => {
+ const old = get().windowState
+ const next = {...old, ...ws}
+ if (isEqual(old, next)) return
+ set(s => {
+ s.windowState = next
+ })
+
+ ignorePromise(
+ T.RPCGen.configGuiSetValueRpcPromise({
+ path: windowStateKey,
+ value: {
+ isNull: false,
+ s: JSON.stringify(next),
+ },
+ })
+ )
+ },
+ }
+
+ return {
+ ...initialStore,
+ dispatch,
+ }
+})
diff --git a/shared/stores/tests/config.test.ts b/shared/stores/tests/config.test.ts
index bd41037413f8..2bafad76faae 100644
--- a/shared/stores/tests/config.test.ts
+++ b/shared/stores/tests/config.test.ts
@@ -1,17 +1,13 @@
///
-import {defaultUseNativeFrame} from '../../constants/platform'
import {noConversationIDKey} from '../../constants/types/chat/common'
import {useConfigState} from '../config'
const resetConfigState = () => {
const {dispatch} = useConfigState.getState()
useConfigState.setState({
- appFocused: true,
configuredAccounts: [],
defaultUsername: '',
- forceSmallNav: false,
globalError: undefined,
- mobileAppState: 'unknown',
outOfDate: {
critical: false,
message: '',
@@ -25,7 +21,6 @@ const resetConfigState = () => {
link: '',
loaded: false,
},
- useNativeFrame: defaultUseNativeFrame,
userSwitching: false,
} as any)
dispatch.resetState()
@@ -126,9 +121,7 @@ test('custom resetState preserves the fields config intentionally carries across
dispatch.setAccounts([{hasStoredSecret: true, username: 'alice'}])
dispatch.setDefaultUsername('alice')
useConfigState.setState({
- forceSmallNav: true,
globalError: new Error('transient'),
- mobileAppState: 'active',
userSwitching: true,
} as any)
@@ -137,8 +130,6 @@ test('custom resetState preserves the fields config intentionally carries across
const state = useConfigState.getState()
expect(state.configuredAccounts).toEqual([{hasStoredSecret: true, username: 'alice'}])
expect(state.defaultUsername).toBe('alice')
- expect(state.forceSmallNav).toBe(true)
- expect(state.mobileAppState).toBe('active')
expect(state.userSwitching).toBe(true)
expect(state.globalError).toBeUndefined()
})
diff --git a/shared/stores/tests/shell.test.ts b/shared/stores/tests/shell.test.ts
new file mode 100644
index 000000000000..8195f62243d6
--- /dev/null
+++ b/shared/stores/tests/shell.test.ts
@@ -0,0 +1,104 @@
+///
+import {defaultUseNativeFrame} from '../../constants/platform'
+import {useShellState} from '../shell'
+
+const defaultWindowState = {
+ dockHidden: false,
+ height: 800,
+ isFullScreen: false,
+ isMaximized: false,
+ width: 600,
+ windowHidden: false,
+ x: 0,
+ y: 0,
+}
+
+const resetShellState = () => {
+ const {dispatch} = useShellState.getState()
+ useShellState.setState({
+ active: true,
+ appFocused: true,
+ forceSmallNav: false,
+ mobileAppState: 'unknown',
+ networkStatus: undefined,
+ notifySound: false,
+ openAtLogin: true,
+ useNativeFrame: defaultUseNativeFrame,
+ windowState: {...defaultWindowState},
+ } as any)
+ dispatch.resetState()
+}
+
+beforeEach(() => {
+ resetShellState()
+})
+
+afterEach(() => {
+ resetShellState()
+})
+
+test('local shell actions update focus and activity state', () => {
+ const {dispatch} = useShellState.getState()
+
+ dispatch.changedFocus(false)
+ dispatch.setActive(false)
+ dispatch.setMobileAppState('background')
+ dispatch.setWindowMaximized(true)
+
+ expect(useShellState.getState()).toEqual(
+ expect.objectContaining({
+ active: false,
+ appFocused: false,
+ mobileAppState: 'background',
+ windowState: expect.objectContaining({isMaximized: true}),
+ })
+ )
+})
+
+test('resetState preserves shell-owned fields across store resets', () => {
+ const {dispatch} = useShellState.getState()
+
+ useShellState.setState({
+ active: false,
+ appFocused: false,
+ forceSmallNav: true,
+ mobileAppState: 'background',
+ networkStatus: {isInit: true, online: false, type: 'notavailable'},
+ notifySound: true,
+ openAtLogin: false,
+ useNativeFrame: !defaultUseNativeFrame,
+ windowState: {
+ ...defaultWindowState,
+ dockHidden: true,
+ height: 720,
+ isMaximized: true,
+ width: 1024,
+ x: 10,
+ y: 20,
+ },
+ } as any)
+
+ dispatch.resetState()
+
+ expect(useShellState.getState()).toEqual(
+ expect.objectContaining({
+ active: false,
+ appFocused: false,
+ forceSmallNav: true,
+ mobileAppState: 'background',
+ networkStatus: {isInit: true, online: false, type: 'notavailable'},
+ notifySound: true,
+ openAtLogin: false,
+ useNativeFrame: !defaultUseNativeFrame,
+ windowState: {
+ ...defaultWindowState,
+ dockHidden: true,
+ height: 720,
+ isMaximized: true,
+ width: 1024,
+ x: 10,
+ y: 20,
+ },
+ })
+ )
+})
diff --git a/shared/stores/tests/unlock-folders.test.ts b/shared/stores/tests/unlock-folders.test.ts
index 531435cd6bd7..53d300922bb3 100644
--- a/shared/stores/tests/unlock-folders.test.ts
+++ b/shared/stores/tests/unlock-folders.test.ts
@@ -1,14 +1,14 @@
///
import {onUnlockFoldersEngineIncoming} from '../unlock-folders'
-const mockOpenUnlockFolders = jest.fn()
+const mockOpen = jest.fn()
const mockCreateSession = jest.fn()
-jest.mock('@/stores/config', () => ({
- useConfigState: {
+jest.mock('@/unlock-folders/store', () => ({
+ useUnlockFoldersState: {
getState: () => ({
dispatch: {
- openUnlockFolders: mockOpenUnlockFolders,
+ open: mockOpen,
},
}),
},
@@ -23,10 +23,10 @@ jest.mock('@/engine/require', () => ({
afterEach(() => {
jest.restoreAllMocks()
mockCreateSession.mockReset()
- mockOpenUnlockFolders.mockReset()
+ mockOpen.mockReset()
})
-test('rekey refresh actions forward the device list to config', () => {
+test('rekey refresh actions forward the device list to the unlock folders store', () => {
onUnlockFoldersEngineIncoming({
payload: {
params: {
@@ -38,7 +38,7 @@ test('rekey refresh actions forward the device list to config', () => {
type: 'keybase.1.rekeyUI.refresh',
} as any)
- expect(mockOpenUnlockFolders).toHaveBeenCalledWith([{deviceID: 'device-1', name: 'device-1', type: 'desktop'}])
+ expect(mockOpen).toHaveBeenCalledWith([{deviceID: 'device-1', name: 'device-1', type: 'desktop'}])
})
test('delegateRekeyUI creates a dangling session and returns its id', () => {
diff --git a/shared/stores/unlock-folders.tsx b/shared/stores/unlock-folders.tsx
index d556aa52cf17..65fdec2a92f9 100644
--- a/shared/stores/unlock-folders.tsx
+++ b/shared/stores/unlock-folders.tsx
@@ -1,14 +1,14 @@
import type * as EngineGen from '@/constants/rpc'
import logger from '@/logger'
import {getEngine} from '@/engine/require'
-import {useConfigState} from '@/stores/config'
+import {useUnlockFoldersState} from '@/unlock-folders/store'
export const onUnlockFoldersEngineIncoming = (action: EngineGen.Actions) => {
switch (action.type) {
case 'keybase.1.rekeyUI.refresh': {
const {problemSetDevices} = action.payload.params
logger.info('Asked for rekey')
- useConfigState.getState().dispatch.openUnlockFolders(problemSetDevices.devices ?? [])
+ useUnlockFoldersState.getState().dispatch.open(problemSetDevices.devices ?? [])
break
}
case 'keybase.1.rekeyUI.delegateRekeyUI': {
@@ -17,7 +17,7 @@ export const onUnlockFoldersEngineIncoming = (action: EngineGen.Actions) => {
dangling: true,
incomingCallMap: {
'keybase.1.rekeyUI.refresh': ({problemSetDevices}) => {
- useConfigState.getState().dispatch.openUnlockFolders(problemSetDevices.devices ?? [])
+ useUnlockFoldersState.getState().dispatch.open(problemSetDevices.devices ?? [])
},
'keybase.1.rekeyUI.rekeySendEvent': () => {}, // ignored debug call from daemon
},
diff --git a/shared/unlock-folders/device-list.desktop.tsx b/shared/unlock-folders/device-list.desktop.tsx
index 7c9774581987..933db39afd1a 100644
--- a/shared/unlock-folders/device-list.desktop.tsx
+++ b/shared/unlock-folders/device-list.desktop.tsx
@@ -1,13 +1,12 @@
import * as Kb from '@/common-adapters'
-import type {State as ConfigStore} from '@/stores/config'
+import type {UnlockFolderDevice} from './store'
export type Props = {
- devices: ConfigStore['unlockFoldersDevices']
+ devices: ReadonlyArray
toPaperKeyInput: () => void
}
-type Device = ConfigStore['unlockFoldersDevices'][0]
-const DeviceRow = ({device}: {device: Device}) => {
+const DeviceRow = ({device}: {device: UnlockFolderDevice}) => {
const icon = (
{
backup: 'icon-paper-key-32',
diff --git a/shared/unlock-folders/index.desktop.tsx b/shared/unlock-folders/index.desktop.tsx
index fd11a095be76..25982cd2766c 100644
--- a/shared/unlock-folders/index.desktop.tsx
+++ b/shared/unlock-folders/index.desktop.tsx
@@ -4,13 +4,13 @@ import DeviceList from './device-list.desktop'
import DragHeader from '../desktop/remote/drag-header.desktop'
import PaperKeyInput from './paper-key-input.desktop'
import Success from './success.desktop'
-import type {State as ConfigStore} from '@/stores/config'
+import type {UnlockFolderDevice} from './store'
type Phase = 'dead' | 'promptOtherDevice' | 'paperKeyInput' | 'success'
export type Props = {
phase: Phase
- devices: ConfigStore['unlockFoldersDevices']
+ devices: ReadonlyArray
onClose: () => void
toPaperKeyInput: () => void
onBackFromPaperKey: () => void
diff --git a/shared/unlock-folders/main2.desktop.tsx b/shared/unlock-folders/main2.desktop.tsx
index 254a1980c5d8..08f4fcaf1012 100644
--- a/shared/unlock-folders/main2.desktop.tsx
+++ b/shared/unlock-folders/main2.desktop.tsx
@@ -4,11 +4,11 @@ import * as RemoteGen from '../constants/remote-actions'
import UnlockFolders from './index.desktop'
import loadRemoteComponent from '../desktop/remote/component-loader.desktop'
import {RemoteDarkModeSync} from '../desktop/remote/remote-component.desktop'
-import type {State as ConfigStore} from '@/stores/config'
+import type {UnlockFolderDevice} from './store'
export type ProxyProps = {
darkMode: boolean
- devices: ConfigStore['unlockFoldersDevices']
+ devices: ReadonlyArray
paperKeyError: string
waiting: boolean
}
diff --git a/shared/unlock-folders/remote-proxy.desktop.tsx b/shared/unlock-folders/remote-proxy.desktop.tsx
index 216f37ada0bb..b1e108a7dbbf 100644
--- a/shared/unlock-folders/remote-proxy.desktop.tsx
+++ b/shared/unlock-folders/remote-proxy.desktop.tsx
@@ -2,8 +2,8 @@ import * as C from '@/constants'
import useBrowserWindow from '../desktop/remote/use-browser-window.desktop'
import useSerializeProps from '../desktop/remote/use-serialize-props.desktop'
import {useColorScheme} from 'react-native'
-import {useConfigState} from '@/stores/config'
import type {ProxyProps} from './main2.desktop'
+import {useUnlockFoldersState} from './store'
const windowOpts = {height: 300, width: 500}
@@ -23,8 +23,8 @@ function UnlockFolders(p: ProxyProps) {
}
const UnlockRemoteProxy = () => {
- const devices = useConfigState(s => s.unlockFoldersDevices)
- const paperKeyError = useConfigState(s => s.unlockFoldersError)
+ const devices = useUnlockFoldersState(s => s.devices)
+ const paperKeyError = useUnlockFoldersState(s => s.paperKeyError)
const waiting = C.Waiting.useAnyWaiting('unlock-folders:waiting')
const isDarkMode = useColorScheme() === 'dark'
if (devices.length) {
diff --git a/shared/unlock-folders/store.tsx b/shared/unlock-folders/store.tsx
new file mode 100644
index 000000000000..aca11f5a9c9f
--- /dev/null
+++ b/shared/unlock-folders/store.tsx
@@ -0,0 +1,54 @@
+import * as T from '@/constants/types'
+import * as Z from '@/util/zustand'
+
+export type UnlockFolderDevice = T.Immutable<{
+ type: T.Devices.DeviceType
+ name: string
+ deviceID: T.Devices.DeviceID
+}>
+
+type Store = T.Immutable<{
+ devices: Array
+ paperKeyError: string
+}>
+
+const initialStore: Store = {
+ devices: [],
+ paperKeyError: '',
+}
+
+export type State = Store & {
+ dispatch: {
+ close: () => void
+ open: (devices: ReadonlyArray) => void
+ resetState: () => void
+ setPaperKeyError: (paperKeyError: string) => void
+ }
+}
+
+export const useUnlockFoldersState = Z.createZustand('unlock-folders', (set, get) => {
+ const dispatch: State['dispatch'] = {
+ close: () => get().dispatch.resetState(),
+ open: devices => {
+ set(s => {
+ s.devices = devices.map(({name, type, deviceID}) => ({
+ deviceID,
+ name,
+ type: T.Devices.stringToDeviceType(type),
+ }))
+ s.paperKeyError = ''
+ })
+ },
+ resetState: Z.defaultReset,
+ setPaperKeyError: paperKeyError => {
+ set(s => {
+ s.paperKeyError = paperKeyError
+ })
+ },
+ }
+
+ return {
+ ...initialStore,
+ dispatch,
+ }
+})