Preferences: Allow more options to be changed

Co-authored-by: Scott Nonnenberg <scott@signal.org>
This commit is contained in:
automated-signal
2026-04-17 18:45:32 -05:00
committed by GitHub
parent 2b35fe523d
commit 8a49a24763
12 changed files with 215 additions and 57 deletions
+33
View File
@@ -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"
+22
View File
@@ -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
);
}
}
+11 -10
View File
@@ -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<string>): Promise<void> {
// 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<void> {
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 {
+1 -1
View File
@@ -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;
+8 -1
View File
@@ -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: () =>
+74 -37
View File
@@ -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<NotificationSettingType>;
onNotificationsChange: CheckboxChangeHandlerType;
onPreferContactAvatarsChange: CheckboxChangeHandlerType;
onReadReceiptsChange: CheckboxChangeHandlerType;
onRelayCallsChange: CheckboxChangeHandlerType;
onSealedSenderIndicatorsChange: CheckboxChangeHandlerType;
onSelectedCameraChange: SelectChangeHandlerType<string | undefined>;
onSelectedMicrophoneChange: SelectChangeHandlerType<AudioDevice | undefined>;
onSelectedSpeakerChange: SelectChangeHandlerType<AudioDevice | undefined>;
@@ -345,9 +351,10 @@ type PropsFunctionType = {
onTextFormattingChange: CheckboxChangeHandlerType;
onThemeChange: SelectChangeHandlerType<ThemeType>;
onToggleNavTabsCollapse: (navTabsCollapsed: boolean) => void;
onTypingIndicatorsChange: CheckboxChangeHandlerType;
onUniversalExpireTimerChange: SelectChangeHandlerType<number>;
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>;
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
openFileInFolder: (path: string) => void;
internalDeleteAllMegaphones: () => Promise<number>;
@@ -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({
/>
<Checkbox
checked={hasLinkPreviews}
description={i18n('icu:Preferences__link-previews--description')}
disabled
description={i18n(
'icu:Preferences__link-previews--new-description'
)}
label={i18n('icu:Preferences__link-previews--title')}
moduleClassName="Preferences__checkbox"
name="linkPreviews"
onChange={noop}
onChange={onLinkPreviewsChange}
/>
<Checkbox
checked={hasPreferContactAvatars}
label={i18n('icu:Preferences__address-book-photos--title')}
description={i18n(
'icu:Preferences__address-book-photos--description'
)}
moduleClassName="Preferences__checkbox"
name="typingIndicators"
onChange={onPreferContactAvatarsChange}
/>
<Checkbox
checked={hasAutoConvertEmoji}
@@ -1697,25 +1722,22 @@ export function Preferences({
<SettingsRow title={i18n('icu:Preferences--messaging')}>
<Checkbox
checked={hasReadReceipts}
disabled
label={i18n('icu:Preferences--read-receipts')}
description={i18n('icu:Preferences--read-receipts--description')}
moduleClassName="Preferences__checkbox"
name="readReceipts"
onChange={noop}
onChange={onReadReceiptsChange}
/>
<Checkbox
checked={hasTypingIndicators}
disabled
label={i18n('icu:Preferences--typing-indicators')}
description={i18n(
'icu:Preferences--typing-indicators--description'
)}
moduleClassName="Preferences__checkbox"
name="typingIndicators"
onChange={noop}
onChange={onTypingIndicatorsChange}
/>
<div className="Preferences__padding">
<div className="Preferences__description">
{i18n('icu:Preferences__privacy--description')}
</div>
</div>
</SettingsRow>
{showDisappearingTimerDialog && (
<DisappearingTimeDialog
@@ -1853,40 +1875,55 @@ export function Preferences({
</div>
</FlowingControl>
</SettingsRow>
{isKeyTransparencyAvailable && (
<SettingsRow>
<SettingsRow title={i18n('icu:Preferences--advanced')}>
<Checkbox
checked={hasSealedSenderIndicators}
label={
<>
{i18n('icu:Preferences__PrivacyPage__ShowStatusIcon__Label')}
<div className="Preferences__Privacy__StatusIcon" />
</>
}
description={i18n(
'icu:Preferences__PrivacyPage__ShowStatusIcon__Description'
)}
moduleClassName="Preferences__checkbox"
name="showStatusIcon"
onChange={onSealedSenderIndicatorsChange}
/>
{isKeyTransparencyAvailable && (
<Checkbox
checked={!hasKeyTransparencyDisabled}
label={i18n(
'icu:Preferences__PrivacyPage__KeyTransparency__Label'
)}
description={
<>
{i18n(
'icu:Preferences__PrivacyPage__KeyTransparency__Description'
)}
&ensp;
<a
href={KEY_TRANSPARENCY_URL}
rel="noreferrer"
target="_blank"
className={tw('text-label-primary')}
>
<I18n
i18n={i18n}
id="icu:Preferences__PrivacyPage__KeyTransparency__LearnMore"
/>
</a>
</>
}
moduleClassName="Preferences__checkbox"
name="keyTransparency"
onChange={() =>
onHasKeyTransparencyDisabledChanged(!hasKeyTransparencyDisabled)
}
/>
<div className="Preferences__padding">
<div className="Preferences__description">
{i18n(
'icu:Preferences__PrivacyPage__KeyTransparency__Description'
)}
&ensp;
<a
href={KEY_TRANSPARENCY_URL}
rel="noreferrer"
target="_blank"
className={tw('text-label-primary')}
>
<I18n
i18n={i18n}
id="icu:Preferences__PrivacyPage__KeyTransparency__LearnMore"
/>
</a>
</div>
</div>
</SettingsRow>
)}
)}
</SettingsRow>
<SettingsRow>
<FlowingControl>
<div
+6 -3
View File
@@ -74,6 +74,7 @@ export type PropsType = {
listId: string,
allowsReplies: boolean
) => unknown;
onStoryViewReceiptsChange: (value: boolean) => unknown;
onViewersUpdated: (
listId: string,
viewerServiceIds: Array<ServiceIdString>
@@ -262,6 +263,7 @@ export function StoriesSettingsModal({
onHideMyStoriesFrom,
onRemoveMembers,
onRepliesNReactionsChanged,
onStoryViewReceiptsChange,
onViewersUpdated,
setMyStoriesToAllSignalConnections,
storyViewReceiptsEnabled,
@@ -466,13 +468,14 @@ export function StoriesSettingsModal({
<hr className="StoriesSettingsModal__divider" />
<Checkbox
disabled
checked={storyViewReceiptsEnabled}
description={i18n('icu:StoriesSettings__view-receipts--description')}
description={i18n(
'icu:StoriesSettings__view-receipts--new-description'
)}
label={i18n('icu:StoriesSettings__view-receipts--label')}
moduleClassName="StoriesSettingsModal__checkbox"
name="view-receipts"
onChange={noop}
onChange={onStoryViewReceiptsChange}
/>
<div className="StoriesSettingsModal__stories-off-container">
+1 -1
View File
@@ -1595,7 +1595,7 @@ export async function mergeAccountRecord(
itemStorage.get('postRegistrationSyncsStatus') !== 'incomplete';
if (previous !== preferContactAvatars && postRegistrationSyncsComplete) {
await window.ConversationController.forceRerender();
await window.ConversationController.rerenderAfterAvatarChange();
}
}
+46 -3
View File
@@ -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}
@@ -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}
+1 -1
View File
@@ -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();
+4
View File
@@ -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) => {