diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index 6f64febfadb..672d00f13e6 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt @@ -267,18 +267,17 @@ public fun MessageItem( val messageAlignment = ChatTheme.messageAlignmentProvider.provideMessageAlignment(messageItem) val description = stringResource(id = R.string.stream_compose_cd_message_item) - val isSwipable = ChatTheme.messageOptionsTheme.optionVisibility - .canReplyToMessage( - message = message, - ownCapabilities = messageItem.ownCapabilities, - ) + val optionVisibility = ChatTheme.messageOptionsTheme.optionVisibility + val isSwipeable = remember(message, messageItem.ownCapabilities, optionVisibility) { + optionVisibility.canReplyToMessage(message, messageItem.ownCapabilities) + } // Remember the message to ensure updated values are captured in the onReply lambda val replyMessage by rememberUpdatedState(message) SwipeToReply( modifier = modifier, onReply = { onReply(replyMessage) }, - isSwipeable = { isSwipable }, + isSwipeable = isSwipeable, swipeToReplyContent = swipeToReplyContent, ) { Box( @@ -735,7 +734,7 @@ private fun getMessageBubbleShape(position: List, ownsMessage: * * @param modifier Modifier for styling. * @param onReply Handler when the user swipes to reply. - * @param isSwipeable Handler to determine if the message is swipeable. + * @param isSwipeable Indicator if swipe-to-reply is enabled. * @param swipeToReplyContent The content to show when swiping to reply. * @param content The swipeable content to show when not swiping to reply. */ @@ -744,7 +743,7 @@ private fun getMessageBubbleShape(position: List, ownsMessage: private fun SwipeToReply( modifier: Modifier = Modifier, onReply: () -> Unit = {}, - isSwipeable: () -> Boolean = { true }, + isSwipeable: Boolean = true, swipeToReplyContent: @Composable RowScope.() -> Unit, content: @Composable () -> Unit, ) { @@ -779,8 +778,8 @@ private fun SwipeToReply( .fillMaxWidth() .onSizeChanged { rowWidth = it.width.toFloat() } .offset { IntOffset(x = offset.value.roundToInt(), y = 0) } - .pointerInput(swipeToReplyWidth) { - if (isSwipeable()) { + .pointerInput(swipeToReplyWidth, isSwipeable) { + if (isSwipeable) { detectHorizontalDragGestures( onHorizontalDrag = { change, dragAmount -> // Only consume if horizontal drag dominates vertical diff --git a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibilityTest.kt b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibilityTest.kt index efb7696a179..33db376c5e6 100644 --- a/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibilityTest.kt +++ b/stream-chat-android-compose/src/test/kotlin/io/getstream/chat/android/compose/util/extensions/MessageOptionItemVisibilityTest.kt @@ -32,6 +32,7 @@ import org.amshove.kluent.`should be` import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource +import java.util.Date internal class MessageOptionItemVisibilityTest { @@ -157,27 +158,38 @@ internal class MessageOptionItemVisibilityTest { @JvmStatic fun canReplyToMessageArguments() = listOf( + // case: reply disabled Arguments.of( MessageOptionItemVisibility(isReplyVisible = false), - randomMessage(), + randomMessage(deletedAt = null, deletedForMe = false), randomChannelCapabilities(), false, ), + // case: message not synced Arguments.of( MessageOptionItemVisibility(), randomMessage(syncStatus = randomSyncStatus(exclude = listOf(SyncStatus.COMPLETED))), randomChannelCapabilities(), false, ), + // case: no QUOTE_MESSAGE capability Arguments.of( MessageOptionItemVisibility(), - randomMessage(), + randomMessage(deletedAt = null, deletedForMe = false), randomChannelCapabilities(exclude = setOf(ChannelCapabilities.QUOTE_MESSAGE)), false, ), + // case: message is deleted Arguments.of( MessageOptionItemVisibility(isReplyVisible = true), - randomMessage(syncStatus = SyncStatus.COMPLETED), + randomMessage(syncStatus = SyncStatus.COMPLETED, deletedAt = Date(), deletedForMe = false), + randomChannelCapabilities(include = setOf(ChannelCapabilities.QUOTE_MESSAGE)), + false, + ), + // case: all conditions met + Arguments.of( + MessageOptionItemVisibility(isReplyVisible = true), + randomMessage(syncStatus = SyncStatus.COMPLETED, deletedAt = null, deletedForMe = false), randomChannelCapabilities(include = setOf(ChannelCapabilities.QUOTE_MESSAGE)), true, ), diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelper.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelper.kt index d0d7d2cba90..0851b0f6b40 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelper.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelper.kt @@ -20,6 +20,7 @@ package io.getstream.chat.android.ui.common.utils import io.getstream.chat.android.client.utils.attachment.isGiphy import io.getstream.chat.android.client.utils.message.hasSharedLocation +import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.client.utils.message.isThreadReply import io.getstream.chat.android.models.AttachmentType import io.getstream.chat.android.models.ChannelCapabilities @@ -59,7 +60,10 @@ public fun canReplyToMessage( replyEnabled: Boolean, message: Message, ownCapabilities: Set, -): Boolean = replyEnabled && message.isSynced() && ownCapabilities.contains(ChannelCapabilities.QUOTE_MESSAGE) +): Boolean = replyEnabled && + message.isSynced() && + !message.isDeleted() && + ownCapabilities.contains(ChannelCapabilities.QUOTE_MESSAGE) /** * Determines whether a thread reply can be made to the given message. diff --git a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelperTest.kt b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelperTest.kt index 736843285d8..d72b6e7de7c 100644 --- a/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelperTest.kt +++ b/stream-chat-android-ui-common/src/test/kotlin/io/getstream/chat/android/ui/common/utils/CapabilitiesHelperTest.kt @@ -31,6 +31,7 @@ import org.amshove.kluent.`should be` import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource +import java.util.Date internal class CapabilitiesHelperTest { @@ -152,27 +153,38 @@ internal class CapabilitiesHelperTest { @JvmStatic fun canReplyToMessageArguments() = listOf( + // case: reply disabled Arguments.of( false, randomMessage(), randomChannelCapabilities(), false, ), + // case: message not synced Arguments.of( randomBoolean(), randomMessage(syncStatus = randomSyncStatus(exclude = listOf(SyncStatus.COMPLETED))), randomChannelCapabilities(), false, ), + // case: no QUOTE_MESSAGE capability Arguments.of( randomBoolean(), - randomMessage(), + randomMessage(deletedAt = null, deletedForMe = false), randomChannelCapabilities(exclude = setOf(ChannelCapabilities.QUOTE_MESSAGE)), false, ), + // case: message is deleted Arguments.of( true, - randomMessage(syncStatus = SyncStatus.COMPLETED), + randomMessage(syncStatus = SyncStatus.COMPLETED, deletedAt = Date(), deletedForMe = false), + randomChannelCapabilities(include = setOf(ChannelCapabilities.QUOTE_MESSAGE)), + false, + ), + // case: all conditions met + Arguments.of( + true, + randomMessage(syncStatus = SyncStatus.COMPLETED, deletedAt = null, deletedForMe = false), randomChannelCapabilities(include = setOf(ChannelCapabilities.QUOTE_MESSAGE)), true, ), diff --git a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/MessageListViewExtensionsKtTest.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/MessageListViewExtensionsKtTest.kt index a18a9b8ad72..e3518973c30 100644 --- a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/MessageListViewExtensionsKtTest.kt +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/MessageListViewExtensionsKtTest.kt @@ -33,6 +33,7 @@ import org.amshove.kluent.`should be` import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource +import java.util.Date internal class MessageListViewExtensionsKtTest { @@ -154,27 +155,42 @@ internal class MessageListViewExtensionsKtTest { @JvmStatic fun canReplyToMessageArguments() = listOf( + // case: reply disabled Arguments.of( randomMessageListViewStyle(replyEnabled = false), - randomMessage(), + randomMessage(deletedAt = null, deletedForMe = false), randomChannelCapabilities(), false, ), + // case: message not synced Arguments.of( randomMessageListViewStyle(), - randomMessage(syncStatus = randomSyncStatus(exclude = listOf(SyncStatus.COMPLETED))), + randomMessage( + syncStatus = randomSyncStatus(exclude = listOf(SyncStatus.COMPLETED)), + deletedAt = null, + deletedForMe = false, + ), randomChannelCapabilities(), false, ), + // case: no QUOTE_MESSAGE capability Arguments.of( randomMessageListViewStyle(), - randomMessage(), + randomMessage(deletedAt = null, deletedForMe = false), randomChannelCapabilities(exclude = setOf(ChannelCapabilities.QUOTE_MESSAGE)), false, ), + // case: message is deleted Arguments.of( randomMessageListViewStyle(replyEnabled = true), - randomMessage(syncStatus = SyncStatus.COMPLETED), + randomMessage(syncStatus = SyncStatus.COMPLETED, deletedAt = Date(), deletedForMe = false), + randomChannelCapabilities(include = setOf(ChannelCapabilities.QUOTE_MESSAGE)), + false, + ), + // case: all conditions met + Arguments.of( + randomMessageListViewStyle(replyEnabled = true), + randomMessage(syncStatus = SyncStatus.COMPLETED, deletedAt = null, deletedForMe = false), randomChannelCapabilities(include = setOf(ChannelCapabilities.QUOTE_MESSAGE)), true, ),