From 111bb56657b9435bc65b5b964eaef3c8570421ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Mion?= Date: Fri, 12 Jun 2026 10:10:52 +0100 Subject: [PATCH 1/3] compose: Disable composer during slow mode While slow mode is active (coolDownTime > 0) the whole composer is locked: the input field and attachment button are disabled, the composer and attachment-button borders switch to the disabled colour, and the input placeholder shows the remaining seconds. It re-enables automatically when the timer reaches zero. Add the stream_ui_message_composer_placeholder_slow_mode string with translations, plus slow-mode snapshots for the full composer and the input. --- .../api/stream-chat-android-compose.api | 1 + .../ui/components/composer/MessageInput.kt | 10 ++++- .../ui/messages/composer/MessageComposer.kt | 18 ++++++++ .../MessageComposerInputCenterContent.kt | 19 ++++++--- .../internal/MessageComposerLeadingContent.kt | 16 +++++-- .../ui/messages/MessageComposerTest.kt | 8 ++++ .../composer/MessageComposerScreenTest.kt | 40 ++++++++++++++++++ ...ges_MessageComposerInputTest_slow_mode.png | Bin 16169 -> 15416 bytes ...messages_MessageComposerTest_slow_mode.png | Bin 0 -> 17494 bytes .../src/main/res/values-es/strings.xml | 1 + .../src/main/res/values-fr/strings.xml | 1 + .../src/main/res/values-hi/strings.xml | 1 + .../src/main/res/values-in/strings.xml | 1 + .../src/main/res/values-it/strings.xml | 1 + .../src/main/res/values-ja/strings.xml | 1 + .../src/main/res/values-ko/strings.xml | 1 + .../src/main/res/values/strings.xml | 1 + 17 files changed, 109 insertions(+), 11 deletions(-) create mode 100644 stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_slow_mode.png 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 52911c626c6..d3efb528411 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -2001,6 +2001,7 @@ public final class io/getstream/chat/android/compose/ui/messages/composer/Compos public final fun getLambda$-1206314464$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$-1267828661$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$-1344661276$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda$-1483541199$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$-173232017$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$1180759242$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; public final fun getLambda$1309976052$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/MessageInput.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/MessageInput.kt index 7db7bf4303a..b95e007005b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/MessageInput.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/MessageInput.kt @@ -98,11 +98,18 @@ public fun MessageInput( recordingActions: AudioRecordingActions = AudioRecordingActions.None, onActiveCommandDismiss: () -> Unit = {}, ) { + // While slow mode is active the whole composer is locked, so the border uses the disabled colour. + val isInCoolDown = messageComposerState.coolDownTime > 0 + val borderColor = if (isInCoolDown) { + ChatTheme.colors.borderUtilityDisabled + } else { + ChatTheme.colors.borderCoreDefault + } Column( modifier = modifier .border( width = 1.dp, - color = ChatTheme.colors.borderCoreDefault, + color = borderColor, shape = MessageInputShape, ) .then( @@ -315,7 +322,6 @@ private fun MessageComposerInputSlowModePreview() { internal fun MessageComposerInputSlowMode() { MessageInput( messageComposerState = PreviewMessageComposerState.copy( - inputValue = "Slow mode, wait 9s", coolDownTime = 9, ), ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt index 55b84203cee..126a65d9f9e 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/MessageComposer.kt @@ -544,6 +544,24 @@ internal fun MessageComposerFixedStyleWithCommandSuggestions() { ) } +@Preview +@Composable +private fun MessageComposerSlowModePreview() { + ChatTheme { + MessageComposerSlowMode() + } +} + +@Composable +internal fun MessageComposerSlowMode() { + MessageComposer( + messageComposerState = PreviewMessageComposerState.copy( + coolDownTime = 9, + ), + onSendMessage = { _, _ -> }, + ) +} + @Preview(showBackground = true) @Composable private fun MessageComposerFloatingStylePreview() { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerInputCenterContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerInputCenterContent.kt index ff219ddb0a3..14624effcbe 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerInputCenterContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerInputCenterContent.kt @@ -113,6 +113,7 @@ internal fun MessageComposerInputCenterContent( if (value.isEmpty()) { TextFieldPlaceholder( canSendMessage = canSendMessage, + coolDownTime = state.coolDownTime, activeCommandDescriptionRes = state.activeCommand?.placeholderRes, ) } @@ -120,7 +121,7 @@ internal fun MessageComposerInputCenterContent( }, maxLines = TextFieldMaxLines, singleLine = false, - enabled = canSendMessage, + enabled = canSendMessage && state.coolDownTime <= 0, keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences), ) } @@ -128,12 +129,20 @@ internal fun MessageComposerInputCenterContent( @Composable private fun TextFieldPlaceholder( canSendMessage: Boolean, + coolDownTime: Int, @StringRes activeCommandDescriptionRes: Int?, ) { - val text = if (canSendMessage) { - stringResource(activeCommandDescriptionRes ?: UiCommonR.string.stream_ui_message_composer_placeholder_default) - } else { - stringResource(UiCommonR.string.stream_ui_message_composer_placeholder_cannot_send_messages) + val text = when { + coolDownTime > 0 -> + stringResource(UiCommonR.string.stream_ui_message_composer_placeholder_slow_mode, coolDownTime) + + canSendMessage -> + stringResource( + activeCommandDescriptionRes ?: UiCommonR.string.stream_ui_message_composer_placeholder_default, + ) + + else -> + stringResource(UiCommonR.string.stream_ui_message_composer_placeholder_cannot_send_messages) } Text( text = text, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerLeadingContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerLeadingContent.kt index 016157cd9a6..521506e1102 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerLeadingContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/composer/internal/MessageComposerLeadingContent.kt @@ -60,6 +60,9 @@ internal fun MessageComposerLeadingContent( val isCommandActive = messageInputState.activeCommand != null + // During slow mode the button stays visible but becomes non-interactive. + val isInCoolDown = messageInputState.coolDownTime > 0 + val showAttachmentButton = canSendMessage && !isRecording && canUploadFile && !isCommandActive AnimatedContent( @@ -73,6 +76,7 @@ internal fun MessageComposerLeadingContent( AttachmentPickerButton( isPickerVisible = isAttachmentPickerVisible, iconRotation = iconRotation, + enabled = !isInCoolDown, onClick = onAttachmentsClick, onClickLabel = onAttachmentsClickLabel, ) @@ -84,6 +88,7 @@ internal fun MessageComposerLeadingContent( private fun AttachmentPickerButton( isPickerVisible: Boolean, iconRotation: Float, + enabled: Boolean = true, onClick: () -> Unit, onClickLabel: String? = null, ) { @@ -106,7 +111,7 @@ private fun AttachmentPickerButton( .padding(end = 8.dp) .border( width = 1.dp, - color = ChatTheme.colors.borderCoreDefault, + color = if (enabled) ChatTheme.colors.borderCoreDefault else ChatTheme.colors.borderUtilityDisabled, shape = CircleShape, ) .then( @@ -120,11 +125,14 @@ private fun AttachmentPickerButton( .testTag("Stream_ComposerAttachmentsButton") .semantics { stateDescription = pickerStateDescription - onClick(label = resolvedActionLabel) { - onClick() - true + if (enabled) { + onClick(label = resolvedActionLabel) { + onClick() + true + } } }, + enabled = enabled, colors = IconButtonDefaults.filledIconButtonColors( containerColor = ChatTheme.colors.backgroundCoreElevation1, disabledContainerColor = ChatTheme.colors.backgroundCoreElevation1, diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/MessageComposerTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/MessageComposerTest.kt index 0acd3cef699..e09d016af35 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/MessageComposerTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/MessageComposerTest.kt @@ -28,6 +28,7 @@ import io.getstream.chat.android.compose.ui.messages.composer.MessageComposerFlo import io.getstream.chat.android.compose.ui.messages.composer.MessageComposerFloatingStyleWithCommandSuggestions import io.getstream.chat.android.compose.ui.messages.composer.MessageComposerFloatingStyleWithUserSuggestions import io.getstream.chat.android.compose.ui.messages.composer.MessageComposerFloatingStyleWithVisibleAttachmentPicker +import io.getstream.chat.android.compose.ui.messages.composer.MessageComposerSlowMode import org.junit.Rule import org.junit.Test @@ -64,6 +65,13 @@ internal class MessageComposerTest : PaparazziComposeTest { } } + @Test + fun `slow mode`() { + snapshotWithDarkMode(contentAlignment = Alignment.BottomCenter) { + MessageComposerSlowMode() + } + } + @Test fun `floating style`() { snapshotWithDarkMode(contentAlignment = Alignment.BottomCenter) { diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/MessageComposerScreenTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/MessageComposerScreenTest.kt index 0426563b6ba..aebebb6499e 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/MessageComposerScreenTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/ui/messages/composer/MessageComposerScreenTest.kt @@ -17,13 +17,17 @@ package io.getstream.chat.android.compose.ui.messages.composer import androidx.annotation.UiThread +import androidx.compose.ui.test.assertIsEnabled +import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import io.getstream.chat.android.client.test.MockedChatClientTest import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.messages.MessageComposerViewModel +import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.previewdata.PreviewUserData import io.getstream.chat.android.randomCommand import io.getstream.chat.android.randomFloat @@ -102,6 +106,42 @@ internal class MessageComposerScreenTest : MockedChatClientTest { composeTestRule.onNodeWithText("Also send in Channel").assertExists() } + @Test + @UiThread + fun `slow mode disables input and attachment button`() { + val capabilities = setOf(ChannelCapabilities.SEND_MESSAGE, ChannelCapabilities.UPLOAD_FILE) + whenever(mockViewModel.messageComposerState) doReturn + MutableStateFlow(MessageComposerState(ownCapabilities = capabilities, coolDownTime = 9)) + + composeTestRule.setContent { + ChatTheme { + MessageComposer(viewModel = mockViewModel) + } + } + + composeTestRule.onNodeWithText("9").assertExists() + composeTestRule.onNodeWithText("Slow mode, wait 9s…").assertExists() + composeTestRule.onNodeWithTag("Stream_ComposerInputField").assertIsNotEnabled() + composeTestRule.onNodeWithTag("Stream_ComposerAttachmentsButton").assertIsNotEnabled() + } + + @Test + @UiThread + fun `composer stays interactive without cooldown`() { + val capabilities = setOf(ChannelCapabilities.SEND_MESSAGE, ChannelCapabilities.UPLOAD_FILE) + whenever(mockViewModel.messageComposerState) doReturn + MutableStateFlow(MessageComposerState(ownCapabilities = capabilities, coolDownTime = 0)) + + composeTestRule.setContent { + ChatTheme { + MessageComposer(viewModel = mockViewModel) + } + } + + composeTestRule.onNodeWithTag("Stream_ComposerInputField").assertIsEnabled() + composeTestRule.onNodeWithTag("Stream_ComposerAttachmentsButton").assertIsEnabled() + } + @Test @UiThread fun `audio recording`() { diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerInputTest_slow_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerInputTest_slow_mode.png index 6fb67eab144a2a32181d2c5d040c73fbcde37f61..a1ef9d8df93fb7273f28a90e09f12cfda9612c87 100644 GIT binary patch literal 15416 zcmeHuX;{q?O3diF|&)pd1j%`TyjPdDO zzN7N}SyHl&O1|T52!YnEEiR(+5!av9BQm4812UK_F>0sz~UTS!y^ z+|*zD??11Q{&nvvJvj4h|znA^*R{UGpW`O^J zRsWry|9#~E1i=3)lm1tf`*SG#V|V>mWd9eU;y+u4@WVg7wZH!QPm-Pg*bkpJ{r}SQ zCnx@oWYFIf(|_!)zpvS^cLDtpzHC6h7`^&%UsZEEH*AO|U!Yy^2ygKhq{g(Z1>}{% zJxUlW%qF6U3af?9SG-FX-O~GdYi=J5=_Eivgoq(CS-;u&r34|q8mbULHvr0CF{e3A z5UpQ4ekr`tuad8)Sk}X7jN)XYxFM{wTtXpADMJ@L!VZQu%2r$zkpee}z-`D?KIOUw zkaaGcf+!qsYu?!`YT?4dZ-w3oy_*6jwXHVdYBcCL`5bOD>h-0oBERIH0UskBT$+v? zI;weaf@Sev6ar05_%)l#o@ld*57C{Z#g6x7ZT>$mryXl zL*mzmC?s@5n|&^~B+(i4Zu0RE;6?EG5dW51{3G$#E{HekvpTjCEO_QxGlW(M(R2gO zHyLpW_oG2i{fiTOJXqojoOgiNyLLA#^kg#*)vgJ=EUy$yojp~;QplqcvJs$1nWuoY z`dJUhawHY;cHdnYNN32<;tKQ}@^ym*rx4@jzBzJu);E@pA3qPv49%)dbk^l%Q`U3V z94xs80F)D$CavqDX=h;ya5^0+n}mgBsCo&!xly9ds z29npsGE&JZ@-*z%2iQvJY>*1TISei5pb2M^^LSgh;jVyQ`|bAb$XHm?qAm3Jawd2{bikMKB>EHEMCg~od}B^ zC>&PFw^^ARTS{n#?FTsfAV+VeM7(>K1d&eELejJ|kPOCnQvk{DT_pbQu2DVd8@)k| zo^G`4sTOO5BWjg(K%{qY8vr_}DFeNcpd{zoT}W67v+L6vi14qCulsZ;G?Jq^LFJ z1yV*XW5|1br>&6bmX_lVMKxNLcy|_GTeC#H z8?F0du&C8%u*2U9M5_3n?R1`R)wgnNr_rGqLYmTdS4e3JPsLQ5WD)NU6_MSmAd;65 zt>~>p>5_EB1r3hTi5FBuxicJD*N2L1TPAuGatvC8nJZ*T1KN)u zO2r;Tfrp*L7GA`3l;|+=>wB|dn(={A26Z0H&!?TkFSdpVxG*$cOj9mVZZTU_4kh>q=iz7G9AH>e%R#w0G%i*Va4>cqxnWibT9c z$=z4oMP{}j9NNHUChVq17~v52&ILFg!$`Un57#tTU%tl(t|*?WJ;$8EP6P46Ca zZP6#sr`oQ56j#g-xLDW7qXNV4ywq`J5opDSco7T+AV^^MDT(33XX$!58iVV_h z>a7-Dbj)eJFHN<&HZ%FV%HuXL%min>{7!uy!=V>n2(WirT)Z!BQNc^)O!~)QK)}jW z7+Y9${q07<^==%0Ic5h?)XMQ2Zj(s~%&&ot9N{!NO&Eu@dnkGdDx=3E#~qLgE^EFG z8xIDF-NEI|^VuwHnhB0Ze;MzsG^*mz)h|${9)>;R$v3*ww%5+k0aL&^$L9A6TaY=ktNJf% zWA?G(eIzX;oT=)vP)Tf}dOeJ0VJvQwn zviUM-OcLe9pGqZEPHZ{ zU}haPZK2gQO*>j28Y#lY+q3QNA}|(!;{7Hm`eefV_3eo91QgyEE-H+18ZJ7QKm;OGPLxph7}a zFa%FQ@cjXAlbkaiHtNJMBkU~4Gw|x8u;bRM`ONDa|58*DyX><{39r5=CJS56fZgP? zv=0QQ-hyZAa~P54emrEP6{{jS(S@V!-{Wu5fGsM-L|Gk6pXiel$RG!5>00071%|e_ z=qoRw0RCQ6Rv@^Lsv4tGd;smvP zDYk}rFpuaYDwsmaztyV`bYlf4RCi+!i#{_Q^y}-+Dm_{x>diV0m6V+K4>jl&JElx4 zDoSFneYC4XyjIqT0qL8VFuPq^0lzqy4R@snkALDAEl)oR_D;s3NK=Wmo_ybi=6pde zD=TY1qstM5w6LsYqjIz;a6`iw5Ia6Ae6w#3KDBcPu(ETI-G{2 z_a84=z&9Or>op11)%muMw)E*SV6jUcbt%|ebP#gXlJ;FU){GOJjaPyDURr;Izx%Ct zJCNPkf^@gX!^fQ#!6tXYZR~a#)JpT?LrZ5@*Bs|MT!Cx&fQXe$8Sh&UDvzoaWg`4} z#AZe~-UkhKI11FJY^_M{m9{XGJmfCUgX!HFl~TXVB_FvB6MknBhmia zE>muCfE6d*i+wcC3G$&TV^>aUg59I zkJ(!{|6*olSI+#ZJO+w;3xoEgd6FIRR17EMtmY9m&C>M-mBMR{H{czJMBEuv?mlri zhzN18;I-aS1K)Vp=mK}?3YvTZx=ilN3%qS_aX1ejD_pJEOnTi-|J5Ow2Wmzq zu=Sb6>mxg@&A30_@4Q5^xVjK~Q`K0p+4Y^e?KCN#zgX_6@toc~ z!bKK>c4VoVr(=&8%^CklR=rSbFfe=HuBDIc^Ng2uU3m)3zsc?pacLWKvhMR+lk-=x z3jw$8;Q({eT4ZNV`cGLTIgbZ` zjvjV6IZ6a0NAELFV;<)#Wz~n5pc-s4_wwovLEUS&WknU-|aO?&Knt+6kCp0VUr&+_&LaP_=w|NX2wY$7ipZ4FE zLwU=IPg5yS$&p%kxU}}9c=7L_uF!X@-R)tz27~Z{yPwvrIj1|!oXxPI8vLatB1})0 z{q{Q7vT{Czb3RL=Rqq1Ll7>y(=9$_KIkr6#WXzFzu^cpbQ1;lUz`9Y#2oV0kwD zBxRihcny9va@-YkIdjRb<+Rs$OobN5h5YJ%5vyO0%n{~49+3wkB06}1{D>)%L+XOx z>1=~YYW0RR*!K|CljM*Hs zP^<>v{W?_IW>3MY&&<10bf4x9OnSWMUe z`msM=PsGT*YjFc?uNSz_ew&D9?wXnb@p+UH(IPKMD2TN1(#iz;og<72K@nT zwK2~Pl_b^!m74fu(68phQIWuU$`#WkBZakDhyw4?J4Al9*NmcmZhGFk!1}jrt95f_k^#3XE>Oq2vJjEocZFksONpyELs68E#8}pcBOYI=m0C(b!;iQJvBx8 zC|~cj(dVMVx)RI_8LLmqz^a;msFFX3d{XjVVfSNC#4d*|@wR~Sa&4WG```7efsT!H z)u!!?#7&2sZT$|h;`!n&DJGckR$jxJ77y~Hb$O!>{_xb<&clAKw%wM(%YH9In??dc z5_{&M3bFW}*5|i30I$Vac-Wz)t>z^rJ%QDJFWVNQa31D!EcAbu z{=nw*+cTPgJTY8@ny7n#OV^*D4sve#4S`qD0x%OKc|k)0+pF-LiDmBr6NaHuzW<>9K>YUu$89nU0WX%j zTa1v`@&plAVHFj3X9Gh03;jwPmU3@xHme*L4>wyaYXA=Ew}72Wz`J~XpP1fgHg!Gg z`AD=Y-8vBk=MAai4>ph~zLgk(uPi zdGI)HOhv>W(Od4cBt(oQIZqka^Aq7b+<~f}fCssQ$HH7GAq=a(umsj<|&MWwWaD&ED#W}=Ks_yRaW z;~A!ZL%v`)I8tzL`SurX)DkNzerh&YuMQOXcJFX$!pP<0a%PI)XipE^86It8v%DJrs1 z24#55DOy-uOS%_78lr?WAF0LY*AVpw66gbJiZ5#JumpZ#lFMSZQNg|3u7?=cZGgP^ zzhb5sgg>GX%aDV|qAZ$z%QOtm3?QA%G_w-6Yz(!-!%J2DtuG+KeNFKek|D^(;YyKY$(La3gXZQR$P5++^(LYz3{~*i$NkjCn2gbjp z)1Qu(e;fW+hyOWE|1irx+tKphR^bn`{5k0VY=ghA%l{y2`peDzF4V*}UTz9SHeO%O zye5D0en@)nVO0xX*$nP!W`FA=B>lr-(yH^-<(=tB!H|3PPA5zw_|5yFBIKrO45tU@ zU0|yQ$omZc);5IR480Y4+tJ*wzH@rt#dGbrwd|vX#fCEwT3CjdIUTr3MYS+jYNh2bm~sX9O#=^~<$ixHT6%YDtpDPtoB6h|qp~+6rplJ(Yvr@Kl>Kpwx8EEc z7~L#8eC%bf-L9nc=~{4T_IEEMM}A}}*KQd8MxJ_KMq%8{jDElkq<8x~fdgqz-aDZj z3?2Jld3+q*!hUmPWSctJ51!U{`-D?s>f7gVJ!Qi)_xP^_Y8&OMr(M9f^vxd>s-fdq zBDtw@>Dslhl#QA||9$(sKi4W7h(SgDf%=1R)dIrieBjkwQ|My#UdPN`&!1M)I;g8N zl=SXBb8JIx$4m>62jS`oLChOgtlldbk7;s1M&FFu0Yrx16&K}!y-}{2hG9}_teidf!~|CBVkP;#wv$p>I(>U~ zh--SSjfp&HQeRGsG);kOw`v(_$<8;S&+dCH}yji(6TfgY?2h)0K@J4G{EXl8R z>N-_htvS~)_&4DK!Ys_fpgb>Ove*kP1*KW0UEOiW4?g1K{w2pScrHeCkj0oubZ%QA zGi$+e_o8s(sLxLLK^*-sp|fTQ zN(hHadf&J$A_|FDd>T2oy3PlBUH6BMkKakNxugT{nH>t3sL8$B#H~xBO@_f_OW2zy z1RKF?5XRHpc6I9cwN<95(I^m1Z%&F}MaIQIlFdBZB4>g(ttX>Sne3YRjbO=yy9@5& zxL!)qRkE-eccFPK3ajr+N**ytgLt9Cu`C~)j~VR@z7Qr!5HL5H#mJgmY$iX?xk1Mp z6XLKTGMNclLufPeRXlM&)8agSQqm4NJDY30go6Xh+NBa?$b6J+yW`lo+Lb~j>X)>i z{p$_&K3`X`Ti8qPqCnL?{JJt=JRul2c_9>|l`*ZFpk8AlFrnTPr_$$Wk z8w^`JGvd^oM^b0cCNt_HlEeg0KCT5k) zCraS4VY6-WH6`V_Y1%VxQfDZ`sK`y{#=B=j#(`d!#}pqXJBF{wg(omjcP-@1{E)cz z9%Hc;{ce6S?n|8*1va0*cLsue>V%;aIjx0D&#|wnIF0gzX&Mw5_C+?$r{ahNhL4h8 z$3-Ndx{@Vb(yx$anYWML0t*D`{2;a$yd*t z48vwhmLjecMXokHc00Yz?2^@I!9y*LN^8mIkhoZ=yEodw9qZQQbg1V@$+o!h?``+J z$>_qmu@3v$M8>`U)R)-9ugUn*qO)2BZRT2Tu8ba+y_g$dEP5-%AG2tBJC4oXA6AX> z2G5Hy_RL4qYzB&db1TPgs`5hxp`MZFYF3iv!so;=+39myaoXOB@39J*Xh_e!Cw^5l z>?%*XzzD^A*zO^$2vBnvPFUdH#+dgj9&>o>&+|1(HJjC{(2Bq&MrlFBlAVqN?NCi~ z4vYV^c(o=vs69<;rH58x;W){8`uJs{EZ*6=QCSVfgNb6{B2%nAoq~9`i2AZofJM#b zfI7TON`DndU?|&BB36i4Z+^aB93Qq*2Uoog1AnVg-0kv!S7= zeFJA|^b41KL~_nN7K%++Wcwy8ASEKHwLm7(ea0HzSmAYeflY5-KdaO?L>8#@)nftO znrH^4EFtgal{xm8y4o-XUXczPQ-funm~t^k3sUJFEvT_=$34WgDNkHuhW{#a-Ov%; z*kU_#^)tP8yN%9GpR`MSKe~BL$xd8~Ro0g!vAvnYWwNUOqRWUst8|`)UVQJaCX?d{ ziYu~u?YiKm`GhVNE$yd8F9)b&K!4I8$E~6BENH@5L3N#HM`;>TIjM&ww@sAfxXd!{ zYhA@QiWX~=AJl7} zr1rt*Mli!>kTwBI(-pKQ6A~jDvQ*P@f_{vkZXBc1Yy%rO{&9`#PtUU5r-#iXks(YA z2(b$r+a#A{F7X#Ye0v?>)8+BTm3-XdnKYV{27|6t8QnZ7CD&y{8GG{t$6BFiukh2YUt+t)wRwoAv+;0oG5OLYfX@9I`hjx zy{N3C?x{!GzVws2s8A%gMliqGiZ{5`K{a|rn75Uvn9Zl9YzMaJn>y^9jyXJ+i?HW4 zUbIL}8t2?UEm2JXL5yOe*hC@t%g6gmUVI61X>HnYsVUYsj{T0|vGl59$u2+w)48aS zSB&;%bZ@cdGAR!6-YwSQ4Rta7ni_QId_Ilv33}2|3q@xRO4N_%)}-sLr-j)tt<+|{ z>-{dV5_^JE`YfiycyHee226(R0^WSTIlsTra_kHW$&t<3@%&Zd5i|pP=Kbg$z?O+# z%C@}=@fshuR=e)$9s4;XykT<{x>c|BF*@K&A~>y#?Dajh)Bxih*>-83mV?Ep!|}y!p2}+g zH!q7;jaEcij`a!kvk)G01_3oo=tM8VE+iIY^enzO6+FxIF#!3R$BYd*sOz9IEFAH3 ziPmwtz?d-!=>x-KSR5m%pKTtF0X0I%GrW-S`B_;}yh{vSg%aJJnEwHvYn5ZLRpi*F zof4$Cn`^hrGLl`OSDJ$_8dP z{orL41A@3wFO=-7=?@tE!dwy+2i`DS7^ibaZIC%M1hzuRDVuc`>nR)ynV69%vkfY^ zuOwUEpVG_=tR+ZrMW;Q<4@N}dM1J znMiw`=<{6GBg8x7j@9vDY}}U#qd9!^E;z|AbSy!L5^N-itRtks8pBXLFJ|V!<}_2v zZVKmvwwFkfXK*IdLNoS}00xKDw#D&4167h8^wjRHf?)dic&pcrwvgcS^XztTkL0aM z_}m{exe=t~_|l^=B|wum+sx5rHKl)*rEAmW z>yu;;$4wX(ajAJyWwpvkB4=*mn0&*a%woUUN-Fk7*7;%gHpk5$&TmbTe z%imvgxTuSYzy3>n$dW;lcP`G+JNo@fp|$qG?IKb&uRaL8(xS(Wn{gD_Q4k4dC4Kcp z=xhVq4SJ|p)aDPFO-T()&EjwZa+5uH#hQf?*!q#Tmdms_t7SGtl9{aD>JoIsl-n;; zGf<*^w&AJBhuh4G9~s3K9a``%epo)nKU`?vnIw zMi1yP5}6v=fJVXn*}j)82mKv6i?yCwPemU%eR~`{$U>Zqn;XJeK)>rYkcXNUVd#O! z4^g}l*abFp=5@*CBxl{*;NhH7`UQe^i-(CCu6K8cqM#(Y_me@s3s@Nh=tCO*J@bp9 z)@Q7Y)>-1tWIxtj`N2qu$H}$4~h0&EC{e96!8M%TGzFvp*+@%t2_*=U~f0 ztk81R=1IcH8wc!g^w8&nF{LPE1!~^lR3P+*KPs}!rl(aFUMAgXqVX3d|IuXC!`fGb z*0QJ$ECK;H+~e>PZt2YeOwo6E`&QJHZLY` zm-a-5Bq)0Edfw>Gn#khbq)UTh-6=#Ec*T}8UIbr#T0^e^bxjsMtktzSBkZdf7B`Z% z^hhZ8ezZQHx;texTg`kC_3GgF!|`T_bA~KFFX-iTqldi~aJEvNrW31$oVyoJ?%X#0N>NAgqk0^txab(lnL55{OTdgj@-|u8eq-T5z5oYnL464t z`p%)`^S3~248)>azP)!kjuek*#1X=s2Zk&og=>RBKm74Rg~xv=SSCF!+ET!&=>~03 zGW3F{7|uCY2tVGP)99QFKW9i|WtIMH(t5E}Bx@NBb(!I=I$a{BalD@&#P&bV)>_rx zIx7@e_~xWR>~^NP%(Iuy*QyT&Kack0&3}n8gIPnD;-CAb$UX5Dq8Mu|{YjJhZ4MRs z33^VM22w_L(jlSMpfguWz915+qg3A^lRFxX0R>XX_7PmQK^CT|BCyS`rO4^8vT zFbsB^5orYCgj*3EHNs@EO>${YSf?L;)(`w`I8{VGaDFynGZGm)_SSFrvgw}qH>lO@6x>7G!RP5TrS&VLV3a+u+#a{Td zeMxpv37f((A$VrA@XnA3Gq!(m89s`1KR*C#t&W{~5ZIb9dTX`b*1N)OjT%5OwgXH>!~kXTm6zJszI7 z@atCsz{A@42LhBx*zx4N^VNx_y@lMqQ)|Zv%S9{Jk-a^?ae7W>mdP9I>&!d(M@nx+ zv}c{a!|720t6XcrwJIUo7WfDzH2M6b4Kd(K+tS{cz0_i2kl+M=n|MwcFj2Lhi#r)t zy>?ur$9N9A?TwzWQNppQ@V>En1AF|(vAdh|v(?d@TJN*Co_y903rc*Tax2@{23%^_ zB;Q+%MQ!Oj0s&XDO)gw`$x#KX9n%%IU+Mdc(f3N(t)aI=?*OmVYqlbT1hHDcr}3vZ zZ8O!iI+tyPENH%RrZIAa7u9M}r;Ma{_95jpd<4MWsOOsyVz3hO+L@GYTyt5racZsU zd^J;+t$vr&1g?vRBTF;1?tpn`BzS%V0i-Y}N%pK?H>S`rZ^gZtx-T7E>Np1w?7FfJlb~hyg+@TcRSpw?svy z2~m0|iHcGZARs05h$KW3NhBeL^z%6H-us=Iv)^;hcYXW2&bQxxa;Czoavb4p`x z%H=~dc0sX*j4&u!&bWW;?v9^4j^!9VzIDs*d$o%5r+*roRXcEh^Nt;BcKr0j>*inr zXX_7(9ht5teu2w)LOn8lpwuSYrm!%lmxUN$J@0eTGlSLPFt!35d5OtnRw*c0oR0Wj z;Z?>Og+l=f3Y%1Z-&Al||M%WMHNTAq#(wMlQ}dgYzxDp9`A6BmoAFPw|F(|5OZh*V zW?DhvhW1*8i#-YoyG(!IR7l?R_ufA>zl{gRe(U{H^P7~v_5P{(N7=ud@lUek7 zYNp~Rqq{ZM+}P~FCP6W1@mGc&2TsQ)9otlh6y_r<{lDB&_LvH9_TzV1;$Oy#r>OoN z;u+3*1;>_Y1_N)ew%ccS@x|M_Ox@z&XA@OPza%hhs2A{sunv~rD7%g0AKtdqG zG%W}+cn)(|z{nOcRfbLDk0*mL2vzVGqqfgy!*TSyt{)Y`-sA>j_O|~? z;o_@0?Yg~n`(*;89JY#b(Im(7tHCoze$38}lk8TwVV-Mv#(p{{_2qiTKn&#o=>u;ob7{%x3Yxf+Fq;m6`bHZOxlf=2@XE;q$ z5%dFi-0s=a68{Nxp#)bk`!LyM;7+eTarlX3>za%6S^3I6nVCvSUv7^cRn+i6aI-PK zHEWe@UxcB2&8{9I2n_s}?{~p*p@#z?qjrq93a`3?ST)VHvvQfh3$yxG-&)mJnp$LA zSN}|zScBPLder=(HgBcezC-fH?(2A&Ep|$2hk_$MAHVW+G+~y9KB`o3PEkGTXSl`t zp9$!&jIhkGtR$l%ia2---fn=s_aQb*;UbN6aXgmdhl*)$$cBbk%)|_@L4;52nb4ik zL-<~K#!!Q->X8_Cj1wPe87YK)ufKd%caz|5B|zircn}5;z7cDFIRu+X zH&B#pbw~G)#P!(nE8W6vgQk>rDLB@uzvxLZuO)4BSL1U)Da?Vl_~E0plyb)Np*to#*qQ+V{V_D-CM!l63);VgdO+Yv}i#W2?1 zYsY|t%PH);5l<;uE^)K+$nGwcWURSJBgS4HYu4UgivQ^>;vF@}j&;FpQ=zXFXDJfd zrgcc+!A1DQ4Sw|7W92rw2}PDvZ_E*o+m*xij_Q4yP@t$TC#{`j%*Wj&==v&v+<#^ zlI3otUWRzQ8+6VO(z|!G6gGQ7=As$>cAIR|8XbS57@MDN&wn$XSSg?4<^IfNzrf0p znh53hMu^i)X=-1k?%)rX(uZx$Oy~s!zPkP}yTlY#W zq?84&-eGXgqsC73sce)g``UaIDN{nOc50;8h2`50w;H%zB2ExGqs!>IW?J!|9)%1& zK0fM&=jK9!isu@Znx@Wyfjd`&?djjxmthj%O($lnv|H&;O1^G)J@P1#Ocf5mtVOoe z)h^2_lvYwp{D+&0#0wm_Puem6_{FDmo^RsvJK4cCn&+6EZb{s1=F4tPgHC#;lV)XY zE3*m`)0X5z+`r|vhS`#4O)w1Yf(|b|*&WmIwGmr0D;jIwLQ0JrIz>pMgdWM^k^O@PC?j@#v?xxM}4eFNhB zZGB84WZv4AFE%1bxDe3b{_>LdoQZL`Oahj1^SK3AcCx=+Y)E-&98>V+14b@kK-6f0 zo}t?IkYJ>Pp3@9u+?b%(494T9(QAe91x7BkI0*<|O?G`SnUIk`^^%XUD$1Iw0sVM( zDiqs6Bri^eVhyR{d-~|}JLB4*&$%olub6sxeUvOgvbs!97{HQ|$hnQ+PXZR%!Fh=N z<|Hnhi-WW^;EnZj=#k@zSG_1qBZ5!1lb1Ra3RTAJ z<x*V(wq;Bx-zG;BKBDgE`3#8?*p za7EBK>hKJNd93MX^*+1oJHD8^)p4Mr7U`GPL|al(tAU|~Xw!p5TC=3gI}e^r3ps!- zr#468g$t=v9jA3nlf0=$koAyt0A!KTZU=kCHyehC>HE&iqInMMxn}+Qh*!#j!} z%x^v%Ua%kRmUN1@tx=L_8n)11?3JV^%ldu{_S->y9CT1JRNDV^I;atEA6YGaXPjhG zL1j5;iB3CF-J4Fre~e4>K1>bX)L3$}1ZhZtK~!|%=XGIKOa6gvGZ~d~J{oo0+aAQQ z9MREFF^eR1nhICPkO)Saz8kK;+rpl10edK1-DuqYVqSY#)ELw^;`CD-kSc0&NWCZE zVElk6Tt%DT-JD>1$Z0PZZ-8yD>(;n78z+|v1R^Q)YaA=1_uBJx@5AXS-pM`@DRCUu zNvSxXWi*m>?*iOMGd>ux&%^)}DduC5GuNkrK)gaG;BYFFge34+kwoRLaCHH7FFB-V z#L3-TYk{M(f%h?NDu8VD*vHBIhV&#E{5`3(xb+t6->jZhz{371NTmsZYrFp4{EkUK9S8rxMx^mUPsX3!&-q= zT96RfUw1nddn2);OQms)I<@Is#2bXS24)Rb|R5yee$bB!- zc{KIM58nCmp327S$)L8S&cuG5Jx(AhS4&ewlD?3cALwLt*&K? z^SFa&x5%+IYG$a9(Dzq)4}+6Z%g3~{ZKg#@P!`vL3n@ap8E-lS3>h(H1N3Vem*0c-<$+`_;TOo4upS))K3wrk}@P})lguL4u zgyM#z@%_krr3Jw_QZBU-q3itWF112>+J_loO2=H!+ATiGCR&bDx@p>XL<5hK98b2P z@*w4znS&HYrQ5T9=me{ozJCx%OB!g8(9`4mxvB@Z)c5wdC$V_DA^XC@w-l=1slpr` zrD#OBnx-zGNZe5F;a1@CT#wYct!NN2;hK4s>3VeaObJI+If@JCeL0JMnOAMiK-9oi z;qmaaAn3a^G-gF*aK}t4KTCo zeK4Asp!89<`G}+)93bOB-$FMxCwz4;_iF9SY*FVE_l0(xVg;GfOR5$q*YDc~6bc&K zTK2pW?URpd-CxcM8eQN}eKQ(`zg&k`B<%ws%gImr;YC8l1TXes=&qn6=*&!%1*9Bo z@33EfzyEYEA%?ZFg+5@EQbhISu53`gV`%078?Pia;d>SJIHOGBv)m_2Ab^;7Jg2jhJufODrtB_>h zJIyoBL40gKC3-r#J6B$O^o-V{(4-11Ry#XCkvzqN_EBbgDH#2HdeWxEm1`CuOq( z$&2x0^=MC-j=t>{$~**DU&GlNf*U4o$f-dsiCgO;*@MuAhj?zdApI2Hw5xp!BK$TL_H_E;uGFAT*o{GoM~$Zj50n`-gL%W_&+zQYb;7!TUtV?d(^X` z9e&q11(&UC)OVgpHf%m#ZY<9LXH#{&ehJfXR#ApCW76(!S}`wJucLDq8C49veEA{M z-b>oievI{lPc}gTlEAP&W|gyOeEgzAHi5zWKL2%Es_zpE>?2Ec#`IzE4rDBn!{E$^ z71XkcXENvF*M(VZ5}FOhLrfHOI2_U3uH6pW`HsPMS}%>^ld(p3^EbOEyU31Xbks}L zH(U%Lx_{!n7>Uvyv|)%kVe0DO;Q z2Uyi%!Cua@Cs~fM*D0;Jk*Py&=r@a_ptQ|0^x3qJ*ogxZ2Ro8oXcNaeW20>qE=n^) z@7A!k4O8guqO8=c{I|igMs`4a^msO0ZofuBt+wQ1Tdkvut931wNxerQ$oNZXT>u90 z8#I@4h#s#pkPA#>Ofm79($ULLROz|ft5=DI=hwVvWZWwU@hbzDX_xe_0N@_J{l%A? zikdCwTtD|`=H3Ja+7|0h>8d@AZH{S#I|!2A^vN6}EW;zk8E#+f2N*i7T+o9fyVKvX2fUfW{*=^^)X8Gw8=f4*Z|YK?X2 zcsBnuPdRV)8%)SZ>@q_SQSUAydye_>if|-MNc^&EcXOOhXI*?L=?bb1^A3G-ZtPq z&3r()*h&}Oj0s0IwWh8XhfjY|c=a&QJ-~<7UQlkg6uef&WUZR%GELtexBQn$tIF=< zJODM!+?)xGs^9-Dksh_P3BCqoOe3Ft3-k!lMk*zgkS}Z3C|Ku)k^&q8PSP$tHCHzY zwFn6L26+*05tTDhCH_*Tmv~M=0YQA7UJ~h}b*DFPSKrP>RhqHU#sm6vTIE^(!wCTL z^*g4du&-{zx2I1Rya|3}|J0?ePM4A+dJc=z<*Vr-0C*Cqet_s+QPKK4r1o z`TdR5pV>Sp6|?y?_?Yq3ZZ&P6OZ%FgC|i+rAHl#ukQ##^!5`xhqfVg^+EoB@GNw*{ zd)4ezRd{iUh=8+)p;WYZbxbT31^!sKIr$1SX7jNA((>RiK`tibr%bm?=*Gs>82>aI zx0giTJ67aQh4C*myscpI8$c}ih9mo5-Yt+itPGx!A8^ikviY!sW0tOpX_l^Y@&jGx zENva0_y+HxBL^O|3{GePxJVL(*I}o zKSus$HEQIH3(Gxaqi^0Rd73f$s-N{2`J5^=DK9z_yHnxC&EGPLpn*~^_B<49zbCm5 z$Q#e~`@E<~ygjc+u5e5*K0CvErK@o9uOvhgTU@_p!SMWa#|BMmRC3>~<4T^{&-!cG z8aB8@Ga+Gnd4JWyfY157$@i%ISZt$Hm6H}C>^#B5IqR^5{`U+7pr{!G;PY&8ar4hj zM=jpOTLow$CCkj$!jvj;7uJ?(+A7|NwuZVkUh91i&^xI+oU<&5nZq4x>ekh5V4lgY z$>{&Gx^?OHQKiCzSaG+TA$WS+dAM3pdDD!tzH^qNBmJS1z;TyNu=m8KZ~QYWtcSMs z{gdE&)wNgm;dcRx;eKPddy;$ddY+xHcF;rJgWSEg($YKn8OM_z({+A-qjN`-JopWFj8aS{NB+Uec^Dd=reQHWP3F$0@vL7iCbI+b@KD_gG)6=mtgj%Q#qQN zJY^%5EUH2A6?$f&%562^xn8YogvlRbf42^QX|h z>X3X?!OnwhWRLB8X+LdkH!Ldeg|74D3!jG7N02?7NmY}6ww4Y2=W^@=Oda>a$ixA} zmO>TiLO#VN${$F1b=v3#nw0~RZckRg+mUB3n3-jY_`JZP*RgPdtD2swU4MCWsKEJl zVr!1Bvp0bR*U$HZPFZW1G_I4{>Qpw0o@N!L6Avf%mB2TW-oyKb03~ybX`VH|+Of+d znhTsCDGwIsAA#;FtuYOny@hsISoTDPc3D|E0=t@NP=SsWZ*y<^T;yXT6V-bbx!vCH zaJBb6iu9;xU5f8(udFAT(|7WzEwh^o3wR%HrQv0Ne?%8ruwFCq(e9F%z0Rty>49s< zy0_JHEBeAUkXjQ5hufNgXY>Phl0$7tGwATuzHw$d&-gjJ4f()6dhI?h+rFi^#XzrC z6J6(U8+jIsGNqkey#W@FC|sT?rsJ0llwQRc`n-Is{7H;2iD zI#tW)c=O2Dk1v1BQE~Wsm>n94Z!6;jE|drZ+i3e~e*Ef_p!TrqwX}pT6^4f~e6gBT z8YbaD{l{0S#!0;f2^vQa1#BuDi@*!VxE&oAQ@mELi=~3}N~88KSs`57fYwgwf+YcL z7nH4>OKzs0!&aiD1fLwa6fZ#ZoP?ZE9h!kKK427Yq5hbWQ49R%X!%et{= z5l^WP>T4&IY7-~`?^O|I#kD*DE~BbpM0 zT;`K(w;FcKe641r9H5rm@r?O2@ku8P<4uZ@k%Oo|67FPv>CF>vlcH9M7ee&Gfy5Ag z)&_r3bbfy~{DqQ%U(`fuYS52?Ee}*IpSHsK-fAijY1~KEa^K{Zdf0Dn3uOYPTySqV zneHg2m4m$4yMgM2`6=p4-|NwE09}=DbLh%YiCU{+ilxq_(&ssCSccH+8->{vFN~w# zC6}dxm`d4Cwt2}S#4HtMrI3p*r6%Re!dl#7ZPyf;u#((WP-q@B`w-RF51XbA2@YR< z(-7hY+? z#OHZTC7!>eJ75m5B%?yM~yb?a9+k08GSbCE%}2X_^a(UIag&?#8fV7WR5w z8F9%15l?3QbgJYCP6QY15{{IEz=+4<1bfLBOkUab8hV9u5+aF{Q)*dt)H=sj$1nGDVkM$e0I}no{mjD{`7V?s_OG8Y8jXk?ub%uY^vRRUxL)|tUB4{8 zkc*i_)N}8F(iRD~nm7d(h-MOI3YWyPbPqCO_9XG=#p-n%cjkZz^R5XVk@38#YGwSj zV$-+&P4uQzRIldsi@uFM0*%jC$;b=BmYQa`A6(3{Km^B1H(T;WB|uREmDs8Al4lfI z>ra(7N(n)Phm#-g_;`8wM#pl7%oc8jWHazBi38#O!qyO!26bGD1E1}fFBLZe)j2b6 zw#zg&nQ&ZK1ZnGFj@sm;BRbiUOPW8gwdqYDNVgm$G{s&kjb6afm)c;+fiX;MM0G#- zg=b^fL|qpp=BpJWKOH%sXM7OsMEgpfl8Gn;!75+rfd=JB`GLR(5rRp->Qilz!?}@2 zPa%hcly^&$mT;wC<5_J}c+SU@MqnX>{RNF=yV^^iv$3pqbPBtU^Xdz^k@a`s|{QD3Tf2QO3DHtp)i zD{QzyPPxqlez+ay7^j_A3#kk4a^=B)$@h;x-uP}OcHfn&lMRb@Y4IZjCp6zP{pExE zqEE+I8MyhhHj)M>4mH^d-Tp?mfM?y4D@{iodDS^hXQ>SrC zdCeV+4y~~EaM4O3JzT|7NKGws6r}D)`dh%+DOFa`g|Ait%=MzdvzpfNi=;`dOx#Ce zHHkqazZBlRl(gP>{3CsIJbJ!33FMU*ot=Hh?x(%M4xjRDI9k)BA4>RdbyQOF4=(X# z?KRO_iS&NJ0tSxAs{jiC>Xg>C8R(N}wuN&C^snK$o^I2<^fX=A8+gH^nKMOk4hwyr z@=5vXQOpv5S?HAn3QD_v5Gb6A?nb9mzna?!R>u>k6+x>nO*M-+l8CmYH10CSeuV_2 zdt+F{yZbvvq37&$43u^>;vyP*Qr7V`1~wvDW_Q@{*zZcqLz~+a0*nvT5|HeXqHadB3rjdxkl9h-ce>&gpMdJj{)2i zS6j_gzce=Zw~7agz86Y`tpJx!Q1`6V0^PM$&nnZdRx{-ws3?*K?aiH($t3n|(DDQ~ zbij^tOP~D&@M-aLDNRWqUrdfK)2~DjqyyZouiL1+n_c2GkSLvw0LcGHCkH*)#hSnZ zTiB=r2;e-J+(Yi@uRi%hjoe4M%8hovWK8Y7%^ICzk6iZ|&dRAXN=H##k z3{Jva1&g?rBylZX)VfA`kax(H7oKr57vr}6RF@-upw*$R`%+Rmp8dsa59G;yh8A7D zLNwQ7HgF`N2i2#QW`$3bXhu$!7uh3^;!bfBG+`33YO^~U?bjT$*h#Iet=ak11c%)Um`v{N{Ewha;# zH5nYlZ$bCnTDMUQ5aDGcBqK2gZ@o}ppPSy>`#csY;(^r|&!31?JW_TVhD_h2FyNmF z@|*Oz(os*P!Wrsn(^rQDxB;rqLCJ1}B~{raM621~2sR^jYsL&}BVgR5sJc0Jp{dO(=3f-YHqd?l+WC- z;ys6|F~kk3Y1-M4OC4iWgDc!}Cs~z*Ly*v)Xlh}a89^C>!1GCqC#n0&R->_hF%DkK z?3!{?ith=yc+gui7=pw?9sO)7sEiRP_S2_?14@Rmkp;D4)N%k-Mw}pnCGRN-b}m36 z5BJN7Hk+3Vu1Dr*x$+{}n38IwntjqQ@F@yjR?4MX2|ms%BA!V&LuK7L0;x1-n5597 z;Zggg)dJPm5^a#aRU>}fOC*s(=o5)9X{sZ)jkxA8(*SlmkFBY!^_k?XUuyjjGvn#y z=MD-Q$nU*)0se*$=tTsc-o&L^&LoCfF*CPi*vJ>SQ6DU@i3nlKgA(^v7-%fX zuo;$4Oy2?+VP*x%sI3ktl@{b?cN64N9pVsnKF$77Vy%hNgC%|_7eOOm(bVkgrX|Gp zo&;R$9Y$eM^fh>v;Tsd~ zL>(q(Bg)VDUC}><)pIR1r&&%uzW}WQ%NGQri>F{GYUn`a!GY?NULE%XzttY3onjvo zF@0v%UZwLx(pb?!7pOvdz}$mewIZ~6LuXn-PWc|j;u~R$tp*&ZJ`i2PlsuWLm1x74 zQ@|J2pZ7ygWP`J9#()Y0^kQx^{ktfK3uz&A9-BV~^K70;3c_8%*d@fxvj%WK!$T4g zL!Guh&vBc2+NIt0vI|%pHIzE{-6aiEX=+!ZOa@dv1eKJbgl45wOR}$OkMMX_Lv7J} zM7ezqHYMjMui4CazZ+=)Nhv=;qyM;Gj5BV1T#wYf+7{3m6F5XeZi*FpMX+0V*jfC5 z;rZnrol_21gE0;Z2~_b=)%KsKFbXSU%{7TFA8Om!i_2ftH}qIpIjJwF_$ytKQss!o zlHDc9Oh)K6JFlP?luNP?qT$I*HzTnWU}^ggr=61~(ZhMogwpqnmTQ`(8_$)Zw)5RF zsH(MFRs)0+NpmS+PPEb4$pnlbkN4{q+b`~PiwMN8K(Uis+tIIuWEs_4%M=&q0wUkk zl6s^!GraA<(#hs2{P%B^kTC&5^B^=lji9}^MgSy$6Nfqm=PS6TYhoE`tr3G59#f6O z{FFe9>jko4D1o}}>_Ti!K#<_*I(r=Q6|+R`s5r?0A}!V<-p^n2}gPA9y9Y6kDP^?p~J-AX*r zq8WAoeGKA%3NZzIUf>kp&Uu_-v_F!(lu0Et+Rp299^BZ;%FU-4Q&8rQtK&%4*`Y==s|SjO{>7|e^|)~_%BN$&&fZ>|M>pCm z1pZLl!cJr2UuGxx{=i|2^)~e~FynK_ZbD;;h0sLc1NwZul=bb z%?<-{D{fC2arh-PH7Aae+3Vnn%N`KV3J0V_GZ@4Wt!~(RugNO@tX54W>UAS!_QmAX zW8txnJ`uPYiXeCBj4Rr1F0w;O(KYcSC23U$v(F1OAn(WA5nTw!X`HhRh#*{{pYFdm zsq(HA-B6+v3uKFHM&qWI&L&9>+9FA995&+k8~}Ud2c^~|p6OM}pJJ@qm!3zzp;GSb z*c*&6bh1vqeC=Qa)N{4uc6RnnMKO*S<*85J>NnJJ;Xg{2TK3CLB42;Jgq!1L^q+Ck z@bgc8C@kEA8yr{0f8qPfMI06pkL&E=2gJ<^`%}r8IwtTV53%_>dNhzmkY;WnsDm8X z^Hjtt55T5q*K?z&?;*i*b(Jat*7lUt%Sl@6i+tuskc;SW&GH@pr`XFRXm%XV7vB!F)Kqr*PR0b*K54%n_7&<;Uvjln>Yk!RVZorL$RBq z%u^u9MfDT!hQ|L013T!^#Z?&3k;Qk?rX_r-DH~$WxRNeCIh9?`f*85i8he0`w}9^F zH^)L~zh;f)b7Sa_)J;iw*a|(jV!KbQuET9BOdlJOKlAn>phn25^9Z>U-LMml%h5TE zb1$-v^~OCMD6+WH)yI8~aUBSqI8BW8Fztavw~wL7leuO4=3AY9iAZt9{8$3cF4Kmm zmM!GRRER&IK>lg(X(!%J9#D2HIdthVrmQ9#a?IF?Ho3VcuBP`)P9i&cUe9B`jW8zj z@A@Tci~<2?mRl&rg_Xq4nLBXdlPYHTdUrp)Sfz@v{IGkqxse5)c86TBH)=MQZMr{q zJvm!9Sh`p{G>;0rvFVPlVaxDs^@)T0H3g;tI}duF-7-WwlxZnJf2KI4Q7 zJnMZ`_N_K1Ea2_5Wzu~NfQ#w0r>qyopKw4J7L8Ftnb@%0g|)`1+S#OyL34dcQ8#}Z z(?TbC<#-kPYbE!!tntl(eVtwxSbZ=&zHzB`L1~v+bWs_=sD*!TcLX_Stz}|4Z5{tJ z7-~UWyFFnGMicnR<`e0CZ!Q$unY>hLHR^vdB{ zaWN5EZ_{CM55Vi9imei6y|qlds8L@0PYRBG^VPL*%SZhr<(NkNBm8GhRC~q~0DzNw z1GpZOR&VTMp#OE2YIkD&*)7$!gB{MIc&n$iafD9*lw7j<4M+8Gd=qO)9k#PbW<7dh zP_0tGKB=Zo^gR{t|7&G67F>WT;BM6NPsy*&*L7w!2{$TxdCf!xS;d>`dVRK&SA|HP zEQZDqiSGjqFlX%W7_YZ%Xb-KqEpj@CYdS&lvi+$4+bp$9kl`hPXOLeM|3){-ImrL4 zU))mo=-TbiYL`O|t|mOtw8VvF%9u=A_JhJhHPP$r02Dd8@#5S^W?Djbu8z5~U$lGd z*E)D=GXXlLx>2P$=GxV-&hz_{=PDk@9!0kJLEgYL1ExJFl zmxi4EN|U%HQQ5peh^HMk>l-><7nUNLp1x^2R1vzH;GB0B_}pptz=eL`kGI491{mS*qqUxu}ymGfw1=jVFWcd9imi*VOC+3w&$8Uz+zvmuQGvwaTHnw6(z7xz< zV`ROCuK&WF0;JS44EL!$CZx=4W@R|T<2?{DdZ^nA17Mmy0I7=(Qpzjc+)2ek?HnFG-;jp(?ifpUxiWar$NyBR zd2@8dH5Wlkp)DfL966M=ro*H8n@W)oGRd8j#Pz%CnLj%4KqzKFRC7FC2E!QTYf ziRa!e*PYE{7pA-B%0$;r89ld*o_x;jKLx;CiS7!olJ^VfI5#pPfj!SEGvWv9PkcA= z{lxmtGnC2mSCx#x{^(jeC|eTpzF}f??F{Q6^%~m1A6XClQU4!*gn}_;CQ8_+p#xI+ a>6ba?5NxI$@Cm$v^KsW>wZB}v^*;c>f-J)T diff --git a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_slow_mode.png b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_slow_mode.png new file mode 100644 index 0000000000000000000000000000000000000000..4e653ec9c7be1aa5f16a155415e6ee5661a6adad GIT binary patch literal 17494 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 LTrInDGxC1`w)APS literal 0 HcmV?d00001 diff --git a/stream-chat-android-ui-common/src/main/res/values-es/strings.xml b/stream-chat-android-ui-common/src/main/res/values-es/strings.xml index 375e5188f4c..76009e1f657 100644 --- a/stream-chat-android-ui-common/src/main/res/values-es/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values-es/strings.xml @@ -132,6 +132,7 @@ "\@username" "\@username" "Enviar un mensaje" + "Modo lento, espera %1$d s…" "Error. El fichero no se puede mostrar" "Error al descargar el archivo: %s" "Descargando… %1$s / %2$s" diff --git a/stream-chat-android-ui-common/src/main/res/values-fr/strings.xml b/stream-chat-android-ui-common/src/main/res/values-fr/strings.xml index 880a46ecba4..ad724b60b5e 100644 --- a/stream-chat-android-ui-common/src/main/res/values-fr/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values-fr/strings.xml @@ -132,6 +132,7 @@ "\@username" "\@username" "Envoyer un message" + "Mode lent, patientez %1$d s…" "Erreur. Le fichier ne peut pas être affiché" "Échec du téléchargement du fichier : %s" "Téléchargement… %1$s / %2$s" diff --git a/stream-chat-android-ui-common/src/main/res/values-hi/strings.xml b/stream-chat-android-ui-common/src/main/res/values-hi/strings.xml index bb9c875c9fd..436cf96530e 100644 --- a/stream-chat-android-ui-common/src/main/res/values-hi/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values-hi/strings.xml @@ -132,6 +132,7 @@ \@username \@username "संदेश भेजें" + "धीमा मोड, %1$d सेकंड प्रतीक्षा करें…" "त्रुटि। फ़ाइल प्रदर्शित नहीं की जा सकती" "फ़ाइल डाउनलोड करने में विफल: %s" "डाउनलोड हो रहा है… %1$s / %2$s" diff --git a/stream-chat-android-ui-common/src/main/res/values-in/strings.xml b/stream-chat-android-ui-common/src/main/res/values-in/strings.xml index ab9385658ca..70a32fec08a 100644 --- a/stream-chat-android-ui-common/src/main/res/values-in/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values-in/strings.xml @@ -131,6 +131,7 @@ "\@username" "\@username" "Kirim pesan" + "Mode lambat, tunggu %1$d dtk…" "Terjadi kesalahan. Berkas tidak dapat ditampilkan" "Gagal mengunduh file: %s" "Mengunduh… %1$s / %2$s" diff --git a/stream-chat-android-ui-common/src/main/res/values-it/strings.xml b/stream-chat-android-ui-common/src/main/res/values-it/strings.xml index 06464835d11..40f211305fa 100644 --- a/stream-chat-android-ui-common/src/main/res/values-it/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values-it/strings.xml @@ -132,6 +132,7 @@ \@username \@username "Invia un messaggio" + "Modalità lenta, attendi %1$d s…" "Errore. Impossibile visualizzare il file" "Impossibile scaricare il file: %s" "Download in corso… %1$s / %2$s" diff --git a/stream-chat-android-ui-common/src/main/res/values-ja/strings.xml b/stream-chat-android-ui-common/src/main/res/values-ja/strings.xml index 97192e1157c..f19166cc686 100644 --- a/stream-chat-android-ui-common/src/main/res/values-ja/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values-ja/strings.xml @@ -131,6 +131,7 @@ \@username \@username "メッセージを送信" + "スローモード、あと%1$d秒…" "エラー。ファイルを表示できません" "ファイルのダウンロードに失敗しました:%s" "ダウンロード中… %1$s / %2$s" diff --git a/stream-chat-android-ui-common/src/main/res/values-ko/strings.xml b/stream-chat-android-ui-common/src/main/res/values-ko/strings.xml index 02d871d470e..0ef38b339c7 100644 --- a/stream-chat-android-ui-common/src/main/res/values-ko/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values-ko/strings.xml @@ -131,6 +131,7 @@ \@username \@username "메시지 보내기" + "슬로우 모드, %1$d초 기다리세요…" "오류. 파일을 표시할 수 없습니다" "파일 다운로드에 실패했습니다: %s" "다운로드 중… %1$s / %2$s" diff --git a/stream-chat-android-ui-common/src/main/res/values/strings.xml b/stream-chat-android-ui-common/src/main/res/values/strings.xml index b4d68c96848..994a2d8e507 100644 --- a/stream-chat-android-ui-common/src/main/res/values/strings.xml +++ b/stream-chat-android-ui-common/src/main/res/values/strings.xml @@ -212,6 +212,7 @@ Send a message You can\'t send messages in this channel + Slow mode, wait %1$ds… Search GIFs \@username \@username From 8fc519fbd6089bbe86ed3a38fa31e0e46657df8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Mion?= Date: Fri, 12 Jun 2026 10:33:53 +0100 Subject: [PATCH 2/3] compose: Add slow mode e2e tests Add an instrumented slow-mode suite to the compose sample, mirroring the iOS SlowMode_Tests. BackendRobot.setCooldown drives the shared mock server (config/cooldown), and the new UserRobot assertions check the cooldown indicator, the hidden send button, and the disabled input and attachment button. CoolDownIndicator gets a testTag so the e2e layer can locate it. --- .../android/compose/pages/MessageListPage.kt | 1 + .../robots/UserRobotMessageListAsserts.kt | 18 +++++ .../android/compose/tests/SlowModeTests.kt | 65 +++++++++++++++++++ .../components/composer/CoolDownIndicator.kt | 2 + .../android/e2e/test/robots/BackendRobot.kt | 6 ++ 5 files changed, 92 insertions(+) create mode 100644 stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/SlowModeTests.kt diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt index 13a9575241b..1a1b1907fd9 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/pages/MessageListPage.kt @@ -62,6 +62,7 @@ open class MessageListPage { companion object { val inputField get() = By.res("Stream_ComposerInputField") val sendButton get() = By.res("Stream_ComposerSendButton") + val cooldownIndicator get() = By.res("Stream_ComposerCooldownIndicator") val saveButton get() = By.res("Stream_ComposerSaveButton") val recordAudioButton get() = By.res("Stream_ComposerAudioRecordingButton") val commandsButton get() = By.res("Stream_ComposerCommandsButton") diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt index be888f50503..7f92ca48985 100644 --- a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/robots/UserRobotMessageListAsserts.kt @@ -29,6 +29,7 @@ import io.getstream.chat.android.compose.uiautomator.findObject import io.getstream.chat.android.compose.uiautomator.findObjects import io.getstream.chat.android.compose.uiautomator.height import io.getstream.chat.android.compose.uiautomator.isDisplayed +import io.getstream.chat.android.compose.uiautomator.isEnabled import io.getstream.chat.android.compose.uiautomator.retryOnStaleObjectException import io.getstream.chat.android.compose.uiautomator.seconds import io.getstream.chat.android.compose.uiautomator.wait @@ -239,6 +240,23 @@ fun UserRobot.assertComposerText(expectedText: String): UserRobot { return this } +fun UserRobot.assertCooldownIsShown(): UserRobot { + assertTrue(Composer.cooldownIndicator.waitToAppear().isDisplayed()) + assertFalse(Composer.sendButton.isDisplayed()) + return this +} + +fun UserRobot.assertCooldownIsNotShown(): UserRobot { + assertFalse(Composer.cooldownIndicator.waitToDisappear().isDisplayed()) + return this +} + +fun UserRobot.assertComposerIsDisabledInSlowMode(): UserRobot { + assertFalse(Composer.inputField.isEnabled()) + assertFalse(Composer.attachmentsButton.isEnabled()) + return this +} + fun UserRobot.assertScrollToBottomButton(isDisplayed: Boolean): UserRobot { if (isDisplayed) { assertTrue(MessageListPage.MessageList.scrollToBottomButton.waitToAppear().isDisplayed()) diff --git a/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/SlowModeTests.kt b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/SlowModeTests.kt new file mode 100644 index 00000000000..bd76a8283e9 --- /dev/null +++ b/stream-chat-android-compose-sample/src/androidTestE2eDebug/kotlin/io/getstream/chat/android/compose/tests/SlowModeTests.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2014-2026 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.tests + +import io.getstream.chat.android.compose.robots.assertComposerIsDisabledInSlowMode +import io.getstream.chat.android.compose.robots.assertCooldownIsShown +import io.getstream.chat.android.compose.sample.ui.InitTestActivity +import io.qameta.allure.kotlin.Allure.step +import org.junit.Test + +class SlowModeTests : StreamTestCase() { + + override fun initTestActivity() = InitTestActivity.UserLogin + + private val cooldownDuration = 15 + private val message = "message" + + @Test + fun test_cooldownIsShownWhenNewMessageIsSent() { + step("GIVEN slow mode is enabled on the channel") { + backendRobot.setCooldown(enabled = true, duration = cooldownDuration) + } + step("AND user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends a new message") { + userRobot.sendMessage(message) + } + step("THEN slow mode is active and the cooldown is shown") { + userRobot.assertCooldownIsShown() + } + } + + @Test + fun test_composerIsDisabledWhenSlowModeIsActive() { + step("GIVEN slow mode is enabled on the channel") { + backendRobot.setCooldown(enabled = true, duration = cooldownDuration) + } + step("AND user opens the channel") { + userRobot.login().openChannel() + } + step("WHEN user sends a new message") { + userRobot.sendMessage(message) + } + step("THEN the cooldown is shown and the composer is locked") { + userRobot + .assertCooldownIsShown() + .assertComposerIsDisabledInSlowMode() + } + } +} diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/CoolDownIndicator.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/CoolDownIndicator.kt index 93f98511a33..134a518d85d 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/CoolDownIndicator.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/composer/CoolDownIndicator.kt @@ -26,6 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.testTag import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import io.getstream.chat.android.compose.ui.theme.ChatTheme @@ -43,6 +44,7 @@ public fun CoolDownIndicator( ) { Box( modifier = modifier + .testTag("Stream_ComposerCooldownIndicator") .size(48.dp) .padding(8.dp) .background(shape = RoundedCornerShape(24.dp), color = ChatTheme.colors.textDisabled), diff --git a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt index 154d038c31f..d1d052a3b03 100644 --- a/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt +++ b/stream-chat-android-e2e-test/src/main/kotlin/io/getstream/chat/android/e2e/test/robots/BackendRobot.kt @@ -56,6 +56,12 @@ public class BackendRobot( return this } + public fun setCooldown(enabled: Boolean, duration: Int): BackendRobot { + waitForMockServerToStart() + mockServer.postRequest("config/cooldown?enabled=$enabled&duration=$duration") + return this + } + public fun revokeToken(duration: Int = 5) { waitForMockServerToStart() mockServer.postRequest("jwt/revoke_token?duration=$duration") From 9bfa16e7be980e2e7c8827ecbdf69965f48c7d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Mion?= Date: Fri, 12 Jun 2026 12:07:39 +0100 Subject: [PATCH 3/3] composer: Activate slow mode without requiring the slow-mode capability The composer gated slow mode on the channel's slow-mode own-capability. The backend grants that capability only when cooldown > 0 and the user cannot skip slow mode, so checking those two conditions directly is equivalent today and keeps the client correct if the backend later changes how the capability is granted, without needing a client release. This also aligns the behaviour with the iOS and Flutter SDKs, which do not require the capability either. --- .../composer/MessageComposerController.kt | 21 ++------ .../composer/MessageComposerControllerTest.kt | 50 +++++++++++++++++++ 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt index ef95a92f87a..86954c367b4 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerController.kt @@ -206,22 +206,6 @@ public class MessageComposerController( initialValue = false, ) - /** - * Signals if the user needs to wait before sending the next message. - * - * Depending on roles & permissions setup in the dashboard, some user groups are allowed - * to send messages instantly even if the slow mode is enabled for the channel. - * - * [SharingStarted.Eagerly] because this [StateFlow] has no collectors, its value is only - * ever read directly. - */ - private val isSlowModeActive = ownCapabilities.map { it.contains(ChannelCapabilities.SLOW_MODE) } - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = false, - ) - /** * Signals whether slow-mode should be ignored (even if it's active). * @@ -1335,7 +1319,10 @@ public class MessageComposerController( * @param lastSentMessageDate The date of the last message. */ private fun handleLastSentMessageDate(cooldownInterval: Int, lastSentMessageDate: Date?) { - val isSlowModeActive = cooldownInterval > 0 && isSlowModeActive.value && !isSlowModeDisabled.value + // Not gated on the SLOW_MODE capability on purpose: the backend grants it only when + // cooldown > 0 and the user cannot skip slow mode, so these two checks stay equivalent + // and keep working if the backend later changes how that capability is granted. + val isSlowModeActive = cooldownInterval > 0 && !isSlowModeDisabled.value if (isSlowModeActive && lastSentMessageDate != null && !isInEditMode) { // Time passed since the last message was successfully sent to the server diff --git a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt index 9d4f7c79bf1..eb5789e81c1 100644 --- a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt +++ b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/feature/messages/composer/MessageComposerControllerTest.kt @@ -25,6 +25,7 @@ import io.getstream.chat.android.client.channel.state.ChannelState import io.getstream.chat.android.client.setup.state.ClientState import io.getstream.chat.android.models.App import io.getstream.chat.android.models.AppSettings +import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.ChannelData import io.getstream.chat.android.models.Command import io.getstream.chat.android.models.Config @@ -158,6 +159,55 @@ internal class MessageComposerControllerTest { assertTrue(controller.state.value.pollsEnabled) } + @Test + fun `slow mode cooldown activates without the slow-mode capability`() = runTest { + // given a channel with a cooldown but without the slow-mode own-capability + val channelData = MutableStateFlow( + ChannelData(id = CHANNEL_ID, type = CHANNEL_TYPE, cooldown = 10, ownCapabilities = emptySet()), + ) + // when the current user has just sent a message + val controller = Fixture() + .givenAppSettings(mock()) + .givenAudioPlayer(mock()) + .givenClientState(User("uid1")) + .givenGlobalState() + .givenInheritedScope(testCoroutines.scope) + .givenChannelState( + channelDataState = channelData, + lastSentMessageDateState = MutableStateFlow(Date()), + ) + .get() + // then slow mode is active even though the slow-mode capability is absent + assertTrue(controller.state.value.coolDownTime > 0) + } + + @Test + fun `slow mode cooldown stays off when the user can skip slow mode`() = runTest { + // given a channel with a cooldown and the skip-slow-mode capability + val channelData = MutableStateFlow( + ChannelData( + id = CHANNEL_ID, + type = CHANNEL_TYPE, + cooldown = 10, + ownCapabilities = setOf(ChannelCapabilities.SKIP_SLOW_MODE), + ), + ) + // when the current user has just sent a message + val controller = Fixture() + .givenAppSettings(mock()) + .givenAudioPlayer(mock()) + .givenClientState(User("uid1")) + .givenGlobalState() + .givenInheritedScope(testCoroutines.scope) + .givenChannelState( + channelDataState = channelData, + lastSentMessageDateState = MutableStateFlow(Date()), + ) + .get() + // then slow mode is not active + assertEquals(0, controller.state.value.coolDownTime) + } + @Test fun `Given user mention When selectMention called Then message input is autocompleted with user name`() = runTest { // Given