mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-09-20 08:45:21 +01:00
Add global default settings for notifying while muted
This commit is contained in:
@@ -6938,6 +6938,22 @@
|
||||
"messageformat": "Calls, mentions, replies",
|
||||
"description": "Summary shown on the 'While muted' row when calls, mentions, and replies all notify while the chat is muted"
|
||||
},
|
||||
"icu:Preferences__WhileMuted__description": {
|
||||
"messageformat": "Choose notifications to show for muted chats.",
|
||||
"description": "Sub-label of the row in notification settings that opens the global 'While muted' page"
|
||||
},
|
||||
"icu:Preferences__WhileMuted__calls__description": {
|
||||
"messageformat": "Ring or notify when a call is started in muted chats.",
|
||||
"description": "On the global 'While muted' settings page, sub-label for the switch that allows call notifications in muted chats"
|
||||
},
|
||||
"icu:Preferences__WhileMuted__mentions__description": {
|
||||
"messageformat": "Notify when you are mentioned in muted chats.",
|
||||
"description": "On the global 'While muted' settings page, sub-label for the switch that allows @mention notifications in muted chats"
|
||||
},
|
||||
"icu:Preferences__WhileMuted__replies__description": {
|
||||
"messageformat": "Notify when someone replies to your message in muted chats.",
|
||||
"description": "On the global 'While muted' settings page, sub-label for the switch that allows reply notifications in muted chats"
|
||||
},
|
||||
"icu:ConversationNotificationsSettings__mentions__label": {
|
||||
"messageformat": "Mentions",
|
||||
"description": "In the conversation notifications settings, this is the label for the mentions option"
|
||||
|
||||
@@ -138,6 +138,9 @@ message AccountData {
|
||||
bool allowSealedSenderFromAnyone = 30;
|
||||
bool allowAutomaticKeyVerification = 31;
|
||||
bool hasSeenAdminDeleteEducationDialog = 32;
|
||||
optional bool notifyForCallsIfMuted = 36; // If unset, consider this disabled
|
||||
optional bool notifyForMentionsIfMuted = 37; // If unset, consider this enabled
|
||||
optional bool notifyForRepliesIfMuted = 38; // If unset, consider this enabled
|
||||
}
|
||||
|
||||
message SubscriberData {
|
||||
@@ -1465,4 +1468,4 @@ message ChatFolder {
|
||||
repeated uint64 includedRecipientIds = 7; // generated recipient id of groups, contacts, and/or note to self
|
||||
repeated uint64 excludedRecipientIds = 8; // generated recipient id of groups, contacts, and/or note to self
|
||||
bytes id = 9; // should be 16 bytes
|
||||
}
|
||||
}
|
||||
@@ -189,7 +189,6 @@ message Payments {
|
||||
}
|
||||
|
||||
message AccountRecord {
|
||||
|
||||
enum PhoneNumberSharingMode {
|
||||
UNKNOWN = 0;
|
||||
EVERYBODY = 1;
|
||||
@@ -313,6 +312,9 @@ message AccountRecord {
|
||||
optional bool releaseNotesChatBlocked = 50;
|
||||
optional bool releaseNotesChatMarkedUnread = 51;
|
||||
optional uint64 releaseNotesChatBlockedAt = 52; // only set if known (>0)
|
||||
OptionalBool notifyForCallsIfMuted = 56; // If unset, consider this off
|
||||
OptionalBool notifyForMentionsIfMuted = 57; // If unset, consider this on
|
||||
OptionalBool notifyForRepliesIfMuted = 58; // If unset, consider this on
|
||||
}
|
||||
|
||||
message StoryDistributionListRecord {
|
||||
|
||||
@@ -1451,6 +1451,21 @@ async function startApp(): Promise<void> {
|
||||
window.ConversationController.repairPinnedConversations();
|
||||
}
|
||||
|
||||
// Existing accounts keep notifying for calls in muted chats; new installs
|
||||
// leave this unset, which means calls will not ring in muted chats.
|
||||
if (
|
||||
window.isBeforeVersion(lastVersion, '8.29.0-alpha') &&
|
||||
itemStorage.get('notifyForCallsIfMuted') == null
|
||||
) {
|
||||
log.info(
|
||||
'Defaulting notifyForCallsIfMuted to true for existing account'
|
||||
);
|
||||
await itemStorage.put('notifyForCallsIfMuted', true);
|
||||
window.ConversationController.getOurConversation()?.captureChange(
|
||||
'notifyForCallsIfMuted'
|
||||
);
|
||||
}
|
||||
|
||||
if (!itemStorage.get('avatarsHaveBeenMigrated', false)) {
|
||||
window.ConversationController.migrateAvatarsForNonAcceptedConversations();
|
||||
}
|
||||
|
||||
@@ -515,6 +515,7 @@ export default {
|
||||
me,
|
||||
navTabsCollapsed: false,
|
||||
notificationContent: 'name',
|
||||
notifyWhileMuted: { calls: false, mentions: true, replies: true },
|
||||
osName: 'windows',
|
||||
otherTabsUnreadStats: {
|
||||
unreadCount: 0,
|
||||
@@ -626,6 +627,7 @@ export default {
|
||||
onNotificationAttentionChange: action('onNotificationAttentionChange'),
|
||||
onNotificationContentChange: action('onNotificationContentChange'),
|
||||
onNotificationsChange: action('onNotificationsChange'),
|
||||
onNotifyWhileMutedChange: action('onNotifyWhileMutedChange'),
|
||||
onPreferContactAvatarsChange: action('onPreferContactAvatarsChange'),
|
||||
onReactionNotificationsChange: action('onReactionNotificationsChange'),
|
||||
onReadReceiptsChange: action('onReadReceiptsChange'),
|
||||
@@ -775,6 +777,10 @@ export const Notifications = Template.bind({});
|
||||
Notifications.args = {
|
||||
settingsLocation: { page: SettingsPage.Notifications },
|
||||
};
|
||||
export const NotificationsWhileMuted = Template.bind({});
|
||||
NotificationsWhileMuted.args = {
|
||||
settingsLocation: { page: SettingsPage.WhileMuted },
|
||||
};
|
||||
export const Privacy = Template.bind({});
|
||||
Privacy.args = {
|
||||
settingsLocation: { page: SettingsPage.Privacy },
|
||||
|
||||
@@ -54,6 +54,11 @@ import type {
|
||||
ZoomFactorType,
|
||||
StorageAccessType,
|
||||
} from '../types/StorageKeys.std.ts';
|
||||
import type {
|
||||
NotifyWhileMuted,
|
||||
NotifyWhileMutedKey,
|
||||
} from '../util/notifyWhileMuted.std.ts';
|
||||
import { getNotifyWhileMutedSummary } from '../util/notifyWhileMuted.std.ts';
|
||||
import type { ThemeSettingType } from '../util/theme.std.ts';
|
||||
import type { AnyToast } from '../types/Toast.dom.tsx';
|
||||
import { ToastType } from '../types/Toast.dom.tsx';
|
||||
@@ -171,6 +176,7 @@ export type PropsDataType = {
|
||||
settingsLocation: SettingsLocation;
|
||||
lastSyncTime?: number;
|
||||
notificationContent: NotificationSettingType;
|
||||
notifyWhileMuted: NotifyWhileMuted;
|
||||
osName: 'linux' | 'macos' | 'windows' | undefined;
|
||||
phoneNumber: string | undefined;
|
||||
selectedCamera?: string;
|
||||
@@ -349,6 +355,10 @@ type PropsFunctionType = {
|
||||
onNotificationAttentionChange: CheckboxChangeHandlerType;
|
||||
onNotificationContentChange: SelectChangeHandlerType<NotificationSettingType>;
|
||||
onNotificationsChange: CheckboxChangeHandlerType;
|
||||
onNotifyWhileMutedChange: (
|
||||
key: NotifyWhileMutedKey,
|
||||
value: boolean
|
||||
) => unknown;
|
||||
onPinRemindersChange: CheckboxChangeHandlerType;
|
||||
onPreferContactAvatarsChange: CheckboxChangeHandlerType;
|
||||
onReactionNotificationsChange: CheckboxChangeHandlerType;
|
||||
@@ -517,6 +527,7 @@ export function Preferences({
|
||||
me,
|
||||
navTabsCollapsed,
|
||||
notificationContent,
|
||||
notifyWhileMuted,
|
||||
onAudioNotificationsChange,
|
||||
onAutoConvertEmojiChange,
|
||||
onAutoDownloadAttachmentChange,
|
||||
@@ -544,6 +555,7 @@ export function Preferences({
|
||||
onNotificationAttentionChange,
|
||||
onNotificationContentChange,
|
||||
onNotificationsChange,
|
||||
onNotifyWhileMutedChange,
|
||||
onPinRemindersChange,
|
||||
onPreferContactAvatarsChange,
|
||||
onReactionNotificationsChange,
|
||||
@@ -1546,6 +1558,16 @@ export function Preferences({
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ClickableItem
|
||||
title={i18n('icu:WhileMuted__title')}
|
||||
value={getNotifyWhileMutedSummary(notifyWhileMuted, i18n)}
|
||||
description={i18n('icu:Preferences__WhileMuted__description')}
|
||||
arrow
|
||||
disabled={!hasNotifications}
|
||||
onClick={() =>
|
||||
setSettingsLocation({ page: SettingsPage.WhileMuted })
|
||||
}
|
||||
/>
|
||||
</List>
|
||||
|
||||
<List
|
||||
@@ -2236,6 +2258,64 @@ export function Preferences({
|
||||
title={pageTitle}
|
||||
/>
|
||||
);
|
||||
} else if (settingsLocation.page === SettingsPage.WhileMuted) {
|
||||
const backButton = (
|
||||
<button
|
||||
aria-label={i18n('icu:goBack')}
|
||||
className="Preferences__back-icon"
|
||||
onClick={() =>
|
||||
setSettingsLocation({ page: SettingsPage.Notifications })
|
||||
}
|
||||
type="button"
|
||||
/>
|
||||
);
|
||||
const pageContents = (
|
||||
<ListGroup>
|
||||
<List>
|
||||
<SwitchItem
|
||||
symbol="phone"
|
||||
title={i18n('icu:WhileMuted__calls__title')}
|
||||
description={i18n(
|
||||
'icu:Preferences__WhileMuted__calls__description'
|
||||
)}
|
||||
checked={notifyWhileMuted.calls}
|
||||
onCheckedChange={checked =>
|
||||
onNotifyWhileMutedChange('calls', checked)
|
||||
}
|
||||
/>
|
||||
<SwitchItem
|
||||
symbol="at"
|
||||
title={i18n('icu:WhileMuted__mentions__title')}
|
||||
description={i18n(
|
||||
'icu:Preferences__WhileMuted__mentions__description'
|
||||
)}
|
||||
checked={notifyWhileMuted.mentions}
|
||||
onCheckedChange={checked =>
|
||||
onNotifyWhileMutedChange('mentions', checked)
|
||||
}
|
||||
/>
|
||||
<SwitchItem
|
||||
symbol="reply"
|
||||
title={i18n('icu:WhileMuted__replies__title')}
|
||||
description={i18n(
|
||||
'icu:Preferences__WhileMuted__replies__description'
|
||||
)}
|
||||
checked={notifyWhileMuted.replies}
|
||||
onCheckedChange={checked =>
|
||||
onNotifyWhileMutedChange('replies', checked)
|
||||
}
|
||||
/>
|
||||
</List>
|
||||
</ListGroup>
|
||||
);
|
||||
content = (
|
||||
<PreferencesContent
|
||||
backButton={backButton}
|
||||
contents={pageContents}
|
||||
contentsRef={settingsPaneRef}
|
||||
title={i18n('icu:WhileMuted__title')}
|
||||
/>
|
||||
);
|
||||
} else if (settingsLocation.page === SettingsPage.NotificationProfilesHome) {
|
||||
content = renderNotificationProfilesHome({
|
||||
setSettingsLocation,
|
||||
@@ -2451,7 +2531,8 @@ export function Preferences({
|
||||
Preferences__button: true,
|
||||
'Preferences__button--notifications': true,
|
||||
'Preferences__button--selected':
|
||||
settingsLocation.page === SettingsPage.Notifications,
|
||||
settingsLocation.page === SettingsPage.Notifications ||
|
||||
settingsLocation.page === SettingsPage.WhileMuted,
|
||||
})}
|
||||
onClick={() =>
|
||||
setSettingsLocation({ page: SettingsPage.Notifications })
|
||||
@@ -2625,6 +2706,7 @@ function List(props: ListProps): ReactNode {
|
||||
}
|
||||
|
||||
type SwitchItemProps = Readonly<{
|
||||
symbol?: AxoSymbol.Name;
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
disabled?: boolean;
|
||||
@@ -2635,6 +2717,7 @@ type SwitchItemProps = Readonly<{
|
||||
function SwitchItem(props: SwitchItemProps): ReactNode {
|
||||
return (
|
||||
<AxoItem.Root>
|
||||
{props.symbol != null && <AxoItem.Icon symbol={props.symbol} />}
|
||||
<AxoItem.Content>
|
||||
<AxoItem.Body>
|
||||
<AxoItem.Title>{props.title}</AxoItem.Title>
|
||||
|
||||
+1
-2
@@ -8,7 +8,6 @@ import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './ConversationNotificationsSettings.dom.tsx';
|
||||
import { ConversationNotificationsSettings } from './ConversationNotificationsSettings.dom.tsx';
|
||||
import { MuteExpiration } from '@signalapp/types';
|
||||
import { DEFAULT_NOTIFY_WHILE_MUTED } from '../../../util/notifyWhileMuted.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@@ -21,7 +20,7 @@ const getCommonProps = () => ({
|
||||
id: 'conversation-id',
|
||||
isGroup: true,
|
||||
muteExpiresAt: undefined,
|
||||
notifyWhileMuted: DEFAULT_NOTIFY_WHILE_MUTED,
|
||||
notifyWhileMuted: { calls: false, mentions: true, replies: true },
|
||||
i18n,
|
||||
onOpenWhileMutedSettings: action('onOpenWhileMutedSettings'),
|
||||
setMuteExpiration: action('setMuteExpiration'),
|
||||
|
||||
+12
-39
@@ -13,6 +13,7 @@ import { isConversationMuted } from '../../../util/isConversationMuted.std.ts';
|
||||
import { getMutedUntilText } from '../../../util/getMutedUntilText.std.ts';
|
||||
import { getConversationMuteMenu } from '../../../util/getMuteOptions.std.ts';
|
||||
import type { NotifyWhileMuted } from '../../../util/notifyWhileMuted.std.ts';
|
||||
import { getNotifyWhileMutedSummary } from '../../../util/notifyWhileMuted.std.ts';
|
||||
|
||||
export type PropsType = {
|
||||
id: string;
|
||||
@@ -43,6 +44,16 @@ export function ConversationNotificationsSettings({
|
||||
? getMutedUntilText(muteExpiresAt, i18n)
|
||||
: null;
|
||||
|
||||
const whileMutedSummary = useMemo(() => {
|
||||
// Mentions and replies only apply to groups, and the "While muted" panel
|
||||
// hides those rows for 1:1 chats, so don't summarize them here either.
|
||||
const summarized = isGroup
|
||||
? notifyWhileMuted
|
||||
: { ...notifyWhileMuted, mentions: false, replies: false };
|
||||
|
||||
return getNotifyWhileMutedSummary(summarized, i18n);
|
||||
}, [i18n, isGroup, notifyWhileMuted]);
|
||||
|
||||
const muteMenu = useMemo(
|
||||
() => getConversationMuteMenu(muteExpiresAt, i18n),
|
||||
[i18n, muteExpiresAt]
|
||||
@@ -107,9 +118,7 @@ export function ConversationNotificationsSettings({
|
||||
<AxoItem.Title id={whileMutedTitleId}>
|
||||
{i18n('icu:WhileMuted__title')}
|
||||
</AxoItem.Title>
|
||||
<AxoItem.Value>
|
||||
{getNotifyWhileMutedText(notifyWhileMuted, isGroup, i18n)}
|
||||
</AxoItem.Value>
|
||||
<AxoItem.Value>{whileMutedSummary}</AxoItem.Value>
|
||||
<AxoItem.Description>
|
||||
{i18n('icu:WhileMuted__description')}
|
||||
</AxoItem.Description>
|
||||
@@ -127,39 +136,3 @@ export function ConversationNotificationsSettings({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getNotifyWhileMutedText(
|
||||
notifyWhileMuted: NotifyWhileMuted,
|
||||
isGroup: boolean,
|
||||
i18n: LocalizerType
|
||||
): string {
|
||||
const { calls } = notifyWhileMuted;
|
||||
// Mentions and replies only apply to groups, and the "While muted" panel
|
||||
// hides those rows for 1:1 chats, so don't summarize them here either.
|
||||
const mentions = isGroup && notifyWhileMuted.mentions;
|
||||
const replies = isGroup && notifyWhileMuted.replies;
|
||||
|
||||
if (calls && mentions && replies) {
|
||||
return i18n('icu:WhileMuted__value--calls-mentions-replies');
|
||||
}
|
||||
if (calls && mentions) {
|
||||
return i18n('icu:WhileMuted__value--calls-mentions');
|
||||
}
|
||||
if (calls && replies) {
|
||||
return i18n('icu:WhileMuted__value--calls-replies');
|
||||
}
|
||||
if (mentions && replies) {
|
||||
return i18n('icu:WhileMuted__value--mentions-replies');
|
||||
}
|
||||
if (calls) {
|
||||
return i18n('icu:WhileMuted__value--calls');
|
||||
}
|
||||
if (mentions) {
|
||||
return i18n('icu:WhileMuted__value--mentions');
|
||||
}
|
||||
if (replies) {
|
||||
return i18n('icu:WhileMuted__value--replies');
|
||||
}
|
||||
|
||||
return i18n('icu:WhileMuted__value--none');
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { action } from '@storybook/addon-actions';
|
||||
import type { Meta } from '@storybook/react';
|
||||
import type { PropsType } from './WhileMutedSettings.dom.tsx';
|
||||
import { WhileMutedSettings } from './WhileMutedSettings.dom.tsx';
|
||||
import { DEFAULT_NOTIFY_WHILE_MUTED } from '../../../util/notifyWhileMuted.std.ts';
|
||||
|
||||
const { i18n } = window.SignalContext;
|
||||
|
||||
@@ -18,7 +17,7 @@ export default {
|
||||
const getCommonProps = () => ({
|
||||
i18n,
|
||||
isGroup: true,
|
||||
notifyWhileMuted: DEFAULT_NOTIFY_WHILE_MUTED,
|
||||
notifyWhileMuted: { calls: false, mentions: true, replies: true },
|
||||
setNotifyWhileMuted: action('setNotifyWhileMuted'),
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import { getActiveProfile } from '../state/selectors/notificationProfiles.dom.ts
|
||||
import { shouldNotify as shouldNotifyDuringNotificationProfile } from '../types/NotificationProfile.std.ts';
|
||||
import { NotificationType } from '../types/notifications.std.ts';
|
||||
import { isMessageUnread } from '../util/isMessageUnread.std.ts';
|
||||
import { getNotifyWhileMuted } from '../util/notifyWhileMuted.std.ts';
|
||||
import { getNotifyWhileMutedForConversation } from '../util/notifyWhileMuted.preload.ts';
|
||||
import { isDirectConversation } from '../util/whatTypeOfConversation.dom.ts';
|
||||
import { isExpiringMessage } from '../types/Message2.preload.ts';
|
||||
import { notificationService } from '../services/notifications.preload.ts';
|
||||
@@ -299,7 +299,9 @@ function isAllowedByConversation(args: MaybeNotifyArgs): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
const notifyWhileMuted = getNotifyWhileMuted(conversation.attributes);
|
||||
const notifyWhileMuted = getNotifyWhileMutedForConversation(
|
||||
conversation.attributes
|
||||
);
|
||||
|
||||
if (notifyWhileMuted.mentions && isMention(args)) {
|
||||
return true;
|
||||
|
||||
Vendored
+1
-1
@@ -413,7 +413,7 @@ export type ConversationAttributesType = {
|
||||
messageRequestResponseType?: number;
|
||||
messagesDeleted?: boolean;
|
||||
muteExpiresAt?: MuteExpiration;
|
||||
// Ternary: undefined means unset, see DEFAULT_NOTIFY_WHILE_MUTED
|
||||
// Ternary: undefined means unset, so the chat follows the global setting
|
||||
notifyForCallsIfMuted?: boolean;
|
||||
notifyForMentionsIfMuted?: boolean;
|
||||
notifyForRepliesIfMuted?: boolean;
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import { drop } from '../util/drop.std.ts';
|
||||
import { isShallowEqual } from '../util/isShallowEqual.std.ts';
|
||||
import type { NotifyWhileMutedKey } from '../util/notifyWhileMuted.std.ts';
|
||||
import { NOTIFY_WHILE_MUTED_FIELDS } from '../util/notifyWhileMuted.std.ts';
|
||||
import { getInitials } from '../util/getInitials.std.ts';
|
||||
import { clearTimeoutIfNecessary } from '../util/clearTimeoutIfNecessary.std.ts';
|
||||
import { getMessageSentTimestamp } from '../util/getMessageSentTimestamp.std.ts';
|
||||
@@ -6112,20 +6113,7 @@ export class ConversationModel {
|
||||
}
|
||||
|
||||
setNotifyWhileMuted(key: NotifyWhileMutedKey, newValue: boolean): void {
|
||||
let attributeName: keyof ConversationAttributesType;
|
||||
switch (key) {
|
||||
case 'calls':
|
||||
attributeName = 'notifyForCallsIfMuted';
|
||||
break;
|
||||
case 'mentions':
|
||||
attributeName = 'notifyForMentionsIfMuted';
|
||||
break;
|
||||
case 'replies':
|
||||
attributeName = 'notifyForRepliesIfMuted';
|
||||
break;
|
||||
default:
|
||||
throw missingCaseError(key);
|
||||
}
|
||||
const attributeName = NOTIFY_WHILE_MUTED_FIELDS[key];
|
||||
|
||||
if (this.get(attributeName) === newValue) {
|
||||
return;
|
||||
|
||||
@@ -1080,6 +1080,11 @@ export class BackupExportStream extends Readable {
|
||||
itemStorage.get('displayBadgesOnProfile') ?? null,
|
||||
keepMutedChatsArchived:
|
||||
itemStorage.get('keepMutedChatsArchived') ?? null,
|
||||
notifyForCallsIfMuted: itemStorage.get('notifyForCallsIfMuted') ?? null,
|
||||
notifyForMentionsIfMuted:
|
||||
itemStorage.get('notifyForMentionsIfMuted') ?? null,
|
||||
notifyForRepliesIfMuted:
|
||||
itemStorage.get('notifyForRepliesIfMuted') ?? null,
|
||||
hasSetMyStoriesPrivacy:
|
||||
itemStorage.get('hasSetMyStoriesPrivacy') ?? null,
|
||||
hasViewedOnboardingStory:
|
||||
|
||||
@@ -890,6 +890,19 @@ export class BackupImportStream extends Writable {
|
||||
'keepMutedChatsArchived',
|
||||
accountSettings?.keepMutedChatsArchived === true
|
||||
);
|
||||
|
||||
await itemStorage.put(
|
||||
'notifyForCallsIfMuted',
|
||||
accountSettings?.notifyForCallsIfMuted ?? undefined
|
||||
);
|
||||
await itemStorage.put(
|
||||
'notifyForMentionsIfMuted',
|
||||
accountSettings?.notifyForMentionsIfMuted ?? undefined
|
||||
);
|
||||
await itemStorage.put(
|
||||
'notifyForRepliesIfMuted',
|
||||
accountSettings?.notifyForRepliesIfMuted ?? undefined
|
||||
);
|
||||
await itemStorage.put(
|
||||
'hasSetMyStoriesPrivacy',
|
||||
accountSettings?.hasSetMyStoriesPrivacy === true
|
||||
|
||||
@@ -110,7 +110,7 @@ import {
|
||||
makeSfuRequest,
|
||||
} from '../textsecure/WebAPI.preload.ts';
|
||||
import { missingCaseError } from '../util/missingCaseError.std.ts';
|
||||
import { getNotifyWhileMuted } from '../util/notifyWhileMuted.std.ts';
|
||||
import { getNotifyWhileMutedForConversation } from '../util/notifyWhileMuted.preload.ts';
|
||||
import { normalizeGroupCallTimestamp } from '../util/ringrtc/normalizeGroupCallTimestamp.std.ts';
|
||||
import { requestCameraPermissions } from '../util/callingPermissions.dom.ts';
|
||||
import {
|
||||
@@ -3492,7 +3492,7 @@ class CallingClass {
|
||||
RingRTC.cancelGroupRing(groupIdBytes, ringId, RingCancelReason.Busy);
|
||||
} else if (
|
||||
conversation.isMuted() &&
|
||||
!getNotifyWhileMuted(conversation.attributes).calls
|
||||
!getNotifyWhileMutedForConversation(conversation.attributes).calls
|
||||
) {
|
||||
log.info(
|
||||
`${logId}: not notifying for calls while muted. Ignoring ring request`
|
||||
@@ -3633,7 +3633,7 @@ class CallingClass {
|
||||
|
||||
if (
|
||||
conversation.isMuted() &&
|
||||
!getNotifyWhileMuted(conversation.attributes).calls
|
||||
!getNotifyWhileMutedForConversation(conversation.attributes).calls
|
||||
) {
|
||||
log.info(`${logId}: not notifying for calls while muted, ignoring`);
|
||||
|
||||
@@ -4387,7 +4387,7 @@ class CallingClass {
|
||||
|
||||
const isAllowedWhileMuted =
|
||||
!conversation.isMuted() ||
|
||||
getNotifyWhileMuted(conversation.attributes).calls;
|
||||
getNotifyWhileMutedForConversation(conversation.attributes).calls;
|
||||
|
||||
if (
|
||||
isNewCall &&
|
||||
|
||||
@@ -638,6 +638,16 @@ export function toAccountRecord({
|
||||
displayBadgesOnProfile: itemStorage.get('displayBadgesOnProfile') ?? null,
|
||||
keepMutedChatsArchived: itemStorage.get('keepMutedChatsArchived') ?? null,
|
||||
|
||||
notifyForCallsIfMuted: toOptionalBool(
|
||||
itemStorage.get('notifyForCallsIfMuted')
|
||||
),
|
||||
notifyForMentionsIfMuted: toOptionalBool(
|
||||
itemStorage.get('notifyForMentionsIfMuted')
|
||||
),
|
||||
notifyForRepliesIfMuted: toOptionalBool(
|
||||
itemStorage.get('notifyForRepliesIfMuted')
|
||||
),
|
||||
|
||||
hasSetMyStoriesPrivacy: itemStorage.get('hasSetMyStoriesPrivacy') ?? null,
|
||||
hasViewedOnboardingStory:
|
||||
itemStorage.get('hasViewedOnboardingStory') ?? null,
|
||||
@@ -1666,6 +1676,9 @@ export async function mergeAccountRecord(
|
||||
backupTier,
|
||||
displayBadgesOnProfile,
|
||||
keepMutedChatsArchived,
|
||||
notifyForCallsIfMuted,
|
||||
notifyForMentionsIfMuted,
|
||||
notifyForRepliesIfMuted,
|
||||
hasCompletedUsernameOnboarding,
|
||||
hasSeenGroupStoryEducationSheet,
|
||||
hasSeenAdminDeleteEducationDialog,
|
||||
@@ -1944,6 +1957,18 @@ export async function mergeAccountRecord(
|
||||
|
||||
await itemStorage.put('displayBadgesOnProfile', displayBadgesOnProfile);
|
||||
await itemStorage.put('keepMutedChatsArchived', keepMutedChatsArchived);
|
||||
await itemStorage.put(
|
||||
'notifyForCallsIfMuted',
|
||||
fromOptionalBool(notifyForCallsIfMuted) ?? undefined
|
||||
);
|
||||
await itemStorage.put(
|
||||
'notifyForMentionsIfMuted',
|
||||
fromOptionalBool(notifyForMentionsIfMuted) ?? undefined
|
||||
);
|
||||
await itemStorage.put(
|
||||
'notifyForRepliesIfMuted',
|
||||
fromOptionalBool(notifyForRepliesIfMuted) ?? undefined
|
||||
);
|
||||
await itemStorage.put('hasSetMyStoriesPrivacy', hasSetMyStoriesPrivacy);
|
||||
{
|
||||
await itemStorage.put('hasViewedOnboardingStory', hasViewedOnboardingStory);
|
||||
|
||||
@@ -14,6 +14,8 @@ import type {
|
||||
import type { AciString } from '../../types/ServiceId.std.ts';
|
||||
import { DEFAULT_CONVERSATION_COLOR } from '../../types/Colors.std.ts';
|
||||
import { getPreferredReactionEmoji as getPreferredReactionEmojiFromStoredValue } from '../../reactions/preferredReactionEmoji.std.ts';
|
||||
import type { NotifyWhileMuted } from '../../util/notifyWhileMuted.std.ts';
|
||||
import { DEFAULT_NOTIFY_IF_MUTED } from '../../util/notifyWhileMuted.std.ts';
|
||||
import { DurationInSeconds } from '../../util/durations/index.std.ts';
|
||||
import * as Bytes from '../../Bytes.std.ts';
|
||||
import { contactByEncryptedUsernameRoute } from '../../util/signalRoutes.std.ts';
|
||||
@@ -217,6 +219,16 @@ export const getBadgeCountMutedConversations = createSelector(
|
||||
}
|
||||
);
|
||||
|
||||
export const getGlobalNotifyWhileMuted = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): NotifyWhileMuted => ({
|
||||
calls: state.notifyForCallsIfMuted ?? DEFAULT_NOTIFY_IF_MUTED.calls,
|
||||
mentions:
|
||||
state.notifyForMentionsIfMuted ?? DEFAULT_NOTIFY_IF_MUTED.mentions,
|
||||
replies: state.notifyForRepliesIfMuted ?? DEFAULT_NOTIFY_IF_MUTED.replies,
|
||||
})
|
||||
);
|
||||
|
||||
export const getTextFormattingEnabled = createSelector(
|
||||
getItems,
|
||||
(state: ItemsStateType): boolean => state.textFormatting ?? true
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useConversationsActions } from '../ducks/conversations.preload.ts';
|
||||
import { useNavActions } from '../ducks/nav.std.ts';
|
||||
import { PanelType } from '../../types/Panels.std.ts';
|
||||
import { getNotifyWhileMuted } from '../../util/notifyWhileMuted.std.ts';
|
||||
import { getGlobalNotifyWhileMuted } from '../selectors/items.dom.ts';
|
||||
|
||||
export type SmartConversationNotificationsSettingsProps = {
|
||||
conversationId: string;
|
||||
@@ -24,13 +25,14 @@ export const SmartConversationNotificationsSettings = memo(
|
||||
const conversationSelector = useSelector(getConversationByIdSelector);
|
||||
const { setMuteExpiration } = useConversationsActions();
|
||||
const { pushPanelForConversation } = useNavActions();
|
||||
const globalNotifyWhileMuted = useSelector(getGlobalNotifyWhileMuted);
|
||||
const conversation = conversationSelector(conversationId);
|
||||
strictAssert(conversation, 'Expected a conversation to be found');
|
||||
const { muteExpiresAt, type: conversationType } = conversation;
|
||||
|
||||
const notifyWhileMuted = useMemo(
|
||||
() => getNotifyWhileMuted(conversation),
|
||||
[conversation]
|
||||
() => getNotifyWhileMuted(conversation, globalNotifyWhileMuted),
|
||||
[conversation, globalNotifyWhileMuted]
|
||||
);
|
||||
|
||||
const handleOpenWhileMutedSettings = useCallback(() => {
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
import {
|
||||
getBackupKey,
|
||||
getCustomColors,
|
||||
getGlobalNotifyWhileMuted,
|
||||
getItems,
|
||||
getNavTabsCollapsed,
|
||||
getPreferredLeftPaneWidth,
|
||||
@@ -113,6 +114,8 @@ import {
|
||||
|
||||
import type { SettingsLocation } from '../../types/Nav.std.ts';
|
||||
import type { StorageAccessType } from '../../types/Storage.d.ts';
|
||||
import type { NotifyWhileMutedKey } from '../../util/notifyWhileMuted.std.ts';
|
||||
import { NOTIFY_WHILE_MUTED_FIELDS } from '../../util/notifyWhileMuted.std.ts';
|
||||
import type { ThemeType } from '../../util/preload.preload.ts';
|
||||
import type { WidthBreakpoint } from '../../components/_util.std.ts';
|
||||
import type { StateType } from '../reducer.preload.ts';
|
||||
@@ -774,6 +777,17 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
);
|
||||
};
|
||||
|
||||
const notifyWhileMuted = useSelector(getGlobalNotifyWhileMuted);
|
||||
const onNotifyWhileMutedChange = (
|
||||
key: NotifyWhileMutedKey,
|
||||
value: boolean
|
||||
) => {
|
||||
const itemKey = NOTIFY_WHILE_MUTED_FIELDS[key];
|
||||
putItem(itemKey, value);
|
||||
const account = window.ConversationController.getOurConversationOrThrow();
|
||||
account.captureChange(itemKey);
|
||||
};
|
||||
|
||||
const [hasPinReminders, onPinRemindersChange] = createItemsAccess(
|
||||
'pinReminders',
|
||||
true,
|
||||
@@ -1029,6 +1043,7 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
me={me}
|
||||
navTabsCollapsed={navTabsCollapsed}
|
||||
notificationContent={notificationContent}
|
||||
notifyWhileMuted={notifyWhileMuted}
|
||||
onAudioNotificationsChange={onAudioNotificationsChange}
|
||||
onAutoConvertEmojiChange={onAutoConvertEmojiChange}
|
||||
onAutoDownloadAttachmentChange={onAutoDownloadAttachmentChange}
|
||||
@@ -1061,6 +1076,7 @@ export function SmartPreferences(): JSX.Element | null {
|
||||
onNotificationAttentionChange={onNotificationAttentionChange}
|
||||
onNotificationContentChange={onNotificationContentChange}
|
||||
onNotificationsChange={onNotificationsChange}
|
||||
onNotifyWhileMutedChange={onNotifyWhileMutedChange}
|
||||
onStartUpdate={startUpdate}
|
||||
onPreferContactAvatarsChange={onPreferContactAvatarsChange}
|
||||
onReactionNotificationsChange={onReactionNotificationsChange}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { memo, useCallback, useMemo } from 'react';
|
||||
import { WhileMutedSettings } from '../../components/conversation/conversation-details/WhileMutedSettings.dom.tsx';
|
||||
import { getIntl } from '../selectors/user.std.ts';
|
||||
import { getConversationByIdSelector } from '../selectors/conversations.dom.ts';
|
||||
import { getGlobalNotifyWhileMuted } from '../selectors/items.dom.ts';
|
||||
import { strictAssert } from '../../util/assert.std.ts';
|
||||
import { useConversationsActions } from '../ducks/conversations.preload.ts';
|
||||
import {
|
||||
@@ -23,12 +24,13 @@ export const SmartWhileMutedSettings = memo(function SmartWhileMutedSettings({
|
||||
const i18n = useSelector(getIntl);
|
||||
const conversationSelector = useSelector(getConversationByIdSelector);
|
||||
const { setNotifyWhileMuted } = useConversationsActions();
|
||||
const globalNotifyWhileMuted = useSelector(getGlobalNotifyWhileMuted);
|
||||
const conversation = conversationSelector(conversationId);
|
||||
strictAssert(conversation, 'Expected a conversation to be found');
|
||||
|
||||
const notifyWhileMuted = useMemo(
|
||||
() => getNotifyWhileMuted(conversation),
|
||||
[conversation]
|
||||
() => getNotifyWhileMuted(conversation, globalNotifyWhileMuted),
|
||||
[conversation, globalNotifyWhileMuted]
|
||||
);
|
||||
const setNotifyWhileMutedForConversation = useCallback(
|
||||
(key: NotifyWhileMutedKey, value: boolean) =>
|
||||
|
||||
@@ -126,6 +126,9 @@ function* createRecords({
|
||||
preferredReactionEmoji: [],
|
||||
displayBadgesOnProfile: true,
|
||||
keepMutedChatsArchived: false,
|
||||
notifyForCallsIfMuted: null,
|
||||
notifyForMentionsIfMuted: null,
|
||||
notifyForRepliesIfMuted: null,
|
||||
hasSetMyStoriesPrivacy: true,
|
||||
hasViewedOnboardingStory: true,
|
||||
storiesDisabled: false,
|
||||
|
||||
@@ -9,37 +9,31 @@ import {
|
||||
} from '../../util/notifyWhileMuted.std.ts';
|
||||
|
||||
describe('getNotifyWhileMuted', () => {
|
||||
it('falls back to the defaults when nothing is set', () => {
|
||||
assert.deepEqual(getNotifyWhileMuted({}), {
|
||||
calls: true,
|
||||
mentions: true,
|
||||
replies: true,
|
||||
});
|
||||
const GLOBAL = { calls: true, mentions: true, replies: true };
|
||||
|
||||
it('follows the global setting when the chat has no settings', () => {
|
||||
assert.deepEqual(getNotifyWhileMuted({}, GLOBAL), GLOBAL);
|
||||
});
|
||||
|
||||
it('honors each setting once it is set', () => {
|
||||
it('lets the chat override the global setting', () => {
|
||||
assert.deepEqual(
|
||||
getNotifyWhileMuted({
|
||||
notifyForCallsIfMuted: true,
|
||||
notifyForMentionsIfMuted: false,
|
||||
notifyForRepliesIfMuted: false,
|
||||
}),
|
||||
{ calls: true, mentions: false, replies: false }
|
||||
getNotifyWhileMuted(
|
||||
{
|
||||
notifyForCallsIfMuted: false,
|
||||
notifyForMentionsIfMuted: false,
|
||||
notifyForRepliesIfMuted: false,
|
||||
},
|
||||
GLOBAL
|
||||
),
|
||||
{ calls: false, mentions: false, replies: false }
|
||||
);
|
||||
});
|
||||
|
||||
it('defaults each setting independently', () => {
|
||||
assert.deepEqual(getNotifyWhileMuted({ notifyForMentionsIfMuted: false }), {
|
||||
calls: true,
|
||||
mentions: false,
|
||||
replies: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(getNotifyWhileMuted({ notifyForCallsIfMuted: true }), {
|
||||
calls: true,
|
||||
mentions: true,
|
||||
replies: true,
|
||||
});
|
||||
it('mixes chat overrides with the global setting', () => {
|
||||
assert.deepEqual(
|
||||
getNotifyWhileMuted({ notifyForCallsIfMuted: false }, GLOBAL),
|
||||
{ calls: false, mentions: true, replies: true }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ export enum SettingsPage {
|
||||
EditChatFolder = 'EditChatFolder',
|
||||
NotificationProfilesHome = 'NotificationProfilesHome',
|
||||
NotificationProfilesCreateFlow = 'NotificationProfilesCreateFlow',
|
||||
WhileMuted = 'WhileMuted',
|
||||
PNP = 'PNP',
|
||||
BackupsDetails = 'BackupsDetails',
|
||||
LocalBackups = 'LocalBackups',
|
||||
|
||||
@@ -215,6 +215,9 @@ export type StorageAccessType = {
|
||||
backupsSubscriberOriginalTransactionId: string;
|
||||
displayBadgesOnProfile: boolean;
|
||||
keepMutedChatsArchived: boolean;
|
||||
notifyForCallsIfMuted: boolean | undefined;
|
||||
notifyForMentionsIfMuted: boolean | undefined;
|
||||
notifyForRepliesIfMuted: boolean | undefined;
|
||||
usernameLastIntegrityCheck: number;
|
||||
usernameCorrupted: boolean;
|
||||
usernameLinkCorrupted: boolean;
|
||||
@@ -429,6 +432,9 @@ export const STORAGE_KEYS_TO_PRESERVE_AFTER_UNLINK = [
|
||||
'universalExpireTimer',
|
||||
'displayBadgesOnProfile',
|
||||
'keepMutedChatsArchived',
|
||||
'notifyForCallsIfMuted',
|
||||
'notifyForMentionsIfMuted',
|
||||
'notifyForRepliesIfMuted',
|
||||
'hasSetMyStoriesPrivacy',
|
||||
'hasViewedOnboardingStory',
|
||||
'hasKeyTransparencyDisabled',
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationAttributesType } from '../model-types.d.ts';
|
||||
import { itemStorage } from '../textsecure/Storage.preload.ts';
|
||||
import type { NotifyWhileMuted } from './notifyWhileMuted.std.ts';
|
||||
import {
|
||||
DEFAULT_NOTIFY_IF_MUTED,
|
||||
getNotifyWhileMuted,
|
||||
} from './notifyWhileMuted.std.ts';
|
||||
|
||||
function getGlobalNotifyWhileMutedFromStorage(): NotifyWhileMuted {
|
||||
return {
|
||||
calls: itemStorage.get(
|
||||
'notifyForCallsIfMuted',
|
||||
DEFAULT_NOTIFY_IF_MUTED.calls
|
||||
),
|
||||
mentions: itemStorage.get(
|
||||
'notifyForMentionsIfMuted',
|
||||
DEFAULT_NOTIFY_IF_MUTED.mentions
|
||||
),
|
||||
replies: itemStorage.get(
|
||||
'notifyForRepliesIfMuted',
|
||||
DEFAULT_NOTIFY_IF_MUTED.replies
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotifyWhileMutedForConversation(
|
||||
attributes: ConversationAttributesType
|
||||
): NotifyWhileMuted {
|
||||
return getNotifyWhileMuted(
|
||||
attributes,
|
||||
getGlobalNotifyWhileMutedFromStorage()
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { ConversationAttributesType } from '../model-types.d.ts';
|
||||
import type { LocalizerType } from '../types/Util.std.ts';
|
||||
|
||||
/**
|
||||
* Which notifications still come through while a chat is muted.
|
||||
@@ -14,20 +14,23 @@ export type NotifyWhileMuted = Readonly<{
|
||||
|
||||
export type NotifyWhileMutedKey = keyof NotifyWhileMuted;
|
||||
|
||||
export const DEFAULT_NOTIFY_WHILE_MUTED: NotifyWhileMuted = {
|
||||
calls: true,
|
||||
export const DEFAULT_NOTIFY_IF_MUTED: NotifyWhileMuted = {
|
||||
calls: false,
|
||||
mentions: true,
|
||||
replies: true,
|
||||
};
|
||||
|
||||
type NotifyWhileMutedFields = Readonly<
|
||||
Pick<
|
||||
ConversationAttributesType,
|
||||
| 'notifyForCallsIfMuted'
|
||||
| 'notifyForMentionsIfMuted'
|
||||
| 'notifyForRepliesIfMuted'
|
||||
>
|
||||
>;
|
||||
export type NotifyWhileMutedFields = Readonly<{
|
||||
notifyForCallsIfMuted?: boolean | undefined;
|
||||
notifyForMentionsIfMuted?: boolean | undefined;
|
||||
notifyForRepliesIfMuted?: boolean | undefined;
|
||||
}>;
|
||||
|
||||
export const NOTIFY_WHILE_MUTED_FIELDS = {
|
||||
calls: 'notifyForCallsIfMuted',
|
||||
mentions: 'notifyForMentionsIfMuted',
|
||||
replies: 'notifyForRepliesIfMuted',
|
||||
} as const satisfies Record<NotifyWhileMutedKey, keyof NotifyWhileMutedFields>;
|
||||
|
||||
/**
|
||||
* Reconciles an incoming record's deprecated `dontNotifyForMentionsIfMuted`
|
||||
@@ -55,13 +58,43 @@ export function resolveLegacyNotifyForMentionsIfMuted(
|
||||
}
|
||||
|
||||
export function getNotifyWhileMuted(
|
||||
fields: NotifyWhileMutedFields
|
||||
conversation: NotifyWhileMutedFields,
|
||||
globalNotifyWhileMuted: NotifyWhileMuted
|
||||
): NotifyWhileMuted {
|
||||
return {
|
||||
calls: fields.notifyForCallsIfMuted ?? DEFAULT_NOTIFY_WHILE_MUTED.calls,
|
||||
calls: conversation.notifyForCallsIfMuted ?? globalNotifyWhileMuted.calls,
|
||||
mentions:
|
||||
fields.notifyForMentionsIfMuted ?? DEFAULT_NOTIFY_WHILE_MUTED.mentions,
|
||||
conversation.notifyForMentionsIfMuted ?? globalNotifyWhileMuted.mentions,
|
||||
replies:
|
||||
fields.notifyForRepliesIfMuted ?? DEFAULT_NOTIFY_WHILE_MUTED.replies,
|
||||
conversation.notifyForRepliesIfMuted ?? globalNotifyWhileMuted.replies,
|
||||
};
|
||||
}
|
||||
|
||||
export function getNotifyWhileMutedSummary(
|
||||
{ calls, mentions, replies }: NotifyWhileMuted,
|
||||
i18n: LocalizerType
|
||||
): string {
|
||||
if (calls && mentions && replies) {
|
||||
return i18n('icu:WhileMuted__value--calls-mentions-replies');
|
||||
}
|
||||
if (calls && mentions) {
|
||||
return i18n('icu:WhileMuted__value--calls-mentions');
|
||||
}
|
||||
if (calls && replies) {
|
||||
return i18n('icu:WhileMuted__value--calls-replies');
|
||||
}
|
||||
if (mentions && replies) {
|
||||
return i18n('icu:WhileMuted__value--mentions-replies');
|
||||
}
|
||||
if (calls) {
|
||||
return i18n('icu:WhileMuted__value--calls');
|
||||
}
|
||||
if (mentions) {
|
||||
return i18n('icu:WhileMuted__value--mentions');
|
||||
}
|
||||
if (replies) {
|
||||
return i18n('icu:WhileMuted__value--replies');
|
||||
}
|
||||
|
||||
return i18n('icu:WhileMuted__value--none');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user