Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,12 @@ class NotificationService :
AppLifecycle.Background -> {
combine(
chatNotificationRepository.messageUpdates,
notificationsSettingsDataStore.showNotifications,
) { items, enabled -> items to enabled }
notificationsSettingsDataStore.settings,
) { items, settings -> items to settings }
}
}
}.collect { (items, enabled) ->
if (!enabled) {
}.collect { (items, settings) ->
if (!settings.showNotifications) {
return@collect
}

Expand All @@ -122,7 +122,11 @@ class NotificationService :
iterator.next()
iterator.remove()
}
message.toNotificationData()?.createMentionNotification()
val notificationData = message.toNotificationData() ?: return@forEach
if (!notificationData.isWhisper && !settings.areChannelNotificationsEnabled(notificationData.channel)) {
return@forEach
}
notificationData.createMentionNotification()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,31 @@
package com.flxrs.dankchat.preferences.notifications

import com.flxrs.dankchat.data.UserName
import kotlinx.serialization.Serializable

@Serializable
data class NotificationsSettings(
val showNotifications: Boolean = true,
val showWhisperNotifications: Boolean = true,
val mentionFormat: MentionFormat = MentionFormat.Name,
)
val mutedChannels: Set<UserName> = emptySet(),
) {
fun areChannelNotificationsEnabled(channel: UserName): Boolean = channel.lowercase() !in mutedChannels

fun withChannelNotificationsEnabled(
channel: UserName,
enabled: Boolean,
): NotificationsSettings {
val normalizedChannel = channel.lowercase()
return copy(
mutedChannels =
when {
enabled -> mutedChannels - normalizedChannel
else -> mutedChannels + normalizedChannel
},
)
}
}

enum class MentionFormat(
val template: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.flxrs.dankchat.preferences.notifications

import android.content.Context
import com.flxrs.dankchat.R
import com.flxrs.dankchat.data.UserName
import com.flxrs.dankchat.di.DispatchersProvider
import com.flxrs.dankchat.utils.datastore.PreferenceKeys
import com.flxrs.dankchat.utils.datastore.booleanOrDefault
Expand Down Expand Up @@ -78,6 +79,13 @@ class NotificationsSettingsDataStore(

fun current() = currentSettings.value

suspend fun setChannelNotificationsEnabled(
channel: UserName,
enabled: Boolean,
) {
update { it.withChannelNotificationsEnabled(channel, enabled) }
}

suspend fun update(transform: suspend (NotificationsSettings) -> NotificationsSettings) {
runCatching { dataStore.updateData(transform) }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ fun FloatingToolbar(
totalMentionCount: Int,
hasActivePinnedMessage: Boolean,
isPinnedMessageShown: Boolean,
channelNotificationsEnabled: Boolean,
onAction: (ToolbarAction) -> Unit,
onAudioOnly: () -> Unit,
onStreamClose: () -> Unit,
Expand Down Expand Up @@ -797,6 +798,7 @@ fun FloatingToolbar(
) {
InlineOverflowMenu(
isLoggedIn = isLoggedIn,
channelNotificationsEnabled = channelNotificationsEnabled,
onDismiss = {
showOverflowMenu = false
overflowInitialMenu = AppBarMenu.Main
Expand Down
23 changes: 22 additions & 1 deletion app/src/main/kotlin/com/flxrs/dankchat/ui/main/MainAppBar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import androidx.compose.material.icons.filled.EmojiEmotions
import androidx.compose.material.icons.filled.Flag
import androidx.compose.material.icons.filled.Image
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material.icons.filled.OpenInBrowser
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.RemoveCircleOutline
Expand Down Expand Up @@ -134,6 +136,7 @@ internal val LocalInlineMenuItemRegistry = staticCompositionLocalOf<InlineMenuIt
@Composable
fun InlineOverflowMenu(
isLoggedIn: Boolean,
channelNotificationsEnabled: Boolean,
onDismiss: () -> Unit,
onAction: (ToolbarAction) -> Unit,
initialMenu: AppBarMenu = AppBarMenu.Main,
Expand Down Expand Up @@ -231,6 +234,7 @@ fun InlineOverflowMenu(

AppBarMenu.Channel -> ChannelMenuContent(
isLoggedIn = isLoggedIn,
notificationsEnabled = channelNotificationsEnabled,
onAction = onAction,
onDismiss = onDismiss,
onBack = { currentMenu = AppBarMenu.Main },
Expand Down Expand Up @@ -485,20 +489,37 @@ private fun ColumnScope.UploadMenuContent(
@Composable
private fun ColumnScope.ChannelMenuContent(
isLoggedIn: Boolean,
notificationsEnabled: Boolean,
onAction: (ToolbarAction) -> Unit,
onDismiss: () -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
InlineSubMenuHeader(title = stringResource(R.string.channel), onBack = onBack)
InlineMenuItem(
text =
stringResource(
when {
notificationsEnabled -> R.string.channel_highlight_notifications_on
else -> R.string.channel_highlight_notifications_off
},
),
icon =
when {
notificationsEnabled -> Icons.Default.Notifications
else -> Icons.Default.NotificationsOff
},
onClick = { onAction(ToolbarAction.ToggleChannelNotifications) },
modifier = modifier,
maxLines = 2,
)
InlineMenuItem(
text = stringResource(R.string.open_channel),
icon = Icons.Default.OpenInBrowser,
onClick = {
onAction(ToolbarAction.OpenChannel)
onDismiss()
},
modifier = modifier,
maxLines = 2,
)
InlineMenuItem(
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/kotlin/com/flxrs/dankchat/ui/main/MainScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ fun MainScreen(

val tabState = channelTabViewModel.uiState.collectAsStateWithLifecycle().value
val activeChannel = tabState.tabs.getOrNull(tabState.selectedIndex)?.channel
val mutedNotificationChannels by channelManagementViewModel.mutedNotificationChannels.collectAsStateWithLifecycle()
val channelNotificationsEnabled = activeChannel == null || activeChannel.lowercase() !in mutedNotificationChannels

// Same key as in ChatComposable, so this resolves the active page's instance
val activePinnedMessageViewModel =
Expand Down Expand Up @@ -673,6 +675,12 @@ fun MainScreen(
dialogViewModel.showBlockChannel()
}

ToolbarAction.ToggleChannelNotifications -> {
activeChannel?.let {
channelManagementViewModel.setChannelNotificationsEnabled(it, !channelNotificationsEnabled)
}
}

ToolbarAction.CaptureImage -> {
if (preferenceStore.hasExternalHostingAcknowledged) onCaptureImage() else dialogViewModel.setPendingUploadAction(onCaptureImage)
}
Expand Down Expand Up @@ -718,6 +726,7 @@ fun MainScreen(
totalMentionCount = tabState.tabs.sumOf { it.mentionCount } + tabState.whisperMentionCount,
hasActivePinnedMessage = activePinnedMessageState != PinnedMessageUiState.Hidden,
isPinnedMessageShown = activePinnedMessageState is PinnedMessageUiState.Expanded,
channelNotificationsEnabled = channelNotificationsEnabled,
onAction = handleToolbarAction,
onAudioOnly = { streamViewModel.toggleAudioOnly() },
onStreamClose = { streamViewModel.closeStream() },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ sealed interface ToolbarAction {

data object BlockChannel : ToolbarAction

data object ToggleChannelNotifications : ToolbarAction

data object CaptureImage : ToolbarAction

data object CaptureVideo : ToolbarAction
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ import com.flxrs.dankchat.data.repo.chat.ChatRepository
import com.flxrs.dankchat.domain.ChannelDataCoordinator
import com.flxrs.dankchat.preferences.DankChatPreferenceStore
import com.flxrs.dankchat.preferences.model.ChannelWithRename
import com.flxrs.dankchat.preferences.notifications.NotificationsSettingsDataStore
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.collections.immutable.toImmutableSet
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
Expand All @@ -33,6 +37,7 @@ class ChannelManagementViewModel(
private val chatNotificationRepository: ChatNotificationRepository,
private val ignoresRepository: IgnoresRepository,
private val channelRepository: ChannelRepository,
private val notificationsSettingsDataStore: NotificationsSettingsDataStore,
channelSelectionDataStore: ChannelSelectionDataStore,
) : ViewModel() {
val channels: StateFlow<ImmutableList<ChannelWithRename>> =
Expand All @@ -41,6 +46,11 @@ class ChannelManagementViewModel(
.map { it.toImmutableList() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())

val mutedNotificationChannels: StateFlow<ImmutableSet<UserName>> =
notificationsSettingsDataStore.settings
.map { it.mutedChannels.toImmutableSet() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentSetOf())

init {
// Restore persisted channel selection, falling back to first channel
viewModelScope.launch {
Expand Down Expand Up @@ -118,6 +128,15 @@ class ChannelManagementViewModel(
chatConnector.reconnect()
}

fun setChannelNotificationsEnabled(
channel: UserName,
enabled: Boolean,
) {
viewModelScope.launch {
notificationsSettingsDataStore.setChannelNotificationsEnabled(channel, enabled)
}
}

fun blockChannel(channel: UserName) = viewModelScope.launch {
runCatching {
if (!preferenceStore.isLoggedIn) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,13 @@ fun MainScreenDialogs(

if (dialogState.showManageChannels) {
val channels by channelManagementViewModel.channels.collectAsStateWithLifecycle()
val mutedNotificationChannels by channelManagementViewModel.mutedNotificationChannels.collectAsStateWithLifecycle()
ManageChannelsDialog(
channels = channels,
mutedNotificationChannels = mutedNotificationChannels,
onApplyChanges = channelManagementViewModel::applyChanges,
onChannelSelect = channelManagementViewModel::selectChannel,
onChannelNotificationsChange = channelManagementViewModel::setChannelNotificationsEnabled,
onDismiss = dialogViewModel::dismissManageChannels,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.OpenInNew
import androidx.compose.material.icons.filled.Notifications
import androidx.compose.material.icons.filled.NotificationsOff
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
Expand Down Expand Up @@ -74,8 +76,10 @@ import sh.calvin.reorderable.rememberReorderableLazyListState
@Composable
fun ManageChannelsDialog(
channels: List<ChannelWithRename>,
mutedNotificationChannels: Set<UserName>,
onApplyChanges: (List<ChannelWithRename>) -> Unit,
onChannelSelect: (UserName) -> Unit,
onChannelNotificationsChange: (UserName, Boolean) -> Unit,
onDismiss: () -> Unit,
) {
var channelToDelete by remember { mutableStateOf<UserName?>(null) }
Expand Down Expand Up @@ -138,6 +142,7 @@ fun ManageChannelsDialog(
ChannelItem(
channelWithRename = channelWithRename,
isEditing = editingChannel == channelWithRename.channel,
notificationsEnabled = channelWithRename.channel.lowercase() !in mutedNotificationChannels,
modifier =
Modifier.longPressDraggableHandle(
onDragStarted = { /* Optional haptic feedback here */ },
Expand All @@ -161,6 +166,9 @@ fun ManageChannelsDialog(
editingChannel = null
},
onDelete = { channelToDelete = channelWithRename.channel },
onNotificationsChange = { enabled ->
onChannelNotificationsChange(channelWithRename.channel, enabled)
},
)
if (index < localChannels.lastIndex) {
HorizontalDivider(
Expand Down Expand Up @@ -205,10 +213,12 @@ fun ManageChannelsDialog(
private fun ChannelItem(
channelWithRename: ChannelWithRename,
isEditing: Boolean,
notificationsEnabled: Boolean,
onNavigate: () -> Unit,
onEdit: () -> Unit,
onRename: (String?) -> Unit,
onDelete: () -> Unit,
onNotificationsChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
) {
Column(modifier = modifier) {
Expand Down Expand Up @@ -244,6 +254,24 @@ private fun ChannelItem(
.padding(horizontal = 8.dp),
)

IconButton(onClick = { onNotificationsChange(!notificationsEnabled) }) {
Icon(
imageVector =
when {
notificationsEnabled -> Icons.Default.Notifications
else -> Icons.Default.NotificationsOff
},
contentDescription =
stringResource(
when {
notificationsEnabled -> R.string.disable_channel_notifications
else -> R.string.enable_channel_notifications
},
channelWithRename.channel.value,
),
)
}

IconButton(onClick = onNavigate) {
Icon(
imageVector = Icons.AutoMirrored.Filled.OpenInNew,
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
<string name="channel_blocked_message">Channel blocked</string>
<string name="no_channels_added">No channels added</string>
<string name="no_channels_added_body">Add a channel to start chatting</string>
<string name="enable_channel_notifications">Enable highlight notifications for %1$s</string>
<string name="disable_channel_notifications">Disable highlight notifications for %1$s</string>
<string name="channel_highlight_notifications_on">Highlight notifications on</string>
<string name="channel_highlight_notifications_off">Highlight notifications off</string>
<string name="confirm_logout_title">Confirm logout</string>
<string name="confirm_logout_message">Are you sure you want to logout?</string>
<string name="confirm_logout_question">Log out?</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.flxrs.dankchat.preferences.notifications

import com.flxrs.dankchat.data.toUserName
import org.junit.jupiter.api.Test
import kotlin.test.assertFalse
import kotlin.test.assertTrue

internal class NotificationsSettingsTest {
@Test
fun `channel notifications can be disabled and enabled`() {
val disabled = NotificationsSettings().withChannelNotificationsEnabled("forsen".toUserName(), enabled = false)

assertFalse(disabled.areChannelNotificationsEnabled("forsen".toUserName()))
assertTrue(disabled.areChannelNotificationsEnabled("iore".toUserName()))

val enabled = disabled.withChannelNotificationsEnabled("forsen".toUserName(), enabled = true)
assertTrue(enabled.areChannelNotificationsEnabled("forsen".toUserName()))
}
}