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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@

- This repo uses React Compiler. Assume React Compiler patterns are enabled when editing React code, and avoid adding `useMemo`/`useCallback` by default unless they are clearly needed for correctness or compatibility with existing code.
- Treat React mount/unmount effects as Strict-Mode-safe. Do not assume a component only mounts once; route-driven async startup and cleanup logic must be idempotent and must not leave refs or guards stuck false after a dev remount.
- When using mount guards like `mountedRef`/`isMountedRef`, always set the ref to `true` inside the effect body and set it to `false` in cleanup. Never rely on `useRef(true)` alone across the component lifetime, because Strict Mode remounts can leave the guard stuck `false` and silently drop async results.
- When a component reads multiple adjacent values from the same store hook, prefer a consolidated selector with `C.useShallow(...)` instead of multiple separate subscriptions.
- 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.
- 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.
24 changes: 0 additions & 24 deletions shared/constants/init/shared.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,6 @@ import {useProvisionState} from '@/stores/provision'
import {usePushState} from '@/stores/push'
import {useSettingsContactsState} from '@/stores/settings-contacts'
import {useSettingsEmailState} from '@/stores/settings-email'
import {useSettingsPhoneState} from '@/stores/settings-phone'
import {useSettingsState} from '@/stores/settings'
import {useSignupState} from '@/stores/signup'
import {useState as useRecoverPasswordState} from '@/stores/recover-password'
import {useTeamsState} from '@/stores/teams'
Expand Down Expand Up @@ -363,27 +361,6 @@ export const initTracker2Callbacks = () => {
})
}

export const initSettingsCallbacks = () => {
const currentState = useSettingsState.getState()
useSettingsState.setState({
dispatch: {
...currentState.dispatch,
defer: {
...currentState.dispatch.defer,
getSettingsPhonePhones: () => {
return useSettingsPhoneState.getState().phones
},
onSettingsEmailNotifyEmailsChanged: (emails: ReadonlyArray<T.RPCChat.Keybase1.Email>) => {
useSettingsEmailState.getState().dispatch.notifyEmailAddressEmailsChanged(emails)
},
onSettingsPhoneSetNumbers: (phoneNumbers?: ReadonlyArray<T.RPCChat.Keybase1.UserPhoneNumber>) => {
useSettingsPhoneState.getState().dispatch.setNumbers(phoneNumbers)
},
},
},
})
}

export const initSharedSubscriptions = () => {
// HMR cleanup: unsubscribe old store subscriptions before re-subscribing
for (const unsub of _sharedUnsubs) unsub()
Expand Down Expand Up @@ -680,7 +657,6 @@ export const initSharedSubscriptions = () => {
initNotificationsCallbacks()
initPushCallbacks()
initRecoverPasswordCallbacks()
initSettingsCallbacks()
initSignupCallbacks()
initTracker2Callbacks()
}
Expand Down
2 changes: 1 addition & 1 deletion shared/desktop/renderer/remote-event-handler.desktop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export const eventFromRemoteWindows = (action: RemoteGen.Actions) => {
break
}
case RemoteGen.stop: {
storeRegistry.getState('settings').dispatch.stop(action.payload.exitCode)
ignorePromise(T.RPCGen.ctlStopRpcPromise({exitCode: action.payload.exitCode}))
break
}
case RemoteGen.trackerChangeFollow: {
Expand Down
2 changes: 1 addition & 1 deletion shared/devices/device-revoke.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {useConfigState} from '@/stores/config'
import * as Kb from '@/common-adapters'
import * as React from 'react'
import * as T from '@/constants/types'
import {settingsDevicesTab} from '@/stores/settings'
import {settingsDevicesTab} from '@/constants/settings'
import {useCurrentUserState} from '@/stores/current-user'

type OwnProps = {device?: T.Devices.Device; deviceID?: T.Devices.DeviceID}
Expand Down
2 changes: 1 addition & 1 deletion shared/incoming-share/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as React from 'react'
import * as Kb from '@/common-adapters'
import * as T from '@/constants/types'
import {MobileSendToChat} from '../chat/send-to-chat'
import {settingsFeedbackTab} from '@/stores/settings'
import {settingsFeedbackTab} from '@/constants/settings'
import * as FS from '@/stores/fs'
import {useConfigState} from '@/stores/config'
import {useFSState} from '@/stores/fs'
Expand Down
21 changes: 14 additions & 7 deletions shared/settings/account/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as C from '@/constants'
import * as Kb from '@/common-adapters'
import * as T from '@/constants/types'
import type * as React from 'react'
import EmailPhoneRow from './email-phone-row'
import {openURL} from '@/util/misc'
import {loadSettings} from '../load-settings'
import {usePWState} from '@/stores/settings-password'
import {useSettingsPhoneState} from '@/stores/settings-phone'
import {useSettingsEmailState} from '@/stores/settings-email'
import {useSettingsState, settingsPasswordTab} from '@/stores/settings'
import {settingsPasswordTab} from '@/constants/settings'

export const SettingsSection = ({children}: {children: React.ReactNode}) => (
<Kb.Box2 direction="vertical" gap="tiny" fullWidth={true} style={styles.section}>
Expand Down Expand Up @@ -112,7 +115,16 @@ const Password = () => {
}

const WebAuthTokenLogin = () => {
const loginBrowserViaWebAuthToken = useSettingsState(s => s.dispatch.loginBrowserViaWebAuthToken)
const generateWebAuthToken = C.useRPC(T.RPCGen.configGenerateWebAuthTokenRpcPromise)
const loginBrowserViaWebAuthToken = () => {
generateWebAuthToken(
[undefined],
link => {
openURL(link)
},
() => {}
)
}
return (
<SettingsSection>
<Kb.Box2 direction="vertical" gap="xtiny" fullWidth={true}>
Expand Down Expand Up @@ -177,11 +189,6 @@ const AccountSettings = () => {
editPhone: s.dispatch.editPhone,
}))
)
const {loadSettings} = useSettingsState(
C.useShallow(s => ({
loadSettings: s.dispatch.loadSettings,
}))
)
const {loadHasRandomPw} = usePWState(
C.useShallow(s => ({
loadHasRandomPw: s.dispatch.loadHasRandomPw,
Expand Down
131 changes: 106 additions & 25 deletions shared/settings/advanced.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,75 @@ import * as Kb from '@/common-adapters'
import * as T from '@/constants/types'
import * as React from 'react'
import {ProxySettings} from './proxy'
import {useSettingsState, traceInProgressKey, processorProfileInProgressKey} from '@/stores/settings'
import {processorProfileInProgressKey, traceInProgressKey} from '@/constants/settings'
import {usePWState} from '@/stores/settings-password'
import {useFSState} from '@/stores/fs'
import {useConfigState} from '@/stores/config'
import {ignorePromise, timeoutPromise} from '@/constants/utils'
import {pprofDir} from '@/constants/platform'
import {clearLocalLogs} from '@/util/misc'
import {useWaitingState} from '@/stores/waiting'

let initialUseNativeFrame: boolean | undefined

const showMakeIcons = __DEV__ && (false as boolean)

const runPprofAction = (
rpc: () => Promise<void>,
waitingKey: string,
durationSeconds: number
) => {
const f = async () => {
await rpc()
const {decrement, increment} = useWaitingState.getState().dispatch
increment(waitingKey)
await timeoutPromise(durationSeconds * 1_000)
decrement(waitingKey)
}
ignorePromise(f())
}

const useLockdownMode = () => {
const [lockdownModeEnabled, setLockdownModeEnabled] = React.useState<boolean | undefined>(undefined)
const loadLockdownModeRPC = C.useRPC(T.RPCGen.accountGetLockdownModeRpcPromise)
const setLockdownModeRPC = C.useRPC(T.RPCGen.accountSetLockdownModeRpcPromise)

const loadLockdownMode = React.useCallback(() => {
if (!useConfigState.getState().loggedIn) {
return
}
loadLockdownModeRPC(
[undefined],
result => {
setLockdownModeEnabled(result.status)
},
() => {
setLockdownModeEnabled(undefined)
}
)
}, [loadLockdownModeRPC])

const setLockdownMode = React.useCallback(
(enabled: boolean) => {
if (!useConfigState.getState().loggedIn) {
return
}
setLockdownModeRPC(
[{enabled}, C.waitingKeySettingsSetLockdownMode],
() => {
setLockdownModeEnabled(enabled)
},
() => {
setLockdownModeEnabled(undefined)
}
)
},
[setLockdownModeRPC]
)

return {loadLockdownMode, lockdownModeEnabled, setLockdownMode}
}

const UseNativeFrame = () => {
const {onChangeUseNativeFrame, useNativeFrame} = useConfigState(
C.useShallow(s => ({
Expand Down Expand Up @@ -40,14 +100,13 @@ const UseNativeFrame = () => {
)
}

const LockdownCheckbox = (p: {hasRandomPW: boolean; settingLockdownMode: boolean}) => {
const {hasRandomPW, settingLockdownMode} = p
const {lockdownModeEnabled, setLockdownMode} = useSettingsState(
C.useShallow(s => ({
lockdownModeEnabled: !!s.lockdownModeEnabled,
setLockdownMode: s.dispatch.setLockdownMode,
}))
)
const LockdownCheckbox = (p: {
hasRandomPW: boolean
lockdownModeEnabled?: boolean
setLockdownMode: (enabled: boolean) => void
settingLockdownMode: boolean
}) => {
const {hasRandomPW, lockdownModeEnabled, setLockdownMode, settingLockdownMode} = p
const onChangeLockdownMode = setLockdownMode
const readMoreUrlProps = Kb.useClickURL('https://keybase.io/docs/lockdown/index')
const label = 'Enable account lockdown mode' + (hasRandomPW ? ' (you need to set a password first)' : '')
Expand Down Expand Up @@ -96,11 +155,7 @@ const Advanced = () => {
openAtLogin: s.openAtLogin,
}))
)
const {loadLockdownMode} = useSettingsState(
C.useShallow(s => ({
loadLockdownMode: s.dispatch.loadLockdownMode,
}))
)
const {loadLockdownMode, lockdownModeEnabled, setLockdownMode} = useLockdownMode()
const setLockdownModeError = C.Waiting.useAnyErrors(C.waitingKeySettingsSetLockdownMode)?.message || ''
const [rememberPassword, setRememberPassword] = React.useState<boolean | undefined>(undefined)

Expand Down Expand Up @@ -187,7 +242,12 @@ const Advanced = () => {
<Kb.Box2 direction="vertical" fullWidth={true}>
<Kb.Box2 direction="vertical" gap="tiny" fullWidth={true} style={styles.section}>
{settingLockdownMode && <Kb.ProgressIndicator />}
<LockdownCheckbox hasRandomPW={hasRandomPW} settingLockdownMode={settingLockdownMode} />
<LockdownCheckbox
hasRandomPW={hasRandomPW}
lockdownModeEnabled={lockdownModeEnabled}
setLockdownMode={setLockdownMode}
settingLockdownMode={settingLockdownMode}
/>
{!!setLockdownModeError && (
<Kb.Text type="BodySmall" style={styles.error}>
{setLockdownModeError}
Expand Down Expand Up @@ -265,17 +325,38 @@ const Developer = () => {

const showPprofControls = clickCount >= clickThreshold
const traceInProgress = C.Waiting.useAnyWaiting(traceInProgressKey)
const {onProcessorProfile, onTrace} = useSettingsState(
C.useShallow(s => ({
onProcessorProfile: s.dispatch.processorProfile,
onTrace: s.dispatch.trace,
}))
)
const onProcessorProfile = () => {
runPprofAction(
async () =>
T.RPCGen.pprofLogProcessorProfileRpcPromise({
logDirForMobile: pprofDir,
profileDurationSeconds: processorProfileDurationSeconds,
}),
processorProfileInProgressKey,
processorProfileDurationSeconds
)
}
const onTrace = () => {
runPprofAction(
async () =>
T.RPCGen.pprofLogTraceRpcPromise({
logDirForMobile: pprofDir,
traceDurationSeconds,
}),
traceInProgressKey,
traceDurationSeconds
)
}
const processorProfileInProgress = C.Waiting.useAnyWaiting(processorProfileInProgressKey)
const navigateAppend = C.useRouterState(s => s.dispatch.navigateAppend)
const onDBNuke = () => navigateAppend('dbNukeConfirm')
const onMakeIcons = () => navigateAppend('makeIcons')
const onClearLogs = useSettingsState(s => s.dispatch.clearLogs)
const onClearLogs = () => {
const f = async () => {
await clearLocalLogs()
}
ignorePromise(f())
}

return (
<Kb.Box2 direction="vertical" fullWidth={true} alignItems="center" flex={1} style={styles.developerContainer}>
Expand Down Expand Up @@ -320,14 +401,14 @@ const Developer = () => {
style={{marginTop: Kb.Styles.globalMargins.small}}
type="Danger"
label={`Trace (${traceDurationSeconds}s)`}
onClick={() => onTrace(traceDurationSeconds)}
onClick={onTrace}
/>
<Kb.Button
waiting={processorProfileInProgress}
style={{marginTop: Kb.Styles.globalMargins.small}}
type="Danger"
label={`CPU Profile (${traceDurationSeconds}s)`}
onClick={() => onProcessorProfile(processorProfileDurationSeconds)}
label={`CPU Profile (${processorProfileDurationSeconds}s)`}
onClick={onProcessorProfile}
/>
<Kb.Text center={true} type="BodySmallSemibold" style={styles.text}>
Trace and profile files are included in logs sent with feedback.
Expand Down
2 changes: 1 addition & 1 deletion shared/settings/archive/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {fsCacheDir, isAndroid} from '@/constants/platform'
import {pickSave} from '@/util/misc'
import * as FsCommon from '@/fs/common'
import {useArchiveState} from '@/stores/archive'
import {settingsArchiveTab} from '@/stores/settings'
import {settingsArchiveTab} from '@/constants/settings'
import {useCurrentUserState} from '@/stores/current-user'
import {getConvoState} from '@/stores/convostate'
import {makeUUID} from '@/util/uuid'
Expand Down
6 changes: 2 additions & 4 deletions shared/settings/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import * as Teams from '@/stores/teams'
import * as T from '@/constants/types'
import * as React from 'react'
import Group from './group'
import {loadSettings} from './load-settings'
import {useSettingsChatState as useSettingsChatState} from '@/stores/settings-chat'
import {useSettingsNotifState} from '@/stores/settings-notifications'
import {useSettingsState} from '@/stores/settings'
import {useConfigState} from '@/stores/config'

const emptyList = new Array<string>()
Expand Down Expand Up @@ -105,13 +105,11 @@ const Security = () => {
}
}, [_contactSettingsSelectedTeams, contactSettingsSelectedTeams])

const loadSettings = useSettingsState(s => s.dispatch.loadSettings)

React.useEffect(() => {
loadSettings()
notifRefresh()
contactSettingsRefresh()
}, [contactSettingsRefresh, loadSettings, notifRefresh])
}, [contactSettingsRefresh, notifRefresh])

return (
<>
Expand Down
6 changes: 3 additions & 3 deletions shared/settings/db-nuke.confirm.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import * as C from '@/constants'
import * as Kb from '@/common-adapters'
import {useSettingsState} from '@/stores/settings'
import * as T from '@/constants/types'

const DbNukeConfirm = () => {
const navigateUp = C.useRouterState(s => s.dispatch.navigateUp)
const dbNuke = C.useRPC(T.RPCGen.ctlDbNukeRpcPromise)
const onCancel = () => {
navigateUp()
}
const dbNuke = useSettingsState(s => s.dispatch.dbNuke)
const onDBNuke = () => {
navigateUp()
dbNuke()
dbNuke([undefined, C.waitingKeySettingsGeneric], () => {}, () => {})
}

return (
Expand Down
8 changes: 4 additions & 4 deletions shared/settings/delete-confirm/check-passphrase.native.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import * as C from '@/constants'
import * as React from 'react'
import * as Kb from '@/common-adapters'
import {useSettingsState} from '@/stores/settings'
import {useDeleteAccount} from '../use-delete-account'
import {usePasswordCheck} from '../use-password-check'

const CheckPassphraseMobile = () => {
const [password, setPassword] = React.useState('')
const [showTyping, setShowTyping] = React.useState(false)

const checkPasswordIsCorrect = useSettingsState(s => s.checkPasswordIsCorrect)
const checkPassword = useSettingsState(s => s.dispatch.checkPassword)
const deleteAccountForever = useSettingsState(s => s.dispatch.deleteAccountForever)
const {checkPassword, checkPasswordIsCorrect} = usePasswordCheck()
const deleteAccountForever = useDeleteAccount()

const onCheckPassword = checkPassword
const deleteForever = () => {
Expand Down
Loading