From a18442e951acbc344d97d3e7f2152d9f8c57e8a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 25 Jan 2024 11:58:20 +0000 Subject: [PATCH 1/9] Bump kotlin from 1.9.20 to 1.9.22 Bumps `kotlin` from 1.9.20 to 1.9.22. Updates `org.jetbrains.kotlin:kotlin-stdlib-jdk8` from 1.9.20 to 1.9.22 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.20...v1.9.22) Updates `org.jetbrains.kotlin:kotlin-gradle-plugin` from 1.9.20 to 1.9.22 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.20...v1.9.22) Updates `org.jetbrains.kotlin.jvm` from 1.9.20 to 1.9.22 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.20...v1.9.22) Updates `org.jetbrains.kotlin.plugin.serialization` from 1.9.20 to 1.9.22 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/master/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.9.20...v1.9.22) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-stdlib-jdk8 dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin.jvm dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin.plugin.serialization dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ce76f5f7c..bad77c674 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ androidxPaging = "3.2.1" androidxPagingCompose = "3.2.1" coil = "2.4.0" emoji2 = "1.4.0" -kotlin = "1.9.20" +kotlin = "1.9.22" kotlinxCoroutines = "1.7.3" kotlinxDatetime = "0.4.0" kotlinxSerializationJson = "1.5.1" From da268ee16c70aa64f8d95e975692d02a66a2ee9a Mon Sep 17 00:00:00 2001 From: Tim Ortel <100865202+TimOrtel@users.noreply.github.com> Date: Wed, 21 Feb 2024 18:14:25 +0100 Subject: [PATCH 2/9] Support conversation muting. (#38) --- .../ConversationCollections.kt | 2 + .../conversation/overview/ConversationList.kt | 52 +++++++++++++++++-- .../overview/ConversationOverviewBody.kt | 1 + .../overview/ConversationOverviewViewModel.kt | 18 +++++++ .../values/conversation_overview_strings.xml | 3 ++ .../overview/ConversationOverviewE2eTest.kt | 36 +++++++++++++ .../metis/shared/ConversationServiceStub.kt | 8 +++ .../content/dto/conversation/ChannelChat.kt | 1 + .../content/dto/conversation/Conversation.kt | 1 + .../content/dto/conversation/GroupChat.kt | 1 + .../content/dto/conversation/OneToOneChat.kt | 1 + .../service/network/ConversationService.kt | 8 +++ .../network/impl/ConversationServiceImpl.kt | 18 +++++++ 13 files changed, 145 insertions(+), 5 deletions(-) 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, From 39467d24a29bcc8dd4c420da562e58df29458585 Mon Sep 17 00:00:00 2001 From: Tim Ortel <100865202+TimOrtel@users.noreply.github.com> Date: Mon, 4 Mar 2024 20:28:25 +0100 Subject: [PATCH 3/9] Bump to version 0.9.0 --- app/build.gradle.kts | 2 +- .../www1/artemis/native_app/android/db/AppDatabase.kt | 2 +- .../informatics/www1/artemis/native_app/core/model/Dashboard.kt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6bd65814c..908d806d9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,7 @@ plugins { android { namespace = "de.tum.informatics.www1.artemis.native_app.android" - val versionName = "0.8.0" + val versionName = "0.9.0" val versionCode = if (!System.getenv("bamboo_buildNumber") .isNullOrEmpty() diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt index 0115609dd..a22343443 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/db/AppDatabase.kt @@ -29,7 +29,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.push.communication_not CommunicationMessageEntity::class ], exportSchema = true, - version = 8 + version = 9 ) @TypeConverters(RoomTypeConverters::class) abstract class AppDatabase : RoomDatabase() { diff --git a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/Dashboard.kt b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/Dashboard.kt index 59fb65b6d..b3cfa3360 100644 --- a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/Dashboard.kt +++ b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/Dashboard.kt @@ -6,4 +6,4 @@ import kotlinx.serialization.Serializable * A dashboard is a collection of courses. */ @Serializable -data class Dashboard(val courses: List) +data class Dashboard(val courses: List = emptyList()) From 2e7c242dc24c2ad6d8ce035d975dacd53d517551 Mon Sep 17 00:00:00 2001 From: "mend-bolt-for-github[bot]" <42819689+mend-bolt-for-github[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 21:33:57 +0000 Subject: [PATCH 4/9] Add .whitesource configuration file --- .whitesource | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .whitesource diff --git a/.whitesource b/.whitesource new file mode 100644 index 000000000..9c7ae90b4 --- /dev/null +++ b/.whitesource @@ -0,0 +1,14 @@ +{ + "scanSettings": { + "baseBranches": [] + }, + "checkRunSettings": { + "vulnerableCheckRunConclusionLevel": "failure", + "displayMode": "diff", + "useMendCheckNames": true + }, + "issueSettings": { + "minSeverityLevel": "LOW", + "issueType": "DEPENDENCY" + } +} \ No newline at end of file From 600af6b572ee80b8976a6d2b61466100e6f0a2ec Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Fri, 20 Sep 2024 17:57:52 +0300 Subject: [PATCH 5/9] added new features & enhancements --- .../network/impl/ExerciseServiceImpl.kt | 24 +- .../core/model/exercise/Exercise.kt | 4 +- .../core/model/exercise/FileUploadExercise.kt | 4 +- .../core/model/exercise/ModelingExercise.kt | 4 +- .../model/exercise/ProgrammingExercise.kt | 4 +- .../core/model/exercise/QuizExercise.kt | 4 +- .../core/model/exercise/TextExercise.kt | 4 +- .../core/model/exercise/UnknownExercise.kt | 4 +- .../core/ui/exercise/ExerciseActionButtons.kt | 45 +- .../src/main/res/values/exercise_strings.xml | 2 + .../courseview/MessagingScreenshots.kt | 14 +- .../feature/exerciseview/ArtemisWebView.kt | 3 +- .../feature/exerciseview/ExerciseViewUi.kt | 1 + .../home/ExerciseScreenTopAppBar.kt | 88 ++-- .../home/overview/ExerciseOverviewTab.kt | 30 +- .../main/res/values/exercise_view_strings.xml | 1 + .../conversation/ui/chatlist/MetisChatList.kt | 23 +- .../ui/post/PostContextBottomSheet.kt | 4 +- .../conversation/ui/reply/ReplyTextField.kt | 393 +++++++++++++----- .../conversation/ui/thread/MetisThreadUi.kt | 12 +- .../ConversationCollections.kt | 23 +- .../storage/ConversationPreferenceService.kt | 5 +- ...onversationPreferenceStorageServiceImpl.kt | 15 +- .../browse_channels/BrowseChannelsScreen.kt | 85 ++-- .../BrowseChannelsViewModel.kt | 42 +- .../create_channel/CreateChannelScreen.kt | 133 +++--- .../create_channel/CreateChannelViewModel.kt | 12 +- .../conversation/overview/ConversationList.kt | 381 +++++++++-------- .../overview/ConversationOverviewBody.kt | 258 ++++++++---- .../overview/ConversationOverviewViewModel.kt | 59 ++- .../res/values/browse_channels_strings.xml | 2 + .../values/conversation_overview_strings.xml | 5 +- .../res/values/create_channel_strings.xml | 5 +- .../feature/metis/ui/ConversationFacadeUi.kt | 8 +- .../metis/ui/SinglePageConversationBody.kt | 94 ++++- 35 files changed, 1199 insertions(+), 596 deletions(-) diff --git a/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/network/impl/ExerciseServiceImpl.kt b/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/network/impl/ExerciseServiceImpl.kt index b87ddd6a6..e671f2fd3 100644 --- a/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/network/impl/ExerciseServiceImpl.kt +++ b/core/data/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/data/service/network/impl/ExerciseServiceImpl.kt @@ -7,11 +7,13 @@ import de.tum.informatics.www1.artemis.native_app.core.data.service.network.Exer import de.tum.informatics.www1.artemis.native_app.core.data.service.KtorProvider import de.tum.informatics.www1.artemis.native_app.core.data.service.impl.JsonProvider import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise -import io.ktor.client.call.body import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.appendPathSegments import io.ktor.http.contentType +import kotlinx.serialization.json.decodeFromJsonElement +import kotlinx.serialization.json.jsonObject internal class ExerciseServiceImpl( private val ktorProvider: KtorProvider, @@ -23,14 +25,20 @@ internal class ExerciseServiceImpl( authToken: String ): NetworkResponse { return performNetworkCall { - ktorProvider.ktorClient.get(serverUrl) { - url { - appendPathSegments("api", "exercises", exerciseId.toString(), "details") + val response = ktorProvider.ktorClient.get(serverUrl) { + url { + appendPathSegments("api", "exercises", exerciseId.toString(), "details") + } + + contentType(ContentType.Application.Json) + cookieAuth(authToken) } - contentType(ContentType.Application.Json) - cookieAuth(authToken) - }.body() + val jsonElement = jsonProvider.applicationJsonConfiguration.parseToJsonElement(response.bodyAsText()) + val exercise = jsonProvider.applicationJsonConfiguration + .decodeFromJsonElement(jsonElement.jsonObject["exercise"]!!) + + exercise } } -} \ No newline at end of file +} diff --git a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/Exercise.kt b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/Exercise.kt index 9e665cc41..f1cad1241 100644 --- a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/Exercise.kt +++ b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/Exercise.kt @@ -39,13 +39,15 @@ sealed class Exercise { abstract val mode: Mode abstract val categories: List abstract val visibleToStudents: Boolean? + abstract val secondCorrectionEnabled: Boolean? + abstract val presentationScoreEnabled: Boolean? abstract val teamMode: Boolean abstract val studentAssignedTeamId: Long? abstract val studentAssignedTeamIdComputed: Boolean abstract val problemStatement: String? abstract val assessmentType: AssessmentType? abstract val allowComplaintsForAutomaticAssessments: Boolean? - abstract val allowManualFeedbackRequests: Boolean? + abstract val allowFeedbackRequests: Boolean? abstract val includedInOverallScore: IncludedInOverallScore abstract val exampleSolutionPublicationDate: Instant? abstract val studentParticipations: List? diff --git a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/FileUploadExercise.kt b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/FileUploadExercise.kt index 8f728d935..a3f0c6220 100644 --- a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/FileUploadExercise.kt +++ b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/FileUploadExercise.kt @@ -20,6 +20,8 @@ data class FileUploadExercise( override val assessmentDueDate: Instant? = null, override val difficulty: Difficulty? = null, override val mode: Mode = Mode.INDIVIDUAL, + override val secondCorrectionEnabled: Boolean = false, + override val presentationScoreEnabled: Boolean = false, override val categories: List = emptyList(), override val visibleToStudents: Boolean? = null, override val teamMode: Boolean = false, @@ -28,7 +30,7 @@ data class FileUploadExercise( override val problemStatement: String? = null, override val assessmentType: AssessmentType? = null, override val allowComplaintsForAutomaticAssessments: Boolean? = null, - override val allowManualFeedbackRequests: Boolean? = null, + override val allowFeedbackRequests: Boolean? = null, override val includedInOverallScore: IncludedInOverallScore = IncludedInOverallScore.INCLUDED_COMPLETELY, override val exampleSolutionPublicationDate: Instant? = null, override val attachments: List = emptyList(), diff --git a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/ModelingExercise.kt b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/ModelingExercise.kt index de29e6863..1f5befe08 100644 --- a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/ModelingExercise.kt +++ b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/ModelingExercise.kt @@ -25,10 +25,12 @@ data class ModelingExercise( override val teamMode: Boolean = false, override val studentAssignedTeamId: Long? = null, override val studentAssignedTeamIdComputed: Boolean = false, + override val secondCorrectionEnabled: Boolean = false, + override val presentationScoreEnabled: Boolean = false, override val problemStatement: String? = null, override val assessmentType: AssessmentType? = null, override val allowComplaintsForAutomaticAssessments: Boolean? = null, - override val allowManualFeedbackRequests: Boolean? = null, + override val allowFeedbackRequests: Boolean? = null, override val includedInOverallScore: IncludedInOverallScore = IncludedInOverallScore.INCLUDED_COMPLETELY, override val exampleSolutionPublicationDate: Instant? = null, override val attachments: List = emptyList(), diff --git a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/ProgrammingExercise.kt b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/ProgrammingExercise.kt index abd6923cc..87eb872f6 100644 --- a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/ProgrammingExercise.kt +++ b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/ProgrammingExercise.kt @@ -29,11 +29,13 @@ data class ProgrammingExercise( override val visibleToStudents: Boolean? = null, override val teamMode: Boolean = false, override val studentAssignedTeamId: Long? = null, + override val secondCorrectionEnabled: Boolean = false, + override val presentationScoreEnabled: Boolean = false, override val studentAssignedTeamIdComputed: Boolean = false, override val problemStatement: String? = null, override val assessmentType: AssessmentType? = null, override val allowComplaintsForAutomaticAssessments: Boolean? = null, - override val allowManualFeedbackRequests: Boolean? = null, + override val allowFeedbackRequests: Boolean? = null, override val includedInOverallScore: IncludedInOverallScore = IncludedInOverallScore.INCLUDED_COMPLETELY, override val exampleSolutionPublicationDate: Instant? = null, override val attachments: List = emptyList(), diff --git a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/QuizExercise.kt b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/QuizExercise.kt index c7bdbaec2..b7875d1d2 100644 --- a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/QuizExercise.kt +++ b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/QuizExercise.kt @@ -30,11 +30,13 @@ data class QuizExercise( override val visibleToStudents: Boolean? = null, override val teamMode: Boolean = false, override val studentAssignedTeamId: Long? = null, + override val secondCorrectionEnabled: Boolean = false, + override val presentationScoreEnabled: Boolean = false, override val studentAssignedTeamIdComputed: Boolean = false, override val problemStatement: String? = null, override val assessmentType: AssessmentType? = null, override val allowComplaintsForAutomaticAssessments: Boolean? = null, - override val allowManualFeedbackRequests: Boolean? = null, + override val allowFeedbackRequests: Boolean? = null, override val includedInOverallScore: IncludedInOverallScore = IncludedInOverallScore.INCLUDED_COMPLETELY, override val exampleSolutionPublicationDate: Instant? = null, override val attachments: List = emptyList(), diff --git a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/TextExercise.kt b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/TextExercise.kt index f7c3a799b..7a80f7a85 100644 --- a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/TextExercise.kt +++ b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/TextExercise.kt @@ -23,12 +23,14 @@ data class TextExercise( override val categories: List = emptyList(), override val visibleToStudents: Boolean? = null, override val teamMode: Boolean = false, + override val secondCorrectionEnabled: Boolean = false, + override val presentationScoreEnabled: Boolean = false, override val studentAssignedTeamId: Long? = null, override val studentAssignedTeamIdComputed: Boolean = false, override val problemStatement: String? = null, override val assessmentType: AssessmentType? = null, override val allowComplaintsForAutomaticAssessments: Boolean? = null, - override val allowManualFeedbackRequests: Boolean? = null, + override val allowFeedbackRequests: Boolean? = null, override val includedInOverallScore: IncludedInOverallScore = IncludedInOverallScore.INCLUDED_COMPLETELY, override val exampleSolutionPublicationDate: Instant? = null, override val attachments: List = emptyList(), diff --git a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/UnknownExercise.kt b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/UnknownExercise.kt index 0ab6e8075..c0c2eb3f9 100644 --- a/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/UnknownExercise.kt +++ b/core/model/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/model/exercise/UnknownExercise.kt @@ -29,7 +29,9 @@ data class UnknownExercise( override val problemStatement: String? = null, override val assessmentType: AssessmentType? = null, override val allowComplaintsForAutomaticAssessments: Boolean? = null, - override val allowManualFeedbackRequests: Boolean? = null, + override val allowFeedbackRequests: Boolean? = null, + override val secondCorrectionEnabled: Boolean = false, + override val presentationScoreEnabled: Boolean = false, override val includedInOverallScore: IncludedInOverallScore = IncludedInOverallScore.INCLUDED_COMPLETELY, override val exampleSolutionPublicationDate: Instant? = null, override val attachments: List = emptyList(), diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseActionButtons.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseActionButtons.kt index 1a1f41c5b..286ac4a67 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseActionButtons.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/exercise/ExerciseActionButtons.kt @@ -1,11 +1,24 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.exercise +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Button +import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.ProgrammingExercise import de.tum.informatics.www1.artemis.native_app.core.model.exercise.QuizExercise @@ -116,6 +129,10 @@ fun ExerciseActionButtons( ) } } + } else { + Row(modifier=Modifier.padding(top=2.dp, bottom = 2.dp)) { + InfoMessageCard() + } } if (templateStatus is ResultTemplateStatus.WithResult) { @@ -187,4 +204,30 @@ class BoundExerciseActions( onClickViewResult = { onClickViewResult(exerciseId) }, onClickViewQuizResults = { onClickViewQuizResults(exerciseId) } ) -} \ No newline at end of file +} + + +@Composable +fun InfoMessageCard() { + Box( + modifier = Modifier + .border(width = 2.dp, color = Color.LightGray) + .background(Color(0xFFB3E5FC)) // Light sky blue background + .padding(10.dp) + .fillMaxWidth() + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = "Information", + modifier = Modifier.padding(end = 8.dp), + tint = Color(0xFF0288D1) + ) + Text( + text = stringResource(id = R.string.exercise_participation_not_possible), + fontSize = 16.sp, + color = Color.Black + ) + } + } +} diff --git a/core/ui/src/main/res/values/exercise_strings.xml b/core/ui/src/main/res/values/exercise_strings.xml index 5b518152b..1929bc041 100644 --- a/core/ui/src/main/res/values/exercise_strings.xml +++ b/core/ui/src/main/res/values/exercise_strings.xml @@ -39,6 +39,8 @@ View result + Participating this exercise is currently + not possible in the mobile app. Start exercise Open exercise View submission diff --git a/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/MessagingScreenshots.kt b/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/MessagingScreenshots.kt index fe970f72c..7852856a9 100644 --- a/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/MessagingScreenshots.kt +++ b/feature/course-view/src/debug/kotlin/de/tum/informatics/www1/artemis/native_app/feature/courseview/MessagingScreenshots.kt @@ -106,10 +106,13 @@ fun `Metis - Conversation Overview`() { ): Flow = flowOf( ConversationPreferenceService.Preferences( favouritesExpanded = true, - channelsExpanded = true, + generalsExpanded = true, groupChatsExpanded = true, personalConversationsExpanded = true, - hiddenExpanded = false + hiddenExpanded = false, + examsExpanded = true, + exercisesExpanded = true, + lecturesExpanded = true, ) ) @@ -140,7 +143,9 @@ fun `Metis - Conversation Overview`() { viewModel = viewModel, onNavigateToConversation = {}, onRequestCreatePersonalConversation = {}, - onRequestAddChannel = {} + onRequestAddChannel = {}, + onRequestBrowseChannel = {}, + canCreateChannel = false, ) }, onNavigateBack = { }, @@ -249,7 +254,8 @@ fun `Metis - Conversation Channel`() { onRequestReactWithEmoji = { _, _, _ -> CompletableDeferred() }, bottomItem = null, onClickViewPost = {}, - onRequestRetrySend = {} + onRequestRetrySend = {}, + title = "" ) } ) diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ArtemisWebView.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ArtemisWebView.kt index 5990ab744..7da78614e 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ArtemisWebView.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ArtemisWebView.kt @@ -10,6 +10,7 @@ import android.webkit.WebSettings import android.webkit.WebView import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -116,7 +117,7 @@ internal fun ArtemisWebView( Box(modifier = modifier) { WebView( - modifier = Modifier, + modifier = Modifier.fillMaxSize(), client = remember(value) { ThemeClient(value) }, state = webViewState, onCreated = { diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt index 5815bb673..623108980 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/ExerciseViewUi.kt @@ -1,6 +1,7 @@ package de.tum.informatics.www1.artemis.native_app.feature.exerciseview import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenTopAppBar.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenTopAppBar.kt index 3914aac2b..31c6dba97 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenTopAppBar.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenTopAppBar.kt @@ -2,6 +2,7 @@ package de.tum.informatics.www1.artemis.native_app.feature.exerciseview.home import androidx.annotation.StringRes import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize @@ -9,11 +10,13 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle @@ -22,7 +25,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -38,6 +41,7 @@ import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.google.accompanist.placeholder.material.placeholder @@ -134,8 +138,22 @@ internal fun TopBarExerciseInformation( ) { val dueDate = exercise.bind { it.dueDate }.orElse(null) val assessmentDueData = exercise.bind { it.assessmentDueDate }.orElse(null) - - // Prepare ui that is movable between long and short toolbars + val releaseData = exercise.bind { it.releaseDate }.orElse(null) + + var maxWidth: Int by remember { mutableIntStateOf(0) } + val updateMaxWidth = { new: Int -> maxWidth = new } + + val dueDateTopBarTextInformation = + @Composable { date: Instant, hintRes: @receiver:StringRes Int -> + TopBarTextInformation( + modifier = Modifier.fillMaxWidth().padding(bottom = 1.dp), + hintColumnWidth = maxWidth, + hint = stringResource(id = hintRes), + dataText = getRelativeTime(to = date).toString(), + dataColor = getDueDateColor(date), + updateHintColumnWidth = updateMaxWidth + ) + } val exerciseInfoUi = @Composable { EmptyDataStateUi( @@ -178,31 +196,27 @@ internal fun TopBarExerciseInformation( } Text( - modifier = Modifier.placeholder(exercise !is DataState.Success), + modifier = Modifier + .placeholder(exercise !is DataState.Success) + .padding(bottom = 4.dp), text = pointsHintText, style = MaterialTheme.typography.bodyLarge ) + + releaseData?.let { + dueDateTopBarTextInformation( + it, + R.string.exercise_view_overview_hint_assessment_release_date + ) + } + } val dueDateColumnUi = @Composable { contentModifier: Modifier -> - var maxWidth: Int by remember { mutableStateOf(0) } - val updateMaxWidth = { new: Int -> maxWidth = new } Column( modifier = contentModifier, - verticalArrangement = Arrangement.spacedBy(8.dp) ) { - val dueDateTopBarTextInformation = - @Composable { date: Instant, hintRes: @receiver:StringRes Int -> - TopBarTextInformation( - modifier = Modifier.fillMaxWidth(), - hintColumnWidth = maxWidth, - hint = stringResource(id = hintRes), - dataText = getRelativeTime(to = date).toString(), - dataColor = getDueDateColor(date), - updateHintColumnWidth = updateMaxWidth - ) - } dueDate?.let { dueDateTopBarTextInformation( @@ -217,21 +231,40 @@ internal fun TopBarExerciseInformation( R.string.exercise_view_overview_hint_assessment_due_date ) } + + val complaintPossible = exercise.bind { exercise -> + exercise.allowComplaintsForAutomaticAssessments + }.orElse(false) + + Text( + modifier = Modifier + .placeholder(exercise !is DataState.Success) + .padding(bottom = 4.dp), + text = "Complaint possible: " + if (complaintPossible == true) "Yes" else "No", + style = MaterialTheme.typography.bodyLarge + ) + } } // Actual UI Column( - modifier = modifier, + modifier = modifier.padding(10.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { - TitleText( + + Text( + text = "Exercise Details", + style = MaterialTheme.typography.titleMedium, modifier = Modifier - .fillMaxWidth() - .graphicsLayer { alpha = titleTextAlpha }, - exerciseDataState = exercise, - style = MaterialTheme.typography.headlineLarge, - maxLines = 2 + .padding(bottom = 1.dp) + .fillMaxWidth(), + textAlign = TextAlign.Start + ) + Divider( + color = Color.Black, + thickness = 3.dp, + modifier = Modifier.padding(vertical = 0.dp) ) // Here we make the distinction in the layout between long toolbar and short toolbar @@ -370,7 +403,7 @@ internal fun StaticTopAppBar( Column(modifier = modifier) { TopAppBar( modifier = Modifier.fillMaxWidth(), - title = {}, + title = { TitleText(modifier = modifier, maxLines = 1, exerciseDataState = exerciseDataState) }, navigationIcon = { TopAppBarNavigationIcon(onNavigateBack = onNavigateBack) }, @@ -382,7 +415,8 @@ internal fun StaticTopAppBar( modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 16.dp), + .padding(horizontal = 16.dp) + .border(2.dp, Color.Black, RoundedCornerShape(4.dp)), titleTextAlpha = 1f, exercise = exerciseDataState, isLongToolbar = isLongToolbar diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt index 2caba8fa8..b351c303f 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/overview/ExerciseOverviewTab.kt @@ -2,9 +2,12 @@ package de.tum.informatics.www1.artemis.native_app.feature.exerciseview.home.ove import android.annotation.SuppressLint import android.webkit.WebView +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import com.google.accompanist.web.WebViewState import de.tum.informatics.www1.artemis.native_app.core.model.exercise.Exercise @@ -15,7 +18,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.exerciseview.ArtemisWe @SuppressLint("SetJavaScriptEnabled") @Composable internal fun ExerciseOverviewTab( - modifier: Modifier, + modifier: Modifier = Modifier, exercise: Exercise, webViewState: WebViewState?, serverUrl: String, @@ -24,24 +27,41 @@ internal fun ExerciseOverviewTab( webView: WebView?, actions: ExerciseActions ) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp)) { - Spacer(modifier = Modifier) + Column( + modifier = modifier + .fillMaxSize() + .background(Color.White), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + ParticipationStatusUi( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), exercise = exercise, actions = actions ) if (exercise !is QuizExercise && webViewState != null) { ArtemisWebView( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .weight(1f), webViewState = webViewState, webView = webView, serverUrl = serverUrl, authToken = authToken, setWebView = setWebView ) + } else { + Text( + text = "No problem statement available.", + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + color = Color.Gray + ) } } } diff --git a/feature/exercise-view/src/main/res/values/exercise_view_strings.xml b/feature/exercise-view/src/main/res/values/exercise_view_strings.xml index d7cc8a39f..8580ba5ff 100644 --- a/feature/exercise-view/src/main/res/values/exercise_view_strings.xml +++ b/feature/exercise-view/src/main/res/values/exercise_view_strings.xml @@ -13,6 +13,7 @@ No points Submission due: Assessment due: + Release date: Your exercise status: diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/chatlist/MetisChatList.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/chatlist/MetisChatList.kt index 6ce519471..64da38bce 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/chatlist/MetisChatList.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/chatlist/MetisChatList.kt @@ -17,6 +17,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -27,6 +28,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.paging.compose.LazyPagingItems +import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.MetisModificationFailure import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.ConversationViewModel @@ -42,6 +44,7 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.S import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IStandalonePost import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.PostPojo import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.PagingStateError +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.ReportVisibleMetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.VisiblePostList import kotlinx.coroutines.Deferred @@ -64,7 +67,8 @@ internal fun MetisChatList( listContentPadding: PaddingValues, state: LazyListState = rememberLazyListState(), isReplyEnabled: Boolean = true, - onClickViewPost: (StandalonePostId) -> Unit + onClickViewPost: (StandalonePostId) -> Unit, + title: String? = "Replying..." ) { ReportVisibleMetisContext(remember(viewModel.metisContext) { VisiblePostList(viewModel.metisContext) }) @@ -75,6 +79,14 @@ internal fun MetisChatList( val bottomItem: PostPojo? by viewModel.chatListUseCase.bottomPost.collectAsState() + val conversationDataState by viewModel.latestUpdatedConversation.collectAsState() + + val updatedTitle by remember(conversationDataState) { + derivedStateOf { + conversationDataState.bind { it.humanReadableName }.orElse("Conversation") + } + } + MetisChatList( modifier = modifier, initialReplyTextProvider = viewModel, @@ -92,7 +104,8 @@ internal fun MetisChatList( onDeletePost = viewModel::deletePost, onRequestReactWithEmoji = viewModel::createOrDeleteReaction, onClickViewPost = onClickViewPost, - onRequestRetrySend = viewModel::retryCreatePost + onRequestRetrySend = viewModel::retryCreatePost, + title = updatedTitle ) } @@ -114,7 +127,8 @@ fun MetisChatList( onDeletePost: (IStandalonePost) -> Deferred, onRequestReactWithEmoji: (IStandalonePost, emojiId: String, create: Boolean) -> Deferred, onClickViewPost: (StandalonePostId) -> Unit, - onRequestRetrySend: (StandalonePostId) -> Unit + onRequestRetrySend: (StandalonePostId) -> Unit, + title: String ) { MetisReplyHandler( initialReplyTextProvider = initialReplyTextProvider, @@ -181,7 +195,8 @@ fun MetisChatList( ReplyTextField( modifier = Modifier.fillMaxWidth(), replyMode = replyMode, - updateFailureState = updateFailureStateDelegate + updateFailureState = updateFailureStateDelegate, + title = title, ) } } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt index c0c3538c3..2e09e64dd 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/post/PostContextBottomSheet.kt @@ -16,10 +16,10 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddReaction import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.filled.MoreHoriz import androidx.compose.material.icons.filled.Reply import androidx.compose.material3.Divider import androidx.compose.material3.Icon @@ -200,7 +200,7 @@ private fun EmojiReactionBar( disabled = false ) { Icon( - imageVector = Icons.Default.MoreHoriz, + imageVector = Icons.Default.AddReaction, contentDescription = null ) } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextField.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextField.kt index 6412f1598..83cc9c20a 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextField.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextField.kt @@ -2,36 +2,15 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Cancel import androidx.compose.material.icons.filled.Done import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Send -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -40,16 +19,18 @@ import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.getTextBeforeSelection import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.ui.AwaitDeferredCompletion -import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.service.MetisModificationFailure import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.thread.ReplyState import kotlinx.coroutines.CompletableDeferred @@ -68,7 +49,8 @@ private const val DisabledContentAlpha = 0.75f internal fun ReplyTextField( modifier: Modifier, replyMode: ReplyMode, - updateFailureState: (MetisModificationFailure?) -> Unit + updateFailureState: (MetisModificationFailure?) -> Unit, + title: String ) { val replyState: ReplyState = rememberReplyState(replyMode, updateFailureState) @@ -96,7 +78,8 @@ internal fun ReplyTextField( .fillMaxWidth() .testTag(TEST_TAG_CAN_CREATE_REPLY), replyMode = replyMode, - onReply = { targetReplyState.onCreateReply() } + onReply = { targetReplyState.onCreateReply() }, + title = "Message $title" ) } @@ -115,7 +98,8 @@ internal fun ReplyTextField( modifier = Modifier .fillMaxWidth() .padding(horizontal = 8.dp), - onCancel = targetReplyState.onCancelSendReply + onCancel = targetReplyState.onCancelSendReply, + title = title ) } } @@ -125,7 +109,7 @@ internal fun ReplyTextField( } @Composable -private fun SendingReplyUi(modifier: Modifier, onCancel: () -> Unit) { +private fun SendingReplyUi(modifier: Modifier, onCancel: () -> Unit, title: String?) { Row( modifier = modifier, verticalAlignment = Alignment.CenterVertically @@ -136,7 +120,7 @@ private fun SendingReplyUi(modifier: Modifier, onCancel: () -> Unit) { ) Text( - text = stringResource(id = R.string.create_answer_sending_reply), + text = title.toString(), textAlign = TextAlign.Center, modifier = Modifier.weight(1f) ) @@ -152,7 +136,8 @@ private fun CreateReplyUi( modifier: Modifier, replyMode: ReplyMode, focusRequester: FocusRequester = remember { FocusRequester() }, - onReply: () -> Unit + onReply: () -> Unit, + title: String? ) { var prevReplyContent by remember { mutableStateOf("") } var displayTextField: Boolean by remember { mutableStateOf(false) } @@ -173,12 +158,6 @@ private fun CreateReplyUi( requestDismissAutoCompletePopup = false } - // Quite hacky! - /* - We do not want to dismiss the popup when the user pressed on their keyboard, so we wait - 100 ms before we actually dismiss the popup. If in the meantime, the user entered a key again - we keep showing the popup. - */ LaunchedEffect(requestDismissAutoCompletePopup) { if (requestDismissAutoCompletePopup) { delay(100) @@ -186,95 +165,280 @@ private fun CreateReplyUi( } } - Box(modifier = modifier) { - if (displayTextField || currentTextFieldValue.text.isNotBlank()) { - val tagChars = LocalReplyAutoCompleteHintProvider.current.legalTagChars - val autoCompleteHints = manageAutoCompleteHints(currentTextFieldValue) - - var textFieldWidth by remember { mutableIntStateOf(0) } - var popupMaxHeight by remember { mutableIntStateOf(0) } - - if (autoCompleteHints.orEmpty().flatMap { it.items } - .isNotEmpty() && mayShowAutoCompletePopup) { - ReplyAutoCompletePopup( - autoCompleteCategories = autoCompleteHints.orEmpty(), - targetWidth = with(LocalDensity.current) { textFieldWidth.toDp() }, - maxHeight = with(LocalDensity.current) { popupMaxHeight.toDp() }, - popupPositionProvider = ReplyAutoCompletePopupPositionProvider, - performAutoComplete = { replacement -> - replyMode.onUpdate( - performAutoComplete( - currentTextFieldValue, - tagChars, - replacement + Column(modifier = modifier) { + Box(modifier = Modifier.fillMaxWidth()) { + if (displayTextField || currentTextFieldValue.text.isNotBlank()) { + val tagChars = LocalReplyAutoCompleteHintProvider.current.legalTagChars + val autoCompleteHints = manageAutoCompleteHints(currentTextFieldValue) + + var textFieldWidth by remember { mutableIntStateOf(0) } + var popupMaxHeight by remember { mutableStateOf(0) } + + if (autoCompleteHints.orEmpty().flatMap { it.items } + .isNotEmpty() && mayShowAutoCompletePopup) { + ReplyAutoCompletePopup( + autoCompleteCategories = autoCompleteHints.orEmpty(), + targetWidth = with(LocalDensity.current) { textFieldWidth.toDp() }, + maxHeight = with(LocalDensity.current) { popupMaxHeight.toDp() }, + popupPositionProvider = ReplyAutoCompletePopupPositionProvider, + performAutoComplete = { replacement -> + replyMode.onUpdate( + performAutoComplete( + currentTextFieldValue, + tagChars, + replacement + ) ) - ) + }, + onDismissRequest = { + requestDismissAutoCompletePopup = true + } + ) + } + + MarkdownTextField( + modifier = Modifier + .fillMaxWidth() + .onSizeChanged { textFieldWidth = it.width } + .padding(vertical = 8.dp, horizontal = 8.dp) + .onGloballyPositioned { coordinates -> + val textFieldWindowTopLeft = coordinates.localToWindow(Offset.Zero) + popupMaxHeight = textFieldWindowTopLeft.y.toInt() + } + .testTag(TEST_TAG_REPLY_TEXT_FIELD), + textFieldValue = currentTextFieldValue, + onTextChanged = replyMode::onUpdate, + focusRequester = focusRequester, + onFocusLost = { + if (displayTextField && currentTextFieldValue.text.isEmpty()) { + displayTextField = false + } + }, + sendButton = { + IconButton( + modifier = Modifier.testTag(TEST_TAG_REPLY_SEND_BUTTON), + onClick = onReply, + enabled = currentTextFieldValue.text.isNotBlank() + ) { + Icon( + imageVector = when (replyMode) { + is ReplyMode.EditMessage -> Icons.Default.Edit + is ReplyMode.NewMessage -> Icons.Default.Send + }, + contentDescription = null + ) + } }, - onDismissRequest = { - requestDismissAutoCompletePopup = true + topRightButton = { + if (replyMode is ReplyMode.EditMessage) { + IconButton(onClick = replyMode.onCancelEditMessage) { + Icon(imageVector = Icons.Default.Cancel, contentDescription = null) + } + } } ) - } - MarkdownTextField( - modifier = Modifier - .fillMaxWidth() - .onSizeChanged { textFieldWidth = it.width } - .padding(vertical = 8.dp, horizontal = 8.dp) - .onGloballyPositioned { coordinates -> - val textFieldWindowTopLeft = coordinates.localToWindow(Offset.Zero) - popupMaxHeight = textFieldWindowTopLeft.y.toInt() - } - .testTag(TEST_TAG_REPLY_TEXT_FIELD), - textFieldValue = currentTextFieldValue, - onTextChanged = replyMode::onUpdate, - focusRequester = focusRequester, - onFocusLost = { - if (displayTextField && currentTextFieldValue.text.isEmpty()) { - displayTextField = false - } - }, - sendButton = { - IconButton( - modifier = Modifier.testTag(TEST_TAG_REPLY_SEND_BUTTON), - onClick = onReply, - enabled = currentTextFieldValue.text.isNotBlank() - ) { - Icon( - imageVector = when (replyMode) { - is ReplyMode.EditMessage -> Icons.Default.Edit - is ReplyMode.NewMessage -> Icons.Default.Send - }, - contentDescription = null - ) - } - }, - topRightButton = { - if (replyMode is ReplyMode.EditMessage) { - IconButton(onClick = replyMode.onCancelEditMessage) { - Icon(imageVector = Icons.Default.Cancel, contentDescription = null) - } + LaunchedEffect(requestFocus) { + if (requestFocus) { + focusRequester.requestFocus() + requestFocus = false } } + } else { + UnfocusedPreviewReplyTextField({ + displayTextField = true + requestFocus = true + }, title = title) + } + } + + if (displayTextField || currentTextFieldValue.text.isNotBlank()) { + FormattingOptions( + currentTextFieldValue = currentTextFieldValue, + onTextChanged = replyMode::onUpdate ) + } + } +} - LaunchedEffect(requestFocus) { - if (requestFocus) { - focusRequester.requestFocus() - requestFocus = false - } - } +enum class MarkdownStyle(val startTag: String, val endTag: String) { + Bold("**", "**"), + Italic("*", "*"), + Underline("", ""), + InlineCode("`", "`"), + CodeBlock("```", "```"), + Blockquote("> ", "") +} + +@Composable +private fun FormattingOptions( + currentTextFieldValue: TextFieldValue, + onTextChanged: (TextFieldValue) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.Start + ) { + // Bold Button + IconButton(onClick = { + applyMarkdownStyle( + style = MarkdownStyle.Bold, + currentTextFieldValue = currentTextFieldValue, + onTextChanged = onTextChanged + ) + }) { + Text( + text = "B", + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold) + ) + } + + // Italic Button + IconButton(onClick = { + applyMarkdownStyle( + style = MarkdownStyle.Italic, + currentTextFieldValue = currentTextFieldValue, + onTextChanged = onTextChanged + ) + }) { + Text( + text = "I", + style = MaterialTheme.typography.bodyMedium.copy(fontStyle = FontStyle.Italic) + ) + } + + // Underline Button + IconButton(onClick = { + applyMarkdownStyle( + style = MarkdownStyle.Underline, + currentTextFieldValue = currentTextFieldValue, + onTextChanged = onTextChanged + ) + }) { + Text( + text = "U", + style = MaterialTheme.typography.bodyMedium.copy(textDecoration = TextDecoration.Underline) + ) + } + + // Inline Code Button + IconButton(onClick = { + applyMarkdownStyle( + style = MarkdownStyle.InlineCode, + currentTextFieldValue = currentTextFieldValue, + onTextChanged = onTextChanged + ) + }) { + Text( + text = "", + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace) + ) + } + + // Code Block Button + IconButton(onClick = { + applyMarkdownStyle( + style = MarkdownStyle.CodeBlock, + currentTextFieldValue = currentTextFieldValue, + onTextChanged = onTextChanged + ) + }) { + Text( + text = "{ }", + style = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace) + ) + } + + // Blockquote Button + IconButton(onClick = { + applyMarkdownStyle( + style = MarkdownStyle.Blockquote, + currentTextFieldValue = currentTextFieldValue, + onTextChanged = onTextChanged + ) + }) { + Text( + text = "\"", + style = MaterialTheme.typography.bodyMedium.copy(fontStyle = FontStyle.Italic) + ) + } + } +} + +private fun applyMarkdownStyle( + style: MarkdownStyle, + currentTextFieldValue: TextFieldValue, + onTextChanged: (TextFieldValue) -> Unit +) { + val selection = currentTextFieldValue.selection + val text = currentTextFieldValue.text + + val startTag = style.startTag + val endTag = style.endTag + + if (selection.collapsed) { + // No text selected + if (style == MarkdownStyle.CodeBlock) { + // Insert code block with newlines + val newText = text.substring(0, selection.start) + + "$startTag\n\n$endTag" + + text.substring(selection.end) + val newCursorPosition = selection.start + startTag.length + 1 + onTextChanged( + TextFieldValue( + text = newText, + selection = TextRange(newCursorPosition, newCursorPosition) + ) + ) } else { - UnfocusedPreviewReplyTextField { - displayTextField = true - requestFocus = true - } + // Other styles + val newText = text.substring(0, selection.start) + startTag + endTag + text.substring(selection.end) + val newCursorPosition = selection.start + startTag.length + onTextChanged( + TextFieldValue( + text = newText, + selection = TextRange(newCursorPosition, newCursorPosition) + ) + ) + } + } else { + val selectedText = text.substring(selection.start, selection.end) + if (style == MarkdownStyle.CodeBlock) { + val newText = text.substring(0, selection.start) + + "$startTag\n$selectedText\n$endTag" + + text.substring(selection.end) + val newSelection = TextRange( + selection.start + startTag.length + 1, + selection.end + startTag.length + 1 + ) + onTextChanged( + TextFieldValue( + text = newText, + selection = newSelection + ) + ) + } else { + val newText = text.substring(0, selection.start) + + startTag + + selectedText + + endTag + + text.substring(selection.end) + val newSelection = TextRange(selection.end + startTag.length + endTag.length) + onTextChanged( + TextFieldValue( + text = newText, + selection = newSelection + ) + ) } } } + @Composable -private fun UnfocusedPreviewReplyTextField(onRequestShowTextField: () -> Unit) { +private fun UnfocusedPreviewReplyTextField(onRequestShowTextField: () -> Unit, title: String?) { Row( modifier = Modifier .fillMaxWidth() @@ -283,7 +447,7 @@ private fun UnfocusedPreviewReplyTextField(onRequestShowTextField: () -> Unit) { verticalAlignment = Alignment.CenterVertically ) { Text( - text = stringResource(id = R.string.create_answer_click_to_write), + text = title.toString(), modifier = Modifier .padding(vertical = 8.dp) .weight(1f) @@ -452,7 +616,8 @@ private fun ReplyTextFieldPreview() { ) { CompletableDeferred() }, - updateFailureState = {} + updateFailureState = {}, + title = "Replying.." ) } } diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/thread/MetisThreadUi.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/thread/MetisThreadUi.kt index d4a02983e..1b84448f2 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/thread/MetisThreadUi.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/thread/MetisThreadUi.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.Divider import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -42,12 +43,12 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui. import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.reply.MetisReplyHandler import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.reply.ReplyTextField import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.shared.isReplyEnabled -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.StandalonePostId import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.ReportVisibleMetisContext import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.visiblemetiscontextreporter.VisibleStandalonePostDetails import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.IBasePost import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.AnswerPostPojo import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.db.pojo.PostPojo +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName import kotlinx.coroutines.CompletableDeferred internal const val TEST_TAG_THREAD_LIST = "TEST_TAG_THREAD_LIST" @@ -84,6 +85,12 @@ internal fun MetisThreadUi( val listState = rememberLazyListState() + val title by remember(conversationDataState) { + derivedStateOf { + conversationDataState.bind { it.humanReadableName }.orElse("Conversation") + } + } + ProvideEmojis { MetisReplyHandler( initialReplyTextProvider = viewModel, @@ -149,7 +156,8 @@ internal fun MetisThreadUi( .fillMaxWidth() .heightIn(max = this@BoxWithConstraints.maxHeight * 0.6f), replyMode = replyMode, - updateFailureState = updateFailureStateDelegate + updateFailureState = updateFailureStateDelegate, + title = title ) } } 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 fd105bc07..c62ef87e5 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 @@ -10,9 +10,20 @@ data class ConversationCollections( val channels: ConversationCollection, val groupChats: ConversationCollection, val directChats: ConversationCollection, - val hidden: ConversationCollection + val hidden: ConversationCollection, + val exerciseChannels: ConversationCollection, + val lectureChannels: ConversationCollection, + val examChannels: ConversationCollection ) { - val conversations: List get() = favorites.conversations + channels.conversations + groupChats.conversations + directChats.conversations + hidden.conversations + val conversations: List + get() = favorites.conversations + + channels.conversations + + groupChats.conversations + + directChats.conversations + + hidden.conversations + + exerciseChannels.conversations + + lectureChannels.conversations + + examChannels.conversations fun filtered(query: String): ConversationCollections { return ConversationCollections( @@ -20,13 +31,17 @@ data class ConversationCollections( groupChats = groupChats.filter { it.filterPredicate(query) }, directChats = directChats.filter { it.filterPredicate(query) }, favorites = favorites.filter { it.filterPredicate(query) }, - hidden = hidden.filter { it.filterPredicate(query) } + hidden = hidden.filter { it.filterPredicate(query) }, + exerciseChannels = exerciseChannels.filter { it.filterPredicate(query) }, + lectureChannels = lectureChannels.filter { it.filterPredicate(query) }, + examChannels = examChannels.filter { it.filterPredicate(query) } ) } data class ConversationCollection( val conversations: List, - val isExpanded: Boolean + val isExpanded: Boolean, + val showPrefix: Boolean = true ) { fun filter(predicate: (Conversation) -> Boolean) = copy(conversations = conversations.filter(predicate)) diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/service/storage/ConversationPreferenceService.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/service/storage/ConversationPreferenceService.kt index cc0185922..e068ce5a2 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/service/storage/ConversationPreferenceService.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/service/storage/ConversationPreferenceService.kt @@ -10,7 +10,10 @@ interface ConversationPreferenceService { data class Preferences( val favouritesExpanded: Boolean, - val channelsExpanded: Boolean, + val generalsExpanded: Boolean, + val examsExpanded: Boolean, + val exercisesExpanded: Boolean, + val lecturesExpanded: Boolean, val groupChatsExpanded: Boolean, val personalConversationsExpanded: Boolean, val hiddenExpanded: Boolean diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/service/storage/impl/ConversationPreferenceStorageServiceImpl.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/service/storage/impl/ConversationPreferenceStorageServiceImpl.kt index d878cb4b9..6334f144d 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/service/storage/impl/ConversationPreferenceStorageServiceImpl.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/service/storage/impl/ConversationPreferenceStorageServiceImpl.kt @@ -18,6 +18,9 @@ internal class ConversationPreferenceStorageServiceImpl(private val context: Con private const val KEY_GROUP_CHATS_EXPANDED = "group_chats" private const val KEY_PERSONAL_CONVERSATIONS_EXPANDED = "personal_conv" private const val KEY_HIDDEN_EXPANDED = "hidden" + private const val KEY_EXAMS_EXPANDED = "exams" + private const val KEY_EXERCISES_EXPANDED = "exercises" + private const val KEY_LECTURES_EXPANDED = "lectures" } private val Context.dataStore by preferencesDataStore("conversation_preferences") @@ -28,20 +31,26 @@ internal class ConversationPreferenceStorageServiceImpl(private val context: Con ): Flow = context.dataStore.data.map { data -> ConversationPreferenceService.Preferences( favouritesExpanded = data[getKey(serverUrl, courseId, KEY_FAVOURITES_EXPANDED)] ?: true, - channelsExpanded = data[getKey(serverUrl, courseId, KEY_CHANNELS_EXPANDED)] ?: true, + generalsExpanded = data[getKey(serverUrl, courseId, KEY_CHANNELS_EXPANDED)] ?: true, groupChatsExpanded = data[getKey(serverUrl, courseId, KEY_GROUP_CHATS_EXPANDED)] ?: true, personalConversationsExpanded = data[getKey(serverUrl, courseId, KEY_PERSONAL_CONVERSATIONS_EXPANDED)] ?: true, - hiddenExpanded = data[getKey(serverUrl, courseId, KEY_HIDDEN_EXPANDED)] ?: false + hiddenExpanded = data[getKey(serverUrl, courseId, KEY_HIDDEN_EXPANDED)] ?: false, + examsExpanded = data[getKey(serverUrl, courseId, KEY_EXAMS_EXPANDED)] ?: true, + exercisesExpanded = data[getKey(serverUrl, courseId, KEY_EXERCISES_EXPANDED)] ?: true, + lecturesExpanded = data[getKey(serverUrl, courseId, KEY_LECTURES_EXPANDED)] ?: true, ) } override suspend fun updatePreferences(serverUrl: String, courseId: Long, preferences: ConversationPreferenceService.Preferences) { context.dataStore.edit { data -> data[getKey(serverUrl, courseId, KEY_FAVOURITES_EXPANDED)] = preferences.favouritesExpanded - data[getKey(serverUrl, courseId, KEY_CHANNELS_EXPANDED)] = preferences.channelsExpanded + data[getKey(serverUrl, courseId, KEY_CHANNELS_EXPANDED)] = preferences.generalsExpanded data[getKey(serverUrl, courseId, KEY_GROUP_CHATS_EXPANDED)] = preferences.groupChatsExpanded data[getKey(serverUrl, courseId, KEY_PERSONAL_CONVERSATIONS_EXPANDED)] = preferences.personalConversationsExpanded data[getKey(serverUrl, courseId, KEY_HIDDEN_EXPANDED)] = preferences.hiddenExpanded + data[getKey(serverUrl, courseId, KEY_EXAMS_EXPANDED)] = preferences.examsExpanded + data[getKey(serverUrl, courseId, KEY_EXERCISES_EXPANDED)] = preferences.exercisesExpanded + data[getKey(serverUrl, courseId, KEY_LECTURES_EXPANDED)] = preferences.lecturesExpanded } } diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsScreen.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsScreen.kt index 900ac4c09..8f8e20ecb 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsScreen.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsScreen.kt @@ -1,21 +1,24 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.browse_channels -import androidx.compose.foundation.clickable +import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Create -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -27,9 +30,7 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign -import androidx.navigation.NavController -import androidx.navigation.NavGraphBuilder -import androidx.navigation.NavOptionsBuilder +import androidx.compose.ui.unit.dp import de.tum.informatics.www1.artemis.native_app.core.ui.AwaitDeferredCompletion import de.tum.informatics.www1.artemis.native_app.core.ui.Spacings import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog @@ -37,7 +38,6 @@ import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateU import de.tum.informatics.www1.artemis.native_app.core.ui.compose.NavigationBackButton import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.R import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.common.ChannelIcons -import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.courseNavGraphBuilderExtensions import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat import kotlinx.coroutines.Deferred import org.koin.androidx.compose.koinViewModel @@ -50,14 +50,12 @@ fun BrowseChannelsScreen( modifier: Modifier, courseId: Long, onNavigateToConversation: (conversationId: Long) -> Unit, - onNavigateToCreateChannel: () -> Unit, onNavigateBack: () -> Unit ) { BrowseChannelsScreen( modifier = modifier, viewModel = koinViewModel { parametersOf(courseId) }, onNavigateToConversation = onNavigateToConversation, - onNavigateToCreateChannel = onNavigateToCreateChannel, onNavigateBack = onNavigateBack ) } @@ -67,10 +65,12 @@ internal fun BrowseChannelsScreen( modifier: Modifier, viewModel: BrowseChannelsViewModel, onNavigateToConversation: (conversationId: Long) -> Unit, - onNavigateToCreateChannel: () -> Unit, onNavigateBack: () -> Unit ) { - val canCreateChannel: Boolean by viewModel.canCreateChannel.collectAsState() + + LaunchedEffect(Unit) { + viewModel.requestReload() + } val channelsDataState by viewModel.channels.collectAsState() @@ -98,13 +98,7 @@ internal fun BrowseChannelsScreen( navigationIcon = { NavigationBackButton(onNavigateBack) } ) }, - floatingActionButton = { - if (canCreateChannel) { - FloatingActionButton(onClick = onNavigateToCreateChannel) { - Icon(imageVector = Icons.Default.Create, contentDescription = null) - } - } - } + ) { padding -> BasicDataStateUi( modifier = Modifier @@ -159,20 +153,57 @@ private fun ChannelChatItem(channelChat: ChannelChat, onClick: () -> Unit) { ListItem( modifier = Modifier .fillMaxWidth() - .clickable(onClick = onClick) .testTag(testTagForBrowsedChannelItem(channelChat.id)), leadingContent = { ChannelIcons(channelChat) }, headlineContent = { Text(channelChat.name) }, supportingContent = { - Text( - text = pluralStringResource( - id = R.plurals.browse_channel_channel_item_member_count, - count = channelChat.numberOfMembers, - channelChat.numberOfMembers + Row( + verticalAlignment = Alignment.CenterVertically + ) { + if (channelChat.isMember) { + Text( + text = stringResource(id = R.string.joined_channel), + modifier = Modifier + .padding(end = 8.dp) + .background( + MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(4.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + color = MaterialTheme.colorScheme.onPrimary, + style = MaterialTheme.typography.labelSmall + ) + } + + Text( + text = pluralStringResource( + id = R.plurals.browse_channel_channel_item_member_count, + count = channelChat.numberOfMembers, + channelChat.numberOfMembers + ) ) - ) + } + }, + trailingContent = { + if (!channelChat.isMember) { + Button( + onClick = onClick, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + shape = RoundedCornerShape(4.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), + modifier = Modifier + .wrapContentSize() + ) { + Text(text = stringResource(id = R.string.join_button_title)) + } + } } ) } + + diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsViewModel.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsViewModel.kt index 42d97faf3..18fb801cf 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsViewModel.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/browse_channels/BrowseChannelsViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.tum.informatics.www1.artemis.native_app.core.common.flatMapLatest import de.tum.informatics.www1.artemis.native_app.core.data.DataState +import de.tum.informatics.www1.artemis.native_app.core.data.onSuccess import de.tum.informatics.www1.artemis.native_app.core.data.retryOnInternet import de.tum.informatics.www1.artemis.native_app.core.data.service.network.AccountDataService import de.tum.informatics.www1.artemis.native_app.core.data.service.network.CourseService @@ -12,16 +13,15 @@ import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvider -import de.tum.informatics.www1.artemis.native_app.core.model.account.isAtLeastTutorInCourse +import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.service.network.ChannelService +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.plus import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext @@ -30,7 +30,7 @@ internal class BrowseChannelsViewModel( private val courseId: Long, private val accountService: AccountService, private val serverConfigurationService: ServerConfigurationService, - private val channelService: de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.service.network.ChannelService, + private val channelService: ChannelService, private val networkStatusProvider: NetworkStatusProvider, accountDataService: AccountDataService, courseService: CourseService, @@ -39,7 +39,7 @@ internal class BrowseChannelsViewModel( private val requestRefresh = MutableSharedFlow(extraBufferCapacity = 1) - val channels: StateFlow>> = flatMapLatest( + val channels: StateFlow>> = flatMapLatest( serverConfigurationService.serverUrl, accountService.authToken, requestRefresh.onStart { emit(Unit) } @@ -47,50 +47,42 @@ internal class BrowseChannelsViewModel( retryOnInternet(networkStatusProvider.currentNetworkStatus) { channelService .getChannels(courseId, serverUrl, authToken) - .bind { channels -> channels.filter { !it.isMember } } + .bind { channels -> channels } } } .stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly) - val canCreateChannel: StateFlow = flatMapLatest( - serverConfigurationService.serverUrl, - accountService.authToken, - requestRefresh.onStart { emit(Unit) } - ) { serverUrl, authToken, _ -> - retryOnInternet(networkStatusProvider.currentNetworkStatus) { - courseService.getCourse(courseId, serverUrl, authToken) - .then { courseWithScore -> - accountDataService - .getAccountData(serverUrl, authToken) - .bind { it.isAtLeastTutorInCourse(courseWithScore.course) } - } - }.map { it.orElse(false) } - - } - .stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly, false) /** * Returns the id of the channel on registration success or null if any error occurred */ - fun registerInChannel(channelChat: de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.ChannelChat): Deferred { + fun registerInChannel(channelChat: ChannelChat): Deferred { return viewModelScope.async(coroutineContext) { val username = when (val authData = accountService.authenticationData.first()) { is AccountService.AuthenticationData.LoggedIn -> authData.username AccountService.AuthenticationData.NotLoggedIn -> return@async null } - channelService.registerInChannel( + val result = channelService.registerInChannel( courseId = courseId, conversationId = channelChat.id, username = username, serverUrl = serverConfigurationService.serverUrl.first(), authToken = accountService.authToken.first() ) - .bind { if (it) channelChat.id else null } + + result.onSuccess { successful -> + if (successful) { + requestReload() + } + } + + result.bind { if (it) channelChat.id else null } .orNull() } } + fun requestReload() { requestRefresh.tryEmit(Unit) } diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_channel/CreateChannelScreen.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_channel/CreateChannelScreen.kt index bf67a6efb..5a1a495ed 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_channel/CreateChannelScreen.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_channel/CreateChannelScreen.kt @@ -4,18 +4,17 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Create -import androidx.compose.material3.Icon +import androidx.compose.material3.Button import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.RadioButton import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable @@ -26,15 +25,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewModelScope import de.tum.informatics.www1.artemis.native_app.core.ui.Spacings import de.tum.informatics.www1.artemis.native_app.core.ui.alert.TextAlertDialog -import de.tum.informatics.www1.artemis.native_app.core.ui.compose.JobAnimatedFloatingActionButton import de.tum.informatics.www1.artemis.native_app.core.ui.compose.NavigationBackButton import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.R import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.PotentiallyIllegalTextField +import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.core.parameter.parametersOf @@ -76,7 +75,7 @@ internal fun CreateChannelScreen( val isNameIllegal by viewModel.isNameIllegal.collectAsState() val isDescriptionIllegal by viewModel.isDescriptionIllegal.collectAsState() - val isPublic by viewModel.isPublic.collectAsState() + val isPrivate by viewModel.isPrivate.collectAsState() val isAnnouncement by viewModel.isAnnouncement.collectAsState() val canCreate by viewModel.canCreate.collectAsState() @@ -93,22 +92,7 @@ internal fun CreateChannelScreen( } ) }, - floatingActionButton = { - JobAnimatedFloatingActionButton( - modifier = Modifier.testTag(TEST_TAG_CREATE_CHANNEL_BUTTON), - enabled = canCreate, - startJob = viewModel::createChannel, - onJobCompleted = { channel -> - if (channel != null) { - onConversationCreated(channel.id) - } else { - isDisplayingErrorDialog = true - } - } - ) { - Icon(imageVector = Icons.Default.Create, contentDescription = null) - } - } + ) { paddingValues -> Column( modifier = Modifier @@ -120,7 +104,8 @@ internal fun CreateChannelScreen( ) { Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = R.string.create_channel_description) + text = stringResource(id = R.string.create_channel_description), + style = MaterialTheme.typography.bodySmall ) PotentiallyIllegalTextField( @@ -155,27 +140,40 @@ internal fun CreateChannelScreen( modifier = Modifier.fillMaxWidth(), title = stringResource(id = R.string.create_channel_channel_accessibility_type), description = stringResource(id = R.string.create_channel_channel_accessibility_type_hint), - choiceOne = stringResource(id = R.string.create_channel_channel_accessibility_type_public), - choiceTwo = stringResource(id = R.string.create_channel_channel_accessibility_type_private), - choice = isPublic, - choiceOneButtonTestTag = TEST_TAG_SET_PUBLIC_BUTTON, - choiceTwoButtonTestTag = TEST_TAG_SET_PRIVATE_BUTTON, - updateChoice = viewModel::updatePublic + isChecked = isPrivate, + onCheckedChange = { viewModel.updatePublic(it) } ) BinarySelection( modifier = Modifier.fillMaxWidth(), title = stringResource(id = R.string.create_channel_channel_announcement_type), description = stringResource(id = R.string.create_channel_channel_announcement_type_hint), - choiceOne = stringResource(id = R.string.create_channel_channel_announcement_type_announcement), - choiceTwo = stringResource(id = R.string.create_channel_channel_announcement_type_unrestricted), - choice = isAnnouncement, - choiceOneButtonTestTag = TEST_TAG_SET_ANNOUNCEMENT_BUTTON, - choiceTwoButtonTestTag = TEST_TAG_SET_UNRESTRICTED_BUTTON, - updateChoice = viewModel::updateAnnouncement + isChecked = isAnnouncement, + onCheckedChange = { viewModel.updateAnnouncement(it) } ) - Box(modifier = Modifier.height(Spacings.FabContentBottomPadding)) + Button( + onClick = { + viewModel.viewModelScope.launch { + val channel = viewModel.createChannel().await() + if (channel != null) { + onConversationCreated(channel.id) + } else { + isDisplayingErrorDialog = true + } + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + enabled = viewModel.canCreate.collectAsState().value, + + ) { + Text(text = "Create Channel") + } + + Spacer(modifier = Modifier.height(16.dp)) + } } @@ -196,68 +194,33 @@ private fun BinarySelection( modifier: Modifier, title: String, description: String, - choiceOne: String, - choiceTwo: String, - choice: Boolean, - choiceOneButtonTestTag: String, - choiceTwoButtonTestTag: String, - updateChoice: (Boolean) -> Unit + isChecked: Boolean, + onCheckedChange: (Boolean) -> Unit ) { Column( modifier = modifier, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Text( + Row( modifier = Modifier.fillMaxWidth(), - text = title, - style = MaterialTheme.typography.titleSmall - ) - - Column { - RadioButtonWithText( - modifier = Modifier.fillMaxWidth(), - buttonTestTag = choiceOneButtonTestTag, - isChecked = choice, - onClick = { updateChoice(true) }, - text = choiceOne + verticalAlignment = Alignment.CenterVertically + ) { + Text( + modifier = Modifier.weight(1f), + text = title, + style = MaterialTheme.typography.titleMedium ) - - RadioButtonWithText( - modifier = Modifier.fillMaxWidth(), - buttonTestTag = choiceTwoButtonTestTag, - isChecked = !choice, - onClick = { - updateChoice(false) - }, - text = choiceTwo + Switch( + checked = isChecked, + onCheckedChange = onCheckedChange ) } Text( modifier = Modifier.fillMaxWidth(), text = description, + style = MaterialTheme.typography.bodySmall ) } } -@Composable -private fun RadioButtonWithText( - modifier: Modifier, - buttonTestTag: String, - isChecked: Boolean, - onClick: () -> Unit, - text: String -) { - Row( - modifier = modifier, - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - modifier = Modifier.testTag(buttonTestTag), - selected = isChecked, - onClick = onClick - ) - - Text(text = text) - } -} diff --git a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_channel/CreateChannelViewModel.kt b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_channel/CreateChannelViewModel.kt index eaed389ef..95fa3e8b9 100644 --- a/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_channel/CreateChannelViewModel.kt +++ b/feature/metis/manage-conversations/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/ui/conversation/create_channel/CreateChannelViewModel.kt @@ -31,16 +31,16 @@ internal class CreateChannelViewModel( private companion object { private const val KEY_NAME = "name" private const val KEY_DESCRIPTION = "description" - private const val KEY_IS_PUBLIC = "is_public" + private const val KEY_IS_PRIVATE = "is_private" private const val KEY_IS_ANNOUNCEMENT = "announcement" } val name: StateFlow = savedStateHandle.getStateFlow(KEY_NAME, "") val description: StateFlow = savedStateHandle.getStateFlow(KEY_DESCRIPTION, "") - val isPublic: StateFlow = savedStateHandle.getStateFlow(KEY_IS_PUBLIC, true) + val isPrivate: StateFlow = savedStateHandle.getStateFlow(KEY_IS_PRIVATE, false) val isAnnouncement: StateFlow = - savedStateHandle.getStateFlow(KEY_IS_ANNOUNCEMENT, true) + savedStateHandle.getStateFlow(KEY_IS_ANNOUNCEMENT, false) val isNameIllegal: StateFlow = name .mapIsChannelNameIllegal() @@ -65,7 +65,7 @@ internal class CreateChannelViewModel( courseId = courseId, name = name.value, description = description.value, - isPublic = isPublic.value, + isPublic = !isPrivate.value, isAnnouncement = isAnnouncement.value, authToken = authToken, serverUrl = serverUrl @@ -81,8 +81,8 @@ internal class CreateChannelViewModel( savedStateHandle[KEY_DESCRIPTION] = description } - fun updatePublic(isPublic: Boolean) { - savedStateHandle[KEY_IS_PUBLIC] = isPublic + fun updatePublic(isPrivate: Boolean) { + savedStateHandle[KEY_IS_PRIVATE] = isPrivate } fun updateAnnouncement(isAnnouncement: Boolean) { 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 56a3101be..85db1562d 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 @@ -2,31 +2,14 @@ package de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversat import androidx.annotation.StringRes import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.ArrowRight -import androidx.compose.material.icons.filled.Favorite -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.material.icons.filled.* import androidx.compose.material3.Divider import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -36,15 +19,12 @@ 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 -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.R @@ -54,20 +34,25 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.d import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.Conversation import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.GroupChat import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.dto.conversation.OneToOneChat -import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableTitle +import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.ui.humanReadableName internal const val TEST_TAG_CONVERSATION_LIST = "conversation list" - internal const val TEST_TAG_HEADER_EXPAND_ICON = "expand icon" internal const val SECTION_FAVORITES_KEY = "favorites" internal const val SECTION_HIDDEN_KEY = "hidden" internal const val SECTION_CHANNELS_KEY = "channels" internal const val SECTION_GROUPS_KEY = "groups" +internal const val SECTION_EXERCISES_KEY = "exercises" +internal const val SECTION_EXAMS_KEY = "exams" +internal const val SECTION_LECTURES_KEY = "lectures" internal const val SECTION_DIRECT_MESSAGES_KEY = "direct-messages" internal const val KEY_SUFFIX_FAVORITES = "_f" internal const val KEY_SUFFIX_CHANNELS = "_c" +internal const val KEY_SUFFIX_EXAMS = "_exa" +internal const val KEY_SUFFIX_EXERCISES = "_exe" +internal const val KEY_SUFFIX_LECTURES = "_l" internal const val KEY_SUFFIX_GROUPS = "_g" internal const val KEY_SUFFIX_PERSONAL = "_p" internal const val KEY_SUFFIX_HIDDEN = "_h" @@ -87,19 +72,22 @@ internal fun ConversationList( onRequestAddChannel: () -> Unit, trailingContent: LazyListScope.() -> Unit ) { - val listWithHeader: LazyListScope.(ConversationCollections.ConversationCollection<*>, String, String, Int, ConversationSectionHeaderAction, () -> Unit) -> Unit = - { collection, key, suffix, textRes, action, toggleIsExpanded -> + + val listWithHeader: LazyListScope.(ConversationCollections.ConversationCollection<*>, String, String, Int, ConversationSectionHeaderAction, () -> Unit, @Composable () -> Unit) -> Unit = + { collection, key, suffix, textRes, action, toggleIsExpanded, icon -> conversationSectionHeader( key = key, text = textRes, onClickAddAction = action, isExpanded = collection.isExpanded, - toggleIsExpanded = toggleIsExpanded + toggleIsExpanded = toggleIsExpanded, + icon = icon ) conversationList( keySuffix = suffix, conversations = collection, + showPrefix = collection.showPrefix, onNavigateToConversation = onNavigateToConversation, onToggleMarkAsFavourite = onToggleMarkAsFavourite, onToggleHidden = onToggleHidden, @@ -115,7 +103,8 @@ internal fun ConversationList( KEY_SUFFIX_FAVORITES, R.string.conversation_overview_section_favorites, NoAction, - viewModel::toggleFavoritesExpanded + { viewModel.toggleFavoritesExpanded() }, + { Icon(imageVector = Icons.Default.Favorite, contentDescription = null) } ) } @@ -123,10 +112,43 @@ internal fun ConversationList( conversationCollections.channels, SECTION_CHANNELS_KEY, KEY_SUFFIX_CHANNELS, - R.string.conversation_overview_section_channels, + R.string.conversation_overview_section_general_channels, OnClickAction(onRequestAddChannel), - viewModel::toggleChannelsExpanded - ) + viewModel::toggleGeneralsExpanded + ) { Icon(imageVector = Icons.Default.ChatBubble, contentDescription = null) } + + if (conversationCollections.exerciseChannels.conversations.isNotEmpty()) { + listWithHeader( + conversationCollections.exerciseChannels, + SECTION_EXERCISES_KEY, + KEY_SUFFIX_EXERCISES, + R.string.conversation_overview_section_exercise_channels, + NoAction, + viewModel::toggleExercisesExpanded + ) { Icon(imageVector = Icons.Default.List, contentDescription = null) } + } + + if (conversationCollections.lectureChannels.conversations.isNotEmpty()) { + listWithHeader( + conversationCollections.lectureChannels, + SECTION_LECTURES_KEY, + KEY_SUFFIX_LECTURES, + R.string.conversation_overview_section_lecture_channels, + NoAction, + viewModel::toggleLecturesExpanded + ) { Icon(imageVector = Icons.Default.InsertDriveFile, contentDescription = null) } + } + + if (conversationCollections.examChannels.conversations.isNotEmpty()) { + listWithHeader( + conversationCollections.examChannels, + SECTION_EXAMS_KEY, + KEY_SUFFIX_EXAMS, + R.string.conversation_overview_section_exam_channels, + NoAction, + viewModel::toggleExamsExpanded + ) { Icon(imageVector = Icons.Default.School, contentDescription = null) } + } listWithHeader( conversationCollections.groupChats, @@ -135,7 +157,7 @@ internal fun ConversationList( R.string.conversation_overview_section_groups, OnClickAction(onRequestCreatePersonalConversation), viewModel::toggleGroupChatsExpanded - ) + ) { Icon(imageVector = Icons.Default.Forum, contentDescription = null) } listWithHeader( conversationCollections.directChats, @@ -144,7 +166,7 @@ internal fun ConversationList( R.string.conversation_overview_section_direct_messages, OnClickAction(onRequestCreatePersonalConversation), viewModel::togglePersonalConversationsExpanded - ) + ) { Icon(imageVector = Icons.Default.Message, contentDescription = null) } if (conversationCollections.hidden.conversations.isNotEmpty()) { listWithHeader( @@ -154,7 +176,7 @@ internal fun ConversationList( R.string.conversation_overview_section_hidden, NoAction, viewModel::toggleHiddenExpanded - ) + ) { Icon(imageVector = Icons.Default.NotInterested, contentDescription = null) } } trailingContent() @@ -166,7 +188,8 @@ private fun LazyListScope.conversationSectionHeader( @StringRes text: Int, isExpanded: Boolean, onClickAddAction: ConversationSectionHeaderAction, - toggleIsExpanded: () -> Unit + toggleIsExpanded: () -> Unit, + icon: @Composable () -> Unit ) { item(key = key) { Column( @@ -177,41 +200,37 @@ private fun LazyListScope.conversationSectionHeader( Divider() Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + modifier = Modifier + .fillMaxWidth() + .clickable { toggleIsExpanded() } + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + icon() + Text( + modifier = Modifier + .weight(1f) + .padding(start = 8.dp), + text = stringResource(id = text), + style = MaterialTheme.typography.titleSmall + ) + } + IconButton( modifier = Modifier.testTag(TEST_TAG_HEADER_EXPAND_ICON), - onClick = { - toggleIsExpanded() - } + onClick = { toggleIsExpanded() } ) { Icon( imageVector = if (isExpanded) Icons.Default.ArrowDropDown else Icons.Default.ArrowRight, - contentDescription = null + contentDescription = null, + modifier = Modifier.size(32.dp) ) } - - Text( - modifier = Modifier - .weight(1f) - .padding(horizontal = 16.dp), - text = stringResource(id = text), - style = MaterialTheme.typography.titleSmall - ) - - if (onClickAddAction is OnClickAction) { - IconButton( - onClick = onClickAddAction.onClick - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null - ) - } - } else { - Box(modifier = Modifier.height(40.dp)) - } } Divider() @@ -222,6 +241,7 @@ private fun LazyListScope.conversationSectionHeader( private fun LazyListScope.conversationList( keySuffix: String, conversations: ConversationCollections.ConversationCollection, + showPrefix: Boolean, onNavigateToConversation: (conversationId: Long) -> Unit, onToggleMarkAsFavourite: (conversationId: Long, favorite: Boolean) -> Unit, onToggleHidden: (conversationId: Long, hidden: Boolean) -> Unit, @@ -230,12 +250,14 @@ private fun LazyListScope.conversationList( if (!conversations.isExpanded) return items( conversations.conversations, - key = { tagForConversation(it.id, keySuffix) }) { conversation -> + key = { tagForConversation(it.id, keySuffix) } + ) { conversation -> ConversationListItem( modifier = Modifier .fillMaxWidth() .testTag(tagForConversation(conversation.id, keySuffix)), conversation = conversation, + showPrefix = showPrefix, onNavigateToConversation = { onNavigateToConversation(conversation.id) }, onToggleMarkAsFavourite = { onToggleMarkAsFavourite( @@ -245,122 +267,124 @@ 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) { - stringResource( - id = R.string.conversation_overview_archived_channel_name, - conversation.name - ) - } else conversation.name - - ListItem( - modifier = contentModifier, - leadingContent = { - PrimaryChannelIcon(channelChat = conversation) - }, - headlineContent = { - Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { - Text(text = channelName, maxLines = 1, color = headlineColor) - - ExtraChannelIcons(channelChat = conversation) - } - }, - trailingContent = { - UnreadMessages(unreadMessagesCount = unreadMessagesCount) - } - ) - } - - is GroupChat -> { - ListItem( - modifier = contentModifier, - headlineContent = { - Text( - conversation.humanReadableTitle, - color = headlineColor - ) - }, - leadingContent = { - Icon(imageVector = Icons.Default.Groups2, contentDescription = null) - }, - trailingContent = { - UnreadMessages(unreadMessagesCount = unreadMessagesCount) - } - ) - } - - is OneToOneChat -> { - ListItem( - modifier = contentModifier, - headlineContent = { - Text( - conversation.humanReadableTitle, - color = headlineColor - ) - }, - trailingContent = { - UnreadMessages(unreadMessagesCount = unreadMessagesCount) - } - ) - } - } - } ) } } -@Composable -private fun UnreadMessages(modifier: Modifier = Modifier, unreadMessagesCount: Long) { - if (unreadMessagesCount > 0) { - Box( - modifier = modifier - .size(24.dp) - .aspectRatio(1f) - .background( - MaterialTheme.colorScheme.primaryContainer, - CircleShape - ), - contentAlignment = Alignment.Center - ) { - Text( - text = unreadMessagesCount.toString(), - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } -} - @Composable private fun ConversationListItem( modifier: Modifier = Modifier, conversation: Conversation, + showPrefix: Boolean, onNavigateToConversation: () -> Unit, onToggleMarkAsFavourite: () -> Unit, onToggleHidden: () -> Unit, onToggleMuted: () -> Unit, - content: @Composable (Modifier) -> Unit ) { var isContextDialogShown by remember { mutableStateOf(false) } val onDismissRequest = { isContextDialogShown = false } + val unreadMessagesCount = conversation.unreadMessagesCount ?: 0 + + val headlineColor = + LocalContentColor.current.copy(alpha = if (conversation.isMuted) 0.6f else 1f) + + val displayName = when (conversation) { + is ChannelChat -> { + val channelName = if (conversation.isArchived) { + stringResource( + id = R.string.conversation_overview_archived_channel_name, + conversation.name + ) + } else conversation.name + + if (showPrefix) { + channelName + } else { + channelName.removeSectionPrefix() + } + } + is GroupChat, is OneToOneChat -> { + val humanReadableTitle = conversation.humanReadableName + if (showPrefix) { + humanReadableTitle + } else { + humanReadableTitle.removeSectionPrefix() + } + } + else -> conversation.humanReadableName + } + Box(modifier = modifier) { - content( - Modifier.combinedClickable( - onClick = onNavigateToConversation, - onLongClick = { isContextDialogShown = true } - ) - ) + when (conversation) { + is ChannelChat -> { + ListItem( + modifier = Modifier.clickable(onClick = onNavigateToConversation), + leadingContent = { + PrimaryChannelIcon(channelChat = conversation) + }, + headlineContent = { + Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) { + Text(text = displayName, maxLines = 1, color = headlineColor) + + ExtraChannelIcons(channelChat = conversation) + } + }, + trailingContent = { + UnreadMessages( + modifier = Modifier.padding(end = 24.dp), + unreadMessagesCount = unreadMessagesCount + ) + } + ) + } + + is GroupChat -> { + ListItem( + modifier = Modifier.clickable(onClick = onNavigateToConversation), + headlineContent = { + Text( + displayName, + color = headlineColor + ) + }, + leadingContent = { + Icon(imageVector = Icons.Default.Groups2, contentDescription = null) + }, + trailingContent = { + UnreadMessages(unreadMessagesCount = unreadMessagesCount) + } + ) + } + + is OneToOneChat -> { + ListItem( + modifier = Modifier.clickable(onClick = onNavigateToConversation), + headlineContent = { + Text( + displayName, + color = headlineColor + ) + }, + trailingContent = { + UnreadMessages(unreadMessagesCount = unreadMessagesCount) + } + ) + } + } + + IconButton( + onClick = { isContextDialogShown = true }, + modifier = Modifier.align(Alignment.CenterEnd) + ) { + Icon(imageVector = Icons.Default.MoreHoriz, contentDescription = null) + } DropdownMenu( expanded = isContextDialogShown, - onDismissRequest = onDismissRequest + onDismissRequest = onDismissRequest, + modifier = Modifier.align(Alignment.TopEnd), + offset = DpOffset(x = (-10).dp, y = 0.dp), ) { DropdownMenuItem( leadingIcon = { @@ -428,8 +452,41 @@ private fun ConversationListItem( } } +@Composable +private fun UnreadMessages(modifier: Modifier = Modifier, unreadMessagesCount: Long) { + if (unreadMessagesCount > 0) { + Box( + modifier = modifier + .size(24.dp) + .aspectRatio(1f) + .background( + MaterialTheme.colorScheme.primaryContainer, + CircleShape + ), + contentAlignment = Alignment.Center + ) { + Text( + text = unreadMessagesCount.toString(), + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } +} + private sealed interface ConversationSectionHeaderAction private data class OnClickAction(val onClick: () -> Unit) : ConversationSectionHeaderAction private object NoAction : ConversationSectionHeaderAction + +private fun String.removeSectionPrefix(): String { + val prefixes = listOf("exercise-", "lecture-", "exam-") + var result = this + for (prefix in prefixes) { + if (result.startsWith(prefix, ignoreCase = true)) { + result = result.removePrefix(prefix) + break + } + } + return result.trim() +} 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 f6399278c..1e1babddc 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 @@ -9,14 +9,22 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AddComment +import androidx.compose.material.icons.filled.ChatBubble +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Tag import androidx.compose.material.icons.filled.WifiOff -import androidx.compose.material3.Button import androidx.compose.material3.Divider +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton @@ -32,6 +40,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import de.tum.informatics.www1.artemis.native_app.core.data.DataState import de.tum.informatics.www1.artemis.native_app.core.ui.common.BasicDataStateUi @@ -50,14 +59,18 @@ fun ConversationOverviewBody( courseId: Long, onNavigateToConversation: (conversationId: Long) -> Unit, onRequestCreatePersonalConversation: () -> Unit, - onRequestAddChannel: () -> Unit + onRequestAddChannel: () -> Unit, + onRequestBrowseChannel: () -> Unit, + canCreateChannel: Boolean ) { ConversationOverviewBody( modifier = modifier, viewModel = koinViewModel { parametersOf(courseId) }, onNavigateToConversation = onNavigateToConversation, onRequestCreatePersonalConversation = onRequestCreatePersonalConversation, - onRequestAddChannel = onRequestAddChannel + onRequestAddChannel = onRequestAddChannel, + onRequestBrowseChannel = onRequestBrowseChannel, + canCreateChannel = canCreateChannel ) } @@ -67,7 +80,9 @@ fun ConversationOverviewBody( viewModel: ConversationOverviewViewModel, onNavigateToConversation: (conversationId: Long) -> Unit, onRequestCreatePersonalConversation: () -> Unit, - onRequestAddChannel: () -> Unit + onRequestAddChannel: () -> Unit, + onRequestBrowseChannel: () -> Unit, + canCreateChannel: Boolean ) { var showCodeOfConduct by rememberSaveable { mutableStateOf(false) } val conversationCollectionsDataState: DataState by viewModel.conversations.collectAsState() @@ -80,76 +95,85 @@ fun ConversationOverviewBody( viewModel.requestReload() } - BasicDataStateUi( - modifier = modifier, - dataState = conversationCollectionsDataState, - loadingText = stringResource(id = R.string.conversation_overview_loading), - failureText = stringResource(id = R.string.conversation_overview_loading_failed), - retryButtonText = stringResource(id = R.string.conversation_overview_loading_try_again), - onClickRetry = viewModel::requestReload - ) { conversationCollection -> - Column( - modifier = Modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - AnimatedVisibility(modifier = Modifier.fillMaxWidth(), visible = !isConnected) { - Box(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier.align(Alignment.Center), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(imageVector = Icons.Default.WifiOff, contentDescription = null) - - Text( - text = stringResource(id = R.string.conversation_overview_not_connected_banner), - style = MaterialTheme.typography.bodyMedium - ) + Box(modifier = Modifier.fillMaxSize()) { + BasicDataStateUi( + modifier = modifier, + dataState = conversationCollectionsDataState, + loadingText = stringResource(id = R.string.conversation_overview_loading), + failureText = stringResource(id = R.string.conversation_overview_loading_failed), + retryButtonText = stringResource(id = R.string.conversation_overview_loading_try_again), + onClickRetry = viewModel::requestReload + ) { conversationCollection -> + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + AnimatedVisibility(modifier = Modifier.fillMaxWidth(), visible = !isConnected) { + Box(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier.align(Alignment.Center), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon(imageVector = Icons.Default.WifiOff, contentDescription = null) + + Text( + text = stringResource(id = R.string.conversation_overview_not_connected_banner), + style = MaterialTheme.typography.bodyMedium + ) + } } } - } - ConversationSearch( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - query = query, - updateQuery = viewModel::onUpdateQuery - ) + ConversationSearch( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + query = query, + updateQuery = viewModel::onUpdateQuery + ) - ConversationList( - modifier = Modifier.fillMaxSize(), - viewModel = viewModel, - conversationCollections = conversationCollection, - onNavigateToConversation = { conversationId -> - viewModel.setConversationMessagesRead(conversationId) - onNavigateToConversation(conversationId) - }, - onToggleMarkAsFavourite = viewModel::markConversationAsFavorite, - onToggleHidden = viewModel::markConversationAsHidden, - onToggleMuted = viewModel::markConversationAsMuted, - onRequestCreatePersonalConversation = onRequestCreatePersonalConversation, - onRequestAddChannel = onRequestAddChannel, - trailingContent = { - item { Divider() } - - item(key = KEY_BUTTON_SHOW_COC) { - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - ) { - OutlinedButton( - modifier = Modifier.align(Alignment.Center), - onClick = { showCodeOfConduct = true } + ConversationList( + modifier = Modifier.fillMaxSize(), + viewModel = viewModel, + conversationCollections = conversationCollection, + onNavigateToConversation = { conversationId -> + viewModel.setConversationMessagesRead(conversationId) + onNavigateToConversation(conversationId) + }, + onToggleMarkAsFavourite = viewModel::markConversationAsFavorite, + onToggleHidden = viewModel::markConversationAsHidden, + onToggleMuted = viewModel::markConversationAsMuted, + onRequestCreatePersonalConversation = onRequestCreatePersonalConversation, + onRequestAddChannel = onRequestAddChannel, + trailingContent = { + item { Divider() } + + item(key = KEY_BUTTON_SHOW_COC) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp) ) { - Text(text = stringResource(id = R.string.conversation_overview_button_show_code_of_conduct)) + OutlinedButton( + modifier = Modifier.align(Alignment.Center), + onClick = { showCodeOfConduct = true } + ) { + Text(text = stringResource(id = R.string.conversation_overview_button_show_code_of_conduct)) + } } } } - } - ) + ) + } } + + ConversationFabMenu( + onCreateChat = onRequestCreatePersonalConversation, + onBrowseChannels = onRequestBrowseChannel, + onCreateChannel = onRequestAddChannel, + canCreateChannel = canCreateChannel + ) } if (showCodeOfConduct) { @@ -168,6 +192,73 @@ fun ConversationOverviewBody( } } +@Composable +fun ConversationFabMenu( + onCreateChat: () -> Unit, + onBrowseChannels: () -> Unit, + onCreateChannel: () -> Unit, + canCreateChannel: Boolean +) { + var expanded by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.BottomEnd + ) { + Box { + FloatingActionButton( + onClick = { expanded = !expanded }, + modifier = Modifier.size(56.dp) + ) { + Icon(imageVector = Icons.Default.AddComment, contentDescription = "Add conversation") + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + offset = DpOffset(x = 0.dp, y = (12).dp) + ) { + DropdownMenuItem( + onClick = { + expanded = false + onCreateChat() + }, + text = { Text(stringResource(id = R.string.create_chat_title)) }, + leadingIcon = { + Icon(imageVector = Icons.Default.ChatBubble, contentDescription = null) + } + ) + DropdownMenuItem( + onClick = { + expanded = false + onBrowseChannels() + }, + text = { Text(stringResource(id = R.string.browse_channels_title)) }, + leadingIcon = { + Icon(imageVector = Icons.Default.Tag, contentDescription = null) + } + ) + if (canCreateChannel) { + DropdownMenuItem( + onClick = { + expanded = false + onCreateChannel() + }, + text = { Text(stringResource(id = R.string.create_channel_title)) }, + leadingIcon = { + Icon(imageVector = Icons.Default.AddComment, contentDescription = null) + } + ) + } + } + } + } +} + + + @Composable private fun ConversationSearch( modifier: Modifier, @@ -181,14 +272,33 @@ private fun ConversationSearch( shape = RoundedCornerShape(10) ) ) { - BasicHintTextField( - modifier = Modifier - .padding(8.dp) - .fillMaxWidth(), - hint = stringResource(id = R.string.conversation_overview_search_hint), - value = query, - onValueChange = updateQuery, - maxLines = 1 - ) + Row(modifier = Modifier.fillMaxWidth()) { + BasicHintTextField( + modifier = Modifier + .weight(1f) + .padding(8.dp), + hint = stringResource(id = R.string.conversation_overview_search_hint), + value = query, + onValueChange = updateQuery, + maxLines = 1 + ) + + if (query.isNotEmpty()) { + IconButton( + onClick = { updateQuery("") }, + modifier = Modifier + .align(Alignment.CenterVertically) + .size(24.dp) + .padding(end = 5.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + } + } } } + 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 035155270..83bf39bb8 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 @@ -183,25 +183,51 @@ class ConversationOverviewViewModel( private val conversationsAsCollections: StateFlow> = combine( updatedConversations, - currentPreferences - ) { conversationsDataState, preferences -> + currentPreferences, + query + ) { conversationsDataState, preferences, query -> conversationsDataState.bind { conversations -> + val isFiltering = query.isNotBlank() + de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ConversationCollections( channels = conversations.filterNotHiddenNorFavourite() - .asCollection(preferences.channelsExpanded), + .filter { !it.filterPredicate("exercise") && !it.filterPredicate("lecture") && !it.filterPredicate("exam") } + .asCollection(isFiltering || preferences.generalsExpanded), + groupChats = conversations.filterNotHiddenNorFavourite() - .asCollection(preferences.groupChatsExpanded), + .asCollection(isFiltering || preferences.groupChatsExpanded), + directChats = conversations.filterNotHiddenNorFavourite() - .asCollection(preferences.personalConversationsExpanded), + .asCollection(isFiltering || preferences.personalConversationsExpanded), + favorites = conversations.filter { it.isFavorite } .asCollection(preferences.favouritesExpanded), + hidden = conversations.filter { it.isHidden } - .asCollection(preferences.hiddenExpanded) + .asCollection(preferences.hiddenExpanded), + + exerciseChannels = conversations.filter { + it is ChannelChat && !it.isFavorite && !it.isHidden && it.filterPredicate("exercise") + }.map { it as ChannelChat } + .asCollection(isFiltering || preferences.exercisesExpanded, showPrefix = false), + + lectureChannels = conversations.filter { + it is ChannelChat && !it.isFavorite && !it.isHidden && it.filterPredicate("lecture") + }.map { it as ChannelChat } + .asCollection(isFiltering || preferences.lecturesExpanded, showPrefix = false), + + examChannels = conversations.filter { + it is ChannelChat && !it.isFavorite && !it.isHidden && it.filterPredicate("exam") + }.map { it as ChannelChat } + .asCollection(isFiltering || preferences.examsExpanded, showPrefix = false) ) } } .stateIn(viewModelScope + coroutineContext, SharingStarted.Eagerly) + + + /** * Holds the latest conversations we could successfully load. */ @@ -359,8 +385,20 @@ class ConversationOverviewViewModel( expandOrCollapseSection { copy(favouritesExpanded = !favouritesExpanded) } } - fun toggleChannelsExpanded() { - expandOrCollapseSection { copy(channelsExpanded = !channelsExpanded) } + fun toggleGeneralsExpanded() { + expandOrCollapseSection { copy(generalsExpanded = !generalsExpanded) } + } + + fun toggleExamsExpanded() { + expandOrCollapseSection { copy(examsExpanded = !examsExpanded) } + } + + fun toggleExercisesExpanded() { + expandOrCollapseSection { copy(exercisesExpanded = !exercisesExpanded) } + } + + fun toggleLecturesExpanded() { + expandOrCollapseSection { copy(lecturesExpanded = !lecturesExpanded) } } fun toggleGroupChatsExpanded() { @@ -391,6 +429,7 @@ class ConversationOverviewViewModel( } private fun List.asCollection( - isExpanded: Boolean - ) = ConversationCollection(this, isExpanded) + isExpanded: Boolean, + showPrefix: Boolean = true + ) = ConversationCollection(this, isExpanded, showPrefix) } diff --git a/feature/metis/manage-conversations/src/main/res/values/browse_channels_strings.xml b/feature/metis/manage-conversations/src/main/res/values/browse_channels_strings.xml index c0a744ce5..333e738dc 100644 --- a/feature/metis/manage-conversations/src/main/res/values/browse_channels_strings.xml +++ b/feature/metis/manage-conversations/src/main/res/values/browse_channels_strings.xml @@ -6,6 +6,8 @@ Loading channels… Something went wrong while loading the channels. Try again + Joined + Join There are no channels in this course you have not already joined. 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 c6b57c801..e11c842a7 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 @@ -12,7 +12,10 @@ Unmute Favorites - Channels + General + Exercises + Lectures + Exams Group Chats Direct Messages Hidden diff --git a/feature/metis/manage-conversations/src/main/res/values/create_channel_strings.xml b/feature/metis/manage-conversations/src/main/res/values/create_channel_strings.xml index e054fc9ad..cd6e1d2c2 100644 --- a/feature/metis/manage-conversations/src/main/res/values/create_channel_strings.xml +++ b/feature/metis/manage-conversations/src/main/res/values/create_channel_strings.xml @@ -1,6 +1,7 @@ Create channel + Create chat A channel is a way to group people together around a project, a topic, or just for fun. You can create as many channels as you want. You will become the first channel moderator. You will not be able to leave the channel. Name @@ -10,12 +11,12 @@ Description (optional) What\'s this channel about? - Private Channel / Public Channel + Private Channel? Every user except instructors will need an invitation to join a private channel. Everybody can join a public channel. Public Private - Announcement Channel + Announcement Channel? Only instructors and channel moderators can create new messages in an announcement channel. Students can only read the messages and answer to them. Announcement Channel Unrestricted Channel diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt index 21fbc2cd2..973363b8f 100644 --- a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/ConversationFacadeUi.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import de.tum.informatics.www1.artemis.native_app.feature.metis.codeofconduct.ui.CodeOfConductFacadeUi +import org.koin.compose.koinInject /** * Displays the conversation ui. If the code of conduct has not yet been accepted, displays a code @@ -22,7 +23,12 @@ fun ConversationFacadeUi( SinglePageConversationBody( modifier = Modifier.fillMaxSize(), courseId = courseId, - initialConfiguration = initialConfiguration + initialConfiguration = initialConfiguration, + accountService = koinInject(), + accountDataService = koinInject(), + courseService = koinInject(), + networkStatusProvider = koinInject(), + serverConfigurationService = koinInject() ) } ) diff --git a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt index 4feabe428..cfe7a811b 100644 --- a/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt +++ b/feature/metis/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/ui/SinglePageConversationBody.kt @@ -4,12 +4,23 @@ import android.os.Parcelable import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import de.tum.informatics.www1.artemis.native_app.core.common.flatMapLatest +import de.tum.informatics.www1.artemis.native_app.core.data.retryOnInternet +import de.tum.informatics.www1.artemis.native_app.core.data.service.network.AccountDataService +import de.tum.informatics.www1.artemis.native_app.core.data.service.network.CourseService +import de.tum.informatics.www1.artemis.native_app.core.datastore.AccountService +import de.tum.informatics.www1.artemis.native_app.core.datastore.ServerConfigurationService +import de.tum.informatics.www1.artemis.native_app.core.datastore.authToken +import de.tum.informatics.www1.artemis.native_app.core.device.NetworkStatusProvider +import de.tum.informatics.www1.artemis.native_app.core.model.account.isAtLeastTutorInCourse import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.ui.ConversationScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.browse_channels.BrowseChannelsScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.create_channel.CreateChannelScreen @@ -19,13 +30,20 @@ import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversati import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.settings.members.ConversationMembersScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.manageconversations.ui.conversation.settings.overview.ConversationSettingsScreen import de.tum.informatics.www1.artemis.native_app.feature.metis.shared.content.StandalonePostId +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @Composable internal fun SinglePageConversationBody( modifier: Modifier, courseId: Long, - initialConfiguration: ConversationConfiguration = NothingOpened + initialConfiguration: ConversationConfiguration = NothingOpened, + accountService: AccountService, + serverConfigurationService: ServerConfigurationService, + courseService: CourseService, + accountDataService: AccountDataService, + networkStatusProvider: NetworkStatusProvider ) { var configuration: ConversationConfiguration by rememberSaveable(initialConfiguration) { mutableStateOf(initialConfiguration) @@ -38,10 +56,36 @@ internal fun SinglePageConversationBody( } } + var canCreateChannel by rememberSaveable { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + + LaunchedEffect(courseId) { + coroutineScope.launch { + val flow = flatMapLatest( + serverConfigurationService.serverUrl, + accountService.authToken + ) { serverUrl, authToken -> + retryOnInternet(networkStatusProvider.currentNetworkStatus) { + courseService.getCourse(courseId, serverUrl, authToken) + .then { courseWithScore -> + accountDataService + .getAccountData(serverUrl, authToken) + .bind { it.isAtLeastTutorInCourse(courseWithScore.course) } + } + }.map { it.orElse(false) } + } + + flow.collect { value -> + canCreateChannel = value + } + } + } + BackHandler(configuration != NothingOpened) { when (val config = configuration) { is ConversationSettings -> configuration = config.prevConfiguration is AddChannelConfiguration -> configuration = config.prevConfiguration + is BrowseChannelConfiguration -> configuration = config.prevConfiguration is CreatePersonalConversation -> configuration = config.prevConfiguration is OpenedConversation -> configuration = if (config.openedThread != null) config.copy(openedThread = null) else NothingOpened @@ -51,7 +95,7 @@ internal fun SinglePageConversationBody( } } - val ConversationOverview: @Composable (Modifier) -> Unit = { m -> + val conversationOverview: @Composable (Modifier) -> Unit = { m -> ConversationOverviewBody( modifier = m.padding(top = 16.dp), courseId = courseId, @@ -60,14 +104,20 @@ internal fun SinglePageConversationBody( configuration = CreatePersonalConversation(configuration) }, onRequestAddChannel = { - configuration = AddChannelConfiguration(false, configuration) - } + if (canCreateChannel) { + configuration = AddChannelConfiguration(configuration) + } + }, + onRequestBrowseChannel = { + configuration = BrowseChannelConfiguration(configuration) + }, + canCreateChannel = canCreateChannel ) } when (val config = configuration) { NothingOpened -> { - ConversationOverview(modifier) + conversationOverview(modifier) } is OpenedConversation -> { @@ -94,28 +144,26 @@ internal fun SinglePageConversationBody( prevConfiguration = config ) }, - conversationsOverview = { mod -> ConversationOverview(mod) } + conversationsOverview = { mod -> conversationOverview(mod) } + ) + } + + is BrowseChannelConfiguration -> { + BrowseChannelsScreen( + modifier = modifier, + courseId = courseId, + onNavigateToConversation = openConversation, + //onNavigateToCreateChannel = {}, + onNavigateBack = { configuration = config.prevConfiguration } ) } is AddChannelConfiguration -> { - if (config.isCreatingChannel) { + if (canCreateChannel) { CreateChannelScreen( modifier = modifier, courseId = courseId, onConversationCreated = openConversation, - onNavigateBack = { - configuration = AddChannelConfiguration(false, config.prevConfiguration) - } - ) - } else { - BrowseChannelsScreen( - modifier = modifier, - courseId = courseId, - onNavigateToConversation = openConversation, - onNavigateToCreateChannel = { - configuration = AddChannelConfiguration(true, config.prevConfiguration) - }, onNavigateBack = { configuration = config.prevConfiguration } ) } @@ -189,6 +237,7 @@ internal fun SinglePageConversationBody( } } + @Parcelize sealed interface ConversationConfiguration : Parcelable @@ -211,7 +260,12 @@ data class NavigateToUserConversation(val username: String) : ConversationConfig @Parcelize private data class AddChannelConfiguration( - val isCreatingChannel: Boolean, + val prevConfiguration: ConversationConfiguration +) : + ConversationConfiguration + +@Parcelize +private data class BrowseChannelConfiguration( val prevConfiguration: ConversationConfiguration ) : ConversationConfiguration From ad81f6c662deb11ec25fb678fe376ca36acd9a05 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Sat, 21 Sep 2024 15:31:59 +0300 Subject: [PATCH 6/9] test updated --- .../manageconversations/overview/BrowseChannelsE2eTest.kt | 1 - .../overview/ConversationOverviewE2eTest.kt | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/overview/BrowseChannelsE2eTest.kt b/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/overview/BrowseChannelsE2eTest.kt index d7b5fd502..a40d62c6d 100644 --- a/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/overview/BrowseChannelsE2eTest.kt +++ b/feature/metis/manage-conversations/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/manageconversations/overview/BrowseChannelsE2eTest.kt @@ -114,7 +114,6 @@ class BrowseChannelsE2eTest : ConversationBaseTest() { modifier = Modifier.fillMaxSize(), viewModel = viewModel, onNavigateToConversation = onNavigateToConversation, - onNavigateToCreateChannel = { }, onNavigateBack = {} ) } 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 2d60a2764..ceab5a47b 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 @@ -395,12 +395,14 @@ class ConversationOverviewE2eTest : ConversationBaseTest() { viewModel = viewModel, onNavigateToConversation = {}, onRequestCreatePersonalConversation = { }, - onRequestAddChannel = {} + onRequestAddChannel = {}, + onRequestBrowseChannel = {}, + canCreateChannel = false ) } composeTestRule.waitUntilAtLeastOneExists( - hasText(context.getString(R.string.conversation_overview_section_channels)), + hasText(context.getString(R.string.conversation_overview_section_general_channels)), DefaultTimeoutMillis ) From bb694dcfec6696cdafe92c9db5a905badaf11052 Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Fri, 27 Sep 2024 13:56:28 +0300 Subject: [PATCH 7/9] minor fix --- .../conversation/overview/ConversationList.kt | 36 ++++++++++--------- 1 file changed, 20 insertions(+), 16 deletions(-) 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 85db1562d..f0e97b241 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 @@ -150,23 +150,27 @@ internal fun ConversationList( ) { Icon(imageVector = Icons.Default.School, contentDescription = null) } } - listWithHeader( - conversationCollections.groupChats, - SECTION_GROUPS_KEY, - KEY_SUFFIX_GROUPS, - R.string.conversation_overview_section_groups, - OnClickAction(onRequestCreatePersonalConversation), - viewModel::toggleGroupChatsExpanded - ) { Icon(imageVector = Icons.Default.Forum, contentDescription = null) } + if (conversationCollections.groupChats.conversations.isNotEmpty()) { + listWithHeader( + conversationCollections.groupChats, + SECTION_GROUPS_KEY, + KEY_SUFFIX_GROUPS, + R.string.conversation_overview_section_groups, + OnClickAction(onRequestCreatePersonalConversation), + viewModel::toggleGroupChatsExpanded + ) { Icon(imageVector = Icons.Default.Forum, contentDescription = null) } + } - listWithHeader( - conversationCollections.directChats, - SECTION_DIRECT_MESSAGES_KEY, - KEY_SUFFIX_PERSONAL, - R.string.conversation_overview_section_direct_messages, - OnClickAction(onRequestCreatePersonalConversation), - viewModel::togglePersonalConversationsExpanded - ) { Icon(imageVector = Icons.Default.Message, contentDescription = null) } + if (conversationCollections.directChats.conversations.isNotEmpty()) { + listWithHeader( + conversationCollections.directChats, + SECTION_DIRECT_MESSAGES_KEY, + KEY_SUFFIX_PERSONAL, + R.string.conversation_overview_section_direct_messages, + OnClickAction(onRequestCreatePersonalConversation), + viewModel::togglePersonalConversationsExpanded + ) { Icon(imageVector = Icons.Default.Message, contentDescription = null) } + } if (conversationCollections.hidden.conversations.isNotEmpty()) { listWithHeader( From 976bc7c85a3bb0daa897dbe8589922a451a092df Mon Sep 17 00:00:00 2001 From: Asli Aykan Date: Tue, 1 Oct 2024 02:50:08 +0300 Subject: [PATCH 8/9] refactor --- .../core/ui/common/BasicHintTextField.kt | 16 ++++++++++++++-- .../exerciseview/home/ExerciseScreenBody.kt | 3 --- .../content/dto/conversation/ChannelChat.kt | 4 +++- .../shared/content/dto/conversation/GroupChat.kt | 7 +++++-- .../content/dto/conversation/OneToOneChat.kt | 4 +++- 5 files changed, 25 insertions(+), 9 deletions(-) diff --git a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/BasicHintTextField.kt b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/BasicHintTextField.kt index 6a8ada4e9..f189deaca 100644 --- a/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/BasicHintTextField.kt +++ b/core/ui/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/core/ui/common/BasicHintTextField.kt @@ -1,6 +1,8 @@ package de.tum.informatics.www1.artemis.native_app.core.ui.common import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.runtime.Composable @@ -11,8 +13,10 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.OffsetMapping import androidx.compose.ui.text.input.TransformedText @@ -27,7 +31,7 @@ fun BasicHintTextField( hintStyle: TextStyle = LocalTextStyle.current ) { var hasFocus by remember { mutableStateOf(false) } - + val keyboardController = LocalSoftwareKeyboardController.current val isValueDisplayed = value.isNotBlank() || (hasFocus && hideHintOnFocus) val currentValue = if (isValueDisplayed) value else hint @@ -48,6 +52,14 @@ fun BasicHintTextField( AnnotatedString(text = currentValue, spanStyle = hintStyle.toSpanStyle()), OffsetMapping.Identity ) - } + }, + keyboardOptions = KeyboardOptions.Default.copy( + imeAction = ImeAction.Done + ), + keyboardActions = KeyboardActions( + onDone = { + keyboardController?.hide() + } + ) ) } \ No newline at end of file diff --git a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenBody.kt b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenBody.kt index 268a0c190..1bd9ba8fd 100644 --- a/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenBody.kt +++ b/feature/exercise-view/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/exerciseview/home/ExerciseScreenBody.kt @@ -5,8 +5,6 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Create import androidx.compose.material.icons.filled.HelpCenter @@ -74,7 +72,6 @@ internal fun ExerciseScreenBody( exerciseOverviewTab( Modifier .fillMaxSize() - .verticalScroll(rememberScrollState()) .padding(horizontal = 8.dp) ) 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 da9397a0e..bc3736712 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 @@ -35,5 +35,7 @@ data class ChannelChat( override fun withUnreadMessagesCount(unreadMessagesCount: Long): Conversation = copy(unreadMessagesCount = unreadMessagesCount) - override fun filterPredicate(query: String): Boolean = query in name + override fun filterPredicate(query: String): Boolean { + return name.contains(query, ignoreCase = true) + } } 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 454b128fd..a1337e044 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 @@ -29,6 +29,9 @@ data class GroupChat( override fun withUnreadMessagesCount(unreadMessagesCount: Long): Conversation = copy(unreadMessagesCount = unreadMessagesCount) - override fun filterPredicate(query: String): Boolean = - if (name != null) query in name else query in humanReadableName + override fun filterPredicate(query: String): Boolean { + return (name!=null && name.contains(query, ignoreCase = true)) || + (humanReadableName.contains(query, ignoreCase = true)) + + } } 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 12a05c1b8..a756463eb 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 @@ -27,5 +27,7 @@ data class OneToOneChat( override fun withUnreadMessagesCount(unreadMessagesCount: Long): Conversation = copy(unreadMessagesCount = unreadMessagesCount) - override fun filterPredicate(query: String): Boolean = query in humanReadableTitle + override fun filterPredicate(query: String): Boolean { + return humanReadableTitle.contains(query, ignoreCase = true) + } } From 7473c77ccdaca334455e7c711c95d26303b206e4 Mon Sep 17 00:00:00 2001 From: Stephan Krusche Date: Fri, 4 Oct 2024 11:35:36 +0200 Subject: [PATCH 9/9] Change to MIT license --- LICENSE | 222 +++-------------------- docker/mysql.yml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 23 insertions(+), 203 deletions(-) diff --git a/LICENSE b/LICENSE index 261eeb9e9..a7504c5ef 100644 --- a/LICENSE +++ b/LICENSE @@ -1,201 +1,21 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +MIT License + +Copyright (c) 2024 TUM Applied Software Engineering + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/docker/mysql.yml b/docker/mysql.yml index dc1118652..72896b51d 100644 --- a/docker/mysql.yml +++ b/docker/mysql.yml @@ -5,7 +5,7 @@ services: mysql: container_name: artemis-mysql - image: docker.io/library/mysql:8.0.33 + image: docker.io/library/mysql:9.0.1 # DO NOT use this default file for production systems! environment: MYSQL_ALLOW_EMPTY_PASSWORD: true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 84a0b92f9..1e2fbf0d4 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists