From 93d52f7831d428f52387cefe99803ffd0ea5a1ad Mon Sep 17 00:00:00 2001 From: Michelle Tang Date: Tue, 18 Aug 2026 18:52:13 -0400 Subject: [PATCH] Support reaction and while muted notifications. --- .../securesms/calls/log/CallLogAdapter.kt | 4 +- .../settings/app/AppSettingsActivity.kt | 1 + .../settings/app/labs/LabsSettingsEvents.kt | 1 - .../settings/app/labs/LabsSettingsFragment.kt | 9 - .../settings/app/labs/LabsSettingsState.kt | 1 - .../app/labs/LabsSettingsViewModel.kt | 5 - .../GlobalMutedNotificationsFragment.kt | 43 ++++ .../notifications/MutedNotificationsEvent.kt | 10 + .../notifications/MutedNotificationsScreen.kt | 84 ++++++++ .../MutedNotificationsViewModel.kt | 105 ++++++++++ .../NotificationsSettingsFragment.kt | 151 +++++++++----- .../NotificationsSettingsState.kt | 3 +- .../NotificationsSettingsViewModel.kt | 8 +- .../settings/app/routes/AppSettingsRoute.kt | 1 + .../ConversationSettingsAction.kt | 2 +- .../ConversationSettingsFragment.kt | 7 +- .../ConversationSettingsRepository.kt | 4 - .../conversation/group/GroupSettingsScreen.kt | 1 - .../conversation/group/GroupSettingsState.kt | 1 - .../group/GroupSettingsViewModel.kt | 3 +- .../individual/IndividualSettingsScreen.kt | 1 - .../individual/IndividualSettingsState.kt | 1 - .../individual/IndividualSettingsViewModel.kt | 3 +- .../individual/ReleaseNotesSettingsScreen.kt | 1 - .../conversation/shared/CommonSettingsRows.kt | 6 +- .../sounds/MutedNotificationsFragment.kt | 47 +++++ .../sounds/SoundsAndNotificationsEvent.kt | 5 + .../SoundsAndNotificationsSettingsFragment.kt | 136 ++++-------- ...SoundsAndNotificationsSettingsFragment2.kt | 59 ------ ...oundsAndNotificationsSettingsRepository.kt | 46 ----- .../SoundsAndNotificationsSettingsScreen.kt | 193 ++++-------------- .../SoundsAndNotificationsSettingsState.kt | 13 +- .../SoundsAndNotificationsSettingsState2.kt | 23 --- ...SoundsAndNotificationsSettingsViewModel.kt | 96 ++++++--- ...oundsAndNotificationsSettingsViewModel2.kt | 102 --------- .../securesms/database/RecipientTable.kt | 25 ++- .../dialogs/GroupMentionSettingDialog.java | 83 -------- .../securesms/keyvalue/LabsValues.kt | 2 - .../securesms/keyvalue/SettingsValues.java | 36 ++++ .../v2/DefaultMessageNotifier.kt | 7 + .../notifications/v2/NotificationState.kt | 9 +- .../v2/NotificationStateProvider.kt | 14 +- .../securesms/recipients/Recipient.kt | 19 +- .../securesms/recipients/RecipientCreator.kt | 2 +- .../res/drawable/symbol_bell_badge_24.xml | 9 + .../main/res/layout/call_log_adapter_item.xml | 21 +- .../layout/group_mention_setting_dialog.xml | 52 ----- .../app_settings_with_change_number.xml | 12 ++ .../res/navigation/conversation_settings.xml | 41 ++-- app/src/main/res/values/strings.xml | 64 ++++-- .../group/GroupSettingsViewModelTest.kt | 1 - .../IndividualSettingsViewModelTest.kt | 6 +- 52 files changed, 759 insertions(+), 820 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/GlobalMutedNotificationsFragment.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsEvent.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsScreen.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsViewModel.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/MutedNotificationsFragment.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment2.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsRepository.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState2.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel2.kt delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/dialogs/GroupMentionSettingDialog.java create mode 100644 app/src/main/res/drawable/symbol_bell_badge_24.xml delete mode 100644 app/src/main/res/layout/group_mention_setting_dialog.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogAdapter.kt index 830351d301..b00c378a18 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/calls/log/CallLogAdapter.kt @@ -11,6 +11,7 @@ import com.bumptech.glide.Glide import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.database.CallTable import org.thoughtcrime.securesms.database.MessageTypes +import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.databinding.CallLogAdapterItemBinding import org.thoughtcrime.securesms.databinding.CallLogCreateCallLinkItemBinding import org.thoughtcrime.securesms.databinding.ConversationListItemClearFilterBinding @@ -335,7 +336,7 @@ class CallLogAdapter( binding.callRecipientAvatar.setAvatar(Glide.with(binding.callRecipientAvatar), recipient, false) binding.callRecipientAvatar.setOnClickListener { onCallClicked(call) } binding.callRecipientBadge.setBadgeFromRecipient(recipient) - binding.callRecipientName.text = if (searchQuery != null) { + binding.callRecipientName.text = if (!searchQuery.isNullOrEmpty()) { SearchUtil.getHighlightedSpan( Locale.getDefault(), { arrayOf(TextAppearanceSpan(context, CoreUiR.style.Signal_Text_TitleSmall)) }, @@ -346,6 +347,7 @@ class CallLogAdapter( } else { recipient.getDisplayName(context) } + binding.callRecipientMuted.visible = recipient.isMuted && recipient.callNotificationSetting == RecipientTable.NotificationSetting.DO_NOT_NOTIFY } private fun presentCallInfo(call: CallLogRow.Call, date: Long) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt index 2d9d8fb207..334ba581b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/AppSettingsActivity.kt @@ -69,6 +69,7 @@ class AppSettingsActivity : DSLSettingsActivity(), GooglePayComponent { AppSettingsRoute.NotificationsRoute.Notifications -> AppSettingsFragmentDirections.actionDirectToNotificationsSettingsFragment() AppSettingsRoute.ChangeNumberRoute.Start -> AppSettingsFragmentDirections.actionDirectToChangeNumberFragment() is AppSettingsRoute.DonationsRoute.Donations -> AppSettingsFragmentDirections.actionDirectToManageDonations().setDirectToCheckoutType(appSettingsRoute.directToCheckoutType) + AppSettingsRoute.NotificationsRoute.MutedNotifications -> AppSettingsFragmentDirections.actionDirectToChatsSettingsFragment() AppSettingsRoute.NotificationsRoute.NotificationProfiles -> AppSettingsFragmentDirections.actionDirectToNotificationProfiles() is AppSettingsRoute.NotificationsRoute.EditProfile -> AppSettingsFragmentDirections.actionDirectToCreateNotificationProfiles() is AppSettingsRoute.NotificationsRoute.ProfileDetails -> AppSettingsFragmentDirections.actionDirectToNotificationProfileDetails( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt index fa36d0f7b6..fc2b250e4a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt @@ -12,6 +12,5 @@ sealed interface LabsSettingsEvents { data class ToggleBetterSearch(val enabled: Boolean) : LabsSettingsEvents data class ToggleStarredMessages(val enabled: Boolean) : LabsSettingsEvents data class ToggleStickerReplies(val enabled: Boolean) : LabsSettingsEvents - data class ToggleMuteBreakthroughNotifications(val enabled: Boolean) : LabsSettingsEvents data class ToggleImprovedMessageDeletion(val enabled: Boolean) : LabsSettingsEvents } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt index 03ed443ad9..90939af9cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt @@ -143,15 +143,6 @@ private fun LabsSettingsContent( ) } - item { - Rows.ToggleRow( - checked = state.muteBreakthroughNotifications, - text = "Improved Notification Management", - label = "Adds per-conversation controls to let calls and replies break through mute. New options in the sounds & notifications settings for a chat.", - onCheckChanged = { onEvent(LabsSettingsEvents.ToggleMuteBreakthroughNotifications(it)) } - ) - } - item { Rows.ToggleRow( checked = state.improvedMessageDeletion, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt index 03bbeb60d9..51237d99a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt @@ -15,6 +15,5 @@ data class LabsSettingsState( val betterSearch: Boolean = false, val starredMessages: Boolean = false, val stickerReplies: Boolean = false, - val muteBreakthroughNotifications: Boolean = false, val improvedMessageDeletion: Boolean = false ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt index 432de19dd8..666695ae9a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt @@ -41,10 +41,6 @@ class LabsSettingsViewModel : ViewModel() { SignalStore.labs.stickerReplies = event.enabled _state.value = _state.value.copy(stickerReplies = event.enabled) } - is LabsSettingsEvents.ToggleMuteBreakthroughNotifications -> { - SignalStore.labs.muteBreakthroughNotifications = event.enabled - _state.value = _state.value.copy(muteBreakthroughNotifications = event.enabled) - } is LabsSettingsEvents.ToggleImprovedMessageDeletion -> { SignalStore.labs.improvedMessageDeletion = event.enabled _state.value = _state.value.copy(improvedMessageDeletion = event.enabled) @@ -60,7 +56,6 @@ class LabsSettingsViewModel : ViewModel() { betterSearch = SignalStore.labs.betterSearch, starredMessages = SignalStore.labs.starredMessages, stickerReplies = SignalStore.labs.stickerReplies, - muteBreakthroughNotifications = SignalStore.labs.muteBreakthroughNotifications, improvedMessageDeletion = SignalStore.labs.improvedMessageDeletion ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/GlobalMutedNotificationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/GlobalMutedNotificationsFragment.kt new file mode 100644 index 0000000000..ef127b05bd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/GlobalMutedNotificationsFragment.kt @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.components.settings.app.notifications + +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.signal.core.ui.compose.ComposeFragment +import org.signal.core.ui.compose.Scaffolds +import org.signal.core.ui.compose.SignalIcons +import org.thoughtcrime.securesms.R + +/** + * Fragment to control default settings when muted + */ +class GlobalMutedNotificationsFragment : ComposeFragment() { + + private val viewModel: MutedNotificationsViewModel by viewModels( + factoryProducer = { MutedNotificationsViewModel.Factory() } + ) + + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() + + Scaffolds.Settings( + title = stringResource(R.string.MutedNotificationsFragment__while), + navigationIcon = SignalIcons.ArrowStart.imageVector, + onNavigationClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }, + navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close), + modifier = Modifier.imePadding() + ) { paddingValues -> + MutedNotificationScreen( + state = state, + onEvent = viewModel::onEvent, + modifier = Modifier.padding(paddingValues) + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsEvent.kt new file mode 100644 index 0000000000..bef569b9e7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsEvent.kt @@ -0,0 +1,10 @@ +package org.thoughtcrime.securesms.components.settings.app.notifications + +/** + * Events emitted by [MutedNotificationScreen] and handled by [MutedNotificationsViewModel] + */ +sealed interface MutedNotificationsEvent { + data class CallsToggled(val allowCalls: Boolean) : MutedNotificationsEvent + data class MentionsToggled(val allowMentions: Boolean) : MutedNotificationsEvent + data class RepliesToggled(val allowReplies: Boolean) : MutedNotificationsEvent +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsScreen.kt new file mode 100644 index 0000000000..e0533a27f2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsScreen.kt @@ -0,0 +1,84 @@ +package org.thoughtcrime.securesms.components.settings.app.notifications + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.Rows +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.util.RemoteConfig + +@Composable +fun MutedNotificationScreen( + state: MutedNotificationsState, + onEvent: (MutedNotificationsEvent) -> Unit = {}, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier + ) { + if (RemoteConfig.internalUser) { + Rows.ToggleRow( + icon = ImageVector.vectorResource(org.signal.core.ui.R.drawable.symbol_phone_24), + checked = state.allowCalls, + text = stringResource(R.string.MutedNotificationsFragment__calls), + label = if (state.isGlobal) stringResource(R.string.MutedNotificationsFragment__calls_body_global) else stringResource(R.string.MutedNotificationsFragment__calls_body), + onCheckChanged = { onEvent(MutedNotificationsEvent.CallsToggled(it)) } + ) + } + if (state.showMentions) { + Rows.ToggleRow( + icon = ImageVector.vectorResource(org.signal.core.ui.R.drawable.symbol_at_24), + checked = state.allowMentions, + text = stringResource(R.string.MutedNotificationsFragment__mentions), + label = if (state.isGlobal) stringResource(R.string.MutedNotificationsFragment__mentions_body_global) else stringResource(R.string.MutedNotificationsFragment__mentions_body), + onCheckChanged = { onEvent(MutedNotificationsEvent.MentionsToggled(it)) } + ) + } + if (RemoteConfig.internalUser) { + if (state.showReplies) { + Rows.ToggleRow( + icon = ImageVector.vectorResource(R.drawable.symbol_reply_24), + checked = state.allowReplies, + text = stringResource(R.string.MutedNotificationsFragment__replies), + label = if (state.isGlobal) stringResource(R.string.MutedNotificationsFragment__replies_body_global) else stringResource(R.string.MutedNotificationsFragment__replies_body), + onCheckChanged = { onEvent(MutedNotificationsEvent.RepliesToggled(it)) } + ) + } + } + } +} + +@DayNightPreviews +@Composable +fun MutedNotificationScreenGlobalPreview() { + Previews.Preview { + MutedNotificationScreen( + state = MutedNotificationsState( + isGlobal = true, + allowCalls = true, + allowMentions = false, + allowReplies = true + ) + ) + } +} + +@DayNightPreviews +@Composable +fun MutedNotificationScreenChatPreview() { + Previews.Preview { + MutedNotificationScreen( + state = MutedNotificationsState( + isGlobal = false, + allowCalls = true, + allowMentions = false, + allowReplies = true + ) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsViewModel.kt new file mode 100644 index 0000000000..e8b75b4703 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/MutedNotificationsViewModel.kt @@ -0,0 +1,105 @@ +package org.thoughtcrime.securesms.components.settings.app.notifications + +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.rx3.asFlow +import kotlinx.coroutines.withContext +import org.signal.core.ui.compose.EventDrivenViewModel +import org.signal.core.util.concurrent.SignalDispatchers +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId + +class MutedNotificationsViewModel(private val recipientId: RecipientId? = null) : EventDrivenViewModel(TAG) { + + companion object { + private val TAG = Log.tag(MutedNotificationsViewModel::class) + } + + private val _state = MutableStateFlow(MutedNotificationsState()) + val state = _state.asStateFlow() + + init { + if (recipientId != null) { + viewModelScope.launch(SignalDispatchers.Default) { + Recipient.observable(recipientId).asFlow().collectLatest { recipient -> + _state.update { + it.copy( + isGlobal = false, + allowCalls = recipient.callNotificationSetting == NotificationSetting.ALWAYS_NOTIFY, + showMentions = recipient.isPushV2Group, + allowMentions = recipient.mentionSetting == NotificationSetting.ALWAYS_NOTIFY, + showReplies = recipient.isPushV2Group, + allowReplies = recipient.replyNotificationSetting == NotificationSetting.ALWAYS_NOTIFY + ) + } + } + } + } + } + + override suspend fun processEvent(event: MutedNotificationsEvent) { + applyEvent(_state.value, event) { _state.value = it } + } + + @VisibleForTesting + suspend fun applyEvent(state: MutedNotificationsState, event: MutedNotificationsEvent, stateEmitter: (MutedNotificationsState) -> Unit) { + when (event) { + is MutedNotificationsEvent.CallsToggled -> { + if (recipientId != null) { + withContext(SignalDispatchers.Default) { + SignalDatabase.recipients.setCallNotificationSetting(recipientId, if (event.allowCalls) NotificationSetting.ALWAYS_NOTIFY else NotificationSetting.DO_NOT_NOTIFY) + } + } else { + SignalStore.settings.allowCallsWhileMuted = event.allowCalls + } + stateEmitter(state.copy(allowCalls = event.allowCalls)) + } + is MutedNotificationsEvent.MentionsToggled -> { + if (recipientId != null) { + withContext(SignalDispatchers.Default) { + SignalDatabase.recipients.setMentionSetting(recipientId, if (event.allowMentions) NotificationSetting.ALWAYS_NOTIFY else NotificationSetting.DO_NOT_NOTIFY) + } + } else { + SignalStore.settings.allowMentionsWhileMuted = event.allowMentions + } + stateEmitter(state.copy(allowMentions = event.allowMentions)) + } + is MutedNotificationsEvent.RepliesToggled -> { + if (recipientId != null) { + withContext(SignalDispatchers.Default) { + SignalDatabase.recipients.setReplyNotificationSetting(recipientId, if (event.allowReplies) NotificationSetting.ALWAYS_NOTIFY else NotificationSetting.DO_NOT_NOTIFY) + } + } else { + SignalStore.settings.allowRepliesWhileMuted = event.allowReplies + } + stateEmitter(state.copy(allowReplies = event.allowReplies)) + } + } + } + + class Factory(private val recipientId: RecipientId? = null) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return modelClass.cast(MutedNotificationsViewModel(recipientId))!! + } + } +} + +data class MutedNotificationsState( + val isGlobal: Boolean = true, + val allowCalls: Boolean = SignalStore.settings.allowCallsWhileMuted, + val showMentions: Boolean = true, + val allowMentions: Boolean = SignalStore.settings.allowMentionsWhileMuted, + val showReplies: Boolean = true, + val allowReplies: Boolean = SignalStore.settings.allowRepliesWhileMuted +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt index 1d55a68be7..a0b11c328f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsFragment.kt @@ -52,8 +52,10 @@ import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRoute import org.thoughtcrime.securesms.components.settings.app.routes.AppSettingsRouter import org.thoughtcrime.securesms.components.settings.models.Banner +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.TurnOnNotificationsBottomSheet +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.RingtoneUtil import org.thoughtcrime.securesms.util.navigation.safeNavigate import org.thoughtcrime.securesms.util.viewModel @@ -85,6 +87,10 @@ class NotificationsSettingsFragment : ComposeFragment() { findNavController().safeNavigate(R.id.action_notificationsSettingsFragment_to_notificationProfilesFragment) } + AppSettingsRoute.NotificationsRoute.MutedNotifications -> { + findNavController().safeNavigate(R.id.action_notificationsSettingsFragment_to_mutedNotificationsFragment) + } + else -> error("Unexpected route: ${it.javaClass.name}") } } @@ -263,6 +269,14 @@ open class DefaultNotificationsSettingsCallbacks( override fun setNotifyWhenContactJoinsSignal(enabled: Boolean) { viewModel.setNotifyWhenContactJoinsSignal(enabled) } + + override fun onMutedClicked() { + appSettingsRouter.navigateTo(AppSettingsRoute.NotificationsRoute.MutedNotifications) + } + + override fun setReactionNotificationEnabled(enabled: Boolean) { + viewModel.setReactionNotificationEnabled(enabled) + } } interface NotificationsSettingsCallbacks { @@ -286,6 +300,8 @@ interface NotificationsSettingsCallbacks { fun setCallVibrateEnabled(enabled: Boolean) = Unit fun onNavigationProfilesClick() = Unit fun setNotifyWhenContactJoinsSignal(enabled: Boolean) = Unit + fun onMutedClicked() = Unit + fun setReactionNotificationEnabled(enabled: Boolean) = Unit object Empty : NotificationsSettingsCallbacks } @@ -314,19 +330,78 @@ fun NotificationsSettingsScreen( } } - item { - Texts.SectionHeader(stringResource(R.string.NotificationsSettingsFragment__messages)) - } - item { Rows.ToggleRow( - text = stringResource(R.string.preferences__notifications), + text = stringResource(R.string.preferences__enable_notifications), enabled = state.messageNotificationsState.canEnableNotifications, checked = state.messageNotificationsState.notificationsEnabled, onCheckChanged = callbacks::setMessageNotificationsEnabled ) } + item { + Rows.RadioListRow( + text = stringResource(R.string.preferences_notifications__show), + labels = stringArrayResource(R.array.pref_notification_privacy_entries), + values = stringArrayResource(R.array.pref_notification_privacy_values), + selectedValue = state.messageNotificationsState.messagePrivacy, + enabled = state.messageNotificationsState.notificationsEnabled, + onSelected = callbacks::setMessageNotificationPrivacy + ) + } + + if (RemoteConfig.internalUser) { + item { + Rows.TextRow( + text = stringResource(R.string.preferences_notifications__while_muted), + label = getWhileMutedString(), + enabled = state.messageNotificationsState.notificationsEnabled, + onClick = callbacks::onMutedClicked + ) + } + + item { + Rows.ToggleRow( + text = stringResource(R.string.preferences_notifications__reaction), + label = stringResource(R.string.preferences_notifications__notify_reaction), + enabled = state.messageNotificationsState.canEnableNotifications, + checked = state.messageNotificationsState.reactionNotificationEnabled, + onCheckChanged = callbacks::setReactionNotificationEnabled + ) + } + } + + // TODO(michelle): Implement unread reminders here + + item { + Rows.ToggleRow( + text = stringResource(R.string.NotificationsSettingsFragment__contact_joins_signal), + label = stringResource(R.string.NotificationsSettingsFragment__notify_contact), + enabled = state.messageNotificationsState.canEnableNotifications, + checked = state.notifyWhenContactJoinsSignal, + onCheckChanged = callbacks::setNotifyWhenContactJoinsSignal + ) + } + + item { + Rows.RadioListRow( + text = stringResource(R.string.preferences__repeat_alerts), + labels = stringArrayResource(R.array.pref_repeat_alerts_entries), + values = stringArrayResource(R.array.pref_repeat_alerts_values), + selectedValue = state.messageNotificationsState.repeatAlerts.toString(), + enabled = state.messageNotificationsState.notificationsEnabled, + onSelected = callbacks::setMessageRepeatAlerts + ) + } + + item { + Dividers.Default() + } + + item { + Texts.SectionHeader(stringResource(R.string.NotificationsSettingsFragment__sounds)) + } + if (deviceState.apiLevel >= 30) { item { Rows.TextRow( @@ -403,28 +478,6 @@ fun NotificationsSettingsScreen( ) } - item { - Rows.RadioListRow( - text = stringResource(R.string.preferences__repeat_alerts), - labels = stringArrayResource(R.array.pref_repeat_alerts_entries), - values = stringArrayResource(R.array.pref_repeat_alerts_values), - selectedValue = state.messageNotificationsState.repeatAlerts.toString(), - enabled = state.messageNotificationsState.notificationsEnabled, - onSelected = callbacks::setMessageRepeatAlerts - ) - } - - item { - Rows.RadioListRow( - text = stringResource(R.string.preferences_notifications__show), - labels = stringArrayResource(R.array.pref_notification_privacy_entries), - values = stringArrayResource(R.array.pref_notification_privacy_values), - selectedValue = state.messageNotificationsState.messagePrivacy, - enabled = state.messageNotificationsState.notificationsEnabled, - onSelected = callbacks::setMessageNotificationPrivacy - ) - } - if (deviceState.apiLevel >= 23 && state.messageNotificationsState.troubleshootNotifications) { item { Rows.TextRow( @@ -467,7 +520,7 @@ fun NotificationsSettingsScreen( item { Rows.ToggleRow( - text = stringResource(R.string.preferences__notifications), + text = stringResource(R.string.preferences__call_notifications), enabled = state.callNotificationsState.canEnableNotifications, checked = state.callNotificationsState.notificationsEnabled, onCheckChanged = callbacks::setCallNotificationsEnabled @@ -500,10 +553,6 @@ fun NotificationsSettingsScreen( Dividers.Default() } - item { - Texts.SectionHeader(stringResource(R.string.NotificationsSettingsFragment__notification_profiles)) - } - item { Rows.TextRow( text = stringResource(R.string.NotificationsSettingsFragment__profiles), @@ -511,26 +560,29 @@ fun NotificationsSettingsScreen( onClick = callbacks::onNavigationProfilesClick ) } - - item { - Dividers.Default() - } - - item { - Texts.SectionHeader(stringResource(R.string.NotificationsSettingsFragment__notify_when)) - } - - item { - Rows.ToggleRow( - text = stringResource(R.string.NotificationsSettingsFragment__contact_joins_signal), - checked = state.notifyWhenContactJoinsSignal, - onCheckChanged = callbacks::setNotifyWhenContactJoinsSignal - ) - } } } } +@Composable +private fun getWhileMutedString(): String { + val body = mutableListOf() + if (SignalStore.settings.allowCallsWhileMuted) { + body.add(stringResource(R.string.MutedNotificationsFragment__calls)) + } + if (SignalStore.settings.allowMentionsWhileMuted) { + body.add(stringResource(R.string.MutedNotificationsFragment__mentions)) + } + if (SignalStore.settings.allowRepliesWhileMuted) { + body.add(stringResource(R.string.MutedNotificationsFragment__replies)) + } + return if (body.isNotEmpty()) { + body.joinToString(", ") + } else { + stringResource(R.string.preferences__none) + } +} + @Composable private fun getLedColor(ledColorString: String): Color { return when (ledColorString) { @@ -594,7 +646,8 @@ private fun rememberTestState(): NotificationsSettingsState = remember { repeatAlerts = 1, messagePrivacy = "", priority = 1, - troubleshootNotifications = true + troubleshootNotifications = true, + reactionNotificationEnabled = true ), callNotificationsState = CallNotificationsState( notificationsEnabled = true, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsState.kt index 81eb443c3e..8e7125e973 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsState.kt @@ -19,7 +19,8 @@ data class MessageNotificationsState( val repeatAlerts: Int, val messagePrivacy: String, val priority: Int, - val troubleshootNotifications: Boolean + val troubleshootNotifications: Boolean, + val reactionNotificationEnabled: Boolean ) data class CallNotificationsState( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt index 5b043a132a..0c5720b163 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/notifications/NotificationsSettingsViewModel.kt @@ -111,6 +111,11 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer refresh() } + fun setReactionNotificationEnabled(enabled: Boolean) { + SignalStore.settings.reactionNotifications = enabled + refresh() + } + /** * @param currentState If provided and [calculateSlowNotifications] = false, then we will copy the slow notification state from it * @param calculateSlowNotifications If true, calculate the true slow notification state (this is not main-thread safe). Otherwise, it will copy from @@ -135,7 +140,8 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer currentState.messageNotificationsState.troubleshootNotifications } else { false - } + }, + reactionNotificationEnabled = SignalStore.settings.reactionNotifications ), callNotificationsState = CallNotificationsState( notificationsEnabled = SignalStore.settings.isCallNotificationsEnabled && canEnableNotifications(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRoute.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRoute.kt index 0a6565192b..668c87177e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRoute.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/routes/AppSettingsRoute.kt @@ -74,6 +74,7 @@ sealed interface AppSettingsRoute : Parcelable { @Parcelize sealed interface NotificationsRoute : AppSettingsRoute { data object Notifications : NotificationsRoute + data object MutedNotifications : NotificationsRoute data object NotificationProfiles : NotificationsRoute data class EditProfile(val profileId: Long = -1L) : NotificationsRoute data class ProfileDetails(val profileId: Long) : NotificationsRoute diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsAction.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsAction.kt index 0017697a35..147e79bce4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsAction.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsAction.kt @@ -86,7 +86,7 @@ sealed interface ConversationSettingsAction { data class OpenChatWallpaper(val recipientId: RecipientId) : ConversationSettingsAction /** Open the sounds and notifications screen. */ - data class NavigateToSoundsAndNotifications(val recipientId: RecipientId, val useInternalScreen: Boolean) : ConversationSettingsAction + data class NavigateToSoundsAndNotifications(val recipientId: RecipientId) : ConversationSettingsAction /** Open the list of starred messages in this chat. */ data class OpenStarredMessages(val threadId: Long) : ConversationSettingsAction diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 5587a81a39..070493b65c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -401,12 +401,7 @@ class ConversationSettingsFragment : ComposeFragment() { startActivity(ChatWallpaperActivity.createIntent(requireContext(), action.recipientId)) } is ConversationSettingsAction.NavigateToSoundsAndNotifications -> { - val directions = if (action.useInternalScreen) { - ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment2(action.recipientId) - } else { - ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(action.recipientId) - } - + val directions = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(action.recipientId) navController.safeNavigate(directions) } is ConversationSettingsAction.OpenStarredMessages -> { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt index a8f2c19cda..1a0f7a4827 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsRepository.kt @@ -85,10 +85,6 @@ class ConversationSettingsRepository( return SignalStore.labs.starredMessages } - fun isInternalUser(): Boolean { - return RemoteConfig.internalUser - } - fun isAddToStoryAvailable(): Boolean { return !SignalStore.story.isFeatureDisabled } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsScreen.kt index 162a3a19b1..de7bfd89c3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsScreen.kt @@ -184,7 +184,6 @@ fun GroupSettingsScreen( item { SoundsAndNotificationsRow( - isInternalUser = state.isInternalUser, enabled = !state.isDeprecatedOrUnregistered, onClick = { onEvent(GroupSettingsEvent.SoundsAndNotificationsClicked) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsState.kt index 8612d836aa..e88fb5957a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsState.kt @@ -25,7 +25,6 @@ data class GroupSettingsState( val threadId: Long = -1L, val storyViewState: StoryViewState = StoryViewState.NONE, val isDeprecatedOrUnregistered: Boolean = false, - val isInternalUser: Boolean = false, val displayInternalRecipientDetails: Boolean = false, val starredMessagesEnabled: Boolean = false, val disappearingMessagesLifespan: Int = 0, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsViewModel.kt index 174fac5640..7e88f66d64 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsViewModel.kt @@ -57,7 +57,6 @@ class GroupSettingsViewModel( groupId = groupId, isDeprecatedOrUnregistered = repository.isDeprecatedOrUnregistered(), starredMessagesEnabled = repository.isStarredMessagesEnabled(), - isInternalUser = repository.isInternalUser(), displayInternalRecipientDetails = repository.isInternalRecipientDetailsEnabled() ) ) @@ -188,7 +187,7 @@ class GroupSettingsViewModel( GroupSettingsEvent.ChatColorAndWallpaperClicked -> _actions.send(ConversationSettingsAction.OpenChatWallpaper(state.recipient.id)) GroupSettingsEvent.SoundsAndNotificationsClicked -> { - _actions.send(ConversationSettingsAction.NavigateToSoundsAndNotifications(state.recipient.id, state.isInternalUser)) + _actions.send(ConversationSettingsAction.NavigateToSoundsAndNotifications(state.recipient.id)) } GroupSettingsEvent.StarredMessagesClicked -> _actions.send(ConversationSettingsAction.OpenStarredMessages(state.threadId)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsScreen.kt index 86791313ff..f80283a28d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsScreen.kt @@ -146,7 +146,6 @@ fun IndividualSettingsScreen( item { SoundsAndNotificationsRow( - isInternalUser = state.isInternalUser, enabled = !state.isDeprecatedOrUnregistered, onClick = { onEvent(IndividualSettingsEvent.SoundsAndNotificationsClicked) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsState.kt index b37e863b94..21c96107b7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsState.kt @@ -20,7 +20,6 @@ data class IndividualSettingsState( val threadId: Long = -1L, val storyViewState: StoryViewState = StoryViewState.NONE, val isDeprecatedOrUnregistered: Boolean = false, - val isInternalUser: Boolean = false, val displayInternalRecipientDetails: Boolean = false, val starredMessagesEnabled: Boolean = false, val disappearingMessagesLifespan: Int = 0, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsViewModel.kt index 310952d778..c302de28b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsViewModel.kt @@ -59,7 +59,6 @@ class IndividualSettingsViewModel( IndividualSettingsState( isDeprecatedOrUnregistered = repository.isDeprecatedOrUnregistered(), starredMessagesEnabled = repository.isStarredMessagesEnabled(), - isInternalUser = repository.isInternalUser(), displayInternalRecipientDetails = repository.isInternalRecipientDetailsEnabled() ) ) @@ -176,7 +175,7 @@ class IndividualSettingsViewModel( _actions.send(ConversationSettingsAction.OpenChatWallpaper(recipientId)) } IndividualSettingsEvent.SoundsAndNotificationsClicked -> { - _actions.send(ConversationSettingsAction.NavigateToSoundsAndNotifications(recipientId, state.isInternalUser)) + _actions.send(ConversationSettingsAction.NavigateToSoundsAndNotifications(recipientId)) } IndividualSettingsEvent.StarredMessagesClicked -> { _actions.send(ConversationSettingsAction.OpenStarredMessages(state.threadId)) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/ReleaseNotesSettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/ReleaseNotesSettingsScreen.kt index 6a9fb26cec..a1e9dad13b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/ReleaseNotesSettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/individual/ReleaseNotesSettingsScreen.kt @@ -106,7 +106,6 @@ fun ReleaseNotesSettingsScreen( item { SoundsAndNotificationsRow( - isInternalUser = state.isInternalUser, enabled = !state.isDeprecatedOrUnregistered, onClick = { onEvent(IndividualSettingsEvent.SoundsAndNotificationsClicked) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/shared/CommonSettingsRows.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/shared/CommonSettingsRows.kt index 6775913fc0..26ac6ea9c2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/shared/CommonSettingsRows.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/shared/CommonSettingsRows.kt @@ -64,13 +64,10 @@ fun ChatColorAndWallpaperRow( fun SoundsAndNotificationsRow( onClick: () -> Unit, modifier: Modifier = Modifier, - isInternalUser: Boolean = false, enabled: Boolean = true ) { - val label = stringResource(R.string.ConversationSettingsFragment__sounds_and_notifications) - Rows.TextRow( - text = if (isInternalUser) "$label (Internal Only)" else label, + text = stringResource(R.string.ConversationSettingsFragment__sounds_and_notifications), icon = painterResource(CoreUiR.drawable.symbol_speaker_24), enabled = enabled, onClick = onClick, @@ -179,7 +176,6 @@ private fun ChatSettingsRowsPreview() { DisappearingMessagesRow(lifespanSeconds = 7.days.inWholeSeconds.toInt(), enabled = false, onClick = {}) ChatColorAndWallpaperRow(onClick = {}) SoundsAndNotificationsRow(onClick = {}) - SoundsAndNotificationsRow(isInternalUser = true, onClick = {}) SoundsAndNotificationsRow(enabled = false, onClick = {}) StarredMessagesRow(onClick = {}) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/MutedNotificationsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/MutedNotificationsFragment.kt new file mode 100644 index 0000000000..90d3437ac2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/MutedNotificationsFragment.kt @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.components.settings.conversation.sounds + +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.signal.core.ui.compose.ComposeFragment +import org.signal.core.ui.compose.Scaffolds +import org.signal.core.ui.compose.SignalIcons +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.settings.app.notifications.MutedNotificationScreen +import org.thoughtcrime.securesms.components.settings.app.notifications.MutedNotificationsViewModel + +/** + * Fragment to control while muted settings for a specific chat + */ +class MutedNotificationsFragment : ComposeFragment() { + + private val viewModel: MutedNotificationsViewModel by viewModels( + factoryProducer = { + MutedNotificationsViewModel.Factory(MutedNotificationsFragmentArgs.fromBundle(requireArguments()).recipientId) + } + ) + + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() + + Scaffolds.Settings( + title = stringResource(R.string.MutedNotificationsFragment__while), + navigationIcon = SignalIcons.ArrowStart.imageVector, + onNavigationClick = { requireActivity().onBackPressedDispatcher.onBackPressed() }, + navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close), + modifier = Modifier.imePadding() + ) { paddingValues -> + MutedNotificationScreen( + state = state, + onEvent = viewModel::onEvent, + modifier = Modifier.padding(paddingValues) + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsEvent.kt index c6408d2bda..757312025e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsEvent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsEvent.kt @@ -54,4 +54,9 @@ sealed interface SoundsAndNotificationsEvent { * [custom notifications settings screen][org.thoughtcrime.securesms.components.settings.conversation.sounds.custom.CustomNotificationsSettingsFragment]. */ data object NavigateToCustomNotifications : SoundsAndNotificationsEvent + + /** + * User tapped "When Muted" and navigates to [MutedNotificationsFragment] + */ + data object NavigateToMutedNotifications : SoundsAndNotificationsEvent } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment.kt index 05a70d019d..46a6c858b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment.kt @@ -1,125 +1,59 @@ package org.thoughtcrime.securesms.components.settings.conversation.sounds +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.fragment.app.viewModels +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.Navigation -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import org.signal.core.ui.compose.ComposeFragment import org.thoughtcrime.securesms.MuteDialog -import org.thoughtcrime.securesms.R -import org.thoughtcrime.securesms.components.settings.DSLConfiguration -import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment -import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon -import org.thoughtcrime.securesms.components.settings.DSLSettingsText -import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil -import org.thoughtcrime.securesms.database.RecipientTable import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter import org.thoughtcrime.securesms.util.navigation.safeNavigate -class SoundsAndNotificationsSettingsFragment : - DSLSettingsFragment( - titleId = R.string.ConversationSettingsFragment__sounds_and_notifications - ) { - - private val mentionLabels: Array by lazy { - resources.getStringArray(R.array.SoundsAndNotificationsSettingsFragment__mention_labels) - } +class SoundsAndNotificationsSettingsFragment : ComposeFragment() { private val viewModel: SoundsAndNotificationsSettingsViewModel by viewModels( factoryProducer = { val recipientId = SoundsAndNotificationsSettingsFragmentArgs.fromBundle(requireArguments()).recipientId - val repository = SoundsAndNotificationsSettingsRepository(requireContext()) - - SoundsAndNotificationsSettingsViewModel.Factory(recipientId, repository) + SoundsAndNotificationsSettingsViewModel.Factory(recipientId) } ) - override fun onResume() { - super.onResume() - viewModel.channelConsistencyCheck() - } + @Composable + override fun FragmentContent() { + val state by viewModel.state.collectAsStateWithLifecycle() - override fun bindAdapter(adapter: MappingAdapter) { - viewModel.state.observe(viewLifecycleOwner) { state -> - if (state.channelConsistencyCheckComplete && state.recipientId != Recipient.UNKNOWN.id) { - adapter.submitList(getConfiguration(state).toMappingModelList()) - } + if (!state.channelConsistencyCheckComplete || state.recipientId == Recipient.UNKNOWN.id) { + return } - } - private fun getConfiguration(state: SoundsAndNotificationsSettingsState): DSLConfiguration { - return configure { - val muteSummary = if (state.muteUntil > 0) { - state.muteUntil.formatMutedUntil(requireContext()) - } else { - getString(R.string.SoundsAndNotificationsSettingsFragment__not_muted) - } - - val muteIcon = if (state.muteUntil > 0) { - R.drawable.ic_bell_disabled_24 - } else { - R.drawable.ic_bell_24 - } - - clickPref( - title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__mute_notifications), - icon = DSLSettingsIcon.from(muteIcon), - summary = DSLSettingsText.from(muteSummary), - onClick = { - if (state.muteUntil <= 0) { - MuteDialog.show(requireContext(), childFragmentManager, viewLifecycleOwner, viewModel::setMuteUntil) - } else { - MaterialAlertDialogBuilder(requireContext()) - .setMessage(muteSummary) - .setPositiveButton(R.string.ConversationSettingsFragment__unmute) { dialog, _ -> - viewModel.unmute() - dialog.dismiss() - } - .setNegativeButton(android.R.string.cancel) { dialog, _ -> dialog.dismiss() } - .show() + SoundsAndNotificationsSettingsScreen( + state = state, + formatMuteUntil = { it.formatMutedUntil(requireContext()) }, + onEvent = { event -> + when (event) { + is SoundsAndNotificationsEvent.NavigateToCustomNotifications -> { + val action = SoundsAndNotificationsSettingsFragmentDirections + .actionSoundsAndNotificationsSettingsFragmentToCustomNotificationsSettingsFragment(state.recipientId) + Navigation.findNavController(requireView()).safeNavigate(action) } - } - ) - - if (state.hasMentionsSupport) { - val mentionSelection = if (state.mentionSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY) { - 0 - } else { - 1 - } - - radioListPref( - title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__mentions), - icon = DSLSettingsIcon.from(R.drawable.ic_at_24), - selected = mentionSelection, - listItems = mentionLabels, - onSelected = { - viewModel.setMentionSetting( - if (it == 0) { - RecipientTable.NotificationSetting.ALWAYS_NOTIFY - } else { - RecipientTable.NotificationSetting.DO_NOT_NOTIFY - } - ) + is SoundsAndNotificationsEvent.NavigateToMutedNotifications -> { + val action = SoundsAndNotificationsSettingsFragmentDirections + .actionSoundsAndNotificationsSettingsFragmentToMutedNotificationsFragment(state.recipientId) + Navigation.findNavController(requireView()).safeNavigate(action) } - ) - } - - val customSoundSummary = if (state.hasCustomNotificationSettings) { - R.string.preferences_on - } else { - R.string.preferences_off - } - - clickPref( - title = DSLSettingsText.from(R.string.SoundsAndNotificationsSettingsFragment__custom_notifications), - icon = DSLSettingsIcon.from(R.drawable.ic_speaker_24), - summary = DSLSettingsText.from(customSoundSummary), - onClick = { - val action = SoundsAndNotificationsSettingsFragmentDirections.actionSoundsAndNotificationsSettingsFragmentToCustomNotificationsSettingsFragment(state.recipientId) - Navigation.findNavController(requireView()).safeNavigate(action) + else -> viewModel.onEvent(event) } - ) - } + }, + onNavigationClick = { + requireActivity().onBackPressedDispatcher.onBackPressed() + }, + onMuteClick = { + MuteDialog.show(requireContext(), childFragmentManager, viewLifecycleOwner) { muteUntil -> + viewModel.onEvent(SoundsAndNotificationsEvent.SetMuteUntil(muteUntil)) + } + } + ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment2.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment2.kt deleted file mode 100644 index 471b743006..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsFragment2.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.conversation.sounds - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.fragment.app.viewModels -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.Navigation -import org.signal.core.ui.compose.ComposeFragment -import org.thoughtcrime.securesms.MuteDialog -import org.thoughtcrime.securesms.components.settings.conversation.preferences.Utils.formatMutedUntil -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.util.navigation.safeNavigate - -class SoundsAndNotificationsSettingsFragment2 : ComposeFragment() { - - private val viewModel: SoundsAndNotificationsSettingsViewModel2 by viewModels( - factoryProducer = { - val recipientId = SoundsAndNotificationsSettingsFragment2Args.fromBundle(requireArguments()).recipientId - SoundsAndNotificationsSettingsViewModel2.Factory(recipientId) - } - ) - - @Composable - override fun FragmentContent() { - val state by viewModel.state.collectAsStateWithLifecycle() - - if (!state.channelConsistencyCheckComplete || state.recipientId == Recipient.UNKNOWN.id) { - return - } - - SoundsAndNotificationsSettingsScreen( - state = state, - formatMuteUntil = { it.formatMutedUntil(requireContext()) }, - onEvent = { event -> - when (event) { - is SoundsAndNotificationsEvent.NavigateToCustomNotifications -> { - val action = SoundsAndNotificationsSettingsFragment2Directions - .actionSoundsAndNotificationsSettingsFragment2ToCustomNotificationsSettingsFragment(state.recipientId) - Navigation.findNavController(requireView()).safeNavigate(action) - } - else -> viewModel.onEvent(event) - } - }, - onNavigationClick = { - requireActivity().onBackPressedDispatcher.onBackPressed() - }, - onMuteClick = { - MuteDialog.show(requireContext(), childFragmentManager, viewLifecycleOwner) { muteUntil -> - viewModel.onEvent(SoundsAndNotificationsEvent.SetMuteUntil(muteUntil)) - } - } - ) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsRepository.kt deleted file mode 100644 index 44ed7c3236..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsRepository.kt +++ /dev/null @@ -1,46 +0,0 @@ -package org.thoughtcrime.securesms.components.settings.conversation.sounds - -import android.content.Context -import org.signal.core.util.concurrent.SignalExecutors -import org.thoughtcrime.securesms.database.RecipientTable -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.notifications.NotificationChannels -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId - -class SoundsAndNotificationsSettingsRepository(private val context: Context) { - - fun ensureCustomChannelConsistency(complete: () -> Unit) { - SignalExecutors.BOUNDED.execute { - if (NotificationChannels.supported()) { - NotificationChannels.getInstance().ensureCustomChannelConsistency() - } - complete() - } - } - - fun setMuteUntil(recipientId: RecipientId, muteUntil: Long) { - SignalExecutors.BOUNDED.execute { - SignalDatabase.recipients.setMuted(recipientId, muteUntil) - } - } - - fun setMentionSetting(recipientId: RecipientId, mentionSetting: RecipientTable.NotificationSetting) { - SignalExecutors.BOUNDED.execute { - SignalDatabase.recipients.setMentionSetting(recipientId, mentionSetting) - } - } - - fun hasCustomNotificationSettings(recipientId: RecipientId, consumer: (Boolean) -> Unit) { - SignalExecutors.BOUNDED.execute { - val recipient = Recipient.resolved(recipientId) - consumer( - if (recipient.notificationChannel != null || !NotificationChannels.supported()) { - true - } else { - NotificationChannels.getInstance().updateWithShortcutBasedChannel(recipient) - } - ) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsScreen.kt index 091271e472..d66c9cff3b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsScreen.kt @@ -5,30 +5,18 @@ package org.thoughtcrime.securesms.components.settings.conversation.sounds -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog +import androidx.compose.ui.res.vectorResource import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Dialogs import org.signal.core.ui.compose.Dividers @@ -37,21 +25,19 @@ import org.signal.core.ui.compose.Rows import org.signal.core.ui.compose.Scaffolds import org.signal.core.ui.compose.SignalIcons import org.signal.core.ui.compose.Texts -import org.signal.core.ui.compose.horizontalGutters -import org.signal.core.ui.compose.theme.SignalTheme import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting -import org.signal.core.ui.R as CoreUiR +import org.thoughtcrime.securesms.util.RemoteConfig @Composable fun SoundsAndNotificationsSettingsScreen( - state: SoundsAndNotificationsSettingsState2, + state: SoundsAndNotificationsSettingsState, formatMuteUntil: (Long) -> String, onEvent: (SoundsAndNotificationsEvent) -> Unit, onNavigationClick: () -> Unit, onMuteClick: () -> Unit ) { - val isMuted = state.muteUntil > 0 + val isMuted = state.isMuted var showUnmuteDialog by remember { mutableStateOf(false) } Scaffolds.Settings( @@ -79,6 +65,14 @@ fun SoundsAndNotificationsSettingsScreen( ) } + item { + Dividers.Default() + } + + item { + Texts.SectionHeader(text = stringResource(R.string.preferences__notifications)) + } + // Mute item { val muteSummary = if (isMuted) { @@ -103,54 +97,18 @@ fun SoundsAndNotificationsSettingsScreen( ) } - // Divider + When muted section - item { - Dividers.Default() - } - - item { - Texts.SectionHeader(text = stringResource(R.string.SoundsAndNotificationsSettingsFragment__when_muted)) - } - - // Calls - item { - NotificationSettingRow( - title = stringResource(R.string.SoundsAndNotificationsSettingsFragment__calls), - dialogTitle = stringResource(R.string.SoundsAndNotificationsSettingsFragment__calls), - dialogMessage = stringResource(R.string.SoundsAndNotificationsSettingsFragment__calls_dialog_message), - icon = painterResource(CoreUiR.drawable.symbol_phone_24), - setting = state.callNotificationSetting, - onSelected = { onEvent(SoundsAndNotificationsEvent.SetCallNotificationSetting(it)) } - ) - } - - // Mentions (only for groups) - if (state.hasMentionsSupport) { + if (RemoteConfig.internalUser || state.hasMentionsSupport) { item { - NotificationSettingRow( - title = stringResource(R.string.SoundsAndNotificationsSettingsFragment__mentions), - dialogTitle = stringResource(R.string.SoundsAndNotificationsSettingsFragment__mentions), - dialogMessage = stringResource(R.string.SoundsAndNotificationsSettingsFragment__mentions_dialog_message), - icon = painterResource(R.drawable.ic_at_24), - setting = state.mentionSetting, - onSelected = { onEvent(SoundsAndNotificationsEvent.SetMentionSetting(it)) } + Rows.TextRow( + text = stringResource(R.string.SoundsAndNotificationsSettingsFragment__when_muted), + label = getMuteSummary(state), + icon = ImageVector.vectorResource(R.drawable.symbol_bell_badge_24), + onClick = { onEvent(SoundsAndNotificationsEvent.NavigateToMutedNotifications) } ) } } - // Replies (only for groups) - if (state.hasMentionsSupport) { - item { - NotificationSettingRow( - title = stringResource(R.string.SoundsAndNotificationsSettingsFragment__replies_to_you), - dialogTitle = stringResource(R.string.SoundsAndNotificationsSettingsFragment__replies_to_you), - dialogMessage = stringResource(R.string.SoundsAndNotificationsSettingsFragment__replies_dialog_message), - icon = painterResource(R.drawable.symbol_reply_24), - setting = state.replyNotificationSetting, - onSelected = { onEvent(SoundsAndNotificationsEvent.SetReplyNotificationSetting(it)) } - ) - } - } +// TODO(michelle): Unread reminders } } @@ -167,100 +125,21 @@ fun SoundsAndNotificationsSettingsScreen( } @Composable -private fun NotificationSettingRow( - title: String, - dialogTitle: String, - dialogMessage: String, - icon: Painter, - setting: NotificationSetting, - onSelected: (NotificationSetting) -> Unit -) { - var showDialog by remember { mutableStateOf(false) } - - val labels = arrayOf( - stringResource(R.string.SoundsAndNotificationsSettingsFragment__always_notify), - stringResource(R.string.SoundsAndNotificationsSettingsFragment__do_not_notify) - ) - val selectedLabel = if (setting == NotificationSetting.ALWAYS_NOTIFY) labels[0] else labels[1] - - Rows.TextRow( - text = title, - label = selectedLabel, - icon = icon, - onClick = { showDialog = true } - ) - - if (showDialog) { - NotificationSettingDialog( - title = dialogTitle, - message = dialogMessage, - labels = labels, - selectedIndex = if (setting == NotificationSetting.ALWAYS_NOTIFY) 0 else 1, - onDismiss = { showDialog = false }, - onSelected = { index -> - onSelected(if (index == 0) NotificationSetting.ALWAYS_NOTIFY else NotificationSetting.DO_NOT_NOTIFY) - showDialog = false - } - ) +fun getMuteSummary(state: SoundsAndNotificationsSettingsState): String { + val body = mutableListOf() + if (RemoteConfig.internalUser && state.callNotificationSetting == NotificationSetting.ALWAYS_NOTIFY) { + body.add(stringResource(R.string.MutedNotificationsFragment__calls)) } -} - -@Composable -private fun NotificationSettingDialog( - title: String, - message: String, - labels: Array, - selectedIndex: Int, - onDismiss: () -> Unit, - onSelected: (Int) -> Unit -) { - Dialog(onDismissRequest = onDismiss) { - Surface( - shape = AlertDialogDefaults.shape, - color = SignalTheme.colors.colorSurface2 - ) { - Column { - Text( - text = title, - style = MaterialTheme.typography.titleLarge, - modifier = Modifier - .padding(top = 24.dp) - .horizontalGutters() - ) - - Text( - text = message, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .padding(top = 8.dp) - .horizontalGutters() - ) - - Column(modifier = Modifier.padding(top = 16.dp, bottom = 16.dp)) { - labels.forEachIndexed { index, label -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .defaultMinSize(minHeight = 48.dp) - .clickable { onSelected(index) } - .horizontalGutters() - ) { - RadioButton( - selected = index == selectedIndex, - onClick = { onSelected(index) } - ) - Text( - text = label, - style = MaterialTheme.typography.bodyLarge, - modifier = Modifier.padding(start = 16.dp) - ) - } - } - } - } - } + if (state.hasMentionsSupport && state.mentionSetting == NotificationSetting.ALWAYS_NOTIFY) { + body.add(stringResource(R.string.MutedNotificationsFragment__mentions)) + } + if (RemoteConfig.internalUser && state.replyNotificationSetting == NotificationSetting.ALWAYS_NOTIFY) { + body.add(stringResource(R.string.MutedNotificationsFragment__replies)) + } + return if (body.isNotEmpty()) { + body.joinToString(", ") + } else { + stringResource(R.string.preferences__none) } } @@ -269,7 +148,7 @@ private fun NotificationSettingDialog( private fun SoundsAndNotificationsSettingsScreenMutedPreview() { Previews.Preview { SoundsAndNotificationsSettingsScreen( - state = SoundsAndNotificationsSettingsState2( + state = SoundsAndNotificationsSettingsState( muteUntil = Long.MAX_VALUE, callNotificationSetting = NotificationSetting.ALWAYS_NOTIFY, mentionSetting = NotificationSetting.ALWAYS_NOTIFY, @@ -291,7 +170,7 @@ private fun SoundsAndNotificationsSettingsScreenMutedPreview() { private fun SoundsAndNotificationsSettingsScreenUnmutedPreview() { Previews.Preview { SoundsAndNotificationsSettingsScreen( - state = SoundsAndNotificationsSettingsState2( + state = SoundsAndNotificationsSettingsState( muteUntil = 0L, callNotificationSetting = NotificationSetting.ALWAYS_NOTIFY, mentionSetting = NotificationSetting.ALWAYS_NOTIFY, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState.kt index e53c4309df..ba85c6d499 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState.kt @@ -1,14 +1,19 @@ package org.thoughtcrime.securesms.components.settings.conversation.sounds -import org.thoughtcrime.securesms.database.RecipientTable +import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId data class SoundsAndNotificationsSettingsState( val recipientId: RecipientId = Recipient.UNKNOWN.id, val muteUntil: Long = 0L, - val mentionSetting: RecipientTable.NotificationSetting = RecipientTable.NotificationSetting.DO_NOT_NOTIFY, + val mentionSetting: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, + val callNotificationSetting: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, + val replyNotificationSetting: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, val hasCustomNotificationSettings: Boolean = false, val hasMentionsSupport: Boolean = false, - val channelConsistencyCheckComplete: Boolean = false -) + val channelConsistencyCheckComplete: Boolean = false, + val unreadReminders: Boolean = false +) { + val isMuted = muteUntil > 0 +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState2.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState2.kt deleted file mode 100644 index 5ad31c553d..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsState2.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.conversation.sounds - -import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientId - -data class SoundsAndNotificationsSettingsState2( - val recipientId: RecipientId = Recipient.UNKNOWN.id, - val muteUntil: Long = 0L, - val mentionSetting: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, - val callNotificationSetting: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, - val replyNotificationSetting: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, - val hasCustomNotificationSettings: Boolean = false, - val hasMentionsSupport: Boolean = false, - val channelConsistencyCheckComplete: Boolean = false -) { - val isMuted = muteUntil > 0 -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel.kt index 418d2d3218..33f5e4c0a5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel.kt @@ -1,60 +1,98 @@ package org.thoughtcrime.securesms.components.settings.conversation.sounds -import androidx.lifecycle.LiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import org.thoughtcrime.securesms.database.RecipientTable +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting +import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientForeverObserver import org.thoughtcrime.securesms.recipients.RecipientId -import org.thoughtcrime.securesms.util.livedata.Store class SoundsAndNotificationsSettingsViewModel( - private val recipientId: RecipientId, - private val repository: SoundsAndNotificationsSettingsRepository -) : ViewModel() { + private val recipientId: RecipientId +) : ViewModel(), RecipientForeverObserver { - private val store = Store(SoundsAndNotificationsSettingsState()) + private val _state = MutableStateFlow(SoundsAndNotificationsSettingsState()) + val state: StateFlow = _state - val state: LiveData = store.stateLiveData + private val liveRecipient = Recipient.live(recipientId) init { - store.update(Recipient.live(recipientId).liveData) { recipient, state -> - state.copy( + liveRecipient.observeForever(this) + onRecipientChanged(liveRecipient.get()) + + viewModelScope.launch(Dispatchers.IO) { + if (NotificationChannels.supported()) { + NotificationChannels.getInstance().ensureCustomChannelConsistency() + } + _state.update { it.copy(channelConsistencyCheckComplete = true) } + } + } + + override fun onRecipientChanged(recipient: Recipient) { + _state.update { + it.copy( recipientId = recipientId, muteUntil = if (recipient.isMuted) recipient.muteUntil else 0L, mentionSetting = recipient.mentionSetting, + callNotificationSetting = recipient.callNotificationSetting, + replyNotificationSetting = recipient.replyNotificationSetting, hasMentionsSupport = recipient.isPushV2Group, hasCustomNotificationSettings = recipient.notificationChannel != null || !NotificationChannels.supported() ) } } - fun setMuteUntil(muteUntil: Long) { - repository.setMuteUntil(recipientId, muteUntil) + override fun onCleared() { + liveRecipient.removeForeverObserver(this) } - fun unmute() { - repository.setMuteUntil(recipientId, 0L) - } - - fun setMentionSetting(mentionSetting: RecipientTable.NotificationSetting) { - repository.setMentionSetting(recipientId, mentionSetting) - } - - fun channelConsistencyCheck() { - store.update { s -> s.copy(channelConsistencyCheckComplete = false) } - repository.ensureCustomChannelConsistency { - store.update { s -> s.copy(channelConsistencyCheckComplete = true) } + fun onEvent(event: SoundsAndNotificationsEvent) { + when (event) { + is SoundsAndNotificationsEvent.SetMuteUntil -> applySetMuteUntil(event.muteUntil) + is SoundsAndNotificationsEvent.Unmute -> applySetMuteUntil(0L) + is SoundsAndNotificationsEvent.SetMentionSetting -> applySetMentionSetting(event.setting) + is SoundsAndNotificationsEvent.SetCallNotificationSetting -> applySetCallNotificationSetting(event.setting) + is SoundsAndNotificationsEvent.SetReplyNotificationSetting -> applySetReplyNotificationSetting(event.setting) + is SoundsAndNotificationsEvent.NavigateToCustomNotifications -> Unit // Navigation handled by UI + is SoundsAndNotificationsEvent.NavigateToMutedNotifications -> Unit // Navigation handled by UI } } - class Factory( - private val recipientId: RecipientId, - private val repository: SoundsAndNotificationsSettingsRepository - ) : ViewModelProvider.Factory { + private fun applySetMuteUntil(muteUntil: Long) { + viewModelScope.launch(Dispatchers.Default) { + SignalDatabase.recipients.setMuted(recipientId, muteUntil) + } + } + + private fun applySetMentionSetting(setting: NotificationSetting) { + viewModelScope.launch(Dispatchers.Default) { + SignalDatabase.recipients.setMentionSetting(recipientId, setting) + } + } + + private fun applySetCallNotificationSetting(setting: NotificationSetting) { + viewModelScope.launch(Dispatchers.Default) { + SignalDatabase.recipients.setCallNotificationSetting(recipientId, setting) + } + } + + private fun applySetReplyNotificationSetting(setting: NotificationSetting) { + viewModelScope.launch(Dispatchers.Default) { + SignalDatabase.recipients.setReplyNotificationSetting(recipientId, setting) + } + } + + class Factory(private val recipientId: RecipientId) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel(recipientId, repository))) + return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel(recipientId))) } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel2.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel2.kt deleted file mode 100644 index d2f1a084b6..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/sounds/SoundsAndNotificationsSettingsViewModel2.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2025 Signal Messenger, LLC - * SPDX-License-Identifier: AGPL-3.0-only - */ - -package org.thoughtcrime.securesms.components.settings.conversation.sounds - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting -import org.thoughtcrime.securesms.database.SignalDatabase -import org.thoughtcrime.securesms.notifications.NotificationChannels -import org.thoughtcrime.securesms.recipients.Recipient -import org.thoughtcrime.securesms.recipients.RecipientForeverObserver -import org.thoughtcrime.securesms.recipients.RecipientId - -class SoundsAndNotificationsSettingsViewModel2( - private val recipientId: RecipientId -) : ViewModel(), RecipientForeverObserver { - - private val _state = MutableStateFlow(SoundsAndNotificationsSettingsState2()) - val state: StateFlow = _state - - private val liveRecipient = Recipient.live(recipientId) - - init { - liveRecipient.observeForever(this) - onRecipientChanged(liveRecipient.get()) - - viewModelScope.launch(Dispatchers.IO) { - if (NotificationChannels.supported()) { - NotificationChannels.getInstance().ensureCustomChannelConsistency() - } - _state.update { it.copy(channelConsistencyCheckComplete = true) } - } - } - - override fun onRecipientChanged(recipient: Recipient) { - _state.update { - it.copy( - recipientId = recipientId, - muteUntil = if (recipient.isMuted) recipient.muteUntil else 0L, - mentionSetting = recipient.mentionSetting, - callNotificationSetting = recipient.callNotificationSetting, - replyNotificationSetting = recipient.replyNotificationSetting, - hasMentionsSupport = recipient.isPushV2Group, - hasCustomNotificationSettings = recipient.notificationChannel != null || !NotificationChannels.supported() - ) - } - } - - override fun onCleared() { - liveRecipient.removeForeverObserver(this) - } - - fun onEvent(event: SoundsAndNotificationsEvent) { - when (event) { - is SoundsAndNotificationsEvent.SetMuteUntil -> applySetMuteUntil(event.muteUntil) - is SoundsAndNotificationsEvent.Unmute -> applySetMuteUntil(0L) - is SoundsAndNotificationsEvent.SetMentionSetting -> applySetMentionSetting(event.setting) - is SoundsAndNotificationsEvent.SetCallNotificationSetting -> applySetCallNotificationSetting(event.setting) - is SoundsAndNotificationsEvent.SetReplyNotificationSetting -> applySetReplyNotificationSetting(event.setting) - is SoundsAndNotificationsEvent.NavigateToCustomNotifications -> Unit // Navigation handled by UI - } - } - - private fun applySetMuteUntil(muteUntil: Long) { - viewModelScope.launch(Dispatchers.Default) { - SignalDatabase.recipients.setMuted(recipientId, muteUntil) - } - } - - private fun applySetMentionSetting(setting: NotificationSetting) { - viewModelScope.launch(Dispatchers.Default) { - SignalDatabase.recipients.setMentionSetting(recipientId, setting) - } - } - - private fun applySetCallNotificationSetting(setting: NotificationSetting) { - viewModelScope.launch(Dispatchers.Default) { - SignalDatabase.recipients.setCallNotificationSetting(recipientId, setting) - } - } - - private fun applySetReplyNotificationSetting(setting: NotificationSetting) { - viewModelScope.launch(Dispatchers.Default) { - SignalDatabase.recipients.setReplyNotificationSetting(recipientId, setting) - } - } - - class Factory(private val recipientId: RecipientId) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel2(recipientId))) - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt index 0e3c3c5312..662a5e5948 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientTable.kt @@ -247,7 +247,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da $SEALED_SENDER_MODE INTEGER DEFAULT 0, $STORAGE_SERVICE_ID TEXT UNIQUE DEFAULT NULL, $STORAGE_SERVICE_PROTO TEXT DEFAULT NULL, - $MENTION_SETTING INTEGER DEFAULT ${NotificationSetting.ALWAYS_NOTIFY.id}, + $MENTION_SETTING INTEGER DEFAULT ${NotificationSetting.SYSTEM_DEFAULT.id}, $CAPABILITIES INTEGER DEFAULT 0, $LAST_SESSION_RESET BLOB DEFAULT NULL, $WALLPAPER BLOB DEFAULT NULL, @@ -271,8 +271,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da $NOTE TEXT DEFAULT NULL, $MESSAGE_EXPIRATION_TIME_VERSION INTEGER DEFAULT 1 NOT NULL, $KEY_TRANSPARENCY_DATA BLOB DEFAULT NULL, - $CALL_NOTIFICATION_SETTING INTEGER DEFAULT ${NotificationSetting.ALWAYS_NOTIFY.id}, - $REPLY_NOTIFICATION_SETTING INTEGER DEFAULT ${NotificationSetting.ALWAYS_NOTIFY.id}, + $CALL_NOTIFICATION_SETTING INTEGER DEFAULT ${NotificationSetting.SYSTEM_DEFAULT.id}, + $REPLY_NOTIFICATION_SETTING INTEGER DEFAULT ${NotificationSetting.SYSTEM_DEFAULT.id}, $BLOCKED_AT INTEGER DEFAULT 0 ) """ @@ -4579,9 +4579,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da MESSAGE_EXPIRATION_TIME_VERSION to 1, SEALED_SENDER_MODE to 0, STORAGE_SERVICE_PROTO to null, - MENTION_SETTING to NotificationSetting.ALWAYS_NOTIFY.id, - CALL_NOTIFICATION_SETTING to NotificationSetting.ALWAYS_NOTIFY.id, - REPLY_NOTIFICATION_SETTING to NotificationSetting.ALWAYS_NOTIFY.id, + MENTION_SETTING to NotificationSetting.SYSTEM_DEFAULT.id, + CALL_NOTIFICATION_SETTING to NotificationSetting.SYSTEM_DEFAULT.id, + REPLY_NOTIFICATION_SETTING to NotificationSetting.SYSTEM_DEFAULT.id, CAPABILITIES to 0, LAST_SESSION_RESET to null, WALLPAPER to null, @@ -5174,13 +5174,22 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da } enum class NotificationSetting(val id: Int) { - ALWAYS_NOTIFY(0), - DO_NOT_NOTIFY(1); + SYSTEM_DEFAULT(0), + DO_NOT_NOTIFY(1), + ALWAYS_NOTIFY(2); companion object { fun fromId(id: Int): NotificationSetting { return entries[id] } + + fun resolve(setting: NotificationSetting, allowedByDefault: Boolean): NotificationSetting { + return if (setting == SYSTEM_DEFAULT) { + if (allowedByDefault) ALWAYS_NOTIFY else DO_NOT_NOTIFY + } else { + setting + } + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/dialogs/GroupMentionSettingDialog.java b/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/dialogs/GroupMentionSettingDialog.java deleted file mode 100644 index e5af3bf81b..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/ui/managegroup/dialogs/GroupMentionSettingDialog.java +++ /dev/null @@ -1,83 +0,0 @@ -package org.thoughtcrime.securesms.groups.ui.managegroup.dialogs; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.content.DialogInterface; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.CheckedTextView; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.core.util.Consumer; - -import com.google.android.material.dialog.MaterialAlertDialogBuilder; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting; - -public final class GroupMentionSettingDialog { - - public static void show(@NonNull Context context, @NonNull NotificationSetting mentionSetting, @Nullable Consumer callback) { - SelectionCallback selectionCallback = new SelectionCallback(mentionSetting, callback); - - new MaterialAlertDialogBuilder(context) - .setTitle(R.string.GroupMentionSettingDialog_notify_me_for_mentions) - .setView(getView(context, mentionSetting, selectionCallback)) - .setPositiveButton(android.R.string.ok, selectionCallback) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - - @SuppressLint("InflateParams") - private static View getView(@NonNull Context context, @NonNull NotificationSetting mentionSetting, @NonNull SelectionCallback selectionCallback) { - View root = LayoutInflater.from(context).inflate(R.layout.group_mention_setting_dialog, null, false); - CheckedTextView alwaysNotify = root.findViewById(R.id.group_mention_setting_always_notify); - CheckedTextView dontNotify = root.findViewById(R.id.group_mention_setting_dont_notify); - - View.OnClickListener listener = (v) -> { - alwaysNotify.setChecked(alwaysNotify == v); - dontNotify.setChecked(dontNotify == v); - - if (alwaysNotify.isChecked()) { - selectionCallback.selection = NotificationSetting.ALWAYS_NOTIFY; - } else if (dontNotify.isChecked()) { - selectionCallback.selection = NotificationSetting.DO_NOT_NOTIFY; - } - }; - - alwaysNotify.setOnClickListener(listener); - dontNotify.setOnClickListener(listener); - - switch (mentionSetting) { - case ALWAYS_NOTIFY: - listener.onClick(alwaysNotify); - break; - case DO_NOT_NOTIFY: - listener.onClick(dontNotify); - break; - } - - return root; - } - - private static class SelectionCallback implements DialogInterface.OnClickListener { - - @NonNull private final NotificationSetting previousMentionSetting; - @NonNull private NotificationSetting selection; - @Nullable private final Consumer callback; - - public SelectionCallback(@NonNull NotificationSetting previousMentionSetting, @Nullable Consumer callback) { - this.previousMentionSetting = previousMentionSetting; - this.selection = previousMentionSetting; - this.callback = callback; - } - - @Override - public void onClick(DialogInterface dialog, int which) { - if (callback != null && selection != previousMentionSetting) { - callback.accept(selection); - } - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt index a05db3074c..3cc62c855a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt @@ -30,8 +30,6 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues( var stickerReplies by booleanValue(STICKER_REPLIES, false).falseForExternalUsers() - var muteBreakthroughNotifications by booleanValue(MUTE_BREAKTHROUGH_NOTIFICATIONS, true).falseForExternalUsers() - var improvedMessageDeletion by booleanValue(IMPROVED_MESSAGE_DELETION, true).falseForExternalUsers() private fun SignalStoreValueDelegate.falseForExternalUsers(): SignalStoreValueDelegate { diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java index 125cc28a32..621bcca22d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java @@ -60,6 +60,10 @@ public final class SettingsValues extends SignalStoreValues { public static final String MESSAGE_IN_CHAT_SOUNDS_ENABLED = "settings.message.in.chats.sounds.enabled"; public static final String MESSAGE_REPEAT_ALERTS = "settings.message.repeat.alerts"; public static final String MESSAGE_NOTIFICATION_PRIVACY = "settings.message.notification.privacy"; + public static final String MESSAGE_NOTIFICATION_REACTION = "settings.message.notification.reaction"; + public static final String MESSAGE_NOTIFICATION_MUTED_CALLS = "settings.message.notifications.muted.call"; + public static final String MESSAGE_NOTIFICATION_MUTED_MENTIONS = "settings.message.notifications.muted.mentions"; + public static final String MESSAGE_NOTIFICATION_MUTED_REPLIES = "settings.message.notifications.muted.replies"; public static final String CALL_NOTIFICATIONS_ENABLED = "settings.call.notifications.enabled"; public static final String CALL_RINGTONE = "settings.call.ringtone"; public static final String CALL_VIBRATE_ENABLED = "settings.call.vibrate.enabled"; @@ -443,6 +447,38 @@ public final class SettingsValues extends SignalStoreValues { putString(MESSAGE_NOTIFICATION_PRIVACY, messageNotificationsPrivacy.toString()); } + public boolean getReactionNotifications() { + return getBoolean(MESSAGE_NOTIFICATION_REACTION, true); + } + + public void setReactionNotifications(boolean show) { + putBoolean(MESSAGE_NOTIFICATION_REACTION, show); + } + + public boolean getAllowCallsWhileMuted() { + return getBoolean(MESSAGE_NOTIFICATION_MUTED_CALLS, false); + } + + public void setAllowCallsWhileMuted(boolean allow) { + putBoolean(MESSAGE_NOTIFICATION_MUTED_CALLS, allow); + } + + public boolean getAllowMentionsWhileMuted() { + return getBoolean(MESSAGE_NOTIFICATION_MUTED_MENTIONS, true); + } + + public void setAllowMentionsWhileMuted(boolean allow) { + putBoolean(MESSAGE_NOTIFICATION_MUTED_MENTIONS, allow); + } + + public boolean getAllowRepliesWhileMuted() { + return getBoolean(MESSAGE_NOTIFICATION_MUTED_REPLIES, true); + } + + public void setAllowRepliesWhileMuted(boolean allow) { + putBoolean(MESSAGE_NOTIFICATION_MUTED_REPLIES, allow); + } + public boolean isCallNotificationsEnabled() { return getBoolean(CALL_NOTIFICATIONS_ENABLED, TextSecurePreferences.isCallNotificationsEnabled(AppDependencies.getApplication())); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt index ef54be3910..41275a26a0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/DefaultMessageNotifier.kt @@ -172,6 +172,13 @@ class DefaultMessageNotifier(context: Application) : MessageNotifier { } } + if (state.reactionsDisabledFilteredMessages.isNotEmpty()) { + Log.i(TAG, "Marking ${state.reactionsDisabledFilteredMessages.size} reactions as notified to skip notification") + state.reactionsDisabledFilteredMessages.forEach { item -> + SignalDatabase.messages.markAsNotified(item.id) + } + } + if (!SignalStore.settings.isMessageNotificationsEnabled) { Log.i(TAG, "Marking ${state.conversations.size} conversations as notified to skip notification") state.conversations.forEach { conversation -> diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationState.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationState.kt index 0d6fec86b6..c457bb1a2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationState.kt @@ -12,7 +12,12 @@ import org.thoughtcrime.securesms.recipients.Recipient /** * Hold all state for notifications for all conversations. */ -data class NotificationState(val conversations: List, val muteFilteredMessages: List, val profileFilteredMessages: List) { +data class NotificationState( + val conversations: List, + val muteFilteredMessages: List, + val profileFilteredMessages: List, + val reactionsDisabledFilteredMessages: List +) { val threadCount: Int = conversations.size val isEmpty: Boolean = conversations.isEmpty() @@ -89,6 +94,6 @@ data class NotificationState(val conversations: List, data class FilteredMessage(val id: Long, val isMms: Boolean) companion object { - val EMPTY = NotificationState(emptyList(), emptyList(), emptyList()) + val EMPTY = NotificationState(emptyList(), emptyList(), emptyList(), emptyList()) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt index 2a7c320bf6..2509206f8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationStateProvider.kt @@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.notifications.profiles.NotificationProfile import org.thoughtcrime.securesms.polls.PollVote import org.thoughtcrime.securesms.recipients.Recipient @@ -94,6 +95,7 @@ object NotificationStateProvider { val conversations: MutableList = mutableListOf() val muteFilteredMessages: MutableList = mutableListOf() val profileFilteredMessages: MutableList = mutableListOf() + val reactionsDisabledFilteredMessages: MutableList = mutableListOf() messages.groupBy { it.thread } .forEach { (thread, threadMessages) -> @@ -105,6 +107,7 @@ object NotificationStateProvider { MessageInclusion.EXCLUDE -> Unit MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) + MessageInclusion.REACTIONS_DISABLED_FILTERED -> reactionsDisabledFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) } if (notification.hasUnreadReactions) { @@ -114,6 +117,7 @@ object NotificationStateProvider { MessageInclusion.EXCLUDE -> Unit MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) + MessageInclusion.REACTIONS_DISABLED_FILTERED -> reactionsDisabledFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) } } } @@ -125,6 +129,7 @@ object NotificationStateProvider { MessageInclusion.EXCLUDE -> Unit MessageInclusion.MUTE_FILTERED -> muteFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) MessageInclusion.PROFILE_FILTERED -> profileFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) + MessageInclusion.REACTIONS_DISABLED_FILTERED -> reactionsDisabledFilteredMessages += NotificationState.FilteredMessage(notification.messageRecord.id, notification.messageRecord.isMms) } } } @@ -141,7 +146,7 @@ object NotificationStateProvider { } } - return NotificationState(conversations, muteFilteredMessages, profileFilteredMessages) + return NotificationState(conversations, muteFilteredMessages, profileFilteredMessages, reactionsDisabledFilteredMessages) } private data class NotificationMessage( @@ -198,7 +203,9 @@ object NotificationStateProvider { } fun includeReaction(reaction: ReactionRecord, notificationProfile: NotificationProfile?): MessageInclusion { - return if (threadRecipient.isMuted) { + return if (!SignalStore.settings.reactionNotifications) { + MessageInclusion.REACTIONS_DISABLED_FILTERED + } else if (threadRecipient.isMuted) { MessageInclusion.MUTE_FILTERED } else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id)) { MessageInclusion.PROFILE_FILTERED @@ -233,6 +240,7 @@ object NotificationStateProvider { INCLUDE, EXCLUDE, MUTE_FILTERED, - PROFILE_FILTERED + PROFILE_FILTERED, + REACTIONS_DISABLED_FILTERED } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt index b9dd886301..38a98f58eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt @@ -51,6 +51,7 @@ import org.thoughtcrime.securesms.phonenumbers.NumberUtil import org.thoughtcrime.securesms.profiles.ProfileName import org.thoughtcrime.securesms.recipients.Recipient.Companion.external import org.thoughtcrime.securesms.service.webrtc.links.CallLinkRoomId +import org.thoughtcrime.securesms.util.RemoteConfig import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.UsernameUtil.isValidUsernameForSearch @@ -109,9 +110,9 @@ class Recipient( private val sealedSenderAccessModeValue: SealedSenderAccessMode = SealedSenderAccessMode.UNKNOWN, private val capabilities: RecipientRecord.Capabilities = RecipientRecord.Capabilities.UNKNOWN, val storageId: ByteArray? = null, - val mentionSetting: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, - private val callNotificationSettingValue: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, - private val replyNotificationSettingValue: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY, + private val mentionSettingValue: NotificationSetting = NotificationSetting.SYSTEM_DEFAULT, + private val callNotificationSettingValue: NotificationSetting = NotificationSetting.SYSTEM_DEFAULT, + private val replyNotificationSettingValue: NotificationSetting = NotificationSetting.SYSTEM_DEFAULT, private val wallpaperValue: ChatWallpaper? = null, private val chatColorsValue: ChatColors? = null, val avatarColor: AvatarColor = AvatarColor.UNKNOWN, @@ -338,13 +339,17 @@ class Recipient( /** The notification channel, if both set and supported by the system. Otherwise null. */ val notificationChannel: String? = if (!NotificationChannels.supported()) null else notificationChannelValue + /** Whether mentions should break through mute for this recipient. */ + val mentionSetting: NotificationSetting + get() = NotificationSetting.resolve(mentionSettingValue, SignalStore.settings.allowMentionsWhileMuted) + /** Whether calls should break through mute for this recipient. */ val callNotificationSetting: NotificationSetting - get() = if (SignalStore.labs.muteBreakthroughNotifications) callNotificationSettingValue else NotificationSetting.ALWAYS_NOTIFY + get() = if (RemoteConfig.internalUser) NotificationSetting.resolve(callNotificationSettingValue, SignalStore.settings.allowCallsWhileMuted) else NotificationSetting.ALWAYS_NOTIFY - /** Whether replies should break through mute for this recipient. Only applicable to groups. */ + /** Whether replies should break through mute for this recipient. */ val replyNotificationSetting: NotificationSetting - get() = if (groupIdValue == null) NotificationSetting.DO_NOT_NOTIFY else if (SignalStore.labs.muteBreakthroughNotifications) replyNotificationSettingValue else mentionSetting + get() = if (groupIdValue == null) NotificationSetting.DO_NOT_NOTIFY else if (RemoteConfig.internalUser) NotificationSetting.resolve(replyNotificationSettingValue, SignalStore.settings.allowRepliesWhileMuted) else mentionSetting /** The state around whether we can send sealed sender to this user. */ val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) { @@ -875,7 +880,7 @@ class Recipient( profileAvatar == other.profileAvatar && notificationChannelValue == other.notificationChannelValue && sealedSenderAccessModeValue == other.sealedSenderAccessModeValue && - mentionSetting == other.mentionSetting && + mentionSettingValue == other.mentionSettingValue && callNotificationSettingValue == other.callNotificationSettingValue && replyNotificationSettingValue == other.replyNotificationSettingValue && wallpaperValue == other.wallpaperValue && diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientCreator.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientCreator.kt index 93c6488686..355bf95beb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientCreator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/RecipientCreator.kt @@ -184,7 +184,7 @@ object RecipientCreator { sealedSenderAccessModeValue = record.sealedSenderAccessMode, capabilities = record.capabilities, storageId = record.storageId, - mentionSetting = record.mentionSetting, + mentionSettingValue = record.mentionSetting, callNotificationSettingValue = record.callNotificationSetting, replyNotificationSettingValue = record.replyNotificationSetting, wallpaperValue = record.wallpaper?.validate(), diff --git a/app/src/main/res/drawable/symbol_bell_badge_24.xml b/app/src/main/res/drawable/symbol_bell_badge_24.xml new file mode 100644 index 0000000000..7588a374e8 --- /dev/null +++ b/app/src/main/res/drawable/symbol_bell_badge_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/call_log_adapter_item.xml b/app/src/main/res/layout/call_log_adapter_item.xml index 7c227f40a4..bf93b4bec9 100644 --- a/app/src/main/res/layout/call_log_adapter_item.xml +++ b/app/src/main/res/layout/call_log_adapter_item.xml @@ -50,21 +50,36 @@ + + - - - - - - - - - diff --git a/app/src/main/res/navigation/app_settings_with_change_number.xml b/app/src/main/res/navigation/app_settings_with_change_number.xml index d7e754111f..b728b3aefa 100644 --- a/app/src/main/res/navigation/app_settings_with_change_number.xml +++ b/app/src/main/res/navigation/app_settings_with_change_number.xml @@ -557,6 +557,14 @@ app:popEnterAnim="@anim/fragment_close_enter" app:popExitAnim="@anim/fragment_close_exit" /> + + @@ -1099,6 +1107,10 @@ + + - - - - - - + android:name="org.thoughtcrime.securesms.components.settings.conversation.sounds.SoundsAndNotificationsSettingsFragment"> - - - - - - + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 805270c7a1..628190a80a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1579,12 +1579,6 @@ This is likely because you registered your Signal account on a different device. - - Notify me for Mentions - Receive notifications when you’re mentioned in muted chats? - Always notify me - Don\'t notify me - Your username, QR code and link aren\'t visible on your profile. Only share your username with people you trust. @@ -3265,7 +3259,7 @@ Mute notifications - Mute until… + Until… @@ -4133,10 +4127,10 @@ Use default Use custom - Mute for 1 hour - Mute for 8 hours - Mute for 1 day - Mute for 7 days + 1 Hour + 8 Hours + 1 Day + 1 Week Always Settings default @@ -4191,6 +4185,10 @@ Inactivity timeout interval Notifications + + Enable notifications + + Call notifications LED color Unknown LED blink pattern @@ -4348,6 +4346,16 @@ Using less data may improve calls on bad networks. In-chat sounds Show + + While muted + + Reaction notifications + + Show notifications for reactions to messages you send + + Unread reminders + + Occasionally notify when there are unread messages in muted chats Ringtone Message font size @@ -5922,8 +5930,19 @@ Messages Calls - Notify when… + + Sounds Contact joins Signal + + Show a notification when a phone contact joins Signal + + Reset notification settings + + Reset notification settings to default, including custom settings for your chats + + Reset all notification settings to default for all chats? + + Reset Notification profiles @@ -5931,6 +5950,27 @@ Create a profile to receive notifications only from people and groups you choose. + + While muted + + Calls + + Ring or notify when a call is started in muted chats. + + Mentions + + Notify when you are mentioned in muted chats. + + Replies + + Notify when someone replies to your message in muted chats. + + Ring or notify when a call is started while this chat is muted. + + Notify when you are mentioned while this chat is muted. + + Notify when someone replies to your message while this chat is muted. + Notification profiles diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsViewModelTest.kt index bbc8e69af2..ec03e5f75b 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/group/GroupSettingsViewModelTest.kt @@ -67,7 +67,6 @@ class GroupSettingsViewModelTest { every { repository.isDeprecatedOrUnregistered() } returns false every { repository.isStarredMessagesEnabled() } returns false - every { repository.isInternalUser() } returns false every { repository.isInternalRecipientDetailsEnabled() } returns false every { repository.isStoriesFeatureEnabled() } returns false every { repository.isAddToStoryAvailable() } returns true diff --git a/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsViewModelTest.kt index b2c185ce12..7ae005abc5 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/components/settings/conversation/individual/IndividualSettingsViewModelTest.kt @@ -69,7 +69,6 @@ class IndividualSettingsViewModelTest { every { repository.isDeprecatedOrUnregistered() } returns false every { repository.isStarredMessagesEnabled() } returns false - every { repository.isInternalUser() } returns false every { repository.isInternalRecipientDetailsEnabled() } returns false every { repository.isStoriesFeatureEnabled() } returns false every { repository.isBlockable(any()) } returns true @@ -335,14 +334,13 @@ class IndividualSettingsViewModelTest { } @Test - fun `sounds and notifications click uses the internal screen for internal users`() = runTest(testDispatcher) { - every { repository.isInternalUser() } returns true + fun `sounds and notifications click navigates`() = runTest(testDispatcher) { val viewModel = createViewModel() val actions = collectActions(viewModel) viewModel.onEvent(IndividualSettingsEvent.SoundsAndNotificationsClicked) - assertEquals(ConversationSettingsAction.NavigateToSoundsAndNotifications(RECIPIENT_ID, useInternalScreen = true), actions.single()) + assertEquals(ConversationSettingsAction.NavigateToSoundsAndNotifications(RECIPIENT_ID), actions.single()) } @Test