From cfa72a64ef481c7c437b34025a7c61952a10898a Mon Sep 17 00:00:00 2001 From: "sha.sdk_deployment" Date: Wed, 29 Nov 2023 08:02:30 +0000 Subject: [PATCH] Added v3.11.0 --- CHANGELOG.md | 3 + README.md | 14 +-- gradle.properties | 2 +- .../sendbird/uikit/samples/BaseApplication.kt | 3 + .../samples/basic/GroupChannelMainActivity.kt | 20 ++-- .../common/extensions/UIKitExtensions.kt | 1 + .../customization/GroupChannelRepository.kt | 4 +- .../adapter/MessageDiffCallback.java | 5 + .../activities/viewholder/MessageType.java | 8 +- .../viewholder/MessageViewHolderFactory.java | 8 ++ .../com/sendbird/uikit/consts/StringSet.kt | 1 + .../uikit/consts/TypingIndicatorType.kt | 22 ++++ .../uikit/fragments/ChannelFragment.java | 6 +- .../model/template_messages/KeySet.kt | 1 + .../ui/messages/TypingIndicatorDotsView.kt | 107 ++++++++++++++++++ .../ui/messages/TypingIndicatorMessageView.kt | 65 +++++++++++ .../internal/ui/messages/TypingMemberView.kt | 76 +++++++++++++ .../viewholders/TypingIndicatorViewHolder.kt | 23 ++++ .../uikit/model/TypingIndicatorMessage.kt | 10 ++ .../model/configurations/ChannelConfig.kt | 24 ++++ .../components/BaseMessageListComponent.java | 43 ++++--- .../sendbird/uikit/vm/ChannelViewModel.java | 24 ++++ .../sb_shape_typing_indicator_dot.xml | 5 + ..._typing_member_message_background_dark.xml | 8 ++ ...typing_member_message_background_light.xml | 8 ++ ...b_view_typing_indicator_dots_component.xml | 43 +++++++ .../sb_view_typing_indicator_message.xml | 6 + ...iew_typing_indicator_message_component.xml | 56 +++++++++ .../sb_view_typing_member_component.xml | 36 ++++++ 29 files changed, 600 insertions(+), 32 deletions(-) create mode 100644 uikit/src/main/java/com/sendbird/uikit/consts/TypingIndicatorType.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingIndicatorDotsView.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingIndicatorMessageView.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingMemberView.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/TypingIndicatorViewHolder.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/model/TypingIndicatorMessage.kt create mode 100644 uikit/src/main/res/drawable/sb_shape_typing_indicator_dot.xml create mode 100644 uikit/src/main/res/drawable/sb_typing_member_message_background_dark.xml create mode 100644 uikit/src/main/res/drawable/sb_typing_member_message_background_light.xml create mode 100644 uikit/src/main/res/layout/sb_view_typing_indicator_dots_component.xml create mode 100644 uikit/src/main/res/layout/sb_view_typing_indicator_message.xml create mode 100644 uikit/src/main/res/layout/sb_view_typing_indicator_message_component.xml create mode 100644 uikit/src/main/res/layout/sb_view_typing_member_component.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 61b2814e..e9889068 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +### v3.11.0 (Nov 29, 2023) with Chat SDK `v4.13.0` +* `VIEW_TYPE_TYPING_INDICATOR` is a new typing indicator UI that can be turned on through `typingIndicatorTypes` option. When turned on, it will be displayed in `ChannelFragment` upon receiving typing event in real time. + * Added `typingIndicatorTypes` in `ChannelConfig`. ### v3.10.1 (Nov 9, 2023) with Chat SDK `v4.13.0` * Added `uikit-samples` project to demonstrate the usage of `UIKit`. * Added `resetToDefault()` in `FragmentProviders`, `ModuleProviders`, `AdapterProviders` and `ViewModelProviders` to reset the providers to default. diff --git a/README.md b/README.md index 7d3daeec..c1f930b1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ We are introducing a new version of the Sendbird UIKit. Version 3 features a new modular architecture with more granular components that give you enhanced flexibility to customize your web and mobile apps. Check out our [migration guides](/changelogs/MIGRATIONGUIDE_V3.md) and download [our samples](https://github.com/sendbird/sendbird-uikit-android/tree/main/uikit-samples) -Sendbird UIKit for Android is a development kit with an user interface that enables an easy and fast integration of standard chat features into new or existing client apps. This repository houses the UIKit source code in addition to two samples as explained below. +Sendbird UIKit for Android is a development kit with an user interface that enables an easy and fast integration of standard chat features into new or existing client apps. This repository houses the UIKit source code in addition to two samples as explained below. - **uikit** is where you can find the open source code. Check out [UIKit Open Source Guidelines](https://github.com/sendbird/sendbird-uikit-android-sources/blob/main/OPENSOURCE_GUIDELINES.md) for more information regarding our stance on open source. - **uikit-samples** consists of four use cases of UIKit. @@ -33,13 +33,13 @@ This section shows you the prerequisites you need for testing Sendbird UIKit for The minimum requirements for UIKit for Android are: -- Android 5.0 (API level 21) or higher +- Android 5.0 (API level 21) or higher - Java 8 or higher -- Support androidx only +- Support androidx only - Android Gradle plugin 4.0.1 or higher - Sendbird Chat SDK for Android 4.0.3 and later -### Try the sample app using your data +### Try the sample app using your data If you would like to try the sample app specifically fit to your usage, you can do so by replacing the default sample app ID with yours, which you can obtain by [creating your Sendbird application from the dashboard](https://sendbird.com/docs/chat/v4/android/quickstart/send-first-message#3-install-and-configure-the-chat-sdk-4-step-1-create-a-sendbird-application-from-your-dashboard). Furthermore, you could also add data of your choice on the dashboard to test. This will allow you to experience the sample app with data from your Sendbird application. @@ -95,11 +95,11 @@ Then, open the `build.gradle` file at the application level. For `Java` and `Kot ```gradle apply plugin: 'com.android.application' -android { +android { buildFeatures { viewBinding true } - + compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -111,6 +111,6 @@ dependencies { } ``` -After saving your `build.gradle` file, click the **Sync** button to apply all the changes. +After saving your `build.gradle` file, click the **Sync** button to apply all the changes.
diff --git a/gradle.properties b/gradle.properties index e36ef25e..bcae3b09 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,5 +16,5 @@ org.gradle.jvmargs=-Xmx1536m # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -UIKIT_VERSION = 3.10.1 +UIKIT_VERSION = 3.11.0 UIKIT_VERSION_CODE = 1 diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/BaseApplication.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/BaseApplication.kt index dfa08f8d..7c6834bb 100644 --- a/uikit-samples/src/main/java/com/sendbird/uikit/samples/BaseApplication.kt +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/BaseApplication.kt @@ -12,6 +12,7 @@ import com.sendbird.uikit.SendbirdUIKit import com.sendbird.uikit.adapter.SendbirdUIKitAdapter import com.sendbird.uikit.consts.ReplyType import com.sendbird.uikit.consts.ThreadReplySelectType +import com.sendbird.uikit.consts.TypingIndicatorType import com.sendbird.uikit.interfaces.CustomParamsHandler import com.sendbird.uikit.interfaces.UserInfo import com.sendbird.uikit.model.configurations.UIKitConfig @@ -105,6 +106,8 @@ class BaseApplication : MultiDexApplication() { UIKitConfig.groupChannelConfig.threadReplySelectType = ThreadReplySelectType.THREAD // set whether to use voice message UIKitConfig.groupChannelConfig.enableVoiceMessage = true + // set typing indicator types + UIKitConfig.groupChannelConfig.typingIndicatorTypes = setOf(TypingIndicatorType.BUBBLE, TypingIndicatorType.TEXT) // set custom params SendbirdUIKit.setCustomParamsHandler(object : CustomParamsHandler { diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/basic/GroupChannelMainActivity.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/basic/GroupChannelMainActivity.kt index b107da1a..80b98196 100644 --- a/uikit-samples/src/main/java/com/sendbird/uikit/samples/basic/GroupChannelMainActivity.kt +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/basic/GroupChannelMainActivity.kt @@ -17,6 +17,7 @@ import com.sendbird.android.user.UnreadMessageCount import com.sendbird.android.user.User import com.sendbird.uikit.SendbirdUIKit import com.sendbird.uikit.activities.ChannelActivity +import com.sendbird.uikit.activities.ChatNotificationChannelActivity import com.sendbird.uikit.providers.FragmentProviders import com.sendbird.uikit.samples.R import com.sendbird.uikit.samples.common.SampleSettingsFragment @@ -99,14 +100,19 @@ class GroupChannelMainActivity : AppCompatActivity() { if (intent.hasExtra(StringSet.PUSH_REDIRECT_CHANNEL)) { val channelUrl = intent.getStringExtra(StringSet.PUSH_REDIRECT_CHANNEL) ?: return - if (intent.hasExtra(StringSet.PUSH_REDIRECT_MESSAGE_ID)) { - val messageId = intent.getLongExtra(StringSet.PUSH_REDIRECT_MESSAGE_ID, 0L) - if (messageId > 0L) { - startActivity(ChannelActivity.newRedirectToMessageThreadIntent(this, channelUrl, messageId)) - intent.removeExtra(StringSet.PUSH_REDIRECT_MESSAGE_ID) - } + val channelType = intent.getStringExtra(StringSet.PUSH_REDIRECT_CHANNEL_TYPE) + if (channelType == StringSet.notification_chat) { + startActivity(ChatNotificationChannelActivity.newIntent(this, channelUrl)) } else { - startActivity(ChannelActivity.newIntent(this, channelUrl)) + if (intent.hasExtra(StringSet.PUSH_REDIRECT_MESSAGE_ID)) { + val messageId = intent.getLongExtra(StringSet.PUSH_REDIRECT_MESSAGE_ID, 0L) + if (messageId > 0L) { + startActivity(ChannelActivity.newRedirectToMessageThreadIntent(this, channelUrl, messageId)) + intent.removeExtra(StringSet.PUSH_REDIRECT_MESSAGE_ID) + } + } else { + startActivity(ChannelActivity.newIntent(this, channelUrl)) + } } intent.removeExtra(StringSet.PUSH_REDIRECT_CHANNEL) } diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/extensions/UIKitExtensions.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/extensions/UIKitExtensions.kt index 494471b0..11bf3d78 100644 --- a/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/extensions/UIKitExtensions.kt +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/extensions/UIKitExtensions.kt @@ -108,6 +108,7 @@ internal fun SampleType?.newRedirectToChannelIntent( putExtra(StringSet.KEY_CHANNEL_URL, channelUrl) putExtra(StringSet.PUSH_REDIRECT_CHANNEL, channelUrl) putExtra(StringSet.PUSH_REDIRECT_MESSAGE_ID, messageId) + putExtra(StringSet.PUSH_REDIRECT_CHANNEL_TYPE, channelType) } } } diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/GroupChannelRepository.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/GroupChannelRepository.kt index aa38418b..4bc8750c 100644 --- a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/GroupChannelRepository.kt +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/GroupChannelRepository.kt @@ -20,11 +20,11 @@ internal object GroupChannelRepository { worker.submit { GroupChannel.createMyGroupChannelListQuery(GroupChannelListQueryParams()).next { channels, e -> WaitingDialog.dismiss() - if (e != null) { + if (e != null || channels.isNullOrEmpty()) { ContextUtils.toastError(activity, "No channels") return@next } - channels?.let { channelCache.addAll(it) } + channelCache.addAll(channels) activity.runOnUiThread { callback(channelCache.random()) } } } diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageDiffCallback.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageDiffCallback.java index 6e7293f2..1f069f0b 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageDiffCallback.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageDiffCallback.java @@ -15,6 +15,7 @@ import com.sendbird.uikit.consts.MessageGroupType; import com.sendbird.uikit.consts.ReplyType; import com.sendbird.uikit.model.MessageListUIParams; +import com.sendbird.uikit.model.TypingIndicatorMessage; import com.sendbird.uikit.utils.MessageUtils; import java.util.List; @@ -134,6 +135,10 @@ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return false; } + if (oldMessage instanceof TypingIndicatorMessage && newMessage instanceof TypingIndicatorMessage) { + return ((TypingIndicatorMessage) oldMessage).getTypingUsers().equals(((TypingIndicatorMessage) newMessage).getTypingUsers()) ; + } + if (messageListUIParams.shouldUseQuotedView()) { BaseMessage oldParentMessage = oldMessage.getParentMessage(); BaseMessage newParentMessage = newMessage.getParentMessage(); diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageType.java b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageType.java index 54d69706..2ddbe9f4 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageType.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageType.java @@ -112,8 +112,14 @@ public enum MessageType { * * since 3.10.0 */ - VIEW_TYPE_FORM_TYPE_MESSAGE(20); + VIEW_TYPE_FORM_TYPE_MESSAGE(20), + /** + * Type of typing indicator. + * + * since 3.11.0 + */ + VIEW_TYPE_TYPING_INDICATOR(21); final int value; MessageType(int value) { diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageViewHolderFactory.java b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageViewHolderFactory.java index 0d25b93d..79369fc5 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageViewHolderFactory.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageViewHolderFactory.java @@ -34,6 +34,7 @@ import com.sendbird.uikit.databinding.SbViewParentMessageInfoHolderBinding; import com.sendbird.uikit.databinding.SbViewSuggestedRepliesMessageBinding; import com.sendbird.uikit.databinding.SbViewTimeLineMessageBinding; +import com.sendbird.uikit.databinding.SbViewTypingIndicatorMessageBinding; import com.sendbird.uikit.internal.extensions.MessageExtensionsKt; import com.sendbird.uikit.internal.ui.viewholders.AdminMessageViewHolder; import com.sendbird.uikit.internal.ui.viewholders.FormMessageViewHolder; @@ -57,9 +58,11 @@ import com.sendbird.uikit.internal.ui.viewholders.ParentMessageInfoViewHolder; import com.sendbird.uikit.internal.ui.viewholders.SuggestedRepliesViewHolder; import com.sendbird.uikit.internal.ui.viewholders.TimelineViewHolder; +import com.sendbird.uikit.internal.ui.viewholders.TypingIndicatorViewHolder; import com.sendbird.uikit.model.MessageListUIParams; import com.sendbird.uikit.model.SuggestedRepliesMessage; import com.sendbird.uikit.model.TimelineMessage; +import com.sendbird.uikit.model.TypingIndicatorMessage; import com.sendbird.uikit.utils.MessageUtils; import java.util.Map; @@ -228,6 +231,9 @@ public static MessageViewHolder createViewHolder(@NonNull LayoutInflater inflate case VIEW_TYPE_FORM_TYPE_MESSAGE: holder = new FormMessageViewHolder(SbViewFormMessageBinding.inflate(inflater, parent, false), messageListUIParams); break; + case VIEW_TYPE_TYPING_INDICATOR: + holder = new TypingIndicatorViewHolder(SbViewTypingIndicatorMessageBinding.inflate(inflater, parent, false), messageListUIParams); + break; default: // unknown message type if (viewType == MessageType.VIEW_TYPE_UNKNOWN_MESSAGE_ME) { @@ -323,6 +329,8 @@ public static MessageType getMessageType(@NonNull BaseMessage message) { type = MessageType.VIEW_TYPE_ADMIN_MESSAGE; } else if (message instanceof SuggestedRepliesMessage) { type = MessageType.VIEW_TYPE_SUGGESTED_REPLIES; + } else if (message instanceof TypingIndicatorMessage) { + type = MessageType.VIEW_TYPE_TYPING_INDICATOR; } else { if (MessageUtils.isMine(message)) { type = MessageType.VIEW_TYPE_UNKNOWN_MESSAGE_ME; diff --git a/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt b/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt index 3990c980..141e252a 100644 --- a/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt +++ b/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt @@ -126,6 +126,7 @@ object StringSet { const val EVENT_MESSAGE_SENT = "EVENT_MESSAGE_SENT" const val EVENT_MESSAGE_RECEIVED = "EVENT_MESSAGE_RECEIVED" const val EVENT_MESSAGE_UPDATED = "EVENT_MESSAGE_UPDATED" + const val EVENT_TYPING_STATUS_UPDATED = "EVENT_TYPING_STATUS_UPDATED" const val INVALID_URL = "INVALID_URL" const val photo = "photo" const val photos = "photos" diff --git a/uikit/src/main/java/com/sendbird/uikit/consts/TypingIndicatorType.kt b/uikit/src/main/java/com/sendbird/uikit/consts/TypingIndicatorType.kt new file mode 100644 index 00000000..9f3cb86b --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/consts/TypingIndicatorType.kt @@ -0,0 +1,22 @@ +package com.sendbird.uikit.consts + +/** + * Represents how the typing indicator is displayed in the channel fragment. + * + * @since 3.11.0 + */ +enum class TypingIndicatorType { + /** + * The user nicknames of those who are typing are visible in the header. + * + * @since 3.11.0 + */ + TEXT, + + /** + * Users who are typing are visible at the bottom of the message list. + * + * @since 3.11.0 + */ + BUBBLE, +} diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java index 6944215c..ea95c3ed 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java @@ -40,6 +40,7 @@ import com.sendbird.uikit.consts.ReplyType; import com.sendbird.uikit.consts.StringSet; import com.sendbird.uikit.consts.ThreadReplySelectType; +import com.sendbird.uikit.consts.TypingIndicatorType; import com.sendbird.uikit.interfaces.LoadingDialogHandler; import com.sendbird.uikit.interfaces.MessageDisplayDataProvider; import com.sendbird.uikit.interfaces.OnConsumableClickListener; @@ -222,7 +223,7 @@ protected void onBindChannelHeaderComponent(@NonNull ChannelHeaderComponent head startActivity(intent); }); - if (channelConfig.getEnableTypingIndicator()) { + if (channelConfig.getEnableTypingIndicator() && channelConfig.getTypingIndicatorTypes().contains(TypingIndicatorType.TEXT)) { viewModel.getTypingMembers().observe(getViewLifecycleOwner(), typingMembers -> { String description = null; if (typingMembers != null && getContext() != null) { @@ -330,6 +331,9 @@ protected void onBindMessageListComponent(@NonNull MessageListComponent messageL case StringSet.MESSAGE_FILL: messageListComponent.notifyMessagesFilled(!anchorDialogShowing.get()); break; + case StringSet.EVENT_TYPING_STATUS_UPDATED: + messageListComponent.notifyTypingIndicatorUpdated(!anchorDialogShowing.get()); + break; } } if (!isInitialCallFinished) { diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/KeySet.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/KeySet.kt index 108d4d06..e85fa377 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/KeySet.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/KeySet.kt @@ -88,6 +88,7 @@ internal object KeySet { const val enable_ogtag = "enable_ogtag" const val enable_mention = "enable_mention" const val enable_typing_indicator = "enable_typing_indicator" + const val typing_indicator_types = "typing_indicator_types" const val enable_reactions = "enable_reactions" const val enable_voice_message = "enable_voice_message" const val enable_multiple_files_message = "enable_multiple_files_message" diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingIndicatorDotsView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingIndicatorDotsView.kt new file mode 100644 index 00000000..4ac0fd5a --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingIndicatorDotsView.kt @@ -0,0 +1,107 @@ +package com.sendbird.uikit.internal.ui.messages + +import android.animation.AnimatorSet +import android.animation.ObjectAnimator +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.content.res.AppCompatResources +import com.sendbird.uikit.R +import com.sendbird.uikit.SendbirdUIKit +import com.sendbird.uikit.databinding.SbViewTypingIndicatorDotsComponentBinding +import com.sendbird.uikit.utils.DrawableUtils + +internal class TypingIndicatorDotsView @JvmOverloads internal constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : BaseMessageView(context, attrs, defStyle) { + override val binding: SbViewTypingIndicatorDotsComponentBinding = SbViewTypingIndicatorDotsComponentBinding.inflate( + LayoutInflater.from(getContext()), + this, + true + ) + + override val layout: View + get() = binding.root + private var animatorSet: AnimatorSet? = null + + init { + val messageBackground = R.drawable.sb_shape_chat_bubble + val messageBackgroundTint = if (SendbirdUIKit.isDarkMode()) { + AppCompatResources.getColorStateList(context, R.color.ondark_04) + } else { + AppCompatResources.getColorStateList(context, R.color.onlight_04) + } + val dotImageTintList = if (SendbirdUIKit.isDarkMode()) { + AppCompatResources.getColorStateList(context, R.color.ondark_01) + } else { + AppCompatResources.getColorStateList(context, R.color.onlight_01) + } + + binding.root.background = + DrawableUtils.setTintList(context, messageBackground, messageBackgroundTint) + + setDotsImageTintList(dotImageTintList) + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + startAnimation() + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + stopAnimation() + } + + private fun setDotsImageTintList(dotImageTintList: ColorStateList) { + with(binding) { + ivLeftTypingDot.imageTintList = dotImageTintList + ivCenterTypingDot.imageTintList = dotImageTintList + ivRightTypingDot.imageTintList = dotImageTintList + } + } + + private fun startAnimation() { + with(binding) { + setAnimation(ivLeftTypingDot, 400L) + setAnimation(ivCenterTypingDot, 600L) + setAnimation(ivRightTypingDot, 800L) + } + } + + private fun stopAnimation() { + animatorSet?.cancel() + animatorSet = null + } + + private fun setAnimation(targetView: View, startDelay: Long) { + val startAlphaAnimator = ObjectAnimator.ofFloat(targetView, "alpha", 0f, 0.12f) + val scaleXAnimator = ObjectAnimator.ofFloat(targetView, "scaleX", 1.0f, 1.2f).apply { + this.startDelay = startDelay + this.duration = 400 + this.repeatCount = ObjectAnimator.INFINITE + this.repeatMode = ObjectAnimator.REVERSE + } + val scaleYAnimator = ObjectAnimator.ofFloat(targetView, "scaleY", 1.0f, 1.2f).apply { + this.startDelay = startDelay + this.duration = 400 + this.repeatCount = ObjectAnimator.INFINITE + this.repeatMode = ObjectAnimator.REVERSE + } + val alphaAnimator = ObjectAnimator.ofFloat(targetView, "alpha", 0.12f, 0.38f).apply { + this.startDelay = startDelay + this.duration = 400 + this.repeatCount = ObjectAnimator.INFINITE + this.repeatMode = ObjectAnimator.REVERSE + } + + animatorSet = AnimatorSet().apply { + playTogether(startAlphaAnimator, scaleXAnimator, scaleYAnimator, alphaAnimator) + start() + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingIndicatorMessageView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingIndicatorMessageView.kt new file mode 100644 index 00000000..b67a30a6 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingIndicatorMessageView.kt @@ -0,0 +1,65 @@ +package com.sendbird.uikit.internal.ui.messages + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import com.sendbird.android.user.User +import com.sendbird.uikit.databinding.SbViewTypingIndicatorMessageComponentBinding + +internal const val TYPING_INDICATOR_MEMBER_VIEW_COUNT = 3 +internal const val TYPING_INDICATOR_MEMBER_COUNT_VIEW_INDEX = 3 + +internal class TypingIndicatorMessageView @JvmOverloads internal constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : BaseMessageView(context, attrs, defStyle) { + private val typingMemberViews: List + + override val binding: SbViewTypingIndicatorMessageComponentBinding = SbViewTypingIndicatorMessageComponentBinding.inflate( + LayoutInflater.from(getContext()), + this, + true + ) + + override val layout: View + get() = binding.root + + init { + typingMemberViews = listOf( + binding.typingUserView1.apply { typingMemberViewType = TypingMemberViewType.MEMBER }, + binding.typingUserView2.apply { typingMemberViewType = TypingMemberViewType.MEMBER }, + binding.typingUserView3.apply { typingMemberViewType = TypingMemberViewType.MEMBER }, + binding.typingUserView4.apply { typingMemberViewType = TypingMemberViewType.COUNTER }, + ) + } + + @Synchronized + fun updateTypingMembers(typingMembers: List) { + for (index: Int in 0 until TYPING_INDICATOR_MEMBER_VIEW_COUNT) { + updateTypingMemberView(index, typingMembers.getOrNull(index)) + } + updateTypingMemberCountView(typingMembers.size) + } + + private fun updateTypingMemberView(index: Int, user: User?) { + val typingMemberView = typingMemberViews.getOrNull(index) ?: return + if (user != null) { + typingMemberView.drawTypingMember(user) + typingMemberView.visibility = VISIBLE + } else { + typingMemberView.visibility = GONE + } + } + + private fun updateTypingMemberCountView(size: Int) { + val typingMemberView = typingMemberViews[TYPING_INDICATOR_MEMBER_COUNT_VIEW_INDEX] + if (size > TYPING_INDICATOR_MEMBER_VIEW_COUNT) { + typingMemberView.drawTypingMemberCount(size - TYPING_INDICATOR_MEMBER_VIEW_COUNT) + typingMemberView.visibility = VISIBLE + } else { + typingMemberView.visibility = GONE + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingMemberView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingMemberView.kt new file mode 100644 index 00000000..66ac8aa5 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/TypingMemberView.kt @@ -0,0 +1,76 @@ + +package com.sendbird.uikit.internal.ui.messages + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import com.sendbird.android.user.User +import com.sendbird.uikit.R +import com.sendbird.uikit.SendbirdUIKit +import com.sendbird.uikit.databinding.SbViewTypingMemberComponentBinding +import com.sendbird.uikit.utils.ViewUtils +import kotlin.math.min + +internal const val MAX_TYPING_MEMBER_COUNT = 99 + +internal class TypingMemberView @JvmOverloads internal constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : BaseMessageView(context, attrs, defStyle) { + override val binding: SbViewTypingMemberComponentBinding = SbViewTypingMemberComponentBinding.inflate( + LayoutInflater.from(getContext()), + this, + true + ) + override val layout: View + get() = binding.root + + var typingMemberViewType: TypingMemberViewType = TypingMemberViewType.MEMBER + set(value) { + when (value) { + TypingMemberViewType.MEMBER -> binding.ivTypingMember.visibility = VISIBLE + TypingMemberViewType.COUNTER -> binding.cvTypingMemberCount.visibility = VISIBLE + } + field = value + } + + init { + @DrawableRes val backgroundResourceId: Int + @ColorRes val textColorResourceId: Int + @ColorRes val textBackgroundResourceId: Int + + if (SendbirdUIKit.isDarkMode()) { + backgroundResourceId = R.drawable.sb_typing_member_message_background_light + textColorResourceId = R.color.ondark_02 + textBackgroundResourceId = R.color.background_400 + } else { + backgroundResourceId = R.drawable.sb_typing_member_message_background_dark + textColorResourceId = R.color.onlight_02 + textBackgroundResourceId = R.color.background_100 + } + + binding.root.setBackgroundResource(backgroundResourceId) + binding.cvTypingMemberCount.setCardBackgroundColor(ContextCompat.getColor(context, textBackgroundResourceId)) + binding.tvTypingMemberCount.setTextColor(ContextCompat.getColor(context, textColorResourceId)) + } + + fun drawTypingMemberCount(memberCount: Int) { + binding.tvTypingMemberCount.text = memberCount.toMemberCountText() + } + + fun drawTypingMember(user: User) { + ViewUtils.drawProfile(binding.ivTypingMember, user.profileUrl, user.plainProfileImageUrl) + } +} + +internal fun Int.toMemberCountText() = "+${min(this, MAX_TYPING_MEMBER_COUNT)}" + +internal enum class TypingMemberViewType { + MEMBER, + COUNTER, +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/TypingIndicatorViewHolder.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/TypingIndicatorViewHolder.kt new file mode 100644 index 00000000..9d8d7e30 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/TypingIndicatorViewHolder.kt @@ -0,0 +1,23 @@ +package com.sendbird.uikit.internal.ui.viewholders + +import android.view.View +import com.sendbird.android.channel.BaseChannel +import com.sendbird.android.message.BaseMessage +import com.sendbird.uikit.activities.viewholder.MessageViewHolder +import com.sendbird.uikit.databinding.SbViewTypingIndicatorMessageBinding +import com.sendbird.uikit.model.MessageListUIParams +import com.sendbird.uikit.model.TypingIndicatorMessage + +internal class TypingIndicatorViewHolder internal constructor( + val binding: SbViewTypingIndicatorMessageBinding, + messageListUIParams: MessageListUIParams +) : MessageViewHolder(binding.root, messageListUIParams) { + + override fun bind(channel: BaseChannel, message: BaseMessage, messageListUIParams: MessageListUIParams) { + if (message is TypingIndicatorMessage) { + binding.typingIndicatorMessageView.updateTypingMembers(message.typingUsers) + } + } + + override fun getClickableViewMap(): Map = mapOf() +} diff --git a/uikit/src/main/java/com/sendbird/uikit/model/TypingIndicatorMessage.kt b/uikit/src/main/java/com/sendbird/uikit/model/TypingIndicatorMessage.kt new file mode 100644 index 00000000..3129523c --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/model/TypingIndicatorMessage.kt @@ -0,0 +1,10 @@ +package com.sendbird.uikit.model + +import com.sendbird.android.message.CustomizableMessage +import com.sendbird.android.user.User + +internal class TypingIndicatorMessage(channelUrl: String, val typingUsers: List) : + CustomizableMessage(channelUrl, Long.MAX_VALUE, Long.MAX_VALUE) { + override val requestId: String + get() = Long.MAX_VALUE.toString() +} diff --git a/uikit/src/main/java/com/sendbird/uikit/model/configurations/ChannelConfig.kt b/uikit/src/main/java/com/sendbird/uikit/model/configurations/ChannelConfig.kt index 222b2bcf..484728b1 100644 --- a/uikit/src/main/java/com/sendbird/uikit/model/configurations/ChannelConfig.kt +++ b/uikit/src/main/java/com/sendbird/uikit/model/configurations/ChannelConfig.kt @@ -6,6 +6,7 @@ import com.sendbird.android.channel.GroupChannel import com.sendbird.android.channel.Role import com.sendbird.uikit.consts.ReplyType import com.sendbird.uikit.consts.ThreadReplySelectType +import com.sendbird.uikit.consts.TypingIndicatorType import com.sendbird.uikit.internal.model.serializer.ReplyTypeAsStringSerializer import com.sendbird.uikit.internal.model.serializer.ThreadReplySelectTypeAsStringSerializer import com.sendbird.uikit.internal.model.template_messages.KeySet @@ -30,6 +31,8 @@ data class ChannelConfig internal constructor( private var _enableMention: Boolean = false, @SerialName(KeySet.enable_typing_indicator) private var _enableTypingIndicator: Boolean = true, + @SerialName(KeySet.typing_indicator_types) + private var _typingIndicatorTypes: Set = setOf(TypingIndicatorType.TEXT), @SerialName(KeySet.enable_reactions) private var _enableReactions: Boolean = true, @SerialName(KeySet.enable_voice_message) @@ -61,6 +64,8 @@ data class ChannelConfig internal constructor( @Transient private var enableTypingIndicatorMutable: Boolean? = null, @Transient + private var typingIndicatorTypesMutable: Set? = null, + @Transient private var enableReactionsMutable: Boolean? = null, @Transient private var enableVoiceMessageMutable: Boolean? = null, @@ -199,6 +204,23 @@ data class ChannelConfig internal constructor( set(value) { enableTypingIndicatorMutable = value } + var typingIndicatorTypes: Set + /** + * Returns Set, which is how typingIndicators are displayed in the channel fragment. + * + * @return The value of Set + * @since 3.11.0 + */ + get() = typingIndicatorTypesMutable ?: _typingIndicatorTypes + /** + * Sets the type of the typing indicator. + * + * @param value of Set + * @since 3.11.0 + */ + set(value) { + typingIndicatorTypesMutable = value + } var enableReactions: Boolean /** * Returns a value that determines whether to display the reactions or not. @@ -341,6 +363,7 @@ data class ChannelConfig internal constructor( this._enableOgTag = config._enableOgTag this._enableMention = config._enableMention this._enableTypingIndicator = config._enableTypingIndicator + this._typingIndicatorTypes = config._typingIndicatorTypes this._enableReactions = config._enableReactions this._enableVoiceMessage = config._enableVoiceMessage this._enableMultipleFilesMessage = config._enableMultipleFilesMessage @@ -358,6 +381,7 @@ data class ChannelConfig internal constructor( this.enableOgTagMutable = null this.enableMentionMutable = null this.enableTypingIndicatorMutable = null + this.typingIndicatorTypesMutable = null this.enableReactionsMutable = null this.enableVoiceMessageMutable = null this.enableMultipleFilesMessageMutable = null diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/BaseMessageListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/BaseMessageListComponent.java index c9e9519a..35449ca7 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/BaseMessageListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/BaseMessageListComponent.java @@ -248,18 +248,13 @@ public void scrollToFirst() { * since 3.0.0 */ public void notifyOtherMessageReceived(boolean showTooltipIfPossible) { - if (messageRecyclerView == null) return; - int firstVisibleItemPosition = messageRecyclerView.getRecyclerView().findFirstVisibleItemPosition(); + int firstVisibleItemPosition = getFirstVisibleItemPosition(); if (useMessageTooltip && (firstVisibleItemPosition > 0 || showTooltipIfPossible)) { messageRecyclerView.showNewMessageTooltip(getTooltipMessage(messageRecyclerView.getContext(), tooltipMessageCount.incrementAndGet())); return; } - if (!hasNextMessages()) { - if (firstVisibleItemPosition == 0) { - scrollToFirst(); - } - } + scrollToFirstIfLastMessageVisible(true); } /** @@ -478,12 +473,16 @@ public void notifyChannelChanged(@NonNull GroupChannel channel) { * since 3.0.0 */ public void notifyMessagesFilled(boolean scrollToFirstIfPossible) { - if (messageRecyclerView == null) return; - int firstVisibleItemPosition = messageRecyclerView.getRecyclerView().findFirstVisibleItemPosition(); + scrollToFirstIfLastMessageVisible(scrollToFirstIfPossible); + } - if (firstVisibleItemPosition == 0 && !hasNextMessages() && scrollToFirstIfPossible) { - scrollToFirst(); - } + /** + * After updating the typing indicator, determines whether to scroll to the bottom. + * + * since 3.11.0 + */ + public void notifyTypingIndicatorUpdated(boolean scrollToFirstIfPossible) { + scrollToFirstIfLastMessageVisible(scrollToFirstIfPossible); } /** @@ -732,7 +731,10 @@ private int scrollToFoundPosition(long createdAt, int offset, MessageRecyclerVie position = size - 1; } - layoutManager.scrollToPositionWithOffset(position, offset); + // To show the top of the item view, scroll to next item position with offset. + layoutManager.scrollToPositionWithOffset( + position + 1 >= size ? position : position + 1, offset + ); return position; } @@ -770,6 +772,21 @@ private void onScrollEndReaches(@NonNull PagerRecyclerView.ScrollDirection direc } } + private void scrollToFirstIfLastMessageVisible(boolean scrollToFirstIfPossible) { + int firstVisibleItemPosition = getFirstVisibleItemPosition(); + + if (firstVisibleItemPosition == 0 && !hasNextMessages() && scrollToFirstIfPossible) { + scrollToFirst(); + } + } + + // Implement the logic to handle the case when messageRecyclerView is null. + // If messageRecyclerView is null, consider returning an appropriate default value -1. + private int getFirstVisibleItemPosition() { + if (messageRecyclerView == null) return -1; + return messageRecyclerView.getRecyclerView().findFirstVisibleItemPosition(); + } + /** * A collection of parameters, which can be applied to a default View. The values of params are not dynamically applied at runtime. * Params cannot be created directly, and it is automatically created together when components are created. diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java index a4248d64..66d7fd2f 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java @@ -30,6 +30,7 @@ import com.sendbird.uikit.consts.MessageLoadState; import com.sendbird.uikit.consts.ReplyType; import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.consts.TypingIndicatorType; import com.sendbird.uikit.interfaces.OnCompleteHandler; import com.sendbird.uikit.internal.extensions.MessageExtensionsKt; import com.sendbird.uikit.internal.wrappers.MessageCollectionImpl; @@ -40,6 +41,7 @@ import com.sendbird.uikit.internal.wrappers.SendbirdUIKitWrapper; import com.sendbird.uikit.log.Logger; import com.sendbird.uikit.model.SuggestedRepliesMessage; +import com.sendbird.uikit.model.TypingIndicatorMessage; import com.sendbird.uikit.model.configurations.ChannelConfig; import com.sendbird.uikit.model.configurations.UIKitConfig; import com.sendbird.uikit.utils.Available; @@ -298,6 +300,9 @@ public void onChannelUpdated(@NonNull GroupChannelContext context, @NonNull Grou } else { typingMembers.setValue(null); } + if (channelConfig.getEnableTypingIndicator() && channelConfig.getTypingIndicatorTypes().contains(TypingIndicatorType.BUBBLE)) { + notifyDataSetChanged(context); + } break; case EVENT_DELIVERY_STATUS_UPDATED: case EVENT_READ_STATUS_UPDATED: @@ -526,6 +531,11 @@ synchronized void notifyDataSetChanged(@NonNull String traceName) { if (suggestedRepliesMessage != null) { copiedList.add(0, suggestedRepliesMessage); } + + TypingIndicatorMessage typingIndicatorMessage = createTypingIndicatorMessage(); + if (typingIndicatorMessage != null) { + copiedList.add(0, typingIndicatorMessage); + } } if (copiedList.size() == 0) { @@ -568,6 +578,20 @@ private SuggestedRepliesMessage createSuggestedRepliesMessage() { return null; } + @Nullable + private TypingIndicatorMessage createTypingIndicatorMessage() { + if (!channelConfig.getEnableTypingIndicator() || !channelConfig.getTypingIndicatorTypes().contains(TypingIndicatorType.BUBBLE)) return null; + GroupChannel groupChannel = channel; + if (groupChannel != null) { + List typingUsers = groupChannel.getTypingUsers(); + if (!typingUsers.isEmpty()) { + return new TypingIndicatorMessage(groupChannel.getUrl(), typingUsers); + } + } + + return null; + } + private void markAsRead() { Logger.dev("markAsRead"); if (channel != null) channel.markAsRead(null); diff --git a/uikit/src/main/res/drawable/sb_shape_typing_indicator_dot.xml b/uikit/src/main/res/drawable/sb_shape_typing_indicator_dot.xml new file mode 100644 index 00000000..6ea7d638 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_typing_indicator_dot.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/uikit/src/main/res/drawable/sb_typing_member_message_background_dark.xml b/uikit/src/main/res/drawable/sb_typing_member_message_background_dark.xml new file mode 100644 index 00000000..03931e5b --- /dev/null +++ b/uikit/src/main/res/drawable/sb_typing_member_message_background_dark.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/uikit/src/main/res/drawable/sb_typing_member_message_background_light.xml b/uikit/src/main/res/drawable/sb_typing_member_message_background_light.xml new file mode 100644 index 00000000..e36f4422 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_typing_member_message_background_light.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/uikit/src/main/res/layout/sb_view_typing_indicator_dots_component.xml b/uikit/src/main/res/layout/sb_view_typing_indicator_dots_component.xml new file mode 100644 index 00000000..e23e3ca2 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_typing_indicator_dots_component.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + diff --git a/uikit/src/main/res/layout/sb_view_typing_indicator_message.xml b/uikit/src/main/res/layout/sb_view_typing_indicator_message.xml new file mode 100644 index 00000000..05ec5486 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_typing_indicator_message.xml @@ -0,0 +1,6 @@ + + diff --git a/uikit/src/main/res/layout/sb_view_typing_indicator_message_component.xml b/uikit/src/main/res/layout/sb_view_typing_indicator_message_component.xml new file mode 100644 index 00000000..c68dc3e8 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_typing_indicator_message_component.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/uikit/src/main/res/layout/sb_view_typing_member_component.xml b/uikit/src/main/res/layout/sb_view_typing_member_component.xml new file mode 100644 index 00000000..f081fe90 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_typing_member_component.xml @@ -0,0 +1,36 @@ + + + + + + + + + +