From 075fc63ecece0629c5623c3efe2957e2cd478cde Mon Sep 17 00:00:00 2001 From: Martin Felber <45291671+FelberMartin@users.noreply.github.com> Date: Fri, 15 Nov 2024 13:42:47 +0100 Subject: [PATCH] `Communication`: Improve auto completion for tagging users and channels (#76) --- .../artemis/native_app/android/ui/Color.kt | 2 +- .../conversation/ui/ConversationViewModel.kt | 4 +- .../ui/reply/ReplyAutoCompletePopup.kt | 24 ++++++---- .../conversation/ui/reply/ReplyTextField.kt | 48 ++++++++++++++----- .../ui/reply/ReplyTextFieldUiTest.kt | 5 +- gradle/libs.versions.toml | 2 +- 6 files changed, 60 insertions(+), 25 deletions(-) diff --git a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/Color.kt b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/Color.kt index 1ffee10f2..ecb818267 100644 --- a/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/Color.kt +++ b/app/src/main/java/de/tum/informatics/www1/artemis/native_app/android/ui/Color.kt @@ -61,7 +61,7 @@ val md_theme_dark_inverseSurface = Color(0xFFE2E2E5) val md_theme_dark_inversePrimary = Color(0xFF006398) val md_theme_dark_shadow = Color(0xFF000000) val md_theme_dark_surfaceTint = Color(0xFF93CCFF) -val md_theme_dark_outlineVariant = Color(0xFF42474E) +val md_theme_dark_outlineVariant = Color(0xFF55515B) val md_theme_dark_scrim = Color(0xFF000000) diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationViewModel.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationViewModel.kt index 576f64db4..f6c320231 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationViewModel.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/ConversationViewModel.kt @@ -488,7 +488,7 @@ internal open class ConversationViewModel( val exerciseAutoCompleteItems = course .exercises - .filter { query in it.title.orEmpty() } + .filter { it.title.orEmpty().contains(query, ignoreCase = true) } .mapNotNull { exercise -> val exerciseTag = when (exercise) { is FileUploadExercise -> "file-upload" @@ -538,7 +538,7 @@ internal open class ConversationViewModel( conversationsDataState.bind { conversations -> val conversationAutoCompleteItems = conversations .filterIsInstance() - .filter { query in it.name } + .filter { it.name.contains(query, ignoreCase = true) } .map { channel -> AutoCompleteHint( hint = channel.name, diff --git a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyAutoCompletePopup.kt b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyAutoCompletePopup.kt index 7e34a609f..d21cb1a08 100644 --- a/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyAutoCompletePopup.kt +++ b/feature/metis/conversation/src/main/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyAutoCompletePopup.kt @@ -11,30 +11,36 @@ import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Divider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.min import androidx.compose.ui.util.fastForEachIndexed import androidx.compose.ui.window.Popup import androidx.compose.ui.window.PopupPositionProvider import androidx.compose.ui.window.PopupProperties import de.tum.informatics.www1.artemis.native_app.feature.metis.conversation.R +const val TEST_TAG_REPLY_AUTO_COMPLETE_POPUP_LIST = "TEST_TAG_REPLY_AUTO_COMPLETE_POPUP_LIST" + private val HintHorizontalPadding = 16.dp +private val PopupVerticalPadding = 8.dp +private val AutoCompletionDialogMaxHeight = 270.dp @Composable internal fun ReplyAutoCompletePopup( popupPositionProvider: PopupPositionProvider, targetWidth: Dp, - maxHeight: Dp, + maxHeightFromScreen: Dp, autoCompleteCategories: List, performAutoComplete: (replacement: String) -> Unit, onDismissRequest: () -> Unit @@ -44,9 +50,11 @@ internal fun ReplyAutoCompletePopup( properties = PopupProperties(dismissOnClickOutside = true), onDismissRequest = onDismissRequest ) { + val maxHeight = min(maxHeightFromScreen, AutoCompletionDialogMaxHeight) ReplyAutoCompletePopupBody( modifier = Modifier - .heightIn(max = maxHeight) + .padding(vertical = PopupVerticalPadding) + .heightIn(max = maxHeight - PopupVerticalPadding * 2) .width(targetWidth), autoCompleteCategories = autoCompleteCategories, performAutoComplete = performAutoComplete @@ -62,12 +70,12 @@ private fun ReplyAutoCompletePopupBody( ) { LazyColumn( modifier = modifier - .background( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(topStart = 45f, topEnd = 45f) - ) - .padding(top = 8.dp) + .clip(MaterialTheme.shapes.large) + .background(color = MaterialTheme.colorScheme.surfaceVariant) + .padding(8.dp) + .testTag(TEST_TAG_REPLY_AUTO_COMPLETE_POPUP_LIST) ) { + autoCompleteCategories.forEachIndexed { categoryIndex, category -> item { AutoCompleteCategoryComposable( 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 54be4f05c..8f75c0eda 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,15 +2,37 @@ 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.* -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.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.* -import androidx.compose.runtime.* +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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester @@ -58,7 +80,7 @@ internal fun ReplyTextField( Surface( modifier = modifier.defaultMinSize(minHeight = 48.dp), color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(5) + shape = MaterialTheme.shapes.large ) { Box( modifier = Modifier @@ -173,14 +195,16 @@ private fun CreateReplyUi( val autoCompleteHints = manageAutoCompleteHints(currentTextFieldValue) var textFieldWidth by remember { mutableIntStateOf(0) } - var popupMaxHeight by remember { mutableStateOf(0) } + var popupMaxHeight by remember { mutableIntStateOf(0) } - if (autoCompleteHints.orEmpty().flatMap { it.items } - .isNotEmpty() && mayShowAutoCompletePopup) { + val showAutoCompletePopup = mayShowAutoCompletePopup + && autoCompleteHints.orEmpty().flatMap { it.items }.isNotEmpty() + + if (showAutoCompletePopup) { ReplyAutoCompletePopup( autoCompleteCategories = autoCompleteHints.orEmpty(), targetWidth = with(LocalDensity.current) { textFieldWidth.toDp() }, - maxHeight = with(LocalDensity.current) { popupMaxHeight.toDp() }, + maxHeightFromScreen = with(LocalDensity.current) { popupMaxHeight.toDp() }, popupPositionProvider = ReplyAutoCompletePopupPositionProvider, performAutoComplete = { replacement -> replyMode.onUpdate( @@ -201,11 +225,11 @@ private fun CreateReplyUi( 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() + val textFieldRootTopLeft = coordinates.localToRoot(Offset.Zero) + popupMaxHeight = textFieldRootTopLeft.y.toInt() } + .padding(8.dp) .testTag(TEST_TAG_REPLY_TEXT_FIELD), textFieldValue = currentTextFieldValue, onTextChanged = replyMode::onUpdate, diff --git a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextFieldUiTest.kt b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextFieldUiTest.kt index 93c4a6edc..f723ea766 100644 --- a/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextFieldUiTest.kt +++ b/feature/metis/conversation/src/test/kotlin/de/tum/informatics/www1/artemis/native_app/feature/metis/conversation/ui/reply/ReplyTextFieldUiTest.kt @@ -10,9 +10,10 @@ import androidx.compose.ui.test.junit4.ComposeContentTestRule import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performTextInput import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollToIndex import androidx.compose.ui.test.performTextClearance +import androidx.compose.ui.test.performTextInput import androidx.compose.ui.text.input.TextFieldValue import androidx.test.ext.junit.runners.AndroidJUnit4 import de.tum.informatics.www1.artemis.native_app.core.data.DataState @@ -140,7 +141,9 @@ class ReplyTextFieldUiTest { private fun ComposeContentTestRule.assertAllAutoCompletionHintsShown() { onNodeWithText("User1").assertExists() + onNodeWithTag(TEST_TAG_REPLY_AUTO_COMPLETE_POPUP_LIST).performScrollToIndex(1) onNodeWithText("User2").assertExists() + onNodeWithTag(TEST_TAG_REPLY_AUTO_COMPLETE_POPUP_LIST).performScrollToIndex(2) onNodeWithText("User3").assertExists() } } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c832b0626..1b8007277 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] compileSdk = "35" targetSdk = "34" -minSdk = "31" +minSdk = "30" buildToolsVersion = "35.0.0" accompanist = "0.30.1"