Skip to content
Merged
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 @@ -143,6 +143,7 @@ fun FloatingToolbar(
totalMentionCount: Int,
hasActivePinnedMessage: Boolean,
isPinnedMessageShown: Boolean,
channelNotificationsEnabled: Boolean,
onAction: (ToolbarAction) -> Unit,
onAudioOnly: () -> Unit,
onStreamClose: () -> Unit,
Expand Down Expand Up @@ -879,6 +880,7 @@ fun FloatingToolbar(
) {
InlineOverflowMenu(
isLoggedIn = isLoggedIn,
channelNotificationsEnabled = channelNotificationsEnabled,
onDismiss = {
showOverflowMenu = false
overflowInitialMenu = AppBarMenu.Main
Expand Down
21 changes: 20 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 @@ -40,11 +40,13 @@ 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.OpenInBrowser
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.RemoveCircleOutline
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.Checkbox
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
Expand Down Expand Up @@ -145,6 +147,7 @@ internal data class CollapsedToolbarAction(
@Composable
internal fun InlineOverflowMenu(
isLoggedIn: Boolean,
channelNotificationsEnabled: Boolean,
onDismiss: () -> Unit,
onAction: (ToolbarAction) -> Unit,
initialMenu: AppBarMenu = AppBarMenu.Main,
Expand Down Expand Up @@ -244,6 +247,7 @@ internal fun InlineOverflowMenu(

AppBarMenu.Channel -> ChannelMenuContent(
isLoggedIn = isLoggedIn,
notificationsEnabled = channelNotificationsEnabled,
onAction = onAction,
onDismiss = onDismiss,
onBack = { currentMenu = AppBarMenu.Main },
Expand Down Expand Up @@ -283,6 +287,7 @@ private fun InlineMenuItem(
modifier: Modifier = Modifier,
maxLines: Int = 1,
hasSubMenu: Boolean = false,
checked: Boolean? = null,
) {
val registry = LocalInlineMenuItemRegistry.current
val isPressed = registry?.pressedKey == text
Expand Down Expand Up @@ -326,6 +331,12 @@ private fun InlineMenuItem(
modifier = Modifier.size(20.dp),
)
}
if (checked != null) {
Checkbox(
checked = checked,
onCheckedChange = null,
)
}
}
}

Expand Down Expand Up @@ -513,20 +524,28 @@ 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(R.string.channel_background_notifications),
icon = Icons.Default.Notifications,
onClick = { onAction(ToolbarAction.ToggleChannelNotifications) },
modifier = modifier,
maxLines = 2,
checked = notificationsEnabled,
)
InlineMenuItem(
text = stringResource(R.string.open_channel),
icon = Icons.Default.OpenInBrowser,
onClick = {
onAction(ToolbarAction.OpenChannel)
onDismiss()
},
modifier = modifier,
maxLines = 2,
)
InlineMenuItem(
Expand Down
6 changes: 6 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 @@ -293,6 +293,7 @@ fun MainScreen(

val tabState = channelTabViewModel.uiState.collectAsStateWithLifecycle().value
val activeChannel = tabState.tabs.getOrNull(tabState.selectedIndex)?.channel
val channelNotificationsEnabled by channelManagementViewModel.activeChannelNotificationsEnabled.collectAsStateWithLifecycle()

// The theater chat shows the streamed channel, so the input has to target it as well
SideEffect(theaterStream, activeChannel) {
Expand Down Expand Up @@ -688,6 +689,10 @@ fun MainScreen(
dialogViewModel.showBlockChannel()
}

ToolbarAction.ToggleChannelNotifications -> {
activeChannel?.let { channelManagementViewModel.toggleChannelNotifications(it) }
}

ToolbarAction.CaptureImage -> {
if (preferenceStore.hasExternalHostingAcknowledged) onCaptureImage() else dialogViewModel.setPendingUploadAction(onCaptureImage)
}
Expand Down Expand Up @@ -733,6 +738,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,11 +13,16 @@ 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.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
Expand All @@ -33,6 +38,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 +47,19 @@ class ChannelManagementViewModel(
.map { it.toImmutableList() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentListOf())

val mutedNotificationChannels: StateFlow<ImmutableSet<UserName>> =
combine(channels, notificationsSettingsDataStore.settings) { channels, settings ->
channels
.map { it.channel }
.filterNot { settings.areChannelNotificationsEnabled(it) }
.toImmutableSet()
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), persistentSetOf())

val activeChannelNotificationsEnabled: StateFlow<Boolean> =
combine(chatChannelProvider.activeChannel, notificationsSettingsDataStore.settings) { channel, settings ->
channel == null || settings.areChannelNotificationsEnabled(channel)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), true)

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

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

fun toggleChannelNotifications(channel: UserName) {
viewModelScope.launch {
notificationsSettingsDataStore.update { it.withChannelNotificationsEnabled(channel, !it.areChannelNotificationsEnabled(channel)) }
}
}

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 @@ -29,6 +29,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 @@ -77,8 +79,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 @@ -149,6 +153,7 @@ fun ManageChannelsDialog(
ChannelItem(
channelWithRename = channelWithRename,
isEditing = editingChannel == channelWithRename.channel,
notificationsEnabled = channelWithRename.channel !in mutedNotificationChannels,
modifier =
Modifier.longPressDraggableHandle(
onDragStarted = { /* Optional haptic feedback here */ },
Expand All @@ -172,6 +177,9 @@ fun ManageChannelsDialog(
editingChannel = null
},
onDelete = { channelToDelete = channelWithRename.channel },
onNotificationsChange = { enabled ->
onChannelNotificationsChange(channelWithRename.channel, enabled)
},
)
if (index < localChannels.lastIndex) {
HorizontalDivider(
Expand Down Expand Up @@ -216,10 +224,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 @@ -255,6 +265,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
3 changes: 3 additions & 0 deletions app/src/main/res/values-b+zh+Hant+TW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
<string name="channel_blocked_message">已封鎖此頻道</string>
<string name="no_channels_added">尚未新增頻道</string>
<string name="no_channels_added_body">新增頻道以開始聊天</string>
<string name="enable_channel_notifications">為 %1$s 啟用背景通知</string>
<string name="disable_channel_notifications">為 %1$s 停用背景通知</string>
<string name="channel_background_notifications">背景通知</string>
<string name="confirm_logout_title">確認登出</string>
<string name="confirm_logout_message">您確定要登出嗎?</string>
<string name="confirm_logout_question">登出?</string>
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-be-rBY/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
<string name="channel_blocked_message">Канал заблакіраваны</string>
<string name="no_channels_added">Няма дададзеных каналаў</string>
<string name="no_channels_added_body">Дадайце канал, каб пачаць размову</string>
<string name="enable_channel_notifications">Уключыць фонавыя апавяшчэнні для %1$s</string>
<string name="disable_channel_notifications">Адключыць фонавыя апавяшчэнні для %1$s</string>
<string name="channel_background_notifications">Фонавыя апавяшчэнні</string>
<string name="confirm_logout_title">Пацверджанне выхаду</string>
<string name="confirm_logout_message">Вы ўпэўнены, што хочаце выйсці?</string>
<string name="confirm_logout_question">Выйсці?</string>
Expand Down
Loading
Loading