Skip to content

Commit

Permalink
[AND-322] Swipe to reply (Compose artifact) (#5627)
Browse files Browse the repository at this point in the history
* Reuse logic to show message options

* Add Tests

* Add ownCapabilities to MessageItemState

* Create new composable to swipe a content

* Create default content shown when swiping to reply to a message

* Use SwipeToReply on MessageList

* Add Tests

* Fix checkstyle

* Fix Checkstyle

* Update CHANGELOG.md

* Fix tests
  • Loading branch information
JcMinarro authored Feb 18, 2025
1 parent 9255bea commit 71e02e7
Show file tree
Hide file tree
Showing 23 changed files with 1,471 additions and 116 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@

### ✅ Added
- Add `MessageListViewModel::scrollToFirstUnreadMessage` method. [#5635](https://github.com/GetStream/stream-chat-android/pull/5635)
- Added Swipe To Reply feature to the Messages. [#5627](https://github.com/GetStream/stream-chat-android/pull/5627)

### ⚠️ Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ public fun AiMessagesScreen(
}
}

val onReply: (Message) -> Unit = { message -> composerViewModel.performMessageAction(Reply(message)) }
MessageList(
modifier = Modifier
.testTag("Stream_MessagesList")
Expand All @@ -250,17 +251,20 @@ public fun AiMessagesScreen(
},
onUserAvatarClick = onUserAvatarClick,
onMessageLinkClick = onMessageLinkClick,
onReply = onReply,
itemContent = { messageListItem ->
MessageContainer(
messageListItemState = messageListItem,
reactionSorting = reactionSorting,
onReply = onReply,
messageItemContent = { itemState ->
MessageItem(
messageItem = itemState,
reactionSorting = reactionSorting,
messageContentFactory = AiMessageContentFactory(),
onUserAvatarClick = { onUserAvatarClick.invoke(itemState.message.user) },
onLongItemClick = {},
onReply = onReply,
centerContent = { messageItemState ->
AiRegularMessageContent(
messageItem = messageItemState,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,9 @@ class MessagesActivity : BaseConnectedActivity() {
null -> Unit
}
},
onReply = { message ->
composerViewModel.performMessageAction(Reply(message))
},
)
}

Expand Down Expand Up @@ -320,7 +323,6 @@ class MessagesActivity : BaseConnectedActivity() {
messageOptions = defaultMessageOptionsState(
selectedMessage = selectedMessage,
currentUser = user,
isInThread = listViewModel.isInThread,
ownCapabilities = selectedMessageState.ownCapabilities,
),
message = selectedMessage,
Expand Down
27 changes: 18 additions & 9 deletions stream-chat-android-compose/api/stream-chat-android-compose.api

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@ import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import io.getstream.chat.android.client.utils.attachment.isGiphy
import io.getstream.chat.android.client.utils.message.isGiphy
import io.getstream.chat.android.compose.R
import io.getstream.chat.android.compose.state.messageoptions.MessageOptionItemState
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.util.extensions.canBlockUser
import io.getstream.chat.android.compose.util.extensions.canCopyMessage
import io.getstream.chat.android.compose.util.extensions.canDeleteMessage
import io.getstream.chat.android.compose.util.extensions.canEditMessage
import io.getstream.chat.android.compose.util.extensions.canFlagMessage
import io.getstream.chat.android.compose.util.extensions.canMarkAsUnread
import io.getstream.chat.android.compose.util.extensions.canPinMessage
import io.getstream.chat.android.compose.util.extensions.canReplyToMessage
import io.getstream.chat.android.compose.util.extensions.canRetryMessage
import io.getstream.chat.android.compose.util.extensions.canThreadReplyToMessage
import io.getstream.chat.android.compose.util.extensions.toSet
import io.getstream.chat.android.models.ChannelCapabilities
import io.getstream.chat.android.models.Message
Expand All @@ -46,7 +54,6 @@ import io.getstream.chat.android.ui.common.state.messages.Reply
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.uiutils.extension.hasLink

/**
* Displays all available [MessageOptionItem]s.
Expand Down Expand Up @@ -87,49 +94,24 @@ public fun MessageOptions(
*
* @param selectedMessage Currently selected message, used to callbacks.
* @param currentUser Current user, used to expose different states for messages.
* @param isInThread If the message is in a thread or not, to block off some options.
* @param ownCapabilities Set of capabilities the user is given for the current channel.
* For a full list @see [ChannelCapabilities].
*/
@Suppress("LongMethod")
@Composable
public fun defaultMessageOptionsState(
selectedMessage: Message,
currentUser: User?,
isInThread: Boolean,
ownCapabilities: Set<String>,
): List<MessageOptionItemState> {
if (selectedMessage.id.isEmpty()) {
return emptyList()
}

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)

val isThreadReplyPossible = !isInThread && isMessageSynced && canThreadReply
val isEditMessagePossible = ((isOwnMessage && canEditOwnMessage) || canEditAnyMessage) && !selectedMessage.isGiphy()
val isDeleteMessagePossible = canDeleteAnyMessage || (isOwnMessage && canDeleteOwnMessage)

// options menu item visibility
val visibility = ChatTheme.messageOptionsTheme.optionVisibility

return listOfNotNull(
if (visibility.isRetryMessageVisible && isOwnMessage && isMessageFailed) {
if (visibility.canRetryMessage(currentUser, selectedMessage)) {
MessageOptionItemState(
title = R.string.stream_compose_resend_message,
iconPainter = painterResource(R.drawable.stream_compose_ic_resend),
Expand All @@ -140,7 +122,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isReplyVisible && isMessageSynced && canQuoteMessage) {
if (visibility.canReplyToMessage(selectedMessage, ownCapabilities)) {
MessageOptionItemState(
title = R.string.stream_compose_reply,
iconPainter = painterResource(R.drawable.stream_compose_ic_reply),
Expand All @@ -151,7 +133,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isThreadReplyVisible && isThreadReplyPossible) {
if (visibility.canThreadReplyToMessage(selectedMessage, ownCapabilities)) {
MessageOptionItemState(
title = R.string.stream_compose_thread_reply,
iconPainter = painterResource(R.drawable.stream_compose_ic_thread),
Expand All @@ -162,7 +144,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isMarkAsUnreadVisible && canMarkAsUnread) {
if (visibility.canMarkAsUnread(ownCapabilities)) {
MessageOptionItemState(
title = R.string.stream_compose_mark_as_unread,
iconPainter = painterResource(R.drawable.stream_compose_ic_mark_as_unread),
Expand All @@ -173,7 +155,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isCopyTextVisible && (isTextOnlyMessage || hasLinks)) {
if (visibility.canCopyMessage(selectedMessage)) {
MessageOptionItemState(
title = R.string.stream_compose_copy_message,
iconPainter = painterResource(R.drawable.stream_compose_ic_copy),
Expand All @@ -184,7 +166,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isEditMessageVisible && isEditMessagePossible) {
if (visibility.canEditMessage(currentUser, selectedMessage, ownCapabilities)) {
MessageOptionItemState(
title = R.string.stream_compose_edit_message,
iconPainter = painterResource(R.drawable.stream_compose_ic_edit),
Expand All @@ -195,7 +177,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isFlagMessageVisible && canFlagMessage && !isOwnMessage) {
if (visibility.canFlagMessage(currentUser, selectedMessage, ownCapabilities)) {
MessageOptionItemState(
title = R.string.stream_compose_flag_message,
iconPainter = painterResource(R.drawable.stream_compose_ic_flag),
Expand All @@ -206,7 +188,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isPinMessageVisible && isMessageSynced && canPinMessage) {
if (visibility.canPinMessage(selectedMessage, ownCapabilities)) {
MessageOptionItemState(
title = if (selectedMessage.pinned) R.string.stream_compose_unpin_message else R.string.stream_compose_pin_message,
action = Pin(selectedMessage),
Expand All @@ -223,7 +205,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isBlockUserVisible && !isOwnMessage) {
if (visibility.canBlockUser(currentUser, selectedMessage)) {
val isSenderBlocked = currentUser?.blockedUserIds?.contains(selectedMessageUserId) == true
val title = if (isSenderBlocked) {
R.string.stream_compose_unblock_user
Expand All @@ -245,7 +227,7 @@ public fun defaultMessageOptionsState(
} else {
null
},
if (visibility.isDeleteMessageVisible && isDeleteMessagePossible) {
if (visibility.canDeleteMessage(currentUser, selectedMessage, ownCapabilities)) {
MessageOptionItemState(
title = R.string.stream_compose_delete_message,
iconPainter = painterResource(R.drawable.stream_compose_ic_delete),
Expand Down Expand Up @@ -320,7 +302,6 @@ private fun MessageOptionsPreview(
val messageOptionsStateList = defaultMessageOptionsState(
selectedMessage = selectedMMessage,
currentUser = currentUser,
isInThread = false,
ownCapabilities = ChannelCapabilities.toSet(),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import io.getstream.chat.android.compose.ui.components.composer.InputField
import io.getstream.chat.android.compose.ui.components.poll.AddAnswerDialog
import io.getstream.chat.android.compose.ui.theme.ChatTheme
import io.getstream.chat.android.compose.ui.util.isErrorOrFailed
import io.getstream.chat.android.compose.util.extensions.toSet
import io.getstream.chat.android.models.ChannelCapabilities
import io.getstream.chat.android.models.Message
import io.getstream.chat.android.models.Option
import io.getstream.chat.android.models.Poll
Expand Down Expand Up @@ -546,6 +548,7 @@ private fun PollMessageContentPreview() {
messageItem = MessageItemState(
message = io.getstream.chat.android.previewdata.PreviewMessageData.messageWithPoll,
isMine = true,
ownCapabilities = ChannelCapabilities.toSet(),
),
)

Expand All @@ -562,6 +565,7 @@ private fun PollMessageContentPreview() {
messageItem = MessageItemState(
message = io.getstream.chat.android.previewdata.PreviewMessageData.messageWithError,
isMine = true,
ownCapabilities = ChannelCapabilities.toSet(),
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,6 @@ private fun SelectedMessageMenuPreview() {
val messageOptionsStateList = defaultMessageOptionsState(
selectedMessage = Message(),
currentUser = User(),
isInThread = false,
ownCapabilities = ChannelCapabilities.toSet(),
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,7 @@ public fun MessagesScreen(
onUserAvatarClick = onUserAvatarClick,
onMessageLinkClick = onMessageLinkClick,
onUserMentionClick = onUserMentionClick,
onReply = { message -> composerViewModel.performMessageAction(Reply(message)) },
onMediaGalleryPreviewResult = remember(listViewModel, composerViewModel) {
{
result ->
Expand Down Expand Up @@ -465,7 +466,6 @@ private fun BoxScope.MessagesScreenMenus(
val newMessageOptions = defaultMessageOptionsState(
selectedMessage = selectedMessage,
currentUser = user,
isInThread = isInThread,
ownCapabilities = ownCapabilities,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ public fun LazyItemScope.MessageContainer(
onLinkClick: ((Message, String) -> Unit)? = null,
onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {},
onUserMentionClick: (User) -> Unit = {},
onReply: (Message) -> Unit = {},
dateSeparatorContent: @Composable LazyItemScope.(DateSeparatorItemState) -> Unit = { dateSeparatorItem ->
with(ChatTheme.componentFactory) {
MessageListDateSeparatorItemContent(dateSeparatorItem = dateSeparatorItem)
Expand Down Expand Up @@ -153,6 +154,7 @@ public fun LazyItemScope.MessageContainer(
onMessageLinkClick = onLinkClick,
onUserMentionClick = onUserMentionClick,
onAddAnswer = onAddAnswer,
onReply = onReply,
)
}
} else {
Expand All @@ -176,6 +178,7 @@ public fun LazyItemScope.MessageContainer(
onLinkClick = onLinkClick,
onUserMentionClick = onUserMentionClick,
onAddAnswer = onAddAnswer,
onReply = onReply,
)
}
},
Expand Down Expand Up @@ -377,6 +380,7 @@ internal fun DefaultMessageItem(
onUserAvatarClick: () -> Unit,
onMediaGalleryPreviewResult: (MediaGalleryPreviewResult?) -> Unit = {},
onUserMentionClick: (User) -> Unit = {},
onReply: (Message) -> Unit = {},
) {
MessageItem(
messageItem = messageItem,
Expand All @@ -398,5 +402,6 @@ internal fun DefaultMessageItem(
onMediaGalleryPreviewResult = onMediaGalleryPreviewResult,
onUserMentionClick = onUserMentionClick,
onAddAnswer = onAddAnswer,
onReply = onReply,
)
}
Loading

0 comments on commit 71e02e7

Please sign in to comment.