Support reaction and while muted notifications.

This commit is contained in:
Michelle Tang
2026-08-19 19:05:50 -04:00
committed by Cody Henthorne
parent e1df942c3c
commit 93d52f7831
52 changed files with 759 additions and 820 deletions
@@ -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) {
@@ -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(
@@ -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
}
@@ -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,
@@ -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
)
@@ -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
)
}
@@ -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)
)
}
}
}
@@ -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
}
@@ -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
)
)
}
}
@@ -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<MutedNotificationsEvent>(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 <T : ViewModel> create(modelClass: Class<T>): 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
)
@@ -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<String>()
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,
@@ -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(
@@ -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(),
@@ -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
@@ -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
@@ -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 -> {
@@ -85,10 +85,6 @@ class ConversationSettingsRepository(
return SignalStore.labs.starredMessages
}
fun isInternalUser(): Boolean {
return RemoteConfig.internalUser
}
fun isAddToStoryAvailable(): Boolean {
return !SignalStore.story.isFeatureDisabled
}
@@ -184,7 +184,6 @@ fun GroupSettingsScreen(
item {
SoundsAndNotificationsRow(
isInternalUser = state.isInternalUser,
enabled = !state.isDeprecatedOrUnregistered,
onClick = { onEvent(GroupSettingsEvent.SoundsAndNotificationsClicked) }
)
@@ -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,
@@ -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))
@@ -146,7 +146,6 @@ fun IndividualSettingsScreen(
item {
SoundsAndNotificationsRow(
isInternalUser = state.isInternalUser,
enabled = !state.isDeprecatedOrUnregistered,
onClick = { onEvent(IndividualSettingsEvent.SoundsAndNotificationsClicked) }
)
@@ -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,
@@ -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))
@@ -106,7 +106,6 @@ fun ReleaseNotesSettingsScreen(
item {
SoundsAndNotificationsRow(
isInternalUser = state.isInternalUser,
enabled = !state.isDeprecatedOrUnregistered,
onClick = { onEvent(IndividualSettingsEvent.SoundsAndNotificationsClicked) }
)
@@ -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 = {})
}
@@ -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)
)
}
}
}
@@ -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
}
@@ -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<String> 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))
}
}
)
}
}
@@ -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))
}
}
)
}
}
@@ -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)
}
)
}
}
}
@@ -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<String>()
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<String>,
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,
@@ -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
}
@@ -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
}
@@ -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<SoundsAndNotificationsSettingsState> = _state
val state: LiveData<SoundsAndNotificationsSettingsState> = 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 <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel(recipientId, repository)))
return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel(recipientId)))
}
}
}
@@ -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<SoundsAndNotificationsSettingsState2> = _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 <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(SoundsAndNotificationsSettingsViewModel2(recipientId)))
}
}
}
@@ -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
}
}
}
}
@@ -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<NotificationSetting> 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<NotificationSetting> callback;
public SelectionCallback(@NonNull NotificationSetting previousMentionSetting, @Nullable Consumer<NotificationSetting> 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);
}
}
}
}
@@ -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<Boolean>.falseForExternalUsers(): SignalStoreValueDelegate<Boolean> {
@@ -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()));
}
@@ -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 ->
@@ -12,7 +12,12 @@ import org.thoughtcrime.securesms.recipients.Recipient
/**
* Hold all state for notifications for all conversations.
*/
data class NotificationState(val conversations: List<NotificationConversation>, val muteFilteredMessages: List<FilteredMessage>, val profileFilteredMessages: List<FilteredMessage>) {
data class NotificationState(
val conversations: List<NotificationConversation>,
val muteFilteredMessages: List<FilteredMessage>,
val profileFilteredMessages: List<FilteredMessage>,
val reactionsDisabledFilteredMessages: List<FilteredMessage>
) {
val threadCount: Int = conversations.size
val isEmpty: Boolean = conversations.isEmpty()
@@ -89,6 +94,6 @@ data class NotificationState(val conversations: List<NotificationConversation>,
data class FilteredMessage(val id: Long, val isMms: Boolean)
companion object {
val EMPTY = NotificationState(emptyList(), emptyList(), emptyList())
val EMPTY = NotificationState(emptyList(), emptyList(), emptyList(), emptyList())
}
}
@@ -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<NotificationConversation> = mutableListOf()
val muteFilteredMessages: MutableList<NotificationState.FilteredMessage> = mutableListOf()
val profileFilteredMessages: MutableList<NotificationState.FilteredMessage> = mutableListOf()
val reactionsDisabledFilteredMessages: MutableList<NotificationState.FilteredMessage> = 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
}
}
@@ -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 &&
@@ -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(),
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M4.44,19.15H7.69C8.09,21.18 9.83,22.65 12,22.65C14.17,22.65 15.91,21.18 16.31,19.15H19.56C21.22,19.15 22.25,18 22.25,16.7C22.25,15.87 21.9,15.22 21.22,14.46C20.02,13.12 19.38,11.65 19.2,9.11L19.18,8.86C18.63,9.08 18.03,9.2 17.4,9.2V9.23C17.62,12.19 18.42,14.02 19.88,15.66C20.34,16.18 20.45,16.42 20.45,16.7C20.45,17.06 20.16,17.35 19.56,17.35H4.44C3.84,17.35 3.55,17.06 3.55,16.7C3.55,16.42 3.66,16.18 4.12,15.66C5.58,14.02 6.38,12.19 6.6,9.23C6.88,5.22 9.17,3.15 12,3.15C12.26,3.15 12.51,3.17 12.75,3.2C12.91,2.58 13.19,2.01 13.57,1.52C13.06,1.41 12.54,1.35 12,1.35C8.23,1.35 5.16,4.12 4.8,9.11C4.62,11.65 3.98,13.12 2.78,14.46C2.1,15.22 1.75,15.87 1.75,16.7C1.75,18 2.78,19.15 4.44,19.15ZM17.4,7.5C19.11,7.5 20.5,6.1 20.5,4.4C20.5,2.69 19.11,1.3 17.4,1.3C15.7,1.3 14.3,2.69 14.3,4.4C14.3,6.1 15.7,7.5 17.4,7.5ZM9.55,19.15H14.45C14.1,20.16 13.16,20.85 12,20.85C10.84,20.85 9.9,20.16 9.55,19.15Z"
android:fillColor="#000000"/>
</vector>
@@ -50,21 +50,36 @@
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/call_recipient_name"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:gravity="start|center_vertical"
android:maxLines="2"
android:textAlignment="viewStart"
android:textAppearance="@style/Signal.Text.BodyLarge"
app:layout_constrainedWidth="true"
app:layout_constraintBottom_toTopOf="@+id/call_info"
app:layout_constraintEnd_toStartOf="@id/call_status_barrier"
app:layout_constraintEnd_toStartOf="@id/call_recipient_muted"
app:layout_constraintHorizontal_bias="0"
app:layout_constraintHorizontal_chainStyle="packed"
app:layout_constraintStart_toEndOf="@+id/call_recipient_avatar"
app:layout_constraintTop_toTopOf="@+id/call_recipient_avatar"
tools:text="Miles Morales" />
<ImageView
android:id="@+id/call_recipient_muted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:layout_marginEnd="16dp"
android:src="@drawable/symbol_bell_slash_compact_16"
app:layout_constraintBottom_toBottomOf="@id/call_recipient_name"
app:layout_constraintEnd_toStartOf="@id/call_status_barrier"
app:layout_constraintStart_toEndOf="@id/call_recipient_name"
app:layout_constraintTop_toTopOf="@id/call_recipient_name"
app:tint="@color/signal_colorOnSurfaceVariant" />
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/call_info"
android:layout_width="0dp"
@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:tools="http://schemas.android.com/tools"
tools:viewBindingIgnore="true"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
style="@style/TextAppearance.AppCompat.Subhead"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
android:paddingLeft="?attr/dialogPreferredPadding"
android:paddingRight="?attr/dialogPreferredPadding"
android:text="@string/GroupMentionSettingDialog_receive_notifications_when_youre_mentioned_in_muted_chats" />
<CheckedTextView
android:id="@+id/group_mention_setting_always_notify"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="20dp"
android:gravity="center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:paddingStart="20dp"
android:paddingEnd="?attr/dialogPreferredPadding"
android:text="@string/GroupMentionSettingDialog_always_notify_me"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
android:theme="@style/Signal.Widget.CompoundButton.RadioButton" />
<CheckedTextView
android:id="@+id/group_mention_setting_dont_notify"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="20dp"
android:gravity="center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:paddingStart="20dp"
android:paddingEnd="?attr/dialogPreferredPadding"
android:text="@string/GroupMentionSettingDialog_dont_notify_me"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
android:theme="@style/Signal.Widget.CompoundButton.RadioButton" />
</LinearLayout>
@@ -557,6 +557,14 @@
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_notificationsSettingsFragment_to_mutedNotificationsFragment"
app:destination="@id/mutedNotificationsFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
<!-- region Privacy -->
@@ -1099,6 +1107,10 @@
<!-- endregion -->
<fragment
android:id="@+id/mutedNotificationsFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.notifications.GlobalMutedNotificationsFragment" />
<!-- Notification Profiles -->
<fragment
android:id="@+id/notificationProfilesFragment"
@@ -70,20 +70,6 @@
</action>
<action
android:id="@+id/action_conversationSettingsFragment_to_soundsAndNotificationsSettingsFragment2"
app:destination="@id/soundsAndNotificationsSettingsFragment2"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit">
<argument
android:name="recipient_id"
app:argType="org.thoughtcrime.securesms.recipients.RecipientId" />
</action>
<action
android:id="@+id/action_conversationSettingsFragment_to_permissionsSettingsFragment"
app:destination="@id/permissionsSettingsFragment"
@@ -160,8 +146,7 @@
<fragment
android:id="@+id/soundsAndNotificationsSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.conversation.sounds.SoundsAndNotificationsSettingsFragment"
tools:layout="@layout/dsl_settings_fragment">
android:name="org.thoughtcrime.securesms.components.settings.conversation.sounds.SoundsAndNotificationsSettingsFragment">
<argument
android:name="recipient_id"
@@ -175,19 +160,9 @@
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
</fragment>
<fragment
android:id="@+id/soundsAndNotificationsSettingsFragment2"
android:name="org.thoughtcrime.securesms.components.settings.conversation.sounds.SoundsAndNotificationsSettingsFragment2">
<argument
android:name="recipient_id"
app:argType="org.thoughtcrime.securesms.recipients.RecipientId" />
<action
android:id="@+id/action_soundsAndNotificationsSettingsFragment2_to_customNotificationsSettingsFragment"
app:destination="@id/customNotificationsSettingsFragment"
android:id="@+id/action_soundsAndNotificationsSettingsFragment_to_mutedNotificationsFragment"
app:destination="@id/mutedNotificationsFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
@@ -225,6 +200,16 @@
</fragment>
<fragment
android:id="@+id/mutedNotificationsFragment"
android:name="org.thoughtcrime.securesms.components.settings.conversation.sounds.MutedNotificationsFragment">
<argument
android:name="recipient_id"
app:argType="org.thoughtcrime.securesms.recipients.RecipientId" />
</fragment>
<fragment
android:id="@+id/shareableGroupLinkFragment"
android:name="org.thoughtcrime.securesms.recipients.ui.sharablegrouplink.ShareableGroupLinkFragment">
+52 -12
View File
@@ -1579,12 +1579,6 @@
<!-- Dialog message -->
<string name="RestoreActivity__no_longer_registered_message">This is likely because you registered your Signal account on a different device.</string>
<!-- GroupMentionSettingDialog -->
<string name="GroupMentionSettingDialog_notify_me_for_mentions">Notify me for Mentions</string>
<string name="GroupMentionSettingDialog_receive_notifications_when_youre_mentioned_in_muted_chats">Receive notifications when youre mentioned in muted chats?</string>
<string name="GroupMentionSettingDialog_always_notify_me">Always notify me</string>
<string name="GroupMentionSettingDialog_dont_notify_me">Don\'t notify me</string>
<!-- ManageProfileFragment -->
<!-- Explanation text about usernames etc displayed underneath buttons to view and edit username etc -->
<string name="ManageProfileFragment__your_username">Your username, QR code and link aren\'t visible on your profile. Only share your username with people you trust.</string>
@@ -3265,7 +3259,7 @@
<!-- Title for a dialog where a user chooses how long they\'d like to mute notifications for -->
<string name="MuteDialog_mute_notifications">Mute notifications</string>
<!-- Dialog option that, when pressed, will open a time picker to let the user choose how long they\'d like to mute notifications for -->
<string name="MuteDialog__mute_until">Mute until…</string>
<string name="MuteDialog__mute_until">Until…</string>
<!-- MuteUntilTimePickerBottomSheet -->
<!-- Title for a dialog where a user chooses how long they\'d like to mute notifications for -->
@@ -4133,10 +4127,10 @@
<string name="arrays__use_default">Use default</string>
<string name="arrays__use_custom">Use custom</string>
<string name="arrays__mute_for_one_hour">Mute for 1 hour</string>
<string name="arrays__mute_for_eight_hours">Mute for 8 hours</string>
<string name="arrays__mute_for_one_day">Mute for 1 day</string>
<string name="arrays__mute_for_seven_days">Mute for 7 days</string>
<string name="arrays__mute_for_one_hour">1 Hour</string>
<string name="arrays__mute_for_eight_hours">8 Hours</string>
<string name="arrays__mute_for_one_day">1 Day</string>
<string name="arrays__mute_for_seven_days">1 Week</string>
<string name="arrays__always">Always</string>
<string name="arrays__settings_default">Settings default</string>
@@ -4191,6 +4185,10 @@
<!-- Legacy preference menu item to choose the interval of inactivity before lockout -->
<string name="preferences__inactivity_timeout_interval">Inactivity timeout interval</string>
<string name="preferences__notifications">Notifications</string>
<!-- Preference item menu to enable notifications -->
<string name="preferences__enable_notifications">Enable notifications</string>
<!-- Preference header for call notifications -->
<string name="preferences__call_notifications">Call notifications</string>
<string name="preferences__led_color">LED color</string>
<string name="preferences__led_color_unknown">Unknown</string>
<string name="preferences__pref_led_blink_title">LED blink pattern</string>
@@ -4348,6 +4346,16 @@
<string name="preference_data_and_storage__using_less_data_may_improve_calls_on_bad_networks">Using less data may improve calls on bad networks.</string>
<string name="preferences_notifications__in_chat_sounds">In-chat sounds</string>
<string name="preferences_notifications__show">Show</string>
<!-- Preference item to set notification settings when chats are muted -->
<string name="preferences_notifications__while_muted">While muted</string>
<!-- Option in settings to turn on reaction notifications -->
<string name="preferences_notifications__reaction">Reaction notifications</string>
<!-- Body option in settings to turn on reaction notifications -->
<string name="preferences_notifications__notify_reaction">Show notifications for reactions to messages you send</string>
<!-- Option in settings to turn on unread reminders -->
<string name="preferences_notifications__unread">Unread reminders</string>
<!-- Body option to turn on unread reminders -->
<string name="preferences_notifications__notify_unread">Occasionally notify when there are unread messages in muted chats</string>
<string name="preferences_notifications__ringtone">Ringtone</string>
<string name="preferences_chats__message_text_size">Message font size</string>
<!-- Option in settings that will provide a shortcut to notification system settings to control the priority of Signal notifications -->
@@ -5922,8 +5930,19 @@
<!-- NotificationsSettingsFragment -->
<string name="NotificationsSettingsFragment__messages">Messages</string>
<string name="NotificationsSettingsFragment__calls">Calls</string>
<string name="NotificationsSettingsFragment__notify_when">Notify when…</string>
<!-- Notification header for sounds -->
<string name="NotificationsSettingsFragment__sounds">Sounds</string>
<string name="NotificationsSettingsFragment__contact_joins_signal">Contact joins Signal</string>
<!-- Notification body to get notified when someone joins Signal -->
<string name="NotificationsSettingsFragment__notify_contact">Show a notification when a phone contact joins Signal</string>
<!-- Option to reset their notification settings -->
<string name="NotificationsSettingsFragment__reset">Reset notification settings</string>
<!-- Option body to reset their notification settings -->
<string name="NotificationsSettingsFragment__reset_notifications">Reset notification settings to default, including custom settings for your chats</string>
<!-- Dialog to reset their notification settings -->
<string name="NotificationsSettingsFragment__reset_body">Reset all notification settings to default for all chats?</string>
<!-- Confirmation button to reset dialog -->
<string name="NotificationsSettingsFragment__reset_confirm">Reset</string>
<!-- Notification preference header -->
<string name="NotificationsSettingsFragment__notification_profiles">Notification profiles</string>
<!-- Notification preference option header -->
@@ -5931,6 +5950,27 @@
<!-- Notification preference summary text -->
<string name="NotificationsSettingsFragment__create_a_profile_to_receive_notifications_only_from_people_and_groups_you_choose">Create a profile to receive notifications only from people and groups you choose.</string>
<!-- Header to describe screen that shows while muted settings -->
<string name="MutedNotificationsFragment__while">While muted</string>
<!-- Title for toggle row to control call settings while muted -->
<string name="MutedNotificationsFragment__calls">Calls</string>
<!-- Description for toggle row to control call settings while muted -->
<string name="MutedNotificationsFragment__calls_body_global">Ring or notify when a call is started in muted chats.</string>
<!-- Title for toggle row to control mentions settings while muted -->
<string name="MutedNotificationsFragment__mentions">Mentions</string>
<!-- Description for toggle row to control mentions settings while muted -->
<string name="MutedNotificationsFragment__mentions_body_global">Notify when you are mentioned in muted chats.</string>
<!-- Title for toggle row to control replies settings while muted -->
<string name="MutedNotificationsFragment__replies">Replies</string>
<!-- Description for toggle row to control replies settings while muted -->
<string name="MutedNotificationsFragment__replies_body_global">Notify when someone replies to your message in muted chats.</string>
<!-- Description for toggle row to control call settings while muted -->
<string name="MutedNotificationsFragment__calls_body">Ring or notify when a call is started while this chat is muted.</string>
<!-- Description for toggle row to control mentions settings while muted -->
<string name="MutedNotificationsFragment__mentions_body">Notify when you are mentioned while this chat is muted.</string>
<!-- Description for toggle row to control replies settings while muted -->
<string name="MutedNotificationsFragment__replies_body">Notify when someone replies to your message while this chat is muted.</string>
<!-- NotificationProfilesFragment -->
<!-- Title for notification profiles screen that shows all existing profiles; Title with hyphenation. Translation can use soft hyphen - Unicode U+00AD -->
<string name="NotificationProfilesFragment__notification_profiles">Notification profiles</string>
@@ -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
@@ -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