Add improved notification settings when muted.

This commit is contained in:
Greyson Parrelli
2026-03-02 13:33:53 -05:00
parent 8a36425cac
commit a95ebb2158
30 changed files with 800 additions and 72 deletions

View File

@@ -62,7 +62,7 @@ class ChatArchiveExporter(private val cursor: Cursor, private val db: SignalData
expireTimerVersion = cursor.requireInt(RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION),
muteUntilMs = cursor.requireLong(RecipientTable.MUTE_UNTIL).takeIf { it > 0 },
markedUnread = ThreadTable.ReadStatus.deserialize(cursor.requireInt(ThreadTable.READ)) == ThreadTable.ReadStatus.FORCED_UNREAD,
dontNotifyForMentionsIfMuted = RecipientTable.MentionSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING),
dontNotifyForMentionsIfMuted = RecipientTable.NotificationSetting.DO_NOT_NOTIFY.id == cursor.requireInt(RecipientTable.MENTION_SETTING),
style = ChatStyleConverter.constructRemoteChatStyle(
db = db,
chatColors = chatColors,

View File

@@ -60,7 +60,7 @@ object ChatArchiveImporter {
.update(
RecipientTable.TABLE_NAME,
contentValuesOf(
RecipientTable.MENTION_SETTING to (if (chat.dontNotifyForMentionsIfMuted) RecipientTable.MentionSetting.DO_NOT_NOTIFY.id else RecipientTable.MentionSetting.ALWAYS_NOTIFY.id),
RecipientTable.MENTION_SETTING to (if (chat.dontNotifyForMentionsIfMuted) RecipientTable.NotificationSetting.DO_NOT_NOTIFY.id else RecipientTable.NotificationSetting.ALWAYS_NOTIFY.id),
RecipientTable.MUTE_UNTIL to (chat.muteUntilMs ?: 0),
RecipientTable.MESSAGE_EXPIRATION_TIME to (chat.expirationTimerMs?.milliseconds?.inWholeSeconds ?: 0),
RecipientTable.MESSAGE_EXPIRATION_TIME_VERSION to chat.expireTimerVersion,

View File

@@ -592,11 +592,19 @@ class ConversationSettingsFragment :
if (!state.recipient.isSelf) {
clickPref(
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__sounds_and_notifications),
title = if (RemoteConfig.internalUser) {
DSLSettingsText.from("${getString(R.string.ConversationSettingsFragment__sounds_and_notifications)} (Internal Only)")
} else {
DSLSettingsText.from(R.string.ConversationSettingsFragment__sounds_and_notifications)
},
icon = DSLSettingsIcon.from(R.drawable.symbol_speaker_24),
isEnabled = !state.isDeprecatedOrUnregistered,
onClick = {
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(state.recipient.id)
val action = if (RemoteConfig.internalUser) {
ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment2(state.recipient.id)
} else {
ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToSoundsAndNotificationsSettingsFragment(state.recipient.id)
}
navController.safeNavigate(action)
}

View File

@@ -0,0 +1,57 @@
/*
* 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
/**
* Represents all user-driven actions that can occur on the Sounds & Notifications settings screen.
*/
sealed interface SoundsAndNotificationsEvent {
/**
* Mutes notifications for this recipient until the given epoch-millisecond timestamp.
*
* @param muteUntil Epoch-millisecond timestamp after which notifications should resume.
* Use [Long.MAX_VALUE] to mute indefinitely.
*/
data class SetMuteUntil(val muteUntil: Long) : SoundsAndNotificationsEvent
/**
* Clears any active mute, immediately restoring notifications for this recipient.
*/
data object Unmute : SoundsAndNotificationsEvent
/**
* Updates the mention notification setting for this recipient.
* Only relevant for group conversations that support @mentions.
*
* @param setting The new [NotificationSetting] to apply for @mention notifications.
*/
data class SetMentionSetting(val setting: NotificationSetting) : SoundsAndNotificationsEvent
/**
* Updates the call notification setting for this recipient.
* Controls whether incoming calls still produce notifications while the conversation is muted.
*
* @param setting The new [NotificationSetting] to apply for call notifications.
*/
data class SetCallNotificationSetting(val setting: NotificationSetting) : SoundsAndNotificationsEvent
/**
* Updates the reply notification setting for this recipient.
* Controls whether replies directed at the current user still produce notifications while muted.
*
* @param setting The new [NotificationSetting] to apply for reply notifications.
*/
data class SetReplyNotificationSetting(val setting: NotificationSetting) : SoundsAndNotificationsEvent
/**
* Signals that the user tapped the "Custom Notifications" row and wishes to navigate to the
* [custom notifications settings screen][org.thoughtcrime.securesms.components.settings.conversation.sounds.custom.CustomNotificationsSettingsFragment].
*/
data object NavigateToCustomNotifications : SoundsAndNotificationsEvent
}

View File

@@ -82,7 +82,7 @@ class SoundsAndNotificationsSettingsFragment :
)
if (state.hasMentionsSupport) {
val mentionSelection = if (state.mentionSetting == RecipientTable.MentionSetting.ALWAYS_NOTIFY) {
val mentionSelection = if (state.mentionSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY) {
0
} else {
1
@@ -96,9 +96,9 @@ class SoundsAndNotificationsSettingsFragment :
onSelected = {
viewModel.setMentionSetting(
if (it == 0) {
RecipientTable.MentionSetting.ALWAYS_NOTIFY
RecipientTable.NotificationSetting.ALWAYS_NOTIFY
} else {
RecipientTable.MentionSetting.DO_NOT_NOTIFY
RecipientTable.NotificationSetting.DO_NOT_NOTIFY
}
)
}

View File

@@ -0,0 +1,59 @@
/*
* 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))
}
}
)
}
}

View File

@@ -25,7 +25,7 @@ class SoundsAndNotificationsSettingsRepository(private val context: Context) {
}
}
fun setMentionSetting(recipientId: RecipientId, mentionSetting: RecipientTable.MentionSetting) {
fun setMentionSetting(recipientId: RecipientId, mentionSetting: RecipientTable.NotificationSetting) {
SignalExecutors.BOUNDED.execute {
SignalDatabase.recipients.setMentionSetting(recipientId, mentionSetting)
}

View File

@@ -0,0 +1,309 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import org.signal.core.ui.compose.DayNightPreviews
import org.signal.core.ui.compose.Dialogs
import org.signal.core.ui.compose.Dividers
import org.signal.core.ui.compose.Previews
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
@Composable
fun SoundsAndNotificationsSettingsScreen(
state: SoundsAndNotificationsSettingsState2,
formatMuteUntil: (Long) -> String,
onEvent: (SoundsAndNotificationsEvent) -> Unit,
onNavigationClick: () -> Unit,
onMuteClick: () -> Unit
) {
val isMuted = state.muteUntil > 0
var showUnmuteDialog by remember { mutableStateOf(false) }
Scaffolds.Settings(
title = stringResource(R.string.ConversationSettingsFragment__sounds_and_notifications),
onNavigationClick = onNavigationClick,
navigationIcon = SignalIcons.ArrowStart.imageVector,
navigationContentDescription = stringResource(R.string.CallScreenTopBar__go_back)
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues)
) {
// Custom notifications
item {
val summary = if (state.hasCustomNotificationSettings) {
stringResource(R.string.preferences_on)
} else {
stringResource(R.string.preferences_off)
}
Rows.TextRow(
text = stringResource(R.string.SoundsAndNotificationsSettingsFragment__custom_notifications),
label = summary,
icon = painterResource(R.drawable.ic_speaker_24),
onClick = { onEvent(SoundsAndNotificationsEvent.NavigateToCustomNotifications) }
)
}
// Mute
item {
val muteSummary = if (isMuted) {
formatMuteUntil(state.muteUntil)
} else {
stringResource(R.string.SoundsAndNotificationsSettingsFragment__not_muted)
}
val muteIcon = if (isMuted) {
R.drawable.ic_bell_disabled_24
} else {
R.drawable.ic_bell_24
}
Rows.TextRow(
text = stringResource(R.string.SoundsAndNotificationsSettingsFragment__mute_notifications),
label = muteSummary,
icon = painterResource(muteIcon),
onClick = {
if (isMuted) showUnmuteDialog = true else onMuteClick()
}
)
}
// 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) {
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)) }
)
}
}
// 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)) }
)
}
}
}
}
if (showUnmuteDialog) {
Dialogs.SimpleAlertDialog(
title = Dialogs.NoTitle,
body = formatMuteUntil(state.muteUntil),
confirm = stringResource(R.string.ConversationSettingsFragment__unmute),
dismiss = stringResource(android.R.string.cancel),
onConfirm = { onEvent(SoundsAndNotificationsEvent.Unmute) },
onDismiss = { showUnmuteDialog = false }
)
}
}
@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
}
)
}
}
@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)
)
}
}
}
}
}
}
}
@DayNightPreviews
@Composable
private fun SoundsAndNotificationsSettingsScreenMutedPreview() {
Previews.Preview {
SoundsAndNotificationsSettingsScreen(
state = SoundsAndNotificationsSettingsState2(
muteUntil = Long.MAX_VALUE,
callNotificationSetting = NotificationSetting.ALWAYS_NOTIFY,
mentionSetting = NotificationSetting.ALWAYS_NOTIFY,
replyNotificationSetting = NotificationSetting.DO_NOT_NOTIFY,
hasMentionsSupport = true,
hasCustomNotificationSettings = false,
channelConsistencyCheckComplete = true
),
formatMuteUntil = { "Always" },
onEvent = {},
onNavigationClick = {},
onMuteClick = {}
)
}
}
@DayNightPreviews
@Composable
private fun SoundsAndNotificationsSettingsScreenUnmutedPreview() {
Previews.Preview {
SoundsAndNotificationsSettingsScreen(
state = SoundsAndNotificationsSettingsState2(
muteUntil = 0L,
callNotificationSetting = NotificationSetting.ALWAYS_NOTIFY,
mentionSetting = NotificationSetting.ALWAYS_NOTIFY,
replyNotificationSetting = NotificationSetting.ALWAYS_NOTIFY,
hasMentionsSupport = false,
hasCustomNotificationSettings = true,
channelConsistencyCheckComplete = true
),
formatMuteUntil = { "" },
onEvent = {},
onNavigationClick = {},
onMuteClick = {}
)
}
}

View File

@@ -7,7 +7,7 @@ import org.thoughtcrime.securesms.recipients.RecipientId
data class SoundsAndNotificationsSettingsState(
val recipientId: RecipientId = Recipient.UNKNOWN.id,
val muteUntil: Long = 0L,
val mentionSetting: RecipientTable.MentionSetting = RecipientTable.MentionSetting.DO_NOT_NOTIFY,
val mentionSetting: RecipientTable.NotificationSetting = RecipientTable.NotificationSetting.DO_NOT_NOTIFY,
val hasCustomNotificationSettings: Boolean = false,
val hasMentionsSupport: Boolean = false,
val channelConsistencyCheckComplete: Boolean = false

View File

@@ -0,0 +1,23 @@
/*
* 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
}

View File

@@ -38,7 +38,7 @@ class SoundsAndNotificationsSettingsViewModel(
repository.setMuteUntil(recipientId, 0L)
}
fun setMentionSetting(mentionSetting: RecipientTable.MentionSetting) {
fun setMentionSetting(mentionSetting: RecipientTable.NotificationSetting) {
repository.setMentionSetting(recipientId, mentionSetting)
}

View File

@@ -0,0 +1,102 @@
/*
* 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.IO) {
SignalDatabase.recipients.setMuted(recipientId, muteUntil)
}
}
private fun applySetMentionSetting(setting: NotificationSetting) {
viewModelScope.launch(Dispatchers.IO) {
SignalDatabase.recipients.setMentionSetting(recipientId, setting)
}
}
private fun applySetCallNotificationSetting(setting: NotificationSetting) {
viewModelScope.launch(Dispatchers.IO) {
SignalDatabase.recipients.setCallNotificationSetting(recipientId, setting)
}
}
private fun applySetReplyNotificationSetting(setting: NotificationSetting) {
viewModelScope.launch(Dispatchers.IO) {
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)))
}
}
}

View File

@@ -168,6 +168,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
const val STORAGE_SERVICE_ID = "storage_service_id"
const val STORAGE_SERVICE_PROTO = "storage_service_proto"
const val MENTION_SETTING = "mention_setting"
const val CALL_NOTIFICATION_SETTING = "call_notification_setting"
const val REPLY_NOTIFICATION_SETTING = "reply_notification_setting"
const val CAPABILITIES = "capabilities"
const val LAST_SESSION_RESET = "last_session_reset"
const val WALLPAPER = "wallpaper"
@@ -241,7 +243,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 ${MentionSetting.ALWAYS_NOTIFY.id},
$MENTION_SETTING INTEGER DEFAULT ${NotificationSetting.ALWAYS_NOTIFY.id},
$CAPABILITIES INTEGER DEFAULT 0,
$LAST_SESSION_RESET BLOB DEFAULT NULL,
$WALLPAPER BLOB DEFAULT NULL,
@@ -264,7 +266,9 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
$NICKNAME_JOINED_NAME TEXT DEFAULT NULL,
$NOTE TEXT DEFAULT NULL,
$MESSAGE_EXPIRATION_TIME_VERSION INTEGER DEFAULT 1 NOT NULL,
$KEY_TRANSPARENCY_DATA BLOB DEFAULT 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}
)
"""
@@ -313,6 +317,8 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
SEALED_SENDER_MODE,
STORAGE_SERVICE_ID,
MENTION_SETTING,
CALL_NOTIFICATION_SETTING,
REPLY_NOTIFICATION_SETTING,
CAPABILITIES,
WALLPAPER,
WALLPAPER_URI,
@@ -1645,7 +1651,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
}
fun setMentionSetting(id: RecipientId, mentionSetting: MentionSetting) {
fun setMentionSetting(id: RecipientId, mentionSetting: NotificationSetting) {
val values = ContentValues().apply {
put(MENTION_SETTING, mentionSetting.id)
}
@@ -1656,6 +1662,30 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
}
fun setCallNotificationSetting(id: RecipientId, setting: NotificationSetting) {
val values = ContentValues().apply {
put(CALL_NOTIFICATION_SETTING, setting.id)
}
if (update(id, values)) {
// TODO rotate storageId once this is actually synced in storage service
// rotateStorageId(id)
AppDependencies.databaseObserver.notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
fun setReplyNotificationSetting(id: RecipientId, setting: NotificationSetting) {
val values = ContentValues().apply {
put(REPLY_NOTIFICATION_SETTING, setting.id)
}
if (update(id, values)) {
// TODO rotate storageId once this is actually synced in storage service
// rotateStorageId(id)
AppDependencies.databaseObserver.notifyRecipientChanged(id)
StorageSyncHelper.scheduleSyncForDataChange()
}
}
/**
* Updates the profile key.
*
@@ -4147,7 +4177,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
SYSTEM_CONTACT_URI to secondaryRecord.systemContactUri,
PROFILE_SHARING to (primaryRecord.profileSharing || secondaryRecord.profileSharing),
CAPABILITIES to max(primaryRecord.capabilities.rawBits, secondaryRecord.capabilities.rawBits),
MENTION_SETTING to if (primaryRecord.mentionSetting != MentionSetting.ALWAYS_NOTIFY) primaryRecord.mentionSetting.id else secondaryRecord.mentionSetting.id,
MENTION_SETTING to if (primaryRecord.mentionSetting != NotificationSetting.ALWAYS_NOTIFY) primaryRecord.mentionSetting.id else secondaryRecord.mentionSetting.id,
PNI_SIGNATURE_VERIFIED to pniVerified.toInt()
)
@@ -4280,7 +4310,7 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
put(BLOCKED, if (groupV2.proto.blocked) "1" else "0")
put(MUTE_UNTIL, groupV2.proto.mutedUntilTimestamp)
put(STORAGE_SERVICE_ID, Base64.encodeWithPadding(groupV2.id.raw))
put(MENTION_SETTING, if (groupV2.proto.dontNotifyForMentionsIfMuted) MentionSetting.DO_NOT_NOTIFY.id else MentionSetting.ALWAYS_NOTIFY.id)
put(MENTION_SETTING, if (groupV2.proto.dontNotifyForMentionsIfMuted) NotificationSetting.DO_NOT_NOTIFY.id else NotificationSetting.ALWAYS_NOTIFY.id)
if (groupV2.proto.hasUnknownFields()) {
put(STORAGE_SERVICE_PROTO, Base64.encodeWithPadding(groupV2.serializedUnknowns!!))
@@ -4861,12 +4891,12 @@ open class RecipientTable(context: Context, databaseHelper: SignalDatabase) : Da
}
}
enum class MentionSetting(val id: Int) {
enum class NotificationSetting(val id: Int) {
ALWAYS_NOTIFY(0),
DO_NOT_NOTIFY(1);
companion object {
fun fromId(id: Int): MentionSetting {
fun fromId(id: Int): NotificationSetting {
return entries[id]
}
}

View File

@@ -150,7 +150,9 @@ object RecipientTableCursorUtil {
sealedSenderAccessMode = RecipientTable.SealedSenderAccessMode.fromMode(cursor.requireInt(RecipientTable.SEALED_SENDER_MODE)),
capabilities = readCapabilities(cursor),
storageId = Base64.decodeNullableOrThrow(cursor.requireString(RecipientTable.STORAGE_SERVICE_ID)),
mentionSetting = RecipientTable.MentionSetting.fromId(cursor.requireInt(RecipientTable.MENTION_SETTING)),
mentionSetting = RecipientTable.NotificationSetting.fromId(cursor.requireInt(RecipientTable.MENTION_SETTING)),
callNotificationSetting = RecipientTable.NotificationSetting.fromId(cursor.requireInt(RecipientTable.CALL_NOTIFICATION_SETTING)),
replyNotificationSetting = RecipientTable.NotificationSetting.fromId(cursor.requireInt(RecipientTable.REPLY_NOTIFICATION_SETTING)),
wallpaper = chatWallpaper,
chatColors = chatColors,
avatarColor = avatarColor,

View File

@@ -157,6 +157,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V300_AddKeyTranspar
import org.thoughtcrime.securesms.database.helpers.migration.V301_RemoveCallLinkEpoch
import org.thoughtcrime.securesms.database.helpers.migration.V302_AddDeletedByColumn
import org.thoughtcrime.securesms.database.helpers.migration.V303_CaseInsensitiveUsernames
import org.thoughtcrime.securesms.database.helpers.migration.V304_CallAndReplyNotificationSettings
import org.thoughtcrime.securesms.database.SQLiteDatabase as SignalSqliteDatabase
/**
@@ -320,10 +321,11 @@ object SignalDatabaseMigrations {
300 to V300_AddKeyTransparencyColumn,
301 to V301_RemoveCallLinkEpoch,
302 to V302_AddDeletedByColumn,
303 to V303_CaseInsensitiveUsernames
303 to V303_CaseInsensitiveUsernames,
304 to V304_CallAndReplyNotificationSettings
)
const val DATABASE_VERSION = 303
const val DATABASE_VERSION = 304
@JvmStatic
fun migrate(context: Application, db: SignalSqliteDatabase, oldVersion: Int, newVersion: Int) {

View File

@@ -0,0 +1,15 @@
package org.thoughtcrime.securesms.database.helpers.migration
import android.app.Application
import org.thoughtcrime.securesms.database.SQLiteDatabase
/**
* Adds per-conversation notification settings for calls and replies when muted.
*/
@Suppress("ClassName")
object V304_CallAndReplyNotificationSettings : SignalDatabaseMigration {
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
db.execSQL("ALTER TABLE recipient ADD COLUMN call_notification_setting INTEGER DEFAULT 0")
db.execSQL("ALTER TABLE recipient ADD COLUMN reply_notification_setting INTEGER DEFAULT 0")
}
}

View File

@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.database.IdentityTable.VerifiedStatus
import org.thoughtcrime.securesms.database.RecipientTable
import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting
import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting
import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState
import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState
import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode
@@ -64,7 +64,9 @@ data class RecipientRecord(
val sealedSenderAccessMode: SealedSenderAccessMode,
val capabilities: Capabilities,
val storageId: ByteArray?,
val mentionSetting: MentionSetting,
val mentionSetting: NotificationSetting,
val callNotificationSetting: NotificationSetting,
val replyNotificationSetting: NotificationSetting,
val wallpaper: ChatWallpaper?,
val chatColors: ChatColors?,
val avatarColor: AvatarColor,

View File

@@ -14,11 +14,11 @@ import androidx.core.util.Consumer;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting;
import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting;
public final class GroupMentionSettingDialog {
public static void show(@NonNull Context context, @NonNull MentionSetting mentionSetting, @Nullable Consumer<MentionSetting> callback) {
public static void show(@NonNull Context context, @NonNull NotificationSetting mentionSetting, @Nullable Consumer<NotificationSetting> callback) {
SelectionCallback selectionCallback = new SelectionCallback(mentionSetting, callback);
new MaterialAlertDialogBuilder(context)
@@ -30,7 +30,7 @@ public final class GroupMentionSettingDialog {
}
@SuppressLint("InflateParams")
private static View getView(@NonNull Context context, @NonNull MentionSetting mentionSetting, @NonNull SelectionCallback selectionCallback) {
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);
@@ -40,9 +40,9 @@ public final class GroupMentionSettingDialog {
dontNotify.setChecked(dontNotify == v);
if (alwaysNotify.isChecked()) {
selectionCallback.selection = MentionSetting.ALWAYS_NOTIFY;
selectionCallback.selection = NotificationSetting.ALWAYS_NOTIFY;
} else if (dontNotify.isChecked()) {
selectionCallback.selection = MentionSetting.DO_NOT_NOTIFY;
selectionCallback.selection = NotificationSetting.DO_NOT_NOTIFY;
}
};
@@ -63,11 +63,11 @@ public final class GroupMentionSettingDialog {
private static class SelectionCallback implements DialogInterface.OnClickListener {
@NonNull private final MentionSetting previousMentionSetting;
@NonNull private MentionSetting selection;
@Nullable private final Consumer<MentionSetting> callback;
@NonNull private final NotificationSetting previousMentionSetting;
@NonNull private NotificationSetting selection;
@Nullable private final Consumer<NotificationSetting> callback;
public SelectionCallback(@NonNull MentionSetting previousMentionSetting, @Nullable Consumer<MentionSetting> callback) {
public SelectionCallback(@NonNull NotificationSetting previousMentionSetting, @Nullable Consumer<NotificationSetting> callback) {
this.previousMentionSetting = previousMentionSetting;
this.selection = previousMentionSetting;
this.callback = callback;

View File

@@ -13,6 +13,7 @@ import androidx.annotation.WorkerThread;
import org.signal.core.util.CursorUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -27,9 +28,31 @@ public final class DoNotDisturbUtil {
private DoNotDisturbUtil() {
}
/**
* Checks whether the user should be disturbed with a call from the given recipient,
* taking into account the recipient's mute and call notification settings as well as
* the system Do Not Disturb state.
*
* For group recipients, only the system interruption filter is checked (no contact priority).
* For 1:1 recipients, the full DND policy including contact priority is evaluated.
*/
@WorkerThread
@SuppressLint("SwitchIntDef")
public static boolean shouldDisturbUserWithCall(@NonNull Context context) {
public static boolean shouldDisturbUserWithCall(@NonNull Context context, @NonNull Recipient recipient) {
if (recipient.isMuted() && recipient.getCallNotificationSetting() == RecipientTable.NotificationSetting.DO_NOT_NOTIFY) {
return false;
}
if (recipient.isGroup()) {
return checkSystemDnd(context);
} else {
return checkSystemDndWithContactPriority(context, recipient);
}
}
@WorkerThread
@SuppressLint("SwitchIntDef")
private static boolean checkSystemDnd(@NonNull Context context) {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
switch (notificationManager.getCurrentInterruptionFilter()) {
@@ -43,7 +66,7 @@ public final class DoNotDisturbUtil {
@WorkerThread
@SuppressLint("SwitchIntDef")
public static boolean shouldDisturbUserWithCall(@NonNull Context context, @NonNull Recipient recipient) {
private static boolean checkSystemDndWithContactPriority(@NonNull Context context, @NonNull Recipient recipient) {
NotificationManager notificationManager = ServiceUtil.getNotificationManager(context);
switch (notificationManager.getCurrentInterruptionFilter()) {

View File

@@ -167,13 +167,13 @@ object NotificationStateProvider {
isUnreadMessage &&
!messageRecord.isOutgoing &&
isGroupStoryReply &&
(isParentStorySentBySelf || messageRecord.hasSelfMentionOrQuoteOfSelf() || (hasSelfRepliedToStory && !messageRecord.isStoryReaction()))
(isParentStorySentBySelf || messageRecord.hasGroupQuoteOrSelfMention() || (hasSelfRepliedToStory && !messageRecord.isStoryReaction()))
fun includeMessage(notificationProfile: NotificationProfile?): MessageInclusion {
return if (isUnreadIncoming || stickyThread || isNotifiableGroupStoryMessage || isIncomingMissedCall) {
if (threadRecipient.isMuted && (threadRecipient.isDoNotNotifyMentions || !messageRecord.shouldBreakThroughMute(threadRecipient))) {
if (threadRecipient.isMuted && !breaksThroughMute()) {
MessageInclusion.MUTE_FILTERED
} else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id) && !(notificationProfile.allowAllMentions && messageRecord.shouldBreakThroughMute(threadRecipient))) {
} else if (notificationProfile != null && !notificationProfile.isRecipientAllowed(threadRecipient.id) && !(notificationProfile.allowAllMentions && messageRecord.hasGroupQuoteOrSelfMention())) {
MessageInclusion.PROFILE_FILTERED
} else {
MessageInclusion.INCLUDE
@@ -183,6 +183,19 @@ object NotificationStateProvider {
}
}
private fun breaksThroughMute(): Boolean {
return when {
isIncomingMissedCall -> threadRecipient.callNotificationSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY
messageRecord.hasSelfMention() -> threadRecipient.mentionSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY
messageRecord.isQuoteOfSelf() -> threadRecipient.replyNotificationSetting == RecipientTable.NotificationSetting.ALWAYS_NOTIFY
else -> false
}
}
private fun MessageRecord.isQuoteOfSelf(): Boolean {
return this is MmsMessageRecord && quote?.author == Recipient.self().id
}
fun includeReaction(reaction: ReactionRecord, notificationProfile: NotificationProfile?): MessageInclusion {
return if (threadRecipient.isMuted) {
MessageInclusion.MUTE_FILTERED
@@ -207,19 +220,12 @@ object NotificationStateProvider {
}
}
private val Recipient.isDoNotNotifyMentions: Boolean
get() = mentionSetting == RecipientTable.MentionSetting.DO_NOT_NOTIFY
private fun MessageRecord.shouldBreakThroughMute(threadRecipient: Recipient): Boolean {
private fun MessageRecord.hasGroupQuoteOrSelfMention(): Boolean {
if (!threadRecipient.isGroup) {
return false
}
return hasSelfMention() || (this is MmsMessageRecord && quote?.author == Recipient.self().id)
}
private fun MessageRecord.hasSelfMentionOrQuoteOfSelf(): Boolean {
return hasSelfMention() || (this is MmsMessageRecord && quote?.author == Recipient.self().id)
}
}
private enum class MessageInclusion {

View File

@@ -28,8 +28,8 @@ import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.conversation.colors.ChatColors
import org.thoughtcrime.securesms.conversation.colors.ChatColors.Id.Auto
import org.thoughtcrime.securesms.conversation.colors.ChatColorsPalette
import org.thoughtcrime.securesms.database.RecipientTable.MentionSetting
import org.thoughtcrime.securesms.database.RecipientTable.MissingRecipientException
import org.thoughtcrime.securesms.database.RecipientTable.NotificationSetting
import org.thoughtcrime.securesms.database.RecipientTable.PhoneNumberSharingState
import org.thoughtcrime.securesms.database.RecipientTable.RegisteredState
import org.thoughtcrime.securesms.database.RecipientTable.SealedSenderAccessMode
@@ -48,6 +48,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.UsernameUtil.isValidUsernameForSearch
import org.thoughtcrime.securesms.wallpaper.ChatWallpaper
@@ -103,7 +104,9 @@ class Recipient(
private val sealedSenderAccessModeValue: SealedSenderAccessMode = SealedSenderAccessMode.UNKNOWN,
private val capabilities: RecipientRecord.Capabilities = RecipientRecord.Capabilities.UNKNOWN,
val storageId: ByteArray? = null,
val mentionSetting: MentionSetting = MentionSetting.ALWAYS_NOTIFY,
val mentionSetting: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY,
private val callNotificationSettingValue: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY,
private val replyNotificationSettingValue: NotificationSetting = NotificationSetting.ALWAYS_NOTIFY,
private val wallpaperValue: ChatWallpaper? = null,
private val chatColorsValue: ChatColors? = null,
val avatarColor: AvatarColor = AvatarColor.UNKNOWN,
@@ -329,6 +332,14 @@ 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 calls should break through mute for this recipient. */
val callNotificationSetting: NotificationSetting
get() = if (RemoteConfig.internalUser) callNotificationSettingValue else NotificationSetting.ALWAYS_NOTIFY
/** Whether replies should break through mute for this recipient. Only applicable to groups. */
val replyNotificationSetting: NotificationSetting
get() = if (groupIdValue == null) NotificationSetting.DO_NOT_NOTIFY else if (RemoteConfig.internalUser) replyNotificationSettingValue else mentionSetting
/** The state around whether we can send sealed sender to this user. */
val sealedSenderAccessMode: SealedSenderAccessMode = if (pni.isPresent && pni == serviceId) {
SealedSenderAccessMode.DISABLED
@@ -810,6 +821,8 @@ class Recipient(
notificationChannelValue == other.notificationChannelValue &&
sealedSenderAccessModeValue == other.sealedSenderAccessModeValue &&
mentionSetting == other.mentionSetting &&
callNotificationSettingValue == other.callNotificationSettingValue &&
replyNotificationSettingValue == other.replyNotificationSettingValue &&
wallpaperValue == other.wallpaperValue &&
chatColorsValue == other.chatColorsValue &&
avatarColor == other.avatarColor &&

View File

@@ -185,6 +185,8 @@ object RecipientCreator {
capabilities = record.capabilities,
storageId = record.storageId,
mentionSetting = record.mentionSetting,
callNotificationSettingValue = record.callNotificationSetting,
replyNotificationSettingValue = record.replyNotificationSetting,
wallpaperValue = record.wallpaper?.validate(),
chatColorsValue = record.chatColors,
avatarColor = avatarColor ?: record.avatarColor,

View File

@@ -13,6 +13,8 @@ import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.signal.core.util.Util;
@@ -116,8 +118,11 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
boolean isRemoteVideoOffer = currentState.getCallSetupState(remotePeer).isRemoteVideoOffer();
Recipient recipient = remotePeer.getRecipient();
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, remotePeer, isRemoteVideoOffer);
if (DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext(), recipient)) {
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_CONNECTING, remotePeer, isRemoteVideoOffer);
}
webRtcInteractor.retrieveTurnServers(remotePeer);
webRtcInteractor.initializeAudioForCall();

View File

@@ -240,18 +240,23 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
CallTable.Direction.INCOMING,
CallTable.Event.ONGOING);
if (!shouldDisturbUserWithCall) {
Log.i(TAG, "Silently ignoring call due to mute settings.");
return currentState.builder()
.changeCallInfoState()
.callState(WebRtcViewModel.State.CALL_INCOMING)
.build();
}
if (shouldDisturbUserWithCall) {
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
boolean started = webRtcInteractor.startWebRtcCallActivityIfPossible();
if (!started) {
Log.i(TAG, "Unable to start call activity due to OS version or not being in the foreground");
AppForegroundObserver.addListener(webRtcInteractor.getForegroundListener());
}
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
boolean started = webRtcInteractor.startWebRtcCallActivityIfPossible();
if (!started) {
Log.i(TAG, "Unable to start call activity due to OS version or not being in the foreground");
AppForegroundObserver.addListener(webRtcInteractor.getForegroundListener());
}
boolean isCallNotificationsEnabled = SignalStore.settings().isCallNotificationsEnabled() && NotificationChannels.getInstance().areNotificationsEnabled();
if (shouldDisturbUserWithCall && isCallNotificationsEnabled) {
if (isCallNotificationsEnabled) {
Uri ringtone = recipient.resolve().getCallRingtone();
RecipientTable.VibrateState vibrateState = recipient.resolve().getCallVibrate();

View File

@@ -121,33 +121,36 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
currentState = WebRtcVideoUtil.initializeVideo(context, webRtcInteractor.getCameraEventListener(), currentState, RemotePeer.GROUP_CALL_ID.longValue());
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, remotePeerGroup, true);
webRtcInteractor.initializeAudioForCall();
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext());
if (shouldDisturbUserWithCall) {
boolean shouldDisturbUserWithCall = DoNotDisturbUtil.shouldDisturbUserWithCall(context.getApplicationContext(), recipient.resolve());
if (!shouldDisturbUserWithCall) {
Log.i(TAG, "Silently ignoring group ring due to mute settings.");
} else {
webRtcInteractor.setCallInProgressNotification(TYPE_INCOMING_RINGING, remotePeerGroup, true);
webRtcInteractor.updatePhoneState(LockManager.PhoneState.INTERACTIVE);
boolean started = webRtcInteractor.startWebRtcCallActivityIfPossible();
if (!started) {
Log.i(TAG, "Unable to start call activity due to OS version or not being in the foreground");
AppForegroundObserver.addListener(webRtcInteractor.getForegroundListener());
}
}
boolean isCallNotificationsEnabled = SignalStore.settings().isCallNotificationsEnabled() && NotificationChannels.getInstance().areNotificationsEnabled();
if (shouldDisturbUserWithCall && isCallNotificationsEnabled) {
Uri ringtone = recipient.resolve().getCallRingtone();
RecipientTable.VibrateState vibrateState = recipient.resolve().getCallVibrate();
boolean isCallNotificationsEnabled = SignalStore.settings().isCallNotificationsEnabled() && NotificationChannels.getInstance().areNotificationsEnabled();
if (isCallNotificationsEnabled) {
Uri ringtone = recipient.resolve().getCallRingtone();
RecipientTable.VibrateState vibrateState = recipient.resolve().getCallVibrate();
if (ringtone == null) {
ringtone = SignalStore.settings().getCallRingtone();
if (ringtone == null) {
ringtone = SignalStore.settings().getCallRingtone();
}
webRtcInteractor.startIncomingRinger(ringtone, vibrateState == RecipientTable.VibrateState.ENABLED || (vibrateState == RecipientTable.VibrateState.DEFAULT && SignalStore.settings().isCallVibrateEnabled()));
}
webRtcInteractor.startIncomingRinger(ringtone, vibrateState == RecipientTable.VibrateState.ENABLED || (vibrateState == RecipientTable.VibrateState.DEFAULT && SignalStore.settings().isCallVibrateEnabled()));
webRtcInteractor.registerPowerButtonReceiver();
}
webRtcInteractor.registerPowerButtonReceiver();
return currentState.builder()
.changeCallSetupState(RemotePeer.GROUP_CALL_ID)
.isRemoteVideoOffer(true)

View File

@@ -249,7 +249,7 @@ object StorageSyncModels {
archived = recipient.syncExtras.isArchived
markedUnread = recipient.syncExtras.isForcedUnread
mutedUntilTimestamp = recipient.muteUntil
dontNotifyForMentionsIfMuted = recipient.mentionSetting == RecipientTable.MentionSetting.DO_NOT_NOTIFY
dontNotifyForMentionsIfMuted = recipient.mentionSetting == RecipientTable.NotificationSetting.DO_NOT_NOTIFY
hideStory = recipient.extras != null && recipient.extras.hideStory()
avatarColor = localToRemoteAvatarColor(recipient.avatarColor)
storySendMode = when (groups.getShowAsStoryState(groupId)) {

View File

@@ -66,6 +66,20 @@
</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"
@@ -120,7 +134,8 @@
<fragment
android:id="@+id/soundsAndNotificationsSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.conversation.sounds.SoundsAndNotificationsSettingsFragment">
android:name="org.thoughtcrime.securesms.components.settings.conversation.sounds.SoundsAndNotificationsSettingsFragment"
tools:layout="@layout/dsl_settings_fragment">
<argument
android:name="recipient_id"
@@ -136,6 +151,24 @@
</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"
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>
<fragment
android:id="@+id/internalDetailsSettingsFragment"
android:name="org.thoughtcrime.securesms.components.settings.conversation.InternalConversationSettingsFragment">

View File

@@ -392,6 +392,16 @@
<item>@string/SoundsAndNotificationsSettingsFragment__do_not_notify</item>
</string-array>
<string-array name="SoundsAndNotificationsSettingsFragment__call_labels">
<item>@string/SoundsAndNotificationsSettingsFragment__always_notify</item>
<item>@string/SoundsAndNotificationsSettingsFragment__do_not_notify</item>
</string-array>
<string-array name="SoundsAndNotificationsSettingsFragment__reply_labels">
<item>@string/SoundsAndNotificationsSettingsFragment__always_notify</item>
<item>@string/SoundsAndNotificationsSettingsFragment__do_not_notify</item>
</string-array>
<integer-array name="PlaybackSpeedToggleTextView__speeds">
<item>100</item>
<item>150</item>

View File

@@ -6006,13 +6006,30 @@
<string name="PermissionsSettingsFragment__who_can_send_messages">Who can send messages and start calls?</string>
<!-- SoundsAndNotificationsSettingsFragment -->
<!-- Label for the setting to mute notifications for a conversation -->
<string name="SoundsAndNotificationsSettingsFragment__mute_notifications">Mute notifications</string>
<!-- Summary text shown when a conversation is not muted -->
<string name="SoundsAndNotificationsSettingsFragment__not_muted">Not muted</string>
<!-- Label for the mentions notification setting in conversation sounds and notifications -->
<string name="SoundsAndNotificationsSettingsFragment__mentions">Mentions</string>
<!-- Dialog option to always send notifications for the selected category even when the conversation is muted -->
<string name="SoundsAndNotificationsSettingsFragment__always_notify">Always notify</string>
<!-- Dialog option to not send notifications if mentioned when the conversation is muted -->
<!-- Dialog option to not send notifications for the selected category when the conversation is muted -->
<string name="SoundsAndNotificationsSettingsFragment__do_not_notify">Do not notify</string>
<!-- Label for the setting to configure custom notification sounds and vibration for a conversation -->
<string name="SoundsAndNotificationsSettingsFragment__custom_notifications">Custom notifications</string>
<!-- Section header for settings that control which notifications still come through when a conversation is muted -->
<string name="SoundsAndNotificationsSettingsFragment__when_muted">When muted</string>
<!-- Label for the calls notification setting in conversation sounds and notifications -->
<string name="SoundsAndNotificationsSettingsFragment__calls">Calls</string>
<!-- Explanatory text shown in the calls notification setting dialog describing what the setting controls -->
<string name="SoundsAndNotificationsSettingsFragment__calls_dialog_message">Ring and receive notifications when a call is started in muted chats.</string>
<!-- Explanatory text shown in the mentions notification setting dialog describing what the setting controls -->
<string name="SoundsAndNotificationsSettingsFragment__mentions_dialog_message">Receive notifications when you are mentioned in muted chats.</string>
<!-- Label for the replies notification setting in conversation sounds and notifications -->
<string name="SoundsAndNotificationsSettingsFragment__replies_to_you">Replies to you</string>
<!-- Explanatory text shown in the replies notification setting dialog describing what the setting controls -->
<string name="SoundsAndNotificationsSettingsFragment__replies_dialog_message">Receive notifications when someone replies to your messages in muted chats.</string>
<!-- StickerKeyboard -->
<string name="StickerKeyboard__recently_used">Recently used</string>

View File

@@ -61,7 +61,7 @@ object RecipientDatabaseTestUtils {
sealedSenderAccessMode: RecipientTable.SealedSenderAccessMode = RecipientTable.SealedSenderAccessMode.UNKNOWN,
capabilities: Long = 0L,
storageId: ByteArray? = null,
mentionSetting: RecipientTable.MentionSetting = RecipientTable.MentionSetting.ALWAYS_NOTIFY,
mentionSetting: RecipientTable.NotificationSetting = RecipientTable.NotificationSetting.ALWAYS_NOTIFY,
wallpaper: ChatWallpaper? = null,
chatColors: ChatColors? = null,
avatarColor: AvatarColor = AvatarColor.A100,
@@ -128,6 +128,8 @@ object RecipientDatabaseTestUtils {
),
storageId = storageId,
mentionSetting = mentionSetting,
callNotificationSetting = RecipientTable.NotificationSetting.ALWAYS_NOTIFY,
replyNotificationSetting = RecipientTable.NotificationSetting.ALWAYS_NOTIFY,
wallpaper = wallpaper,
chatColors = chatColors,
avatarColor = avatarColor,