diff --git a/CHANGELOG.md b/CHANGELOG.md index acacff44c01..4d652bfbecb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,10 +21,6 @@ ### ❌ Removed -## stream-chat-android-core -### ✅ Added -- Expose `UserId` alias that Represents a user id. [#5616](https://github.com/GetStream/stream-chat-android/pull/5616) - ## stream-chat-android-offline ### 🐞 Fixed @@ -53,7 +49,6 @@ ### ⬆️ Improved ### ✅ Added -- Added `DateFormatter::formatRelativeDate` method to format a relative date. [#5587](https://github.com/GetStream/stream-chat-android/pull/5587) ### ⚠️ Changed @@ -61,10 +56,8 @@ ## stream-chat-android-ui-components ### 🐞 Fixed -- Fix poll attachment picker not hidden if disabled in the dashboard. [#5562](https://github.com/GetStream/stream-chat-android/pull/5562) ### ⬆️ Improved -- `DateDividerViewHolder` now uses `DateFormatter::formatRelativeDate` to format the date. [#5587](https://github.com/GetStream/stream-chat-android/pull/5587) ### ✅ Added @@ -72,6 +65,45 @@ ### ❌ Removed +## stream-chat-android-compose +### 🐞 Fixed + +### ⬆️ Improved +- Autofocus the input fields in the poll creation screen. [#5629](https://github.com/GetStream/stream-chat-android/pull/5629) + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-markdown-transformer +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +# February 07th, 2025 - 6.11.0 +## stream-chat-android-core +### ✅ Added +- Expose `UserId` alias that Represents a user id. [#5616](https://github.com/GetStream/stream-chat-android/pull/5616) + +## stream-chat-android-ui-common +### ✅ Added +- Added `DateFormatter::formatRelativeDate` method to format a relative date. [#5587](https://github.com/GetStream/stream-chat-android/pull/5587) + +## stream-chat-android-ui-components +### 🐞 Fixed +- Fix poll attachment picker not hidden if disabled in the dashboard. [#5562](https://github.com/GetStream/stream-chat-android/pull/5562) + +### ⬆️ Improved +- `DateDividerViewHolder` now uses `DateFormatter::formatRelativeDate` to format the date. [#5587](https://github.com/GetStream/stream-chat-android/pull/5587) + ## stream-chat-android-compose ### 🐞 Fixed - Fix poll attachment picker not hidden if disabled in the dashboard. [#5562](https://github.com/GetStream/stream-chat-android/pull/5562) @@ -103,19 +135,6 @@ ### ⚠️ Changed - 🚨 Breaking change: Change `@Composable public fun MessageContainer` function to `@Composable public fun LazyItemScope.MessageContainer`. [#5593](https://github.com/GetStream/stream-chat-android/pull/5593) -### ❌ Removed - -## stream-chat-android-markdown-transformer -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - # January 23th, 2025 - 6.10.0 ## Common changes for all artifacts ### ⬆️ Improved diff --git a/Gemfile.lock b/Gemfile.lock index b9092ab280c..4d8a89d97a3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -186,7 +186,7 @@ GEM puma (6.4.3) nio4r (~> 2.0) racc (1.8.1) - rack (3.1.8) + rack (3.1.10) rack-protection (4.1.0) base64 (>= 0.1.0) logger (>= 1.6.0) diff --git a/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt b/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt index ca648c03ebb..d7f06bc304b 100644 --- a/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt +++ b/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt @@ -6,7 +6,7 @@ object Configuration { const val sampleTargetSdk = 34 const val minSdk = 21 const val majorVersion = 6 - const val minorVersion = 10 + const val minorVersion = 11 const val patchVersion = 0 const val versionName = "$majorVersion.$minorVersion.$patchVersion" const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" diff --git a/metrics/size.json b/metrics/size.json index 32ac1351bc6..b2fa9a45c73 100644 --- a/metrics/size.json +++ b/metrics/size.json @@ -3,7 +3,7 @@ "stream-chat-android-client": 15308, "stream-chat-android-offline": 15620, "stream-chat-android-ui-components": 20992, - "stream-chat-android-compose": 22388 + "stream-chat-android-compose": 22392 }, "release": { "stream-chat-android-client": 3244, diff --git a/stream-chat-android-ai-assistant/detekt-baseline.xml b/stream-chat-android-ai-assistant/detekt-baseline.xml index 251e2b443fc..97a8cda418e 100644 --- a/stream-chat-android-ai-assistant/detekt-baseline.xml +++ b/stream-chat-android-ai-assistant/detekt-baseline.xml @@ -2,27 +2,14 @@ - FunctionNaming:AiMessageText.kt$@Composable private fun ClickableText( text: AnnotatedString, modifier: Modifier = Modifier, style: TextStyle = TextStyle.Default, maxLines: Int = Int.MAX_VALUE, onLongPress: () -> Unit, onClick: (Int) -> Unit, ) - FunctionNaming:AiMessageText.kt$@Composable public fun AiMessageText( message: Message, currentUser: User?, typingState: TypingState, modifier: Modifier = Modifier, onAnimationState: (Boolean) -> Unit, onLongItemClick: (Message) -> Unit, onLinkClick: ((Message, String) -> Unit)? = null, ) - FunctionNaming:AiMessagesScreen.kt$@Composable internal fun BoxScope.DefaultAiStartButton( isAiStarted: Boolean, onStartAiAssistant: () -> Unit, onStopAiAssistant: () -> Unit, ) - FunctionNaming:AiMessagesScreen.kt$@Composable internal fun DefaultBottomBarContent( viewModelFactory: MessagesViewModelFactory, onComposerLinkPreviewClick: ((LinkPreview) -> Unit)? = null, skipPushNotification: Boolean = false, skipEnrichUrl: Boolean = false, ) - FunctionNaming:AiMessagesScreen.kt$@Composable internal fun DefaultTopBarContent( viewModelFactory: MessagesViewModelFactory, backAction: BackAction, onHeaderTitleClick: (channel: Channel) -> Unit, onChannelAvatarClick: () -> Unit, ) - FunctionNaming:AiMessagesScreen.kt$@Composable public fun AiMessagesScreen( viewModelFactory: MessagesViewModelFactory, isAiStarted: Boolean, onStartAiAssistant: () -> Unit, onStopAiAssistant: () -> Unit, showHeader: Boolean = true, typingState: TypingState, reactionSorting: ReactionSorting = ReactionSortingByFirstReactionAt, onBackPressed: () -> Unit = {}, onComposerLinkPreviewClick: ((LinkPreview) -> Unit)? = null, onHeaderTitleClick: (channel: Channel) -> Unit = {}, onChannelAvatarClick: () -> Unit = {}, onMessageLinkClick: ((Message, String) -> Unit)? = null, onUserAvatarClick: (User) -> Unit = {}, skipPushNotification: Boolean = false, skipEnrichUrl: Boolean = false, threadMessagesStart: ThreadMessagesStart = ThreadMessagesStart.BOTTOM, aiStartButton: @Composable BoxScope.() -> Unit = { DefaultAiStartButton( isAiStarted = isAiStarted, onStartAiAssistant = onStartAiAssistant, onStopAiAssistant = onStopAiAssistant, ) }, topBarContent: @Composable (BackAction) -> Unit = { DefaultTopBarContent( viewModelFactory = viewModelFactory, backAction = it, onHeaderTitleClick = onHeaderTitleClick, onChannelAvatarClick = onChannelAvatarClick, ) }, bottomBarContent: @Composable (isAnimating: Boolean) -> Unit = { DefaultBottomBarContent( viewModelFactory = viewModelFactory, onComposerLinkPreviewClick = onComposerLinkPreviewClick, skipPushNotification = skipPushNotification, skipEnrichUrl = skipEnrichUrl, ) }, ) - FunctionNaming:AiRegularMessageContent.kt$@Composable internal fun DefaultMessageTextContent( message: Message, currentUser: User?, typingState: TypingState, onAnimationState: (Boolean) -> Unit, onLongItemClick: (Message) -> Unit, onLinkClick: ((Message, String) -> Unit)? = null, ) - FunctionNaming:AiRegularMessageContent.kt$@Composable public fun AiRegularMessageContent( messageItem: MessageItemState, modifier: Modifier = Modifier, onLongItemClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, onAnimationState: (Boolean) -> Unit, onLinkClick: ((Message, String) -> Unit)? = null, typingState: TypingState, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) - FunctionNaming:AiTypingIndicator.kt$@Composable private fun SingleDot( scale: Float, shimmerInstance: Shimmer, ) - FunctionNaming:AiTypingIndicator.kt$@Composable public fun AiTypingIndicator( modifier: Modifier = Modifier, text: String, textStyle: TextStyle = TextStyle( fontSize = 16.sp, color = ChatTheme.colors.textHighEmphasis, ), ) LongMethod:AiMessageContentFactory.kt$AiMessageContentFactory$@Composable override fun MessageFooterContent(messageItem: MessageItemState) LongMethod:AiMessageText.kt$@Composable public fun AiMessageText( message: Message, currentUser: User?, typingState: TypingState, modifier: Modifier = Modifier, onAnimationState: (Boolean) -> Unit, onLongItemClick: (Message) -> Unit, onLinkClick: ((Message, String) -> Unit)? = null, ) - LongMethod:AiMessagesScreen.kt$@Composable public fun AiMessagesScreen( viewModelFactory: MessagesViewModelFactory, isAiStarted: Boolean, onStartAiAssistant: () -> Unit, onStopAiAssistant: () -> Unit, showHeader: Boolean = true, typingState: TypingState, reactionSorting: ReactionSorting = ReactionSortingByFirstReactionAt, onBackPressed: () -> Unit = {}, onComposerLinkPreviewClick: ((LinkPreview) -> Unit)? = null, onHeaderTitleClick: (channel: Channel) -> Unit = {}, onChannelAvatarClick: () -> Unit = {}, onMessageLinkClick: ((Message, String) -> Unit)? = null, onUserAvatarClick: (User) -> Unit = {}, skipPushNotification: Boolean = false, skipEnrichUrl: Boolean = false, threadMessagesStart: ThreadMessagesStart = ThreadMessagesStart.BOTTOM, aiStartButton: @Composable BoxScope.() -> Unit = { DefaultAiStartButton( isAiStarted = isAiStarted, onStartAiAssistant = onStartAiAssistant, onStopAiAssistant = onStopAiAssistant, ) }, topBarContent: @Composable (BackAction) -> Unit = { DefaultTopBarContent( viewModelFactory = viewModelFactory, backAction = it, onHeaderTitleClick = onHeaderTitleClick, onChannelAvatarClick = onChannelAvatarClick, ) }, bottomBarContent: @Composable (isAnimating: Boolean) -> Unit = { DefaultBottomBarContent( viewModelFactory = viewModelFactory, onComposerLinkPreviewClick = onComposerLinkPreviewClick, skipPushNotification = skipPushNotification, skipEnrichUrl = skipEnrichUrl, ) }, ) LongMethod:AiRegularMessageContent.kt$@Composable public fun AiRegularMessageContent( messageItem: MessageItemState, modifier: Modifier = Modifier, onLongItemClick: (Message) -> Unit = {}, onGiphyActionClick: (GiphyAction) -> Unit = {}, onQuotedMessageClick: (Message) -> Unit = {}, onAnimationState: (Boolean) -> Unit, onLinkClick: ((Message, String) -> Unit)? = null, typingState: TypingState, onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {}, ) MagicNumber:AiMessageText.kt$10 MagicNumber:AiMessagesScreen.kt$0.75f MagicNumber:AiMessagesScreen.kt$300f MagicNumber:AiTypingIndicator.kt$0.55f MagicNumber:AiTypingIndicator.kt$4 - TopLevelPropertyNaming:AiMessageText.kt$/** * The tag used to annotate URLs in the message text. */ internal const val AnnotationTagUrl: AnnotationTag = "URL" - TopLevelPropertyNaming:AiMessageText.kt$/** * The tag used to annotate emails in the message text. */ internal const val AnnotationTagEmail: AnnotationTag = "EMAIL" TopLevelPropertyNaming:AiTypingIndicator.kt$private const val delayUnit = 200 diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 971145c9b04..c7abe4f9d31 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1774,18 +1774,20 @@ public final class io/getstream/chat/android/compose/ui/messages/attachments/pol public final class io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchInput { public static final field $stable I - public synthetic fun (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;IILkotlin/jvm/internal/DefaultConstructorMarker;)V - public synthetic fun (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;IILkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/Object; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/Object; - public final fun component4-PjHm6EE ()I - public final fun copy-YyDlPXQ (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;I)Lio/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchInput; - public static synthetic fun copy-YyDlPXQ$default (Lio/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchInput;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;IILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchInput; + public final fun component4 ()Ljava/lang/Object; + public final fun component5-PjHm6EE ()I + public final fun copy-l6dddJE (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;I)Lio/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchInput; + public static synthetic fun copy-l6dddJE$default (Lio/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchInput;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/Object;Ljava/lang/Object;IILjava/lang/Object;)Lio/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchInput; public fun equals (Ljava/lang/Object;)Z public final fun getDescription ()Ljava/lang/String; public final fun getKeyboardType-PjHm6EE ()I public final fun getMaxValue ()Ljava/lang/Object; + public final fun getMinValue ()Ljava/lang/Object; public final fun getValue ()Ljava/lang/Object; public fun hashCode ()I public final fun setValue (Ljava/lang/Object;)V diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollOptionInput.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollOptionInput.kt index 9d237e29880..b802f5e0c93 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollOptionInput.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollOptionInput.kt @@ -30,9 +30,13 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.stringResource @@ -69,6 +73,7 @@ import io.getstream.chat.android.compose.ui.util.buildAnnotatedMessageText * @param keyboardOptions The [KeyboardOptions] to be applied to the input. * @param decorationBox Composable function that represents the input field decoration as it's filled with content. */ +@Suppress("LongMethod") @Composable public fun PollOptionInput( value: String, @@ -86,6 +91,7 @@ public fun PollOptionInput( val typography = ChatTheme.typography val colors = ChatTheme.colors val textColor = ChatTheme.colors.textHighEmphasis + val focusRequester = remember { FocusRequester() } Box(modifier = modifier.height(ChatTheme.dimens.pollOptionInputHeight)) { BasicTextField( @@ -94,6 +100,7 @@ public fun PollOptionInput( .clip(shape = shape) .background(ChatTheme.colors.inputBackground) .padding(innerPadding) + .focusRequester(focusRequester) .semantics { contentDescription = description }, value = value, onValueChange = { @@ -139,6 +146,11 @@ public fun PollOptionInput( ) } } + + // Request focus initially when the Input is first drawn. + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } } @Preview diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/factory/AttachmentsPickerPollTabFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/factory/AttachmentsPickerPollTabFactory.kt index c096714e718..598a81b95ba 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/factory/AttachmentsPickerPollTabFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/factory/AttachmentsPickerPollTabFactory.kt @@ -118,7 +118,7 @@ public class AttachmentsPickerPollTabFactory : AttachmentsPickerTabFactory { val pollSwitchItemFactory = ChatTheme.pollSwitchitemFactory var optionItemList by remember { mutableStateOf(emptyList()) } var switchItemList: List by remember { mutableStateOf(pollSwitchItemFactory.providePollSwitchItemList()) } - var hasErrorOnOptions by remember { mutableStateOf(false) } + var hasError by remember { mutableStateOf(false) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { @@ -139,7 +139,7 @@ public class AttachmentsPickerPollTabFactory : AttachmentsPickerTabFactory { .background(ChatTheme.colors.appBackground), ) { val (question, onQuestionChanged) = rememberSaveable { mutableStateOf("") } - val isEnabled = question.isNotBlank() && optionItemList.any { it.title.isNotBlank() } && !hasErrorOnOptions + val isEnabled = question.isNotBlank() && optionItemList.any { it.title.isNotBlank() } && !hasError val hasChanges = question.isNotBlank() || optionItemList.any { it.title.isNotBlank() } var isShowingDiscardDialog by remember { mutableStateOf(false) } @@ -175,7 +175,7 @@ public class AttachmentsPickerPollTabFactory : AttachmentsPickerTabFactory { onQuestionsChanged = { optionItemList = it switchItemList = updateMaxVotesAllowedSwitch(optionItemList, switchItemList) - hasErrorOnOptions = it.fastAny { item -> item.pollOptionError != null } + hasError = hasError(optionItemList, switchItemList) }, ) @@ -185,7 +185,7 @@ public class AttachmentsPickerPollTabFactory : AttachmentsPickerTabFactory { pollSwitchItems = switchItemList, onSwitchesChanged = { switchItemList = it - hasErrorOnOptions = it.fastAny { item -> item.pollOptionError != null } + hasError = hasError(optionItemList, switchItemList) }, ) @@ -202,6 +202,28 @@ public class AttachmentsPickerPollTabFactory : AttachmentsPickerTabFactory { } } +/** + * Checks if there are any errors in the 'options' list, or any errors or missing fields in the 'switches' list. + */ +private fun hasError( + options: List, + switches: List, +): Boolean { + // Check errors in options + val hasErrorInOptions = options.fastAny { item -> + item.pollOptionError != null + } + // Check errors or missing fields in switches + val hasErrorInSwitches = switches.fastAny { item -> + val hasError = item.pollOptionError != null + val isMissingMandatoryInput = item.enabled && + item.pollSwitchInput != null && + item.pollSwitchInput.value.toString().isEmpty() + hasError || isMissingMandatoryInput + } + return hasErrorInOptions || hasErrorInSwitches +} + /** * Updates the max votes allowed switch based on the number of options available. * diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchItem.kt index 7bcf9fa7d0c..c479df0d2c0 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchItem.kt @@ -26,6 +26,7 @@ import java.util.UUID * @property title The title of this poll item. * @property enabled Indicates if this switch is enabled or not. * @property key The key that identifies this poll item. + * @property pollSwitchInput Optional input field to be presented when the switch is enabled. * @property pollOptionError Indicates this option has an error. */ @Immutable @@ -42,12 +43,14 @@ public data class PollSwitchItem( * * @property value The default value of the switch. * @property description The description of the input in the switch (shown as hint/contentDescription). - * @property maxValue The maximum vale of the switch. Normally, you can use the limit of the decimal format of the [value]. + * @property minValue The minimum value of the switch. Normally, you can use the limit of the decimal format of the [value]. + * @property maxValue The maximum value of the switch. Normally, you can use the limit of the decimal format of the [value]. * @property keyboardType The type of the input of the switch and decide the keyboard type of the input. */ public data class PollSwitchInput( public var value: Any, public val description: String = "", + public val minValue: Any? = null, public val maxValue: Any? = null, public val keyboardType: KeyboardType = KeyboardType.Text, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchList.kt index cb8c8dba00a..da4f26e335e 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/attachments/poll/PollSwitchList.kt @@ -68,7 +68,6 @@ import io.getstream.chat.android.compose.ui.theme.ChatTheme * @param onSwitchesChanged A lambda that will be executed when a switch on the list is changed. * @param itemHeightSize The height size of the question item. * @param itemInnerPadding The inner padding size of the question item. - * It provides the index information [from] and [to] as a receiver, so you must swap the item of the [questions] list. */ @Composable public fun PollSwitchList( @@ -189,24 +188,34 @@ public fun PollSwitchList( if (switchInput.keyboardType == KeyboardType.Number || switchInput.keyboardType == KeyboardType.Decimal ) { - val newInt = if (newValue.isBlank()) 0 else newValue.toInt() - val maxInt = switchInput.maxValue?.toString()?.toInt() ?: 0 - - if (newInt > maxInt) { - this[index] = item.copy( - pollSwitchInput = item.pollSwitchInput.copy(value = newValue), - pollOptionError = PollOptionNumberExceed( - context.getString( - R.string.stream_compose_poll_option_error_exceed, - maxInt, - ), - ), - ) - } else { + if (newValue.isBlank()) { + // If newValue is empty, don't validate this[index] = item.copy( pollSwitchInput = item.pollSwitchInput.copy(value = newValue), pollOptionError = null, ) + } else { + // Validate min/max range + val min = switchInput.minValue?.toString()?.toIntOrNull() ?: 0 + val max = switchInput.maxValue?.toString()?.toIntOrNull() ?: 0 + val value = newValue.toInt() // assume it is always numeric + if (value < min || value > max) { + this[index] = item.copy( + pollSwitchInput = item.pollSwitchInput.copy(value = newValue), + pollOptionError = PollOptionNumberExceed( + context.getString( + R.string.stream_compose_poll_option_error_exceed, + min, + max, + ), + ), + ) + } else { + this[index] = item.copy( + pollSwitchInput = item.pollSwitchInput.copy(value = newValue), + pollOptionError = null, + ) + } } } else { this[index] = item.copy( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/PollSwitchItemFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/PollSwitchItemFactory.kt index fb3420da3b4..767ed19111f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/PollSwitchItemFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/PollSwitchItemFactory.kt @@ -61,8 +61,9 @@ public class DefaultPollSwitchItemFactory( PollSwitchItem( title = context.getString(R.string.stream_compose_poll_option_switch_multiple_answers), pollSwitchInput = PollSwitchInput( - value = 0, + value = "", description = context.getString(R.string.stream_compose_poll_option_max_number_of_answers_hint), + minValue = 1, maxValue = 2, keyboardType = KeyboardType.Decimal, ), diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index 275ad9eeac6..64b847a90a9 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -209,7 +209,7 @@ Add an option Add an option This is already an option - Type a number under %d + Type a number between %d and %d Discard poll Are you sure want to discard your poll? Keep Editing diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/poll/PollOptionInputTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/poll/PollOptionInputTest.kt new file mode 100644 index 00000000000..0e9909954f5 --- /dev/null +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/components/poll/PollOptionInputTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * 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. + */ + +package io.getstream.chat.android.compose.ui.components.poll + +import app.cash.paparazzi.DeviceConfig +import app.cash.paparazzi.Paparazzi +import io.getstream.chat.android.compose.ui.SnapshotTest +import org.junit.Rule +import org.junit.Test + +internal class PollOptionInputTest : SnapshotTest { + + @get:Rule + override val paparazzi: Paparazzi = Paparazzi(deviceConfig = DeviceConfig.PIXEL_2) + + @Test + fun `empty input`() { + snapshotWithDarkMode { + PollOptionInput( + value = "", + onValueChange = {}, + description = "Description", + decorationBox = { innerTextField -> innerTextField.invoke() }, + ) + } + } + + @Test + fun `with input`() { + snapshotWithDarkMode { + PollOptionInput( + value = "Entered text", + onValueChange = {}, + description = "Description", + decorationBox = { innerTextField -> innerTextField.invoke() }, + ) + } + } +} diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/DefaultPollSwitchItemFactoryTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/DefaultPollSwitchItemFactoryTest.kt index eae51fa94d2..941eff0665b 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/DefaultPollSwitchItemFactoryTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/util/DefaultPollSwitchItemFactoryTest.kt @@ -51,7 +51,8 @@ internal class DefaultPollSwitchItemFactoryTest { Assertions.assertEquals("Multiple answers", items[0].title) Assertions.assertEquals("maxVotesAllowed", items[0].key) Assertions.assertFalse(items[0].enabled) - Assertions.assertEquals(0, items[0].pollSwitchInput?.value) + Assertions.assertEquals("", items[0].pollSwitchInput?.value) + Assertions.assertEquals(1, items[0].pollSwitchInput?.minValue) Assertions.assertEquals(2, items[0].pollSwitchInput?.maxValue) Assertions.assertEquals("Max number of answers", items[0].pollSwitchInput?.description) Assertions.assertEquals(KeyboardType.Decimal, items[0].pollSwitchInput?.keyboardType) diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollOptionInputTest_empty_input.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollOptionInputTest_empty_input.png new file mode 100644 index 00000000000..27d01576761 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollOptionInputTest_empty_input.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollOptionInputTest_with_input.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollOptionInputTest_with_input.png new file mode 100644 index 00000000000..ee392b33c39 Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.components.poll_PollOptionInputTest_with_input.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot_AttachmentsPickerPollTabFactory_content_dark_mode_composable.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot_AttachmentsPickerPollTabFactory_content_dark_mode_composable.png index 7f8ed6c85a2..a4fe9cfb78d 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot_AttachmentsPickerPollTabFactory_content_dark_mode_composable.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot_AttachmentsPickerPollTabFactory_content_dark_mode_composable.png differ diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot_AttachmentsPickerPollTabFactory_content_light_mode_composable.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot_AttachmentsPickerPollTabFactory_content_light_mode_composable.png index bec928454b5..fc5d337027e 100644 Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot_AttachmentsPickerPollTabFactory_content_light_mode_composable.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.poll_PollUITest_snapshot_AttachmentsPickerPollTabFactory_content_light_mode_composable.png differ diff --git a/stream-chat-android-ui-utils/detekt-baseline.xml b/stream-chat-android-ui-utils/detekt-baseline.xml index dba38f7b560..dcd115ea62b 100644 --- a/stream-chat-android-ui-utils/detekt-baseline.xml +++ b/stream-chat-android-ui-utils/detekt-baseline.xml @@ -3,6 +3,5 @@ LongMethod:ChannelKtTest.kt$ChannelKtTest.Companion$@JvmStatic fun arguments() - LongParameterList:Channel.kt$( context: Context, currentUser: User?, @StringRes userOnlineResId: Int, @StringRes userLastSeenJustNowResId: Int, @StringRes userLastSeenResId: Int, @PluralsRes memberCountResId: Int, @StringRes memberCountWithOnlineResId: Int, )