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
6 changes: 6 additions & 0 deletions apps/webapp/src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -1688,6 +1688,12 @@
"preferencesOptionsEmojiReplaceDetail": ":-) → [icon]",
"preferencesOptionsEnableAgcCheckbox": "Automatic gain control (AGC)",
"preferencesOptionsEnableAgcDetails": "Enable to allow your microphone volume to be adjusted automatically to ensure all participants in a call are heard with similar and comfortable loudness.",
"preferencesOptionsEnableHardwareAcceleration": "Hardware acceleration",
"preferencesOptionsEnableHardwareAccelerationDetails": "This improves stability and video performance while reducing CPU usage. If you change the setting, you need to restart the app.",
"preferencesOptionsEnableHardwareAccelerationModalCancel": "Cancel",
"preferencesOptionsEnableHardwareAccelerationModalMessage": "To update the hardware acceleration setting, you need to restart the app.",
"preferencesOptionsEnableHardwareAccelerationModalOk": "Restart App",
"preferencesOptionsEnableHardwareAccelerationModalTitle": "Restart app",
"preferencesOptionsEnablePressSpaceToUnmute": "Unmute with space bar",
"preferencesOptionsEnablePressSpaceToUnmuteDetails": "Enable to unmute your microphone by pressing and holding the space bar as long as you want to speak. You can use this option in full view.",
"preferencesOptionsEnableSoundlessIncomingCalls": "Silence other calls",
Expand Down
7 changes: 7 additions & 0 deletions apps/webapp/src/script/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,13 @@ const Config = {

return window.desktopAppConfig;
},
getDesktopSettings: () => {
if (!Runtime.isDesktopApp()) {
return undefined;
}

return window.desktopAppSettings;
},
};

export {Config};
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,11 @@ import {ChangeEvent, useCallback, useEffect, useRef, useState} from 'react';
import type {WebappProperties} from '@wireapp/api-client/lib/user/data/';
import {amplify} from 'amplify';

import {Runtime} from '@wireapp/commons';
import {Checkbox, CheckboxLabel} from '@wireapp/react-ui-kit';
import {WebAppEvents} from '@wireapp/webapp-events';

import {PrimaryModal} from 'Components/Modals/PrimaryModal';
import type {MediaConstraintsHandler} from 'Repositories/media/MediaConstraintsHandler';
import type {PropertiesRepository} from 'Repositories/properties/PropertiesRepository';
import {PROPERTIES_TYPE} from 'Repositories/properties/PropertiesType';
Expand Down Expand Up @@ -54,6 +56,20 @@ const CallOptions = ({constraintsHandler, propertiesRepository}: CallOptionsProp
!!propertiesRepository.properties.settings.call.enable_press_space_to_unmute,
);

const hardwareAccelerationDescriptionId = 'status-preference-hardware-acceleration-description';

const desktopSettings = Config.getDesktopSettings();

const isHardwareAccelerationChangeable = Runtime.isDesktopApp() && !!desktopSettings;

const [isHardwareAccelerationEnabled, setIsHardwareAccelerationEnabled] = useState<boolean>(() => {
if (!isHardwareAccelerationChangeable) {
return true; // default in browser (but not changeable)
}

return desktopSettings.isHardwareAccelerationEnabled();
});

useEffect(() => {
const updateProperties = ({settings}: WebappProperties) => {
setVbrEncoding(!isCbrEncodingEnforced && settings.call.enable_vbr_encoding);
Expand Down Expand Up @@ -102,6 +118,33 @@ const CallOptions = ({constraintsHandler, propertiesRepository}: CallOptionsProp
[propertiesRepository],
);

const showHardwareAccelerationRestartModal = () => {
PrimaryModal.show(PrimaryModal.type.CONFIRM, {
size: 'large',
primaryAction: {
action: confirmHardwareAccelerationChange,
text: t('preferencesOptionsEnableHardwareAccelerationModalOk'),
},
text: {
message: t('preferencesOptionsEnableHardwareAccelerationModalMessage'),
title: t('preferencesOptionsEnableHardwareAccelerationModalTitle'),
},
});
};

const confirmHardwareAccelerationChange = () => {
if (!desktopSettings) {
return;
}

const currentHwValue = desktopSettings.isHardwareAccelerationEnabled();

desktopSettings.setHardwareAccelerationEnabled(!currentHwValue);
setIsHardwareAccelerationEnabled(!currentHwValue);

amplify.publish(WebAppEvents.LIFECYCLE.RESTART);
};

return (
<PreferencesSection title={t('preferencesOptionsCall')}>
<div>
Expand Down Expand Up @@ -153,6 +196,25 @@ const CallOptions = ({constraintsHandler, propertiesRepository}: CallOptionsProp
</p>
</div>
)}

{isHardwareAccelerationChangeable && (
<div className="checkbox-margin">
<Checkbox
onChange={showHardwareAccelerationRestartModal}
checked={isHardwareAccelerationEnabled}
id="status-preference-hardware-acceleration"
data-uie-name="status-preference-hardware-acceleration"
aria-describedby={hardwareAccelerationDescriptionId}
>
<CheckboxLabel htmlFor="status-preference-hardware-acceleration">
{t('preferencesOptionsEnableHardwareAcceleration')}
</CheckboxLabel>
</Checkbox>
<p id={hardwareAccelerationDescriptionId} className="preferences-detail preferences-detail-intended">
{t('preferencesOptionsEnableHardwareAccelerationDetails')}
</p>
</div>
)}
</PreferencesSection>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
/*
* Wire
* Copyright (C) 2026 Wire Swiss GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import {act, fireEvent, render} from '@testing-library/react';
import {amplify} from 'amplify';

import {Runtime} from '@wireapp/commons';
import {WebAppEvents} from '@wireapp/webapp-events';

import {PrimaryModal} from 'Components/Modals/PrimaryModal';
import type {MediaConstraintsHandler} from 'Repositories/media/MediaConstraintsHandler';
import type {PropertiesRepository} from 'Repositories/properties/PropertiesRepository';
import {withTheme} from 'src/script/auth/util/test/TestUtil';

import {CallOptions} from './CallOptions';

import {Config} from '../../../../../Config';

jest.mock('Util/localizerUtil', () => ({
t: (key: string) => key,
}));

type DesktopSettingsMock = {
isHardwareAccelerationEnabled: jest.Mock;
setHardwareAccelerationEnabled: jest.Mock;
};

type PrimaryModalOptions = Parameters<typeof PrimaryModal.show>[1];

const mockDesktopSettings = (hwEnabled: boolean): DesktopSettingsMock => ({
isHardwareAccelerationEnabled: jest.fn().mockReturnValue(hwEnabled),
setHardwareAccelerationEnabled: jest.fn(),
});

const getDefaultProps = () => {
const constraintsHandler = {
getAgcPreference: jest.fn().mockReturnValue(false),
setAgcPreference: jest.fn(),
} as unknown as MediaConstraintsHandler;

const propertiesRepository = {
properties: {
settings: {
call: {
enable_vbr_encoding: false,
enable_soundless_incoming_calls: false,
enable_press_space_to_unmute: false,
},
},
} as PropertiesRepository['properties'],
savePreference: jest.fn(),
} as unknown as PropertiesRepository;

return {
constraintsHandler,
propertiesRepository,
};
};

const setupDesktop = (hwEnabled: boolean): DesktopSettingsMock => {
const desktopSettings = mockDesktopSettings(hwEnabled);
jest.spyOn(Runtime, 'isDesktopApp').mockReturnValue(true);
jest
.spyOn(Config, 'getDesktopSettings')
.mockReturnValue(desktopSettings as ReturnType<typeof Config.getDesktopSettings>);
return desktopSettings;
};

beforeEach(() => {
jest.restoreAllMocks();

jest.spyOn(Config, 'getConfig').mockReturnValue({
FEATURE: {
ENFORCE_CONSTANT_BITRATE: false,
ENABLE_PRESS_SPACE_TO_UNMUTE: false,
},
} as ReturnType<typeof Config.getConfig>);
});

describe('CallOptions — hardware acceleration checkbox', () => {
it('is not shown in a browser (non-desktop)', () => {
jest.spyOn(Runtime, 'isDesktopApp').mockReturnValue(false);
jest.spyOn(Config, 'getDesktopSettings').mockReturnValue(undefined);

const {queryByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));

expect(queryByTestId('status-preference-hardware-acceleration')).toBeNull();
});

it('is not shown when desktopSettings is null even on desktop', () => {
jest.spyOn(Runtime, 'isDesktopApp').mockReturnValue(true);
jest.spyOn(Config, 'getDesktopSettings').mockReturnValue(undefined);

const {queryByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));

expect(queryByTestId('status-preference-hardware-acceleration')).toBeNull();
});

it('is shown and checked when hardware acceleration is enabled', () => {
setupDesktop(true);

const {getByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));
const checkbox = getByTestId('status-preference-hardware-acceleration') as HTMLInputElement;

expect(checkbox.checked).toBe(true);
});

it('is shown and unchecked when hardware acceleration is disabled', () => {
setupDesktop(false);

const {getByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));
const checkbox = getByTestId('status-preference-hardware-acceleration') as HTMLInputElement;

expect(checkbox.checked).toBe(false);
});

it('opens a confirmation modal when the checkbox is clicked without immediately applying the change', () => {
const desktopSettings = setupDesktop(true);
const showModalSpy = jest.spyOn(PrimaryModal, 'show').mockImplementation(() => {});

const {getByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));
fireEvent.click(getByTestId('status-preference-hardware-acceleration'));

expect(showModalSpy).toHaveBeenCalledTimes(1);
expect(showModalSpy).toHaveBeenCalledWith(
PrimaryModal.type.CONFIRM,
expect.objectContaining({
primaryAction: expect.objectContaining({action: expect.any(Function)}),
}),
);
expect(desktopSettings.setHardwareAccelerationEnabled).not.toHaveBeenCalled();
});

it('does not publish a restart event when the modal is merely opened', () => {
setupDesktop(true);
jest.spyOn(PrimaryModal, 'show').mockImplementation(() => {});
const publishSpy = jest.spyOn(amplify, 'publish');

const {getByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));
fireEvent.click(getByTestId('status-preference-hardware-acceleration'));

expect(publishSpy).not.toHaveBeenCalledWith(WebAppEvents.LIFECYCLE.RESTART);
});

it('disables hardware acceleration and triggers restart when confirmed', () => {
const desktopSettings = setupDesktop(true);
let capturedAction: (() => void) | undefined;

jest.spyOn(PrimaryModal, 'show').mockImplementation((_type, options: PrimaryModalOptions) => {
const primaryAction = options?.primaryAction;
if (primaryAction && 'action' in primaryAction && typeof primaryAction.action === 'function') {
capturedAction = primaryAction.action as () => void;
}
});

const publishSpy = jest.spyOn(amplify, 'publish');

const {getByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));
fireEvent.click(getByTestId('status-preference-hardware-acceleration'));

expect(capturedAction).toBeDefined();
act(() => capturedAction?.());

expect(desktopSettings.setHardwareAccelerationEnabled).toHaveBeenCalledWith(false);
expect(publishSpy).toHaveBeenCalledWith(WebAppEvents.LIFECYCLE.RESTART);

const checkbox = getByTestId('status-preference-hardware-acceleration') as HTMLInputElement;
expect(checkbox.checked).toBe(false);
});

it('enables hardware acceleration and triggers restart when confirmed', () => {
const desktopSettings = setupDesktop(false);
let capturedAction: (() => void) | undefined;

jest.spyOn(PrimaryModal, 'show').mockImplementation((_type, options: PrimaryModalOptions) => {
const primaryAction = options?.primaryAction;
if (primaryAction && 'action' in primaryAction && typeof primaryAction.action === 'function') {
capturedAction = primaryAction.action as () => void;
}
});

const publishSpy = jest.spyOn(amplify, 'publish');

const {getByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));
fireEvent.click(getByTestId('status-preference-hardware-acceleration'));

expect(capturedAction).toBeDefined();
act(() => capturedAction?.());

expect(desktopSettings.setHardwareAccelerationEnabled).toHaveBeenCalledWith(true);
expect(publishSpy).toHaveBeenCalledWith(WebAppEvents.LIFECYCLE.RESTART);

const checkbox = getByTestId('status-preference-hardware-acceleration') as HTMLInputElement;
expect(checkbox.checked).toBe(true);
});

it('makes no changes when the confirmation modal is dismissed', () => {
const desktopSettings = setupDesktop(true);
jest.spyOn(PrimaryModal, 'show').mockImplementation(() => {});
const publishSpy = jest.spyOn(amplify, 'publish');

const {getByTestId} = render(withTheme(<CallOptions {...getDefaultProps()} />));
fireEvent.click(getByTestId('status-preference-hardware-acceleration'));

expect(desktopSettings.setHardwareAccelerationEnabled).not.toHaveBeenCalled();
expect(publishSpy).not.toHaveBeenCalledWith(WebAppEvents.LIFECYCLE.RESTART);

const checkbox = getByTestId('status-preference-hardware-acceleration') as HTMLInputElement;
expect(checkbox.checked).toBe(true);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ declare global {
interface Window {
openGraphAsync?: (url: string) => Promise<OpenGraphResult>;
desktopAppConfig?: {version: string; supportsCallingPopoutWindow?: boolean};
desktopAppSettings?: {
setHardwareAccelerationEnabled: (enabled: boolean) => void;
isHardwareAccelerationEnabled: () => boolean;
};
}
}
const logger = getLogger('LinkPreviewRepository');
Expand Down
6 changes: 6 additions & 0 deletions apps/webapp/src/types/i18n.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1692,6 +1692,12 @@ declare module 'I18n/en-US.json' {
'preferencesOptionsEmojiReplaceDetail': `:-) → [icon]`;
'preferencesOptionsEnableAgcCheckbox': `Automatic gain control (AGC)`;
'preferencesOptionsEnableAgcDetails': `Enable to allow your microphone volume to be adjusted automatically to ensure all participants in a call are heard with similar and comfortable loudness.`;
'preferencesOptionsEnableHardwareAcceleration': `Hardware acceleration`;
'preferencesOptionsEnableHardwareAccelerationDetails': `This improves stability and video performance while reducing CPU usage. If you change the setting, you need to restart the app.`;
'preferencesOptionsEnableHardwareAccelerationModalCancel': `Cancel`;
'preferencesOptionsEnableHardwareAccelerationModalMessage': `To update the hardware acceleration setting, you need to restart the app.`;
'preferencesOptionsEnableHardwareAccelerationModalOk': `Restart App`;
'preferencesOptionsEnableHardwareAccelerationModalTitle': `Restart app`;
'preferencesOptionsEnablePressSpaceToUnmute': `Unmute with space bar`;
'preferencesOptionsEnablePressSpaceToUnmuteDetails': `Enable to unmute your microphone by pressing and holding the space bar as long as you want to speak. You can use this option in full view.`;
'preferencesOptionsEnableSoundlessIncomingCalls': `Silence other calls`;
Expand Down
Loading