diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 02d8dad7e9..5b89e2c3d5 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -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" diff --git a/app/main.ts b/app/main.ts index 802bc412db..14524e6363 100644 --- a/app/main.ts +++ b/app/main.ts @@ -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) { for (const arg of argv) { const route = parseSignalRoute(arg); diff --git a/ts/components/Preferences.stories.tsx b/ts/components/Preferences.stories.tsx index a33e98271a..61756f8e7f 100644 --- a/ts/components/Preferences.stories.tsx +++ b/ts/components/Preferences.stories.tsx @@ -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'), diff --git a/ts/components/Preferences.tsx b/ts/components/Preferences.tsx index 857ac814b9..d8835bef23 100644 --- a/ts/components/Preferences.tsx +++ b/ts/components/Preferences.tsx @@ -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 @@ -189,6 +192,7 @@ type PropsFunctionType = { onAutoLaunchChange: CheckboxChangeHandlerType; onCallNotificationsChange: CheckboxChangeHandlerType; onCallRingtoneNotificationChange: CheckboxChangeHandlerType; + onContentProtectionChange: CheckboxChangeHandlerType; onCountMutedConversationsChange: CheckboxChangeHandlerType; onEmojiSkinToneDefaultChange: (emojiSkinTone: EmojiSkinTone) => void; onHasStoriesDisabledChanged: SelectChangeHandlerType; @@ -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(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(null); useEffect(() => { const settingsPane = settingsPaneRef.current; @@ -1365,6 +1386,41 @@ export function Preferences({ } /> + {isContentProtectionSupported && ( + + + + )} + {confirmContentProtection ? ( + 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')} + + ) : null} 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, diff --git a/ts/types/Settings.ts b/ts/types/Settings.ts index 9e2436f955..56864e8114 100644 --- a/ts/types/Settings.ts +++ b/ts/types/Settings.ts @@ -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 diff --git a/ts/util/createIPCEvents.ts b/ts/util/createIPCEvents.ts index 14bd6d8f68..d2a573d0c4 100644 --- a/ts/util/createIPCEvents.ts +++ b/ts/util/createIPCEvents.ts @@ -192,6 +192,8 @@ type ValuesWithGetters = Omit< | 'mediaPermissions' | 'mediaCameraPermissions' | 'autoLaunch' + | 'spellCheck' + | 'contentProtection' | 'systemTraySetting' >; @@ -240,6 +242,7 @@ export type IPCEventsGettersType = { getZoomFactor: () => Promise; getLocaleOverride: () => Promise; getSpellCheck: () => Promise; + getContentProtection: () => Promise; getSystemTraySetting: () => Promise; getThemeSetting: () => Promise; // 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') ?? diff --git a/ts/util/preload.ts b/ts/util/preload.ts index fda82b8ab6..ec83ce930e 100644 --- a/ts/util/preload.ts +++ b/ts/util/preload.ts @@ -26,6 +26,7 @@ export type ThemeType = 'light' | 'dark' | 'system'; export type EphemeralSettings = { spellCheck: boolean; + contentProtection: boolean; systemTraySetting: SystemTraySetting; themeSetting: ThemeType; localeOverride: string | null; diff --git a/ts/windows/settings/app.tsx b/ts/windows/settings/app.tsx index b7bb1f28d6..cedb94fa82 100644 --- a/ts/windows/settings/app.tsx +++ b/ts/windows/settings/app.tsx @@ -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} diff --git a/ts/windows/settings/preload.ts b/ts/windows/settings/preload.ts index 9ac4894606..0143c7b5e9 100644 --- a/ts/windows/settings/preload.ts +++ b/ts/windows/settings/preload.ts @@ -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 ),