Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ a single server mode.
| `"isFlashFrameEnabled": true` | Sets if the flash frame will be enabled. |
| `"isMinimizeOnCloseEnabled": false` | Sets if the app will be minimized on close. |
| `"doCheckForUpdatesOnStartup": true` | Sets if the app will check for updates on startup. |
| `"isMenuBarEnabled": true` | Sets if the menu bar will be enabled. |
| `"isMenuBarEnabled": false` | Windows/Linux: keep the menu bar always visible. When false, Alt shows it temporarily. Unused on macOS. |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
| `"isTrayIconEnabled": true` | Enables Tray Icon, the app will be hidden to the tray on close. Overrides `"isMinimizeOnCloseEnabled"` |
| `"isUpdatingEnabled": true` | Sets if the app can be updated by the user. |
| `"isAddNewServersEnabled": true` | Sets if the user can add new servers. |
Expand Down
2 changes: 1 addition & 1 deletion docs/silent-installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ Same locations as `servers.json` (see table above).
| `isFlashFrameEnabled` | boolean | `false` | Flash taskbar on new messages |
| `isMinimizeOnCloseEnabled` | boolean | `false` | Minimize to taskbar instead of closing |
| `doCheckForUpdatesOnStartup` | boolean | `true` | Check for updates on startup |
| `isMenuBarEnabled` | boolean | `true` | Show menu bar |
| `isMenuBarEnabled` | boolean | `false` (Windows/Linux; unused on macOS) | Keep the menu bar always visible. When `false`, press Alt to show it temporarily. |
| `isTrayIconEnabled` | boolean | `true` | Enable system tray icon |
| `isUpdatingEnabled` | boolean | `true` | Allow user to update the app |
| `isAddNewServersEnabled` | boolean | `true` | Allow adding new servers |
Expand Down
32 changes: 30 additions & 2 deletions src/app/PersistableValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,23 @@ type PersistableValues_4_16_1 = PersistableValues_4_16_0 & {
isDownloadsPercentageEnabled: boolean;
};

type PersistableValues_4_16_2 = PersistableValues_4_16_1 & {
/**
* Bumped when the Windows/Linux menu-bar default changes so existing
* installs pick up the new auto-hide default once without clobbering a
* later user choice. macOS ignores this field.
*/
menuBarDefaultRevision?: number;
};

export type PersistableValues = Pick<
PersistableValues_4_16_1,
keyof PersistableValues_4_16_1
PersistableValues_4_16_2,
keyof PersistableValues_4_16_2
>;

/** Current menu-bar default policy for Windows/Linux (auto-hide, Alt reveals). */
export const MENU_BAR_DEFAULT_REVISION = 1;

export const migrations = {
'>=3.1.0': (before: PersistableValues_0_0_0): PersistableValues_3_1_0 => {
const { currentServerUrl, ...rest } = before;
Expand Down Expand Up @@ -250,4 +262,20 @@ export const migrations = {
...before,
isDownloadsPercentageEnabled: false,
}),
'>=4.16.2': (before: PersistableValues_4_16_1): PersistableValues_4_16_2 => {
// electron-store only runs this when projectVersion satisfies >=4.16.2.
// mergePersistableValues also applies the same revision for builds that
// ship the policy before that version bump (see MENU_BAR_DEFAULT_REVISION).
if (process.platform === 'darwin') {
return {
...before,
menuBarDefaultRevision: MENU_BAR_DEFAULT_REVISION,
};
}
return {
...before,
isMenuBarEnabled: false,
menuBarDefaultRevision: MENU_BAR_DEFAULT_REVISION,
};
},
};
54 changes: 54 additions & 0 deletions src/app/__tests__/PersistableValues.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,58 @@ describe('PersistableValues migrations', () => {
expect.objectContaining({ navigationLayout: 'tabs' })
);
});

describe('4.16.2 menu bar default (auto-hide on Windows/Linux)', () => {
const originalPlatform = process.platform;

afterEach(() => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
configurable: true,
});
});

it('forces isMenuBarEnabled off on linux and stamps the revision', () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
configurable: true,
});
const before = {
isMenuBarEnabled: true,
isDownloadsPercentageEnabled: false,
} as unknown as Parameters<(typeof migrations)['>=4.16.2']>[0];

const after = migrations['>=4.16.2'](before);
expect(after.isMenuBarEnabled).toBe(false);
expect(after.menuBarDefaultRevision).toBe(1);
});

it('forces isMenuBarEnabled off on win32 (was always hidden while stored true)', () => {
Object.defineProperty(process, 'platform', {
value: 'win32',
configurable: true,
});
const before = {
isMenuBarEnabled: true,
isDownloadsPercentageEnabled: false,
} as unknown as Parameters<(typeof migrations)['>=4.16.2']>[0];

expect(migrations['>=4.16.2'](before).isMenuBarEnabled).toBe(false);
});

it('preserves isMenuBarEnabled on darwin and still stamps the revision', () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
configurable: true,
});
const before = {
isMenuBarEnabled: true,
isDownloadsPercentageEnabled: false,
} as unknown as Parameters<(typeof migrations)['>=4.16.2']>[0];

const after = migrations['>=4.16.2'](before);
expect(after.isMenuBarEnabled).toBe(true);
expect(after.menuBarDefaultRevision).toBe(1);
});
});
});
48 changes: 30 additions & 18 deletions src/app/main/data.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import * as store from '../../store';
import { APP_SETTINGS_LOADED } from '../actions';
import { MENU_BAR_DEFAULT_REVISION } from '../PersistableValues';

Check failure on line 3 in src/app/main/data.spec.ts

View workflow job for this annotation

GitHub Actions / check (macos-latest)

`../PersistableValues` import should occur before import of `../actions`

Check failure on line 3 in src/app/main/data.spec.ts

View workflow job for this annotation

GitHub Actions / check (windows-latest)

`../PersistableValues` import should occur before import of `../actions`

Check failure on line 3 in src/app/main/data.spec.ts

View workflow job for this annotation

GitHub Actions / check (ubuntu-latest)

`../PersistableValues` import should occur before import of `../actions`
import { mergePersistableValues } from './data';
import { getPersistedValues } from './persistence';
import {
getPersistedMeta,
getPersistedValues,
setPersistedMeta,
} from './persistence';

jest.mock('electron', () => ({
app: {
Expand All @@ -22,6 +27,9 @@
jest.mock('./persistence', () => ({
getPersistedValues: jest.fn().mockReturnValue({}),
persistValues: jest.fn(),
// Literal 1 matches MENU_BAR_DEFAULT_REVISION (jest.mock factories are hoisted).
getPersistedMeta: jest.fn().mockReturnValue(1),
setPersistedMeta: jest.fn(),
}));

jest.mock('fs', () => ({
Expand Down Expand Up @@ -50,6 +58,7 @@
(store.dispatch as jest.Mock).mockImplementation(mockDispatch);
(store.select as jest.Mock).mockImplementation(mockSelect);
(getPersistedValues as jest.Mock).mockReturnValue({});
(getPersistedMeta as jest.Mock).mockReturnValue(MENU_BAR_DEFAULT_REVISION);
});

describe('mergePersistableValues', () => {
Expand All @@ -72,7 +81,7 @@
mockSelect.mockReturnValue(mockInitialValues);
});

describe('menubar recovery mechanism', () => {
describe('menu bar settings (no layout coupling)', () => {
const originalPlatform = process.platform;

afterEach(() => {
Expand All @@ -83,7 +92,7 @@
});
});

it('should enable menubar on Linux when in tabs layout with menubar disabled', async () => {
it('preserves isMenuBarEnabled=false on Linux with tabs layout (no recovery force-on)', async () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
writable: true,
Expand All @@ -102,14 +111,14 @@
expect(mockDispatch).toHaveBeenCalledWith({
type: APP_SETTINGS_LOADED,
payload: expect.objectContaining({
isMenuBarEnabled: true,
isMenuBarEnabled: false,
}),
});
});

it('should not modify settings on Linux when in sidebar layout with menubar disabled', async () => {
it('preserves isMenuBarEnabled=false on Windows with tabs layout', async () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
value: 'win32',
writable: true,
configurable: true,
});
Expand All @@ -118,7 +127,7 @@
mockSelect.mockReturnValue({
...mockInitialValues,
isMenuBarEnabled: false,
navigationLayout: 'sidebar',
navigationLayout: 'tabs',
});

await mergePersistableValues(localStorage);
Expand All @@ -131,52 +140,55 @@
});
});

it('should not modify settings on non-Linux platforms when in tabs layout with menubar disabled', async () => {
it('one-shot: forces isMenuBarEnabled off on Linux when menuBarDefaultRevision is stale', async () => {
Object.defineProperty(process, 'platform', {
value: 'darwin',
value: 'linux',
writable: true,
configurable: true,
});
const localStorage = {};

(getPersistedMeta as jest.Mock).mockReturnValue(0);
mockSelect.mockReturnValue({
...mockInitialValues,
isMenuBarEnabled: false,
isMenuBarEnabled: true,
navigationLayout: 'tabs',
});

await mergePersistableValues(localStorage);
await mergePersistableValues({});

expect(mockDispatch).toHaveBeenCalledWith({
type: APP_SETTINGS_LOADED,
payload: expect.objectContaining({
isMenuBarEnabled: false,
}),
});
expect(setPersistedMeta).toHaveBeenCalledWith(
'menuBarDefaultRevision',
MENU_BAR_DEFAULT_REVISION
);
});

it('should not modify settings when menubar is already enabled', async () => {
it('one-shot: does not re-force isMenuBarEnabled after revision is applied', async () => {
Object.defineProperty(process, 'platform', {
value: 'linux',
writable: true,
configurable: true,
});
const localStorage = {};

(getPersistedMeta as jest.Mock).mockReturnValue(MENU_BAR_DEFAULT_REVISION);

Check failure on line 176 in src/app/main/data.spec.ts

View workflow job for this annotation

GitHub Actions / check (macos-latest)

Replace `MENU_BAR_DEFAULT_REVISION` with `⏎········MENU_BAR_DEFAULT_REVISION⏎······`

Check failure on line 176 in src/app/main/data.spec.ts

View workflow job for this annotation

GitHub Actions / check (windows-latest)

Replace `MENU_BAR_DEFAULT_REVISION` with `⏎········MENU_BAR_DEFAULT_REVISION⏎······`

Check failure on line 176 in src/app/main/data.spec.ts

View workflow job for this annotation

GitHub Actions / check (ubuntu-latest)

Replace `MENU_BAR_DEFAULT_REVISION` with `⏎········MENU_BAR_DEFAULT_REVISION⏎······`
mockSelect.mockReturnValue({
...mockInitialValues,
isMenuBarEnabled: true,
navigationLayout: 'sidebar',
navigationLayout: 'tabs',
});

await mergePersistableValues(localStorage);
await mergePersistableValues({});

expect(mockDispatch).toHaveBeenCalledWith({
type: APP_SETTINGS_LOADED,
payload: expect.objectContaining({
isMenuBarEnabled: true,
}),
});
expect(setPersistedMeta).not.toHaveBeenCalled();
});
});

Expand Down
33 changes: 23 additions & 10 deletions src/app/main/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,14 @@
import { getSystemCertificateStatus } from '../../systemCertificates';
import { normalizeNumber } from '../../ui/main/rootWindow';
import { APP_SETTINGS_LOADED } from '../actions';
import { MENU_BAR_DEFAULT_REVISION } from '../PersistableValues';

Check failure on line 11 in src/app/main/data.ts

View workflow job for this annotation

GitHub Actions / check (macos-latest)

`../PersistableValues` import should occur before import of `../actions`

Check failure on line 11 in src/app/main/data.ts

View workflow job for this annotation

GitHub Actions / check (windows-latest)

`../PersistableValues` import should occur before import of `../actions`

Check failure on line 11 in src/app/main/data.ts

View workflow job for this annotation

GitHub Actions / check (ubuntu-latest)

`../PersistableValues` import should occur before import of `../actions`
import { selectPersistableValues } from '../selectors';
import { getPersistedValues, persistValues } from './persistence';
import {
getPersistedMeta,
getPersistedValues,
persistValues,
setPersistedMeta,
} from './persistence';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

const loadUserDataOverriddenSettings = async (): Promise<
Record<string, string>
Expand Down Expand Up @@ -164,15 +170,22 @@
},
};

if (
values.navigationLayout !== 'sidebar' &&
!values.isMenuBarEnabled &&
process.platform === 'linux'
) {
values = {
...values,
isMenuBarEnabled: true,
};
// One-shot policy: Windows used to force-hide the bar while storing
// isMenuBarEnabled=true; Linux defaulted to always-on. Both now auto-hide
// (Alt reveals) unless the user pins the bar. The revision is stored as
// config meta so later persistValues() snapshots cannot re-apply the force.
const appliedMenuBarRevision = getPersistedMeta(

Check failure on line 177 in src/app/main/data.ts

View workflow job for this annotation

GitHub Actions / check (macos-latest)

Replace `⏎····'menuBarDefaultRevision',⏎····0⏎··` with `'menuBarDefaultRevision',·0`

Check failure on line 177 in src/app/main/data.ts

View workflow job for this annotation

GitHub Actions / check (windows-latest)

Replace `⏎····'menuBarDefaultRevision',⏎····0⏎··` with `'menuBarDefaultRevision',·0`

Check failure on line 177 in src/app/main/data.ts

View workflow job for this annotation

GitHub Actions / check (ubuntu-latest)

Replace `⏎····'menuBarDefaultRevision',⏎····0⏎··` with `'menuBarDefaultRevision',·0`
'menuBarDefaultRevision',
0
);
if (appliedMenuBarRevision < MENU_BAR_DEFAULT_REVISION) {
if (process.platform !== 'darwin') {
values = {
...values,
isMenuBarEnabled: false,
};
}
setPersistedMeta('menuBarDefaultRevision', MENU_BAR_DEFAULT_REVISION);
}

const mergedOverrides: Record<string, unknown> = {
Expand Down
15 changes: 15 additions & 0 deletions src/app/main/persistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,21 @@ const getElectronStore = (): ElectronStore<PersistableValues> => {
export const getPersistedValues = (): PersistableValues =>
getElectronStore().store;

/** Read a store key that is not necessarily mirrored in Redux. */
export const getPersistedMeta = <T>(key: string, fallback: T): T => {
const value = getElectronStore().get(key as keyof PersistableValues);
return (value as T | undefined) ?? fallback;
};

/** Write a store key without replacing the rest of the config. */
export const setPersistedMeta = (key: string, value: unknown): void => {
try {
getElectronStore().set(key as keyof PersistableValues, value as never);
} catch (error) {
error instanceof Error && console.error(error);
}
};

let lastSavedTime = 0;

export const persistValues = (values: PersistableValues): void => {
Expand Down
6 changes: 2 additions & 4 deletions src/i18n/en.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,8 +295,7 @@
},
"menubar": {
"title": "Menu bar",
"description": "Show menu bar on the top of the window.",
"disabledHint": "Cannot disable menu bar when the workspace bar is disabled. App settings would become inaccessible."
"description": "Keep the menu bar always visible at the top of the window. When off, press Alt to show it temporarily."
},
"sidebar": {
"title": "Workspace bar",
Expand All @@ -308,8 +307,7 @@
"description": "Choose how to switch between workspaces",
"workspaceTabs": "Tabs",
"workspaceBar": "Sidebar",
"workspaceHidden": "Titlebar",
"disabledHint": "Enable the menu bar first to use Workspace tabs."
"workspaceHidden": "Titlebar"
},
"trayIcon": {
"title": "Tray icon",
Expand Down
2 changes: 1 addition & 1 deletion src/ui/components/SettingsView/GeneralTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export const GeneralTab = () => {
{isDarwin && <TransparentWindow />}
<TrayIcon />
{isWin32 && <MinimizeOnClose />}
{!isDarwin && !isWin32 && <MenuBar />}
{!isDarwin && <MenuBar />}
<FlashFrame />
<DownloadsPercentage />
</FieldGroup>
Expand Down
11 changes: 1 addition & 10 deletions src/ui/components/SettingsView/features/MenuBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@ export const MenuBar = (props: MenuBarProps) => {
const isMenuBarEnabled = useSelector(
({ isMenuBarEnabled }: RootState) => isMenuBarEnabled
);
const navigationLayout = useSelector(
({ navigationLayout }: RootState) => navigationLayout
);
const dispatch = useDispatch<Dispatch<RootAction>>();
const { t } = useTranslation();
const handleChange = useCallback(
Expand All @@ -34,20 +31,14 @@ export const MenuBar = (props: MenuBarProps) => {
);

const isMenuBarEnabledId = useId();
const canToggle = !isMenuBarEnabled || navigationLayout === 'sidebar';

return (
<ToggleField
id={isMenuBarEnabledId}
label={t('settings.options.menubar.title')}
description={
navigationLayout !== 'sidebar' && isMenuBarEnabled
? t('settings.options.menubar.disabledHint')
: t('settings.options.menubar.description')
}
description={t('settings.options.menubar.description')}
checked={isMenuBarEnabled}
onChange={handleChange}
disabled={!canToggle}
className={props.className}
/>
);
Expand Down
Loading
Loading