diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 3b305e3f7..951ed9701 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -969,6 +969,11 @@ "supportDescription": "Having issues? Explore help articles or open a ticket with Session Support.", "supportGoTo": "Go to Support Page", "systemInformationDesktop": "System Information: {information}", + "settingsCannotChangeDesktop": "Cannot Update Setting", + "settingsStartCategoryDesktop": "Startup", + "launchOnStartDesktop": "Launch on Startup", + "launchOnStartDescriptionDesktop": "Launch Session automatically when your computer starts up.", + "launchOnStartupDisabledDesktop": "This setting is managed by your system on Linux. To have Session start automatically, please add it to your startup applications in your system settings.", "tapToRetry": "Tap to retry", "theContinue": "Continue", "theDefault": "Default", diff --git a/preload.js b/preload.js index 68a925d9c..695f09cf4 100644 --- a/preload.js +++ b/preload.js @@ -12,6 +12,7 @@ const { isEmpty } = require('lodash'); const { setupI18n } = require('./ts/util/i18n/i18n'); const { UserUtils } = require('./ts/session/utils'); const { BlindingActions } = require('./ts/webworker/workers/browser/libsession_worker_interface'); +const { SettingsKey } = require('./ts/data/settings-key'); const { crowdinLocale } = ipc.sendSync('locale-data'); @@ -135,6 +136,7 @@ window.setStartInTray = async startInTray => return; }); ipc.send('start-in-tray-on-start', startInTray); + void window.setSettingValue(SettingsKey.settingsStartInTray, startInTray); }); window.getStartInTray = async () => { @@ -146,6 +148,29 @@ window.getStartInTray = async () => { }); }; +window.setAutoStartEnabled = async autoStart => + new Promise((resolve, reject) => { + ipc.once('set-auto-start-enabled-response', (_event, error) => { + if (error) { + reject(error); + return; + } + resolve(); + return; + }); + ipc.send('set-auto-start-enabled', autoStart); + void window.setSettingValue(SettingsKey.settingsAutoStart, autoStart); + }); + +window.getAutoStartEnabled = async () => { + return new Promise(resolve => { + ipc.once('get-auto-start-enabled-response', (_event, value) => { + resolve(value); + }); + ipc.send('get-auto-start-enabled'); + }); +}; + window.getOpengroupPruning = async () => { return new Promise(resolve => { ipc.once('get-opengroup-pruning-response', (_event, value) => { diff --git a/ts/components/dialog/LocalizedPopupDialog.tsx b/ts/components/dialog/LocalizedPopupDialog.tsx index f6bd58a46..65ca2794e 100644 --- a/ts/components/dialog/LocalizedPopupDialog.tsx +++ b/ts/components/dialog/LocalizedPopupDialog.tsx @@ -50,15 +50,17 @@ export function LocalizedPopupDialog(props: LocalizedPopupDialogState) { - - - {tr('okay')} - - + {!props.hideOkayButton ? ( + + + {tr('okay')} + + + ) : null} ); diff --git a/ts/components/dialog/user-settings/components/SettingsToggleBasic.tsx b/ts/components/dialog/user-settings/components/SettingsToggleBasic.tsx index 6540e642e..04031f01f 100644 --- a/ts/components/dialog/user-settings/components/SettingsToggleBasic.tsx +++ b/ts/components/dialog/user-settings/components/SettingsToggleBasic.tsx @@ -1,21 +1,49 @@ -import type { SettingsToggles } from 'react'; +import { useCallback, type SettingsToggles } from 'react'; +import { useDispatch } from 'react-redux'; import { PanelButtonTextWithSubText } from '../../../buttons/panel/PanelButton'; import { PanelToggleButton } from '../../../buttons/panel/PanelToggleButton'; import { type TrArgs } from '../../../../localization/localeTools'; +import { showLocalizedPopupDialog } from '../../LocalizedPopupDialog'; -export function SettingsToggleBasic({ - active, - baseDataTestId, - onClick, - text, - subText, -}: { +type UnavailableProps = { + unavailable: boolean; + modalReasonTitle: TrArgs; + modalReasonDescription: TrArgs; +}; + +type SettingsToggleBasicProps = { text: TrArgs; subText: TrArgs; baseDataTestId: SettingsToggles; active: boolean; + unavailableProps?: UnavailableProps; onClick: () => Promise; -}) { +}; + +export function SettingsToggleBasic({ + text, + subText, + baseDataTestId, + active, + onClick, + unavailableProps, +}: SettingsToggleBasicProps) { + const dispatch = useDispatch(); + + const handleClick = useCallback(async () => { + if (!unavailableProps?.unavailable) { + return onClick(); + } + return showLocalizedPopupDialog( + { + title: unavailableProps?.modalReasonTitle, + description: unavailableProps?.modalReasonDescription, + hideOkayButton: true, + }, + dispatch + ); + }, [unavailableProps, dispatch, onClick]); + return ( } active={active} - onClick={onClick} + onClick={handleClick} toggleDataTestId={`${baseDataTestId}-settings-toggle`} rowDataTestId={`${baseDataTestId}-settings-row`} /> diff --git a/ts/components/dialog/user-settings/pages/PreferencesSettingsPage.tsx b/ts/components/dialog/user-settings/pages/PreferencesSettingsPage.tsx index 6571f4e4c..10e5c6333 100644 --- a/ts/components/dialog/user-settings/pages/PreferencesSettingsPage.tsx +++ b/ts/components/dialog/user-settings/pages/PreferencesSettingsPage.tsx @@ -22,13 +22,12 @@ import { SettingsKey } from '../../../../data/settings-key'; import { ToastUtils } from '../../../../session/utils'; import { PanelRadioButton } from '../../../buttons/panel/PanelRadioButton'; import { useHasEnterSendEnabled } from '../../../../state/selectors/settings'; +import { isLinux } from '../../../../OS'; async function toggleStartInTray() { try { const newValue = !(await window.getStartInTray()); - // make sure to write it here too, as this is the value used on the UI to mark the toggle as true/false - await window.setSettingValue(SettingsKey.settingsStartInTray, newValue); await window.setStartInTray(newValue); if (!newValue) { ToastUtils.pushRestartNeeded(); @@ -38,6 +37,16 @@ async function toggleStartInTray() { } } +async function toggleAutoStart() { + try { + const newValue = !(await window.getAutoStartEnabled()); + + await window.setAutoStartEnabled(newValue); + } catch (e) { + window.log.warn('auto start change error:', e); + } +} + function SendWithShiftEnter() { const initialSetting = useHasEnterSendEnabled(); const selectedWithSettingTrue = 'enterForNewLine'; @@ -100,6 +109,11 @@ export function PreferencesSettingsPage(modalState: UserSettingsModalState) { const closeAction = useUserSettingsCloseAction(modalState); const title = useUserSettingsTitle(modalState); const isStartInTrayActive = Boolean(window.getSettingValue(SettingsKey.settingsStartInTray)); + + const platformIsLinux = isLinux(); + const isAutoStartActive = platformIsLinux + ? false + : Boolean(window.getSettingValue(SettingsKey.settingsAutoStart)); const forceUpdate = useUpdate(); return ( @@ -144,6 +158,24 @@ export function PreferencesSettingsPage(modalState: UserSettingsModalState) { active={isStartInTrayActive} /> + + + { + await toggleAutoStart(); + forceUpdate(); + }} + active={isAutoStartActive} + unavailableProps={{ + unavailable: platformIsLinux, + modalReasonTitle: { token: 'settingsCannotChangeDesktop' }, + modalReasonDescription: { token: 'launchOnStartupDisabledDesktop' }, + }} + /> + ); diff --git a/ts/data/settings-key.ts b/ts/data/settings-key.ts index 0f1656d96..dbaa4234c 100644 --- a/ts/data/settings-key.ts +++ b/ts/data/settings-key.ts @@ -7,6 +7,7 @@ const settingsSpellCheck = 'spell-check'; const settingsLinkPreview = 'link-preview-setting'; const hasBlindedMsgRequestsEnabled = 'hasBlindedMsgRequestsEnabled'; const settingsStartInTray = 'start-in-tray-setting'; +const settingsAutoStart = 'auto-start-setting'; const settingsOpengroupPruning = 'prune-setting'; const settingsNotification = 'notification-setting'; const settingsAudioNotification = 'audio-notification-setting'; @@ -31,6 +32,7 @@ export const SettingsKey = { settingsSpellCheck, settingsLinkPreview, settingsStartInTray, + settingsAutoStart, settingsOpengroupPruning, hasBlindedMsgRequestsEnabled, settingsNotification, diff --git a/ts/mains/main_node.ts b/ts/mains/main_node.ts index df0ee1ab9..384036f3e 100644 --- a/ts/mains/main_node.ts +++ b/ts/mains/main_node.ts @@ -177,6 +177,7 @@ import { initializeMainProcessLogger } from '../util/logger/main_process_logging import * as log from '../util/logger/log'; import { DURATION } from '../session/constants'; import { tr } from '../localization/localeTools'; +import { isMacOS, isWindows } from '../OS'; function prepareURL(pathSegments: Array, moreKeys?: { theme: any }) { const urlObject: url.UrlObject = { @@ -1023,6 +1024,46 @@ ipc.on('get-start-in-tray', event => { } }); +ipc.on('set-auto-start-enabled', (event, newValue) => { + try { + // Set the login item settings based on the platform + if (isMacOS()) { + // macOS + app.setLoginItemSettings({ + openAtLogin: newValue, + openAsHidden: false, + }); + } else if (isWindows()) { + // Windows - For Squirrel-based apps, we need to handle the stub launcher - https://www.electronjs.org/docs/latest/api/app/#appsetloginitemsettingssettings-macos-windows + const appFolder = path.dirname(process.execPath); + const ourExeName = path.basename(process.execPath); + const stubLauncher = path.resolve(appFolder, '..', ourExeName); + + app.setLoginItemSettings({ + openAtLogin: newValue, + path: stubLauncher, + args: [], + }); + } else { + throw new Error(`Unsupported platform for auto start: ${process.platform}`); + } + + event.sender.send('set-auto-start-enabled-response', null); + } catch (e) { + event.sender.send('set-auto-start-enabled-response', e); + } +}); + +ipc.on('get-auto-start-enabled', event => { + try { + const loginSettings = app.getLoginItemSettings(); + const isEnabled = loginSettings.openAtLogin; + event.sender.send('get-auto-start-enabled-response', isEnabled); + } catch (e) { + event.sender.send('get-auto-start-enabled-response', false); + } +}); + ipcMain.on('update-badge-count', (_event, count) => { if (app.isReady()) { app.setBadgeCount(isNumber(count) && isFinite(count) && count >= 0 ? count : 0); diff --git a/ts/react.d.ts b/ts/react.d.ts index 9559888a6..65992f18f 100644 --- a/ts/react.d.ts +++ b/ts/react.d.ts @@ -95,6 +95,7 @@ declare module 'react' { | 'conversation-trimming' | 'auto-update' | 'auto-dark-mode' + | 'auto-start' | 'hide-menu-bar'; type SettingsRadio = diff --git a/ts/state/ducks/modalDialog.tsx b/ts/state/ducks/modalDialog.tsx index 92e33f1d1..a64103693 100644 --- a/ts/state/ducks/modalDialog.tsx +++ b/ts/state/ducks/modalDialog.tsx @@ -55,6 +55,7 @@ export type OpenUrlModalState = { urlToOpen: string } | null; export type LocalizedPopupDialogState = { title: TrArgs; description: TrArgs; + hideOkayButton?: boolean; } | null; export type SessionProInfoState = { variant: SessionProInfoVariant } | null; diff --git a/ts/window.d.ts b/ts/window.d.ts index 883aeb817..0f7264ea9 100644 --- a/ts/window.d.ts +++ b/ts/window.d.ts @@ -74,6 +74,8 @@ declare global { }) => Promise; setStartInTray: (val: boolean) => Promise; getStartInTray: () => Promise; + setAutoStartEnabled: (val: boolean) => Promise; + getAutoStartEnabled: () => Promise; getOpengroupPruning: () => Promise; setOpengroupPruning: (val: boolean) => Promise; closeAbout: () => void;