From 960101dd234bf1027a0dd8f7240dad24aab7f0fa Mon Sep 17 00:00:00 2001 From: Petar Velikov Date: Tue, 22 Oct 2024 12:38:30 +0200 Subject: [PATCH 01/40] [PBE-3749] Implement ChatClient::markThreadRead operation. (#5447) * [PBE-3749] Implement ChatClient::markThreadRead operation. * [PBE-3749] Update CHANGELOG for markThreadRead. * [PBE-3749] Fix failing test. * [PBE-3749] Separate `markThreadRead` from `markRead`. --------- Co-authored-by: PetarVelikov --- CHANGELOG.md | 2 + .../api/stream-chat-android-client.api | 2 + .../chat/android/client/ChatClient.kt | 16 +++++ .../chat/android/client/api/ChatApi.kt | 7 +++ .../chat/android/client/api2/MoshiChatApi.kt | 10 ++- .../api2/model/requests/MarkReadRequest.kt | 3 +- .../android/client/channel/ChannelClient.kt | 10 +++ .../client/chatclient/WhenMarkReadThread.kt | 62 +++++++++++++++++++ 8 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenMarkReadThread.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index bb92fdfbd50..d3ff387bcbb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ - Avoid multiple `ChatClient::connectUser` invocations by sharing the `Call`. [#5439](https://github.com/GetStream/stream-chat-android/pull/5439) ### ✅ Added +- Add `ChatClient::markThreadRead` to mark a given thread as read. [#5447](https://github.com/GetStream/stream-chat-android/pull/5447) +- Add `ChannelClient::markThreadRead` to mark a given thread as read. [#5447](https://github.com/GetStream/stream-chat-android/pull/5447) ### ⚠️ Changed diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index 7953fb30943..e01068c1764 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -97,6 +97,7 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun markAllRead ()Lio/getstream/result/call/Call; public final fun markMessageRead (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markRead (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; + public final fun markThreadRead (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markUnread (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun muteChannel (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun muteChannel (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/getstream/result/call/Call; @@ -646,6 +647,7 @@ public final class io/getstream/chat/android/client/channel/ChannelClient { public static synthetic fun keystroke$default (Lio/getstream/chat/android/client/channel/ChannelClient;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/result/call/Call; public final fun markMessageRead (Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markRead ()Lio/getstream/result/call/Call; + public final fun markThreadRead (Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markUnread (Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun mute ()Lio/getstream/result/call/Call; public final fun mute (Ljava/lang/Integer;)Lio/getstream/result/call/Call; diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt index a85486abfb6..033b36e552e 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt @@ -2196,6 +2196,22 @@ internal constructor( } } + /** + * Marks a given thread as read. + * + * @param channelType The type of the channel in which the thread resides. + * @param channelId The ID of the channel in which the thread resides. + * @param threadId The ID of the thread to mark as read. + */ + @CheckResult + public fun markThreadRead( + channelType: String, + channelId: String, + threadId: String, + ): Call { + return api.markThreadRead(channelType, channelId, threadId) + } + @CheckResult public fun showChannel(channelType: String, channelId: String): Call { return api.showChannel(channelType, channelId) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt index 9a3c5c0e86d..c349c4a7125 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt @@ -275,6 +275,13 @@ internal interface ChatApi { messageId: String = "", ): Call + @CheckResult + fun markThreadRead( + channelType: String, + channelId: String, + threadId: String, + ): Call + @CheckResult fun markUnread( channelType: String, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt index b09c55f896f..7f4abf9b36c 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt @@ -701,7 +701,15 @@ constructor( return channelApi.markRead( channelType = channelType, channelId = channelId, - request = MarkReadRequest(messageId), + request = MarkReadRequest(message_id = messageId), + ).toUnitCall() + } + + override fun markThreadRead(channelType: String, channelId: String, threadId: String): Call { + return channelApi.markRead( + channelType = channelType, + channelId = channelId, + request = MarkReadRequest(thread_id = threadId), ).toUnitCall() } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/MarkReadRequest.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/MarkReadRequest.kt index 82a624bb627..0692202860c 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/MarkReadRequest.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/MarkReadRequest.kt @@ -20,5 +20,6 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class MarkReadRequest( - val message_id: String, + val message_id: String = "", + val thread_id: String? = null, ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt index 7bc44409800..5a14c2a2359 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt @@ -403,6 +403,16 @@ public class ChannelClient internal constructor( return client.markMessageRead(channelType, channelId, messageId) } + /** + * Marks a given thread in the channel as read. + * + * @param threadId The ID of the thread to mark as read. + */ + @CheckResult + public fun markThreadRead(threadId: String): Call { + return client.markThreadRead(channelType, channelId, threadId) + } + @CheckResult public fun markUnread(messageId: String): Call { return client.markUnread(channelType, channelId, messageId) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenMarkReadThread.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenMarkReadThread.kt new file mode 100644 index 00000000000..1d6709fbb9e --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenMarkReadThread.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014-2024 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.client.chatclient + +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.test.TestCall +import io.getstream.chat.android.test.callFrom +import io.getstream.result.Error +import io.getstream.result.Result +import io.getstream.result.call.Call +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever + +internal class WhenMarkReadThread : BaseChatClientTest() { + + @Test + fun `Given markRead api call successful ChatClient should return success result`() = runTest { + val apiResult = callFrom { } + val sut = Fixture().givenMarkReadApiResult(apiResult).get() + + val result = sut.markThreadRead("channelType", "channelId", "threadId").await() + + result shouldBeInstanceOf Result.Success::class + } + + @Test + fun `Given markRead api call fails ChatClient should return error result`() = runTest { + val apiResult = TestCall(Result.Failure(Error.GenericError("Error"))) + val sut = Fixture().givenMarkReadApiResult(apiResult).get() + + val result = sut.markThreadRead("channelType", "channelId", "threadId").await() + + result shouldBeInstanceOf Result.Failure::class + } + + private inner class Fixture { + + fun givenMarkReadApiResult(result: Call) = apply { + whenever(api.markThreadRead(any(), any(), any())) doReturn result + } + + fun get(): ChatClient = chatClient + } +} From 31909e3a68dd0d54368f49b0fdf11396ea9ade1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 11 Oct 2024 12:50:38 +0200 Subject: [PATCH 02/40] Create a new method to know if a message belong to a thread --- .../api/stream-chat-android-client.api | 1 + .../chat/android/client/utils/message/MessageUtils.kt | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index e01068c1764..0a2f81eee07 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -2879,6 +2879,7 @@ public final class io/getstream/chat/android/client/utils/internal/toggle/Toggle } public final class io/getstream/chat/android/client/utils/message/MessageUtils { + public static final fun belongsToThread (Lio/getstream/chat/android/models/Message;)Z public static final fun hasAudioRecording (Lio/getstream/chat/android/models/Message;)Z public static final fun isDeleted (Lio/getstream/chat/android/models/Message;)Z public static final fun isEphemeral (Lio/getstream/chat/android/models/Message;)Z diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt index 77257c21bb4..a44f4ddb980 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/utils/message/MessageUtils.kt @@ -155,6 +155,11 @@ public fun Message.isThreadStart(): Boolean = threadParticipants.isNotEmpty() */ public fun Message.isThreadReply(): Boolean = !parentId.isNullOrEmpty() +/** + * @return If the message belongs to a thread. + */ +public fun Message.belongsToThread(): Boolean = this.isThreadStart() || this.isThreadReply() + /** * @return If the message contains quoted message. */ From 299bcc26f490f6846a6c1fc678339543dd8dcaac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 11 Oct 2024 17:50:02 +0200 Subject: [PATCH 03/40] Create a method to access to a thread from any message that belong to the thread --- .../api/stream-chat-android-ui-common.api | 1 + .../feature/messages/list/MessageListController.kt | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index 3ba493817f7..4fb992daece 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -185,6 +185,7 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public final fun muteUser (Ljava/lang/String;Ljava/lang/Integer;)V public static synthetic fun muteUser$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)V public final fun onCleared ()V + public final fun openRelatedThread (Lio/getstream/chat/android/models/Message;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun performGiphyAction (Lio/getstream/chat/android/ui/common/state/messages/list/GiphyAction;)V public final fun performMessageAction (Lio/getstream/chat/android/ui/common/state/messages/MessageAction;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun pinMessage (Lio/getstream/chat/android/models/Message;)V diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt index 7d8cd55dead..4dbf7de1bc3 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt @@ -31,6 +31,7 @@ import io.getstream.chat.android.client.utils.message.isGiphy import io.getstream.chat.android.client.utils.message.isModerationBounce import io.getstream.chat.android.client.utils.message.isModerationError import io.getstream.chat.android.client.utils.message.isSystem +import io.getstream.chat.android.client.utils.message.isThreadStart import io.getstream.chat.android.core.internal.InternalStreamChatApi import io.getstream.chat.android.core.internal.coroutines.DispatcherProvider import io.getstream.chat.android.core.internal.exhaustive @@ -1146,6 +1147,18 @@ public class MessageListController( } } + /** + * Open the thread for the given message. + * If the message is a thread start, it will open the thread. + * If the message is a reply, it will open the thread for the parent message. + */ + public suspend fun openRelatedThread(message: Message) { + when (message.isThreadStart()) { + true -> enterThreadMode(message) + else -> message.parentId?.let { enterThreadSequential(it) } + } + } + /** * Changes the current [_mode] to be [MessageMode.MessageThread] and uses [ChatClient] to get the [ThreadState] for * the current thread. From dcb0dce4d1ebb9b0a46d874c47661d4afe925d4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 11 Oct 2024 17:51:22 +0200 Subject: [PATCH 04/40] Show Thread Footnote on reply messages on the channel --- .../ui/components/messages/MessageFooter.kt | 19 +++++++++++-------- .../compose/ui/messages/list/MessageItem.kt | 4 ++-- .../messages/MessageListViewModel.kt | 2 +- .../src/main/res/values-en/strings.xml | 4 ---- .../src/main/res/values/strings.xml | 8 +++++--- 5 files changed, 19 insertions(+), 18 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageFooter.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageFooter.kt index 06cb688ca22..20cebf474da 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageFooter.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/MessageFooter.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import io.getstream.chat.android.client.utils.message.belongsToThread import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.DateFormatType import io.getstream.chat.android.compose.ui.components.Timestamp @@ -52,19 +53,21 @@ public fun MessageFooter( messageItem: MessageItemState, ) { val message = messageItem.message - val hasThread = message.threadParticipants.isNotEmpty() val alignment = ChatTheme.messageAlignmentProvider.provideMessageAlignment(messageItem) - if (hasThread && !messageItem.isInThread) { - val replyCount = message.replyCount + if (message.belongsToThread() && !messageItem.isInThread) { + val threadFooterText = when (message.replyCount) { + 0 -> LocalContext.current.resources.getString(R.string.stream_compose_thread_reply) + else -> LocalContext.current.resources.getQuantityString( + R.plurals.stream_compose_message_list_thread_footnote, + message.replyCount, + message.replyCount, + ) + } MessageThreadFooter( participants = message.threadParticipants, messageAlignment = alignment, - text = LocalContext.current.resources.getQuantityString( - R.plurals.stream_compose_message_list_thread_footnote, - replyCount, - replyCount, - ), + text = threadFooterText, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index c96668028b2..22993f8c000 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt @@ -55,11 +55,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp +import io.getstream.chat.android.client.utils.message.belongsToThread import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.client.utils.message.isGiphyEphemeral import io.getstream.chat.android.client.utils.message.isPinned import io.getstream.chat.android.client.utils.message.isPoll -import io.getstream.chat.android.client.utils.message.isThreadStart import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.state.mediagallerypreview.MediaGalleryPreviewResult import io.getstream.chat.android.compose.state.reactionoptions.ReactionOptionItemState @@ -196,7 +196,7 @@ public fun MessageItem( Modifier.combinedClickable( interactionSource = remember { MutableInteractionSource() }, indication = null, - onClick = { if (message.isThreadStart()) onThreadClick(message) }, + onClick = { if (message.belongsToThread()) onThreadClick(message) }, onLongClick = { if (!message.isUploading()) onLongItemClick(message) }, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt index 85e5c3359ff..ae4427fcd63 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt @@ -250,7 +250,7 @@ public class MessageListViewModel( */ public fun openMessageThread(message: Message) { viewModelScope.launch { - messageListController.enterThreadMode(message) + messageListController.openRelatedThread(message) } } diff --git a/stream-chat-android-compose/src/main/res/values-en/strings.xml b/stream-chat-android-compose/src/main/res/values-en/strings.xml index 88631047081..8902d10ea9c 100644 --- a/stream-chat-android-compose/src/main/res/values-en/strings.xml +++ b/stream-chat-android-compose/src/main/res/values-en/strings.xml @@ -64,10 +64,6 @@ Send Cancel Shuffle - - Thread Reply - %d Thread Replies - %d Reply %d Replies diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index cbec8b5a76c..c98e1541562 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -65,9 +65,11 @@ Cancel Shuffle Edited - - Thread Reply - %d Thread Replies + Thread Reply + %d Thread Replies + + @string/stream_compose_message_list_thread_footnote_thread_reply + @string/stream_compose_message_list_thread_footnote_thread_replies %d Reply From 773dbca210e126b3a9b543873a69e06f3bc859fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 11 Oct 2024 19:30:43 +0200 Subject: [PATCH 05/40] Show Thread Footnote on reply messages on the channel (XML) --- .../api/stream-chat-android-ui-components.api | 16 ++++++++++ .../feature/messages/list/MessageListView.kt | 29 ++++++++++++++----- .../adapter/view/internal/FootnoteView.kt | 11 +++++-- .../decorator/internal/FootnoteDecorator.kt | 10 +++---- .../messages/MessageListViewModel.kt | 17 +++++++++++ .../messages/MessageListViewModelBinding.kt | 1 + .../main/res/values/strings_message_list.xml | 8 +++-- 7 files changed, 73 insertions(+), 19 deletions(-) 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 16bc3d682a2..22d7c275049 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 @@ -2225,6 +2225,7 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis public final fun setOnUserClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnUserClickListener;)V public final fun setOnUserReactionClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnUserReactionClickListener;)V public final fun setOnViewPollResultClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnViewPollResultClickListener;)V + public final fun setOpenThreadHandler (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OpenThreadHandler;)V public final fun setOwnCapabilities (Ljava/util/Set;)V public final fun setReactionViewClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$ReactionViewClickListener;)V public final fun setReactionsEnabled (Z)V @@ -2476,6 +2477,10 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public abstract fun onViewPollResultClick (Lio/getstream/chat/android/models/Poll;)Z } +public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OpenThreadHandler { + public abstract fun onOpenThread (Lio/getstream/chat/android/models/Message;)V +} + public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$ReactionViewClickListener { public abstract fun onReactionViewClick (Lio/getstream/chat/android/models/Message;)V } @@ -4553,6 +4558,17 @@ public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListVi public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$OpenThread : io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event { + public fun (Lio/getstream/chat/android/models/Message;)V + public final fun component1 ()Lio/getstream/chat/android/models/Message; + public final fun copy (Lio/getstream/chat/android/models/Message;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$OpenThread; + public static synthetic fun copy$default (Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$OpenThread;Lio/getstream/chat/android/models/Message;ILjava/lang/Object;)Lio/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$OpenThread; + public fun equals (Ljava/lang/Object;)Z + public final fun getMessage ()Lio/getstream/chat/android/models/Message; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event$PinMessage : io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel$Event { public fun (Lio/getstream/chat/android/models/Message;)V public final fun component1 ()Lio/getstream/chat/android/models/Message; 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 0a9b37ab1b0..5ecebbf706a 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 @@ -37,6 +37,7 @@ import io.getstream.chat.android.client.ChatClient import io.getstream.chat.android.client.utils.attachment.isGiphy import io.getstream.chat.android.client.utils.attachment.isImage import io.getstream.chat.android.client.utils.attachment.isVideo +import io.getstream.chat.android.client.utils.message.belongsToThread import io.getstream.chat.android.client.utils.message.isModerationError import io.getstream.chat.android.client.utils.message.isThreadReply import io.getstream.chat.android.core.ExperimentalStreamChatApi @@ -197,6 +198,9 @@ public class MessageListView : ConstraintLayout { private var threadStartHandler = ThreadStartHandler { throw IllegalStateException("onStartThreadHandler must be set.") } + private var openThreadHandler = OpenThreadHandler { + throw IllegalStateException("onStartThreadHandler must be set.") + } private var replyMessageClickListener = OnReplyMessageClickListener { // no-op false @@ -341,8 +345,8 @@ public class MessageListView : ConstraintLayout { val replyTo = message.replyTo when { - message.replyCount > 0 -> { - threadStartHandler.onStartThread(message) + message.belongsToThread() -> { + openThreadHandler.onOpenThread(message) true } @@ -452,12 +456,8 @@ public class MessageListView : ConstraintLayout { private val defaultThreadClickListener = OnThreadClickListener { message -> - if (message.replyCount > 0) { - threadStartHandler.onStartThread(message) - true - } else { - false - } + message.belongsToThread() + .also { if (it) openThreadHandler.onOpenThread(message) } } private val attachmentGalleryDestination = @@ -1856,6 +1856,15 @@ public class MessageListView : ConstraintLayout { this.threadStartHandler = threadStartHandler } + /** + * Sets the handler used when opening a thread. + * + * @param openThreadHandler The handler to use. + */ + public fun setOpenThreadHandler(openThreadHandler: OpenThreadHandler) { + this.openThreadHandler = openThreadHandler + } + /** * Sets the handler used when the message is going to be flagged. * @@ -2462,6 +2471,10 @@ public class MessageListView : ConstraintLayout { public fun onStartThread(message: Message) } + public fun interface OpenThreadHandler { + public fun onOpenThread(message: Message) + } + public fun interface GiphySendHandler { public fun onSendGiphy(action: GiphyAction) } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/internal/FootnoteView.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/internal/FootnoteView.kt index da9e9f10b15..e2c9caf66e1 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/internal/FootnoteView.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/view/internal/FootnoteView.kt @@ -112,9 +112,14 @@ internal class FootnoteView : LinearLayoutCompat { root.isVisible = true threadsOrnamentLeft.isVisible = !isMine threadsOrnamentRight.isVisible = isMine - - threadRepliesButton.text = - resources.getQuantityString(R.plurals.stream_ui_message_list_thread_reply, replyCount, replyCount) + threadRepliesButton.text = when (replyCount) { + 0 -> resources.getString(R.string.stream_ui_message_list_thread_footnote_thread_reply) + else -> resources.getQuantityString( + R.plurals.stream_ui_message_list_thread_footnote, + replyCount, + replyCount, + ) + } threadRepliesButton.setTextStyle(style.textStyleThreadCounter) } setupUserAvatars(isMine, threadParticipants) diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/decorator/internal/FootnoteDecorator.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/decorator/internal/FootnoteDecorator.kt index bcfae43da16..e07520ef14a 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/decorator/internal/FootnoteDecorator.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/decorator/internal/FootnoteDecorator.kt @@ -21,6 +21,7 @@ import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isVisible +import io.getstream.chat.android.client.utils.message.belongsToThread import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.client.utils.message.isEphemeral import io.getstream.chat.android.client.utils.message.isGiphy @@ -243,11 +244,10 @@ internal class FootnoteDecorator( anchorView: View, data: MessageListItem.MessageItem, ) { - val isSimpleFootnoteMode = data.message.replyCount == 0 || data.isThreadMode - if (isSimpleFootnoteMode) { - setupSimpleFootnoteWithRootConstraints(footnoteView, root, anchorView, data) - } else { - setupThreadFootnote(footnoteView, root, threadGuideline, data) + val isThreadFootnote = data.message.belongsToThread() && !data.isThreadMode + when (isThreadFootnote) { + true -> setupThreadFootnote(footnoteView, root, threadGuideline, data) + false -> setupSimpleFootnoteWithRootConstraints(footnoteView, root, anchorView, data) } footnoteView.applyGravity(data.isMine) } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt index afafbf214cc..695f16ebdad 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModel.kt @@ -228,6 +228,7 @@ public class MessageListViewModel( is Event.BottomEndRegionReached -> onBottomEndRegionReached(event.messageId) is Event.LastMessageRead -> messageListController.markLastMessageRead() is Event.ThreadModeEntered -> onThreadModeEntered(event.parentMessage) + is Event.OpenThread -> onOpenThread(event.message) is Event.BackButtonPressed -> onBackButtonPressed() is Event.MarkAsUnreadMessage -> messageListController.markUnread(event.message) is Event.DeleteMessage -> messageListController.deleteMessage(event.message, event.hard) @@ -411,6 +412,17 @@ public class MessageListViewModel( } } + /** + * Handles an event to open a thread. + * + * @param message The message to open the thread for. + */ + private fun onOpenThread(message: Message) { + viewModelScope.launch { + messageListController.openRelatedThread(message) + } + } + /** * Handles reacting to messages while taking into account if unique reactions are enforced. * @@ -527,6 +539,11 @@ public class MessageListViewModel( */ public data class ThreadModeEntered(val parentMessage: Message) : Event() + /** + * When the user + */ + public data class OpenThread(val message: Message) : Event() + /** * When the user deletes a message. * diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelBinding.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelBinding.kt index 26b0644e63b..2fffd036aa9 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelBinding.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/viewmodel/messages/MessageListViewModelBinding.kt @@ -66,6 +66,7 @@ public fun MessageListViewModel.bindView( view.setLastMessageReadHandler { onEvent(LastMessageRead) } view.setMessageDeleteHandler { onEvent(DeleteMessage(it, hard = false)) } view.setThreadStartHandler { onEvent(ThreadModeEntered(it)) } + view.setOpenThreadHandler { onEvent(MessageListViewModel.Event.OpenThread(it)) } view.setMessageFlagHandler { onEvent( FlagMessage( diff --git a/stream-chat-android-ui-components/src/main/res/values/strings_message_list.xml b/stream-chat-android-ui-components/src/main/res/values/strings_message_list.xml index 02393eb798f..8f5c9a6b29b 100644 --- a/stream-chat-android-ui-components/src/main/res/values/strings_message_list.xml +++ b/stream-chat-android-ui-components/src/main/res/values/strings_message_list.xml @@ -32,9 +32,11 @@ %1$s / %2$s Pinned by %s You - - Thread Reply - %d Thread Replies + Thread reply + %d Thread Replies + + @string/stream_ui_message_list_thread_footnote_thread_reply + @string/stream_ui_message_list_thread_footnote_thread_replies %d Reply From 5a326e92f5561b14e574de5e4b8e01f23605cd04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 4 Oct 2024 17:46:25 +0200 Subject: [PATCH 06/40] Add Answers to Poll --- .../api/stream-chat-android-core.api | 40 +++++++++++++++---- .../io/getstream/chat/android/models/Poll.kt | 13 ++++++ 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/stream-chat-android-core/api/stream-chat-android-core.api b/stream-chat-android-core/api/stream-chat-android-core.api index 93c65295cd4..9545c83ac09 100644 --- a/stream-chat-android-core/api/stream-chat-android-core.api +++ b/stream-chat-android-core/api/stream-chat-android-core.api @@ -123,6 +123,27 @@ public final class io/getstream/chat/android/models/AndFilterObject : io/getstre public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/models/Answer { + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Ljava/util/Date;Lio/getstream/chat/android/models/User;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/util/Date; + public final fun component5 ()Ljava/util/Date; + public final fun component6 ()Lio/getstream/chat/android/models/User; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Ljava/util/Date;Lio/getstream/chat/android/models/User;)Lio/getstream/chat/android/models/Answer; + public static synthetic fun copy$default (Lio/getstream/chat/android/models/Answer;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Date;Ljava/util/Date;Lio/getstream/chat/android/models/User;ILjava/lang/Object;)Lio/getstream/chat/android/models/Answer; + public fun equals (Ljava/lang/Object;)Z + public final fun getCreatedAt ()Ljava/util/Date; + public final fun getId ()Ljava/lang/String; + public final fun getPollId ()Ljava/lang/String; + public final fun getText ()Ljava/lang/String; + public final fun getUpdatedAt ()Ljava/util/Date; + public final fun getUser ()Lio/getstream/chat/android/models/User; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/models/App { public fun (Ljava/lang/String;Lio/getstream/chat/android/models/FileUploadConfig;Lio/getstream/chat/android/models/FileUploadConfig;)V public final fun component1 ()Ljava/lang/String; @@ -1365,7 +1386,8 @@ public final class io/getstream/chat/android/models/OrFilterObject : io/getstrea } public final class io/getstream/chat/android/models/Poll { - public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/VotingVisibility;ZIZZLjava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/Date;Ljava/util/Date;Z)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/VotingVisibility;ZIZZLjava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/Date;Ljava/util/Date;ZLjava/util/List;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/VotingVisibility;ZIZZLjava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/Date;Ljava/util/Date;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component10 ()Ljava/util/Map; public final fun component11 ()Ljava/util/List; @@ -1373,6 +1395,7 @@ public final class io/getstream/chat/android/models/Poll { public final fun component13 ()Ljava/util/Date; public final fun component14 ()Ljava/util/Date; public final fun component15 ()Z + public final fun component16 ()Ljava/util/List; public final fun component2 ()Ljava/lang/String; public final fun component3 ()Ljava/lang/String; public final fun component4 ()Ljava/util/List; @@ -1381,11 +1404,12 @@ public final class io/getstream/chat/android/models/Poll { public final fun component7 ()I public final fun component8 ()Z public final fun component9 ()Z - public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/VotingVisibility;ZIZZLjava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/Date;Ljava/util/Date;Z)Lio/getstream/chat/android/models/Poll; - public static synthetic fun copy$default (Lio/getstream/chat/android/models/Poll;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/VotingVisibility;ZIZZLjava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/Date;Ljava/util/Date;ZILjava/lang/Object;)Lio/getstream/chat/android/models/Poll; + public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/VotingVisibility;ZIZZLjava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/Date;Ljava/util/Date;ZLjava/util/List;)Lio/getstream/chat/android/models/Poll; + public static synthetic fun copy$default (Lio/getstream/chat/android/models/Poll;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/List;Lio/getstream/chat/android/models/VotingVisibility;ZIZZLjava/util/Map;Ljava/util/List;Ljava/util/List;Ljava/util/Date;Ljava/util/Date;ZLjava/util/List;ILjava/lang/Object;)Lio/getstream/chat/android/models/Poll; public fun equals (Ljava/lang/Object;)Z public final fun getAllowAnswers ()Z public final fun getAllowUserSuggestedOptions ()Z + public final fun getAnswers ()Ljava/util/List; public final fun getClosed ()Z public final fun getCreatedAt ()Ljava/util/Date; public final fun getDescription ()Ljava/lang/String; @@ -1405,8 +1429,8 @@ public final class io/getstream/chat/android/models/Poll { } public final class io/getstream/chat/android/models/PollConfig { - public fun (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/getstream/chat/android/models/VotingVisibility;ZIZ)V - public synthetic fun (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/getstream/chat/android/models/VotingVisibility;ZIZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/getstream/chat/android/models/VotingVisibility;ZIZZ)V + public synthetic fun (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/getstream/chat/android/models/VotingVisibility;ZIZZILkotlin/jvm/internal/DefaultConstructorMarker;)V public final fun component1 ()Ljava/lang/String; public final fun component2 ()Ljava/util/List; public final fun component3 ()Ljava/lang/String; @@ -1414,9 +1438,11 @@ public final class io/getstream/chat/android/models/PollConfig { public final fun component5 ()Z public final fun component6 ()I public final fun component7 ()Z - public final fun copy (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/getstream/chat/android/models/VotingVisibility;ZIZ)Lio/getstream/chat/android/models/PollConfig; - public static synthetic fun copy$default (Lio/getstream/chat/android/models/PollConfig;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/getstream/chat/android/models/VotingVisibility;ZIZILjava/lang/Object;)Lio/getstream/chat/android/models/PollConfig; + public final fun component8 ()Z + public final fun copy (Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/getstream/chat/android/models/VotingVisibility;ZIZZ)Lio/getstream/chat/android/models/PollConfig; + public static synthetic fun copy$default (Lio/getstream/chat/android/models/PollConfig;Ljava/lang/String;Ljava/util/List;Ljava/lang/String;Lio/getstream/chat/android/models/VotingVisibility;ZIZZILjava/lang/Object;)Lio/getstream/chat/android/models/PollConfig; public fun equals (Ljava/lang/Object;)Z + public final fun getAllowAnswers ()Z public final fun getAllowUserSuggestedOptions ()Z public final fun getDescription ()Ljava/lang/String; public final fun getEnforceUniqueVote ()Z diff --git a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Poll.kt b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Poll.kt index d0bbc88cb5b..33607fe7804 100644 --- a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Poll.kt +++ b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/Poll.kt @@ -56,6 +56,7 @@ public data class Poll( val createdAt: Date, val updatedAt: Date, val closed: Boolean, + val answers: List = emptyList(), ) { /** @@ -67,6 +68,16 @@ public data class Poll( public fun getVotes(option: Option): List = votes.filter { it.optionId == option.id } } +@Immutable +public data class Answer( + val id: String, + val pollId: String, + val text: String, + val createdAt: Date, + val updatedAt: Date, + val user: User?, +) + /** * The Option object represents an answer option in a poll. * @@ -90,6 +101,7 @@ public data class Option( * @property enforceUniqueVote If set to true, a user can only vote once. Default is true. * @property maxVotesAllowed The maximum number of votes a user can cast. Default is 1. * @property allowUserSuggestedOptions If set to true, users can suggest new options. Default is false. + * @property allowAnswers If set to true, users can send answers. Default is false. */ public data class PollConfig( val name: String, @@ -99,6 +111,7 @@ public data class PollConfig( val enforceUniqueVote: Boolean = true, val maxVotesAllowed: Int = 1, val allowUserSuggestedOptions: Boolean = false, + val allowAnswers: Boolean = false, ) /** From ebaa21dc2f6831dc016ffc58155583e53ed281e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 4 Oct 2024 17:52:25 +0200 Subject: [PATCH 07/40] Implement new endpoint to cast an answer --- .../api/stream-chat-android-client.api | 1 + .../chat/android/client/ChatClient.kt | 9 ++++ .../chat/android/client/api/ChatApi.kt | 3 ++ .../chat/android/client/api2/MoshiChatApi.kt | 23 ++++++++- .../client/api2/mapping/PollMapping.kt | 50 ++++++++++++++++++- .../client/api2/model/dto/PollsDtos.kt | 2 + .../api2/model/requests/PollsRequest.kt | 4 +- 7 files changed, 88 insertions(+), 4 deletions(-) diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index 0a2f81eee07..519e98c210c 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -17,6 +17,7 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun appSettings ()Lio/getstream/result/call/Call; public final fun banUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/getstream/result/call/Call; public final fun blockUser (Ljava/lang/String;)Lio/getstream/result/call/Call; + public final fun castPollAnswer (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun castPollVote (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Option;)Lio/getstream/result/call/Call; public final fun channel (Ljava/lang/String;)Lio/getstream/chat/android/client/channel/ChannelClient; public final fun channel (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/chat/android/client/channel/ChannelClient; diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt index 033b36e552e..0d506fc7f7f 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt @@ -1536,6 +1536,15 @@ internal constructor( return api.castPollVote(messageId, pollId, option.id) } + @CheckResult + public fun castPollAnswer( + messageId: String, + pollId: String, + answer: String, + ): Call { + return api.castPollAnswer(messageId, pollId, answer) + } + /** * Remove a vote for a poll in a message. * diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt index c349c4a7125..978759f6791 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt @@ -493,6 +493,9 @@ internal interface ChatApi { @CheckResult fun castPollVote(messageId: String, pollId: String, optionId: String): Call + @CheckResult + fun castPollAnswer(messageId: String, pollId: String, answer: String): Call + @CheckResult fun removePollVote(messageId: String, pollId: String, voteId: String): Call diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt index 7f4abf9b36c..dad1f94aa7a 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt @@ -1176,11 +1176,31 @@ constructor( messageId: String, pollId: String, optionId: String, + ): Call = castVote( + messageId = messageId, + pollId = pollId, + vote = UpstreamVoteDto(option_id = optionId), + ) + + override fun castPollAnswer( + messageId: String, + pollId: String, + answer: String, + ): Call = castVote( + messageId = messageId, + pollId = pollId, + vote = UpstreamVoteDto(answer_text = answer), + ) + + private fun castVote( + messageId: String, + pollId: String, + vote: UpstreamVoteDto, ): Call { return pollsApi.castPollVote( messageId, pollId, - PollVoteRequest(UpstreamVoteDto(optionId)), + PollVoteRequest(vote), ).map { it.vote.toDomain(currentUserIdProvider()) } } @@ -1221,6 +1241,7 @@ constructor( enforce_unique_vote = pollConfig.enforceUniqueVote, max_votes_allowed = pollConfig.maxVotesAllowed, allow_user_suggested_options = pollConfig.allowUserSuggestedOptions, + allow_answers = pollConfig.allowAnswers, ), ).map { it.poll.toDomain(currentUserIdProvider()) } } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt index 1f223d82af4..d038d9c66ee 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt @@ -19,6 +19,7 @@ package io.getstream.chat.android.client.api2.mapping import io.getstream.chat.android.client.api2.model.dto.DownstreamOptionDto import io.getstream.chat.android.client.api2.model.dto.DownstreamPollDto import io.getstream.chat.android.client.api2.model.dto.DownstreamVoteDto +import io.getstream.chat.android.models.Answer import io.getstream.chat.android.models.Option import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.UserId @@ -32,8 +33,38 @@ import io.getstream.chat.android.models.VotingVisibility */ internal fun DownstreamPollDto.toDomain(currentUserId: UserId?): Poll { val ownUserId = currentUserId ?: own_votes.firstOrNull()?.user?.id - val votes = latest_votes_by_option?.values?.flatten()?.map { it.toDomain(currentUserId) } ?: emptyList() - val ownVotes = (own_votes.map { it.toDomain(currentUserId) } + votes.filter { it.user?.id == ownUserId }) + val votes = latest_votes_by_option + ?.values + ?.flatten() + ?.filter { it.is_answer != true } + ?.map { it.toDomain(currentUserId) } ?: emptyList() + val ownVotes = ( + own_votes + .filter { it.is_answer != true } + .map { it.toDomain(currentUserId) } + + votes.filter { it.user?.id == ownUserId } + ) + .associateBy { it.id } + .values + .toList() + + val answer = ( + ( + latest_votes_by_option + ?.values + ?.flatten() + ?.filter { it.is_answer == true } + ?.map { it.toAnswerDomain(currentUserId) } + ?: emptyList() + ) + + own_votes + .filter { it.is_answer == true } + .map { it.toAnswerDomain(currentUserId) } + ) + .also { + println("JcLog: answerCount: ${it.size}") + it.forEach { println("JcLog: Answer: (${it.user?.name}) -> ${it.text}") } + } .associateBy { it.id } .values .toList() @@ -54,6 +85,7 @@ internal fun DownstreamPollDto.toDomain(currentUserId: UserId?): Poll { createdAt = created_at, updatedAt = updated_at, closed = is_closed, + answers = answer, ) } @@ -81,6 +113,20 @@ internal fun DownstreamVoteDto.toDomain(currentUserId: UserId?): Vote = Vote( user = user?.toDomain(currentUserId), ) +/** + * Transforms DownstreamVoteDto to Answer + * + * @return Answer + */ +internal fun DownstreamVoteDto.toAnswerDomain(currentUserId: UserId?): Answer = Answer( + id = id, + pollId = poll_id, + text = answer_text ?: "", + createdAt = created_at, + updatedAt = updated_at, + user = user?.toDomain(currentUserId), +) + /** * Transforms String to VotingVisibility * diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PollsDtos.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PollsDtos.kt index 0c7f771fbcb..660c0212110 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PollsDtos.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PollsDtos.kt @@ -51,6 +51,8 @@ internal data class DownstreamVoteDto( val updated_at: Date, val user: DownstreamUserDto?, val user_id: String?, + val is_answer: Boolean?, + val answer_text: String?, ) /** diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/PollsRequest.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/PollsRequest.kt index 5f9a2a7c552..835570b6180 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/PollsRequest.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/PollsRequest.kt @@ -48,6 +48,7 @@ internal data class PollRequest( val enforce_unique_vote: Boolean, val max_votes_allowed: Int, val allow_user_suggested_options: Boolean, + val allow_answers: Boolean, ) { companion object { @@ -95,5 +96,6 @@ internal data class PollUpdateRequest( */ @JsonClass(generateAdapter = true) internal data class UpstreamVoteDto( - val option_id: String, + val option_id: String? = null, + val answer_text: String? = null, ) From 85e82d166edf1c3ac993d05cef2ea0063e1db18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 4 Oct 2024 18:10:42 +0200 Subject: [PATCH 08/40] Store answers on DB --- .../converter/internal/AnswerConverter.kt | 59 +++++++++++++++++++ .../database/internal/ChatDatabase.kt | 4 +- .../domain/message/internal/MessageMapper.kt | 23 ++++++++ .../domain/message/internal/Poll.kt | 11 ++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/converter/internal/AnswerConverter.kt diff --git a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/converter/internal/AnswerConverter.kt b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/converter/internal/AnswerConverter.kt new file mode 100644 index 00000000000..a1ed8e3441d --- /dev/null +++ b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/converter/internal/AnswerConverter.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2014-2022 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.offline.repository.database.converter.internal + +import androidx.room.TypeConverter +import com.squareup.moshi.adapter +import io.getstream.chat.android.offline.repository.domain.message.internal.AnswerEntity + +internal class AnswerConverter { + + @OptIn(ExperimentalStdlibApi::class) + private val entityAdapter = moshi.adapter() + + @OptIn(ExperimentalStdlibApi::class) + private val entityListAdapter = moshi.adapter>() + + @TypeConverter + fun stringToAnswer(data: String?): AnswerEntity? { + return data?.let { + entityAdapter.fromJson(it) + } + } + + @TypeConverter + fun answerToString(entity: AnswerEntity?): String? { + return entity?.let { + entityAdapter.toJson(it) + } + } + + @TypeConverter + fun stringToAnswerList(data: String?): List? { + if (data.isNullOrEmpty() || data == "null") { + return emptyList() + } + return entityListAdapter.fromJson(data) + } + + @TypeConverter + fun answerListToString(entities: List?): String? { + return entities?.let { + entityListAdapter.toJson(it) + } + } +} diff --git a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/internal/ChatDatabase.kt b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/internal/ChatDatabase.kt index 906af415e06..bd3513e3614 100644 --- a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/internal/ChatDatabase.kt +++ b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/database/internal/ChatDatabase.kt @@ -22,6 +22,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.sqlite.db.SupportSQLiteDatabase +import io.getstream.chat.android.offline.repository.database.converter.internal.AnswerConverter import io.getstream.chat.android.offline.repository.database.converter.internal.DateConverter import io.getstream.chat.android.offline.repository.database.converter.internal.ExtraDataConverter import io.getstream.chat.android.offline.repository.database.converter.internal.FilterObjectConverter @@ -74,10 +75,11 @@ import io.getstream.chat.android.offline.repository.domain.user.internal.UserEnt SyncStateEntity::class, PollEntity::class, ], - version = 77, + version = 78, exportSchema = false, ) @TypeConverters( + AnswerConverter::class, FilterObjectConverter::class, ListConverter::class, MapConverter::class, diff --git a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/message/internal/MessageMapper.kt b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/message/internal/MessageMapper.kt index 2cdc399297b..de4fe057822 100644 --- a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/message/internal/MessageMapper.kt +++ b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/message/internal/MessageMapper.kt @@ -16,6 +16,7 @@ package io.getstream.chat.android.offline.repository.domain.message.internal +import io.getstream.chat.android.models.Answer import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.Option import io.getstream.chat.android.models.Poll @@ -225,6 +226,7 @@ internal fun Poll.toEntity(): PollEntity = PollEntity( voteCountsByOption = voteCountsByOption, ownVotes = ownVotes.map { it.toEntity() }, closed = closed, + answers = answers.map { it.toEntity() }, ) internal fun Option.toEntity(): OptionEntity = OptionEntity( @@ -241,6 +243,15 @@ internal fun Vote.toEntity(): VoteEntity = VoteEntity( userId = user?.id, ) +private fun Answer.toEntity(): AnswerEntity = AnswerEntity( + id = id, + pollId = pollId, + text = text, + createdAt = createdAt, + updatedAt = updatedAt, + userId = user?.id, +) + private fun VotingVisibility.toEntity(): String = when (this) { VotingVisibility.ANONYMOUS -> "anonymous" VotingVisibility.PUBLIC -> "public" @@ -264,6 +275,7 @@ internal suspend fun PollEntity.toModel( voteCountsByOption = voteCountsByOption, ownVotes = ownVotes.map { it.toModel(getUser) }, closed = closed, + answers = answers.map { it.toModel(getUser) }, ) private fun OptionEntity.toModel(): Option = Option( @@ -282,6 +294,17 @@ private suspend fun VoteEntity.toModel( user = userId?.let { getUser(it) }, ) +private suspend fun AnswerEntity.toModel( + getUser: suspend (userId: String) -> User, +): Answer = Answer( + id = id, + pollId = pollId, + text = text, + createdAt = createdAt, + updatedAt = updatedAt, + user = userId?.let { getUser(it) }, +) + private fun String.toVotingVisibility(): VotingVisibility = when (this) { "public" -> VotingVisibility.PUBLIC "anonymous" -> VotingVisibility.ANONYMOUS diff --git a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/message/internal/Poll.kt b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/message/internal/Poll.kt index bcde794495d..7209835a86d 100644 --- a/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/message/internal/Poll.kt +++ b/stream-chat-android-offline/src/main/java/io/getstream/chat/android/offline/repository/domain/message/internal/Poll.kt @@ -40,6 +40,7 @@ internal class PollEntity( val createdAt: Date, val updatedAt: Date, val closed: Boolean, + val answers: List, ) @JsonClass(generateAdapter = true) @@ -58,4 +59,14 @@ internal class VoteEntity( val userId: String?, ) +@JsonClass(generateAdapter = true) +internal class AnswerEntity( + val id: String, + val pollId: String, + val text: String, + val createdAt: Date, + val updatedAt: Date, + val userId: String?, +) + internal const val POLL_ENTITY_TABLE_NAME = "stream_chat_poll" From 81dcbfb7ee69abd45e116a7fc89bbe48a258940e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 4 Oct 2024 18:12:44 +0200 Subject: [PATCH 09/40] Create new event for Answers --- .../api/stream-chat-android-client.api | 25 +++++++++++++++++++ .../client/api2/mapping/EventMapping.kt | 18 +++++++++++++ .../client/api2/model/dto/EventDtos.kt | 9 +++++++ .../android/client/channel/ChannelClient.kt | 2 ++ .../chat/android/client/events/ChatEvent.kt | 15 +++++++++++ .../client/parser2/adapters/EventAdapter.kt | 17 +++++++++++-- .../internal/EventHandlerSequential.kt | 13 ++++++++++ .../logic/channel/internal/ChannelLogic.kt | 9 +++++++ 8 files changed, 106 insertions(+), 2 deletions(-) diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index 519e98c210c..821644964e4 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -910,6 +910,31 @@ public abstract class io/getstream/chat/android/client/errors/cause/StreamSdkExc public synthetic fun (Ljava/lang/Throwable;Lkotlin/jvm/internal/DefaultConstructorMarker;)V } +public final class io/getstream/chat/android/client/events/AnswerCastedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasPoll { + public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/util/Date; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Ljava/lang/String; + public final fun component5 ()Ljava/lang/String; + public final fun component6 ()Ljava/lang/String; + public final fun component7 ()Lio/getstream/chat/android/models/Poll; + public final fun component8 ()Lio/getstream/chat/android/models/Answer; + public final fun copy (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;)Lio/getstream/chat/android/client/events/AnswerCastedEvent; + public static synthetic fun copy$default (Lio/getstream/chat/android/client/events/AnswerCastedEvent;Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Answer;ILjava/lang/Object;)Lio/getstream/chat/android/client/events/AnswerCastedEvent; + public fun equals (Ljava/lang/Object;)Z + public fun getChannelId ()Ljava/lang/String; + public fun getChannelType ()Ljava/lang/String; + public fun getCid ()Ljava/lang/String; + public fun getCreatedAt ()Ljava/util/Date; + public final fun getNewAnswer ()Lio/getstream/chat/android/models/Answer; + public fun getPoll ()Lio/getstream/chat/android/models/Poll; + public fun getRawCreatedAt ()Ljava/lang/String; + public fun getType ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/client/events/ChannelDeletedEvent : io/getstream/chat/android/client/events/CidEvent, io/getstream/chat/android/client/events/HasChannel { public fun (Ljava/lang/String;Ljava/util/Date;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Channel;Lio/getstream/chat/android/models/User;)V public final fun component1 ()Ljava/lang/String; diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt index 7b66fc65963..bef3b8fd996 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt @@ -18,6 +18,7 @@ package io.getstream.chat.android.client.api2.mapping +import io.getstream.chat.android.client.api2.model.dto.AnswerCastedEventDto import io.getstream.chat.android.client.api2.model.dto.ChannelDeletedEventDto import io.getstream.chat.android.client.api2.model.dto.ChannelHiddenEventDto import io.getstream.chat.android.client.api2.model.dto.ChannelTruncatedEventDto @@ -73,6 +74,7 @@ import io.getstream.chat.android.client.api2.model.dto.UserUpdatedEventDto import io.getstream.chat.android.client.api2.model.dto.VoteCastedEventDto import io.getstream.chat.android.client.api2.model.dto.VoteChangedEventDto import io.getstream.chat.android.client.api2.model.dto.VoteRemovedEventDto +import io.getstream.chat.android.client.events.AnswerCastedEvent import io.getstream.chat.android.client.events.ChannelDeletedEvent import io.getstream.chat.android.client.events.ChannelHiddenEvent import io.getstream.chat.android.client.events.ChannelTruncatedEvent @@ -194,6 +196,7 @@ internal fun ChatEventDto.toDomain(currentUserId: UserId?): ChatEvent { is PollUpdatedEventDto -> toDomain(currentUserId) is VoteCastedEventDto -> toDomain(currentUserId) is VoteChangedEventDto -> toDomain(currentUserId) + is AnswerCastedEventDto -> toDomain(currentUserId) is VoteRemovedEventDto -> toDomain(currentUserId) } } @@ -791,6 +794,21 @@ private fun VoteCastedEventDto.toDomain(currentUserId: UserId?): VoteCastedEvent ) } +private fun AnswerCastedEventDto.toDomain(currentUserId: UserId?): AnswerCastedEvent { + val newAnswer = poll_vote.toAnswerDomain(currentUserId) + val (channelType, channelId) = cid.cidToTypeAndId() + return AnswerCastedEvent( + type = type, + createdAt = created_at.date, + rawCreatedAt = created_at.rawDate, + cid = cid, + channelType = channelType, + channelId = channelId, + poll = poll.toDomain(currentUserId), + newAnswer = newAnswer, + ) +} + private fun VoteChangedEventDto.toDomain(currentUserId: UserId?): VoteChangedEvent { val pollVote = poll_vote.toDomain(currentUserId) val (channelType, channelId) = cid.cidToTypeAndId() diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt index 47a860c1e24..a462bc0bc8b 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/EventDtos.kt @@ -495,6 +495,15 @@ internal data class VoteCastedEventDto( val poll_vote: DownstreamVoteDto, ) : ChatEventDto() +@JsonClass(generateAdapter = true) +internal data class AnswerCastedEventDto( + val type: String, + val cid: String, + val created_at: ExactDate, + val poll: DownstreamPollDto, + val poll_vote: DownstreamVoteDto, +) : ChatEventDto() + @JsonClass(generateAdapter = true) internal data class VoteChangedEventDto( val type: String, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt index 5a14c2a2359..f0943e13b1d 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt @@ -24,6 +24,7 @@ import io.getstream.chat.android.client.api.models.PinnedMessagesPagination import io.getstream.chat.android.client.api.models.QueryChannelRequest import io.getstream.chat.android.client.api.models.SendActionRequest import io.getstream.chat.android.client.api.models.WatchChannelRequest +import io.getstream.chat.android.client.events.AnswerCastedEvent import io.getstream.chat.android.client.events.ChannelDeletedEvent import io.getstream.chat.android.client.events.ChannelHiddenEvent import io.getstream.chat.android.client.events.ChannelTruncatedEvent @@ -257,6 +258,7 @@ public class ChannelClient internal constructor( is VoteCastedEvent -> event.cid == cid is VoteChangedEvent -> event.cid == cid is VoteRemovedEvent -> event.cid == cid + is AnswerCastedEvent -> event.cid == cid is UnknownEvent -> event.rawData["cid"] == cid is HealthEvent, is NotificationChannelMutesUpdatedEvent, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt index c633d514fd6..e171347e746 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/events/ChatEvent.kt @@ -18,6 +18,7 @@ package io.getstream.chat.android.client.events import io.getstream.chat.android.client.clientstate.DisconnectCause import io.getstream.chat.android.client.errors.ChatError +import io.getstream.chat.android.models.Answer import io.getstream.chat.android.models.Channel import io.getstream.chat.android.models.Member import io.getstream.chat.android.models.Message @@ -715,6 +716,20 @@ public data class VoteCastedEvent( val newVote: Vote, ) : CidEvent(), HasPoll +/** + * Triggered when a vote is casted. + */ +public data class AnswerCastedEvent( + override val type: String, + override val createdAt: Date, + override val rawCreatedAt: String?, + override val cid: String, + override val channelType: String, + override val channelId: String, + override val poll: Poll, + val newAnswer: Answer, +) : CidEvent(), HasPoll + /** * Triggered when a vote is changed. */ diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/EventAdapter.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/EventAdapter.kt index 03f45263ee5..f22909f4923 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/EventAdapter.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/EventAdapter.kt @@ -22,6 +22,7 @@ import com.squareup.moshi.JsonWriter import com.squareup.moshi.Moshi import com.squareup.moshi.Types import com.squareup.moshi.rawType +import io.getstream.chat.android.client.api2.model.dto.AnswerCastedEventDto import io.getstream.chat.android.client.api2.model.dto.ChannelDeletedEventDto import io.getstream.chat.android.client.api2.model.dto.ChannelHiddenEventDto import io.getstream.chat.android.client.api2.model.dto.ChannelTruncatedEventDto @@ -145,6 +146,7 @@ internal class EventDtoAdapter( private val pollClosedEventAdapter = moshi.adapter(PollClosedEventDto::class.java) private val voteCastedEventAdapter = moshi.adapter(VoteCastedEventDto::class.java) private val voteChangedEventAdapter = moshi.adapter(VoteChangedEventDto::class.java) + private val answerCastedEventAdapter = moshi.adapter(AnswerCastedEventDto::class.java) private val voteRemovedEventAdapter = moshi.adapter(VoteRemovedEventDto::class.java) @Suppress("LongMethod", "ComplexMethod", "ReturnCount") @@ -216,8 +218,14 @@ internal class EventDtoAdapter( EventType.POLL_UPDATED -> pollUpdatedEventAdapter EventType.POLL_DELETED -> pollDeletedEventAdapter EventType.POLL_CLOSED -> pollClosedEventAdapter - EventType.POLL_VOTE_CASTED -> voteCastedEventAdapter - EventType.POLL_VOTE_CHANGED -> voteChangedEventAdapter + EventType.POLL_VOTE_CASTED -> when (map.containsAnswer()) { + true -> answerCastedEventAdapter + else -> voteCastedEventAdapter + } + EventType.POLL_VOTE_CHANGED -> when (map.containsAnswer()) { + true -> answerCastedEventAdapter + else -> voteChangedEventAdapter + } EventType.POLL_VOTE_REMOVED -> voteRemovedEventAdapter else -> // Custom case, early return return UnknownEventDto( @@ -231,6 +239,11 @@ internal class EventDtoAdapter( return adapter.fromJsonValue(map) } + private fun Map.containsAnswer(): Boolean { + return (((this["poll_vote"] as? Map)?.get("is_answer") as? Boolean) ?: false) + .also { println("JcLog: containsAnswer -> $it") } + } + override fun toJson(writer: JsonWriter, value: ChatEventDto?) { error("Can't convert this event to Json $value") } diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt index 45629292f53..53f5107fb61 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt @@ -18,6 +18,7 @@ package io.getstream.chat.android.state.event.handler.internal import androidx.annotation.VisibleForTesting import io.getstream.chat.android.client.ChatEventListener +import io.getstream.chat.android.client.events.AnswerCastedEvent import io.getstream.chat.android.client.events.ChannelDeletedEvent import io.getstream.chat.android.client.events.ChannelHiddenEvent import io.getstream.chat.android.client.events.ChannelTruncatedEvent @@ -715,6 +716,18 @@ internal class EventHandlerSequential( ), ) } + is AnswerCastedEvent -> { + val answers = ( + batch.getPoll(event.poll.id)?.answers + ?.associateBy { it.id } + ?: emptyMap() + ) + (event.newAnswer.id to event.newAnswer) + batch.addPoll( + event.poll.copy( + answers = answers.values.toList(), + ), + ) + } else -> Unit } } diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt index 86105bb4ad7..918e67a6ba5 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt @@ -21,6 +21,7 @@ import io.getstream.chat.android.client.api.models.Pagination import io.getstream.chat.android.client.api.models.QueryChannelRequest import io.getstream.chat.android.client.api.models.WatchChannelRequest import io.getstream.chat.android.client.channel.state.ChannelState +import io.getstream.chat.android.client.events.AnswerCastedEvent import io.getstream.chat.android.client.events.ChannelDeletedEvent import io.getstream.chat.android.client.events.ChannelHiddenEvent import io.getstream.chat.android.client.events.ChannelTruncatedEvent @@ -712,6 +713,14 @@ internal class ChannelLogic( ), ) } + is AnswerCastedEvent -> { + val answers = event.poll.answers.associateBy { it.id } + (event.newAnswer.id to event.newAnswer) + channelStateLogic.upsertPoll( + event.poll.copy( + answers = answers.values.toList(), + ), + ) + } } } From e588587161dc15848b4ae2fa157c9af9641b2ef1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 4 Oct 2024 18:18:06 +0200 Subject: [PATCH 10/40] Create new method on UI Common to cast an answer --- .../api/stream-chat-android-ui-common.api | 1 + .../messages/list/MessageListController.kt | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index 4fb992daece..42f187122b9 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -134,6 +134,7 @@ public final class io/getstream/chat/android/ui/common/feature/messages/list/Mes public final fun banUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V public static synthetic fun banUser$default (Lio/getstream/chat/android/ui/common/feature/messages/list/MessageListController;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)V public final fun blockUser (Ljava/lang/String;)V + public final fun castAnswer (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V public final fun castVote (Ljava/lang/String;Ljava/lang/String;Lio/getstream/chat/android/models/Option;)V public final fun clearNewMessageState ()V public final fun closePoll (Lio/getstream/chat/android/models/Poll;)V diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt index 4dbf7de1bc3..b275d2e8892 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/MessageListController.kt @@ -1810,6 +1810,21 @@ public class MessageListController( }) } + /** + * Cast an answer for a poll in a message. + * + * @param messageId The message id where the poll is. + * @param pollId The poll id. + * @param answer The answer to cast. + */ + public fun castAnswer( + messageId: String, + pollId: String, + answer: String, + ) { + chatClient.castPollAnswer(messageId, pollId, answer).enqueue() + } + /** * Remove a vote for a poll in a message. * From 7029d4c9c11d7a5464cef6d297ecddbeb112ec5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 4 Oct 2024 18:14:32 +0200 Subject: [PATCH 11/40] Create new UI to show answers --- .../compose/ui/components/poll/PollAnswers.kt | 272 ++++++++++++++++++ .../src/main/res/values/strings.xml | 2 + 2 files changed, 274 insertions(+) create mode 100644 stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt new file mode 100644 index 00000000000..fe7ad7a4f4d --- /dev/null +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-chat-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.getstream.chat.android.compose.ui.components.poll + +import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.AlertDialog +import androidx.compose.material.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Popup +import io.getstream.chat.android.compose.R +import io.getstream.chat.android.compose.ui.components.avatar.UserAvatar +import io.getstream.chat.android.compose.ui.components.composer.InputField +import io.getstream.chat.android.compose.ui.theme.ChatTheme +import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel +import io.getstream.chat.android.models.Answer +import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll + +@Suppress("LongMethod") +@Composable +public fun PollAnswersDialog( + selectedPoll: SelectedPoll, + listViewModel: MessageListViewModel, + onDismissRequest: () -> Unit, + onBackPressed: () -> Unit, +) { + val state = remember { + MutableTransitionState(false).apply { + // Start the animation immediately. + targetState = true + } + } + val showAddAnswerDialog = remember { mutableStateOf(false) } + if (showAddAnswerDialog.value) { + AddAnswerDialog( + onDismiss = { showAddAnswerDialog.value = false }, + onNewAnswer = { newAnswer -> + listViewModel.castAnswer(selectedPoll.message, selectedPoll.poll, newAnswer) + }, + ) + } + Popup( + alignment = Alignment.BottomCenter, + onDismissRequest = onDismissRequest, + ) { + @Suppress("MagicNumber") + AnimatedVisibility( + visibleState = state, + enter = fadeIn() + slideInVertically( + animationSpec = tween(400), + initialOffsetY = { fullHeight -> fullHeight / 2 }, + ), + exit = fadeOut(animationSpec = tween(200)) + + slideOutVertically(animationSpec = tween(400)), + label = "poll answers dialog", + ) { + val poll = selectedPoll.poll + + BackHandler { onBackPressed.invoke() } + + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(ChatTheme.colors.appBackground), + ) { + item { + PollDialogHeader( + title = stringResource(id = R.string.stream_compose_poll_answers), + onBackPressed = onBackPressed, + ) + } + + items( + items = poll.answers, + key = { answer -> answer.id }, + ) { answer -> + PollAnswersItem(answer = answer) + } + + item { Spacer(modifier = Modifier.height(16.dp)) } + + if (!poll.closed) { + item { + Box( + modifier = Modifier + .clickable { showAddAnswerDialog.value = true } + .fillMaxWidth() + .padding(horizontal = 16.dp) + .background( + color = ChatTheme.colors.inputBackground, + shape = RoundedCornerShape( + topStart = 12.dp, + topEnd = 12.dp, + bottomStart = 12.dp, + bottomEnd = 12.dp, + ), + ), + contentAlignment = Alignment.Center, + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 11.dp), + text = stringResource(id = R.string.stream_compose_add_answer), + textAlign = TextAlign.Center, + color = ChatTheme.colors.primaryAccent, + fontSize = 16.sp, + ) + } + } + } + } + } + } +} + +@Composable +internal fun PollAnswersItem( + answer: Answer, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .background( + color = ChatTheme.colors.inputBackground, + shape = RoundedCornerShape( + topStart = 12.dp, + topEnd = 12.dp, + bottomStart = 12.dp, + bottomEnd = 12.dp, + ), + ) + .padding(horizontal = 16.dp, vertical = 16.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + modifier = Modifier + .weight(1f), + text = answer.text, + color = ChatTheme.colors.textHighEmphasis, + fontWeight = FontWeight.Bold, + fontSize = 16.sp, + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + Row(verticalAlignment = Alignment.CenterVertically) { + val user = answer.user + if (user != null) { + UserAvatar( + modifier = Modifier.size(20.dp), + user = user, + showOnlineIndicator = false, + ) + + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp) + .weight(1f), + text = user.name, + color = ChatTheme.colors.textHighEmphasis, + fontSize = 14.sp, + ) + } + + Text( + modifier = Modifier.padding(bottom = 2.dp), + text = ChatTheme.dateFormatter.formatDate(answer.createdAt), + color = ChatTheme.colors.textLowEmphasis, + fontSize = 14.sp, + ) + } + } +} + +@Composable +internal fun AddAnswerDialog( + onDismiss: () -> Unit, + onNewAnswer: (newOption: String) -> Unit, +) { + val newOption = remember { mutableStateOf("") } + val focusRequester = remember { FocusRequester() } + AlertDialog( + title = { Text(text = stringResource(R.string.stream_compose_add_answer)) }, + text = { + InputField( + value = newOption.value, + onValueChange = { newOption.value = it }, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester), + decorationBox = { innerTextField -> + Column { + innerTextField() + } + }, + ) + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + }, + onDismissRequest = { onDismiss.invoke() }, + confirmButton = { + TextButton( + enabled = newOption.value.isNotBlank(), + onClick = { + onNewAnswer.invoke(newOption.value) + onDismiss.invoke() + }, + ) { + Text(stringResource(R.string.stream_compose_confirm)) + } + }, + dismissButton = { + TextButton( + onClick = { onDismiss.invoke() }, + ) { + Text(stringResource(R.string.stream_compose_dismiss)) + } + }, + ) +} diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index c98e1541562..d3e63a340ec 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -216,6 +216,8 @@ See All %d Options Poll Options Poll Results + Poll Answers + Add an Answer %d votes 📊 Poll created: %s 📊 Poll closed: %s From bcd30324cfc128e8c47cbc8583e556ed3c943025 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 4 Oct 2024 18:17:12 +0200 Subject: [PATCH 12/40] Integrate IU for Answers --- .../api/stream-chat-android-compose.api | 30 +++++++++++--- .../components/messages/PollMessageContent.kt | 40 +++++++++++++++++-- .../compose/ui/components/poll/PollAnswers.kt | 2 +- .../components/poll/PollMoreOptionsDialog.kt | 3 +- .../components/poll/PollViewResultDialog.kt | 2 +- .../compose/ui/messages/MessagesScreen.kt | 11 +++++ .../ui/messages/list/MessageContainer.kt | 4 ++ .../compose/ui/messages/list/MessageItem.kt | 4 ++ .../compose/ui/messages/list/MessageList.kt | 10 +++++ .../compose/ui/util/PollSwitchItemFactory.kt | 1 + .../messages/MessageListViewModel.kt | 18 +++++++-- .../api/stream-chat-android-ui-common.api | 8 ++++ .../common/state/messages/poll/PollState.kt | 2 +- 13 files changed, 120 insertions(+), 15 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index a360f6fa354..6e9963b3b62 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1204,7 +1204,7 @@ public final class io/getstream/chat/android/compose/ui/components/messages/Owne } public final class io/getstream/chat/android/compose/ui/components/messages/PollMessageContentKt { - public static final fun PollMessageContent (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V + public static final fun PollMessageContent (Landroidx/compose/ui/Modifier;Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V } public final class io/getstream/chat/android/compose/ui/components/messages/QuotedMessageContentKt { @@ -1249,6 +1249,21 @@ public final class io/getstream/chat/android/compose/ui/components/moderatedmess public static final fun ModeratedMessageOptionItem (Lio/getstream/chat/android/ui/common/state/messages/list/ModeratedMessageOption;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V } +public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollAnswersKt { + public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollAnswersKt; + public static field lambda-1 Lkotlin/jvm/functions/Function3; + public static field lambda-2 Lkotlin/jvm/functions/Function3; + public static field lambda-3 Lkotlin/jvm/functions/Function3; + public static field lambda-4 Lkotlin/jvm/functions/Function2; + public static field lambda-5 Lkotlin/jvm/functions/Function3; + public fun ()V + public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-4$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; + public final fun getLambda-5$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; +} + public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollDialogHeaderKt { public static final field INSTANCE Lio/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollDialogHeaderKt; public static field lambda-1 Lkotlin/jvm/functions/Function2; @@ -1297,6 +1312,10 @@ public final class io/getstream/chat/android/compose/ui/components/poll/Composab public final fun getLambda-5$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; } +public final class io/getstream/chat/android/compose/ui/components/poll/PollAnswersKt { + public static final fun PollAnswersDialog (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V +} + public final class io/getstream/chat/android/compose/ui/components/poll/PollDialogHeaderKt { public static final fun PollDialogHeader (Ljava/lang/String;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V } @@ -1880,17 +1899,17 @@ public final class io/getstream/chat/android/compose/ui/messages/list/Composable } public final class io/getstream/chat/android/compose/ui/messages/list/MessageContainerKt { - public static final fun MessageContainer (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V + public static final fun MessageContainer (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListItemState;Lio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIII)V } public final class io/getstream/chat/android/compose/ui/messages/list/MessageItemKt { public static final field HighlightFadeOutDurationMillis I - public static final fun MessageItem (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;IIII)V + public static final fun MessageItem (Lio/getstream/chat/android/ui/common/state/messages/list/MessageItemState;Lio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function1;Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;IIII)V } public final class io/getstream/chat/android/compose/ui/messages/list/MessageListKt { - public static final fun MessageList (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lio/getstream/chat/android/models/ReactionSorting;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Lio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIIII)V - public static final fun MessageList (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lio/getstream/chat/android/models/ReactionSorting;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIIIII)V + public static final fun MessageList (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lio/getstream/chat/android/models/ReactionSorting;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Lio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIIIII)V + public static final fun MessageList (Lio/getstream/chat/android/ui/common/state/messages/list/MessageListState;Lio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lio/getstream/chat/android/models/ReactionSorting;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/layout/PaddingValues;Lio/getstream/chat/android/compose/ui/messages/list/MessagesLazyListState;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function3;Landroidx/compose/runtime/Composer;IIIIII)V } public final class io/getstream/chat/android/compose/ui/messages/list/MessagesKt { @@ -3407,6 +3426,7 @@ public final class io/getstream/chat/android/compose/viewmodel/messages/MessageL public final fun addPollOption (Lio/getstream/chat/android/models/Poll;Ljava/lang/String;)V public final fun banUser (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)V public static synthetic fun banUser$default (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;ILjava/lang/Object;)V + public final fun castAnswer (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Poll;Ljava/lang/String;)V public final fun castVote (Lio/getstream/chat/android/models/Message;Lio/getstream/chat/android/models/Poll;Lio/getstream/chat/android/models/Option;)V public final fun clearNewMessageState ()V public final fun closePoll (Ljava/lang/String;)V diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt index 7391e990c3b..41314d4262e 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt @@ -59,6 +59,7 @@ import io.getstream.chat.android.client.utils.message.isDeleted import io.getstream.chat.android.compose.R import io.getstream.chat.android.compose.ui.components.avatar.UserAvatarRow 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.models.Message @@ -67,7 +68,6 @@ import io.getstream.chat.android.models.Poll import io.getstream.chat.android.models.User import io.getstream.chat.android.models.Vote import io.getstream.chat.android.models.VotingVisibility -import io.getstream.chat.android.previewdata.PreviewMessageData import io.getstream.chat.android.ui.common.state.messages.list.MessageItemState import io.getstream.chat.android.ui.common.state.messages.list.MessagePosition import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType @@ -83,16 +83,18 @@ import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType * @param selectPoll Callback when a user selects a poll. * @param onClosePoll Callback when a user closes a poll. * @param onAddPollOption Callback when a user adds a new option to the poll. + * @param onAddAnswer Callback when a user adds a new answer to the poll. * @param onLongItemClick Handler when the user selects a message, on long tap. */ -@Composable @Suppress("LongParameterList", "LongMethod") +@Composable public fun PollMessageContent( modifier: Modifier, messageItem: MessageItemState, onCastVote: (Message, Poll, Option) -> Unit, onRemoveVote: (Message, Poll, Vote) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, + onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit, onClosePoll: (String) -> Unit, onAddPollOption: (poll: Poll, option: String) -> Unit, onLongItemClick: (Message) -> Unit = {}, @@ -139,6 +141,9 @@ public fun PollMessageContent( onRemoveVote.invoke(message, poll, vote) }, selectPoll = selectPoll, + onAddAnswer = { answer -> + onAddAnswer.invoke(message, poll, answer) + }, onClosePoll = onClosePoll, onAddPollOption = onAddPollOption, ) @@ -174,14 +179,15 @@ public fun PollMessageContent( } } -@Composable @Suppress("LongParameterList", "LongMethod") +@Composable private fun PollMessageContent( message: Message, poll: Poll, isMine: Boolean, onClosePoll: (String) -> Unit, onCastVote: (Option) -> Unit, + onAddAnswer: (answer: String) -> Unit, onRemoveVote: (Vote) -> Unit, onAddPollOption: (poll: Poll, option: String) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, @@ -189,6 +195,14 @@ private fun PollMessageContent( val showDialog = remember { mutableStateOf(false) } val heightMax = LocalConfiguration.current.screenHeightDp val isClosed = poll.closed + val showAddAnswerDialog = remember { mutableStateOf(false) } + + if (showAddAnswerDialog.value) { + AddAnswerDialog( + onDismiss = { showAddAnswerDialog.value = false }, + onNewAnswer = { newAnswer -> onAddAnswer.invoke(newAnswer) }, + ) + } if (showDialog.value) { NewOptionDialog( @@ -257,6 +271,24 @@ private fun PollMessageContent( } } + if (poll.allowAnswers) { + if (poll.answers.isNotEmpty()) { + item { + PollOptionButton( + text = "View Answers", + onButtonClicked = { selectPoll.invoke(message, poll, PollSelectionType.ViewAnswers) }, + ) + } + } else if (!poll.closed) { + item { + PollOptionButton( + text = "Add Answer", + onButtonClicked = { showAddAnswerDialog.value = true }, + ) + } + } + } + if (poll.options.size > 10) { item { PollOptionButton( @@ -498,6 +530,7 @@ private fun PollMessageContentPreview() { onCastVote = { _, _, _ -> }, onRemoveVote = { _, _, _ -> }, selectPoll = { _, _, _ -> }, + onAddAnswer = { _, _, _ -> }, onClosePoll = {}, onAddPollOption = { _, _ -> }, messageItem = MessageItemState( @@ -513,6 +546,7 @@ private fun PollMessageContentPreview() { onCastVote = { _, _, _ -> }, onRemoveVote = { _, _, _ -> }, selectPoll = { _, _, _ -> }, + onAddAnswer = { _, _, _ -> }, onClosePoll = {}, onAddPollOption = { _, _ -> }, messageItem = MessageItemState( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt index fe7ad7a4f4d..c197d72df9b 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt @@ -63,7 +63,7 @@ import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel import io.getstream.chat.android.models.Answer import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll -@Suppress("LongMethod") +@Suppress("LongMethod", "MagicNumber") @Composable public fun PollAnswersDialog( selectedPoll: SelectedPoll, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt index 2e574b7e1ee..ae451a78bc3 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollMoreOptionsDialog.kt @@ -71,9 +71,10 @@ import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll * @param onDismissRequest Handler for dismissing the dialog. * @param onBackPressed Handler for pressing a back button. */ +@Suppress("LongMethod") @Composable public fun PollMoreOptionsDialog( - selectedPoll: SelectedPoll?, + selectedPoll: SelectedPoll, listViewModel: MessageListViewModel, onDismissRequest: () -> Unit, onBackPressed: () -> Unit, diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt index eb150b2cced..1500aec1e62 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollViewResultDialog.kt @@ -74,7 +74,7 @@ import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll */ @Composable public fun PollViewResultDialog( - selectedPoll: SelectedPoll?, + selectedPoll: SelectedPoll, onDismissRequest: () -> Unit, onBackPressed: () -> Unit, ) { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt index 0ce0e480797..c1f863010b5 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt @@ -63,6 +63,7 @@ import io.getstream.chat.android.compose.state.messageoptions.MessageOptionItemS import io.getstream.chat.android.compose.ui.components.SimpleDialog import io.getstream.chat.android.compose.ui.components.messageoptions.defaultMessageOptionsState import io.getstream.chat.android.compose.ui.components.moderatedmessage.ModeratedMessageDialog +import io.getstream.chat.android.compose.ui.components.poll.PollAnswersDialog import io.getstream.chat.android.compose.ui.components.poll.PollMoreOptionsDialog import io.getstream.chat.android.compose.ui.components.poll.PollViewResultDialog import io.getstream.chat.android.compose.ui.components.reactionpicker.ReactionsPicker @@ -688,6 +689,7 @@ public fun BoxScope.AttachmentsPickerMenu( name = action.question, options = action.options.filter { it.title.isNotEmpty() }.map { it.title }, allowUserSuggestedOptions = action.switches.any { it.key == "allowUserSuggestedOptions" && it.enabled }, + allowAnswers = action.switches.any { it.key == "allowAnswers" && it.enabled }, votingVisibility = if (action.switches.any { it.key == "votingVisibility" && it.enabled }) { VotingVisibility.ANONYMOUS } else { @@ -832,4 +834,13 @@ public fun PollDialogs(listViewModel: MessageListViewModel) { onBackPressed = { dismiss.invoke() }, ) } + + if (selectedPoll?.pollSelectionType == PollSelectionType.ViewAnswers) { + PollAnswersDialog( + selectedPoll = selectedPoll, + listViewModel = listViewModel, + onDismissRequest = { dismiss.invoke() }, + onBackPressed = { dismiss.invoke() }, + ) + } } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt index fd7bdfb6110..3c3b501269c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageContainer.kt @@ -90,6 +90,7 @@ public fun MessageContainer( onCastVote: (Message, Poll, Option) -> Unit = { _, _, _ -> }, onRemoveVote: (Message, Poll, Vote) -> Unit = { _, _, _ -> }, selectPoll: (Message, Poll, PollSelectionType) -> Unit = { _, _, _ -> }, + onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit = { _, _, _ -> }, onClosePoll: (String) -> Unit = {}, onAddPollOption: (poll: Poll, option: String) -> Unit = { _, _ -> }, onGiphyActionClick: (GiphyAction) -> Unit = {}, @@ -131,6 +132,7 @@ public fun MessageContainer( }, onLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onAddAnswer = onAddAnswer, ) }, typingIndicatorContent: @Composable (TypingItemState) -> Unit = { }, @@ -284,6 +286,7 @@ internal fun DefaultMessageItem( onGiphyActionClick: (GiphyAction) -> Unit, onLinkClick: ((Message, String) -> Unit)? = null, onPollUpdated: (Message, Poll) -> Unit = { _, _ -> }, + onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit = { _, _, _ -> }, onCastVote: (Message, Poll, Option) -> Unit, onAddPollOption: (poll: Poll, option: String) -> Unit = { _, _ -> }, onRemoveVote: (Message, Poll, Vote) -> Unit, @@ -312,5 +315,6 @@ internal fun DefaultMessageItem( onLinkClick = onLinkClick, onMediaGalleryPreviewResult = onMediaGalleryPreviewResult, onUserMentionClick = onUserMentionClick, + onAddAnswer = onAddAnswer, ) } diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt index 22993f8c000..c19281b779e 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageItem.kt @@ -142,6 +142,7 @@ public fun MessageItem( onCastVote: (Message, Poll, Option) -> Unit = { _, _, _ -> }, onRemoveVote: (Message, Poll, Vote) -> Unit = { _, _, _ -> }, selectPoll: (Message, Poll, PollSelectionType) -> Unit = { _, _, _ -> }, + onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit = { _, _, _ -> }, onClosePoll: (String) -> Unit = {}, onAddPollOption: (poll: Poll, option: String) -> Unit = { _, _ -> }, onGiphyActionClick: (GiphyAction) -> Unit = {}, @@ -176,6 +177,7 @@ public fun MessageItem( onCastVote = onCastVote, onRemoveVote = onRemoveVote, selectPoll = selectPoll, + onAddAnswer = onAddAnswer, onClosePoll = onClosePoll, onAddPollOption = onAddPollOption, ) @@ -453,6 +455,7 @@ internal fun DefaultMessageItemCenterContent( onCastVote: (Message, Poll, Option) -> Unit, onRemoveVote: (Message, Poll, Vote) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, + onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit, onClosePoll: (String) -> Unit, onAddPollOption: (poll: Poll, option: String) -> Unit, ) { @@ -474,6 +477,7 @@ internal fun DefaultMessageItemCenterContent( onClosePoll = onClosePoll, onAddPollOption = onAddPollOption, onLongItemClick = onLongItemClick, + onAddAnswer = onAddAnswer, ) } else if (messageItem.message.isEmojiOnlyWithoutBubble()) { EmojiMessageContent( diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt index 875365ffa52..41dd9eef77f 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/list/MessageList.kt @@ -126,6 +126,9 @@ public fun MessageList( selectPoll: (Message, Poll, PollSelectionType) -> Unit = { message, poll, selectionType -> viewModel.displayPollMoreOptions(selectedPoll = SelectedPoll(poll, message, selectionType)) }, + onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit = { message, poll, answer -> + viewModel.castAnswer(message, poll, answer) + }, onClosePoll: (String) -> Unit = { pollId -> viewModel.closePoll(pollId = pollId) }, @@ -180,6 +183,7 @@ public fun MessageList( onUserAvatarClick = onUserAvatarClick, onLinkClick = onMessageLinkClick, onUserMentionClick = onUserMentionClick, + onAddAnswer = onAddAnswer, ) }, ) { @@ -205,6 +209,8 @@ public fun MessageList( onMessagesPageEndReached = onMessagesPageEndReached, onScrollToBottom = onScrollToBottomClicked, onMessageLinkClick = onMessageLinkClick, + onClosePoll = onClosePoll, + onAddAnswer = onAddAnswer, ) } @@ -241,6 +247,7 @@ internal fun DefaultMessageContainer( onCastVote: (Message, Poll, Option) -> Unit, onRemoveVote: (Message, Poll, Vote) -> Unit, selectPoll: (Message, Poll, PollSelectionType) -> Unit, + onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit, onClosePoll: (String) -> Unit = { _ -> }, onAddPollOption: (poll: Poll, option: String) -> Unit, onQuotedMessageClick: (Message) -> Unit, @@ -266,6 +273,7 @@ internal fun DefaultMessageContainer( onUserAvatarClick = onUserAvatarClick, onLinkClick = onLinkClick, onUserMentionClick = onUserMentionClick, + onAddAnswer = onAddAnswer, ) } @@ -349,6 +357,7 @@ public fun MessageList( onCastVote: (Message, Poll, Option) -> Unit = { _, _, _ -> }, onRemoveVote: (Message, Poll, Vote) -> Unit = { _, _, _ -> }, selectPoll: (Message, Poll, PollSelectionType) -> Unit = { _, _, _ -> }, + onAddAnswer: (message: Message, poll: Poll, answer: String) -> Unit, onClosePoll: (String) -> Unit = { _ -> }, onAddPollOption: (poll: Poll, option: String) -> Unit = { _, _ -> }, onThreadClick: (Message) -> Unit = {}, @@ -394,6 +403,7 @@ public fun MessageList( onUserAvatarClick = onUserAvatarClick, onLinkClick = onMessageLinkClick, onUserMentionClick = onUserMentionClick, + onAddAnswer = onAddAnswer, ) }, ) { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/PollSwitchItemFactory.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/PollSwitchItemFactory.kt index c4685646d22..cf44eefad17 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/PollSwitchItemFactory.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/util/PollSwitchItemFactory.kt @@ -76,6 +76,7 @@ public class DefaultPollSwitchItemFactory( ), PollSwitchItem( title = context.getString(R.string.stream_compose_poll_option_switch_add_comment), + key = "allowAnswers", enabled = false, ), ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt index ae4427fcd63..d82f1b482c2 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel.kt @@ -42,7 +42,6 @@ import io.getstream.chat.android.ui.common.state.messages.poll.PollSelectionType import io.getstream.chat.android.ui.common.state.messages.poll.PollState import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll import io.getstream.log.taggedLogger -import io.getstream.result.call.Call import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map @@ -438,8 +437,6 @@ public class MessageListViewModel( * @param message The message where the poll is. * @param poll The poll that want to be casted a vote. * @param option The option to vote for. - * - * @return Executable async [Call] responsible for casting a vote. */ public fun castVote(message: Message, poll: Poll, option: Option) { messageListController.castVote( @@ -449,6 +446,21 @@ public class MessageListViewModel( ) } + /** + * Cast an answer for a poll in a message. + * + * @param message The message where the poll is. + * @param poll The poll that want to be casted an answer. + * @param answer The answer that should be casted. + */ + public fun castAnswer(message: Message, poll: Poll, answer: String) { + messageListController.castAnswer( + messageId = message.id, + pollId = poll.id, + answer = answer, + ) + } + /** * Remove a vote for a poll in a message. * diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index 42f187122b9..1e7e941bce6 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -1549,6 +1549,14 @@ public final class io/getstream/chat/android/ui/common/state/messages/poll/PollS public fun toString ()Ljava/lang/String; } +public final class io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType$ViewAnswers : io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType { + public static final field $stable I + public static final field INSTANCE Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType$ViewAnswers; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType$ViewResult : io/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType { public static final field $stable I public static final field INSTANCE Lio/getstream/chat/android/ui/common/state/messages/poll/PollSelectionType$ViewResult; diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/poll/PollState.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/poll/PollState.kt index 3e9526c0706..fe35187e75a 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/poll/PollState.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/state/messages/poll/PollState.kt @@ -41,8 +41,8 @@ public data class SelectedPoll( @Stable public sealed class PollSelectionType { public data object MoreOption : PollSelectionType() - public data object ViewResult : PollSelectionType() + public data object ViewAnswers : PollSelectionType() } internal fun PollState.stringify(): String { From 84a73523c45302c3e585c8d465ecf2c31c549fee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 4 Oct 2024 18:47:06 +0200 Subject: [PATCH 13/40] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ff387bcbb..1183ad4b5ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -94,6 +94,7 @@ - Add `AttachmentsPickerSystemTabFactory(filesAllowed, mediaAllowed, captureImageAllowed, captureVideoAllowed, pollAllowed)` to to customize which attachment pickers are allowed. [#5430](https://github.com/GetStream/stream-chat-android/pull/5430) - Added `ChatTheme.imageAssetTransformer` to transform image assets before rendering them on the UI. [#5438](https://github.com/GetStream/stream-chat-android/pull/5438) - Added option to suggest new options on a poll. [#5424](https://github.com/GetStream/stream-chat-android/pull/5424) +- Added UI for Answers. [#5432](https://github.com/GetStream/stream-chat-android/pull/5432) ### ⚠️ Changed - Exposed `DefaultMessageComposerRecordingContent` and `DefaultAudioRecordButton` components. [#5433](https://github.com/GetStream/stream-chat-android/pull/5433) From d9ff62028f313bf6144ca6e988566106654ff2c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Tue, 8 Oct 2024 15:33:45 +0200 Subject: [PATCH 14/40] Remove unneeded logs --- .../chat/android/client/api2/mapping/PollMapping.kt | 4 ---- .../chat/android/client/parser2/adapters/EventAdapter.kt | 6 ++---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt index d038d9c66ee..8a291a1bce0 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt @@ -61,10 +61,6 @@ internal fun DownstreamPollDto.toDomain(currentUserId: UserId?): Poll { .filter { it.is_answer == true } .map { it.toAnswerDomain(currentUserId) } ) - .also { - println("JcLog: answerCount: ${it.size}") - it.forEach { println("JcLog: Answer: (${it.user?.name}) -> ${it.text}") } - } .associateBy { it.id } .values .toList() diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/EventAdapter.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/EventAdapter.kt index f22909f4923..6fb43a5a2a4 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/EventAdapter.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/parser2/adapters/EventAdapter.kt @@ -239,10 +239,8 @@ internal class EventDtoAdapter( return adapter.fromJsonValue(map) } - private fun Map.containsAnswer(): Boolean { - return (((this["poll_vote"] as? Map)?.get("is_answer") as? Boolean) ?: false) - .also { println("JcLog: containsAnswer -> $it") } - } + private fun Map.containsAnswer(): Boolean = + (((this["poll_vote"] as? Map)?.get("is_answer") as? Boolean) ?: false) override fun toJson(writer: JsonWriter, value: ChatEventDto?) { error("Can't convert this event to Json $value") From 1f08703fa72ae53427d2d318bc19ed6fc21a2a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Thu, 17 Oct 2024 12:30:04 +0200 Subject: [PATCH 15/40] Create extension functions to process polls from "Poll Events" --- .../client/extensions/internal/Poll.kt | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Poll.kt diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Poll.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Poll.kt new file mode 100644 index 00000000000..21434cd1abd --- /dev/null +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Poll.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2014-2024 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.client.extensions.internal + +import io.getstream.chat.android.client.events.AnswerCastedEvent +import io.getstream.chat.android.client.events.VoteCastedEvent +import io.getstream.chat.android.client.events.VoteChangedEvent +import io.getstream.chat.android.client.events.VoteRemovedEvent +import io.getstream.chat.android.core.internal.InternalStreamChatApi +import io.getstream.chat.android.models.Poll + +@InternalStreamChatApi +public fun VoteChangedEvent.processPoll( + currentUserId: String?, + getOldPoll: (String) -> Poll?, +): Poll { + val oldPoll = getOldPoll(poll.id) + val ownVotes = newVote.takeIf { it.user?.id == currentUserId }?.let { listOf(it) } + ?: oldPoll?.ownVotes + return poll.copy( + ownVotes = ownVotes ?: emptyList(), + answers = oldPoll?.answers ?: poll.answers, + ) +} + +@InternalStreamChatApi +public fun VoteCastedEvent.processPoll( + currentUserId: String?, + getOldPoll: (String) -> Poll?, +): Poll { + val oldPoll = getOldPoll(poll.id) + val ownVotes = ( + oldPoll?.ownVotes?.associateBy { it.id } + ?: emptyMap() + ) + + listOfNotNull(newVote.takeIf { it.user?.id == currentUserId }).associateBy { it.id } + return poll.copy( + ownVotes = ownVotes.values.toList(), + answers = oldPoll?.answers ?: poll.answers, + ) +} + +@InternalStreamChatApi +public fun VoteRemovedEvent.processPoll( + getOldPoll: (String) -> Poll?, +): Poll { + val oldPoll = getOldPoll(poll.id) + val ownVotes = (oldPoll?.ownVotes?.associateBy { it.id } ?: emptyMap()) - removedVote.id + return poll.copy( + ownVotes = ownVotes.values.toList(), + answers = oldPoll?.answers ?: poll.answers, + ) +} + +@InternalStreamChatApi +public fun AnswerCastedEvent.processPoll( + getOldPoll: (String) -> Poll?, +): Poll { + val oldPoll = getOldPoll(poll.id) + val answers = ( + oldPoll?.answers?.associateBy { it.id } + ?: emptyMap() + ) + (newAnswer.id to newAnswer) + return poll.copy( + answers = answers.values.toList(), + ownVotes = oldPoll?.ownVotes ?: poll.ownVotes, + ) +} From b678dd8477aca7a751905d18cfa3c1052033b4cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Thu, 17 Oct 2024 12:48:19 +0200 Subject: [PATCH 16/40] Use extension functions to process polls and store on DB --- .../internal/EventHandlerSequential.kt | 51 ++----------------- 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt index 53f5107fb61..a58aef194e6 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt @@ -75,6 +75,7 @@ import io.getstream.chat.android.client.extensions.internal.addMember import io.getstream.chat.android.client.extensions.internal.addMembership import io.getstream.chat.android.client.extensions.internal.enrichIfNeeded import io.getstream.chat.android.client.extensions.internal.mergeReactions +import io.getstream.chat.android.client.extensions.internal.processPoll import io.getstream.chat.android.client.extensions.internal.removeMember import io.getstream.chat.android.client.extensions.internal.removeMembership import io.getstream.chat.android.client.extensions.internal.updateMember @@ -682,52 +683,10 @@ internal class EventHandlerSequential( is PollClosedEvent -> batch.addPoll(event.poll) is PollDeletedEvent -> batch.addPoll(event.poll) is PollUpdatedEvent -> batch.addPoll(event.poll) - is VoteCastedEvent -> { - val ownVotes = - ( - batch.getPoll(event.poll.id)?.ownVotes?.associateBy { it.id } - ?: emptyMap() - ) + - listOfNotNull(event.newVote.takeIf { it.user?.id == currentUserId }).associateBy { it.id } - batch.addPoll( - event.poll.copy( - ownVotes = ownVotes.values.toList(), - ), - ) - } - is VoteChangedEvent -> { - val ownVotes = event.newVote.takeIf { it.user?.id == currentUserId }?.let { listOf(it) } - ?: batch.getPoll(event.poll.id)?.ownVotes - batch.addPoll( - event.poll.copy( - ownVotes = ownVotes ?: emptyList(), - ), - ) - } - is VoteRemovedEvent -> { - val ownVotes = - ( - batch.getPoll(event.poll.id)?.ownVotes?.associateBy { it.id } - ?: emptyMap() - ) - event.removedVote.id - batch.addPoll( - event.poll.copy( - ownVotes = ownVotes.values.toList(), - ), - ) - } - is AnswerCastedEvent -> { - val answers = ( - batch.getPoll(event.poll.id)?.answers - ?.associateBy { it.id } - ?: emptyMap() - ) + (event.newAnswer.id to event.newAnswer) - batch.addPoll( - event.poll.copy( - answers = answers.values.toList(), - ), - ) - } + is VoteCastedEvent -> batch.addPoll(event.processPoll(currentUserId, batch::getPoll)) + is VoteChangedEvent -> batch.addPoll(event.processPoll(currentUserId, batch::getPoll)) + is VoteRemovedEvent -> batch.addPoll(event.processPoll(batch::getPoll)) + is AnswerCastedEvent -> batch.addPoll(event.processPoll(batch::getPoll)) else -> Unit } } From 5a2cc441efefb90391d4d08c967a29087ee1ac12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Thu, 17 Oct 2024 12:49:16 +0200 Subject: [PATCH 17/40] Use extension functions to process polls and update them on the channelState --- .../logic/channel/internal/ChannelLogic.kt | 49 +++---------------- 1 file changed, 7 insertions(+), 42 deletions(-) diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt index 918e67a6ba5..e898d03268b 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt @@ -80,6 +80,7 @@ import io.getstream.chat.android.client.extensions.getCreatedAtOrDefault import io.getstream.chat.android.client.extensions.getCreatedAtOrNull import io.getstream.chat.android.client.extensions.internal.NEVER import io.getstream.chat.android.client.extensions.internal.applyPagination +import io.getstream.chat.android.client.extensions.internal.processPoll import io.getstream.chat.android.client.persistance.repository.RepositoryFacade import io.getstream.chat.android.client.query.pagination.AnyChannelPaginationRequest import io.getstream.chat.android.models.Channel @@ -679,48 +680,12 @@ internal class ChannelLogic( is PollClosedEvent -> channelStateLogic.upsertPoll(event.poll) is PollDeletedEvent -> channelStateLogic.upsertPoll(event.poll) is PollUpdatedEvent -> channelStateLogic.upsertPoll(event.poll) - is VoteCastedEvent -> { - val ownVotes = - ( - channelStateLogic.getPoll(event.poll.id)?.ownVotes?.associateBy { it.id } - ?: emptyMap() - ) + - listOfNotNull(event.newVote.takeIf { it.user?.id == currentUserId }).associateBy { it.id } - channelStateLogic.upsertPoll( - event.poll.copy( - ownVotes = ownVotes.values.toList(), - ), - ) - } - is VoteChangedEvent -> { - val ownVotes = event.newVote.takeIf { it.user?.id == currentUserId }?.let { listOf(it) } - ?: channelStateLogic.getPoll(event.poll.id)?.ownVotes - channelStateLogic.upsertPoll( - event.poll.copy( - ownVotes = ownVotes ?: emptyList(), - ), - ) - } - is VoteRemovedEvent -> { - val ownVotes = - ( - channelStateLogic.getPoll(event.poll.id)?.ownVotes?.associateBy { it.id } - ?: emptyMap() - ) - event.removedVote.id - channelStateLogic.upsertPoll( - event.poll.copy( - ownVotes = ownVotes.values.toList(), - ), - ) - } - is AnswerCastedEvent -> { - val answers = event.poll.answers.associateBy { it.id } + (event.newAnswer.id to event.newAnswer) - channelStateLogic.upsertPoll( - event.poll.copy( - answers = answers.values.toList(), - ), - ) - } + is VoteCastedEvent -> + channelStateLogic.upsertPoll(event.processPoll(currentUserId, channelStateLogic::getPoll)) + is VoteChangedEvent -> + channelStateLogic.upsertPoll(event.processPoll(currentUserId, channelStateLogic::getPoll)) + is VoteRemovedEvent -> channelStateLogic.upsertPoll(event.processPoll(channelStateLogic::getPoll)) + is AnswerCastedEvent -> channelStateLogic.upsertPoll(event.processPoll(channelStateLogic::getPoll)) } } From 222d7a2c150bf4cfdc5e33ce810d70f223e2fc13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Thu, 17 Oct 2024 14:39:02 +0200 Subject: [PATCH 18/40] Update answers labels --- .../compose/ui/components/messages/PollMessageContent.kt | 4 ++-- stream-chat-android-compose/src/main/res/values/strings.xml | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt index 41314d4262e..b8c6ecc4552 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt @@ -275,14 +275,14 @@ private fun PollMessageContent( if (poll.answers.isNotEmpty()) { item { PollOptionButton( - text = "View Answers", + text = stringResource(R.string.stream_compose_view_answers), onButtonClicked = { selectPoll.invoke(message, poll, PollSelectionType.ViewAnswers) }, ) } } else if (!poll.closed) { item { PollOptionButton( - text = "Add Answer", + text = stringResource(R.string.stream_compose_add_answer), onButtonClicked = { showAddAnswerDialog.value = true }, ) } diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index d3e63a340ec..b92e945cb7b 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -216,8 +216,8 @@ See All %d Options Poll Options Poll Results - Poll Answers - Add an Answer + Poll Comments + Add a Comment %d votes 📊 Poll created: %s 📊 Poll closed: %s @@ -232,4 +232,5 @@ Suggest an Option Confirm Dismiss + View Comments From 5599860dfcd717ffe136dac29bf957e4a4147d95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Thu, 17 Oct 2024 16:21:31 +0200 Subject: [PATCH 19/40] Keep answers and ownVotes after a poll is closed and/or updated --- .../client/extensions/internal/Poll.kt | 19 +++++++++++++++++++ .../internal/EventHandlerSequential.kt | 4 ++-- .../logic/channel/internal/ChannelLogic.kt | 4 ++-- 3 files changed, 23 insertions(+), 4 deletions(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Poll.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Poll.kt index 21434cd1abd..264d7efb1f9 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Poll.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/extensions/internal/Poll.kt @@ -17,6 +17,8 @@ package io.getstream.chat.android.client.extensions.internal import io.getstream.chat.android.client.events.AnswerCastedEvent +import io.getstream.chat.android.client.events.PollClosedEvent +import io.getstream.chat.android.client.events.PollUpdatedEvent import io.getstream.chat.android.client.events.VoteCastedEvent import io.getstream.chat.android.client.events.VoteChangedEvent import io.getstream.chat.android.client.events.VoteRemovedEvent @@ -80,3 +82,20 @@ public fun AnswerCastedEvent.processPoll( ownVotes = oldPoll?.ownVotes ?: poll.ownVotes, ) } + +@InternalStreamChatApi +public fun PollClosedEvent.processPoll( + getOldPoll: (String) -> Poll?, +): Poll = + getOldPoll(poll.id)?.copy(closed = true) ?: poll + +@InternalStreamChatApi +public fun PollUpdatedEvent.processPoll( + getOldPoll: (String) -> Poll?, +): Poll { + val oldPoll = getOldPoll(poll.id) + return poll.copy( + ownVotes = oldPoll?.ownVotes ?: poll.ownVotes, + answers = oldPoll?.answers ?: poll.answers, + ) +} diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt index a58aef194e6..18319f7d0ca 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/event/handler/internal/EventHandlerSequential.kt @@ -680,9 +680,9 @@ internal class EventHandlerSequential( is UserUpdatedEvent -> if (event.user.id == currentUserId) { repos.insertCurrentUser(event.user) } - is PollClosedEvent -> batch.addPoll(event.poll) + is PollClosedEvent -> batch.addPoll(event.processPoll(batch::getPoll)) is PollDeletedEvent -> batch.addPoll(event.poll) - is PollUpdatedEvent -> batch.addPoll(event.poll) + is PollUpdatedEvent -> batch.addPoll(event.processPoll(batch::getPoll)) is VoteCastedEvent -> batch.addPoll(event.processPoll(currentUserId, batch::getPoll)) is VoteChangedEvent -> batch.addPoll(event.processPoll(currentUserId, batch::getPoll)) is VoteRemovedEvent -> batch.addPoll(event.processPoll(batch::getPoll)) diff --git a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt index e898d03268b..65a2afc11b5 100644 --- a/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt +++ b/stream-chat-android-state/src/main/java/io/getstream/chat/android/state/plugin/logic/channel/internal/ChannelLogic.kt @@ -677,9 +677,9 @@ internal class ChannelLogic( is UnknownEvent, is UserDeletedEvent, -> Unit // Ignore these events - is PollClosedEvent -> channelStateLogic.upsertPoll(event.poll) + is PollClosedEvent -> channelStateLogic.upsertPoll(event.processPoll(channelStateLogic::getPoll)) is PollDeletedEvent -> channelStateLogic.upsertPoll(event.poll) - is PollUpdatedEvent -> channelStateLogic.upsertPoll(event.poll) + is PollUpdatedEvent -> channelStateLogic.upsertPoll(event.processPoll(channelStateLogic::getPoll)) is VoteCastedEvent -> channelStateLogic.upsertPoll(event.processPoll(currentUserId, channelStateLogic::getPoll)) is VoteChangedEvent -> From 897afd7f5fd35903d5b804847fd00486ad110556 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Thu, 17 Oct 2024 16:22:14 +0200 Subject: [PATCH 20/40] Add customization to show avatars on anonymous polls --- .../api/stream-chat-android-compose.api | 6 +++--- .../compose/ui/components/poll/PollAnswers.kt | 10 ++++++++-- .../android/compose/ui/messages/MessagesScreen.kt | 13 +++++++++++-- 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index 6e9963b3b62..b18ea6e23e9 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1313,7 +1313,7 @@ public final class io/getstream/chat/android/compose/ui/components/poll/Composab } public final class io/getstream/chat/android/compose/ui/components/poll/PollAnswersKt { - public static final fun PollAnswersDialog (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V + public static final fun PollAnswersDialog (Lio/getstream/chat/android/ui/common/state/messages/poll/SelectedPoll;ZLio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function0;Landroidx/compose/runtime/Composer;I)V } public final class io/getstream/chat/android/compose/ui/components/poll/PollDialogHeaderKt { @@ -1500,8 +1500,8 @@ public final class io/getstream/chat/android/compose/ui/messages/MessagesScreenK public static final fun MessageDialogs (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Landroidx/compose/runtime/Composer;I)V public static final fun MessageMenus (Landroidx/compose/foundation/layout/BoxScope;Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lio/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel;ZZLandroidx/compose/runtime/Composer;I)V public static final fun MessageModerationDialog (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Lio/getstream/chat/android/compose/viewmodel/messages/MessageComposerViewModel;ZZLandroidx/compose/runtime/Composer;I)V - public static final fun MessagesScreen (Lio/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory;ZLio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZLio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V - public static final fun PollDialogs (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;Landroidx/compose/runtime/Composer;I)V + public static final fun MessagesScreen (Lio/getstream/chat/android/compose/viewmodel/messages/MessagesViewModelFactory;ZLio/getstream/chat/android/models/ReactionSorting;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function1;ZZZLio/getstream/chat/android/compose/ui/messages/list/ThreadMessagesStart;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;III)V + public static final fun PollDialogs (Lio/getstream/chat/android/compose/viewmodel/messages/MessageListViewModel;ZLandroidx/compose/runtime/Composer;I)V } public final class io/getstream/chat/android/compose/ui/messages/attachments/AttachmentsPickerKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt index c197d72df9b..5e08348c569 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt @@ -61,12 +61,14 @@ import io.getstream.chat.android.compose.ui.components.composer.InputField import io.getstream.chat.android.compose.ui.theme.ChatTheme import io.getstream.chat.android.compose.viewmodel.messages.MessageListViewModel import io.getstream.chat.android.models.Answer +import io.getstream.chat.android.models.VotingVisibility import io.getstream.chat.android.ui.common.state.messages.poll.SelectedPoll @Suppress("LongMethod", "MagicNumber") @Composable public fun PollAnswersDialog( selectedPoll: SelectedPoll, + showAnonymousAvatar: Boolean, listViewModel: MessageListViewModel, onDismissRequest: () -> Unit, onBackPressed: () -> Unit, @@ -121,7 +123,10 @@ public fun PollAnswersDialog( items = poll.answers, key = { answer -> answer.id }, ) { answer -> - PollAnswersItem(answer = answer) + PollAnswersItem( + answer = answer, + showAvatar = (poll.votingVisibility == VotingVisibility.PUBLIC) || showAnonymousAvatar, + ) } item { Spacer(modifier = Modifier.height(16.dp)) } @@ -164,6 +169,7 @@ public fun PollAnswersDialog( @Composable internal fun PollAnswersItem( answer: Answer, + showAvatar: Boolean, ) { Column( modifier = Modifier @@ -194,7 +200,7 @@ internal fun PollAnswersItem( Spacer(modifier = Modifier.height(16.dp)) Row(verticalAlignment = Alignment.CenterVertically) { - val user = answer.user + val user = answer.user?.takeIf { showAvatar } if (user != null) { UserAvatar( modifier = Modifier.size(20.dp), diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt index c1f863010b5..e509d18cb75 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/messages/MessagesScreen.kt @@ -126,6 +126,7 @@ import io.getstream.chat.android.ui.common.state.messages.updateMessage * @param skipPushNotification If new messages should skip triggering a push notification when sent. False by default. * @param skipEnrichUrl If new messages being sent, or existing ones being updated should skip enriching the URL. * If URL is not enriched, it will not be displayed as a link attachment. False by default. + * @param showAnonymousAvatar If the user avatar should be shown on comments for polls with anonymous voting visibility. * @param threadMessagesStart Thread messages start at the bottom or top of the screen. * @param topBarContent custom top bar content to be displayed on top of the messages list. * @param bottomBarContent custom bottom bar content to be displayed at the bottom of the messages list. @@ -146,6 +147,7 @@ public fun MessagesScreen( onUserMentionClick: (User) -> Unit = {}, skipPushNotification: Boolean = false, skipEnrichUrl: Boolean = false, + showAnonymousAvatar: Boolean = false, threadMessagesStart: ThreadMessagesStart = ThreadMessagesStart.BOTTOM, topBarContent: @Composable (BackAction) -> Unit = { DefaultTopBarContent( @@ -292,7 +294,10 @@ public fun MessagesScreen( skipEnrichUrl = skipEnrichUrl, ) MessageDialogs(listViewModel = listViewModel) - PollDialogs(listViewModel = listViewModel) + PollDialogs( + listViewModel = listViewModel, + showAnonymousAvatar = showAnonymousAvatar, + ) } } @@ -814,7 +819,10 @@ public fun MessageDialogs(listViewModel: MessageListViewModel) { } @Composable -public fun PollDialogs(listViewModel: MessageListViewModel) { +public fun PollDialogs( + listViewModel: MessageListViewModel, + showAnonymousAvatar: Boolean, +) { val dismiss = { listViewModel.displayPollMoreOptions(null) } val selectedPoll = listViewModel.pollState.selectedPoll @@ -838,6 +846,7 @@ public fun PollDialogs(listViewModel: MessageListViewModel) { if (selectedPoll?.pollSelectionType == PollSelectionType.ViewAnswers) { PollAnswersDialog( selectedPoll = selectedPoll, + showAnonymousAvatar = showAnonymousAvatar, listViewModel = listViewModel, onDismissRequest = { dismiss.invoke() }, onBackPressed = { dismiss.invoke() }, From 13e82e25cf8b7f2918f4ca9db050453454921f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Thu, 17 Oct 2024 17:25:10 +0200 Subject: [PATCH 21/40] Change how answer are parsed --- .../android/client/api2/mapping/PollMapping.kt | 17 +---------------- .../android/client/api2/model/dto/PollsDtos.kt | 1 + 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt index 8a291a1bce0..40355ef906f 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/PollMapping.kt @@ -48,22 +48,7 @@ internal fun DownstreamPollDto.toDomain(currentUserId: UserId?): Poll { .values .toList() - val answer = ( - ( - latest_votes_by_option - ?.values - ?.flatten() - ?.filter { it.is_answer == true } - ?.map { it.toAnswerDomain(currentUserId) } - ?: emptyList() - ) + - own_votes - .filter { it.is_answer == true } - .map { it.toAnswerDomain(currentUserId) } - ) - .associateBy { it.id } - .values - .toList() + val answer = latest_answers?.map { it.toAnswerDomain(currentUserId) } ?: emptyList() return Poll( id = id, diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PollsDtos.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PollsDtos.kt index 660c0212110..ba9e909738a 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PollsDtos.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/dto/PollsDtos.kt @@ -90,6 +90,7 @@ internal data class DownstreamPollDto( val options: List, val vote_counts_by_option: Map?, val latest_votes_by_option: Map>?, + val latest_answers: List?, val created_at: Date, val created_by: DownstreamUserDto, val created_by_id: String, From 918ccd1c7071f13a039e14fce485d697f7ea6ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Fri, 18 Oct 2024 12:23:31 +0200 Subject: [PATCH 22/40] Add option to update a comment --- .../api/stream-chat-android-compose.api | 6 ++--- .../components/messages/PollMessageContent.kt | 1 + .../compose/ui/components/poll/PollAnswers.kt | 27 ++++++++++++++++--- .../src/main/res/values/strings.xml | 1 + 4 files changed, 28 insertions(+), 7 deletions(-) diff --git a/stream-chat-android-compose/api/stream-chat-android-compose.api b/stream-chat-android-compose/api/stream-chat-android-compose.api index b18ea6e23e9..a57bc90308c 100644 --- a/stream-chat-android-compose/api/stream-chat-android-compose.api +++ b/stream-chat-android-compose/api/stream-chat-android-compose.api @@ -1254,14 +1254,12 @@ public final class io/getstream/chat/android/compose/ui/components/poll/Composab public static field lambda-1 Lkotlin/jvm/functions/Function3; public static field lambda-2 Lkotlin/jvm/functions/Function3; public static field lambda-3 Lkotlin/jvm/functions/Function3; - public static field lambda-4 Lkotlin/jvm/functions/Function2; - public static field lambda-5 Lkotlin/jvm/functions/Function3; + public static field lambda-4 Lkotlin/jvm/functions/Function3; public fun ()V public final fun getLambda-1$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-2$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; public final fun getLambda-3$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; - public final fun getLambda-4$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function2; - public final fun getLambda-5$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; + public final fun getLambda-4$stream_chat_android_compose_release ()Lkotlin/jvm/functions/Function3; } public final class io/getstream/chat/android/compose/ui/components/poll/ComposableSingletons$PollDialogHeaderKt { diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt index b8c6ecc4552..261ece6b61c 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/messages/PollMessageContent.kt @@ -199,6 +199,7 @@ private fun PollMessageContent( if (showAddAnswerDialog.value) { AddAnswerDialog( + initMessage = "", onDismiss = { showAddAnswerDialog.value = false }, onNewAnswer = { newAnswer -> onAddAnswer.invoke(newAnswer) }, ) diff --git a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt index 5e08348c569..8ee0fcfa62d 100644 --- a/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt +++ b/stream-chat-android-compose/src/main/java/io/getstream/chat/android/compose/ui/components/poll/PollAnswers.kt @@ -43,6 +43,8 @@ import androidx.compose.material.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -73,6 +75,8 @@ public fun PollAnswersDialog( onDismissRequest: () -> Unit, onBackPressed: () -> Unit, ) { + val user by listViewModel.user.collectAsState() + val currentUserAnswer = selectedPoll.poll.answers.firstOrNull { it.user?.id == user?.id } val state = remember { MutableTransitionState(false).apply { // Start the animation immediately. @@ -82,6 +86,7 @@ public fun PollAnswersDialog( val showAddAnswerDialog = remember { mutableStateOf(false) } if (showAddAnswerDialog.value) { AddAnswerDialog( + initMessage = currentUserAnswer?.text ?: "", onDismiss = { showAddAnswerDialog.value = false }, onNewAnswer = { newAnswer -> listViewModel.castAnswer(selectedPoll.message, selectedPoll.poll, newAnswer) @@ -127,6 +132,7 @@ public fun PollAnswersDialog( answer = answer, showAvatar = (poll.votingVisibility == VotingVisibility.PUBLIC) || showAnonymousAvatar, ) + Spacer(modifier = Modifier.height(8.dp)) } item { Spacer(modifier = Modifier.height(16.dp)) } @@ -153,7 +159,12 @@ public fun PollAnswersDialog( modifier = Modifier .fillMaxWidth() .padding(vertical = 11.dp), - text = stringResource(id = R.string.stream_compose_add_answer), + text = stringResource( + id = when (currentUserAnswer == null) { + true -> R.string.stream_compose_add_answer + false -> R.string.stream_compose_edit_answer + }, + ), textAlign = TextAlign.Center, color = ChatTheme.colors.primaryAccent, fontSize = 16.sp, @@ -231,13 +242,23 @@ internal fun PollAnswersItem( @Composable internal fun AddAnswerDialog( + initMessage: String, onDismiss: () -> Unit, onNewAnswer: (newOption: String) -> Unit, ) { - val newOption = remember { mutableStateOf("") } + val newOption = remember { mutableStateOf(initMessage) } val focusRequester = remember { FocusRequester() } AlertDialog( - title = { Text(text = stringResource(R.string.stream_compose_add_answer)) }, + title = { + Text( + text = stringResource( + when (initMessage.isBlank()) { + true -> R.string.stream_compose_add_answer + false -> R.string.stream_compose_edit_answer + }, + ), + ) + }, text = { InputField( value = newOption.value, diff --git a/stream-chat-android-compose/src/main/res/values/strings.xml b/stream-chat-android-compose/src/main/res/values/strings.xml index b92e945cb7b..b6f5b3640d4 100644 --- a/stream-chat-android-compose/src/main/res/values/strings.xml +++ b/stream-chat-android-compose/src/main/res/values/strings.xml @@ -218,6 +218,7 @@ Poll Results Poll Comments Add a Comment + Update Comment %d votes 📊 Poll created: %s 📊 Poll closed: %s From 8e46f46a68810d8086c48a88f0fff29faa2629eb Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 14:44:27 +0000 Subject: [PATCH 23/40] AUTOMATION: Version Bump and CHANGELOG Update --- CHANGELOG.md | 60 +------------------ .../getstream/chat/android/Configuration.kt | 2 +- 2 files changed, 2 insertions(+), 60 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1183ad4b5ed..8f2e0dbe5b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,5 @@ -# UNRELEASED CHANGELOG +# October 23th, 2024 - 6.5.2 ## Common changes for all artifacts -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - ### ⚠️ Changed - Deprecate `NotInFilterObject` because it is not supported backend-side anymore. [#5393](https://github.com/GetStream/stream-chat-android/pull/5393) - Deprecate `AttachmentsPickerTabFactories.defaultFactoriesWithoutStoragePermissions()` in favor of a more configurable method. [#5430](https://github.com/GetStream/stream-chat-android/pull/5430) @@ -25,67 +19,28 @@ - Add `ChatClient::markThreadRead` to mark a given thread as read. [#5447](https://github.com/GetStream/stream-chat-android/pull/5447) - Add `ChannelClient::markThreadRead` to mark a given thread as read. [#5447](https://github.com/GetStream/stream-chat-android/pull/5447) -### ⚠️ Changed - -### ❌ Removed - -## stream-chat-android-offline -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - ## stream-chat-android-state -### 🐞 Fixed - -### ⬆️ Improved - ### ✅ Added - Add `GlobalState::unreadThreadsCount` property to observe the number of unread threads. [#5452](https://github.com/GetStream/stream-chat-android/pull/5452) -### ⚠️ Changed - -### ❌ Removed - ## stream-chat-android-ui-common -### 🐞 Fixed - -### ⬆️ Improved - ### ✅ Added - Added `ImageAssetTransformer` to transform image assets before rendering them on the UI. [#5438](https://github.com/GetStream/stream-chat-android/pull/5438) -### ⚠️ Changed - -### ❌ Removed - ## stream-chat-android-ui-components ### 🐞 Fixed - Fix max allowed votes base on available options. [#5431](https://github.com/GetStream/stream-chat-android/pull/5431) - Fix `CAMERA` permission request when using the capture photo/video attachment picker from `AttachmentsPickerSystemTabFactory`. [#5430](https://github.com/GetStream/stream-chat-android/pull/5430) - Fixed `MessageListView` scroll to the bottom behaviour when a new message is added. [#5427](https://github.com/GetStream/stream-chat-android/pull/5427) -### ⬆️ Improved - ### ✅ Added - Added `ChatUI.imageAssetTransformer` to transform image assets before rendering them on the UI. [#5438](https://github.com/GetStream/stream-chat-android/pull/5438) -### ⚠️ Changed - -### ❌ Removed - ## stream-chat-android-compose ### 🐞 Fixed - Fix `CAMERA` permission request when using the capture photo/video attachment picker from `AttachmentsPickerSystemTabFactory`. [#5430](https://github.com/GetStream/stream-chat-android/pull/5430) - Fixed `MessageList` scroll to the bottom behaviour when a new message is added. [#5427](https://github.com/GetStream/stream-chat-android/pull/5427) -### ⬆️ Improved - ### ✅ Added - Added `ChannelListViewModel.refresh` method to refresh the channel list. [#5425](https://github.com/GetStream/stream-chat-android/pull/5425) - Add `PinnedMessageList` component for showing the list of pinned messages in a channel. [#5420](https://github.com/GetStream/stream-chat-android/pull/5420) @@ -99,19 +54,6 @@ ### ⚠️ Changed - Exposed `DefaultMessageComposerRecordingContent` and `DefaultAudioRecordButton` components. [#5433](https://github.com/GetStream/stream-chat-android/pull/5433) -### ❌ Removed - -## stream-chat-android-markdown-transformer -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - # September 26th, 2024 - 6.5.1 ## Common changes for all artifacts ### ⚠️ Changed diff --git a/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt b/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt index 359f113debc..afbb02b03ba 100644 --- a/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt +++ b/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt @@ -7,7 +7,7 @@ object Configuration { const val minSdk = 21 const val majorVersion = 6 const val minorVersion = 5 - const val patchVersion = 1 + const val patchVersion = 2 const val versionName = "$majorVersion.$minorVersion.$patchVersion" const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" const val artifactGroup = "io.getstream" From 9e6d55b937974734baa20f8a801075710937501d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:01:06 +0000 Subject: [PATCH 24/40] AUTOMATION: Changelog update --- CHANGELOG.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2e0dbe5b2..39053cb94d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,92 @@ +# UNRELEASED CHANGELOG +## Common changes for all artifacts +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-client +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-offline +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-state +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-ui-common +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-ui-components +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-compose +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-markdown-transformer +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + # October 23th, 2024 - 6.5.2 ## Common changes for all artifacts ### ⚠️ Changed From 4b2f217355856f7962a9ec9090a683534e2df68f Mon Sep 17 00:00:00 2001 From: adasiewiczr Date: Thu, 24 Oct 2024 00:24:44 +0000 Subject: [PATCH 25/40] AUTOMATION: Clean Detekt Baseline Files --- stream-chat-android-compose/detekt-baseline.xml | 1 - 1 file changed, 1 deletion(-) diff --git a/stream-chat-android-compose/detekt-baseline.xml b/stream-chat-android-compose/detekt-baseline.xml index 6994996c20b..c8f7091cda8 100644 --- a/stream-chat-android-compose/detekt-baseline.xml +++ b/stream-chat-android-compose/detekt-baseline.xml @@ -24,7 +24,6 @@ LongMethod:MessageOptions.kt$@Composable public fun defaultMessageOptionsState( selectedMessage: Message, currentUser: User?, isInThread: Boolean, ownCapabilities: Set<String>, ): List<MessageOptionItemState> LongMethod:PollCreationDiscardDialog.kt$@Composable public fun PollCreationDiscardDialog( usePlatformDefaultWidth: Boolean = false, onCancelClicked: () -> Unit, onDiscardClicked: () -> Unit, ) LongMethod:PollMessageContent.kt$@Composable private fun PollOptionItem( modifier: Modifier = Modifier, poll: Poll, option: Option, voteCount: Int, totalVoteCount: Int, users: List<User>, checkedCount: Int, checked: Boolean, onCastVote: () -> Unit, onRemoveVote: () -> Unit, ) - LongMethod:PollMoreOptionsDialog.kt$@Composable public fun PollMoreOptionsDialog( selectedPoll: SelectedPoll?, listViewModel: MessageListViewModel, onDismissRequest: () -> Unit, onBackPressed: () -> Unit, ) LongMethod:PollOptionList.kt$@Composable public fun PollOptionList( modifier: Modifier = Modifier, lazyListState: LazyListState = rememberLazyListState(), title: String = stringResource(id = R.string.stream_compose_poll_option_title), optionItems: List<PollOptionItem> = emptyList(), onQuestionsChanged: (List<PollOptionItem>) -> Unit, itemHeightSize: Dp = ChatTheme.dimens.pollOptionInputHeight, itemInnerPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 4.dp), ) LongMethod:PollSwitchList.kt$@Composable public fun PollSwitchList( modifier: Modifier = Modifier, pollSwitchItems: List<PollSwitchItem>, onSwitchesChanged: (List<PollSwitchItem>) -> Unit, itemHeightSize: Dp = ChatTheme.dimens.pollOptionInputHeight, itemInnerPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 16.dp), ) LongMethod:StreamTypography.kt$StreamTypography.Companion$public fun defaultTypography(fontFamily: FontFamily? = null): StreamTypography From e1b42f8e8cdfe53ff6548f62cc34e38b6dc2b785 Mon Sep 17 00:00:00 2001 From: PetarVelikov Date: Fri, 1 Nov 2024 13:50:56 +0100 Subject: [PATCH 26/40] [PBE-0] Fix recursive calls to ChatEventDto#toDomain. --- .../chat/android/client/api2/mapping/EventMapping.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt index bef3b8fd996..39798daadb5 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/mapping/EventMapping.kt @@ -154,10 +154,10 @@ internal fun ChatEventDto.toDomain(currentUserId: UserId?): ChatEvent { is ChannelUserUnbannedEventDto -> toDomain(currentUserId) is ChannelVisibleEventDto -> toDomain(currentUserId) is ConnectedEventDto -> toDomain(currentUserId) - is ConnectionErrorEventDto -> toDomain(currentUserId) - is ConnectingEventDto -> toDomain(currentUserId) - is DisconnectedEventDto -> toDomain(currentUserId) - is ErrorEventDto -> toDomain(currentUserId) + is ConnectionErrorEventDto -> toDomain() + is ConnectingEventDto -> toDomain() + is DisconnectedEventDto -> toDomain() + is ErrorEventDto -> toDomain() is GlobalUserBannedEventDto -> toDomain(currentUserId) is GlobalUserUnbannedEventDto -> toDomain(currentUserId) is HealthEventDto -> toDomain() From 46acdd08d8ec8ae3e7125b615ad5eab6f9962341 Mon Sep 17 00:00:00 2001 From: PetarVelikov Date: Thu, 31 Oct 2024 15:18:28 +0100 Subject: [PATCH 27/40] [PBE-0] Fix MessageModerationAction flag literal. --- .../getstream/chat/android/models/MessageModerationDetails.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/MessageModerationDetails.kt b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/MessageModerationDetails.kt index 7d3f823a1ae..f9ceb62784d 100644 --- a/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/MessageModerationDetails.kt +++ b/stream-chat-android-core/src/main/java/io/getstream/chat/android/models/MessageModerationDetails.kt @@ -48,7 +48,7 @@ public data class MessageModerationAction( * A flagged message means it was sent for review in the dashboard but the message was still published. */ public val flag: MessageModerationAction = MessageModerationAction( - rawValue = "MESSAGE_RESPONSE_ACTION_BOUNCE", + rawValue = "MESSAGE_RESPONSE_ACTION_FLAG", ) /** From d7da13b52d95cf692c0fbd9e4aa472e2d6286ea4 Mon Sep 17 00:00:00 2001 From: PetarVelikov Date: Mon, 4 Nov 2024 18:53:18 +0100 Subject: [PATCH 28/40] [PBE-6325] Fix attachment picker permission crashes. --- .../camera/internal/CameraAttachmentFragment.kt | 10 +++++++--- .../factory/file/internal/FileAttachmentFragment.kt | 10 +++++++--- .../factory/media/internal/MediaAttachmentFragment.kt | 10 +++++++--- .../system/internal/AttachmentsPickerSystemFragment.kt | 8 +++++++- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/camera/internal/CameraAttachmentFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/camera/internal/CameraAttachmentFragment.kt index 02dca235120..8c0dbc5796a 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/camera/internal/CameraAttachmentFragment.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/camera/internal/CameraAttachmentFragment.kt @@ -131,12 +131,16 @@ internal class CameraAttachmentFragment : Fragment() { } private fun onPermissionGranted() { - binding.grantPermissionsInclude.grantPermissionsContainer.isVisible = false - activityResultLauncher?.launch(Unit) + _binding?.run { + grantPermissionsInclude.grantPermissionsContainer.isVisible = false + activityResultLauncher?.launch(Unit) + } } private fun onPermissionDenied() { - binding.grantPermissionsInclude.grantPermissionsContainer.isVisible = true + _binding?.run { + grantPermissionsInclude.grantPermissionsContainer.isVisible = true + } } override fun onDestroyView() { diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/file/internal/FileAttachmentFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/file/internal/FileAttachmentFragment.kt index 4e5e09ea2ef..d34bc54e33f 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/file/internal/FileAttachmentFragment.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/file/internal/FileAttachmentFragment.kt @@ -183,12 +183,16 @@ internal class FileAttachmentFragment : Fragment() { } private fun onPermissionGranted() { - binding.grantPermissionsInclude.grantPermissionsContainer.isVisible = false - populateAttachments() + _binding?.run { + grantPermissionsInclude.grantPermissionsContainer.isVisible = false + populateAttachments() + } } private fun onPermissionDenied() { - binding.grantPermissionsInclude.grantPermissionsContainer.isVisible = true + _binding?.run { + grantPermissionsInclude.grantPermissionsContainer.isVisible = true + } } private fun populateAttachments() { diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/media/internal/MediaAttachmentFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/media/internal/MediaAttachmentFragment.kt index 558e480b3aa..36d6477cb42 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/media/internal/MediaAttachmentFragment.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/media/internal/MediaAttachmentFragment.kt @@ -144,12 +144,16 @@ internal class MediaAttachmentFragment : Fragment() { } private fun onPermissionGranted() { - binding.grantPermissionsInclude.grantPermissionsContainer.isVisible = false - populateAttachments() + _binding?.run { + grantPermissionsInclude.grantPermissionsContainer.isVisible = false + populateAttachments() + } } private fun onPermissionDenied() { - binding.grantPermissionsInclude.grantPermissionsContainer.isVisible = true + _binding?.run { + grantPermissionsInclude.grantPermissionsContainer.isVisible = true + } } private fun updateMediaAttachment(attachmentMetaData: AttachmentMetaData) { diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/system/internal/AttachmentsPickerSystemFragment.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/system/internal/AttachmentsPickerSystemFragment.kt index f0ceda2d80d..bbaa91caa15 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/system/internal/AttachmentsPickerSystemFragment.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/composer/attachment/picker/factory/system/internal/AttachmentsPickerSystemFragment.kt @@ -94,6 +94,12 @@ internal class AttachmentsPickerSystemFragment : Fragment() { setupViews() } + override fun onDestroyView() { + super.onDestroyView() + captureMedia?.unregister() + _binding = null + } + private fun setupViews() { // Adjust visibility of the tabs based on the enabled flags if (!config.fileAttachmentsTabEnabled) { @@ -148,7 +154,7 @@ internal class AttachmentsPickerSystemFragment : Fragment() { captureMedia?.let { binding.buttonCapture.setOnClickListener { checkCameraPermissions { - captureMedia?.launch(Unit) + if (_binding != null) captureMedia?.launch(Unit) } } } From 8ee731f186d9c4852e1ca4b921d1e6205e513fb4 Mon Sep 17 00:00:00 2001 From: PetarVelikov Date: Tue, 5 Nov 2024 09:42:10 +0100 Subject: [PATCH 29/40] [PBE-6325] Add crash fixes to CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 39053cb94d6..cf0e7d794ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,6 +56,10 @@ ## stream-chat-android-ui-components ### 🐞 Fixed +- Fix crash in `CameraAttachmentFragment` related to permissions checks. [#5465](https://github.com/GetStream/stream-chat-android/pull/5465) +- Fix crash in `FileAttachmentFragment` related to permissions checks. [#5465](https://github.com/GetStream/stream-chat-android/pull/5465) +- Fix crash in `MediaAttachmentFragment` related to permissions checks. [#5465](https://github.com/GetStream/stream-chat-android/pull/5465) +- Fix crash in `AttachmentsPickerSystemFragment` related to permissions checks. [#5465](https://github.com/GetStream/stream-chat-android/pull/5465) ### ⬆️ Improved From 8865c1d9f358b1b084f08c065523b497af4fec29 Mon Sep 17 00:00:00 2001 From: PetarVelikov Date: Fri, 25 Oct 2024 21:07:24 +0200 Subject: [PATCH 30/40] [PBE-3749] Add ChatClient::markThreadUnread. --- .../api/stream-chat-android-client.api | 2 + .../chat/android/client/ChatClient.kt | 18 ++++++ .../chat/android/client/api/ChatApi.kt | 8 +++ .../chat/android/client/api2/MoshiChatApi.kt | 16 +++++ .../api2/model/requests/MarkUnreadRequest.kt | 1 + .../android/client/channel/ChannelClient.kt | 11 ++++ .../client/chatclient/WhenMarkUnreadThread.kt | 62 +++++++++++++++++++ 7 files changed, 118 insertions(+) create mode 100644 stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenMarkUnreadThread.kt diff --git a/stream-chat-android-client/api/stream-chat-android-client.api b/stream-chat-android-client/api/stream-chat-android-client.api index 821644964e4..a1ceb87eca5 100644 --- a/stream-chat-android-client/api/stream-chat-android-client.api +++ b/stream-chat-android-client/api/stream-chat-android-client.api @@ -99,6 +99,7 @@ public final class io/getstream/chat/android/client/ChatClient { public final fun markMessageRead (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markRead (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markThreadRead (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; + public final fun markThreadUnread (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markUnread (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun muteChannel (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun muteChannel (Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Lio/getstream/result/call/Call; @@ -649,6 +650,7 @@ public final class io/getstream/chat/android/client/channel/ChannelClient { public final fun markMessageRead (Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markRead ()Lio/getstream/result/call/Call; public final fun markThreadRead (Ljava/lang/String;)Lio/getstream/result/call/Call; + public final fun markThreadUnread (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun markUnread (Ljava/lang/String;)Lio/getstream/result/call/Call; public final fun mute ()Lio/getstream/result/call/Call; public final fun mute (Ljava/lang/Integer;)Lio/getstream/result/call/Call; diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt index 0d506fc7f7f..23f24044527 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/ChatClient.kt @@ -2485,6 +2485,24 @@ internal constructor( } } + /** + * Marks a given thread starting from the given message as unread. + * + * @param channelType Type of the channel. + * @param channelId Id of the channel. + * @param threadId Id of the thread to mark as unread. + * @param messageId Id of the message from where the thread should be marked as unread. + */ + @CheckResult + public fun markThreadUnread( + channelType: String, + channelId: String, + threadId: String, + messageId: String, + ): Call { + return api.markThreadUnread(channelType, channelId, threadId = threadId, messageId = messageId) + } + @CheckResult public fun updateUsers(users: List): Call> { return api.updateUsers(users) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt index 978759f6791..170dc025d19 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api/ChatApi.kt @@ -289,6 +289,14 @@ internal interface ChatApi { messageId: String, ): Call + @CheckResult + fun markThreadUnread( + channelType: String, + channelId: String, + threadId: String, + messageId: String, + ): Call + @CheckResult fun showChannel(channelType: String, channelId: String): Call diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt index dad1f94aa7a..751261350c1 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/MoshiChatApi.kt @@ -721,6 +721,22 @@ constructor( ).toUnitCall() } + override fun markThreadUnread( + channelType: String, + channelId: String, + threadId: String, + messageId: String, + ): Call { + return channelApi.markUnread( + channelType = channelType, + channelId = channelId, + request = MarkUnreadRequest( + thread_id = threadId, + message_id = messageId, + ), + ).toUnitCall() + } + override fun markAllRead(): Call { return channelApi.markAllRead().toUnitCall() } diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/MarkUnreadRequest.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/MarkUnreadRequest.kt index f9914c0d56e..c50ee27d505 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/MarkUnreadRequest.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/api2/model/requests/MarkUnreadRequest.kt @@ -21,4 +21,5 @@ import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) internal data class MarkUnreadRequest( val message_id: String, + val thread_id: String? = null, ) diff --git a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt index f0943e13b1d..4fa4b2541be 100644 --- a/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt +++ b/stream-chat-android-client/src/main/java/io/getstream/chat/android/client/channel/ChannelClient.kt @@ -420,6 +420,17 @@ public class ChannelClient internal constructor( return client.markUnread(channelType, channelId, messageId) } + /** + * Marks a given thread in the channel starting from the given message as unread. + * + * @param messageId Id of the message from where the thread should be marked as unread. + * @param threadId Id of the thread to mark as unread. + */ + @CheckResult + public fun markThreadUnread(threadId: String, messageId: String): Call { + return client.markThreadUnread(channelType, channelId, threadId = threadId, messageId = messageId) + } + @CheckResult public fun markRead(): Call { return client.markRead(channelType, channelId) diff --git a/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenMarkUnreadThread.kt b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenMarkUnreadThread.kt new file mode 100644 index 00000000000..89befb49a6c --- /dev/null +++ b/stream-chat-android-client/src/test/java/io/getstream/chat/android/client/chatclient/WhenMarkUnreadThread.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014-2024 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.client.chatclient + +import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.test.TestCall +import io.getstream.chat.android.test.callFrom +import io.getstream.result.Error +import io.getstream.result.Result +import io.getstream.result.call.Call +import kotlinx.coroutines.test.runTest +import org.amshove.kluent.shouldBeInstanceOf +import org.junit.jupiter.api.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.whenever + +internal class WhenMarkUnreadThread : BaseChatClientTest() { + + @Test + fun `Given markUnread api call successful ChatClient should return success result`() = runTest { + val apiResult = callFrom { } + val sut = Fixture().givenMarkUnreadApiResult(apiResult).get() + + val result = sut.markThreadUnread("channelType", "channelId", "threadId", "messageId").await() + + result shouldBeInstanceOf Result.Success::class + } + + @Test + fun `Given markUnread api call fails ChatClient should return error result`() = runTest { + val apiResult = TestCall(Result.Failure(Error.GenericError("Error"))) + val sut = Fixture().givenMarkUnreadApiResult(apiResult).get() + + val result = sut.markThreadUnread("channelType", "channelId", "threadId", "messageId").await() + + result shouldBeInstanceOf Result.Failure::class + } + + private inner class Fixture { + + fun givenMarkUnreadApiResult(result: Call) = apply { + whenever(api.markThreadUnread(any(), any(), any(), any())) doReturn result + } + + fun get(): ChatClient = chatClient + } +} From 6846c1f3c574b3ef67903e6135f7d598de0b883c Mon Sep 17 00:00:00 2001 From: PetarVelikov Date: Mon, 28 Oct 2024 09:04:15 +0100 Subject: [PATCH 31/40] [PBE-3749] Add ChatClient::markThreadUnread to CHANGELOG.md. --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf0e7d794ce..c9c48da83f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ ### ⬆️ Improved ### ✅ Added +- Add `ChatClient::markThreadUnread` to mark a given thread as unread. [#5457](https://github.com/GetStream/stream-chat-android/pull/5457) +- Add `ChannelClient::markThreadUnread` to mark a given thread as unread. [#5457](https://github.com/GetStream/stream-chat-android/pull/5457) ### ⚠️ Changed From 370a6c0cf5e8c9d417a1661c9c9ba8e10ec938ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Mon, 4 Nov 2024 13:29:45 +0100 Subject: [PATCH 32/40] Refactor default data separator handle to add data when messages are from different days --- .../api/stream-chat-android-ui-common.api | 6 +- .../messages/list/DateSeparatorHandler.kt | 62 +++++++------------ 2 files changed, 24 insertions(+), 44 deletions(-) diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index 1e7e941bce6..d582dcf5c69 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -118,10 +118,8 @@ public abstract interface class io/getstream/chat/android/ui/common/feature/mess } public final class io/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler$Companion { - public final fun getDefaultDateSeparatorHandler (J)Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler; - public static synthetic fun getDefaultDateSeparatorHandler$default (Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler$Companion;JILjava/lang/Object;)Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler; - public final fun getDefaultThreadDateSeparatorHandler (J)Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler; - public static synthetic fun getDefaultThreadDateSeparatorHandler$default (Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler$Companion;JILjava/lang/Object;)Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler; + public final fun getDefaultDateSeparatorHandler ()Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler; + public final fun getDefaultThreadDateSeparatorHandler ()Lio/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler; } public final class io/getstream/chat/android/ui/common/feature/messages/list/MessageListController { diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler.kt b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler.kt index 4c12491151e..31eb7540fb3 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler.kt +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/feature/messages/list/DateSeparatorHandler.kt @@ -20,6 +20,8 @@ import io.getstream.chat.android.client.extensions.getCreatedAtOrDefault import io.getstream.chat.android.client.extensions.getCreatedAtOrNull import io.getstream.chat.android.client.extensions.internal.NEVER import io.getstream.chat.android.models.Message +import java.util.Calendar +import java.util.Date /** * A SAM designed to evaluate if a date separator should be added between messages. @@ -38,60 +40,40 @@ public fun interface DateSeparatorHandler { public companion object { + private val defaultDateSeparatorHandler = DateSeparatorHandler { previousMessage, message -> + !message.getCreatedAtOrDefault(NEVER).isInTheSameDay( + previousMessage?.getCreatedAtOrNull() ?: NEVER, + ) + } + /** - * @param separatorTimeMillis Time difference between two message after which we add the date separator. + * Creates a [DateSeparatorHandler] returning true if the messages are not in the same day. * * @return The default normal list date separator handler. */ - public fun getDefaultDateSeparatorHandler( - separatorTimeMillis: Long = DateSeparatorDefaultHourThreshold, - ): DateSeparatorHandler = DateSeparatorHandler { previousMessage, message -> - if (previousMessage == null) { - true - } else { - shouldAddDateSeparator(previousMessage, message, separatorTimeMillis) - } - } + public fun getDefaultDateSeparatorHandler(): DateSeparatorHandler = defaultDateSeparatorHandler /** - * @param separatorTimeMillis Time difference between two message after which we add the date separator. + * Creates a [DateSeparatorHandler] returning true if the messages are not in the same day. * * @return The default thread date separator handler. */ - public fun getDefaultThreadDateSeparatorHandler( - separatorTimeMillis: Long = DateSeparatorDefaultHourThreshold, - ): DateSeparatorHandler = DateSeparatorHandler { previousMessage, message -> - if (previousMessage == null) { - false - } else { - shouldAddDateSeparator(previousMessage, message, separatorTimeMillis) + public fun getDefaultThreadDateSeparatorHandler(): DateSeparatorHandler = + DateSeparatorHandler { previousMessage, message -> + previousMessage?.let { defaultDateSeparatorHandler.shouldAddDateSeparator(it, message) } ?: false } - } /** - * @param previousMessage The [Message] before the one we are currently evaluating. - * @param message The [Message] before which we want to add a date separator or not. + * Checks if the two dates are in the same day. * - * @return Whether to add the date separator or not depending on the time difference. + * @param that The date to compare with. + * @return True if the two dates are in the same day, false otherwise. */ - private fun shouldAddDateSeparator( - previousMessage: Message?, - message: Message, - separatorTimeMillis: Long, - ): Boolean { - return ( - message.getCreatedAtOrDefault(NEVER).time - ( - previousMessage?.getCreatedAtOrNull()?.time - ?: NEVER.time - ) - ) > - separatorTimeMillis + private fun Date.isInTheSameDay(that: Date): Boolean { + val thisCalendar = Calendar.getInstance().apply { time = this@isInTheSameDay } + val thatCalendar = Calendar.getInstance().apply { time = that } + return thisCalendar.get(Calendar.DAY_OF_YEAR) == thatCalendar.get(Calendar.DAY_OF_YEAR) && + thisCalendar.get(Calendar.YEAR) == thatCalendar.get(Calendar.YEAR) } - - /** - * The default threshold for showing date separators. If the message difference in millis is equal to this - * number, then we show a separator, if it's enabled in the list. - */ - private const val DateSeparatorDefaultHourThreshold: Long = 4 * 60 * 60 * 1000 } } From 88e08783bb1da12e387bf1affca88f654366a179 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Tue, 29 Oct 2024 17:38:14 +0100 Subject: [PATCH 33/40] Refactor Linkify --- .../chat/android/ui/utils/Linkify.kt | 211 ++++++------------ 1 file changed, 70 insertions(+), 141 deletions(-) diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt index e868c788635..34302b55359 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt @@ -26,7 +26,7 @@ import android.text.util.Linkify import android.widget.TextView import androidx.core.util.PatternsCompat import io.getstream.chat.android.core.internal.InternalStreamChatApi -import java.util.regex.Matcher +import java.util.Locale import java.util.regex.Pattern /** @@ -38,16 +38,6 @@ import java.util.regex.Pattern @InternalStreamChatApi public object Linkify { - private val COMPARATOR: Comparator = Comparator { a, b -> - when { - a.start < b.start -> -1 - a.end > b.end -> -1 - a.start > b.start -> 1 - a.end < b.end -> 1 - else -> 0 - } - } - /** * Scans the provided TextView and turns all occurrences * of the link types into clickable links. @@ -79,41 +69,26 @@ public object Linkify { * Scans the provided spannable text and turns all occurrences * of the link types into clickable links (Currently only support web urls). * - * @param text Spannable whose text is to be marked-up with links. + * @param spannable Spannable whose text is to be marked-up with links. * @return True if at least one link is found and applied. */ @SuppressLint("RestrictedApi") - private fun addLinks(text: Spannable): Boolean { - val links = mutableListOf() - gatherLinks( - links, - text, - PatternsCompat.AUTOLINK_WEB_URL, - arrayOf("http://", "https://", "rtsp://"), - Linkify.sUrlMatchFilter, - null, - ) - gatherLinks( - links, - text, - PatternsCompat.AUTOLINK_EMAIL_ADDRESS, - arrayOf("mailto:"), - null, - null, - ) - - pruneOverlaps(links, text) - - if (links.isEmpty()) return false - - links.forEach { link -> - if (link.markwonAddedSpan == null) { - applyLink(link.url!!, link.start, link.end, text) - } - } - - return true - } + private fun addLinks(spannable: Spannable): Boolean = + ( + gatherLinks( + spannable, + PatternsCompat.AUTOLINK_WEB_URL, + listOf("http://", "https://", "rtsp://"), + Linkify.sUrlMatchFilter, + ) + gatherLinks( + spannable, + PatternsCompat.AUTOLINK_EMAIL_ADDRESS, + listOf("mailto:"), + null, + ) + ).pruneOverlaps(spannable) + .map { spannable.setSpan(it.span, it.start, it.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } + .isNotEmpty() private fun addLinkMovementMethod(t: TextView) { val m = t.movementMethod @@ -124,119 +99,73 @@ public object Linkify { } } - private fun applyLink( - url: String, - start: Int, - end: Int, - text: Spannable, - ) { - val urlSpanFactory = DEFAULT_SPAN_FACTORY - val span = urlSpanFactory(url) - text.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) - } - - private fun makeUrl( - url: String?, - prefixes: Array, - matcher: Matcher, - filter: Linkify.TransformFilter?, - ): String? { - if (url == null) return null - - var transformedUrl = filter?.transformUrl(matcher, url) ?: url - - var hasPrefix = false - for (i in prefixes.indices) { - if (transformedUrl.regionMatches(0, prefixes[i], 0, prefixes[i].length, ignoreCase = true)) { - hasPrefix = true - - // Fix capitalization if necessary - if (!transformedUrl.regionMatches(0, prefixes[i], 0, prefixes[i].length, ignoreCase = false)) { - transformedUrl = prefixes[i] + transformedUrl.substring(prefixes[i].length) - } - break + /** + * Create a URLSpan from url string. + * If the url starts with any of the prefixes, the prefix is replaced with the prefix itself to ensure the + * url is valid. + * Otherwise, the first prefix is prepended to the url. + * + * @param prefixes List of prefixes to check for. + * @return URLSpan with the valid url. + */ + private fun String.makeUrlSpan(prefixes: List): URLSpan = URLSpan( + prefixes + .map { it.lowercase(Locale.US) } + .fold(this to false) { acc, prefix -> + acc.first + .takeIf { it.startsWith(prefix, ignoreCase = true) } + ?.replace(prefix, prefix, ignoreCase = true) + ?.let { it to true } + ?: acc } - } - if (!hasPrefix && prefixes.isNotEmpty()) { - transformedUrl = prefixes[0] + transformedUrl - } - return transformedUrl - } + .takeIf { it.second } + ?.first + ?: (prefixes.first() + this), + ) - @Suppress("LongParameterList") + /** + * Apply the regex pattern to the text and return the list of LinkSpecs. + * + * @param spannable Spannable text to apply the pattern. + * @param pattern Pattern to apply. + * @param schemes List of schemes to check for. + * @param matchFilter Filter to apply on the matched text. + * + * @return List of LinkSpecs. + */ private fun gatherLinks( - links: MutableList, - s: Spannable, + spannable: Spannable, pattern: Pattern, - schemes: Array, + schemes: List, matchFilter: Linkify.MatchFilter?, - transformFilter: Linkify.TransformFilter?, - ) { - val m = pattern.matcher(s) + ): List { + val specs = mutableListOf() + val m = pattern.matcher(spannable) while (m.find()) { val start = m.start() val end = m.end() - if (matchFilter == null || matchFilter.acceptMatch(s, start, end)) { - val url: String? = makeUrl(m.group(0), schemes, m, transformFilter) - val spec = LinkSpec(url = url, start = start, end = end) - links.add(spec) + if (matchFilter == null || matchFilter.acceptMatch(spannable, start, end)) { + m.group(0)?.makeUrlSpan(schemes)?.let { specs.add(SpanSpec(span = it, start = start, end = end)) } } } + return specs } - @Suppress("NestedBlockDepth") - private fun pruneOverlaps(links: MutableList, text: Spannable) { - // Append spans added by Markwon to remove any overlap. - val urlSpans: Array = text.getSpans(0, text.length, URLSpan::class.java) - urlSpans.forEach { span -> - val spec = LinkSpec( - markwonAddedSpan = span, - start = text.getSpanStart(span), - end = text.getSpanEnd(span), + private fun List.pruneOverlaps(text: Spannable): List = + this - text.getSpans(0, text.length, URLSpan::class.java).map { + SpanSpec( + span = it, + start = text.getSpanStart(it), + end = text.getSpanEnd(it), ) - links.add(spec) - } - - links.sortWith(COMPARATOR) - - var len = links.size - var i = 0 - while (i < len - 1) { - val a: LinkSpec = links[i] - val b: LinkSpec = links[i + 1] - var remove = -1 - if (a.start <= b.start && a.end > b.start) { - when { - b.end <= a.end -> { - remove = i + 1 - } - a.end - a.start > b.end - b.start -> { - remove = i + 1 - } - a.end - a.start < b.end - b.start -> { - remove = i - } - } - if (remove != -1) { - val span: URLSpan? = links[remove].markwonAddedSpan - if (span != null) { - text.removeSpan(span) - } - links.removeAt(remove) - len-- - continue - } - } - i++ - } - } + }.flatMap { link -> + this.filter { it.start <= link.start && it.end >= link.end } + + this.filter { link.start <= it.start && link.end >= it.end } + }.toSet() - private data class LinkSpec( - val markwonAddedSpan: URLSpan? = null, - val url: String? = null, + private data class SpanSpec( + val span: URLSpan, val start: Int, val end: Int, ) - - private val DEFAULT_SPAN_FACTORY: (string: String?) -> URLSpan = ::URLSpan } From 714fdf53a8dfb31ced0fe73c6b1add972e0b6d8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Tue, 29 Oct 2024 17:50:50 +0100 Subject: [PATCH 34/40] Create UserSpan --- .../chat/android/ui/utils/UserSpan.kt | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/UserSpan.kt diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/UserSpan.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/UserSpan.kt new file mode 100644 index 00000000000..ff80dbed435 --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/UserSpan.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2014-2024 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.utils + +import android.text.style.ClickableSpan +import android.view.View +import io.getstream.chat.android.models.User + +/** + * A [ClickableSpan] that represents a [User]. + * + * This class is used to display a user's name in a [android.widget.TextView] and make it clickable. + * @property user The user that this span represents. + */ +internal class UserSpan(val user: User) : ClickableSpan() { + override fun onClick(widget: View) { /* no-op */ } +} From a04aa55cdc5cdd593ad12f3e9b19ba9d34ec1cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Tue, 29 Oct 2024 17:55:33 +0100 Subject: [PATCH 35/40] Extract TextViewLinkHnalder from ui common module --- .../api/stream-chat-android-ui-common.api | 6 --- .../chat/android/ui/common/utils/Utils.java | 26 ---------- .../LongClickFriendlyLinkMovementMethod.kt | 4 +- .../android/ui/utils/TextViewLinkHandler.kt | 51 +++++++++++++++++++ 4 files changed, 53 insertions(+), 34 deletions(-) create mode 100644 stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt diff --git a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api index d582dcf5c69..0e65ca8a8c4 100644 --- a/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api +++ b/stream-chat-android-ui-common/api/stream-chat-android-ui-common.api @@ -1680,12 +1680,6 @@ public class io/getstream/chat/android/ui/common/utils/Utils { public static fun showSoftKeyboard (Landroid/view/View;)V } -public abstract class io/getstream/chat/android/ui/common/utils/Utils$TextViewLinkHandler : android/text/method/LinkMovementMethod { - public fun ()V - public abstract fun onLinkClick (Ljava/lang/String;)V - public fun onTouchEvent (Landroid/widget/TextView;Landroid/text/Spannable;Landroid/view/MotionEvent;)Z -} - public final class io/getstream/chat/android/ui/common/utils/extensions/AttachmentKt { public static final fun getDisplayableName (Lio/getstream/chat/android/models/Attachment;)Ljava/lang/String; public static final fun getImagePreviewUrl (Lio/getstream/chat/android/models/Attachment;)Ljava/lang/String; diff --git a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/Utils.java b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/Utils.java index 22573afaca4..d0ae9b53ccc 100644 --- a/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/Utils.java +++ b/stream-chat-android-ui-common/src/main/kotlin/io/getstream/chat/android/ui/common/utils/Utils.java @@ -55,31 +55,5 @@ public static String getMimeType(String filePath) { return type; } - public static abstract class TextViewLinkHandler extends LinkMovementMethod { - public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { - if (event.getAction() != MotionEvent.ACTION_UP) - return super.onTouchEvent(widget, buffer, event); - int x = (int) event.getX(); - int y = (int) event.getY(); - - x -= widget.getTotalPaddingLeft(); - y -= widget.getTotalPaddingTop(); - - x += widget.getScrollX(); - y += widget.getScrollY(); - - Layout layout = widget.getLayout(); - int line = layout.getLineForVertical(y); - int off = layout.getOffsetForHorizontal(line, x); - - URLSpan[] link = buffer.getSpans(off, off, URLSpan.class); - if (link.length != 0) { - onLinkClick(link[0].getURL()); - } - return true; - } - - abstract public void onLinkClick(String url); - } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt index f61cfdec61e..7432f3972bf 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt @@ -20,8 +20,8 @@ import android.text.method.LinkMovementMethod import android.view.View import android.widget.TextView import androidx.core.widget.doAfterTextChanged -import io.getstream.chat.android.ui.common.utils.Utils import io.getstream.chat.android.ui.feature.messages.list.internal.LongClickFriendlyLinkMovementMethod.Companion.set +import io.getstream.chat.android.ui.utils.TextViewLinkHandler import io.getstream.chat.android.ui.utils.shouldConsumeLongTap /** @@ -36,7 +36,7 @@ internal class LongClickFriendlyLinkMovementMethod private constructor( private val textView: TextView, private val longClickTarget: View, private val onLinkClicked: (url: String) -> Unit, -) : Utils.TextViewLinkHandler() { +) : TextViewLinkHandler() { private var isLongClick = false init { diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt new file mode 100644 index 00000000000..4a54aa8280c --- /dev/null +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2014-2024 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.utils + +import android.text.Spannable +import android.text.method.LinkMovementMethod +import android.text.style.URLSpan +import android.view.MotionEvent +import android.widget.TextView + +internal abstract class TextViewLinkHandler : LinkMovementMethod() { + override fun onTouchEvent( + widget: TextView, + buffer: Spannable, + event: MotionEvent, + ): Boolean { + if (event.action != MotionEvent.ACTION_UP) return super.onTouchEvent(widget, buffer, event) + + var x = event.x.toInt() + var y = event.y.toInt() + + x -= widget.totalPaddingLeft + y -= widget.totalPaddingTop + + x += widget.scrollX + y += widget.scrollY + + val layout = widget.layout + val line = layout.getLineForVertical(y) + val off = layout.getOffsetForHorizontal(line, x.toFloat()) + + buffer.getSpans(off, off, URLSpan::class.java).firstOrNull()?.let { onLinkClick(it.url) } + return true + } + + abstract fun onLinkClick(url: String) +} From ae4050c16d26d7afa895da9752451af81c1ed00b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Tue, 29 Oct 2024 18:23:47 +0100 Subject: [PATCH 36/40] Create new listener to be used when a mention is clicked --- .../api/stream-chat-android-ui-components.api | 6 +++++ .../feature/messages/list/MessageListView.kt | 23 ++++++++++++++++++- .../adapter/MessageListListenerContainer.kt | 2 ++ .../MessageListListenerContainerImpl.kt | 10 ++++++++ 4 files changed, 40 insertions(+), 1 deletion(-) 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 22d7c275049..22643e15300 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 @@ -2210,6 +2210,7 @@ public final class io/getstream/chat/android/ui/feature/messages/list/MessageLis public final fun setOnAttachmentDownloadClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnAttachmentDownloadClickListener;)V public final fun setOnEnterThreadListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnEnterThreadListener;)V public final fun setOnLinkClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnLinkClickListener;)V + public final fun setOnMentionClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMentionClickListener;)V public final fun setOnMessageClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageClickListener;)V public final fun setOnMessageLongClickListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageLongClickListener;)V public final fun setOnMessageRetryListener (Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageRetryListener;)V @@ -2413,6 +2414,10 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public abstract fun onLinkClick (Ljava/lang/String;)Z } +public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMentionClickListener { + public abstract fun onMentionClick (Lio/getstream/chat/android/models/User;)Z +} + public abstract interface class io/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageClickListener { public abstract fun onMessageClick (Lio/getstream/chat/android/models/Message;)Z } @@ -2972,6 +2977,7 @@ public abstract interface class io/getstream/chat/android/ui/feature/messages/li public abstract fun getAttachmentDownloadClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnAttachmentDownloadClickListener; public abstract fun getGiphySendListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnGiphySendListener; public abstract fun getLinkClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnLinkClickListener; + public abstract fun getMentionClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMentionClickListener; public abstract fun getMessageClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageClickListener; public abstract fun getMessageLongClickListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageLongClickListener; public abstract fun getMessageRetryListener ()Lio/getstream/chat/android/ui/feature/messages/list/MessageListView$OnMessageRetryListener; 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 5ecebbf706a..424c6fbeeb0 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 @@ -572,7 +572,9 @@ public class MessageListView : ConstraintLayout { return@OnReactionViewClickListener true } private val defaultUserClickListener = OnUserClickListener { - /* Empty */ + false + } + private val defaultMentionClickListener = OnMentionClickListener { false } private val defaultGiphySendListener = @@ -1671,6 +1673,21 @@ public class MessageListView : ConstraintLayout { } } + /** + * Sets the mention click listener to be used by MessageListView. + * + * @param listener The listener to use. If null, the default will be used instead. + */ + public fun setOnMentionClickListener(listener: OnMentionClickListener?) { + if (listener == null) { + listenerContainer.mentionClickListener = defaultMentionClickListener + } else { + listenerContainer.mentionClickListener = OnMentionClickListener { user -> + listener.onMentionClick(user) || defaultMentionClickListener.onMentionClick(user) + } + } + } + /** * Sets the link click listener to be used by MessageListView. * @@ -2356,6 +2373,10 @@ public class MessageListView : ConstraintLayout { public fun onUserClick(user: User): Boolean } + public fun interface OnMentionClickListener { + public fun onMentionClick(user: User): Boolean + } + @Deprecated( message = "Use OnReactionViewClickListener instead", replaceWith = ReplaceWith("OnReactionViewClickListener"), diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenerContainer.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenerContainer.kt index 35287783f7b..e7cba2ac7f1 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenerContainer.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenerContainer.kt @@ -27,6 +27,7 @@ import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAtta import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAttachmentDownloadClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnGiphySendListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnLinkClickListener +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMentionClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageLongClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageRetryListener @@ -69,6 +70,7 @@ public sealed interface MessageListListeners { public val attachmentDownloadClickListener: OnAttachmentDownloadClickListener public val reactionViewClickListener: OnReactionViewClickListener public val userClickListener: OnUserClickListener + public val mentionClickListener: OnMentionClickListener public val giphySendListener: OnGiphySendListener public val linkClickListener: OnLinkClickListener public val unreadLabelReachedListener: OnUnreadLabelReachedListener diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenerContainerImpl.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenerContainerImpl.kt index 806e850bd58..a9cee0bc3a2 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenerContainerImpl.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/MessageListListenerContainerImpl.kt @@ -20,6 +20,7 @@ import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAtta import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnAttachmentDownloadClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnGiphySendListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnLinkClickListener +import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMentionClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageLongClickListener import io.getstream.chat.android.ui.feature.messages.list.MessageListView.OnMessageRetryListener @@ -42,6 +43,7 @@ internal class MessageListListenerContainerImpl( attachmentDownloadClickListener: OnAttachmentDownloadClickListener = OnAttachmentDownloadClickListener(EmptyFunctions.ONE_PARAM), reactionViewClickListener: OnReactionViewClickListener = OnReactionViewClickListener(EmptyFunctions.ONE_PARAM), userClickListener: OnUserClickListener = OnUserClickListener(EmptyFunctions.ONE_PARAM), + mentionClickListener: OnMentionClickListener = OnMentionClickListener(EmptyFunctions.ONE_PARAM), giphySendListener: OnGiphySendListener = OnGiphySendListener(EmptyFunctions.ONE_PARAM), linkClickListener: OnLinkClickListener = OnLinkClickListener(EmptyFunctions.ONE_PARAM), onUnreadLabelReachedListener: OnUnreadLabelReachedListener = OnUnreadLabelReachedListener { }, @@ -119,6 +121,14 @@ internal class MessageListListenerContainerImpl( } } + override var mentionClickListener: OnMentionClickListener by ListenerDelegate( + mentionClickListener, + ) { realListener -> + OnMentionClickListener { user -> + realListener().onMentionClick(user) + } + } + override var giphySendListener: OnGiphySendListener by ListenerDelegate( giphySendListener, ) { realListener -> From 3ee9dbc1c561ff575dc365f43e0a098caeb7a621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Wed, 30 Oct 2024 12:11:27 +0100 Subject: [PATCH 37/40] Use mentionClick listener within our ViewHolders --- .../impl/CustomAttachmentsViewHolder.kt | 2 ++ .../impl/FileAttachmentsViewHolder.kt | 1 + .../impl/LinkAttachmentsViewHolder.kt | 1 + .../impl/MediaAttachmentsViewHolder.kt | 1 + .../impl/MessagePlainTextViewHolder.kt | 1 + .../LongClickFriendlyLinkMovementMethod.kt | 19 ++++++++++++++++--- .../android/ui/utils/TextViewLinkHandler.kt | 3 +++ 7 files changed, 25 insertions(+), 3 deletions(-) diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/CustomAttachmentsViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/CustomAttachmentsViewHolder.kt index f7094e5234a..22f17e87c3c 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/CustomAttachmentsViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/CustomAttachmentsViewHolder.kt @@ -138,6 +138,7 @@ public class CustomAttachmentsViewHolder internal constructor( textView = messageText, longClickTarget = messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, + onMentionClicked = container.mentionClickListener::onMentionClick, ) } } @@ -152,6 +153,7 @@ public class CustomAttachmentsViewHolder internal constructor( textView = binding.messageText, longClickTarget = binding.messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, + onMentionClicked = container.mentionClickListener::onMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/FileAttachmentsViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/FileAttachmentsViewHolder.kt index 2a89ee3664f..6281826f749 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/FileAttachmentsViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/FileAttachmentsViewHolder.kt @@ -146,6 +146,7 @@ public class FileAttachmentsViewHolder internal constructor( textView = binding.messageText, longClickTarget = binding.messageContainer, onLinkClicked = listenerContainer.linkClickListener::onLinkClick, + onMentionClicked = listenerContainer.mentionClickListener::onMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/LinkAttachmentsViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/LinkAttachmentsViewHolder.kt index 5fa8fe4c3b1..acec744c7d1 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/LinkAttachmentsViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/LinkAttachmentsViewHolder.kt @@ -132,6 +132,7 @@ public class LinkAttachmentsViewHolder internal constructor( textView = binding.messageText, longClickTarget = binding.messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, + onMentionClicked = container.mentionClickListener::onMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MediaAttachmentsViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MediaAttachmentsViewHolder.kt index fea1a728704..619aa710652 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MediaAttachmentsViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MediaAttachmentsViewHolder.kt @@ -212,6 +212,7 @@ public class MediaAttachmentsViewHolder internal constructor( textView = binding.messageText, longClickTarget = binding.messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, + onMentionClicked = container.mentionClickListener::onMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MessagePlainTextViewHolder.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MessagePlainTextViewHolder.kt index f6721a694ea..f052411b67e 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MessagePlainTextViewHolder.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/adapter/viewholder/impl/MessagePlainTextViewHolder.kt @@ -72,6 +72,7 @@ public class MessagePlainTextViewHolder internal constructor( textView = messageText, longClickTarget = messageContainer, onLinkClicked = container.linkClickListener::onLinkClick, + onMentionClicked = container.mentionClickListener::onMentionClick, ) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt index 7432f3972bf..907d03aa536 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/feature/messages/list/internal/LongClickFriendlyLinkMovementMethod.kt @@ -20,6 +20,7 @@ import android.text.method.LinkMovementMethod import android.view.View import android.widget.TextView import androidx.core.widget.doAfterTextChanged +import io.getstream.chat.android.models.User import io.getstream.chat.android.ui.feature.messages.list.internal.LongClickFriendlyLinkMovementMethod.Companion.set import io.getstream.chat.android.ui.utils.TextViewLinkHandler import io.getstream.chat.android.ui.utils.shouldConsumeLongTap @@ -36,6 +37,7 @@ internal class LongClickFriendlyLinkMovementMethod private constructor( private val textView: TextView, private val longClickTarget: View, private val onLinkClicked: (url: String) -> Unit, + private val onUserClicked: (user: User) -> Unit, ) : TextViewLinkHandler() { private var isLongClick = false @@ -54,11 +56,21 @@ internal class LongClickFriendlyLinkMovementMethod private constructor( } override fun onLinkClick(url: String) { + if (checkLongClick()) return + onLinkClicked(url) + } + + private fun checkLongClick(): Boolean { if (isLongClick) { isLongClick = false - return + return true } - onLinkClicked(url) + return false + } + + override fun onUserClick(user: User) { + if (checkLongClick()) return + onUserClicked(user) } companion object { @@ -66,8 +78,9 @@ internal class LongClickFriendlyLinkMovementMethod private constructor( textView: TextView, longClickTarget: View, onLinkClicked: (url: String) -> Unit, + onMentionClicked: (user: User) -> Unit, ) { - LongClickFriendlyLinkMovementMethod(textView, longClickTarget, onLinkClicked) + LongClickFriendlyLinkMovementMethod(textView, longClickTarget, onLinkClicked, onMentionClicked) } } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt index 4a54aa8280c..efbf8baf38b 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/TextViewLinkHandler.kt @@ -21,6 +21,7 @@ import android.text.method.LinkMovementMethod import android.text.style.URLSpan import android.view.MotionEvent import android.widget.TextView +import io.getstream.chat.android.models.User internal abstract class TextViewLinkHandler : LinkMovementMethod() { override fun onTouchEvent( @@ -44,8 +45,10 @@ internal abstract class TextViewLinkHandler : LinkMovementMethod() { val off = layout.getOffsetForHorizontal(line, x.toFloat()) buffer.getSpans(off, off, URLSpan::class.java).firstOrNull()?.let { onLinkClick(it.url) } + buffer.getSpans(off, off, UserSpan::class.java).firstOrNull()?.let { onUserClick(it.user) } return true } abstract fun onLinkClick(url: String) + abstract fun onUserClick(user: User) } From 256a1d7a3f77cb5d251b9fa72b5528224ed71aa8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jc=20Mi=C3=B1arro?= Date: Wed, 30 Oct 2024 12:16:17 +0100 Subject: [PATCH 38/40] Add span on mention tags --- .../markdown/MarkdownTextTransformer.kt | 2 +- .../AutoLinkableTextTransformer.kt | 2 +- .../chat/android/ui/utils/Linkify.kt | 44 ++++++++++++------- 3 files changed, 31 insertions(+), 17 deletions(-) diff --git a/stream-chat-android-markdown-transformer/src/main/kotlin/io/getstream/chat/android/markdown/MarkdownTextTransformer.kt b/stream-chat-android-markdown-transformer/src/main/kotlin/io/getstream/chat/android/markdown/MarkdownTextTransformer.kt index c27f11ba187..3e2c836e932 100644 --- a/stream-chat-android-markdown-transformer/src/main/kotlin/io/getstream/chat/android/markdown/MarkdownTextTransformer.kt +++ b/stream-chat-android-markdown-transformer/src/main/kotlin/io/getstream/chat/android/markdown/MarkdownTextTransformer.kt @@ -47,6 +47,6 @@ public class MarkdownTextTransformer @JvmOverloads constructor( override fun transformAndApply(textView: TextView, messageItem: MessageListItem.MessageItem) { val displayedText = getDisplayedText(messageItem) markwon.setMarkdown(textView, displayedText.fixItalicAtEnd()) - Linkify.addLinks(textView) + Linkify.addLinks(textView, messageItem.message.mentionedUsers) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/transformer/AutoLinkableTextTransformer.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/transformer/AutoLinkableTextTransformer.kt index 08f377d5feb..097aa0b31e1 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/transformer/AutoLinkableTextTransformer.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/helper/transformer/AutoLinkableTextTransformer.kt @@ -31,6 +31,6 @@ public class AutoLinkableTextTransformer( override fun transformAndApply(textView: TextView, messageItem: MessageListItem.MessageItem) { transformer(textView, messageItem) - Linkify.addLinks(textView) + Linkify.addLinks(textView, messageItem.message.mentionedUsers) } } diff --git a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt index 34302b55359..8f4c5c33518 100644 --- a/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt +++ b/stream-chat-android-ui-components/src/main/kotlin/io/getstream/chat/android/ui/utils/Linkify.kt @@ -21,11 +21,13 @@ import android.text.Spannable import android.text.SpannableString import android.text.Spanned import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan import android.text.style.URLSpan import android.text.util.Linkify import android.widget.TextView import androidx.core.util.PatternsCompat import io.getstream.chat.android.core.internal.InternalStreamChatApi +import io.getstream.chat.android.models.User import java.util.Locale import java.util.regex.Pattern @@ -48,17 +50,21 @@ public object Linkify { * make sure it is not repeatedly called on same text. * * @param textView TextView whose text is to be marked-up with links. + * @param mentionableUsers List of users to be marked-up with links. */ - public fun addLinks(textView: TextView) { + public fun addLinks( + textView: TextView, + mentionableUsers: List, + ) { val t: CharSequence = textView.text if (t is Spannable) { - if (addLinks(t)) { + if (addLinks(t, mentionableUsers)) { addLinkMovementMethod(textView) } } else { val s = SpannableString.valueOf(t) - if (addLinks(s)) { + if (addLinks(s, mentionableUsers)) { addLinkMovementMethod(textView) textView.text = s } @@ -73,19 +79,27 @@ public object Linkify { * @return True if at least one link is found and applied. */ @SuppressLint("RestrictedApi") - private fun addLinks(spannable: Spannable): Boolean = + private fun addLinks( + spannable: Spannable, + mentionableUsers: List, + ): Boolean = ( - gatherLinks( + gatherSpanSpecs( spannable, PatternsCompat.AUTOLINK_WEB_URL, - listOf("http://", "https://", "rtsp://"), Linkify.sUrlMatchFilter, - ) + gatherLinks( + ) { it.makeUrlSpan(listOf("http://", "https://", "rtsp://")) } + gatherSpanSpecs( spannable, PatternsCompat.AUTOLINK_EMAIL_ADDRESS, - listOf("mailto:"), + null, - ) + ) { it.makeUrlSpan(listOf("mailto:")) } + mentionableUsers.flatMap { user -> + gatherSpanSpecs( + spannable, + Pattern.compile("((?:\\B|^)(@${user.name})(?:\\b|\$))"), + null, + ) { UserSpan(user) } + } ).pruneOverlaps(spannable) .map { spannable.setSpan(it.span, it.start, it.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) } .isNotEmpty() @@ -128,16 +142,16 @@ public object Linkify { * * @param spannable Spannable text to apply the pattern. * @param pattern Pattern to apply. - * @param schemes List of schemes to check for. * @param matchFilter Filter to apply on the matched text. + * @param createSpan Function to create the span from the matched text. * - * @return List of LinkSpecs. + * @return List of SpanSpec. */ - private fun gatherLinks( + private fun gatherSpanSpecs( spannable: Spannable, pattern: Pattern, - schemes: List, matchFilter: Linkify.MatchFilter?, + createSpan: (String) -> ClickableSpan, ): List { val specs = mutableListOf() val m = pattern.matcher(spannable) @@ -145,7 +159,7 @@ public object Linkify { val start = m.start() val end = m.end() if (matchFilter == null || matchFilter.acceptMatch(spannable, start, end)) { - m.group(0)?.makeUrlSpan(schemes)?.let { specs.add(SpanSpec(span = it, start = start, end = end)) } + m.group(0)?.let(createSpan)?.let { specs.add(SpanSpec(span = it, start = start, end = end)) } } } return specs @@ -164,7 +178,7 @@ public object Linkify { }.toSet() private data class SpanSpec( - val span: URLSpan, + val span: ClickableSpan, val start: Int, val end: Int, ) From 83935516dd42d36ed4c4d188f142606d4c0ff1db Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:33:53 +0000 Subject: [PATCH 39/40] AUTOMATION: Version Bump and CHANGELOG Update --- CHANGELOG.md | 84 +------------------ .../getstream/chat/android/Configuration.kt | 2 +- 2 files changed, 2 insertions(+), 84 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c48da83f1..bbee01cb34f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,61 +1,9 @@ -# UNRELEASED CHANGELOG -## Common changes for all artifacts -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - +# November 06th, 2024 - 6.5.3 ## stream-chat-android-client -### 🐞 Fixed - -### ⬆️ Improved - ### ✅ Added - Add `ChatClient::markThreadUnread` to mark a given thread as unread. [#5457](https://github.com/GetStream/stream-chat-android/pull/5457) - Add `ChannelClient::markThreadUnread` to mark a given thread as unread. [#5457](https://github.com/GetStream/stream-chat-android/pull/5457) -### ⚠️ Changed - -### ❌ Removed - -## stream-chat-android-offline -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - -## stream-chat-android-state -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - -## stream-chat-android-ui-common -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - ## stream-chat-android-ui-components ### 🐞 Fixed - Fix crash in `CameraAttachmentFragment` related to permissions checks. [#5465](https://github.com/GetStream/stream-chat-android/pull/5465) @@ -63,36 +11,6 @@ - Fix crash in `MediaAttachmentFragment` related to permissions checks. [#5465](https://github.com/GetStream/stream-chat-android/pull/5465) - Fix crash in `AttachmentsPickerSystemFragment` related to permissions checks. [#5465](https://github.com/GetStream/stream-chat-android/pull/5465) -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - -## stream-chat-android-compose -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - -## stream-chat-android-markdown-transformer -### 🐞 Fixed - -### ⬆️ Improved - -### ✅ Added - -### ⚠️ Changed - -### ❌ Removed - # October 23th, 2024 - 6.5.2 ## Common changes for all artifacts ### ⚠️ Changed diff --git a/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt b/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt index afbb02b03ba..e1ce250be7e 100644 --- a/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt +++ b/buildSrc/src/main/kotlin/io/getstream/chat/android/Configuration.kt @@ -7,7 +7,7 @@ object Configuration { const val minSdk = 21 const val majorVersion = 6 const val minorVersion = 5 - const val patchVersion = 2 + const val patchVersion = 3 const val versionName = "$majorVersion.$minorVersion.$patchVersion" const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" const val artifactGroup = "io.getstream" From 8e2aeea208bc9d2bb3883b51bfa5ac3591f5eb4b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 6 Nov 2024 16:52:50 +0000 Subject: [PATCH 40/40] AUTOMATION: Changelog update --- CHANGELOG.md | 89 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbee01cb34f..c97f2f2928a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,92 @@ +# UNRELEASED CHANGELOG +## Common changes for all artifacts +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-client +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-offline +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-state +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-ui-common +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-ui-components +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-compose +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + +## stream-chat-android-markdown-transformer +### 🐞 Fixed + +### ⬆️ Improved + +### ✅ Added + +### ⚠️ Changed + +### ❌ Removed + # November 06th, 2024 - 6.5.3 ## stream-chat-android-client ### ✅ Added