From 8a49a247638b8bd4591d8806e281ad0316e2ca65 Mon Sep 17 00:00:00 2001 From: automated-signal <37887102+automated-signal@users.noreply.github.com> Date: Fri, 17 Apr 2026 18:45:32 -0500 Subject: [PATCH] Preferences: Allow more options to be changed Co-authored-by: Scott Nonnenberg --- _locales/en/messages.json | 33 ++++++ stylesheets/components/Preferences.scss | 22 ++++ ts/ConversationController.preload.ts | 21 ++-- ts/components/Checkbox.dom.tsx | 2 +- ts/components/Preferences.dom.stories.tsx | 9 +- ts/components/Preferences.dom.tsx | 111 ++++++++++++------ ts/components/StoriesSettingsModal.dom.tsx | 9 +- ts/services/storageRecordOps.preload.ts | 2 +- ts/state/smart/Preferences.preload.tsx | 49 +++++++- .../smart/StoriesSettingsModal.preload.tsx | 8 ++ ts/test-mock/settings/settings_test.node.ts | 2 +- ts/windows/main/start.preload.ts | 4 + 12 files changed, 215 insertions(+), 57 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 9c7e171f4f..bbb5c0ed63 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -2382,6 +2382,15 @@ "messageformat": "Cancel", "description": "Preferences > Edit Chat Folder Page > Cancel Button" }, + "icu:Preferences__PrivacyPage__ShowStatusIcon__Label": { + "messageformat": "Show status icon", + "description": "Preferences > Privacy Page > Show Status Icon Checkbox > Label" + }, + "icu:Preferences__PrivacyPage__ShowStatusIcon__Description": { + "messageformat": "Show an icon in message details when they were delivered using sealed sender.", + "description": "Preferences > Privacy Page > Show Status Icon Checkbox > Description" + }, + "icu:Preferences__PrivacyPage__KeyTransparency__Label": { "messageformat": "Automatic key verification", "description": "Preferences > Privacy Page > Key Transparency Checkbox > Label" @@ -7850,6 +7859,18 @@ "messageformat": "To change this setting, open the Signal app on your mobile device and navigate to Settings > Chats", "description": "Description for the generate link previews setting" }, + "icu:Preferences__link-previews--new-description": { + "messageformat": "Retrieve link previews directly from websites for messages you send.", + "description": "Description for the generate link previews setting" + }, + "icu:Preferences__address-book-photos--title": { + "messageformat": "Use address book photos", + "description": "Title for the generate link previews setting" + }, + "icu:Preferences__address-book-photos--description": { + "messageformat": "Display contact photos from your address book if available.", + "description": "Description for the generate link previews setting" + }, "icu:Preferences__auto-convert-emoji--title": { "messageformat": "Convert typed emoticons to emoji", "description": "Title for the auto convert emoji setting" @@ -8658,10 +8679,18 @@ "messageformat": "Read receipts", "description": "Label for the read receipts setting" }, + "icu:Preferences--read-receipts--description": { + "messageformat": "If disabled, you won't see read receipts from others.", + "description": "Description for the read receipts setting" + }, "icu:Preferences--typing-indicators": { "messageformat": "Typing indicators", "description": "Label for the typing indicators setting" }, + "icu:Preferences--typing-indicators--description": { + "messageformat": "If disabled, you won't see typing indicators from others.", + "description": "Description for the typing indicators setting" + }, "icu:Preferences--updates": { "messageformat": "Updates", "description": "Header for settings having to do with updates" @@ -9340,6 +9369,10 @@ "messageformat": "View Receipts", "description": "Label of view receipts checkbox in story settings" }, + "icu:StoriesSettings__view-receipts--new-description": { + "messageformat": "See and share when stories are viewed. If disabled, you won't see when others view your story.", + "description": "Label of view receipts checkbox in story settings" + }, "icu:StoriesSettings__view-receipts--description": { "messageformat": "To change this setting, open the Signal app on your mobile device and navigate to Settings -> Stories", "description": "Description of how view receipts can be changed in story settings" diff --git a/stylesheets/components/Preferences.scss b/stylesheets/components/Preferences.scss index 69a3cc499f..7090ac7f5d 100644 --- a/stylesheets/components/Preferences.scss +++ b/stylesheets/components/Preferences.scss @@ -1250,3 +1250,25 @@ $secondary-text-color: light-dark( .TimePickerPopup { scrollbar-width: 0; } + +.Preferences__Privacy__StatusIcon { + margin-inline-start: 3px; + margin-bottom: -4px; + + width: 18px; + height: 18px; + display: inline-block; + + @include mixins.light-theme { + @include mixins.color-svg( + '../images/icons/v2/unidentified-delivery-solid-20.svg', + variables.$color-gray-60 + ); + } + @include mixins.dark-theme { + @include mixins.color-svg( + '../images/icons/v2/unidentified-delivery-solid-20.svg', + variables.$color-gray-25 + ); + } +} diff --git a/ts/ConversationController.preload.ts b/ts/ConversationController.preload.ts index 0a9e660720..1471c06549 100644 --- a/ts/ConversationController.preload.ts +++ b/ts/ConversationController.preload.ts @@ -1577,17 +1577,18 @@ export class ConversationController { return this.#_initialPromise; } - // A number of things outside conversation.attributes affect conversation re-rendering. - // If it's scoped to a given conversation, it's easy to trigger('change'). There are - // important values in storage and the storage service which change rendering pretty - // radically, so this function is necessary to force regeneration of props. - async forceRerender(identifiers?: Array): Promise { + // When the user changes their avatar preferences (address book vs. signal profile), we + // need to regenerate all cached conversation props. But only if that contact had an + // avatar taken from the address book. + async rerenderAfterAvatarChange(): Promise { let count = 0; - const conversations = identifiers - ? identifiers.map(identifier => this.get(identifier)).filter(isNotNil) - : this.#_conversations.slice(); + const conversations = this.#_conversations.filter( + conversation => + conversation.get('avatar') && + isDirectConversation(conversation.attributes) + ); log.info( - `forceRerender: Starting to loop through ${conversations.length} conversations` + `rerenderAfterAvatarChange: Starting to loop through ${conversations.length} conversations` ); for (const conversation of conversations) { @@ -1604,7 +1605,7 @@ export class ConversationController { await sleep(300); } } - log.info(`forceRerender: Updated ${count} conversations`); + log.info(`rerenderAfterAvatarChange: Updated ${count} conversations`); } onConvoOpenStart(conversationId: string): void { diff --git a/ts/components/Checkbox.dom.tsx b/ts/components/Checkbox.dom.tsx index 39c63fae10..87761e9230 100644 --- a/ts/components/Checkbox.dom.tsx +++ b/ts/components/Checkbox.dom.tsx @@ -18,7 +18,7 @@ export type PropsType = { description?: ReactNode; disabled?: boolean; isRadio?: boolean; - label: string; + label: ReactNode; moduleClassName?: string; name: string; onChange: (value: boolean) => unknown; diff --git a/ts/components/Preferences.dom.stories.tsx b/ts/components/Preferences.dom.stories.tsx index 03493bc961..8153e6815d 100644 --- a/ts/components/Preferences.dom.stories.tsx +++ b/ts/components/Preferences.dom.stories.tsx @@ -452,8 +452,10 @@ export default { hasMinimizeToSystemTray: true, hasNotificationAttention: false, hasNotifications: true, + hasPreferContactAvatars: true, hasReadReceipts: true, hasRelayCalls: false, + hasSealedSenderIndicators: true, hasSpellCheck: true, hasStoriesDisabled: false, hasTextFormatting: true, @@ -577,6 +579,7 @@ export default { onKeepMutedChatsArchivedChange: action('onKeepMutedChatsArchivedChange'), onLocaleChange: action('onLocaleChange'), onLastSyncTimeChange: action('onLastSyncTimeChange'), + onLinkPreviewsChange: action('onLinkPreviewsChange'), onMediaCameraPermissionsChange: action('onMediaCameraPermissionsChange'), onMediaPermissionsChange: action('onMediaPermissionsChange'), onMessageAudioChange: action('onMessageAudioChange'), @@ -587,7 +590,10 @@ export default { onNotificationAttentionChange: action('onNotificationAttentionChange'), onNotificationContentChange: action('onNotificationContentChange'), onNotificationsChange: action('onNotificationsChange'), + onPreferContactAvatarsChange: action('onPreferContactAvatarsChange'), + onReadReceiptsChange: action('onReadReceiptsChange'), onRelayCallsChange: action('onRelayCallsChange'), + onSealedSenderIndicatorsChange: action('onSealedSenderIndicatorsChange'), onSelectedCameraChange: action('onSelectedCameraChange'), onSelectedMicrophoneChange: action('onSelectedMicrophoneChange'), onSelectedSpeakerChange: action('onSelectedSpeakerChange'), @@ -597,9 +603,10 @@ export default { onTextFormattingChange: action('onTextFormattingChange'), onThemeChange: action('onThemeChange'), onToggleNavTabsCollapse: action('onToggleNavTabsCollapse'), + onTypingIndicatorsChange: action('onTypingIndicatorsChange'), onUniversalExpireTimerChange: action('onUniversalExpireTimerChange'), - onWhoCanSeeMeChange: action('onWhoCanSeeMeChange'), onWhoCanFindMeChange: action('onWhoCanFindMeChange'), + onWhoCanSeeMeChange: action('onWhoCanSeeMeChange'), onZoomFactorChange: action('onZoomFactorChange'), openFileInFolder: action('openFileInFolder'), pickLocalBackupFolder: () => diff --git a/ts/components/Preferences.dom.tsx b/ts/components/Preferences.dom.tsx index 11f6d8aea1..ec55a4bf39 100644 --- a/ts/components/Preferences.dom.tsx +++ b/ts/components/Preferences.dom.tsx @@ -149,10 +149,12 @@ export type PropsDataType = { hasMediaCameraPermissions: boolean | undefined; hasMediaPermissions: boolean | undefined; hasMessageAudio: boolean; + hasSealedSenderIndicators: boolean; hasMinimizeToAndStartInSystemTray: boolean | undefined; hasMinimizeToSystemTray: boolean | undefined; hasNotificationAttention: boolean; hasNotifications: boolean; + hasPreferContactAvatars: boolean; hasReadReceipts: boolean; hasRelayCalls?: boolean; hasSpellCheck: boolean | undefined; @@ -327,6 +329,7 @@ type PropsFunctionType = { onIncomingCallNotificationsChange: CheckboxChangeHandlerType; onKeepMutedChatsArchivedChange: CheckboxChangeHandlerType; onLastSyncTimeChange: (time: number) => unknown; + onLinkPreviewsChange: CheckboxChangeHandlerType; onLocaleChange: (locale: string | null | undefined) => void; onMediaCameraPermissionsChange: CheckboxChangeHandlerType; onMediaPermissionsChange: CheckboxChangeHandlerType; @@ -336,7 +339,10 @@ type PropsFunctionType = { onNotificationAttentionChange: CheckboxChangeHandlerType; onNotificationContentChange: SelectChangeHandlerType; onNotificationsChange: CheckboxChangeHandlerType; + onPreferContactAvatarsChange: CheckboxChangeHandlerType; + onReadReceiptsChange: CheckboxChangeHandlerType; onRelayCallsChange: CheckboxChangeHandlerType; + onSealedSenderIndicatorsChange: CheckboxChangeHandlerType; onSelectedCameraChange: SelectChangeHandlerType; onSelectedMicrophoneChange: SelectChangeHandlerType; onSelectedSpeakerChange: SelectChangeHandlerType; @@ -345,9 +351,10 @@ type PropsFunctionType = { onTextFormattingChange: CheckboxChangeHandlerType; onThemeChange: SelectChangeHandlerType; onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void; + onTypingIndicatorsChange: CheckboxChangeHandlerType; onUniversalExpireTimerChange: SelectChangeHandlerType; - onWhoCanSeeMeChange: SelectChangeHandlerType; onWhoCanFindMeChange: SelectChangeHandlerType; + onWhoCanSeeMeChange: SelectChangeHandlerType; onZoomFactorChange: SelectChangeHandlerType; openFileInFolder: (path: string) => void; internalDeleteAllMegaphones: () => Promise; @@ -453,8 +460,10 @@ export function Preferences({ hasMinimizeToSystemTray, hasNotificationAttention, hasNotifications, + hasPreferContactAvatars, hasReadReceipts, hasRelayCalls, + hasSealedSenderIndicators, hasSpellCheck, hasStoriesDisabled, hasTextFormatting, @@ -499,6 +508,7 @@ export function Preferences({ onIncomingCallNotificationsChange, onKeepMutedChatsArchivedChange, onLastSyncTimeChange, + onLinkPreviewsChange, onLocaleChange, onMediaCameraPermissionsChange, onMediaPermissionsChange, @@ -508,7 +518,10 @@ export function Preferences({ onNotificationAttentionChange, onNotificationContentChange, onNotificationsChange, + onPreferContactAvatarsChange, + onReadReceiptsChange, onRelayCallsChange, + onSealedSenderIndicatorsChange, onSelectedCameraChange, onSelectedMicrophoneChange, onSelectedSpeakerChange, @@ -517,9 +530,10 @@ export function Preferences({ onTextFormattingChange, onThemeChange, onToggleNavTabsCollapse, + onTypingIndicatorsChange, onUniversalExpireTimerChange, - onWhoCanSeeMeChange, onWhoCanFindMeChange, + onWhoCanSeeMeChange, onZoomFactorChange, otherTabsUnreadStats, settingsLocation, @@ -1186,12 +1200,23 @@ export function Preferences({ /> + -
-
- {i18n('icu:Preferences__privacy--description')} -
-
{showDisappearingTimerDialog && ( - {isKeyTransparencyAvailable && ( - + + + {i18n('icu:Preferences__PrivacyPage__ShowStatusIcon__Label')} +
+ + } + description={i18n( + 'icu:Preferences__PrivacyPage__ShowStatusIcon__Description' + )} + moduleClassName="Preferences__checkbox" + name="showStatusIcon" + onChange={onSealedSenderIndicatorsChange} + /> + {isKeyTransparencyAvailable && ( + {i18n( + 'icu:Preferences__PrivacyPage__KeyTransparency__Description' + )} +   + + + + + } moduleClassName="Preferences__checkbox" name="keyTransparency" onChange={() => onHasKeyTransparencyDisabledChanged(!hasKeyTransparencyDisabled) } /> -
-
- {i18n( - 'icu:Preferences__PrivacyPage__KeyTransparency__Description' - )} -   - - - -
-
- - )} + )} +
unknown; + onStoryViewReceiptsChange: (value: boolean) => unknown; onViewersUpdated: ( listId: string, viewerServiceIds: Array @@ -262,6 +263,7 @@ export function StoriesSettingsModal({ onHideMyStoriesFrom, onRemoveMembers, onRepliesNReactionsChanged, + onStoryViewReceiptsChange, onViewersUpdated, setMyStoriesToAllSignalConnections, storyViewReceiptsEnabled, @@ -466,13 +468,14 @@ export function StoriesSettingsModal({
diff --git a/ts/services/storageRecordOps.preload.ts b/ts/services/storageRecordOps.preload.ts index 8aa709009c..4a69389b16 100644 --- a/ts/services/storageRecordOps.preload.ts +++ b/ts/services/storageRecordOps.preload.ts @@ -1595,7 +1595,7 @@ export async function mergeAccountRecord( itemStorage.get('postRegistrationSyncsStatus') !== 'incomplete'; if (previous !== preferContactAvatars && postRegistrationSyncsComplete) { - await window.ConversationController.forceRerender(); + await window.ConversationController.rerenderAfterAvatarChange(); } } diff --git a/ts/state/smart/Preferences.preload.tsx b/ts/state/smart/Preferences.preload.tsx index e57b877e2e..a07013d0ed 100644 --- a/ts/state/smart/Preferences.preload.tsx +++ b/ts/state/smart/Preferences.preload.tsx @@ -587,9 +587,7 @@ export function SmartPreferences(): React.JSX.Element | null { } = items; const defaultConversationColor = items.defaultConversationColor || DEFAULT_CONVERSATION_COLOR; - const hasLinkPreviews = items.linkPreviews ?? false; - const hasReadReceipts = items['read-receipt-setting'] ?? false; - const hasTypingIndicators = items.typingIndicators ?? false; + const blockedCount = (items['blocked-groups']?.length ?? 0) + (items['blocked-uuids']?.length ?? 0); @@ -637,6 +635,43 @@ export function SmartPreferences(): React.JSX.Element | null { return [value, setter]; } + const [hasLinkPreviews, onLinkPreviewsChange] = createItemsAccess( + 'linkPreviews', + false, + () => { + const account = window.ConversationController.getOurConversationOrThrow(); + account.captureChange('linkPreviews'); + } + ); + const [hasPreferContactAvatars, onPreferContactAvatarsChange] = + createItemsAccess('preferContactAvatars', false, () => { + const account = window.ConversationController.getOurConversationOrThrow(); + account.captureChange('preferContactAvatars'); + drop(window.ConversationController.rerenderAfterAvatarChange()); + }); + + const [hasReadReceipts, onReadReceiptsChange] = createItemsAccess( + 'read-receipt-setting', + false, + () => { + const account = window.ConversationController.getOurConversationOrThrow(); + account.captureChange('read-receipt-setting'); + } + ); + const [hasTypingIndicators, onTypingIndicatorsChange] = createItemsAccess( + 'typingIndicators', + false, + () => { + const account = window.ConversationController.getOurConversationOrThrow(); + account.captureChange('typingIndicators'); + } + ); + const [hasSealedSenderIndicators, onSealedSenderIndicatorsChange] = + createItemsAccess('sealedSenderIndicators', false, () => { + const account = window.ConversationController.getOurConversationOrThrow(); + account.captureChange('sealedSenderIndicators'); + }); + const [autoDownloadAttachment, onAutoDownloadAttachmentChange] = createItemsAccess( 'auto-download-attachment', @@ -654,6 +689,7 @@ export function SmartPreferences(): React.JSX.Element | null { 'autoConvertEmoji', true ); + const [hasKeepMutedChatsArchived, onKeepMutedChatsArchivedChange] = createItemsAccess('keepMutedChatsArchived', false, () => { const account = window.ConversationController.getOurConversationOrThrow(); @@ -900,8 +936,10 @@ export function SmartPreferences(): React.JSX.Element | null { hasMinimizeToSystemTray={hasMinimizeToSystemTray} hasNotificationAttention={hasNotificationAttention} hasNotifications={hasNotifications} + hasPreferContactAvatars={hasPreferContactAvatars} hasReadReceipts={hasReadReceipts} hasRelayCalls={hasRelayCalls} + hasSealedSenderIndicators={hasSealedSenderIndicators} hasSpellCheck={hasSpellCheck} hasStoriesDisabled={hasStoriesDisabled} hasTextFormatting={hasTextFormatting} @@ -950,6 +988,7 @@ export function SmartPreferences(): React.JSX.Element | null { onIncomingCallNotificationsChange={onIncomingCallNotificationsChange} onKeepMutedChatsArchivedChange={onKeepMutedChatsArchivedChange} onLastSyncTimeChange={onLastSyncTimeChange} + onLinkPreviewsChange={onLinkPreviewsChange} onLocaleChange={onLocaleChange} onMediaCameraPermissionsChange={onMediaCameraPermissionsChange} onMediaPermissionsChange={onMediaPermissionsChange} @@ -962,7 +1001,10 @@ export function SmartPreferences(): React.JSX.Element | null { onNotificationContentChange={onNotificationContentChange} onNotificationsChange={onNotificationsChange} onStartUpdate={startUpdate} + onPreferContactAvatarsChange={onPreferContactAvatarsChange} + onReadReceiptsChange={onReadReceiptsChange} onRelayCallsChange={onRelayCallsChange} + onSealedSenderIndicatorsChange={onSealedSenderIndicatorsChange} onSelectedCameraChange={onSelectedCameraChange} onSelectedMicrophoneChange={onSelectedMicrophoneChange} onSelectedSpeakerChange={onSelectedSpeakerChange} @@ -971,6 +1013,7 @@ export function SmartPreferences(): React.JSX.Element | null { onTextFormattingChange={onTextFormattingChange} onThemeChange={onThemeChange} onToggleNavTabsCollapse={toggleNavTabsCollapse} + onTypingIndicatorsChange={onTypingIndicatorsChange} onUniversalExpireTimerChange={onUniversalExpireTimerChange} onWhoCanFindMeChange={onWhoCanFindMeChange} onWhoCanSeeMeChange={onWhoCanSeeMeChange} diff --git a/ts/state/smart/StoriesSettingsModal.preload.tsx b/ts/state/smart/StoriesSettingsModal.preload.tsx index c08d58a91f..da80dc4803 100644 --- a/ts/state/smart/StoriesSettingsModal.preload.tsx +++ b/ts/state/smart/StoriesSettingsModal.preload.tsx @@ -19,6 +19,7 @@ import { useGlobalModalActions } from '../ducks/globalModals.preload.ts'; import { useStoryDistributionListsActions } from '../ducks/storyDistributionLists.preload.ts'; import { useStoriesActions } from '../ducks/stories.preload.ts'; import { useConversationsActions } from '../ducks/conversations.preload.ts'; +import { useItemsActions } from '../ducks/items.preload.ts'; export const SmartStoriesSettingsModal = memo( function SmartStoriesSettingsModal() { @@ -35,10 +36,16 @@ export const SmartStoriesSettingsModal = memo( updateStoryViewers, } = useStoryDistributionListsActions(); const { toggleGroupsForStorySend } = useConversationsActions(); + const { putItem } = useItemsActions(); const signalConnections = useSelector(getAllSignalConnections); const getPreferredBadge = useSelector(getPreferredBadgeSelector); const storyViewReceiptsEnabled = useSelector(getHasStoryViewReceiptSetting); + const onStoryViewReceiptsChange = (value: boolean) => { + putItem('storyViewReceiptsEnabled', value); + const account = window.ConversationController.getOurConversationOrThrow(); + account.captureChange('storyViewReceiptsEnabled'); + }; const i18n = useSelector(getIntl); const me = useSelector(getMe); const candidateConversations = useSelector(getCandidateContactsForNewGroup); @@ -68,6 +75,7 @@ export const SmartStoriesSettingsModal = memo( onRemoveMembers={removeMembersFromDistributionList} onRepliesNReactionsChanged={allowsRepliesChanged} onViewersUpdated={updateStoryViewers} + onStoryViewReceiptsChange={onStoryViewReceiptsChange} setMyStoriesToAllSignalConnections={setMyStoriesToAllSignalConnections} storyViewReceiptsEnabled={storyViewReceiptsEnabled} theme={theme} diff --git a/ts/test-mock/settings/settings_test.node.ts b/ts/test-mock/settings/settings_test.node.ts index 85035d9921..27c02a5dd9 100644 --- a/ts/test-mock/settings/settings_test.node.ts +++ b/ts/test-mock/settings/settings_test.node.ts @@ -49,7 +49,7 @@ describe('settings', function (this: Mocha.Suite) { await window.getByText('Notification content').waitFor(); await window.getByRole('button', { name: 'Privacy' }).click(); - await window.getByText('Read receipts').waitFor(); + await window.getByText('Read receipts', { exact: true }).waitFor(); await window.getByRole('button', { name: 'Data usage' }).click(); await window.getByText('Sent media quality').waitFor(); diff --git a/ts/windows/main/start.preload.ts b/ts/windows/main/start.preload.ts index e5bbd271f6..282b6be6b6 100644 --- a/ts/windows/main/start.preload.ts +++ b/ts/windows/main/start.preload.ts @@ -77,6 +77,10 @@ if ( conversationId ); }, + getConversations: () => + window.ConversationController.getAll().map( + conversation => conversation.attributes + ), getConversation: (id: string) => window.ConversationController.get(id), getMessageById: (id: string) => window.MessageCache.getById(id)?.attributes, getMessageBySentAt: async (timestamp: number) => {