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/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api
index 3e8235e73af..86dd403d9e4 100644
--- a/stream-chat-android-compose/api/stream-chat-android-compose.api
+++ b/stream-chat-android-compose/api/stream-chat-android-compose.api
@@ -2010,6 +2010,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/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-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 6fb67eab144..a1ef9d8df93 100644
Binary files a/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerInputTest_slow_mode.png and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerInputTest_slow_mode.png differ
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 00000000000..4e653ec9c7b
Binary files /dev/null and b/stream-chat-android-compose/src/test/snapshots/images/io.getstream.chat.android.compose.ui.messages_MessageComposerTest_slow_mode.png differ
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")
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/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
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