From 2b89f90627a32fb05161a241d53b02f8f954a2fe Mon Sep 17 00:00:00 2001 From: "sha.sdk_deployment" Date: Fri, 3 Nov 2023 09:10:29 +0000 Subject: [PATCH] Added v3.10.0 --- CHANGELOG.md | 5 + gradle.properties | 2 +- .../BaseApplication.kt | 4 + .../com/sendbird/uikit/SendbirdUIKit.java | 2 +- .../adapter/BaseMessageListAdapter.java | 2 +- .../activities/adapter/FormFieldAdapter.kt | 94 ++++++++++ .../adapter/MessageDiffCallback.java | 7 + .../adapter/MessageListAdapter.java | 56 ++++++ .../adapter/SuggestedRepliesAdapter.kt | 76 ++++++++ .../activities/viewholder/MessageType.java | 20 ++- .../viewholder/MessageViewHolderFactory.java | 25 +++ .../com/sendbird/uikit/consts/StringSet.kt | 18 ++ .../fragments/BaseMessageListFragment.java | 1 - .../uikit/fragments/ChannelFragment.java | 24 +++ .../internal/extensions/MessageExtensions.kt | 62 +++++++ .../MessageListAdapterExtensions.kt | 10 ++ .../MessageListComponentExtensions.kt | 10 ++ .../interfaces/OnSubmitButtonClickListener.kt | 8 + .../internal/model/ExtendedMessageType.kt | 36 ---- .../com/sendbird/uikit/internal/model/Form.kt | 84 +++++++++ .../model/template_messages/KeySet.kt | 2 + .../internal/ui/messages/FormFieldView.kt | 167 +++++++++++++++++ .../internal/ui/messages/FormMessageView.kt | 168 ++++++++++++++++++ .../messages/SuggestedRepliesMessageView.kt | 67 +++++++ .../ui/messages/SuggestedReplyView.kt | 45 +++++ .../ui/viewholders/FormMessageViewHolder.kt | 29 +++ .../viewholders/SuggestedRepliesViewHolder.kt | 29 +++ .../uikit/model/SuggestedRepliesMessage.kt | 11 ++ .../model/configurations/ChannelConfig.kt | 54 +++++- .../components/BaseMessageListComponent.java | 1 + .../components/MessageListComponent.java | 46 +++++ .../uikit/vm/BaseMessageListViewModel.java | 8 + .../sendbird/uikit/vm/ChannelViewModel.java | 29 +++ ...hape_edit_text_form_field_invalid_dark.xml | 8 + ...ape_edit_text_form_field_invalid_light.xml | 8 + ...shape_edit_text_form_field_normal_dark.xml | 8 + ...hape_edit_text_form_field_normal_light.xml | 8 + ..._shape_round_rect_background_ondark_02.xml | 9 + ...shape_round_rect_background_onlight_04.xml | 9 + .../drawable/sb_shape_submit_button_dark.xml | 8 + .../drawable/sb_shape_submit_button_light.xml | 8 + .../sb_suggested_replies_button_dark.xml | 24 +++ .../sb_suggested_replies_button_light.xml | 24 +++ .../main/res/layout/sb_view_form_field.xml | 6 + .../layout/sb_view_form_field_component.xml | 89 ++++++++++ .../main/res/layout/sb_view_form_message.xml | 6 + .../layout/sb_view_form_message_component.xml | 100 +++++++++++ .../sb_view_suggested_replies_message.xml | 6 + ...ew_suggested_replies_message_component.xml | 29 +++ .../res/layout/sb_view_suggested_reply.xml | 6 + .../sb_view_suggested_reply_component.xml | 23 +++ uikit/src/main/res/values/dimens.xml | 1 + uikit/src/main/res/values/strings.xml | 6 + 53 files changed, 1544 insertions(+), 44 deletions(-) create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/adapter/FormFieldAdapter.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/adapter/SuggestedRepliesAdapter.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageListAdapterExtensions.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageListComponentExtensions.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnSubmitButtonClickListener.kt delete mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/model/ExtendedMessageType.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/model/Form.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormFieldView.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormMessageView.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedRepliesMessageView.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedReplyView.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/FormMessageViewHolder.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/SuggestedRepliesViewHolder.kt create mode 100644 uikit/src/main/java/com/sendbird/uikit/model/SuggestedRepliesMessage.kt create mode 100644 uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_dark.xml create mode 100644 uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_light.xml create mode 100644 uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_dark.xml create mode 100644 uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_light.xml create mode 100644 uikit/src/main/res/drawable/sb_shape_round_rect_background_ondark_02.xml create mode 100644 uikit/src/main/res/drawable/sb_shape_round_rect_background_onlight_04.xml create mode 100644 uikit/src/main/res/drawable/sb_shape_submit_button_dark.xml create mode 100644 uikit/src/main/res/drawable/sb_shape_submit_button_light.xml create mode 100644 uikit/src/main/res/drawable/sb_suggested_replies_button_dark.xml create mode 100644 uikit/src/main/res/drawable/sb_suggested_replies_button_light.xml create mode 100644 uikit/src/main/res/layout/sb_view_form_field.xml create mode 100644 uikit/src/main/res/layout/sb_view_form_field_component.xml create mode 100644 uikit/src/main/res/layout/sb_view_form_message.xml create mode 100644 uikit/src/main/res/layout/sb_view_form_message_component.xml create mode 100644 uikit/src/main/res/layout/sb_view_suggested_replies_message.xml create mode 100644 uikit/src/main/res/layout/sb_view_suggested_replies_message_component.xml create mode 100644 uikit/src/main/res/layout/sb_view_suggested_reply.xml create mode 100644 uikit/src/main/res/layout/sb_view_suggested_reply_component.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index e9fb80ca..fe454d30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ # Changelog +### v3.10.0 (Nov 3, 2023) with Chat SDK `v4.13.0` +* Added the `Suggested Replies` feature to enable quick and effective question asking to the bot. + * Added `ChannelConfig.enableSuggestedReplies` configuration to enable/disable `Suggested Replies` feature. +* Added the `Form type message` feature to enable the user to submit a form type message received by the bot. + * Added `ChannelConfig.enableFormTypeMessage` configuration to enable/disable `Form type message` feature. ### v3.9.3 (Oct 26, 2023) with Chat SDK `v4.13.0` * Improve stability. diff --git a/gradle.properties b/gradle.properties index 2630eb6c..667ca39e 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.9.3 +UIKIT_VERSION = 3.10.0 UIKIT_VERSION_CODE = 1 diff --git a/uikit-sample/src/main/kotlin/com/sendbird/uikit_messaging_android/BaseApplication.kt b/uikit-sample/src/main/kotlin/com/sendbird/uikit_messaging_android/BaseApplication.kt index e8f794ff..aff389bb 100644 --- a/uikit-sample/src/main/kotlin/com/sendbird/uikit_messaging_android/BaseApplication.kt +++ b/uikit-sample/src/main/kotlin/com/sendbird/uikit_messaging_android/BaseApplication.kt @@ -117,6 +117,10 @@ class BaseApplication : MultiDexApplication() { UIKitConfig.groupChannelConfig.enableVoiceMessage = true // set whether to use Multiple Files Message UIKitConfig.groupChannelConfig.enableMultipleFilesMessage = true + // set whether to use suggested replies + UIKitConfig.groupChannelConfig.enableSuggestedReplies = true + // set whether to use form type message + UIKitConfig.groupChannelConfig.enableFormTypeMessage = true // set custom params SendbirdUIKit.setCustomParamsHandler(object : CustomParamsHandler { diff --git a/uikit/src/main/java/com/sendbird/uikit/SendbirdUIKit.java b/uikit/src/main/java/com/sendbird/uikit/SendbirdUIKit.java index b3c5f1ef..e2351ea0 100644 --- a/uikit/src/main/java/com/sendbird/uikit/SendbirdUIKit.java +++ b/uikit/src/main/java/com/sendbird/uikit/SendbirdUIKit.java @@ -977,7 +977,7 @@ public static CustomParamsHandler getCustomParamsHandler() { * @param botId The bot ID that is created in dashboard. * @param isDistinct Determines whether to reuse an existing channel or create a new channel. * @param handler The callback handler that lets you know if the request was successful or not. - * @since 3.8.0 + * since 3.8.0 */ public static void startChatWithAiBot(@NonNull Context context, @NonNull String botId, boolean isDistinct, @Nullable CompletionHandler handler) { User currentUser = SendbirdChat.getCurrentUser(); diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BaseMessageListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BaseMessageListAdapter.java index bd17b869..e3aa6584 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BaseMessageListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BaseMessageListAdapter.java @@ -40,7 +40,6 @@ import com.sendbird.uikit.model.MessageListUIParams; import com.sendbird.uikit.model.MessageUIConfig; import com.sendbird.uikit.model.configurations.ChannelConfig; -import com.sendbird.uikit.utils.TextUtils; import org.jetbrains.annotations.TestOnly; @@ -70,6 +69,7 @@ abstract public class BaseMessageListAdapter extends BaseMessageAdapter listItemLongClickListener; @Nullable protected OnItemClickListener mentionClickListener; + @NonNull private final MessageListUIParams messageListUIParams; @Nullable diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/FormFieldAdapter.kt b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/FormFieldAdapter.kt new file mode 100644 index 00000000..fe5ec23d --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/FormFieldAdapter.kt @@ -0,0 +1,94 @@ +package com.sendbird.uikit.activities.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import com.sendbird.uikit.activities.viewholder.BaseViewHolder +import com.sendbird.uikit.databinding.SbViewFormFieldBinding +import com.sendbird.uikit.internal.model.Form +import com.sendbird.uikit.internal.model.FormField + +internal class FormFieldAdapter : BaseAdapter>() { + private val formFields: MutableList = mutableListOf() + + fun isReadyToSubmit(): Boolean { + return formFields.all { it.isReadyToSubmit() } + } + + fun updateValidation() { + formFields.forEachIndexed { index, formField -> + val lastValidation = formField.lastValidation + val validation = formField.isReadyToSubmit() + formField.lastValidation = validation + if (lastValidation != validation) { + notifyItemChanged(index) + } + } + } + + fun setFormFields(form: Form) { + val newFormFields = if (form.isAnswered) { + form.formFields.filter { it.answer != null } + } else { + form.formFields + } + val diffResult = DiffUtil.calculateDiff(FormFieldDiffCallback(formFields, newFormFields)) + formFields.clear() + formFields.addAll(newFormFields) + diffResult.dispatchUpdatesTo(this) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + return FormFieldViewHolder( + SbViewFormFieldBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + } + + override fun getItemCount(): Int { + return formFields.size + } + + override fun getItem(position: Int): FormField { + return formFields[position] + } + + override fun getItems(): List { + return formFields.toList() + } + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + internal class FormFieldViewHolder( + val binding: SbViewFormFieldBinding + ) : BaseViewHolder(binding.root) { + override fun bind(item: FormField) { + binding.formFieldView.drawFormField(item) + } + } + + private class FormFieldDiffCallback( + private val oldList: List, + private val newList: List + ) : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldList[oldItemPosition] + val newItem = newList[newItemPosition] + return oldItem.formFieldKey == newItem.formFieldKey && + oldItem.messageId == newItem.messageId + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] + } + } +} 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 d60b2b40..6e7293f2 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 @@ -18,6 +18,7 @@ import com.sendbird.uikit.utils.MessageUtils; import java.util.List; +import java.util.Map; class MessageDiffCallback extends DiffUtil.Callback { @NonNull @@ -83,6 +84,12 @@ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { } } + Map oldExtendedMessagePayload = oldMessage.getExtendedMessagePayload(); + Map newExtendedMessagePayload = newMessage.getExtendedMessagePayload(); + if (!oldExtendedMessagePayload.equals(newExtendedMessagePayload)) { + return false; + } + if (messageListUIParams.shouldUseMessageReceipt()) { if (oldChannel.getUnreadMemberCount(newMessage) != newChannel.getUnreadMemberCount(newMessage)) { return false; diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageListAdapter.java index f9e54721..dee1c03e 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageListAdapter.java @@ -1,11 +1,19 @@ package com.sendbird.uikit.activities.adapter; +import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.sendbird.android.channel.GroupChannel; import com.sendbird.android.message.BaseMessage; +import com.sendbird.uikit.activities.viewholder.MessageViewHolder; +import com.sendbird.uikit.interfaces.OnItemClickListener; +import com.sendbird.uikit.internal.extensions.MessageListAdapterExtensionsKt; +import com.sendbird.uikit.internal.interfaces.OnSubmitButtonClickListener; +import com.sendbird.uikit.internal.ui.viewholders.FormMessageViewHolder; +import com.sendbird.uikit.internal.ui.viewholders.SuggestedRepliesViewHolder; import com.sendbird.uikit.internal.wrappers.SendbirdUIKitImpl; import com.sendbird.uikit.internal.wrappers.SendbirdUIKitWrapper; import com.sendbird.uikit.model.MessageListUIParams; @@ -14,6 +22,9 @@ * MessageListAdapter provides a binding from a {@link BaseMessage} type data set to views that are displayed within a RecyclerView. */ public class MessageListAdapter extends BaseMessageListAdapter { + @Nullable + protected OnItemClickListener suggestedRepliesClickListener; + public MessageListAdapter(boolean useMessageGroupUI) { this(null, useMessageGroupUI); } @@ -40,4 +51,49 @@ public MessageListAdapter(@Nullable GroupChannel channel, @NonNull MessageListUI .build(), sendbirdUIKit); } + + @Override + public void onBindViewHolder(@NonNull MessageViewHolder holder, int position) { + super.onBindViewHolder(holder, position); + if (holder instanceof SuggestedRepliesViewHolder) { + SuggestedRepliesViewHolder suggestedRepliesViewHolder = (SuggestedRepliesViewHolder) holder; + suggestedRepliesViewHolder.setSuggestedRepliesClickedListener((view, pos, data) -> { + int messagePosition = holder.getBindingAdapterPosition(); + if (messagePosition != NO_POSITION && suggestedRepliesClickListener != null) { + suggestedRepliesClickListener.onItemClick(view, pos, data); + } + }); + } + + if (holder instanceof FormMessageViewHolder) { + FormMessageViewHolder formMessageViewHolder = (FormMessageViewHolder) holder; + formMessageViewHolder.setOnSubmitClickListener((message, form) -> { + final OnSubmitButtonClickListener finalListener = MessageListAdapterExtensionsKt.getSubmitButtonClickListener(this); + if (finalListener != null) { + finalListener.onClicked(message, form); + } + }); + } + } + + /** + * Returns a callback to be invoked when the suggested replies is clicked. + * + * @return {OnItemClickListener} to be invoked when the suggested replies is clicked. + * since 3.10.0 + */ + @Nullable + public OnItemClickListener getSuggestedRepliesClickListener() { + return suggestedRepliesClickListener; + } + + /** + * Register a callback to be invoked when the suggested replies is clicked. + * + * @param suggestedRepliesClickListener The callback to be registered. + * since 3.10.0 + */ + public void setSuggestedRepliesClickListener(@Nullable OnItemClickListener suggestedRepliesClickListener) { + this.suggestedRepliesClickListener = suggestedRepliesClickListener; + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/SuggestedRepliesAdapter.kt b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/SuggestedRepliesAdapter.kt new file mode 100644 index 00000000..9b5da3ef --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/SuggestedRepliesAdapter.kt @@ -0,0 +1,76 @@ +package com.sendbird.uikit.activities.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import com.sendbird.uikit.activities.viewholder.BaseViewHolder +import com.sendbird.uikit.databinding.SbViewSuggestedReplyBinding +import com.sendbird.uikit.interfaces.OnItemClickListener + +internal class SuggestedRepliesAdapter : BaseAdapter>() { + var onItemClickListener: OnItemClickListener? = null + var suggestedReplies: List = listOf() + set(value) { + val diffResult = DiffUtil.calculateDiff(SuggestedReplyDiffCallback(field, value)) + field = value + diffResult.dispatchUpdatesTo(this) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + return SuggestedReplyViewHolder( + SbViewSuggestedReplyBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ).apply { + this.binding.suggestedReplyView.setOnClickListener { + val item = getItem(absoluteAdapterPosition) + val index = suggestedReplies.indexOf(item) + if (index == -1) return@setOnClickListener + onItemClickListener?.onItemClick(binding.root, index, item) + } + } + } + + override fun getItemCount(): Int { + return suggestedReplies.size + } + + override fun getItem(position: Int): String { + return suggestedReplies[position] + } + + override fun getItems(): List { + return suggestedReplies.toList() + } + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + holder.bind(getItem(position)) + } + + private class SuggestedReplyViewHolder( + val binding: SbViewSuggestedReplyBinding + ) : BaseViewHolder(binding.root) { + override fun bind(item: String) { + binding.suggestedReplyView.drawSuggestedReplies(item) + } + } + + private class SuggestedReplyDiffCallback( + private val oldList: List, + private val newList: List + ) : DiffUtil.Callback() { + override fun getOldListSize(): Int { + return oldList.size + } + + override fun getNewListSize(): Int { + return newList.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldList[oldItemPosition] == newList[newItemPosition] + } + } +} 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 38440786..54d69706 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 @@ -89,16 +89,30 @@ public enum MessageType { /** * Type of MultipleFilesMessage sent by the current user. * - * @since 3.9.0 + * since 3.9.0 */ VIEW_TYPE_MULTIPLE_FILES_MESSAGE_ME(17), /** * Type of MultipleFilesMessage sent by users other than the current user . * - * @since 3.9.0 + * since 3.9.0 */ - VIEW_TYPE_MULTIPLE_FILES_MESSAGE_OTHER(18); + VIEW_TYPE_MULTIPLE_FILES_MESSAGE_OTHER(18), + + /** + * Type of suggested replies. + * + * since 3.10.0 + */ + VIEW_TYPE_SUGGESTED_REPLIES(19), + + /** + * Type of forms message. + * + * since 3.10.0 + */ + VIEW_TYPE_FORM_TYPE_MESSAGE(20); final 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 8be04858..0d25b93d 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 @@ -5,6 +5,7 @@ import androidx.annotation.NonNull; +import com.sendbird.android.channel.ChannelType; import com.sendbird.android.message.AdminMessage; import com.sendbird.android.message.BaseMessage; import com.sendbird.android.message.FileMessage; @@ -12,6 +13,7 @@ import com.sendbird.android.message.UserMessage; import com.sendbird.uikit.consts.StringSet; import com.sendbird.uikit.databinding.SbViewAdminMessageBinding; +import com.sendbird.uikit.databinding.SbViewFormMessageBinding; import com.sendbird.uikit.databinding.SbViewMyFileImageMessageBinding; import com.sendbird.uikit.databinding.SbViewMyFileMessageBinding; import com.sendbird.uikit.databinding.SbViewMyFileVideoMessageBinding; @@ -30,9 +32,11 @@ import com.sendbird.uikit.databinding.SbViewOtherUserMessageBinding; import com.sendbird.uikit.databinding.SbViewOtherVoiceMessageBinding; import com.sendbird.uikit.databinding.SbViewParentMessageInfoHolderBinding; +import com.sendbird.uikit.databinding.SbViewSuggestedRepliesMessageBinding; import com.sendbird.uikit.databinding.SbViewTimeLineMessageBinding; import com.sendbird.uikit.internal.extensions.MessageExtensionsKt; import com.sendbird.uikit.internal.ui.viewholders.AdminMessageViewHolder; +import com.sendbird.uikit.internal.ui.viewholders.FormMessageViewHolder; import com.sendbird.uikit.internal.ui.viewholders.MyFileMessageViewHolder; import com.sendbird.uikit.internal.ui.viewholders.MyImageFileMessageViewHolder; import com.sendbird.uikit.internal.ui.viewholders.MyMultipleFilesMessageViewHolder; @@ -51,11 +55,15 @@ import com.sendbird.uikit.internal.ui.viewholders.OtherVideoFileMessageViewHolder; import com.sendbird.uikit.internal.ui.viewholders.OtherVoiceMessageViewHolder; 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.model.MessageListUIParams; +import com.sendbird.uikit.model.SuggestedRepliesMessage; import com.sendbird.uikit.model.TimelineMessage; import com.sendbird.uikit.utils.MessageUtils; +import java.util.Map; + /** * A Factory manages a type of messages. */ @@ -214,6 +222,12 @@ public static MessageViewHolder createViewHolder(@NonNull LayoutInflater inflate case VIEW_TYPE_VOICE_MESSAGE_OTHER: holder = new OtherVoiceMessageViewHolder(SbViewOtherVoiceMessageBinding.inflate(inflater, parent, false), messageListUIParams); break; + case VIEW_TYPE_SUGGESTED_REPLIES: + holder = new SuggestedRepliesViewHolder(SbViewSuggestedRepliesMessageBinding.inflate(inflater, parent, false), messageListUIParams); + break; + case VIEW_TYPE_FORM_TYPE_MESSAGE: + holder = new FormMessageViewHolder(SbViewFormMessageBinding.inflate(inflater, parent, false), messageListUIParams); + break; default: // unknown message type if (viewType == MessageType.VIEW_TYPE_UNKNOWN_MESSAGE_ME) { @@ -245,6 +259,15 @@ public static int getViewType(@NonNull BaseMessage message) { public static MessageType getMessageType(@NonNull BaseMessage message) { MessageType type; + Map extendedMessagePayload = message.getExtendedMessagePayload(); + if (!extendedMessagePayload.isEmpty()) { + if (message.getChannelType() == ChannelType.GROUP + && !MessageExtensionsKt.getForms(message).isEmpty() + ) { + return MessageType.VIEW_TYPE_FORM_TYPE_MESSAGE; + } + } + if (message instanceof UserMessage) { if (MessageUtils.isMine(message)) { type = MessageType.VIEW_TYPE_USER_MESSAGE_ME; @@ -298,6 +321,8 @@ public static MessageType getMessageType(@NonNull BaseMessage message) { type = MessageType.VIEW_TYPE_TIME_LINE; } else if (message instanceof AdminMessage) { type = MessageType.VIEW_TYPE_ADMIN_MESSAGE; + } else if (message instanceof SuggestedRepliesMessage) { + type = MessageType.VIEW_TYPE_SUGGESTED_REPLIES; } 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 27191e9b..3990c980 100644 --- a/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt +++ b/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt @@ -147,4 +147,22 @@ object StringSet { const val quote_reply = "quote_reply" const val thread = "thread" const val parent = "parent" + + // AI ChatBot + const val sub_type = "sub_type" + const val sub_data = "sub_data" + const val suggested_replies = "suggested_replies" + const val forms = "forms" + const val key = "key" + const val fields = "fields" + const val data = "data" + const val title = "title" + const val input_type = "input_type" + const val regex = "regex" + const val placeholder = "placeholder" + const val required = "required" + const val text = "text" + const val phone = "phone" + const val email = "email" + const val password = "password" } diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/BaseMessageListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/BaseMessageListFragment.java index cfda2024..a4551595 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/BaseMessageListFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/BaseMessageListFragment.java @@ -424,7 +424,6 @@ void showWarningDialog(@NonNull BaseMessage message) { cancel -> Logger.dev("cancel")); } - @VisibleForTesting void showConfirmDialog(@NonNull String message) { if (getContext() == null) return; DialogUtils.showConfirmDialog( 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 09251707..6944215c 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java @@ -49,6 +49,8 @@ import com.sendbird.uikit.interfaces.OnInputTextChangedListener; import com.sendbird.uikit.interfaces.OnItemClickListener; import com.sendbird.uikit.interfaces.OnItemLongClickListener; +import com.sendbird.uikit.internal.extensions.MessageExtensionsKt; +import com.sendbird.uikit.internal.extensions.MessageListComponentExtensionsKt; import com.sendbird.uikit.internal.model.VoicePlayerManager; import com.sendbird.uikit.log.Logger; import com.sendbird.uikit.model.DialogListItem; @@ -200,6 +202,7 @@ public void onDestroy() { if (!isInitCallFinished.get()) { shouldDismissLoadingDialog(); } + MessageExtensionsKt.clearTemporaryAnswers(getChannelUrl()); } /** @@ -255,6 +258,7 @@ protected void onBindMessageListComponent(@NonNull MessageListComponent messageL messageListComponent.setOnEmojiReactionClickListener(emojiReactionClickListener != null ? emojiReactionClickListener : (view, position, message, reactionKey) -> toggleReaction(view, message, reactionKey)); messageListComponent.setOnEmojiReactionLongClickListener(emojiReactionLongClickListener != null ? emojiReactionLongClickListener : (view, position, message, reactionKey) -> showEmojiReactionDialog(message, position)); messageListComponent.setOnEmojiReactionMoreButtonClickListener(emojiReactionMoreButtonClickListener != null ? emojiReactionMoreButtonClickListener : (view, position, message) -> showEmojiListDialog(message)); + messageListComponent.setSuggestedRepliesClickListener((view, position, data) -> onSuggestedRepliesClicked(data)); messageListComponent.setOnTooltipClickListener(tooltipClickListener != null ? tooltipClickListener : this::onMessageTooltipClicked); messageListComponent.setOnQuoteReplyMessageLongClickListener(this::onQuoteReplyMessageLongClicked); @@ -269,6 +273,14 @@ protected void onBindMessageListComponent(@NonNull MessageListComponent messageL return false; }); + MessageListComponentExtensionsKt.setSubmitButtonClickListener(messageListComponent, (message, form) -> { + MessageExtensionsKt.submitForm(message, form, (e) -> { + if (e != null) { + showConfirmDialog(getString(R.string.sb_forms_submit_failed)); + } + }); + }); + final ChannelModule module = getModule(); viewModel.getMessageList().observeAlways(getViewLifecycleOwner(), receivedMessageData -> { boolean isInitialCallFinished = isInitCallFinished.getAndSet(true); @@ -501,6 +513,18 @@ protected void onThreadInfoClicked(@NonNull View view, int position, @NonNull Ba startMessageThreadActivity(message); } + /** + * Called when the a suggested replies view is clicked + * + * @param suggestedReply Clicked suggested reply data. + * since 3.10.0 + */ + protected void onSuggestedRepliesClicked(@NonNull String suggestedReply) { + UserMessageCreateParams params = new UserMessageCreateParams(suggestedReply); + sendUserMessage(params); + } + + /** * Find the same message as the message ID and move it to the matching message. * diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageExtensions.kt b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageExtensions.kt index ddf64f97..03ffb7b4 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageExtensions.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageExtensions.kt @@ -1,15 +1,20 @@ package com.sendbird.uikit.internal.extensions import android.content.Context +import com.sendbird.android.annotation.AIChatBotExperimental +import com.sendbird.android.handler.CompletionHandler import com.sendbird.android.message.BaseFileMessage import com.sendbird.android.message.BaseMessage import com.sendbird.android.message.FileMessage import com.sendbird.android.message.MultipleFilesMessage import com.sendbird.uikit.R import com.sendbird.uikit.consts.StringSet +import com.sendbird.uikit.internal.model.Form +import com.sendbird.uikit.internal.singleton.JsonParser import com.sendbird.uikit.internal.singleton.MessageDisplayDataManager import com.sendbird.uikit.model.UserMessageDisplayData import com.sendbird.uikit.utils.MessageUtils +import java.util.concurrent.ConcurrentHashMap internal fun BaseMessage.hasParentMessage() = parentMessageId != 0L @@ -75,3 +80,60 @@ internal fun BaseFileMessage.getName(context: Context): String { } } } + +@OptIn(AIChatBotExperimental::class) +internal fun BaseMessage.submitForm(form: Form, handler: CompletionHandler? = null) { + val answers = form.formFields.fold(mutableMapOf()) { acc, formField -> + val answer = formField.temporaryAnswer ?: return@fold acc + acc.apply { put(answer.formFieldKey, answer.answer) } + } + + this.submitForm(form.formKey, answers) { e -> + handler?.onResult(e) + } +} + +internal val BaseMessage.suggestedReplies: List + get() { + val suggestedReplies = extendedMessagePayload[StringSet.suggested_replies] ?: return emptyList() + return try { + JsonParser.fromJson(suggestedReplies) + } catch (e: Exception) { + emptyList() + } + } + +internal val formMap: MutableMap>> = ConcurrentHashMap() + +internal val BaseMessage.forms: List
+ get() { + formMap[this.messageId]?.let { return it.second } + val forms = extendedMessagePayload[StringSet.forms] ?: return emptyList() + return try { + JsonParser.fromJson>(forms).onEach { form -> + // setting answer to formField manually. + val answeredList = form.answeredList + form.formFields.forEach { formField -> + formField.messageId = this.messageId + formField.answer = answeredList?.find { it.formFieldKey == formField.formFieldKey } + } + }.also { + formMap[messageId] = this.channelUrl to it + } + } catch (e: Exception) { + emptyList() + } + } + +internal fun clearTemporaryAnswers(channelUrl: String) { + formMap.forEach { + if (it.value.first == channelUrl) { + it.value.second.forEach { form -> + form.formFields.forEach { formField -> + formField.temporaryAnswer = null + formField.lastValidation = null + } + } + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageListAdapterExtensions.kt b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageListAdapterExtensions.kt new file mode 100644 index 00000000..00ef18c4 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageListAdapterExtensions.kt @@ -0,0 +1,10 @@ +package com.sendbird.uikit.internal.extensions + +import com.sendbird.uikit.activities.adapter.MessageListAdapter +import com.sendbird.uikit.internal.interfaces.OnSubmitButtonClickListener +private var submitButtonClickListener: OnSubmitButtonClickListener? = null +internal var MessageListAdapter.submitButtonClickListener: OnSubmitButtonClickListener? + get() = com.sendbird.uikit.internal.extensions.submitButtonClickListener + set(value) { + com.sendbird.uikit.internal.extensions.submitButtonClickListener = value + } diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageListComponentExtensions.kt b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageListComponentExtensions.kt new file mode 100644 index 00000000..8a56bd1d --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageListComponentExtensions.kt @@ -0,0 +1,10 @@ +package com.sendbird.uikit.internal.extensions +import com.sendbird.uikit.internal.interfaces.OnSubmitButtonClickListener +import com.sendbird.uikit.modules.components.MessageListComponent + +private var submitButtonClickListener: OnSubmitButtonClickListener? = null +internal var MessageListComponent.submitButtonClickListener: OnSubmitButtonClickListener? + get() = com.sendbird.uikit.internal.extensions.submitButtonClickListener + set(value) { + com.sendbird.uikit.internal.extensions.submitButtonClickListener = value + } diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnSubmitButtonClickListener.kt b/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnSubmitButtonClickListener.kt new file mode 100644 index 00000000..0df73bbe --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnSubmitButtonClickListener.kt @@ -0,0 +1,8 @@ +package com.sendbird.uikit.internal.interfaces + +import com.sendbird.android.message.BaseMessage +import com.sendbird.uikit.internal.model.Form + +internal fun interface OnSubmitButtonClickListener { + fun onClicked(message: BaseMessage, form: Form) +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/ExtendedMessageType.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/ExtendedMessageType.kt deleted file mode 100644 index 34144b0b..00000000 --- a/uikit/src/main/java/com/sendbird/uikit/internal/model/ExtendedMessageType.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.sendbird.uikit.internal.model - -import com.sendbird.android.channel.ChannelType -import com.sendbird.android.message.BaseMessage -import com.sendbird.uikit.activities.viewholder.MessageType -import com.sendbird.uikit.internal.extensions.toStringMap -import com.sendbird.uikit.internal.model.template_messages.KeySet -import org.json.JSONObject - -internal enum class ExtendedMessageType(val value: String) { - Notification("0"); - - companion object { - @JvmStatic - fun from(message: BaseMessage): MessageType? { - val subType = message.extendedMessage[KeySet.sub_type] - val subData = message.extendedMessage[KeySet.sub_data] - return if (subData != null && subType != null) { - val subDataMap = JSONObject(subData).toStringMap() - val channelType = subDataMap[KeySet.channel_type] - - ExtendedMessageType.values().firstOrNull { it.value == subType }?.let { - when (it) { - Notification -> { - if (channelType == ChannelType.FEED.value) { - MessageType.VIEW_TYPE_FEED_NOTIFICATION - } else { - MessageType.VIEW_TYPE_CHAT_NOTIFICATION - } - } - } - } - } else null - } - } -} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/Form.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/Form.kt new file mode 100644 index 00000000..a80322c3 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/Form.kt @@ -0,0 +1,84 @@ +package com.sendbird.uikit.internal.model + +import com.sendbird.uikit.consts.StringSet +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import java.util.regex.PatternSyntaxException + +@Serializable +internal data class Form( + @SerialName(StringSet.key) + val formKey: String, + @SerialName(StringSet.fields) + val formFields: List, + @SerialName(StringSet.data) + val data: Map? = null +) { + val answeredList: List? + get() { + return data?.map { Answer(it.key, it.value) } + } + val isAnswered: Boolean + get() = data != null +} + +@Serializable +internal data class FormField( + @SerialName(StringSet.key) + val formFieldKey: String, + @SerialName(StringSet.title) + val title: String, + @SerialName(StringSet.input_type) + val inputType: String? = null, + @SerialName(StringSet.regex) + val regex: String? = null, + @SerialName(StringSet.placeholder) + val placeholder: String? = null, + @SerialName(StringSet.required) + val required: Boolean, + @Transient + var messageId: Long = 0L, + @Transient + var answer: Answer? = null, + @Transient + var temporaryAnswer: Answer? = null, + @Transient + var lastValidation: Boolean? = null +) { + val formFileInputType: FormFieldInputType + get() = FormFieldInputType.from(inputType) + + fun isValid(s: String): Boolean { + val regex = this.regex?.toRegex() ?: return true + return try { + s.matches(regex) + } catch (_: PatternSyntaxException) { + true // if the regex is invalid pattern, it assumes the given string is valid. + } + } + + fun isReadyToSubmit(): Boolean { + val answer = temporaryAnswer + if (answer != null && !isValid(answer.answer)) return false + return !required || answer != null + } +} + +internal data class Answer( + val formFieldKey: String, + var answer: String +) + +internal enum class FormFieldInputType(val value: String) { + TEXT(StringSet.text), + PHONE(StringSet.phone), + EMAIL(StringSet.email), + PASSWORD(StringSet.password); + + companion object { + fun from(inputType: String?): FormFieldInputType { + return values().find { it.value.equals(inputType, true) } ?: TEXT + } + } +} 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 7c158862..108d4d06 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 @@ -91,6 +91,8 @@ internal object KeySet { const val enable_reactions = "enable_reactions" const val enable_voice_message = "enable_voice_message" const val enable_multiple_files_message = "enable_multiple_files_message" + const val enable_suggested_replies = "enable_suggested_replies" + const val enable_form_type_message = "enable_form_type_message" const val reply_type = "reply_type" const val thread_reply_select_type = "thread_reply_select_type" const val enable_message_receipt_status = "enable_message_receipt_status" diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormFieldView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormFieldView.kt new file mode 100644 index 00000000..675bd82c --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormFieldView.kt @@ -0,0 +1,167 @@ +package com.sendbird.uikit.internal.ui.messages + +import android.content.Context +import android.text.Editable +import android.text.TextWatcher +import android.text.method.PasswordTransformationMethod +import android.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import com.sendbird.uikit.R +import com.sendbird.uikit.SendbirdUIKit +import com.sendbird.uikit.databinding.SbViewFormFieldComponentBinding +import com.sendbird.uikit.internal.extensions.setAppearance +import com.sendbird.uikit.internal.model.Answer +import com.sendbird.uikit.internal.model.FormField +import com.sendbird.uikit.internal.model.FormFieldInputType + +internal class FormFieldView @JvmOverloads internal constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : BaseMessageView(context, attrs, defStyle) { + override val binding: SbViewFormFieldComponentBinding = SbViewFormFieldComponentBinding.inflate( + LayoutInflater.from(getContext()), + this, + true + ) + override val layout: View + get() = binding.root + + private val etFormFieldBackground = if (SendbirdUIKit.isDarkMode()) { + ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_field_normal_dark, null) + } else { + ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_field_normal_light, null) + } + + private val etFormFieldBackgroundError = if (SendbirdUIKit.isDarkMode()) { + ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_field_invalid_dark, null) + } else { + ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_field_invalid_light, null) + } + + private var textWatcher: FormFieldTextWatcher? = null + + init { + val isDarkMode = SendbirdUIKit.isDarkMode() + binding.tvFormFieldTitle.setAppearance( + context, + if (isDarkMode) R.style.SendbirdCaption3OnDark02 + else R.style.SendbirdCaption3OnLight02 + ) + + binding.tvFormFieldTitleOptional.setAppearance( + context, + if (isDarkMode) R.style.SendbirdCaption3OnDark03 + else R.style.SendbirdCaption3OnLight03 + ) + + binding.etFormField.background = etFormFieldBackground + + binding.etFormField.setAppearance( + context, + if (isDarkMode) R.style.SendbirdBody3OnDark01 + else R.style.SendbirdBody3OnLight01 + ) + + binding.etFormField.setHintTextColor( + ContextCompat.getColor(context, if (isDarkMode) R.color.ondark_03 else R.color.onlight_03) + ) + + binding.tvFormFieldError.setAppearance( + context, + if (isDarkMode) R.style.SendbirdCaption4Error200 + else R.style.SendbirdCaption4Error300 + ) + + binding.answeredLayout.background = if (isDarkMode) { + ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_round_rect_background_onlight_04, null) + } else { + ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_round_rect_background_ondark_02, null) + } + binding.iconDone.setColorFilter( + ContextCompat.getColor(context, if (isDarkMode) R.color.secondary_300 else R.color.secondary_200) + ) + + binding.tvAnswer.setAppearance( + context, + if (isDarkMode) R.style.SendbirdBody3OnDark01 + else R.style.SendbirdBody3OnLight01 + ) + } + + fun drawFormField(formField: FormField) { + textWatcher?.let { binding.etFormField.removeTextChangedListener(it) } + binding.tvFormFieldTitle.text = formField.title + binding.tvFormFieldTitleOptional.visibility = if (formField.required) GONE else VISIBLE + + when (formField.lastValidation) { + true, null -> showValidFormField() + false -> showInvalidFormField() + } + + val answer = formField.answer + if (answer == null) { + binding.unansweredLayout.visibility = VISIBLE + binding.answeredLayout.visibility = GONE + if (formField.formFileInputType == FormFieldInputType.PASSWORD) { + binding.etFormField.transformationMethod = PasswordTransformationMethod() + } else { + binding.etFormField.transformationMethod = null + } + binding.etFormField.setText(formField.temporaryAnswer?.answer ?: "") + textWatcher = FormFieldTextWatcher(formField).also { + binding.etFormField.addTextChangedListener(it) + } + formField.placeholder?.let { binding.etFormField.hint = it } + } else { + binding.unansweredLayout.visibility = GONE + binding.answeredLayout.visibility = VISIBLE + binding.tvAnswer.text = answer.answer + } + } + + fun showValidFormField() { + binding.etFormField.background = etFormFieldBackground + binding.tvFormFieldError.visibility = GONE + } + + fun showInvalidFormField() { + binding.etFormField.background = etFormFieldBackgroundError + binding.tvFormFieldError.visibility = VISIBLE + } + + private inner class FormFieldTextWatcher( + private val formField: FormField + ) : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun afterTextChanged(s: Editable?) {} + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + if (s.isEmpty()) { + formField.temporaryAnswer = null + formField.lastValidation = null + showValidFormField() + return + } + + if (!formField.isValid(s.toString())) { + formField.lastValidation = false + showInvalidFormField() + } else { + formField.lastValidation = true + showValidFormField() + } + + if (formField.temporaryAnswer == null) { + Log.e("nathan", "set temporary answer $formField") + formField.temporaryAnswer = Answer(formField.formFieldKey, s.toString()) + return + } + formField.temporaryAnswer?.answer = s.toString() + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormMessageView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormMessageView.kt new file mode 100644 index 00000000..fe7c3d4a --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormMessageView.kt @@ -0,0 +1,168 @@ +package com.sendbird.uikit.internal.ui.messages + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.sendbird.android.message.BaseMessage +import com.sendbird.uikit.R +import com.sendbird.uikit.SendbirdUIKit +import com.sendbird.uikit.activities.adapter.FormFieldAdapter +import com.sendbird.uikit.databinding.SbViewFormMessageComponentBinding +import com.sendbird.uikit.internal.extensions.forms +import com.sendbird.uikit.internal.extensions.setAppearance +import com.sendbird.uikit.model.MessageListUIParams +import com.sendbird.uikit.utils.DrawableUtils +import com.sendbird.uikit.utils.ViewUtils + +internal class FormMessageView @JvmOverloads internal constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = R.attr.sb_widget_other_user_message +) : BaseMessageView(context, attrs, defStyle) { + override val binding: SbViewFormMessageComponentBinding + private val editedAppearance: Int + private val sentAtAppearance: Int + private val nicknameAppearance: Int + private val messageAppearance: Int + private val formFieldAdapter: FormFieldAdapter = FormFieldAdapter() + + override val layout: View + get() = binding.root + + init { + val a = context.theme.obtainStyledAttributes(attrs, R.styleable.MessageView_User, defStyle, 0) + try { + binding = SbViewFormMessageComponentBinding.inflate(LayoutInflater.from(getContext()), this, true) + val isDarkMode = SendbirdUIKit.isDarkMode() + sentAtAppearance = a.getResourceId( + R.styleable.MessageView_User_sb_message_time_text_appearance, + R.style.SendbirdCaption4OnLight03 + ) + nicknameAppearance = a.getResourceId( + R.styleable.MessageView_User_sb_message_sender_name_text_appearance, + R.style.SendbirdCaption1OnLight02 + ) + editedAppearance = a.getResourceId( + R.styleable.MessageView_User_sb_message_other_edited_mark_text_appearance, + R.style.SendbirdBody3OnLight02 + ) + messageAppearance = a.getResourceId( + R.styleable.MessageView_User_sb_message_other_text_appearance, + R.style.SendbirdBody3OnLight01 + ) + val messageBackground = a.getResourceId( + R.styleable.MessageView_User_sb_message_other_background, + R.drawable.sb_shape_chat_bubble + ) + val messageBackgroundTint = + a.getColorStateList(R.styleable.MessageView_User_sb_message_other_background_tint) + + binding.contentPanel.background = + DrawableUtils.setTintList(context, messageBackground, messageBackgroundTint) + + binding.rvFormFields.adapter = formFieldAdapter + binding.rvFormFields.layoutManager = LinearLayoutManager(context) + binding.rvFormFields.addItemDecoration( + ItemSpacingDecoration(resources.getDimensionPixelSize(R.dimen.sb_size_8)) + ) + + binding.buttonSubmit.background = if (isDarkMode) { + ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_submit_button_dark, null) + } else { + ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_submit_button_light, null) + } + + binding.buttonSubmit.setAppearance( + context, + if (isDarkMode) R.style.SendbirdButtonOnLight01 else R.style.SendbirdButtonOnDark01 + ) + val linkTextColor = a.getColorStateList(R.styleable.MessageView_User_sb_message_other_link_text_color) + val clickedLinkBackgroundColor = a.getResourceId( + R.styleable.MessageView_User_sb_message_other_clicked_link_background_color, + R.color.primary_100 + ) + binding.tvMessageFormDisabled.setLinkTextColor(linkTextColor) + binding.tvMessageFormDisabled.clickedLinkBackgroundColor = ContextCompat.getColor(context, clickedLinkBackgroundColor) + } finally { + a.recycle() + } + } + + fun drawFormMessage(message: BaseMessage, messageListUIParams: MessageListUIParams) { + val form = message.forms.firstOrNull() ?: return + formFieldAdapter.setFormFields(form) + messageUIConfig?.let { + it.otherSentAtTextUIConfig.mergeFromTextAppearance(context, sentAtAppearance) + it.otherNicknameTextUIConfig.mergeFromTextAppearance(context, nicknameAppearance) + it.otherMessageBackground?.let { background -> binding.contentPanel.background = background } + it.otherEditedTextMarkUIConfig.mergeFromTextAppearance(context, editedAppearance) + it.otherMessageTextUIConfig.mergeFromTextAppearance(context, messageAppearance) + it.linkedTextColor?.let { linkedTextColor -> binding.tvMessageFormDisabled.setLinkTextColor(linkedTextColor) } + } + + if (messageListUIParams.channelConfig.enableFormTypeMessage) { + binding.formEnabledLayout.visibility = VISIBLE + binding.tvMessageFormDisabled.visibility = GONE + } else { + binding.formEnabledLayout.visibility = GONE + binding.tvMessageFormDisabled.visibility = VISIBLE + } + + ViewUtils.drawTextMessage( + binding.tvMessageFormDisabled, + message, + messageUIConfig, + false, + null, + null + ) + + if (form.answeredList == null) { + setSubmitButtonVisibility(View.VISIBLE) + } else { + setSubmitButtonVisibility(View.GONE) + } + ViewUtils.drawNickname(binding.tvNickname, message, messageUIConfig, false) + ViewUtils.drawProfile(binding.ivProfileView, message) + ViewUtils.drawSentAt(binding.tvSentAt, message, messageUIConfig) + } + + private fun setSubmitButtonVisibility(visibility: Int) { + if (visibility !in setOf(View.VISIBLE, View.GONE)) return + binding.buttonSubmit.visibility = visibility + } + + fun setSubmitButtonClickListener(listener: OnClickListener?) { + binding.buttonSubmit.setOnClickListener { view -> + val isReadyToSubmit = formFieldAdapter.isReadyToSubmit() + formFieldAdapter.updateValidation() + if (!isReadyToSubmit) { + return@setOnClickListener + } + listener?.onClick(view) + } + } + + private class ItemSpacingDecoration( + private val spacing: Int + ) : RecyclerView.ItemDecoration() { + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + val position = parent.getChildAdapterPosition(view) + val itemCount = state.itemCount + val isLastPosition = position == (itemCount - 1) + + with(outRect) { + left = 0 + top = 0 + right = 0 + bottom = if (isLastPosition) 0 else spacing + } + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedRepliesMessageView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedRepliesMessageView.kt new file mode 100644 index 00000000..4a687bbd --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedRepliesMessageView.kt @@ -0,0 +1,67 @@ +package com.sendbird.uikit.internal.ui.messages + +import android.content.Context +import android.graphics.Rect +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.sendbird.uikit.R +import com.sendbird.uikit.activities.adapter.SuggestedRepliesAdapter +import com.sendbird.uikit.databinding.SbViewSuggestedRepliesMessageComponentBinding +import com.sendbird.uikit.interfaces.OnItemClickListener +import com.sendbird.uikit.internal.extensions.suggestedReplies +import com.sendbird.uikit.model.SuggestedRepliesMessage + +internal class SuggestedRepliesMessageView @JvmOverloads internal constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : BaseMessageView(context, attrs, defStyle) { + var onItemClickListener: OnItemClickListener? = null + private val suggestedRepliesAdapter = SuggestedRepliesAdapter() + override val binding: SbViewSuggestedRepliesMessageComponentBinding = SbViewSuggestedRepliesMessageComponentBinding.inflate( + LayoutInflater.from(getContext()), + this, + true + ) + + override val layout: View + get() = binding.root + + init { + binding.rvSuggestedReplies.layoutManager = LinearLayoutManager(context) + val spacing = resources.getDimensionPixelSize(R.dimen.sb_size_8) + binding.rvSuggestedReplies.addItemDecoration(LinearLayoutManagerItemDecoration(spacing)) + binding.rvSuggestedReplies.adapter = suggestedRepliesAdapter + suggestedRepliesAdapter.onItemClickListener = OnItemClickListener { v, position, data -> + onItemClickListener?.onItemClick(v, position, data) + } + } + + fun drawSuggestedReplies(message: SuggestedRepliesMessage) { + suggestedRepliesAdapter.suggestedReplies = message.anchor.suggestedReplies + } + + private class LinearLayoutManagerItemDecoration( + private val spacing: Int + ) : RecyclerView.ItemDecoration() { + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + super.getItemOffsets(outRect, view, parent, state) + if (parent.layoutManager is LinearLayoutManager) { + val layoutManager = parent.layoutManager as LinearLayoutManager + if (layoutManager.orientation == LinearLayoutManager.VERTICAL) { + outRect.bottom = spacing + } else { + outRect.right = spacing + } + } + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedReplyView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedReplyView.kt new file mode 100644 index 00000000..6ccd1082 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedReplyView.kt @@ -0,0 +1,45 @@ +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.uikit.R +import com.sendbird.uikit.SendbirdUIKit +import com.sendbird.uikit.databinding.SbViewSuggestedReplyComponentBinding +import com.sendbird.uikit.internal.extensions.setAppearance + +internal class SuggestedReplyView @JvmOverloads internal constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : BaseMessageView(context, attrs, defStyle) { + override val binding: SbViewSuggestedReplyComponentBinding = SbViewSuggestedReplyComponentBinding.inflate( + LayoutInflater.from(getContext()), + this, + true + ) + override val layout: View + get() = binding.root + + init { + val textAppearance = if (SendbirdUIKit.isDarkMode()) { + R.style.SendbirdBody3Primary200 + } else { + R.style.SendbirdBody3Primary300 + } + + val backgroundResourceId = if (SendbirdUIKit.isDarkMode()) { + R.drawable.sb_suggested_replies_button_dark + } else { + R.drawable.sb_suggested_replies_button_light + } + + binding.tvSuggestedReply.setAppearance(context, textAppearance) + binding.tvSuggestedReply.setBackgroundResource(backgroundResourceId) + } + + fun drawSuggestedReplies(suggestedReply: String) { + binding.tvSuggestedReply.text = suggestedReply + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/FormMessageViewHolder.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/FormMessageViewHolder.kt new file mode 100644 index 00000000..058f6179 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/FormMessageViewHolder.kt @@ -0,0 +1,29 @@ +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.android.message.UserMessage +import com.sendbird.uikit.activities.viewholder.MessageViewHolder +import com.sendbird.uikit.databinding.SbViewFormMessageBinding +import com.sendbird.uikit.internal.extensions.forms +import com.sendbird.uikit.internal.interfaces.OnSubmitButtonClickListener +import com.sendbird.uikit.model.MessageListUIParams + +internal class FormMessageViewHolder internal constructor( + val binding: SbViewFormMessageBinding, + messageListUIParams: MessageListUIParams +) : MessageViewHolder(binding.root, messageListUIParams) { + var onSubmitClickListener: OnSubmitButtonClickListener? = null + override fun bind(channel: BaseChannel, message: BaseMessage, messageListUIParams: MessageListUIParams) { + if (message !is UserMessage) return + val form = message.forms.firstOrNull() ?: return + binding.formsMessageView.messageUIConfig = messageUIConfig + binding.formsMessageView.drawFormMessage(message, messageListUIParams) + binding.formsMessageView.setSubmitButtonClickListener { + onSubmitClickListener?.onClicked(message, form) + } + } + + override fun getClickableViewMap(): Map = mapOf() +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/SuggestedRepliesViewHolder.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/SuggestedRepliesViewHolder.kt new file mode 100644 index 00000000..17c5616e --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/SuggestedRepliesViewHolder.kt @@ -0,0 +1,29 @@ +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.SbViewSuggestedRepliesMessageBinding +import com.sendbird.uikit.interfaces.OnItemClickListener +import com.sendbird.uikit.model.MessageListUIParams +import com.sendbird.uikit.model.SuggestedRepliesMessage + +internal class SuggestedRepliesViewHolder internal constructor( + val binding: SbViewSuggestedRepliesMessageBinding, + messageListUIParams: MessageListUIParams +) : MessageViewHolder(binding.root, messageListUIParams) { + var suggestedRepliesClickedListener: OnItemClickListener? = null + + override fun bind(channel: BaseChannel, message: BaseMessage, messageListUIParams: MessageListUIParams) { + binding.suggestedRepliesMessageView.messageUIConfig = messageUIConfig + if (message is SuggestedRepliesMessage) { + binding.suggestedRepliesMessageView.drawSuggestedReplies(message) + binding.suggestedRepliesMessageView.onItemClickListener = OnItemClickListener { view, position, data -> + suggestedRepliesClickedListener?.onItemClick(view, position, data) + } + } + } + + override fun getClickableViewMap(): Map = mapOf() +} diff --git a/uikit/src/main/java/com/sendbird/uikit/model/SuggestedRepliesMessage.kt b/uikit/src/main/java/com/sendbird/uikit/model/SuggestedRepliesMessage.kt new file mode 100644 index 00000000..15ed121e --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/model/SuggestedRepliesMessage.kt @@ -0,0 +1,11 @@ +package com.sendbird.uikit.model + +import com.sendbird.android.message.BaseMessage +import com.sendbird.android.message.CustomizableMessage + +internal class SuggestedRepliesMessage( + val anchor: BaseMessage +) : CustomizableMessage(anchor.channelUrl, anchor.messageId + anchor.createdAt, anchor.createdAt + 1) { + override val requestId: String + get() = anchor.requestId + createdAt +} 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 99635af3..222b2bcf 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 @@ -36,6 +36,10 @@ data class ChannelConfig internal constructor( private var _enableVoiceMessage: Boolean = false, @SerialName(KeySet.enable_multiple_files_message) private var _enableMultipleFilesMessage: Boolean = false, + @SerialName(KeySet.enable_suggested_replies) + private var _enableSuggestedReplies: Boolean = false, + @SerialName(KeySet.enable_form_type_message) + private var _enableFormTypeMessage: Boolean = false, @SerialName(KeySet.thread_reply_select_type) @Serializable(with = ThreadReplySelectTypeAsStringSerializer::class) private var _threadReplySelectType: ThreadReplySelectType = ThreadReplySelectType.THREAD, @@ -65,7 +69,11 @@ data class ChannelConfig internal constructor( @Transient private var threadReplySelectTypeMutable: ThreadReplySelectType? = null, @Transient - private var replyTypeMutable: ReplyType? = null + private var replyTypeMutable: ReplyType? = null, + @Transient + private var enableSuggestedRepliesMutable: Boolean? = null, + @Transient + private var enableFormTypeMessageMutable: Boolean? = null ) : Parcelable { companion object { /** @@ -286,6 +294,47 @@ data class ChannelConfig internal constructor( set(value) { replyTypeMutable = value } + var enableSuggestedReplies: Boolean + /** + * Returns a value that determines whether to use the suggested replies or not. + * true, if channel displays suggested replies in the message list. + * false, otherwise. + * + * @return true if the suggested replies is enabled, false otherwise + * @since 3.10.0 + */ + get() = enableSuggestedRepliesMutable ?: _enableSuggestedReplies + + /** + * Sets whether to use the suggested replies or not. + * + * @param value true if the suggested replies is enabled, false otherwise + * @since 3.10.0 + */ + set(value) { + enableSuggestedRepliesMutable = value + } + + var enableFormTypeMessage: Boolean + /** + * Returns a value that determines whether to use the form type message or not. + * true, if channel displays form type message in the message list if it contains form type message data. + * false, otherwise. + * + * @return true if the form type is enabled, false otherwise + * @since 3.10.0 + */ + get() = enableFormTypeMessageMutable ?: _enableFormTypeMessage + + /** + * Sets whether to use the form type message or not. + * + * @param value true if the form type message is enabled, false otherwise + * @since 3.10.0 + */ + set(value) { + enableFormTypeMessageMutable = value + } @JvmSynthetic internal fun merge(config: ChannelConfig): ChannelConfig { @@ -295,6 +344,8 @@ data class ChannelConfig internal constructor( this._enableReactions = config._enableReactions this._enableVoiceMessage = config._enableVoiceMessage this._enableMultipleFilesMessage = config._enableMultipleFilesMessage + this._enableSuggestedReplies = config._enableSuggestedReplies + this._enableFormTypeMessage = config._enableFormTypeMessage this._threadReplySelectType = config._threadReplySelectType this._replyType = config._replyType this.input.merge(config.input) @@ -312,6 +363,7 @@ data class ChannelConfig internal constructor( this.enableMultipleFilesMessageMutable = null this.threadReplySelectTypeMutable = null this.replyTypeMutable = null + this.enableSuggestedRepliesMutable = null this.input.clear() } 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 37875fc2..c9e9519a 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 @@ -158,6 +158,7 @@ public void setAdapter(@NonNull LA adapter) { if (this.adapter.getMentionClickListener() == null) { this.adapter.setMentionClickListener(this::onMessageMentionClicked); } + if (messageRecyclerView == null) return; messageRecyclerView.getRecyclerView().setAdapter(this.adapter); } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/MessageListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/MessageListComponent.java index 418395fc..2e2f78a6 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/MessageListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/MessageListComponent.java @@ -12,6 +12,9 @@ import com.sendbird.uikit.consts.StringSet; import com.sendbird.uikit.interfaces.OnItemClickListener; import com.sendbird.uikit.interfaces.OnItemLongClickListener; +import com.sendbird.uikit.internal.extensions.MessageListAdapterExtensionsKt; +import com.sendbird.uikit.internal.extensions.MessageListComponentExtensionsKt; +import com.sendbird.uikit.internal.interfaces.OnSubmitButtonClickListener; import com.sendbird.uikit.model.MessageListUIParams; import com.sendbird.uikit.providers.AdapterProviders; @@ -27,6 +30,8 @@ public class MessageListComponent extends BaseMessageListComponent quoteReplyMessageLongClickListener; @Nullable private OnItemClickListener threadInfoClickListener; + @Nullable + private OnItemClickListener suggestedRepliesClickListener; /** * Constructor @@ -37,6 +42,23 @@ public MessageListComponent() { super(new Params(), true, true); } + @Override + public void setAdapter(@NonNull MessageListAdapter adapter) { + super.setAdapter(adapter); + if (adapter.getSuggestedRepliesClickListener() == null) { + adapter.setSuggestedRepliesClickListener(this::onSuggestedRepliesClicked); + } + + if (MessageListAdapterExtensionsKt.getSubmitButtonClickListener(adapter) == null) { + MessageListAdapterExtensionsKt.setSubmitButtonClickListener(adapter, (message, form) -> { + OnSubmitButtonClickListener listener = MessageListComponentExtensionsKt.getSubmitButtonClickListener(this); + if (listener != null) { + listener.onClicked(message, form); + } + }); + } + } + /** * Returns a collection of parameters applied to this component. * @@ -104,6 +126,20 @@ protected void onListItemLongClicked(@NonNull View view, @NonNull String identif } } + /** + * Called when the suggested replies button is clicked. + * + * @param view The clicked view. + * @param position The position of clicked view. + * @param suggestedReply The content of clicked view. + * since 3.10.0 + */ + protected void onSuggestedRepliesClicked(@NonNull View view, int position, @NonNull String suggestedReply) { + if (suggestedRepliesClickListener != null) { + suggestedRepliesClickListener.onItemClick(view, position, suggestedReply); + } + } + /** * Register a callback to be invoked when the quoted message is clicked. * @@ -171,6 +207,16 @@ protected void onThreadInfoClicked(@NonNull View view, int position, @NonNull Ba threadInfoClickListener.onItemClick(view, position, message); } + /** + * Register a callback to be invoked when the suggested replies button is clicked. + * + * @param suggestedRepliesClickListener The callback to be registered + * since 3.10.0 + */ + public void setSuggestedRepliesClickListener(@Nullable OnItemClickListener suggestedRepliesClickListener) { + this.suggestedRepliesClickListener = suggestedRepliesClickListener; + } + /** * 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/BaseMessageListViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/BaseMessageListViewModel.java index 7aec5dc3..4ddc516f 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/BaseMessageListViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/BaseMessageListViewModel.java @@ -28,6 +28,7 @@ import com.sendbird.uikit.interfaces.AuthenticateHandler; import com.sendbird.uikit.interfaces.OnCompleteHandler; import com.sendbird.uikit.interfaces.OnPagedDataLoader; +import com.sendbird.uikit.internal.extensions.MessageExtensionsKt; import com.sendbird.uikit.internal.wrappers.SendbirdUIKitImpl; import com.sendbird.uikit.internal.wrappers.SendbirdUIKitWrapper; import com.sendbird.uikit.log.Logger; @@ -355,6 +356,10 @@ void onMessagesUpdated(@NonNull MessageContext context, @NonNull GroupChannel gr PendingMessageRepository.getInstance().clearAllFileInfo(messages); cachedMessages.addAll(messages); } else { + for (BaseMessage message : messages) { + MessageExtensionsKt.getFormMap().remove(message.getMessageId()); + } + cachedMessages.updateAll(messages); } notifyDataSetChanged(context); @@ -372,6 +377,9 @@ void onMessagesDeleted(@NonNull MessageContext context, @NonNull GroupChannel gr if (context.getMessagesSendingStatus() == SendingStatus.SUCCEEDED) { // Remove the succeeded message from the succeeded message datasource. + for (BaseMessage message : messages) { + MessageExtensionsKt.getFormMap().remove(message.getMessageId()); + } cachedMessages.deleteAll(messages); notifyDataSetChanged(context); } else if (context.getMessagesSendingStatus() == SendingStatus.PENDING) { 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 3702bf8f..a4248d64 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java @@ -31,6 +31,7 @@ import com.sendbird.uikit.consts.ReplyType; import com.sendbird.uikit.consts.StringSet; import com.sendbird.uikit.interfaces.OnCompleteHandler; +import com.sendbird.uikit.internal.extensions.MessageExtensionsKt; import com.sendbird.uikit.internal.wrappers.MessageCollectionImpl; import com.sendbird.uikit.internal.wrappers.MessageCollectionWrapper; import com.sendbird.uikit.internal.wrappers.SendbirdChatImpl; @@ -38,6 +39,7 @@ import com.sendbird.uikit.internal.wrappers.SendbirdUIKitImpl; import com.sendbird.uikit.internal.wrappers.SendbirdUIKitWrapper; import com.sendbird.uikit.log.Logger; +import com.sendbird.uikit.model.SuggestedRepliesMessage; import com.sendbird.uikit.model.configurations.ChannelConfig; import com.sendbird.uikit.model.configurations.UIKitConfig; import com.sendbird.uikit.utils.Available; @@ -519,6 +521,11 @@ synchronized void notifyDataSetChanged(@NonNull String traceName) { if (!hasNext()) { copiedList.addAll(0, pendingMessages); copiedList.addAll(0, failedMessages); + + SuggestedRepliesMessage suggestedRepliesMessage = createSuggestedRepliesMessage(); + if (suggestedRepliesMessage != null) { + copiedList.add(0, suggestedRepliesMessage); + } } if (copiedList.size() == 0) { @@ -539,6 +546,28 @@ private void removeThreadMessages(@NonNull List src) { } } + @Nullable + private SuggestedRepliesMessage createSuggestedRepliesMessage() { + if (!channelConfig.getEnableSuggestedReplies()) return null; + if (hasNext()) return null; + + if (collection != null) { + List pendingMessages = collection.getPendingMessages(); + List failedMessages = collection.getFailedMessages(); + if (!pendingMessages.isEmpty() || !failedMessages.isEmpty()) return null; + } + + GroupChannel groupChannel = channel; + if (groupChannel != null) { + BaseMessage lastMessage = groupChannel.getLastMessage(); + if (lastMessage != null && !MessageExtensionsKt.getSuggestedReplies(lastMessage).isEmpty()) { + return new SuggestedRepliesMessage(lastMessage); + } + } + + return null; + } + private void markAsRead() { Logger.dev("markAsRead"); if (channel != null) channel.markAsRead(null); diff --git a/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_dark.xml b/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_dark.xml new file mode 100644 index 00000000..6e0f1255 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_dark.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_light.xml b/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_light.xml new file mode 100644 index 00000000..3b1343e9 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_light.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_dark.xml b/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_dark.xml new file mode 100644 index 00000000..5f50f4f0 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_dark.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_light.xml b/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_light.xml new file mode 100644 index 00000000..3dac074a --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_light.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/uikit/src/main/res/drawable/sb_shape_round_rect_background_ondark_02.xml b/uikit/src/main/res/drawable/sb_shape_round_rect_background_ondark_02.xml new file mode 100644 index 00000000..7395563b --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_round_rect_background_ondark_02.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/uikit/src/main/res/drawable/sb_shape_round_rect_background_onlight_04.xml b/uikit/src/main/res/drawable/sb_shape_round_rect_background_onlight_04.xml new file mode 100644 index 00000000..d06636da --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_round_rect_background_onlight_04.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/uikit/src/main/res/drawable/sb_shape_submit_button_dark.xml b/uikit/src/main/res/drawable/sb_shape_submit_button_dark.xml new file mode 100644 index 00000000..e8472ec9 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_submit_button_dark.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/uikit/src/main/res/drawable/sb_shape_submit_button_light.xml b/uikit/src/main/res/drawable/sb_shape_submit_button_light.xml new file mode 100644 index 00000000..caeae5d8 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_shape_submit_button_light.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/uikit/src/main/res/drawable/sb_suggested_replies_button_dark.xml b/uikit/src/main/res/drawable/sb_suggested_replies_button_dark.xml new file mode 100644 index 00000000..4df28639 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_suggested_replies_button_dark.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/uikit/src/main/res/drawable/sb_suggested_replies_button_light.xml b/uikit/src/main/res/drawable/sb_suggested_replies_button_light.xml new file mode 100644 index 00000000..ee4b8c14 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_suggested_replies_button_light.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/uikit/src/main/res/layout/sb_view_form_field.xml b/uikit/src/main/res/layout/sb_view_form_field.xml new file mode 100644 index 00000000..cfd6cffb --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_form_field.xml @@ -0,0 +1,6 @@ + + diff --git a/uikit/src/main/res/layout/sb_view_form_field_component.xml b/uikit/src/main/res/layout/sb_view_form_field_component.xml new file mode 100644 index 00000000..e5ef9713 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_form_field_component.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/uikit/src/main/res/layout/sb_view_form_message.xml b/uikit/src/main/res/layout/sb_view_form_message.xml new file mode 100644 index 00000000..edce1d85 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_form_message.xml @@ -0,0 +1,6 @@ + + diff --git a/uikit/src/main/res/layout/sb_view_form_message_component.xml b/uikit/src/main/res/layout/sb_view_form_message_component.xml new file mode 100644 index 00000000..f61d2cb6 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_form_message_component.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/uikit/src/main/res/layout/sb_view_suggested_replies_message.xml b/uikit/src/main/res/layout/sb_view_suggested_replies_message.xml new file mode 100644 index 00000000..68aa23ca --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_suggested_replies_message.xml @@ -0,0 +1,6 @@ + + diff --git a/uikit/src/main/res/layout/sb_view_suggested_replies_message_component.xml b/uikit/src/main/res/layout/sb_view_suggested_replies_message_component.xml new file mode 100644 index 00000000..9c38ce02 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_suggested_replies_message_component.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/uikit/src/main/res/layout/sb_view_suggested_reply.xml b/uikit/src/main/res/layout/sb_view_suggested_reply.xml new file mode 100644 index 00000000..b10e2cce --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_suggested_reply.xml @@ -0,0 +1,6 @@ + + diff --git a/uikit/src/main/res/layout/sb_view_suggested_reply_component.xml b/uikit/src/main/res/layout/sb_view_suggested_reply_component.xml new file mode 100644 index 00000000..489922c3 --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_suggested_reply_component.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/uikit/src/main/res/values/dimens.xml b/uikit/src/main/res/values/dimens.xml index 94c8e0af..8ecddd54 100644 --- a/uikit/src/main/res/values/dimens.xml +++ b/uikit/src/main/res/values/dimens.xml @@ -175,4 +175,5 @@ 296dp 280dp 156dp + 244dp diff --git a/uikit/src/main/res/values/strings.xml b/uikit/src/main/res/values/strings.xml index dc938219..77b34cd9 100644 --- a/uikit/src/main/res/values/strings.xml +++ b/uikit/src/main/res/values/strings.xml @@ -215,4 +215,10 @@ Voice message + + + Submit + Please check the value + Submit failed + (optional)