Add content protection checkbox to Settings window

This commit is contained in:
Fedor Indutny
2025-05-12 15:16:19 -07:00
committed by GitHub
parent a2c74c3a8b
commit bc3b6a07bb
10 changed files with 149 additions and 3 deletions

View File

@@ -6936,6 +6936,30 @@
"messageformat": "Restart",
"description": "Button to restart Signal to apply language changes"
},
"icu:Preferences__Privacy__Application": {
"messageformat": "Application",
"description": "Title of Application section in Settings > Privacy"
},
"icu:Preferences__content-protection--label": {
"messageformat": "Screen security",
"description": "Label of checkbox in Settings > Privacy > Application"
},
"icu:Preferences__content-protection--description": {
"messageformat": "Prevent screenshots of Signal on this computer for added privacy.",
"description": "Description of checkbox in Settings > Privacy > Application"
},
"icu:Preferences__content-protection__modal--title": {
"messageformat": "Disable screen security?",
"description": "Title of confirm disabling screen security modal in Settings > Privacy > Application"
},
"icu:Preferences__content-protection__modal--body": {
"messageformat": "If disabled, this may allow Microsoft Windows to capture screenshots of Signal and use them for features that may not be private.",
"description": "Body of confirm disabling screen security modal in Settings > Privacy > Application"
},
"icu:Preferences__content-protection__modal--disable": {
"messageformat": "Disable",
"description": "Text of the button confirm disabling screen security modal in Settings > Privacy > Application"
},
"icu:DialogUpdate--version-available": {
"messageformat": "Update to version {version} available",
"description": "Tooltip for new update available"

View File

@@ -571,6 +571,15 @@ async function handleCommonWindowEvents(window: BrowserWindow) {
const focusInterval = setInterval(setWindowFocus, 10000);
window.on('closed', () => clearInterval(focusInterval));
const contentProtection = ephemeralConfig.get('contentProtection');
// Apply content protection by default on Windows, unless explicitly disabled
// by user in settings.
if (contentProtection ?? OS.isWindows()) {
window.once('ready-to-show', async () => {
window.setContentProtection(true);
});
}
await zoomFactorService.syncWindow(window);
nativeThemeNotifier.addWindow(window);
@@ -2132,6 +2141,7 @@ app.on('ready', async () => {
'ephemeral-setting-changed',
sendPreferencesChangedEventToWindows
);
settingsChannel.on('ephemeral-setting-changed', onEphemeralSettingChanged);
// We use this event only a single time to log the startup time of the app
// from when it's first ready until the loading screen disappears.
@@ -2902,6 +2912,20 @@ const sendPreferencesChangedEventToWindows = () => {
};
ipc.on('preferences-changed', sendPreferencesChangedEventToWindows);
const onEphemeralSettingChanged = (name: string) => {
if (name !== 'contentProtection') {
return;
}
const contentProtection = ephemeralConfig.get('contentProtection');
for (const window of activeWindows) {
if (typeof contentProtection === 'boolean') {
window.setContentProtection(contentProtection);
}
}
};
function maybeGetIncomingSignalRoute(argv: Array<string>) {
for (const arg of argv) {
const route = parseSignalRoute(arg);

View File

@@ -111,6 +111,7 @@ export default {
hasAutoLaunch: true,
hasCallNotifications: true,
hasCallRingtoneNotification: false,
hasContentProtection: false,
hasCountMutedConversations: false,
hasHideMenuBar: false,
hasIncomingCallNotifications: true,
@@ -136,6 +137,8 @@ export default {
isSyncSupported: true,
isSystemTraySupported: true,
isInternalUser: false,
isContentProtectionSupported: true,
isContentProtectionNeeded: true,
isMinimizeToAndStartInSystemTraySupported: true,
lastSyncTime: Date.now(),
localeOverride: null,
@@ -182,6 +185,7 @@ export default {
onCallRingtoneNotificationChange: action(
'onCallRingtoneNotificationChange'
),
onContentProtectionChange: action('onContentProtectionChange'),
onCountMutedConversationsChange: action('onCountMutedConversationsChange'),
onEmojiSkinToneDefaultChange: action('onEmojiSkinToneDefaultChange'),
onHasStoriesDisabledChanged: action('onHasStoriesDisabledChanged'),

View File

@@ -96,6 +96,7 @@ export type PropsDataType = {
hasAutoLaunch: boolean;
hasCallNotifications: boolean;
hasCallRingtoneNotification: boolean;
hasContentProtection: boolean;
hasCountMutedConversations: boolean;
hasHideMenuBar?: boolean;
hasIncomingCallNotifications: boolean;
@@ -145,6 +146,8 @@ export type PropsDataType = {
isSystemTraySupported: boolean;
isMinimizeToAndStartInSystemTraySupported: boolean;
isInternalUser: boolean;
isContentProtectionNeeded: boolean;
isContentProtectionSupported: boolean;
availableCameras: Array<
Pick<MediaDeviceInfo, 'deviceId' | 'groupId' | 'kind' | 'label'>
@@ -189,6 +192,7 @@ type PropsFunctionType = {
onAutoLaunchChange: CheckboxChangeHandlerType;
onCallNotificationsChange: CheckboxChangeHandlerType;
onCallRingtoneNotificationChange: CheckboxChangeHandlerType;
onContentProtectionChange: CheckboxChangeHandlerType;
onCountMutedConversationsChange: CheckboxChangeHandlerType;
onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void;
onHasStoriesDisabledChanged: SelectChangeHandlerType<boolean>;
@@ -297,6 +301,7 @@ export function Preferences({
hasAutoLaunch,
hasCallNotifications,
hasCallRingtoneNotification,
hasContentProtection,
hasCountMutedConversations,
hasHideMenuBar,
hasIncomingCallNotifications,
@@ -326,6 +331,8 @@ export function Preferences({
isSystemTraySupported,
isMinimizeToAndStartInSystemTraySupported,
isInternalUser,
isContentProtectionNeeded,
isContentProtectionSupported,
lastSyncTime,
makeSyncRequest,
notificationContent,
@@ -336,6 +343,7 @@ export function Preferences({
onAutoLaunchChange,
onCallNotificationsChange,
onCallRingtoneNotificationChange,
onContentProtectionChange,
onCountMutedConversationsChange,
onEmojiSkinToneDefaultChange,
onHasStoriesDisabledChanged,
@@ -392,6 +400,8 @@ export function Preferences({
const [confirmDelete, setConfirmDelete] = useState(false);
const [confirmStoriesOff, setConfirmStoriesOff] = useState(false);
const [confirmContentProtection, setConfirmContentProtection] =
useState(false);
const [page, setPage] = useState<Page>(initialPage);
const [showSyncFailed, setShowSyncFailed] = useState(false);
const [nowSyncing, setNowSyncing] = useState(false);
@@ -460,6 +470,17 @@ export function Preferences({
[onSelectedMicrophoneChange, availableMicrophones]
);
const handleContentProtectionChange = useCallback(
(value: boolean) => {
if (value === true || !isContentProtectionNeeded) {
onContentProtectionChange(value);
} else {
setConfirmContentProtection(true);
}
},
[onContentProtectionChange, isContentProtectionNeeded]
);
const settingsPaneRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
const settingsPane = settingsPaneRef.current;
@@ -1365,6 +1386,41 @@ export function Preferences({
}
/>
</SettingsRow>
{isContentProtectionSupported && (
<SettingsRow title={i18n('icu:Preferences__Privacy__Application')}>
<Checkbox
checked={hasContentProtection}
description={i18n(
'icu:Preferences__content-protection--description'
)}
label={i18n('icu:Preferences__content-protection--label')}
moduleClassName="Preferences__checkbox"
name="contentProtection"
onChange={handleContentProtectionChange}
/>
</SettingsRow>
)}
{confirmContentProtection ? (
<ConfirmationDialog
dialogName="Preference.confirmContentProtection"
actions={[
{
action: () => onContentProtectionChange(false),
style: 'negative',
text: i18n(
'icu:Preferences__content-protection__modal--disable'
),
},
]}
i18n={i18n}
onClose={() => {
setConfirmContentProtection(false);
}}
title={i18n('icu:Preferences__content-protection__modal--title')}
>
{i18n('icu:Preferences__content-protection__modal--body')}
</ConfirmationDialog>
) : null}
<SettingsRow title={i18n('icu:Stories__title')}>
<Control
left={

View File

@@ -21,6 +21,7 @@ const EPHEMERAL_NAME_MAP = new Map([
['systemTraySetting', 'system-tray-setting'],
['themeSetting', 'theme-setting'],
['localeOverride', 'localeOverride'],
['contentProtection', 'contentProtection'],
]);
type ResponseQueueEntry = Readonly<{
@@ -123,6 +124,7 @@ export class SettingsChannel extends EventEmitter {
this.#installEphemeralSetting('systemTraySetting');
this.#installEphemeralSetting('localeOverride');
this.#installEphemeralSetting('spellCheck');
this.#installEphemeralSetting('contentProtection');
installPermissionsHandler({ session: session.defaultSession, userConfig });
@@ -288,7 +290,7 @@ export class SettingsChannel extends EventEmitter {
// Notify main to notify windows of preferences change. As for DB-backed
// settings, those are set by the renderer, and afterwards the renderer IPC sends
// to main the event 'preferences-changed'.
this.emit('ephemeral-setting-changed');
this.emit('ephemeral-setting-changed', name);
const mainWindow = this.#mainWindow;
if (!mainWindow || !mainWindow.webContents) {
@@ -308,7 +310,7 @@ export class SettingsChannel extends EventEmitter {
public override on(
type: 'ephemeral-setting-changed',
callback: () => void
callback: (name: string) => void
): this;
public override on(
@@ -330,7 +332,10 @@ export class SettingsChannel extends EventEmitter {
value: string
): boolean;
public override emit(type: 'ephemeral-setting-changed'): boolean;
public override emit(
type: 'ephemeral-setting-changed',
name: string
): boolean;
public override emit(
type: SettingChangeEventType<keyof SettingsValuesType>,

View File

@@ -33,6 +33,12 @@ export const isDrawAttentionSupported = (OS: OSType): boolean => !OS.isMacOS();
export const isSystemTraySupported = (OS: OSType): boolean =>
OS.isWindows() || OS.isLinux();
export const isContentProtectionSupported = (OS: OSType): boolean =>
OS.isWindows() || OS.isMacOS();
export const isContentProtectionNeeded = (OS: OSType): boolean =>
OS.isWindows();
export const getDefaultSystemTraySetting = (
OS: OSType,
appVersion: string

View File

@@ -192,6 +192,8 @@ type ValuesWithGetters = Omit<
| 'mediaPermissions'
| 'mediaCameraPermissions'
| 'autoLaunch'
| 'spellCheck'
| 'contentProtection'
| 'systemTraySetting'
>;
@@ -240,6 +242,7 @@ export type IPCEventsGettersType = {
getZoomFactor: () => Promise<ZoomFactorType>;
getLocaleOverride: () => Promise<string | null>;
getSpellCheck: () => Promise<boolean>;
getContentProtection: () => Promise<boolean>;
getSystemTraySetting: () => Promise<SystemTraySetting>;
getThemeSetting: () => Promise<ThemeType>;
// Events
@@ -338,6 +341,12 @@ export function createIPCEvents(
account.captureChange('hasStoriesDisabled');
window.textsecure.server?.onHasStoriesDisabledChange(value);
},
getContentProtection: () => {
return getEphemeralSetting('contentProtection');
},
setContentProtection: async (value: boolean) => {
await setEphemeralSetting('contentProtection', value);
},
getStoryViewReceiptsEnabled: () => {
return (
window.storage.get('storyViewReceiptsEnabled') ??

View File

@@ -26,6 +26,7 @@ export type ThemeType = 'light' | 'dark' | 'system';
export type EphemeralSettings = {
spellCheck: boolean;
contentProtection: boolean;
systemTraySetting: SystemTraySetting;
themeSetting: ThemeType;
localeOverride: string | null;

View File

@@ -51,6 +51,7 @@ SettingsWindowProps.onRender(
hasAutoLaunch,
hasCallNotifications,
hasCallRingtoneNotification,
hasContentProtection,
hasCountMutedConversations,
hasHideMenuBar,
hasIncomingCallNotifications,
@@ -77,6 +78,8 @@ SettingsWindowProps.onRender(
isNotificationAttentionSupported,
isSyncSupported,
isSystemTraySupported,
isContentProtectionSupported,
isContentProtectionNeeded,
isInternalUser,
lastSyncTime,
makeSyncRequest,
@@ -88,6 +91,7 @@ SettingsWindowProps.onRender(
onAutoLaunchChange,
onCallNotificationsChange,
onCallRingtoneNotificationChange,
onContentProtectionChange,
onCountMutedConversationsChange,
onEmojiSkinToneDefaultChange,
onHasStoriesDisabledChanged,
@@ -166,6 +170,7 @@ SettingsWindowProps.onRender(
hasAutoLaunch={hasAutoLaunch}
hasCallNotifications={hasCallNotifications}
hasCallRingtoneNotification={hasCallRingtoneNotification}
hasContentProtection={hasContentProtection}
hasCountMutedConversations={hasCountMutedConversations}
hasHideMenuBar={hasHideMenuBar}
hasIncomingCallNotifications={hasIncomingCallNotifications}
@@ -195,6 +200,8 @@ SettingsWindowProps.onRender(
isNotificationAttentionSupported={isNotificationAttentionSupported}
isSyncSupported={isSyncSupported}
isSystemTraySupported={isSystemTraySupported}
isContentProtectionSupported={isContentProtectionSupported}
isContentProtectionNeeded={isContentProtectionNeeded}
isInternalUser={isInternalUser}
lastSyncTime={lastSyncTime}
localeOverride={localeOverride}
@@ -207,6 +214,7 @@ SettingsWindowProps.onRender(
onAutoLaunchChange={onAutoLaunchChange}
onCallNotificationsChange={onCallNotificationsChange}
onCallRingtoneNotificationChange={onCallRingtoneNotificationChange}
onContentProtectionChange={onContentProtectionChange}
onCountMutedConversationsChange={onCountMutedConversationsChange}
onEmojiSkinToneDefaultChange={onEmojiSkinToneDefaultChange}
onHasStoriesDisabledChanged={onHasStoriesDisabledChanged}

View File

@@ -48,6 +48,7 @@ const settingNotificationSetting = createSetting('notificationSetting');
const settingRelayCalls = createSetting('alwaysRelayCalls');
const settingSentMediaQuality = createSetting('sentMediaQualitySetting');
const settingSpellCheck = createSetting('spellCheck');
const settingContentProtection = createSetting('contentProtection');
const settingTextFormatting = createSetting('textFormatting');
const settingTheme = createSetting('themeSetting');
const settingSystemTraySetting = createSetting('systemTraySetting');
@@ -176,6 +177,7 @@ async function renderPreferences() {
hasAutoLaunch,
hasCallNotifications,
hasCallRingtoneNotification,
hasContentProtection,
hasCountMutedConversations,
hasHideMenuBar,
hasIncomingCallNotifications,
@@ -223,6 +225,7 @@ async function renderPreferences() {
hasAutoLaunch: settingAutoLaunch.getValue(),
hasCallNotifications: settingCallSystemNotification.getValue(),
hasCallRingtoneNotification: settingCallRingtoneNotification.getValue(),
hasContentProtection: settingContentProtection.getValue(),
hasCountMutedConversations: settingCountMutedConversations.getValue(),
hasHideMenuBar: settingHideMenuBar.getValue(),
hasIncomingCallNotifications: settingIncomingCallNotification.getValue(),
@@ -321,6 +324,7 @@ async function renderPreferences() {
hasAutoLaunch,
hasCallNotifications,
hasCallRingtoneNotification,
hasContentProtection,
hasCountMutedConversations,
hasHideMenuBar,
hasIncomingCallNotifications,
@@ -385,6 +389,8 @@ async function renderPreferences() {
isSyncSupported: !isSyncNotSupported,
isInternalUser,
isSystemTraySupported: Settings.isSystemTraySupported(OS),
isContentProtectionSupported: Settings.isContentProtectionSupported(OS),
isContentProtectionNeeded: Settings.isContentProtectionNeeded(OS),
isMinimizeToAndStartInSystemTraySupported:
Settings.isMinimizeToAndStartInSystemTraySupported(OS),
@@ -408,6 +414,9 @@ async function renderPreferences() {
onCallRingtoneNotificationChange: attachRenderCallback(
settingCallRingtoneNotification.setValue
),
onContentProtectionChange: attachRenderCallback(
settingContentProtection.setValue
),
onCountMutedConversationsChange: attachRenderCallback(
settingCountMutedConversations.setValue
),