diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ConversationCollections.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ConversationCollections.kt index ba920472e..fd105bc07 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ConversationCollections.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ConversationCollections.kt @@ -12,6 +12,8 @@ data class ConversationCollections( val directChats: ConversationCollection, val hidden: ConversationCollection ) { + val conversations: List get() = favorites.conversations + channels.conversations + groupChats.conversations + directChats.conversations + hidden.conversations + fun filtered(query: String): ConversationCollections { return ConversationCollections( channels = channels.filter { it.filterPredicate(query) }, diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt index cc7db7fd7..56a3101be 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationList.kt @@ -25,12 +25,15 @@ import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material.icons.filled.Groups2 import androidx.compose.material.icons.filled.NotificationsActive import androidx.compose.material.icons.filled.NotificationsOff +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -79,6 +82,7 @@ internal fun ConversationList( onNavigateToConversation: (conversationId: Long) -> Unit, onToggleMarkAsFavourite: (conversationId: Long, favorite: Boolean) -> Unit, onToggleHidden: (conversationId: Long, hidden: Boolean) -> Unit, + onToggleMuted: (conversationId: Long, muted: Boolean) -> Unit, onRequestCreatePersonalConversation: () -> Unit, onRequestAddChannel: () -> Unit, trailingContent: LazyListScope.() -> Unit @@ -98,7 +102,8 @@ internal fun ConversationList( conversations = collection, onNavigateToConversation = onNavigateToConversation, onToggleMarkAsFavourite = onToggleMarkAsFavourite, - onToggleHidden = onToggleHidden + onToggleHidden = onToggleHidden, + onToggleMuted = onToggleMuted ) } @@ -220,6 +225,7 @@ private fun LazyListScope.conversationList( onNavigateToConversation: (conversationId: Long) -> Unit, onToggleMarkAsFavourite: (conversationId: Long, favorite: Boolean) -> Unit, onToggleHidden: (conversationId: Long, hidden: Boolean) -> Unit, + onToggleMuted: (conversationId: Long, muted: Boolean) -> Unit, ) { if (!conversations.isExpanded) return items( @@ -238,9 +244,13 @@ private fun LazyListScope.conversationList( ) }, onToggleHidden = { onToggleHidden(conversation.id, !conversation.isHidden) }, + onToggleMuted = { onToggleMuted(conversation.id, !conversation.isMuted) }, content = { contentModifier -> val unreadMessagesCount = conversation.unreadMessagesCount ?: 0 + val headlineColor = + LocalContentColor.current.copy(alpha = if (conversation.isMuted) 0.6f else 1f) + when (conversation) { is ChannelChat -> { val channelName = if (conversation.isArchived) { @@ -257,7 +267,7 @@ private fun LazyListScope.conversationList( }, headlineContent = { Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - Text(text = channelName, maxLines = 1) + Text(text = channelName, maxLines = 1, color = headlineColor) ExtraChannelIcons(channelChat = conversation) } @@ -271,7 +281,12 @@ private fun LazyListScope.conversationList( is GroupChat -> { ListItem( modifier = contentModifier, - headlineContent = { Text(conversation.humanReadableTitle) }, + headlineContent = { + Text( + conversation.humanReadableTitle, + color = headlineColor + ) + }, leadingContent = { Icon(imageVector = Icons.Default.Groups2, contentDescription = null) }, @@ -284,7 +299,12 @@ private fun LazyListScope.conversationList( is OneToOneChat -> { ListItem( modifier = contentModifier, - headlineContent = { Text(conversation.humanReadableTitle) }, + headlineContent = { + Text( + conversation.humanReadableTitle, + color = headlineColor + ) + }, trailingContent = { UnreadMessages(unreadMessagesCount = unreadMessagesCount) } @@ -324,6 +344,7 @@ private fun ConversationListItem( onNavigateToConversation: () -> Unit, onToggleMarkAsFavourite: () -> Unit, onToggleHidden: () -> Unit, + onToggleMuted: () -> Unit, content: @Composable (Modifier) -> Unit ) { var isContextDialogShown by remember { mutableStateOf(false) } @@ -365,7 +386,7 @@ private fun ConversationListItem( DropdownMenuItem( leadingIcon = { Icon( - imageVector = if (conversation.isHidden) Icons.Default.NotificationsActive else Icons.Default.NotificationsOff, + imageVector = if (conversation.isHidden) Icons.Default.Visibility else Icons.Default.VisibilityOff, contentDescription = null ) }, @@ -382,6 +403,27 @@ private fun ConversationListItem( onDismissRequest() } ) + + DropdownMenuItem( + leadingIcon = { + Icon( + imageVector = if (conversation.isMuted) Icons.Default.NotificationsActive else Icons.Default.NotificationsOff, + contentDescription = null + ) + }, + text = { + Text( + text = stringResource( + id = if (conversation.isMuted) R.string.conversation_overview_conversation_item_unmark_as_muted + else R.string.conversation_overview_conversation_item_mark_as_muted + ) + ) + }, + onClick = { + onToggleMuted() + onDismissRequest() + } + ) } } } diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt index fc8c43f5f..f6399278c 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewBody.kt @@ -127,6 +127,7 @@ fun ConversationOverviewBody( }, onToggleMarkAsFavourite = viewModel::markConversationAsFavorite, onToggleHidden = viewModel::markConversationAsHidden, + onToggleMuted = viewModel::markConversationAsMuted, onRequestCreatePersonalConversation = onRequestCreatePersonalConversation, onRequestAddChannel = onRequestAddChannel, trailingContent = { diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewViewModel.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewViewModel.kt index 800df2363..035155270 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewViewModel.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/overview/ConversationOverviewViewModel.kt @@ -313,6 +313,24 @@ class ConversationOverviewViewModel( } } + fun markConversationAsMuted(conversationId: Long, muted: Boolean): Deferred { + return viewModelScope.async(coroutineContext) { + conversationService.markConversationMuted( + courseId, + conversationId, + muted, + accountService.authToken.first(), + serverConfigurationService.serverUrl.first() + ) + .onSuccess { isSuccessful -> + if (isSuccessful) { + onRequestReload.tryEmit(Unit) + } + } + .or(false) + } + } + fun markConversationAsFavorite(conversationId: Long, favorite: Boolean): Deferred { return viewModelScope.async(coroutineContext) { conversationService.markConversationAsFavorite( diff --git a/feature/metis/manage-conversations/src/main/res/values/conversation_overview_strings.xml b/feature/metis/manage-conversations/src/main/res/values/conversation_overview_strings.xml index f625beac5..c6b57c801 100644 --- a/feature/metis/manage-conversations/src/main/res/values/conversation_overview_strings.xml +++ b/feature/metis/manage-conversations/src/main/res/values/conversation_overview_strings.xml @@ -8,6 +8,9 @@ Hide Unhide + Mute + Unmute + Favorites Channels Group Chats diff --git a/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/overview/ConversationOverviewE2eTest.kt b/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/overview/ConversationOverviewE2eTest.kt index 51559f7ae..2d60a2764 100644 --- a/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/overview/ConversationOverviewE2eTest.kt +++ b/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/overview/ConversationOverviewE2eTest.kt @@ -188,6 +188,42 @@ class ConversationOverviewE2eTest : ConversationBaseTest() { ) } + @Test(timeout = DefaultTestTimeoutMillis) + fun `can mark conversation as muted`() { + val chat = runBlocking { createPersonalConversation() } + + markConversationImpl( + originalTag = getTagForConversation(chat), + newTag = tagForConversation(chat.id, KEY_SUFFIX_PERSONAL), + textToClick = context.getString(R.string.conversation_overview_conversation_item_mark_as_muted), + checkExists = { conversations.any { conv -> conv.id == chat.id && conv.isMuted } } + ) + } + + @Test(timeout = DefaultTestTimeoutMillis) + fun `can mark hidden conversation as not muted`() { + val chat = runBlocking { + val chat = createPersonalConversation() + + conversationService.markConversationMuted( + courseId = course.id!!, + conversationId = chat.id, + muted = true, + authToken = accessToken, + serverUrl = testServerUrl + ).orThrow("Could not mark conversation as hidden") + + chat + } + + markConversationImpl( + originalTag = tagForConversation(chat.id, KEY_SUFFIX_PERSONAL), + newTag = getTagForConversation(chat), + textToClick = context.getString(R.string.conversation_overview_conversation_item_unmark_as_muted), + checkExists = { conversations.none { conv -> conv.id == chat.id && conv.isMuted } } + ) + } + /** * Checks that updates to conversations are automatically received over the websocket connection. */ diff --git a/feature/metis/shared/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ConversationServiceStub.kt b/feature/metis/shared/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ConversationServiceStub.kt index b62d96d15..b4778efae 100644 --- a/feature/metis/shared/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ConversationServiceStub.kt +++ b/feature/metis/shared/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/ConversationServiceStub.kt @@ -140,4 +140,12 @@ open class ConversationServiceStub( authToken: String, serverUrl: String ): NetworkResponse = NetworkResponse.Failure(StubException) + + override suspend fun markConversationMuted( + courseId: Long, + conversationId: Long, + muted: Boolean, + authToken: String, + serverUrl: String + ): NetworkResponse = NetworkResponse.Failure(StubException) } diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/ChannelChat.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/ChannelChat.kt index a7378ff62..da9397a0e 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/ChannelChat.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/ChannelChat.kt @@ -15,6 +15,7 @@ data class ChannelChat( override val unreadMessagesCount: Long? = 0L, override val isFavorite: Boolean = false, override val isHidden: Boolean = false, + override val isMuted: Boolean = false, override val isCreator: Boolean = false, override val isMember: Boolean = false, override val numberOfMembers: Int = 0, diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/Conversation.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/Conversation.kt index 93a8aac66..b9d9596af 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/Conversation.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/Conversation.kt @@ -13,6 +13,7 @@ sealed class Conversation { abstract val unreadMessagesCount: Long? abstract val isFavorite: Boolean abstract val isHidden: Boolean + abstract val isMuted: Boolean abstract val isCreator: Boolean abstract val isMember: Boolean abstract val numberOfMembers: Int diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/GroupChat.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/GroupChat.kt index 478da6d83..454b128fd 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/GroupChat.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/GroupChat.kt @@ -16,6 +16,7 @@ data class GroupChat( override val unreadMessagesCount: Long? = 0L, override val isFavorite: Boolean = false, override val isHidden: Boolean = false, + override val isMuted: Boolean = false, override val isCreator: Boolean = false, override val isMember: Boolean = false, override val numberOfMembers: Int = 0, diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/OneToOneChat.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/OneToOneChat.kt index 7684efdcb..12a05c1b8 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/OneToOneChat.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/content/dto/conversation/OneToOneChat.kt @@ -16,6 +16,7 @@ data class OneToOneChat( override val unreadMessagesCount: Long? = 0L, override val isFavorite: Boolean = false, override val isHidden: Boolean = false, + override val isMuted: Boolean = false, override val isCreator: Boolean = false, override val isMember: Boolean = false, override val numberOfMembers: Int = 0, diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/ConversationService.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/ConversationService.kt index c60b1967a..f905a97ab 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/ConversationService.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/ConversationService.kt @@ -132,6 +132,14 @@ interface ConversationService { authToken: String, serverUrl: String ): NetworkResponse + + suspend fun markConversationMuted( + courseId: Long, + conversationId: Long, + muted: Boolean, + authToken: String, + serverUrl: String + ): NetworkResponse } suspend fun ConversationService.getConversation( diff --git a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/impl/ConversationServiceImpl.kt b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/impl/ConversationServiceImpl.kt index e21a17256..3cec1c73b 100644 --- a/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/impl/ConversationServiceImpl.kt +++ b/feature/metis/shared/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/shared/service/network/impl/ConversationServiceImpl.kt @@ -372,6 +372,24 @@ class ConversationServiceImpl(private val ktorProvider: KtorProvider) : Conversa appendPathSegments("favorite") } + override suspend fun markConversationMuted( + courseId: Long, + conversationId: Long, + muted: Boolean, + authToken: String, + serverUrl: String + ) = performActionOnConversation( + courseId, + conversationId, + authToken = authToken, + serverUrl = serverUrl, + httpRequestBlock = { + parameter("isMuted", muted) + } + ) { + appendPathSegments("muted") + } + private suspend fun performActionOnUser( courseId: Long, conversation: Conversation,