diff --git a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt index 513ac73be6..bd8fc40b19 100644 --- a/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt +++ b/core-ui/src/main/java/com/anytypeio/anytype/core_ui/foundation/Foundation.kt @@ -23,11 +23,12 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.anytypeio.anytype.core_models.membership.Membership.Status +import com.anytypeio.anytype.core_models.membership.MembershipStatus import com.anytypeio.anytype.core_ui.R +import com.anytypeio.anytype.core_ui.common.DefaultPreviews import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular import com.anytypeio.anytype.core_ui.views.BodyRegular import com.anytypeio.anytype.core_ui.views.ButtonPrimary @@ -36,7 +37,6 @@ import com.anytypeio.anytype.core_ui.views.ButtonSize import com.anytypeio.anytype.core_ui.views.ButtonWarningLoading import com.anytypeio.anytype.core_ui.views.HeadlineHeading import com.anytypeio.anytype.core_ui.views.Title1 -import com.anytypeio.anytype.core_models.membership.MembershipStatus @Composable fun Toolbar( @@ -344,7 +344,7 @@ fun Announcement( } } -@Preview +@DefaultPreviews @Composable fun WarningPreview() { Warning( @@ -357,7 +357,7 @@ fun WarningPreview() { ) } -@Preview +@DefaultPreviews @Composable fun AnnouncementPreview() { Announcement( diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt index 3acbfc654d..95abfebd5a 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/ChatViewModel.kt @@ -107,6 +107,7 @@ class ChatViewModel @Inject constructor( val showNotificationPermissionDialog = MutableStateFlow(false) private val dateFormatter = SimpleDateFormat("d MMMM YYYY") + private val messageRateLimiter = MessageRateLimiter() private var account: Id = "" @@ -266,6 +267,8 @@ class ChatViewModel @Inject constructor( isUserAuthor = msg.creator == account, isEdited = msg.modifiedAt > msg.createdAt, reactions = msg.reactions + .toList() + .sortedByDescending { (emoji, ids) -> ids.size } .map { (emoji, ids) -> ChatView.Message.Reaction( emoji = emoji, @@ -625,6 +628,10 @@ class ChatViewModel @Inject constructor( when (val mode = chatBoxMode.value) { is ChatBoxMode.Default -> { // TODO consider moving this use-case inside chat container + if (messageRateLimiter.shouldShowRateLimitWarning()) { + uXCommands.emit(UXCommand.ShowRateLimitWarning) + } + addChatMessage.async( params = Command.ChatCommand.AddMessage( chat = vmParams.ctx, @@ -1179,6 +1186,7 @@ class ChatViewModel @Inject constructor( data object JumpToBottom : UXCommand() data class SetChatBoxInput(val input: String) : UXCommand() data class OpenFullScreenImage(val url: String) : UXCommand() + data object ShowRateLimitWarning: UXCommand() } sealed class ChatBoxMode { diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/MessageRateLimiter.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/MessageRateLimiter.kt new file mode 100644 index 0000000000..6524b9971d --- /dev/null +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/presentation/MessageRateLimiter.kt @@ -0,0 +1,27 @@ +package com.anytypeio.anytype.feature_chats.presentation + +class MessageRateLimiter( + private val maxMessages: Int = DEFAULT_MAX_MESSAGES, + private val timeWindowSeconds: Int = DEFAULT_TIME_WINDOW_SECONDS +) { + private val messageTimestamps = mutableListOf() + + fun shouldShowRateLimitWarning(): Boolean { + val currentTime = System.currentTimeMillis() + val windowStart = currentTime - (timeWindowSeconds * 1000) + + // Remove timestamps outside the current window + messageTimestamps.removeAll { it < windowStart } + + // Add current message timestamp + messageTimestamps.add(currentTime) + + // Return true if we've exceeded the rate limit + return messageTimestamps.size > maxMessages + } + + companion object { + private const val DEFAULT_MAX_MESSAGES = 5 + private const val DEFAULT_TIME_WINDOW_SECONDS = 5 + } +} \ No newline at end of file diff --git a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt index 1e3f16a892..1f59e3d05e 100644 --- a/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt +++ b/feature-chats/src/main/java/com/anytypeio/anytype/feature_chats/ui/ChatScreen.kt @@ -69,8 +69,11 @@ import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.Url import com.anytypeio.anytype.core_ui.foundation.AlertConfig import com.anytypeio.anytype.core_ui.foundation.AlertIcon +import com.anytypeio.anytype.core_ui.foundation.BUTTON_SECONDARY import com.anytypeio.anytype.core_ui.foundation.Divider import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_BLUE +import com.anytypeio.anytype.core_ui.foundation.GRADIENT_TYPE_RED +import com.anytypeio.anytype.core_ui.foundation.GenericAlert import com.anytypeio.anytype.core_ui.foundation.noRippleClickable import com.anytypeio.anytype.core_ui.views.Caption1Medium import com.anytypeio.anytype.core_ui.views.Caption1Regular @@ -108,6 +111,7 @@ fun ChatScreenWrapper( ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) var showReactionSheet by remember { mutableStateOf(false) } + var showSendRateLimitWarning by remember { mutableStateOf(false) } val context = LocalContext.current Box( modifier = modifier.fillMaxSize() @@ -245,6 +249,9 @@ fun ChatScreenWrapper( is UXCommand.OpenFullScreenImage -> { onRequestOpenFullScreenImage(command.url) } + is UXCommand.ShowRateLimitWarning -> { + showSendRateLimitWarning = true + } } } } @@ -264,6 +271,34 @@ fun ChatScreenWrapper( ) } } + + if (showSendRateLimitWarning) { + ModalBottomSheet( + onDismissRequest = { + showSendRateLimitWarning = false + }, + sheetState = sheetState, + containerColor = colorResource(id = R.color.background_secondary), + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + dragHandle = null + ) { + GenericAlert( + config = AlertConfig.WithOneButton( + title = stringResource(R.string.chat_send_message_rate_limit_title), + firstButtonText = stringResource(id = R.string.button_okay), + firstButtonType = BUTTON_SECONDARY, + description = stringResource(R.string.chat_send_message_rate_limit_desc), + icon = AlertConfig.Icon( + gradient = GRADIENT_TYPE_RED, + icon = R.drawable.ic_alert_message + ) + ), + onFirstButtonClicked = { + showSendRateLimitWarning = false + } + ) + } + } } @Composable diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index 0d0118c3a9..84ae133434 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -1877,6 +1877,9 @@ Please provide specific details of your needs here. Reply Add Reaction + Hold up! Turbo typing detected! + Looks like you\'re sending messages at lightning speed. Give it a sec before your next one. + It’s empty here. Create your first objects to get started. Delete completely