diff --git a/shared/constants/config.tsx b/shared/constants/config.tsx index 4e1a2bc1d857..d2aa68b02c40 100644 --- a/shared/constants/config.tsx +++ b/shared/constants/config.tsx @@ -76,6 +76,7 @@ export type Store = { networkStatus?: {online: boolean; type: T.Config.ConnectionType; isInit?: boolean} notifySound: boolean openAtLogin: boolean + justQuit: boolean outOfDate: T.Config.OutOfDate remoteWindowNeedsProps: Map> revokedTrigger: number @@ -126,6 +127,7 @@ const initialStore: Store = { installerRanCount: 0, isOnline: true, justDeletedSelf: '', + justQuit: false, justRevokedSelf: '', loadOnStartPhase: 'notStarted', loggedIn: false, @@ -189,6 +191,7 @@ type State = Store & { filePickerError: (error: Error) => void initAppUpdateLoop: () => void initNotifySound: () => void + initJustQuit: () => void initOpenAtLogin: () => void initUseNativeFrame: () => void installerRan: () => void @@ -218,6 +221,7 @@ type State = Store & { setHTTPSrvInfo: (address: string, token: string) => void setIncomingShareUseOriginal: (use: boolean) => void setJustDeletedSelf: (s: string) => void + setJustQuit: (justQuit: boolean) => void setLoggedIn: (l: boolean, causedByStartup: boolean) => void setMobileAppState: (nextAppState: 'active' | 'background' | 'inactive') => void setNavigatorExists: () => void @@ -241,6 +245,7 @@ type State = Store & { } export const openAtLoginKey = 'openAtLogin' +export const justQuitKey = 'justQuit' export const _useConfigState = Z.createZustand((set, get) => { const nativeFrameKey = 'useNativeFrame' const notifySoundKey = 'notifySound' @@ -460,6 +465,16 @@ export const _useConfigState = Z.createZustand((set, get) => { } ignorePromise(f()) }, + initJustQuit: () => { + const f = async () => { + const val = await T.RPCGen.configGuiGetValueRpcPromise({path: justQuitKey}) + const justQuit = val.b + if (typeof justQuit === 'boolean') { + get().dispatch.setJustQuit(justQuit) + } + } + ignorePromise(f()) + }, initNotifySound: () => { const f = async () => { const val = await T.RPCGen.configGuiGetValueRpcPromise({path: notifySoundKey}) @@ -1007,6 +1022,11 @@ export const _useConfigState = Z.createZustand((set, get) => { s.justDeletedSelf = self }) }, + setJustQuit: (justQuit: boolean) => { + set(s => { + s.justQuit = justQuit + }) + }, setLoggedIn: (loggedIn, causedByStartup) => { const changed = get().loggedIn !== loggedIn set(s => { diff --git a/shared/constants/platform-specific/index.desktop.tsx b/shared/constants/platform-specific/index.desktop.tsx index ec8e05dc62a6..d8c0a86ded8e 100644 --- a/shared/constants/platform-specific/index.desktop.tsx +++ b/shared/constants/platform-specific/index.desktop.tsx @@ -199,32 +199,44 @@ export const initPlatformListener = () => { setupReachabilityWatcher() C.useConfigState.subscribe((s, old) => { - if (s.openAtLogin === old.openAtLogin) return - const {openAtLogin} = s - const f = async () => { - if (__DEV__) { - console.log('onSetOpenAtLogin disabled for dev mode') - return - } else { - await T.RPCGen.configGuiSetValueRpcPromise({ - path: ConfigConstants.openAtLoginKey, - value: {b: openAtLogin, isNull: false}, - }) - } - if (isLinux || isWindows) { - const enabled = - (await T.RPCGen.ctlGetOnLoginStartupRpcPromise()) === T.RPCGen.OnLoginStartupStatus.enabled - if (enabled !== openAtLogin) { - await T.RPCGen.ctlSetOnLoginStartupRpcPromise({enabled: openAtLogin}).catch(err => { - logger.warn(`Error in sending ctlSetOnLoginStartup: ${err.message}`) + if (s.openAtLogin !== old.openAtLogin) { + const {openAtLogin} = s + const f = async () => { + if (__DEV__) { + console.log('onSetOpenAtLogin disabled for dev mode') + return + } else { + await T.RPCGen.configGuiSetValueRpcPromise({ + path: ConfigConstants.openAtLoginKey, + value: {b: openAtLogin, isNull: false}, }) } - } else { - logger.info(`Login item settings changed! now ${openAtLogin ? 'on' : 'off'}`) - await setOpenAtLogin?.(openAtLogin) + if (isLinux || isWindows) { + const enabled = + (await T.RPCGen.ctlGetOnLoginStartupRpcPromise()) === T.RPCGen.OnLoginStartupStatus.enabled + if (enabled !== openAtLogin) { + await T.RPCGen.ctlSetOnLoginStartupRpcPromise({enabled: openAtLogin}).catch(err => { + logger.warn(`Error in sending ctlSetOnLoginStartup: ${err.message}`) + }) + } + } else { + logger.info(`Login item settings changed! now ${openAtLogin ? 'on' : 'off'}`) + await setOpenAtLogin?.(openAtLogin) + } } + C.ignorePromise(f()) + } + + if (s.justQuit !== old.justQuit) { + const {justQuit} = s + const f = async () => { + await T.RPCGen.configGuiSetValueRpcPromise({ + path: ConfigConstants.justQuitKey, + value: {b: justQuit, isNull: false}, + }) + } + C.ignorePromise(f()) } - C.ignorePromise(f()) }) C.useDaemonState.subscribe((s, old) => { @@ -237,6 +249,7 @@ export const initPlatformListener = () => { } C.useConfigState.getState().dispatch.initNotifySound() C.useConfigState.getState().dispatch.initOpenAtLogin() + C.useConfigState.getState().dispatch.initJustQuit() C.useConfigState.getState().dispatch.initAppUpdateLoop() C.useProfileState.setState(s => { diff --git a/shared/desktop/app/main-window.desktop.tsx b/shared/desktop/app/main-window.desktop.tsx index 5bbde2c7fe95..7a4a4c445de6 100644 --- a/shared/desktop/app/main-window.desktop.tsx +++ b/shared/desktop/app/main-window.desktop.tsx @@ -108,6 +108,7 @@ let useNativeFrame = defaultUseNativeFrame let isDarkMode = false let darkModePreference: undefined | 'system' | 'alwaysDark' | 'alwaysLight' let disableSpellCheck = false +let justQuit = false /** * loads data that we normally save from configGuiSetValue. At this point the service might not exist so we must read it directly @@ -123,6 +124,7 @@ const loadWindowState = () => { const guiConfig = JSON.parse(s) as | Partial<{ openAtLogin: unknown + justQuit: unknown useNativeFrame: unknown ui: Partial<{ disableSpellCheck: unknown @@ -133,6 +135,7 @@ const loadWindowState = () => { | undefined openAtLogin = typeof guiConfig?.openAtLogin === 'boolean' ? guiConfig.openAtLogin : openAtLogin + justQuit = typeof guiConfig?.justQuit === 'boolean' ? guiConfig.justQuit : justQuit useNativeFrame = typeof guiConfig?.useNativeFrame === 'boolean' ? guiConfig.useNativeFrame : useNativeFrame @@ -230,9 +233,11 @@ const fixWindowsScalingIssue = (win: Electron.BrowserWindow) => { } const maybeShowWindowOrDock = (win: Electron.BrowserWindow) => { - const openedAtLogin = Electron.app.getLoginItemSettings().wasOpenedAtLogin - // app.getLoginItemSettings().restoreState is Mac only, so consider it always on in Windows - const isRestore = !!env.KEYBASE_RESTORE_UI || Electron.app.getLoginItemSettings().restoreState || isWindows + const openedAtLogin = + Electron.app.getLoginItemSettings().wasOpenedAtLogin || + Electron.app.getLoginItemSettings().executableWillLaunchAtLogin + // app.getLoginItemSettings().restoreState is Mac only + const isRestore = !!env.KEYBASE_RESTORE_UI || Electron.app.getLoginItemSettings().restoreState const hideWindowOnStart = env.KEYBASE_AUTOSTART === '1' const openHidden = Electron.app.getLoginItemSettings().wasOpenedAsHidden logger.info('KEYBASE_AUTOSTART =', env.KEYBASE_AUTOSTART) @@ -339,7 +344,7 @@ const MainWindow = () => { win.setFullScreen(true) } - menuHelper(win) + menuHelper(win, justQuit) if (showDevTools) { win.webContents.openDevTools({mode: 'detach', title: `${__DEV__ ? 'DEV' : 'Prod'} Keybase Devtools`}) diff --git a/shared/desktop/app/menu-helper.desktop.tsx b/shared/desktop/app/menu-helper.desktop.tsx index 31affd4f0e31..3ce3d232bfa4 100644 --- a/shared/desktop/app/menu-helper.desktop.tsx +++ b/shared/desktop/app/menu-helper.desktop.tsx @@ -19,7 +19,7 @@ const reallyQuit = () => { }, 2000) } -export default function makeMenu(window: Electron.BrowserWindow) { +export default function makeMenu(window: Electron.BrowserWindow, justQuit: boolean) { const editMenu = new Electron.MenuItem({ label: 'Edit', submenu: Electron.Menu.buildFromTemplate([ @@ -99,17 +99,25 @@ export default function makeMenu(window: Electron.BrowserWindow) { new Electron.MenuItem({ accelerator: 'CmdOrCtrl+Q', click() { - closeWindows() - }, - label: 'Minimize to Tray', - }), - new Electron.MenuItem({ - accelerator: 'CmdOrCtrl+Option+Q', - click() { - reallyQuit() + if (justQuit) { + reallyQuit() + } else { + closeWindows() + } }, - label: 'Quit Keybase Completely', + label: justQuit ? 'Quit Keybase Completely' : 'Minimize to Tray', }), + ...(justQuit + ? [] + : [ + new Electron.MenuItem({ + accelerator: 'CmdOrCtrl+Option+Q', + click() { + reallyQuit() + }, + label: 'Quit Keybase Completely', + }), + ]), ]), }), {...editMenu}, diff --git a/shared/desktop/app/node2.desktop.tsx b/shared/desktop/app/node2.desktop.tsx index 4964791b9b67..50f5a1da58b1 100644 --- a/shared/desktop/app/node2.desktop.tsx +++ b/shared/desktop/app/node2.desktop.tsx @@ -839,7 +839,7 @@ const plumbEvents = () => { .catch(() => {}) if (action.payload.windowComponent !== 'menubar') { - menuHelper(remoteWindow) + menuHelper(remoteWindow, false) } if (showDevTools && !skipSecondaryDevtools) { diff --git a/shared/settings/advanced.tsx b/shared/settings/advanced.tsx index cb2a9c6382f6..599e5a8b9198 100644 --- a/shared/settings/advanced.tsx +++ b/shared/settings/advanced.tsx @@ -71,11 +71,13 @@ const Advanced = () => { const settingLockdownMode = C.Waiting.useAnyWaiting(Constants.setLockdownModeWaitingKey) const hasRandomPW = C.useSettingsPasswordState(s => !!s.randomPW) const openAtLogin = C.useConfigState(s => s.openAtLogin) + const justQuit = C.useConfigState(s => s.justQuit) const rememberPassword = C.useSettingsPasswordState(s => s.rememberPassword) const setLockdownModeError = C.Waiting.useAnyErrors(Constants.setLockdownModeWaitingKey)?.message || '' const setRememberPassword = C.useSettingsPasswordState(s => s.dispatch.setRememberPassword) const onChangeRememberPassword = setRememberPassword const onSetOpenAtLogin = C.useConfigState(s => s.dispatch.setOpenAtLogin) + const setJustQuit = C.useConfigState(s => s.dispatch.setJustQuit) const [disableSpellCheck, setDisableSpellcheck] = React.useState(undefined) @@ -157,6 +159,13 @@ const Advanced = () => { {!C.isMobile && ( )} + {!C.isMobile && ( + + )} {!C.isMobile && (