diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d652bfbecb..5e26753109e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,7 @@ ### ⬆️ Improved ### ✅ Added +- Added Swipe To Reply feature to the MessageListView. [#5626](https://github.com/GetStream/stream-chat-android/pull/5626) ### ⚠️ Changed diff --git a/stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt b/stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt index 389fc25806d..9a376bcd688 100644 --- a/stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt +++ b/stream-chat-android-core/src/testFixtures/kotlin/io/getstream/chat/android/Mother.kt @@ -19,6 +19,7 @@ package io.getstream.chat.android import io.getstream.chat.android.models.Attachment import io.getstream.chat.android.models.AttachmentType import io.getstream.chat.android.models.Channel +import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.ChannelConfig import io.getstream.chat.android.models.ChannelInfo import io.getstream.chat.android.models.ChannelMute @@ -49,6 +50,7 @@ public fun positiveRandomInt(maxInt: Int = Int.MAX_VALUE - 1): Int = public fun positiveRandomLong(maxLong: Long = Long.MAX_VALUE - 1): Long = Random.nextLong(1, maxLong + 1) +public fun randomFloat(): Float = Random.nextFloat() public fun randomInt(): Int = Random.nextInt() public fun randomIntBetween(min: Int, max: Int): Int = Random.nextInt(min, max + 1) public fun randomLong(): Long = Random.nextLong() @@ -560,3 +562,48 @@ public fun randomReactionGroup( firstReactionAt = firstReactionAt, lastReactionAt = lastReactionAt, ) + +public fun allChannelCapabilities(): Set = setOf( + ChannelCapabilities.BAN_CHANNEL_MEMBERS, + ChannelCapabilities.CONNECT_EVENTS, + ChannelCapabilities.DELETE_ANY_MESSAGE, + ChannelCapabilities.DELETE_CHANNEL, + ChannelCapabilities.DELETE_OWN_MESSAGE, + ChannelCapabilities.FLAG_MESSAGE, + ChannelCapabilities.FREEZE_CHANNEL, + ChannelCapabilities.LEAVE_CHANNEL, + ChannelCapabilities.JOIN_CHANNEL, + ChannelCapabilities.MUTE_CHANNEL, + ChannelCapabilities.PIN_MESSAGE, + ChannelCapabilities.QUOTE_MESSAGE, + ChannelCapabilities.READ_EVENTS, + ChannelCapabilities.SEARCH_MESSAGES, + ChannelCapabilities.SEND_CUSTOM_EVENTS, + ChannelCapabilities.SEND_LINKS, + ChannelCapabilities.SEND_MESSAGE, + ChannelCapabilities.SEND_REACTION, + ChannelCapabilities.SEND_REPLY, + ChannelCapabilities.SET_CHANNEL_COOLDOWN, + ChannelCapabilities.UPDATE_ANY_MESSAGE, + ChannelCapabilities.UPDATE_CHANNEL, + ChannelCapabilities.UPDATE_CHANNEL_MEMBERS, + ChannelCapabilities.UPDATE_OWN_MESSAGE, + ChannelCapabilities.UPLOAD_FILE, + ChannelCapabilities.TYPING_EVENTS, + ChannelCapabilities.SLOW_MODE, + ChannelCapabilities.SKIP_SLOW_MODE, + ChannelCapabilities.JOIN_CALL, + ChannelCapabilities.CREATE_CALL, + ChannelCapabilities.CAST_POLL_VOTE, + ChannelCapabilities.SEND_POLL, +) + +public fun randomChannelCapabilities( + exclude: Set = emptySet(), + include: Set = emptySet(), +): Set = + allChannelCapabilities() + .minus(exclude) + .shuffled() + .let { it.take(positiveRandomInt(it.size)) } + .toSet() + include 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 4d2acaf7ad1..ea8c19bb0fd 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 @@ -504,7 +504,7 @@ public class MessageComposerController( setMessageMode(MessageMode.MessageThread(messageAction.message)) } is Reply -> { - messageActions.value = messageActions.value + messageAction + messageActions.value = (messageActions.value.filterNot { it is Reply } + messageAction).toSet() } is Edit -> { setMessageInputInternal(messageAction.message.text, MessageInput.Source.Edit) diff --git a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api index 91c256f1f1b..1d3b5bdb12d 100644 --- a/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api +++ b/stream-chat-android-ui-components/api/stream-chat-android-ui-components.api @@ -2542,7 +2542,7 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public final class io/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle : io/getstream/chat/android/ui/helper/ViewStyle { public static final field Companion Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle$Companion; - public fun (Lio/getstream/chat/android/ui/feature/messages/list/ScrollButtonViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$NewMessagesBehaviour;Lio/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle;Lio/getstream/chat/android/ui/feature/messages/list/GiphyViewHolderStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle;Lio/getstream/chat/android/ui/feature/messages/list/UnreadLabelButtonStyle;ZIIZIZIIIZIIZIIZIZIIZZZZZZLio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;IILio/getstream/chat/android/ui/font/TextStyle;ILio/getstream/chat/android/ui/font/TextStyle;IIIIIIZIIIIIIIIIIIIZZ)V + public fun (Lio/getstream/chat/android/ui/feature/messages/list/ScrollButtonViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$NewMessagesBehaviour;Lio/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle;Lio/getstream/chat/android/ui/feature/messages/list/GiphyViewHolderStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle;Lio/getstream/chat/android/ui/feature/messages/list/UnreadLabelButtonStyle;ZIIZIZIIIZIIZIIZIZIIZZZZZZLio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;IILio/getstream/chat/android/ui/font/TextStyle;ILio/getstream/chat/android/ui/font/TextStyle;IIIIIIZIIIIIIIIIIIIZZZI)V public final fun component1 ()Lio/getstream/chat/android/ui/feature/messages/list/ScrollButtonViewStyle; public final fun component10 ()I public final fun component11 ()Z @@ -2601,11 +2601,13 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis public final fun component6 ()Lio/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle; public final fun component60 ()Z public final fun component61 ()Z + public final fun component62 ()Z + public final fun component63 ()I public final fun component7 ()Lio/getstream/chat/android/ui/feature/messages/list/UnreadLabelButtonStyle; public final fun component8 ()Z public final fun component9 ()I - public final fun copy (Lio/getstream/chat/android/ui/feature/messages/list/ScrollButtonViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$NewMessagesBehaviour;Lio/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle;Lio/getstream/chat/android/ui/feature/messages/list/GiphyViewHolderStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle;Lio/getstream/chat/android/ui/feature/messages/list/UnreadLabelButtonStyle;ZIIZIZIIIZIIZIIZIZIIZZZZZZLio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;IILio/getstream/chat/android/ui/font/TextStyle;ILio/getstream/chat/android/ui/font/TextStyle;IIIIIIZIIIIIIIIIIIIZZ)Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle; - public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/ScrollButtonViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$NewMessagesBehaviour;Lio/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle;Lio/getstream/chat/android/ui/feature/messages/list/GiphyViewHolderStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle;Lio/getstream/chat/android/ui/feature/messages/list/UnreadLabelButtonStyle;ZIIZIZIIIZIIZIIZIZIIZZZZZZLio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;IILio/getstream/chat/android/ui/font/TextStyle;ILio/getstream/chat/android/ui/font/TextStyle;IIIIIIZIIIIIIIIIIIIZZIILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle; + public final fun copy (Lio/getstream/chat/android/ui/feature/messages/list/ScrollButtonViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$NewMessagesBehaviour;Lio/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle;Lio/getstream/chat/android/ui/feature/messages/list/GiphyViewHolderStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle;Lio/getstream/chat/android/ui/feature/messages/list/UnreadLabelButtonStyle;ZIIZIZIIIZIIZIIZIZIIZZZZZZLio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;IILio/getstream/chat/android/ui/font/TextStyle;ILio/getstream/chat/android/ui/font/TextStyle;IIIIIIZIIIIIIIIIIIIZZZI)Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/ScrollButtonViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$NewMessagesBehaviour;Lio/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle;Lio/getstream/chat/android/ui/feature/messages/list/GiphyViewHolderStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageViewStyle;Lio/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle;Lio/getstream/chat/android/ui/feature/messages/list/UnreadLabelButtonStyle;ZIIZIZIIIZIIZIIZIZIIZZZZZZLio/getstream/chat/android/ui/font/TextStyle;Lio/getstream/chat/android/ui/font/TextStyle;IILio/getstream/chat/android/ui/font/TextStyle;ILio/getstream/chat/android/ui/font/TextStyle;IIIIIIZIIIIIIIIIIIIZZZIIILjava/lang/Object;)Lio/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle; public fun equals (Ljava/lang/Object;)Z public final fun getAudioRecordPlayerViewStyle ()Lio/getstream/chat/android/ui/feature/messages/list/MessageViewStyle; public final fun getBackgroundColor ()I @@ -2659,6 +2661,8 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis public final fun getScrollButtonEndMargin ()I public final fun getScrollButtonViewStyle ()Lio/getstream/chat/android/ui/feature/messages/list/ScrollButtonViewStyle; public final fun getShowReactionsForUnsentMessages ()Z + public final fun getSwipeToReplyEnabled ()Z + public final fun getSwipeToReplyIcon ()I public final fun getThreadMessagesStart ()I public final fun getThreadReplyIcon ()I public final fun getThreadsEnabled ()Z @@ -2795,7 +2799,7 @@ public abstract class io/getstream/chat/android/ui/feature/messages/list/adapter public fun (Landroid/view/View;)V public abstract fun bindData (Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItem;Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItemPayloadDiff;)V protected final fun getContext ()Landroid/content/Context; - protected final fun getData ()Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItem; + public final fun getData ()Lio/getstream/chat/android/ui/feature/messages/list/adapter/MessageListItem; public fun messageContainerView ()Landroid/view/View; public fun onAttachedToWindow ()V public fun onDetachedFromWindow ()V diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle.kt index 36c76941b91..934d7a92eaf 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListItemStyle.kt @@ -200,7 +200,7 @@ public data class MessageListItemStyle( internal val MESSAGE_STROKE_COLOR_MINE = R.color.stream_ui_literal_transparent internal const val MESSAGE_STROKE_WIDTH_MINE: Float = 0f internal val MESSAGE_STROKE_COLOR_THEIRS = R.color.stream_ui_grey_whisper - internal val MESSAGE_STROKE_WIDTH_THEIRS: Float = 1.dpToPxPrecise() + internal val MESSAGE_STROKE_WIDTH_THEIRS: Float by lazy { 1.dpToPxPrecise() } private const val BASE_MESSAGE_MAX_WIDTH_FACTOR = 1 private const val DEFAULT_MESSAGE_MAX_WIDTH_FACTOR = 0.75f diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt index 358d0c8bd04..a5ab3ccb620 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListView.kt @@ -29,6 +29,7 @@ import android.widget.Toast import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isVisible import androidx.fragment.app.FragmentManager +import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView.ItemAnimator @@ -108,6 +109,8 @@ import io.getstream.chat.android.ui.feature.messages.list.background.MessageBack import io.getstream.chat.android.ui.feature.messages.list.background.MessageBackgroundFactoryImpl import io.getstream.chat.android.ui.feature.messages.list.internal.HiddenMessageListItemPredicate import io.getstream.chat.android.ui.feature.messages.list.internal.MessageListScrollHelper +import io.getstream.chat.android.ui.feature.messages.list.internal.SwipeReplyCallback +import io.getstream.chat.android.ui.feature.messages.list.internal.canReplyToMessage import io.getstream.chat.android.ui.feature.messages.list.internal.poll.AllPollOptionsDialogFragment import io.getstream.chat.android.ui.feature.messages.list.internal.poll.PollResultsDialogFragment import io.getstream.chat.android.ui.feature.messages.list.options.message.MessageOptionItem @@ -666,6 +669,7 @@ public class MessageListView : ConstraintLayout { initRecyclerView() initScrollHelper() + initSwipeToReply() initLoadingView() initEmptyStateView() messageListViewStyle?.unreadLabelButtonStyle?.let { initUnreadLabelButton(it) } @@ -727,6 +731,30 @@ public class MessageListView : ConstraintLayout { } } + private fun initSwipeToReply() { + messageListViewStyle?.swipeToReplyIcon + ?.takeUnless { messageListViewStyle?.swipeToReplyEnabled == false } + ?.let(context::getDrawable) + ?.let { swipeToReplyIcon -> + SwipeReplyCallback(swipeToReplyIcon) { message -> + message + ?.let { messageListViewStyle?.canReplyToMessage(it, ownCapabilities) } + ?: false + }.let { swipeReplyCallback -> + ItemTouchHelper(swipeReplyCallback).let { itemTouchHelper -> + swipeReplyCallback.onReply = { + // We need to detach and attach the itemTouchHelper to the RecyclerView to make it work + // after the reply action is completed. + itemTouchHelper.attachToRecyclerView(null) + itemTouchHelper.attachToRecyclerView(binding.chatMessagesRV) + messageReplyHandler.onMessageReply(it.cid, it) + } + itemTouchHelper.attachToRecyclerView(binding.chatMessagesRV) + } + } + } + } + private fun configureAttributes(attributeSet: AttributeSet?) { context.obtainStyledAttributes( attributeSet, diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle.kt index bb04fb7796f..ba6ebf5571f 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageListViewStyle.kt @@ -22,6 +22,7 @@ import android.graphics.Typeface import android.util.AttributeSet import android.view.Gravity import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes import androidx.annotation.LayoutRes import io.getstream.chat.android.ui.R import io.getstream.chat.android.ui.common.state.messages.list.MessageOptionsUserReactionAlignment @@ -100,6 +101,8 @@ import io.getstream.chat.android.ui.utils.extensions.use * @property optionsOverlayMessageOptionsMarginEnd Defines the end margin between the message option list on the options overlay and the parent. * @property showReactionsForUnsentMessages If we need to show the edit reactions bubble for unsent messages on the options overlay. * @property readCountEnabled Enables/disables read count. Enabled by default. + * @property swipeToReplyEnabled Enables/disables swipe to reply feature. Enabled by default. + * @property swipeToReplyIcon Icon for swipe to reply feature. Default value is [R.drawable.stream_ui_ic_arrow_curve_left_grey]. */ public data class MessageListViewStyle( public val scrollButtonViewStyle: ScrollButtonViewStyle, @@ -163,28 +166,30 @@ public data class MessageListViewStyle( public val optionsOverlayMessageOptionsMarginEnd: Int, public val showReactionsForUnsentMessages: Boolean, public val readCountEnabled: Boolean, + public val swipeToReplyEnabled: Boolean, + @DrawableRes public val swipeToReplyIcon: Int, ) : ViewStyle { public companion object { private val DEFAULT_BACKGROUND_COLOR = R.color.stream_ui_white_snow - private val DEFAULT_SCROLL_BUTTON_ELEVATION = 3.dpToPx().toFloat() - private val DEFAULT_SCROLL_BUTTON_MARGIN = 6.dpToPx() - private val DEFAULT_SCROLL_BUTTON_INTERNAL_MARGIN = 2.dpToPx() - private val DEFAULT_SCROLL_BUTTON_BADGE_ELEVATION = DEFAULT_SCROLL_BUTTON_ELEVATION - - private val DEFAULT_EDIT_REACTIONS_MARGIN_TOP = 0.dpToPx() - private val DEFAULT_EDIT_REACTIONS_MARGIN_BOTTOM = 0.dpToPx() - private val DEFAULT_EDIT_REACTIONS_MARGIN_START = 50.dpToPx() - private val DEFAULT_EDIT_REACTIONS_MARGIN_END = 8.dpToPx() - - private val DEFAULT_USER_REACTIONS_MARGIN_TOP = 8.dpToPx() - private val DEFAULT_USER_REACTIONS_MARGIN_BOTTOM = 0.dpToPx() - private val DEFAULT_USER_REACTIONS_MARGIN_START = 8.dpToPx() - private val DEFAULT_USER_REACTIONS_MARGIN_END = 8.dpToPx() - - private val DEFAULT_MESSAGE_OPTIONS_MARGIN_TOP = 24.dpToPx() - private val DEFAULT_MESSAGE_OPTIONS_MARGIN_BOTTOM = 0.dpToPx() - private val DEFAULT_MESSAGE_OPTIONS_MARGIN_START = 50.dpToPx() - private val DEFAULT_MESSAGE_OPTIONS_MARGIN_END = 8.dpToPx() + private val DEFAULT_SCROLL_BUTTON_ELEVATION by lazy { 3.dpToPx().toFloat() } + private val DEFAULT_SCROLL_BUTTON_MARGIN by lazy { 6.dpToPx() } + private val DEFAULT_SCROLL_BUTTON_INTERNAL_MARGIN by lazy { 2.dpToPx() } + private val DEFAULT_SCROLL_BUTTON_BADGE_ELEVATION by lazy { DEFAULT_SCROLL_BUTTON_ELEVATION } + + private val DEFAULT_EDIT_REACTIONS_MARGIN_TOP by lazy { 0.dpToPx() } + private val DEFAULT_EDIT_REACTIONS_MARGIN_BOTTOM by lazy { 0.dpToPx() } + private val DEFAULT_EDIT_REACTIONS_MARGIN_START by lazy { 50.dpToPx() } + private val DEFAULT_EDIT_REACTIONS_MARGIN_END by lazy { 8.dpToPx() } + + private val DEFAULT_USER_REACTIONS_MARGIN_TOP by lazy { 8.dpToPx() } + private val DEFAULT_USER_REACTIONS_MARGIN_BOTTOM by lazy { 0.dpToPx() } + private val DEFAULT_USER_REACTIONS_MARGIN_START by lazy { 8.dpToPx() } + private val DEFAULT_USER_REACTIONS_MARGIN_END by lazy { 8.dpToPx() } + + private val DEFAULT_MESSAGE_OPTIONS_MARGIN_TOP by lazy { 24.dpToPx() } + private val DEFAULT_MESSAGE_OPTIONS_MARGIN_BOTTOM by lazy { 0.dpToPx() } + private val DEFAULT_MESSAGE_OPTIONS_MARGIN_START by lazy { 50.dpToPx() } + private val DEFAULT_MESSAGE_OPTIONS_MARGIN_END by lazy { 8.dpToPx() } /** * Creates an [MessageListViewStyle] instance with the default values. @@ -339,6 +344,11 @@ public data class MessageListViewStyle( R.drawable.stream_ui_ic_arrow_curve_left_grey, ) + val replyToSwipeIcon: Int = attributes.getResourceId( + R.styleable.MessageListView_streamUiSwipeToReplyIcon, + R.drawable.stream_ui_ic_arrow_curve_left_grey, + ) + val replyEnabled = attributes.getBoolean(R.styleable.MessageListView_streamUiReplyEnabled, true) val threadReplyIcon = attributes.getResourceId( @@ -594,6 +604,11 @@ public data class MessageListViewStyle( true, ) + val swipeToReplyEnabled = attributes.getBoolean( + R.styleable.MessageListView_streamUiSwipeToReply, + true, + ) + val userBlockEnabled = attributes.getBoolean( R.styleable.MessageListView_streamUiBlockUserOptionEnabled, true, @@ -673,6 +688,8 @@ public data class MessageListViewStyle( optionsOverlayMessageOptionsMarginEnd = optionsOverlayMessageOptionsMarginEnd, showReactionsForUnsentMessages = showReactionsForUnsentMessages, readCountEnabled = readCountEnabled, + swipeToReplyEnabled = swipeToReplyEnabled, + swipeToReplyIcon = replyToSwipeIcon, ).let(TransformStyle.messageListStyleTransformer::transform) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle.kt index a9485e1e88b..05c7eb3a74c 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/MessageReplyStyle.kt @@ -171,7 +171,7 @@ public data class MessageReplyStyle( ).let(TransformStyle.messageReplyStyleTransformer::transform) } - private val MESSAGE_STROKE_WIDTH_THEIRS: Float = 1.dpToPxPrecise() + private val MESSAGE_STROKE_WIDTH_THEIRS: Float by lazy { 1.dpToPxPrecise() } private const val VALUE_NOT_SET = Integer.MAX_VALUE internal val DEFAULT_TEXT_COLOR = R.color.stream_ui_text_color_primary internal val DEFAULT_TEXT_SIZE = R.dimen.stream_ui_text_medium diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/BaseMessageItemViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/BaseMessageItemViewHolder.kt index 43dcb0ae10a..de9cad7c9e4 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/BaseMessageItemViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/BaseMessageItemViewHolder.kt @@ -39,7 +39,7 @@ public abstract class BaseMessageItemViewHolder( * Can be used for listeners that need to pass along the currently * bound data as a parameter. */ - protected lateinit var data: T + public lateinit var data: T private set private var highlightAnimation: ValueAnimator? = null diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/MessageListViewExtensions.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/MessageListViewExtensions.kt new file mode 100644 index 00000000000..fcd16c2ab80 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/MessageListViewExtensions.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:Suppress("TooManyFunctions") + +package io.getstream.chat.android.ui.feature.messages.list.internal + +import io.getstream.chat.android.client.utils.attachment.isGiphy +import io.getstream.chat.android.models.AttachmentType +import io.getstream.chat.android.models.ChannelCapabilities +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.SyncStatus +import io.getstream.chat.android.models.User +import io.getstream.chat.android.ui.feature.messages.list.MessageListViewStyle +import io.getstream.chat.android.uiutils.extension.hasLink + +private fun Message.isMessageFailed(): Boolean = syncStatus == SyncStatus.FAILED_PERMANENTLY +private fun Message.isSynced(): Boolean = syncStatus == SyncStatus.COMPLETED +private fun Message.isTextOnly(): Boolean = text.isNotEmpty() && attachments.isEmpty() +private fun Message.hasLinks(): Boolean = attachments.any { it.hasLink() && !it.isGiphy() } +private fun Message.isOwnMessage(currentUser: User?): Boolean = user.id == currentUser?.id +private fun Message.isGiphyCommand(): Boolean = command == AttachmentType.GIPHY +private fun Set.canEditOwnMessage(): Boolean = contains(ChannelCapabilities.UPDATE_OWN_MESSAGE) +private fun Set.canEditAnyMessage(): Boolean = contains(ChannelCapabilities.UPDATE_ANY_MESSAGE) +private fun Set.canDeleteOwnMessage(): Boolean = contains(ChannelCapabilities.DELETE_OWN_MESSAGE) +private fun Set.canDeleteAnyMessage(): Boolean = contains(ChannelCapabilities.DELETE_ANY_MESSAGE) +private fun Set.canFlagMessage(): Boolean = contains(ChannelCapabilities.FLAG_MESSAGE) +private fun Set.canPinMessage(): Boolean = contains(ChannelCapabilities.PIN_MESSAGE) +private fun Set.canMarkAsUnread(): Boolean = contains(ChannelCapabilities.READ_EVENTS) + +internal fun MessageListViewStyle.canReplyToMessage( + message: Message, + ownCapabilities: Set, +): Boolean = replyEnabled && message.isSynced() && ownCapabilities.contains(ChannelCapabilities.QUOTE_MESSAGE) + +internal fun MessageListViewStyle.canThreadReplyToMessage( + message: Message, + ownCapabilities: Set, +): Boolean = threadsEnabled && message.isSynced() && ownCapabilities.contains(ChannelCapabilities.QUOTE_MESSAGE) + +internal fun MessageListViewStyle.canCopyMessage( + message: Message, +): Boolean = copyTextEnabled && (message.isTextOnly() || message.hasLinks()) + +internal fun MessageListViewStyle.canEditMessage( + currentUser: User?, + message: Message, + ownCapabilities: Set, +): Boolean = editMessageEnabled && + with(ownCapabilities) { ((message.isOwnMessage(currentUser) && canEditOwnMessage()) || canEditAnyMessage()) } && + !message.isGiphyCommand() + +internal fun MessageListViewStyle.canDeleteMessage( + currentUser: User?, + message: Message, + ownCapabilities: Set, +): Boolean = deleteMessageEnabled && + with(ownCapabilities) { ((message.isOwnMessage(currentUser) && canDeleteOwnMessage()) || canDeleteAnyMessage()) } + +internal fun MessageListViewStyle.canFlagMessage( + currentUser: User?, + message: Message, + ownCapabilities: Set, +): Boolean = flagEnabled && ownCapabilities.canFlagMessage() && !message.isOwnMessage(currentUser) + +internal fun MessageListViewStyle.canPinMessage( + message: Message, + ownCapabilities: Set, +): Boolean = pinMessageEnabled && message.isSynced() && ownCapabilities.canPinMessage() + +internal fun MessageListViewStyle.canBlockUser( + currentUser: User?, + message: Message, +): Boolean = blockUserEnabled && !message.isOwnMessage(currentUser) + +internal fun MessageListViewStyle.canMarkAsUnread( + ownCapabilities: Set, +): Boolean = markAsUnreadEnabled && ownCapabilities.canMarkAsUnread() + +internal fun MessageListViewStyle.canRetryMessage( + currentUser: User?, + message: Message, +): Boolean = retryMessageEnabled && message.isOwnMessage(currentUser) && message.isMessageFailed() diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/SwipeReplyCallback.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/SwipeReplyCallback.kt new file mode 100644 index 00000000000..6a73a823602 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/SwipeReplyCallback.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.ui.feature.messages.list.internal + +import android.graphics.Canvas +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.view.View +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.ItemTouchHelper.ACTION_STATE_IDLE +import androidx.recyclerview.widget.ItemTouchHelper.RIGHT +import androidx.recyclerview.widget.RecyclerView +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.ui.feature.messages.list.adapter.BaseMessageItemViewHolder +import io.getstream.chat.android.ui.feature.messages.list.adapter.MessageListItem + +internal class SwipeReplyCallback( + val replyDrawable: Drawable, + val canReply: (Message?) -> Boolean, +) : ItemTouchHelper.Callback() { + + private val RecyclerView.ViewHolder.message: Message? + get() = asBaseMessageItemViewHolder()?.messageItem?.message + + private val BaseMessageItemViewHolder<*>.messageItem: MessageListItem.MessageItem? + get() = data as? MessageListItem.MessageItem + + private fun RecyclerView.ViewHolder.asBaseMessageItemViewHolder(): BaseMessageItemViewHolder<*>? = + this as? BaseMessageItemViewHolder<*> + + var onReply: (message: Message) -> Unit = {} + + override fun getMovementFlags(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder): Int = + when (canReply(viewHolder.message)) { + true -> makeMovementFlags(ACTION_STATE_IDLE, RIGHT) + false -> 0 + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder, + ): Boolean = false + + override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float = 0.3f + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + viewHolder.message?.let { onReply(it) } + } + + override fun onChildDraw( + canvas: Canvas, + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + dX: Float, + dY: Float, + actionState: Int, + isCurrentlyActive: Boolean, + ) { + val rightAlignment = minOf(dX.toInt(), replyDrawable.intrinsicWidth * REPLY_DRAWABLE_SIZE_MULTIPLIER) + val topAlignment = viewHolder.itemView.middleVerticalPoint - replyDrawable.intrinsicHeight / 2 + replyDrawable.bounds = Rect( + rightAlignment - replyDrawable.intrinsicWidth, + topAlignment, + rightAlignment, + topAlignment + replyDrawable.intrinsicHeight, + ) + super.onChildDraw( + canvas, + recyclerView, + viewHolder, + minOf(dX, (recyclerView.width / 2).toFloat()), + dY, + actionState, + isCurrentlyActive, + ) + replyDrawable.draw(canvas) + } + + private val View.middleVerticalPoint: Int + get() = (top + bottom) / 2 + + companion object { + private const val REPLY_DRAWABLE_SIZE_MULTIPLIER = 3 + } +} diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory.kt index 92fdac3d952..158419acbec 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/options/message/MessageOptionItemsFactory.kt @@ -17,11 +17,7 @@ package io.getstream.chat.android.ui.feature.messages.list.options.message import android.content.Context -import io.getstream.chat.android.client.utils.attachment.isGiphy -import io.getstream.chat.android.models.AttachmentType -import io.getstream.chat.android.models.ChannelCapabilities import io.getstream.chat.android.models.Message -import io.getstream.chat.android.models.SyncStatus import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.R import io.getstream.chat.android.ui.common.state.messages.BlockUser @@ -36,8 +32,17 @@ import io.getstream.chat.android.ui.common.state.messages.Resend import io.getstream.chat.android.ui.common.state.messages.ThreadReply import io.getstream.chat.android.ui.common.state.messages.UnblockUser import io.getstream.chat.android.ui.feature.messages.list.MessageListViewStyle +import io.getstream.chat.android.ui.feature.messages.list.internal.canBlockUser +import io.getstream.chat.android.ui.feature.messages.list.internal.canCopyMessage +import io.getstream.chat.android.ui.feature.messages.list.internal.canDeleteMessage +import io.getstream.chat.android.ui.feature.messages.list.internal.canEditMessage +import io.getstream.chat.android.ui.feature.messages.list.internal.canFlagMessage +import io.getstream.chat.android.ui.feature.messages.list.internal.canMarkAsUnread +import io.getstream.chat.android.ui.feature.messages.list.internal.canPinMessage +import io.getstream.chat.android.ui.feature.messages.list.internal.canReplyToMessage +import io.getstream.chat.android.ui.feature.messages.list.internal.canRetryMessage +import io.getstream.chat.android.ui.feature.messages.list.internal.canThreadReplyToMessage import io.getstream.chat.android.ui.utils.extensions.getDrawableCompat -import io.getstream.chat.android.uiutils.extension.hasLink /** * An interface that allows the creation of message option items. @@ -106,25 +111,8 @@ public open class DefaultMessageOptionItemsFactory( val selectedMessageUserId = selectedMessage.user.id - val isTextOnlyMessage = selectedMessage.text.isNotEmpty() && selectedMessage.attachments.isEmpty() - val hasLinks = selectedMessage.attachments.any { it.hasLink() && !it.isGiphy() } - val isOwnMessage = selectedMessageUserId == currentUser?.id - val isMessageSynced = selectedMessage.syncStatus == SyncStatus.COMPLETED - val isMessageFailed = selectedMessage.syncStatus == SyncStatus.FAILED_PERMANENTLY - - // user capabilities - val canQuoteMessage = ownCapabilities.contains(ChannelCapabilities.QUOTE_MESSAGE) - val canThreadReply = ownCapabilities.contains(ChannelCapabilities.SEND_REPLY) - val canPinMessage = ownCapabilities.contains(ChannelCapabilities.PIN_MESSAGE) - val canDeleteOwnMessage = ownCapabilities.contains(ChannelCapabilities.DELETE_OWN_MESSAGE) - val canDeleteAnyMessage = ownCapabilities.contains(ChannelCapabilities.DELETE_ANY_MESSAGE) - val canEditOwnMessage = ownCapabilities.contains(ChannelCapabilities.UPDATE_OWN_MESSAGE) - val canEditAnyMessage = ownCapabilities.contains(ChannelCapabilities.UPDATE_ANY_MESSAGE) - val canMarkAsUnread = ownCapabilities.contains(ChannelCapabilities.READ_EVENTS) - val canFlagMessage = ownCapabilities.contains(ChannelCapabilities.FLAG_MESSAGE) - return listOfNotNull( - if (style.retryMessageEnabled && isOwnMessage && isMessageFailed) { + if (style.canRetryMessage(currentUser, selectedMessage)) { MessageOptionItem( optionText = context.getString(R.string.stream_ui_message_list_resend_message), optionIcon = context.getDrawableCompat(style.retryIcon)!!, @@ -133,7 +121,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.replyEnabled && isMessageSynced && canQuoteMessage) { + if (style.canReplyToMessage(selectedMessage, ownCapabilities)) { MessageOptionItem( optionText = context.getString(R.string.stream_ui_message_list_reply), optionIcon = context.getDrawableCompat(style.replyIcon)!!, @@ -142,7 +130,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.threadsEnabled && !isInThread && isMessageSynced && canThreadReply) { + if (style.canThreadReplyToMessage(selectedMessage, ownCapabilities) && !isInThread) { MessageOptionItem( optionText = context.getString(R.string.stream_ui_message_list_thread_reply), optionIcon = context.getDrawableCompat(style.threadReplyIcon)!!, @@ -151,7 +139,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.markAsUnreadEnabled && canMarkAsUnread) { + if (style.canMarkAsUnread(ownCapabilities)) { MessageOptionItem( optionText = context.getString(R.string.stream_ui_message_list_mark_as_unread), optionIcon = context.getDrawableCompat(style.markAsUnreadIcon)!!, @@ -160,7 +148,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.copyTextEnabled && (isTextOnlyMessage || hasLinks)) { + if (style.canCopyMessage(selectedMessage)) { MessageOptionItem( optionText = context.getString(R.string.stream_ui_message_list_copy_message), optionIcon = context.getDrawableCompat(style.copyIcon)!!, @@ -169,9 +157,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.editMessageEnabled && ((isOwnMessage && canEditOwnMessage) || canEditAnyMessage) && - selectedMessage.command != AttachmentType.GIPHY - ) { + if (style.canEditMessage(currentUser, selectedMessage, ownCapabilities)) { MessageOptionItem( optionText = context.getString(R.string.stream_ui_message_list_edit_message), optionIcon = context.getDrawableCompat(style.editIcon)!!, @@ -180,7 +166,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.flagEnabled && canFlagMessage && !isOwnMessage) { + if (style.canFlagMessage(currentUser, selectedMessage, ownCapabilities)) { MessageOptionItem( optionText = context.getString(R.string.stream_ui_message_list_flag_message), optionIcon = context.getDrawableCompat(style.flagIcon)!!, @@ -189,7 +175,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.pinMessageEnabled && isMessageSynced && canPinMessage) { + if (style.canPinMessage(selectedMessage, ownCapabilities)) { val (pinText, pinIcon) = if (selectedMessage.pinned) { R.string.stream_ui_message_list_unpin_message to style.unpinIcon } else { @@ -204,7 +190,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.blockUserEnabled && !isOwnMessage) { + if (style.canBlockUser(currentUser, selectedMessage)) { val isSenderBlocked = currentUser?.blockedUserIds?.contains(selectedMessageUserId) == true val text = if (isSenderBlocked) { R.string.stream_ui_message_list_unblock_user @@ -229,7 +215,7 @@ public open class DefaultMessageOptionItemsFactory( } else { null }, - if (style.deleteMessageEnabled && (canDeleteAnyMessage || (isOwnMessage && canDeleteOwnMessage))) { + if (style.canDeleteMessage(currentUser, selectedMessage, ownCapabilities)) { MessageOptionItem( optionText = context.getString(R.string.stream_ui_message_list_delete_message), optionIcon = context.getDrawableCompat(style.deleteIcon)!!, diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/reactions/view/ViewReactionsViewStyle.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/reactions/view/ViewReactionsViewStyle.kt index 11c44437a51..4cc1e2c7cd8 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/reactions/view/ViewReactionsViewStyle.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/reactions/view/ViewReactionsViewStyle.kt @@ -91,7 +91,7 @@ public data class ViewReactionsViewStyle( private val DEFAULT_BUBBLE_BORDER_COLOR_MINE = R.color.stream_ui_grey_whisper private val DEFAULT_BUBBLE_COLOR_MINE = R.color.stream_ui_grey_whisper private val DEFAULT_BUBBLE_COLOR_THEIRS = R.color.stream_ui_grey_gainsboro - private val DEFAULT_BUBBLE_BORDER_WIDTH_MINE = 1.dpToPx() * 1.5f + private val DEFAULT_BUBBLE_BORDER_WIDTH_MINE: Float by lazy { 1.dpToPx() * 1.5f } private const val REACTION_SORTING_BY_FIRST_REACTION_AT = 0 private const val REACTION_SORTING_BY_LAST_REACTION_AT = 1 diff --git a/stream-chat-android-ui-components/src/main/res/values/attrs_message_list_view.xml b/stream-chat-android-ui-components/src/main/res/values/attrs_message_list_view.xml index e8abb580d69..59f62ce4b09 100644 --- a/stream-chat-android-ui-components/src/main/res/values/attrs_message_list_view.xml +++ b/stream-chat-android-ui-components/src/main/res/values/attrs_message_list_view.xml @@ -406,6 +406,7 @@ + @@ -609,6 +610,7 @@ + diff --git a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/Mother.kt b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/Mother.kt index 1d993f0add5..bcabe531022 100644 --- a/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/Mother.kt +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/Mother.kt @@ -16,12 +16,31 @@ package io.getstream.chat.android.ui +import android.graphics.drawable.Drawable import io.getstream.chat.android.models.ChannelUserRead import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.ReactionSorting import io.getstream.chat.android.randomBoolean +import io.getstream.chat.android.randomFloat +import io.getstream.chat.android.randomInt import io.getstream.chat.android.randomMessage import io.getstream.chat.android.ui.common.state.messages.list.MessagePosition +import io.getstream.chat.android.ui.feature.messages.common.AudioRecordPlayerViewStyle +import io.getstream.chat.android.ui.feature.messages.list.GiphyViewHolderStyle +import io.getstream.chat.android.ui.feature.messages.list.MessageListItemStyle +import io.getstream.chat.android.ui.feature.messages.list.MessageListView +import io.getstream.chat.android.ui.feature.messages.list.MessageListViewStyle +import io.getstream.chat.android.ui.feature.messages.list.MessageReplyStyle +import io.getstream.chat.android.ui.feature.messages.list.MessageViewStyle +import io.getstream.chat.android.ui.feature.messages.list.ScrollButtonViewStyle +import io.getstream.chat.android.ui.feature.messages.list.UnreadLabelButtonStyle import io.getstream.chat.android.ui.feature.messages.list.adapter.MessageListItem +import io.getstream.chat.android.ui.feature.messages.list.reactions.edit.EditReactionsViewStyle +import io.getstream.chat.android.ui.feature.messages.list.reactions.view.ViewReactionsViewStyle +import io.getstream.chat.android.ui.font.TextStyle +import io.getstream.chat.android.ui.helper.ViewPadding +import io.getstream.chat.android.ui.helper.ViewSize +import org.mockito.kotlin.mock public fun randomMessageItem( message: Message = randomMessage(), @@ -41,3 +60,498 @@ public fun randomMessageItem( isMessageRead = isMessageRead, showMessageFooter = showMessageFooter, ) + +@Suppress("LongMethod") +public fun randomMessageListViewStyle( + scrollButtonViewStyle: ScrollButtonViewStyle = randomScrollButtonViewStyle(), + scrollButtonBehaviour: MessageListView.NewMessagesBehaviour = randomNewMessagesBehaviour(), + itemStyle: MessageListItemStyle = randomMessageListItemStyle(), + giphyViewHolderStyle: GiphyViewHolderStyle = randomGiphyViewHolderStyle(), + audioRecordPlayerViewStyle: MessageViewStyle = MessageViewStyle( + own = randomAudioRecordPlayerViewStyle(), + theirs = randomAudioRecordPlayerViewStyle(), + ), + replyMessageStyle: MessageReplyStyle = randomMessageReplyStyle(), + unreadLabelButtonStyle: UnreadLabelButtonStyle = randomUnreadLabelButtonStyle(), + reactionsEnabled: Boolean = randomBoolean(), + backgroundColor: Int = randomInt(), + replyIcon: Int = randomInt(), + replyEnabled: Boolean = randomBoolean(), + threadReplyIcon: Int = randomInt(), + threadsEnabled: Boolean = randomBoolean(), + retryIcon: Int = randomInt(), + copyIcon: Int = randomInt(), + markAsUnreadIcon: Int = randomInt(), + editMessageEnabled: Boolean = randomBoolean(), + editIcon: Int = randomInt(), + flagIcon: Int = randomInt(), + flagEnabled: Boolean = randomBoolean(), + pinIcon: Int = randomInt(), + unpinIcon: Int = randomInt(), + pinMessageEnabled: Boolean = randomBoolean(), + deleteIcon: Int = randomInt(), + deleteMessageEnabled: Boolean = randomBoolean(), + blockUserIcon: Int = randomInt(), + unblockUserIcon: Int = randomInt(), + blockUserEnabled: Boolean = randomBoolean(), + copyTextEnabled: Boolean = randomBoolean(), + markAsUnreadEnabled: Boolean = randomBoolean(), + retryMessageEnabled: Boolean = randomBoolean(), + deleteConfirmationEnabled: Boolean = randomBoolean(), + flagMessageConfirmationEnabled: Boolean = randomBoolean(), + messageOptionsText: TextStyle = mock(), + warningMessageOptionsText: TextStyle = mock(), + messageOptionsBackgroundColor: Int = randomInt(), + userReactionsBackgroundColor: Int = randomInt(), + userReactionsTitleText: TextStyle = mock(), + optionsOverlayDimColor: Int = randomInt(), + emptyViewTextStyle: TextStyle = mock(), + loadingView: Int = randomInt(), + messagesStart: Int = randomInt(), + threadMessagesStart: Int = randomInt(), + messageOptionsUserReactionAlignment: Int = randomInt(), + scrollButtonBottomMargin: Int = randomInt(), + scrollButtonEndMargin: Int = randomInt(), + disableScrollWhenShowingDialog: Boolean = randomBoolean(), + optionsOverlayEditReactionsMarginTop: Int = randomInt(), + optionsOverlayEditReactionsMarginBottom: Int = randomInt(), + optionsOverlayEditReactionsMarginStart: Int = randomInt(), + optionsOverlayEditReactionsMarginEnd: Int = randomInt(), + optionsOverlayUserReactionsMarginTop: Int = randomInt(), + optionsOverlayUserReactionsMarginBottom: Int = randomInt(), + optionsOverlayUserReactionsMarginStart: Int = randomInt(), + optionsOverlayUserReactionsMarginEnd: Int = randomInt(), + optionsOverlayMessageOptionsMarginTop: Int = randomInt(), + optionsOverlayMessageOptionsMarginBottom: Int = randomInt(), + optionsOverlayMessageOptionsMarginStart: Int = randomInt(), + optionsOverlayMessageOptionsMarginEnd: Int = randomInt(), + showReactionsForUnsentMessages: Boolean = randomBoolean(), + readCountEnabled: Boolean = randomBoolean(), + swipeToReplyIcon: Drawable? = null, +): MessageListViewStyle = MessageListViewStyle( + scrollButtonViewStyle = scrollButtonViewStyle, + scrollButtonBehaviour = scrollButtonBehaviour, + itemStyle = itemStyle, + giphyViewHolderStyle = giphyViewHolderStyle, + audioRecordPlayerViewStyle = audioRecordPlayerViewStyle, + replyMessageStyle = replyMessageStyle, + unreadLabelButtonStyle = unreadLabelButtonStyle, + reactionsEnabled = reactionsEnabled, + backgroundColor = backgroundColor, + replyIcon = replyIcon, + replyEnabled = replyEnabled, + threadReplyIcon = threadReplyIcon, + threadsEnabled = threadsEnabled, + retryIcon = retryIcon, + copyIcon = copyIcon, + markAsUnreadIcon = markAsUnreadIcon, + editMessageEnabled = editMessageEnabled, + editIcon = editIcon, + flagIcon = flagIcon, + flagEnabled = flagEnabled, + pinIcon = pinIcon, + unpinIcon = unpinIcon, + pinMessageEnabled = pinMessageEnabled, + deleteIcon = deleteIcon, + deleteMessageEnabled = deleteMessageEnabled, + blockUserIcon = blockUserIcon, + unblockUserIcon = unblockUserIcon, + blockUserEnabled = blockUserEnabled, + copyTextEnabled = copyTextEnabled, + markAsUnreadEnabled = markAsUnreadEnabled, + retryMessageEnabled = retryMessageEnabled, + deleteConfirmationEnabled = deleteConfirmationEnabled, + flagMessageConfirmationEnabled = flagMessageConfirmationEnabled, + messageOptionsText = messageOptionsText, + warningMessageOptionsText = warningMessageOptionsText, + messageOptionsBackgroundColor = messageOptionsBackgroundColor, + userReactionsBackgroundColor = userReactionsBackgroundColor, + userReactionsTitleText = userReactionsTitleText, + optionsOverlayDimColor = optionsOverlayDimColor, + emptyViewTextStyle = emptyViewTextStyle, + loadingView = loadingView, + messagesStart = messagesStart, + threadMessagesStart = threadMessagesStart, + messageOptionsUserReactionAlignment = messageOptionsUserReactionAlignment, + scrollButtonBottomMargin = scrollButtonBottomMargin, + scrollButtonEndMargin = scrollButtonEndMargin, + disableScrollWhenShowingDialog = disableScrollWhenShowingDialog, + optionsOverlayEditReactionsMarginTop = optionsOverlayEditReactionsMarginTop, + optionsOverlayEditReactionsMarginBottom = optionsOverlayEditReactionsMarginBottom, + optionsOverlayEditReactionsMarginStart = optionsOverlayEditReactionsMarginStart, + optionsOverlayEditReactionsMarginEnd = optionsOverlayEditReactionsMarginEnd, + optionsOverlayUserReactionsMarginTop = optionsOverlayUserReactionsMarginTop, + optionsOverlayUserReactionsMarginBottom = optionsOverlayUserReactionsMarginBottom, + optionsOverlayUserReactionsMarginStart = optionsOverlayUserReactionsMarginStart, + optionsOverlayUserReactionsMarginEnd = optionsOverlayUserReactionsMarginEnd, + optionsOverlayMessageOptionsMarginTop = optionsOverlayMessageOptionsMarginTop, + optionsOverlayMessageOptionsMarginBottom = optionsOverlayMessageOptionsMarginBottom, + optionsOverlayMessageOptionsMarginStart = optionsOverlayMessageOptionsMarginStart, + optionsOverlayMessageOptionsMarginEnd = optionsOverlayMessageOptionsMarginEnd, + showReactionsForUnsentMessages = showReactionsForUnsentMessages, + readCountEnabled = readCountEnabled, + swipeToReplyIcon = swipeToReplyIcon, +) + +public fun randomScrollButtonViewStyle( + scrollButtonEnabled: Boolean = randomBoolean(), + scrollButtonUnreadEnabled: Boolean = randomBoolean(), + scrollButtonColor: Int = randomInt(), + scrollButtonRippleColor: Int = randomInt(), + scrollButtonBadgeColor: Int? = randomInt(), + scrollButtonElevation: Float = randomFloat(), + scrollButtonIcon: Drawable? = null, + scrollButtonBadgeTextStyle: TextStyle = mock(), + scrollButtonBadgeIcon: Drawable? = null, + scrollButtonBadgeGravity: Int = randomInt(), + scrollButtonBadgeElevation: Float = randomFloat(), + scrollButtonInternalMargin: Int = randomInt(), +): ScrollButtonViewStyle = ScrollButtonViewStyle( + scrollButtonEnabled = scrollButtonEnabled, + scrollButtonUnreadEnabled = scrollButtonUnreadEnabled, + scrollButtonColor = scrollButtonColor, + scrollButtonRippleColor = scrollButtonRippleColor, + scrollButtonBadgeColor = scrollButtonBadgeColor, + scrollButtonElevation = scrollButtonElevation, + scrollButtonIcon = scrollButtonIcon, + scrollButtonBadgeTextStyle = scrollButtonBadgeTextStyle, + scrollButtonBadgeIcon = scrollButtonBadgeIcon, + scrollButtonBadgeGravity = scrollButtonBadgeGravity, + scrollButtonBadgeElevation = scrollButtonBadgeElevation, + scrollButtonInternalMargin = scrollButtonInternalMargin, +) + +public fun randomNewMessagesBehaviour(): MessageListView.NewMessagesBehaviour = + MessageListView.NewMessagesBehaviour.entries.toTypedArray().random() + +public fun randomMessageListItemStyle( + messageBackgroundColorMine: Int? = randomInt(), + messageBackgroundColorTheirs: Int? = randomInt(), + messageLinkTextColorMine: Int? = randomInt(), + messageLinkTextColorTheirs: Int? = randomInt(), + messageLinkBackgroundColorMine: Int = randomInt(), + messageLinkBackgroundColorTheirs: Int = randomInt(), + linkDescriptionMaxLines: Int = randomInt(), + textStyleMine: TextStyle = mock(), + textStyleTheirs: TextStyle = mock(), + textStyleUserName: TextStyle = mock(), + textStyleMessageDate: TextStyle = mock(), + textStyleMessageLanguage: TextStyle = mock(), + textStyleThreadCounter: TextStyle = mock(), + textStyleReadCounter: TextStyle = mock(), + threadSeparatorTextStyle: TextStyle = mock(), + textStyleLinkLabel: TextStyle = mock(), + textStyleLinkTitle: TextStyle = mock(), + textStyleLinkDescription: TextStyle = mock(), + dateSeparatorBackgroundColor: Int = randomInt(), + textStyleDateSeparator: TextStyle = mock(), + reactionsViewStyle: ViewReactionsViewStyle = randomViewReactionsViewStyle(), + editReactionsViewStyle: EditReactionsViewStyle = randomEditReactionsViewStyle(), + iconIndicatorSent: Drawable = mock(), + iconIndicatorRead: Drawable = mock(), + iconIndicatorPendingSync: Drawable = mock(), + iconOnlyVisibleToYou: Drawable = mock(), + textStyleMessageDeletedMine: TextStyle? = mock(), + messageDeletedBackgroundMine: Int? = randomInt(), + textStyleMessageDeletedTheirs: TextStyle? = mock(), + messageDeletedBackgroundTheirs: Int? = randomInt(), + messageStrokeColorMine: Int = randomInt(), + messageStrokeWidthMine: Float = randomFloat(), + messageStrokeColorTheirs: Int = randomInt(), + messageStrokeWidthTheirs: Float = randomFloat(), + textStyleSystemMessage: TextStyle = mock(), + textStyleErrorMessage: TextStyle = mock(), + pinnedMessageIndicatorTextStyle: TextStyle = mock(), + pinnedMessageIndicatorIcon: Drawable = mock(), + pinnedMessageBackgroundColor: Int = randomInt(), + messageStartMargin: Int = randomInt(), + messageEndMargin: Int = randomInt(), + messageMaxWidthFactorMine: Float = randomFloat(), + messageMaxWidthFactorTheirs: Float = randomFloat(), + showMessageDeliveryStatusIndicator: Boolean = randomBoolean(), + iconFailedMessage: Drawable = mock(), + iconBannedMessage: Drawable = mock(), + systemMessageAlignment: Int = randomInt(), + loadingMoreView: Int = randomInt(), + unreadSeparatorBackgroundColor: Int = randomInt(), + unreadSeparatorTextStyle: TextStyle = mock(), +): MessageListItemStyle = MessageListItemStyle( + messageBackgroundColorMine = messageBackgroundColorMine, + messageBackgroundColorTheirs = messageBackgroundColorTheirs, + messageLinkTextColorMine = messageLinkTextColorMine, + messageLinkTextColorTheirs = messageLinkTextColorTheirs, + messageLinkBackgroundColorMine = messageLinkBackgroundColorMine, + messageLinkBackgroundColorTheirs = messageLinkBackgroundColorTheirs, + linkDescriptionMaxLines = linkDescriptionMaxLines, + textStyleMine = textStyleMine, + textStyleTheirs = textStyleTheirs, + textStyleUserName = textStyleUserName, + textStyleMessageDate = textStyleMessageDate, + textStyleMessageLanguage = textStyleMessageLanguage, + textStyleThreadCounter = textStyleThreadCounter, + textStyleReadCounter = textStyleReadCounter, + threadSeparatorTextStyle = threadSeparatorTextStyle, + textStyleLinkLabel = textStyleLinkLabel, + textStyleLinkTitle = textStyleLinkTitle, + textStyleLinkDescription = textStyleLinkDescription, + dateSeparatorBackgroundColor = dateSeparatorBackgroundColor, + textStyleDateSeparator = textStyleDateSeparator, + reactionsViewStyle = reactionsViewStyle, + editReactionsViewStyle = editReactionsViewStyle, + iconIndicatorSent = iconIndicatorSent, + iconIndicatorRead = iconIndicatorRead, + iconIndicatorPendingSync = iconIndicatorPendingSync, + iconOnlyVisibleToYou = iconOnlyVisibleToYou, + textStyleMessageDeletedMine = textStyleMessageDeletedMine, + messageDeletedBackgroundMine = messageDeletedBackgroundMine, + textStyleMessageDeletedTheirs = textStyleMessageDeletedTheirs, + messageDeletedBackgroundTheirs = messageDeletedBackgroundTheirs, + messageStrokeColorMine = messageStrokeColorMine, + messageStrokeWidthMine = messageStrokeWidthMine, + messageStrokeColorTheirs = messageStrokeColorTheirs, + messageStrokeWidthTheirs = messageStrokeWidthTheirs, + textStyleSystemMessage = textStyleSystemMessage, + textStyleErrorMessage = textStyleErrorMessage, + pinnedMessageIndicatorTextStyle = pinnedMessageIndicatorTextStyle, + pinnedMessageIndicatorIcon = pinnedMessageIndicatorIcon, + pinnedMessageBackgroundColor = pinnedMessageBackgroundColor, + messageStartMargin = messageStartMargin, + messageEndMargin = messageEndMargin, + messageMaxWidthFactorMine = messageMaxWidthFactorMine, + messageMaxWidthFactorTheirs = messageMaxWidthFactorTheirs, + showMessageDeliveryStatusIndicator = showMessageDeliveryStatusIndicator, + iconFailedMessage = iconFailedMessage, + iconBannedMessage = iconBannedMessage, + systemMessageAlignment = systemMessageAlignment, + loadingMoreView = loadingMoreView, + unreadSeparatorBackgroundColor = unreadSeparatorBackgroundColor, + unreadSeparatorTextStyle = unreadSeparatorTextStyle, + messageDeletedBackground = randomInt(), + textStyleMessageDeleted = mock(), +) + +public fun randomViewReactionsViewStyle( + bubbleBorderColorMine: Int = randomInt(), + bubbleBorderColorTheirs: Int? = randomInt(), + bubbleColorMine: Int = randomInt(), + bubbleColorTheirs: Int = randomInt(), + bubbleBorderWidthMine: Float = randomFloat(), + bubbleBorderWidthTheirs: Float? = randomFloat(), + totalHeight: Int = randomInt(), + horizontalPadding: Int = randomInt(), + itemSize: Int = randomInt(), + bubbleHeight: Int = randomInt(), + bubbleRadius: Int = randomInt(), + largeTailBubbleCy: Int = randomInt(), + largeTailBubbleRadius: Int = randomInt(), + largeTailBubbleOffset: Int = randomInt(), + smallTailBubbleCy: Int = randomInt(), + smallTailBubbleRadius: Int = randomInt(), + smallTailBubbleOffset: Int = randomInt(), + verticalPadding: Int = randomInt(), + messageOptionsUserReactionOrientation: Int = randomInt(), + reactionSorting: ReactionSorting = mock(), +): ViewReactionsViewStyle = ViewReactionsViewStyle( + bubbleBorderColorMine = bubbleBorderColorMine, + bubbleBorderColorTheirs = bubbleBorderColorTheirs, + bubbleColorMine = bubbleColorMine, + bubbleColorTheirs = bubbleColorTheirs, + bubbleBorderWidthMine = bubbleBorderWidthMine, + bubbleBorderWidthTheirs = bubbleBorderWidthTheirs, + totalHeight = totalHeight, + horizontalPadding = horizontalPadding, + itemSize = itemSize, + bubbleHeight = bubbleHeight, + bubbleRadius = bubbleRadius, + largeTailBubbleCy = largeTailBubbleCy, + largeTailBubbleRadius = largeTailBubbleRadius, + largeTailBubbleOffset = largeTailBubbleOffset, + smallTailBubbleCy = smallTailBubbleCy, + smallTailBubbleRadius = smallTailBubbleRadius, + smallTailBubbleOffset = smallTailBubbleOffset, + verticalPadding = verticalPadding, + messageOptionsUserReactionOrientation = messageOptionsUserReactionOrientation, + reactionSorting = reactionSorting, +) + +public fun randomEditReactionsViewStyle( + bubbleColorMine: Int = randomInt(), + bubbleColorTheirs: Int = randomInt(), + horizontalPadding: Int = randomInt(), + itemSize: Int = randomInt(), + bubbleHeight: Int = randomInt(), + bubbleRadius: Int = randomInt(), + largeTailBubbleCyOffset: Int = randomInt(), + largeTailBubbleRadius: Int = randomInt(), + largeTailBubbleOffset: Int = randomInt(), + smallTailBubbleCyOffset: Int = randomInt(), + smallTailBubbleRadius: Int = randomInt(), + smallTailBubbleOffset: Int = randomInt(), + reactionsColumn: Int = randomInt(), + verticalPadding: Int = randomInt(), +): EditReactionsViewStyle = EditReactionsViewStyle( + bubbleColorMine = bubbleColorMine, + bubbleColorTheirs = bubbleColorTheirs, + horizontalPadding = horizontalPadding, + itemSize = itemSize, + bubbleHeight = bubbleHeight, + bubbleRadius = bubbleRadius, + largeTailBubbleCyOffset = largeTailBubbleCyOffset, + largeTailBubbleRadius = largeTailBubbleRadius, + largeTailBubbleOffset = largeTailBubbleOffset, + smallTailBubbleCyOffset = smallTailBubbleCyOffset, + smallTailBubbleRadius = smallTailBubbleRadius, + smallTailBubbleOffset = smallTailBubbleOffset, + reactionsColumn = reactionsColumn, + verticalPadding = verticalPadding, +) + +public fun randomGiphyViewHolderStyle( + cardBackgroundColor: Int = randomInt(), + cardElevation: Float = randomFloat(), + cardButtonDividerColor: Int = randomInt(), + giphyIcon: Drawable = mock(), + labelTextStyle: TextStyle = mock(), + queryTextStyle: TextStyle = mock(), + cancelButtonTextStyle: TextStyle = mock(), + shuffleButtonTextStyle: TextStyle = mock(), + sendButtonTextStyle: TextStyle = mock(), +): GiphyViewHolderStyle = GiphyViewHolderStyle( + cardBackgroundColor = cardBackgroundColor, + cardElevation = cardElevation, + cardButtonDividerColor = cardButtonDividerColor, + giphyIcon = giphyIcon, + labelTextStyle = labelTextStyle, + queryTextStyle = queryTextStyle, + cancelButtonTextStyle = cancelButtonTextStyle, + shuffleButtonTextStyle = shuffleButtonTextStyle, + sendButtonTextStyle = sendButtonTextStyle, +) + +public fun randomAudioRecordPlayerViewStyle( + height: Int = randomInt(), + padding: ViewPadding = randomViewPadding(), + backgroundDrawable: Drawable? = null, + backgroundDrawableTint: Int? = randomInt(), + playbackProgressContainerSize: ViewSize = randomViewSize(), + playbackButtonSize: ViewSize = randomViewSize(), + playbackButtonPadding: ViewPadding = randomViewPadding(), + playbackButtonElevation: Int = randomInt(), + playbackButtonBackground: Drawable? = null, + playbackButtonBackgroundTint: Int? = randomInt(), + progressBarDrawable: Drawable? = null, + progressBarDrawableTint: Int? = randomInt(), + progressBarSize: ViewSize = randomViewSize(), + playIconDrawable: Drawable? = null, + playIconDrawableTint: Int? = randomInt(), + pauseIconDrawable: Drawable? = null, + pauseIconDrawableTint: Int? = randomInt(), + durationTextViewSize: ViewSize = randomViewSize(), + durationTextMarginStart: Int = randomInt(), + durationTextStyle: TextStyle = mock(), + waveBarHeight: Int = randomInt(), + waveBarMarginStart: Int = randomInt(), + waveBarColorPlayed: Int = randomInt(), + waveBarColorFuture: Int = randomInt(), + scrubberDrawable: Drawable? = null, + scrubberDrawableTint: Int? = randomInt(), + scrubberWidthDefault: Int = randomInt(), + scrubberWidthPressed: Int = randomInt(), + isFileIconContainerVisible: Boolean = randomBoolean(), + fileIconContainerWidth: Int = randomInt(), + audioFileIconDrawable: Drawable? = null, + speedButtonTextStyle: TextStyle = mock(), + speedButtonBackground: Drawable? = null, + speedButtonBackgroundTint: Int? = randomInt(), + speedButtonSize: ViewSize = randomViewSize(), + speedButtonElevation: Int = randomInt(), +): AudioRecordPlayerViewStyle = AudioRecordPlayerViewStyle( + height = height, + padding = padding, + backgroundDrawable = backgroundDrawable, + backgroundDrawableTint = backgroundDrawableTint, + playbackProgressContainerSize = playbackProgressContainerSize, + playbackButtonSize = playbackButtonSize, + playbackButtonPadding = playbackButtonPadding, + playbackButtonElevation = playbackButtonElevation, + playbackButtonBackground = playbackButtonBackground, + playbackButtonBackgroundTint = playbackButtonBackgroundTint, + progressBarDrawable = progressBarDrawable, + progressBarDrawableTint = progressBarDrawableTint, + progressBarSize = progressBarSize, + playIconDrawable = playIconDrawable, + playIconDrawableTint = playIconDrawableTint, + pauseIconDrawable = pauseIconDrawable, + pauseIconDrawableTint = pauseIconDrawableTint, + durationTextViewSize = durationTextViewSize, + durationTextMarginStart = durationTextMarginStart, + durationTextStyle = durationTextStyle, + waveBarHeight = waveBarHeight, + waveBarMarginStart = waveBarMarginStart, + waveBarColorPlayed = waveBarColorPlayed, + waveBarColorFuture = waveBarColorFuture, + scrubberDrawable = scrubberDrawable, + scrubberDrawableTint = scrubberDrawableTint, + scrubberWidthDefault = scrubberWidthDefault, + scrubberWidthPressed = scrubberWidthPressed, + isFileIconContainerVisible = isFileIconContainerVisible, + fileIconContainerWidth = fileIconContainerWidth, + audioFileIconDrawable = audioFileIconDrawable, + speedButtonTextStyle = speedButtonTextStyle, + speedButtonBackground = speedButtonBackground, + speedButtonBackgroundTint = speedButtonBackgroundTint, + speedButtonSize = speedButtonSize, + speedButtonElevation = speedButtonElevation, +) + +public fun randomViewSize( + width: Int = randomInt(), + height: Int = randomInt(), +): ViewSize = ViewSize(width, height) + +public fun randomViewPadding( + start: Int = randomInt(), + top: Int = randomInt(), + end: Int = randomInt(), + bottom: Int = randomInt(), +): ViewPadding = ViewPadding(start, top, end, bottom) + +public fun randomMessageReplyStyle( + messageBackgroundColorMine: Int = randomInt(), + messageBackgroundColorTheirs: Int = randomInt(), + linkBackgroundColorMine: Int = randomInt(), + linkBackgroundColorTheirs: Int = randomInt(), + textStyleMine: TextStyle = mock(), + textStyleTheirs: TextStyle = mock(), + linkStyleMine: TextStyle = mock(), + linkStyleTheirs: TextStyle = mock(), + messageStrokeColorMine: Int = randomInt(), + messageStrokeWidthMine: Float = randomFloat(), + messageStrokeColorTheirs: Int = randomInt(), + messageStrokeWidthTheirs: Float = randomFloat(), +): MessageReplyStyle = MessageReplyStyle( + messageBackgroundColorMine = messageBackgroundColorMine, + messageBackgroundColorTheirs = messageBackgroundColorTheirs, + linkBackgroundColorMine = linkBackgroundColorMine, + linkBackgroundColorTheirs = linkBackgroundColorTheirs, + textStyleMine = textStyleMine, + textStyleTheirs = textStyleTheirs, + linkStyleMine = linkStyleMine, + linkStyleTheirs = linkStyleTheirs, + messageStrokeColorMine = messageStrokeColorMine, + messageStrokeWidthMine = messageStrokeWidthMine, + messageStrokeColorTheirs = messageStrokeColorTheirs, + messageStrokeWidthTheirs = messageStrokeWidthTheirs, +) + +public fun randomUnreadLabelButtonStyle( + unreadLabelButtonEnabled: Boolean = randomBoolean(), + unreadLabelButtonColor: Int = randomInt(), + unreadLabelButtonRippleColor: Int = randomInt(), + unreadLabelButtonTextStyle: TextStyle = mock(), +): UnreadLabelButtonStyle = UnreadLabelButtonStyle( + unreadLabelButtonEnabled = unreadLabelButtonEnabled, + unreadLabelButtonColor = unreadLabelButtonColor, + unreadLabelButtonRippleColor = unreadLabelButtonRippleColor, + unreadLabelButtonTextStyle = unreadLabelButtonTextStyle, +) 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 new file mode 100644 index 00000000000..f74548fd1dc --- /dev/null +++ b/stream-chat-android-ui-components/src/test/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/MessageListViewExtensionsKtTest.kt @@ -0,0 +1,475 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.ui.feature.messages.list.internal + +import io.getstream.chat.android.models.AttachmentType +import io.getstream.chat.android.models.ChannelCapabilities +import io.getstream.chat.android.models.Message +import io.getstream.chat.android.models.SyncStatus +import io.getstream.chat.android.models.User +import io.getstream.chat.android.randomAttachment +import io.getstream.chat.android.randomBoolean +import io.getstream.chat.android.randomChannelCapabilities +import io.getstream.chat.android.randomMessage +import io.getstream.chat.android.randomString +import io.getstream.chat.android.randomSyncStatus +import io.getstream.chat.android.ui.feature.messages.list.MessageListViewStyle +import io.getstream.chat.android.ui.randomMessageListViewStyle +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 + +internal class MessageListViewExtensionsKtTest { + + @ParameterizedTest + @MethodSource("canReplyToMessageArguments") + fun `Verify canReplyToMessage() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + message: Message, + ownCapabilities: Set, + expectedResult: Boolean, + ) { + messageListViewStyle.canReplyToMessage(message, ownCapabilities) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canThreadReplyToMessageArguments") + fun `Verify canThreadReplyToMessage() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + message: Message, + ownCapabilities: Set, + expectedResult: Boolean, + ) { + messageListViewStyle.canThreadReplyToMessage(message, ownCapabilities) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canCopyMessageArguments") + fun `Verify canCopyMessage() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + message: Message, + expectedResult: Boolean, + ) { + messageListViewStyle.canCopyMessage(message) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canEditMessageArguments") + fun `Verify canEditMessage() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + currentUser: User?, + message: Message, + ownCapabilities: Set, + expectedResult: Boolean, + ) { + messageListViewStyle.canEditMessage(currentUser, message, ownCapabilities) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canDeleteMessageArguments") + fun `Verify canDeleteMessage() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + currentUser: User?, + message: Message, + ownCapabilities: Set, + expectedResult: Boolean, + ) { + messageListViewStyle.canDeleteMessage(currentUser, message, ownCapabilities) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canFlagMessageArguments") + fun `Verify canFlagMessage() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + currentUser: User?, + message: Message, + ownCapabilities: Set, + expectedResult: Boolean, + ) { + messageListViewStyle.canFlagMessage(currentUser, message, ownCapabilities) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canPinMessageArguments") + fun `Verify canPinMessage() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + message: Message, + ownCapabilities: Set, + expectedResult: Boolean, + ) { + messageListViewStyle.canPinMessage(message, ownCapabilities) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canBlockUserArguments") + fun `Verify canBlockUser() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + currentUser: User?, + message: Message, + expectedResult: Boolean, + ) { + messageListViewStyle.canBlockUser(currentUser, message) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canMarkAsUnreadArguments") + fun `Verify canMarkAsUnread() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + ownCapabilities: Set, + expectedResult: Boolean, + ) { + messageListViewStyle.canMarkAsUnread(ownCapabilities) `should be` expectedResult + } + + @ParameterizedTest + @MethodSource("canRetryMessageArguments") + fun `Verify canRetryMessage() extension function return proper value`( + messageListViewStyle: MessageListViewStyle, + currentUser: User?, + message: Message, + expectedResult: Boolean, + ) { + messageListViewStyle.canRetryMessage(currentUser, message) `should be` expectedResult + } + + companion object { + + private val currentUser = User(id = randomString()) + + @JvmStatic + fun canReplyToMessageArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(replyEnabled = false), + randomMessage(), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomMessage(syncStatus = randomSyncStatus(exclude = listOf(SyncStatus.COMPLETED))), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomMessage(), + randomChannelCapabilities(exclude = setOf(ChannelCapabilities.QUOTE_MESSAGE)), + false, + ), + Arguments.of( + randomMessageListViewStyle(replyEnabled = true), + randomMessage(syncStatus = SyncStatus.COMPLETED), + randomChannelCapabilities(include = setOf(ChannelCapabilities.QUOTE_MESSAGE)), + true, + ), + ) + + @JvmStatic + fun canMarkAsUnreadArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(markAsUnreadEnabled = false), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomChannelCapabilities(exclude = setOf(ChannelCapabilities.READ_EVENTS)), + false, + ), + Arguments.of( + randomMessageListViewStyle(markAsUnreadEnabled = true), + randomChannelCapabilities(include = setOf(ChannelCapabilities.READ_EVENTS)), + true, + ), + ) + + @JvmStatic + fun canPinMessageArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(pinMessageEnabled = false), + randomMessage(), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomMessage(syncStatus = randomSyncStatus(exclude = listOf(SyncStatus.COMPLETED))), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomMessage(), + randomChannelCapabilities(exclude = setOf(ChannelCapabilities.PIN_MESSAGE)), + false, + ), + Arguments.of( + randomMessageListViewStyle(pinMessageEnabled = true), + randomMessage(syncStatus = SyncStatus.COMPLETED), + randomChannelCapabilities(include = setOf(ChannelCapabilities.PIN_MESSAGE)), + true, + ), + ) + + @JvmStatic + fun canEditMessageArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(editMessageEnabled = false), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + currentUser.takeIf { randomBoolean() }, + randomMessage(command = AttachmentType.GIPHY), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + randomChannelCapabilities( + exclude = setOf( + ChannelCapabilities.UPDATE_OWN_MESSAGE, + ChannelCapabilities.UPDATE_ANY_MESSAGE, + ), + ), + false, + ), + Arguments.of( + randomMessageListViewStyle(editMessageEnabled = true), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + randomChannelCapabilities(include = setOf(ChannelCapabilities.UPDATE_ANY_MESSAGE)), + true, + ), + Arguments.of( + randomMessageListViewStyle(editMessageEnabled = true), + currentUser, + randomMessage(user = currentUser), + randomChannelCapabilities( + include = setOf(ChannelCapabilities.UPDATE_OWN_MESSAGE), + exclude = setOf(ChannelCapabilities.UPDATE_ANY_MESSAGE), + ), + true, + ), + ) + + @JvmStatic + fun canBlockUserArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(blockUserEnabled = false), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + currentUser, + randomMessage(user = currentUser), + false, + ), + Arguments.of( + randomMessageListViewStyle(blockUserEnabled = true), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + true, + ), + ) + + @JvmStatic + fun canRetryMessageArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(retryMessageEnabled = false), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + currentUser, + randomMessage(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + currentUser, + randomMessage( + user = currentUser, + syncStatus = randomSyncStatus(exclude = listOf(SyncStatus.FAILED_PERMANENTLY)), + ), + false, + ), + Arguments.of( + randomMessageListViewStyle(retryMessageEnabled = true), + currentUser, + randomMessage( + user = currentUser, + syncStatus = SyncStatus.FAILED_PERMANENTLY, + ), + true, + ), + ) + + @JvmStatic + fun canFlagMessageArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(flagEnabled = false), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + randomChannelCapabilities(exclude = setOf(ChannelCapabilities.FLAG_MESSAGE)), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + currentUser, + randomMessage(user = currentUser), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(flagEnabled = true), + currentUser, + randomMessage(), + randomChannelCapabilities(include = setOf(ChannelCapabilities.FLAG_MESSAGE)), + true, + ), + ) + + @JvmStatic + fun canDeleteMessageArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(deleteMessageEnabled = false), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + randomChannelCapabilities( + exclude = setOf( + ChannelCapabilities.DELETE_OWN_MESSAGE, + ChannelCapabilities.DELETE_ANY_MESSAGE, + ), + ), + false, + ), + Arguments.of( + randomMessageListViewStyle(deleteMessageEnabled = true), + currentUser.takeIf { randomBoolean() }, + randomMessage(), + randomChannelCapabilities(include = setOf(ChannelCapabilities.DELETE_ANY_MESSAGE)), + true, + ), + Arguments.of( + randomMessageListViewStyle(deleteMessageEnabled = true), + currentUser, + randomMessage(user = currentUser), + randomChannelCapabilities( + include = setOf(ChannelCapabilities.DELETE_OWN_MESSAGE), + exclude = setOf(ChannelCapabilities.DELETE_ANY_MESSAGE), + ), + true, + ), + ) + + @JvmStatic + fun canThreadReplyToMessageArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(threadsEnabled = false), + randomMessage(), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomMessage(syncStatus = randomSyncStatus(exclude = listOf(SyncStatus.COMPLETED))), + randomChannelCapabilities(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomMessage(), + randomChannelCapabilities(exclude = setOf(ChannelCapabilities.QUOTE_MESSAGE)), + false, + ), + Arguments.of( + randomMessageListViewStyle(threadsEnabled = true), + randomMessage(syncStatus = SyncStatus.COMPLETED), + randomChannelCapabilities(include = setOf(ChannelCapabilities.QUOTE_MESSAGE)), + true, + ), + ) + + @JvmStatic + fun canCopyMessageArguments() = listOf( + Arguments.of( + randomMessageListViewStyle(copyTextEnabled = false), + randomMessage(), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomMessage(text = ""), + false, + ), + Arguments.of( + randomMessageListViewStyle(), + randomMessage(attachments = listOf(randomAttachment(titleLink = null, ogUrl = null))), + false, + ), + Arguments.of( + randomMessageListViewStyle(copyTextEnabled = true), + randomMessage( + text = randomString(), + attachments = emptyList(), + ), + true, + ), + Arguments.of( + randomMessageListViewStyle(copyTextEnabled = true), + randomMessage( + text = randomString(), + attachments = listOf(randomAttachment(titleLink = randomString(), ogUrl = null)), + ), + true, + ), + Arguments.of( + randomMessageListViewStyle(copyTextEnabled = true), + randomMessage( + text = randomString(), + attachments = listOf(randomAttachment(titleLink = null, ogUrl = randomString())), + ), + true, + ), + ) + } +}