diff --git a/CHANGELOG.md b/CHANGELOG.md
index 202b2d99..f99c8675 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,10 @@
# Changelog
+### v3.21.0 (Sep 12, 2024) with Chat SDK `v4.19.0`
+* Changed the Form type message UI rendering due to the modification of the Form model from BaseMessage to MessageForm.
+* Sendbird Business Messaging changes
+ * Changed behavior not to send viewed stats in case the message is fallback message.
+ * Fixed not collecting viewed stats when the category filter is changed.
+
### v3.20.1 (Aug 30, 2024) with Chat SDK `v4.18.0`
* Added support for EmojiCategory. You can now filter emojis for different messages when adding Reactions to a message.
* New Interfaces
@@ -12,8 +18,7 @@
* Note: You need to set your custom EmojiCategory using [Sendbird Platform API](https://sendbird.com/docs/chat/platform-api/v3/message/reactions-and-emojis/reactions-and-emojis-overview) in advance.
* Fixed a crash in the new version due to new fields not having default value.
-### v3.20.0 (Aug 29, 2024) with Chat SDK `v4.18.0` *DEPRECATED*
-* **Deprecated as this version would cause `MissingFieldException` from `NotificationTemplate` due to adding a new field without a default value.**
+### v3.20.0 (Aug 29, 2024) with Chat SDK `v4.18.0`
* Added support for EmojiCategory. You can now filter emojis for different messages when adding Reactions to a message.
* New Interfaces
```kotlin
diff --git a/gradle.properties b/gradle.properties
index ee17e4c5..11e18dd7 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -22,5 +22,5 @@ android.nonTransitiveRClass=false
android.nonFinalResIds=false
android.enableR8.fullMode=false
-UIKIT_VERSION = 3.20.1
+UIKIT_VERSION = 3.21.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 fa93c5da..77c031a2 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
@@ -84,7 +84,7 @@ class BaseApplication : MultiDexApplication() {
*/
fun setupConfigurations() {
when (PreferenceUtils.selectedSampleType) {
- SampleType.Basic -> {
+ null, SampleType.Basic -> {
// set whether to use user profile
UIKitConfig.common.enableUsingDefaultUserProfile = true
// set whether to use typing indicators in channel list
diff --git a/uikit/build.gradle b/uikit/build.gradle
index 85be09a6..d648af07 100644
--- a/uikit/build.gradle
+++ b/uikit/build.gradle
@@ -70,7 +70,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Sendbird
- api 'com.sendbird.sdk:sendbird-chat:4.18.0'
+ api 'com.sendbird.sdk:sendbird-chat:4.19.0'
implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
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
deleted file mode 100644
index 3e7694a1..00000000
--- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/FormFieldAdapter.kt
+++ /dev/null
@@ -1,95 +0,0 @@
-package com.sendbird.uikit.activities.adapter
-
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.recyclerview.widget.DiffUtil
-import com.sendbird.android.message.Form
-import com.sendbird.android.message.FormField
-import com.sendbird.uikit.activities.viewholder.BaseViewHolder
-import com.sendbird.uikit.databinding.SbViewFormFieldBinding
-import com.sendbird.uikit.internal.extensions.lastValidation
-
-internal class FormFieldAdapter : BaseAdapter>() {
- private val formFields: MutableList = mutableListOf()
-
- fun isSubmittable(): Boolean {
- return formFields.all { it.isSubmittable }
- }
-
- fun updateValidation() {
- formFields.forEachIndexed { index, formField ->
- val lastValidation = formField.lastValidation
- val validation = formField.isSubmittable
- formField.lastValidation = validation
- if (lastValidation != validation) {
- notifyItemChanged(index)
- }
- }
- }
-
- fun setFormFields(form: Form) {
- val newFormFields = if (form.isSubmitted) {
- 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.key == newItem.key &&
- 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/FormItemAdapter.kt b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/FormItemAdapter.kt
new file mode 100644
index 00000000..329fcf32
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/FormItemAdapter.kt
@@ -0,0 +1,130 @@
+package com.sendbird.uikit.activities.adapter
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.recyclerview.widget.DiffUtil
+import androidx.recyclerview.widget.ListAdapter
+import androidx.recyclerview.widget.RecyclerView
+import com.sendbird.android.message.MessageForm
+import com.sendbird.android.message.MessageFormItem
+import com.sendbird.uikit.databinding.SbViewFormItemTextBinding
+import com.sendbird.uikit.databinding.SbViewFormItemChipBinding
+import com.sendbird.uikit.databinding.SbViewFormItemTextareaBinding
+import com.sendbird.uikit.internal.extensions.convertToViewType
+import com.sendbird.uikit.internal.extensions.isEqualTo
+import com.sendbird.uikit.internal.extensions.isSubmittable
+import com.sendbird.uikit.internal.extensions.shouldCheckValidation
+import com.sendbird.uikit.internal.interfaces.OnFormValidationChangedListener
+
+internal class FormItemAdapter(private val onValidationChangedListener: OnFormValidationChangedListener) : ListAdapter(diffUtil) {
+ private var messageForm: MessageForm? = null
+ private var validations: MutableList? = null
+
+ fun isSubmittable(): Boolean {
+ return currentList.all { messageFormItem ->
+ messageFormItem.isSubmittable
+ }
+ }
+
+ fun updateValidation() {
+ currentList.forEachIndexed { index, messageFormItem ->
+ val lastValidation = messageFormItem.shouldCheckValidation
+ val validation = messageFormItem.isSubmittable
+ messageFormItem.shouldCheckValidation = validation
+ if (lastValidation != validation) {
+ notifyItemChanged(index)
+ }
+ }
+ }
+
+ fun setMessageForm(form: MessageForm) {
+ messageForm = form
+ validations = MutableList(form.items.size) { true }
+ submitList(form.items)
+ }
+
+ private fun updateValidation(index: Int, isValid: Boolean) {
+ validations?.set(index, isValid)
+ onValidationChangedListener.onValidationChanged(validations?.all { it } == true)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MessageFormItemViewHolder {
+ return when (viewType) {
+ MessageFormViewType.TEXT.value -> FormItemTextViewHolder(
+ SbViewFormItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ )
+ MessageFormViewType.TEXTAREA.value -> FormItemTextareaViewHolder(
+ SbViewFormItemTextareaBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ )
+ MessageFormViewType.CHIP.value -> FormItemChipViewHolder(
+ SbViewFormItemChipBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ )
+ else -> FormItemTextViewHolder(
+ SbViewFormItemTextBinding.inflate(LayoutInflater.from(parent.context), parent, false)
+ )
+ }
+ }
+
+ override fun getItemViewType(position: Int): Int {
+ return getItem(position).style?.layout?.convertToViewType() ?: MessageFormViewType.TEXT.value
+ }
+
+ override fun onBindViewHolder(holder: MessageFormItemViewHolder, position: Int) {
+ holder.bind(getItem(position), messageForm?.isSubmitted == false) {
+ updateValidation(position, it)
+ }
+ }
+
+ private class FormItemTextViewHolder(
+ val binding: SbViewFormItemTextBinding
+ ) : MessageFormItemViewHolder(binding.root) {
+ override fun bind(item: MessageFormItem, isEnabled: Boolean, onValidationChangedListener: OnFormValidationChangedListener) {
+ binding.formItemView.onValidationListener = onValidationChangedListener
+ binding.formItemView.drawFormItem(item, isEnabled, item.shouldCheckValidation)
+ }
+ }
+
+ private class FormItemTextareaViewHolder(
+ val binding: SbViewFormItemTextareaBinding
+ ) : MessageFormItemViewHolder(binding.root) {
+ override fun bind(item: MessageFormItem, isEnabled: Boolean, onValidationChangedListener: OnFormValidationChangedListener) {
+ binding.formItemView.onValidationListener = onValidationChangedListener
+ binding.formItemView.drawFormItem(item, isEnabled, item.shouldCheckValidation)
+ }
+ }
+
+ private class FormItemChipViewHolder(
+ val binding: SbViewFormItemChipBinding
+ ) : MessageFormItemViewHolder(binding.root) {
+ override fun bind(item: MessageFormItem, isEnabled: Boolean, onValidationChangedListener: OnFormValidationChangedListener) {
+ binding.formItemView.onValidationListener = onValidationChangedListener
+ binding.formItemView.drawFormItem(item, isEnabled, item.shouldCheckValidation)
+ }
+ }
+
+ abstract class MessageFormItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ abstract fun bind(item: MessageFormItem, isEnabled: Boolean, onValidationChangedListener: OnFormValidationChangedListener)
+ }
+
+ companion object {
+ val diffUtil = object : DiffUtil.ItemCallback() {
+ override fun areItemsTheSame(oldItem: MessageFormItem, newItem: MessageFormItem): Boolean {
+ return oldItem.id == newItem.id
+ }
+
+ override fun areContentsTheSame(oldItem: MessageFormItem, newItem: MessageFormItem): Boolean {
+ return oldItem.draftValues isEqualTo newItem.draftValues &&
+ oldItem.submittedValues isEqualTo newItem.submittedValues &&
+ !(oldItem.required == false && newItem.required == false)
+ }
+ }
+ }
+}
+
+internal enum class MessageFormViewType(val value: Int) {
+ TEXT(0),
+ TEXTAREA(1),
+ CHIP(2),
+ UNKNOWN(3)
+}
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 1778d894..46be967a 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,7 +1,5 @@
package com.sendbird.uikit.activities.adapter;
-import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
-
import android.view.ViewGroup;
import androidx.annotation.NonNull;
@@ -19,8 +17,8 @@
import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener;
import com.sendbird.uikit.internal.ui.viewholders.FormMessageViewHolder;
import com.sendbird.uikit.internal.ui.viewholders.OtherTemplateMessageViewHolder;
-import com.sendbird.uikit.internal.utils.TemplateViewCachePool;
import com.sendbird.uikit.internal.ui.viewholders.OtherUserMessageViewHolder;
+import com.sendbird.uikit.internal.utils.TemplateViewCachePool;
import com.sendbird.uikit.model.MessageListUIParams;
/**
@@ -84,6 +82,12 @@ public MessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewT
finalListener.onFeedbackClicked(view, rating);
}
});
+ otherTemplateMessageViewHolder.setOnSuggestedRepliesClickListener((view, pos, data) -> {
+ final OnItemClickListener finalListener = this.suggestedRepliesClickListener;
+ if (finalListener != null) {
+ finalListener.onItemClick(view, pos, data);
+ }
+ });
} else if (viewHolder instanceof FormMessageViewHolder) {
FormMessageViewHolder formMessageViewHolder = (FormMessageViewHolder) viewHolder;
formMessageViewHolder.setOnSubmitClickListener((message, form) -> {
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 821190d0..c14ffdf7 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
@@ -264,8 +264,9 @@ public static int getViewType(@NonNull BaseMessage message) {
public static MessageType getMessageType(@NonNull BaseMessage message) {
MessageType type;
- MessageTemplateStatus messageTemplateStatus = MessageTemplateExtensionsKt.getMessageTemplateStatus(message);
- if (messageTemplateStatus != null) {
+ // NOT_APPLICABLE is possible when the message is a unknown version of template message or the message is not a template message.
+ final MessageTemplateStatus messageTemplateStatus = MessageTemplateExtensionsKt.getMessageTemplateStatus(message);
+ if (MessageTemplateExtensionsKt.isTemplateMessage(message) && messageTemplateStatus != null) {
switch (messageTemplateStatus) {
case CACHED:
case LOADING:
@@ -273,11 +274,11 @@ public static MessageType getMessageType(@NonNull BaseMessage message) {
case FAILED_TO_PARSE:
return MessageType.VIEW_TYPE_TEMPLATE_MESSAGE_OTHER;
case NOT_APPLICABLE:
- break;
+ return MessageType.VIEW_TYPE_UNKNOWN_MESSAGE_OTHER;
}
}
- if (message.getChannelType() == ChannelType.GROUP && !message.getForms().isEmpty()) {
+ if (message.getChannelType() == ChannelType.GROUP && message.getMessageForm() != null) {
return MessageType.VIEW_TYPE_FORM_TYPE_MESSAGE;
}
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 2693a053..575fe0d7 100644
--- a/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt
@@ -150,11 +150,10 @@ object StringSet {
const val delete = "delete"
// template message
- const val template = "template"
const val message_template_params = "message_template_params"
const val message_template_status = "message_template_status"
- const val container_type = "container_type"
const val ui = "ui"
+ const val default = "default"
// Config
const val none = "none"
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 23e2449f..9cd2221e 100644
--- a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java
+++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java
@@ -27,7 +27,7 @@
import com.sendbird.android.message.Feedback;
import com.sendbird.android.message.FeedbackRating;
import com.sendbird.android.message.FileMessage;
-import com.sendbird.android.message.Form;
+import com.sendbird.android.message.MessageForm;
import com.sendbird.android.message.SendingStatus;
import com.sendbird.android.params.MessageListParams;
import com.sendbird.android.params.UserMessageCreateParams;
@@ -579,8 +579,8 @@ protected void onSuggestedRepliesClicked(@NonNull String suggestedReply) {
* @param form The form to be submitted
* since 3.12.1
*/
- protected void onFormSubmitButtonClicked(@NonNull BaseMessage message, @NonNull Form form) {
- message.submitForm(form, (e) -> {
+ protected void onFormSubmitButtonClicked(@NonNull BaseMessage message, @NonNull MessageForm form) {
+ message.submitMessageForm((e) -> {
if (e != null) {
showConfirmDialog(getString(R.string.sb_forms_submit_failed));
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/interfaces/FormSubmitButtonClickListener.kt b/uikit/src/main/java/com/sendbird/uikit/interfaces/FormSubmitButtonClickListener.kt
index 9c9b0a70..4248a70c 100644
--- a/uikit/src/main/java/com/sendbird/uikit/interfaces/FormSubmitButtonClickListener.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/interfaces/FormSubmitButtonClickListener.kt
@@ -1,7 +1,7 @@
package com.sendbird.uikit.interfaces
import com.sendbird.android.message.BaseMessage
-import com.sendbird.android.message.Form
+import com.sendbird.android.message.MessageForm
/**
* Interface definition for a callback to be invoked when the submit button of the form is clicked.
@@ -16,5 +16,5 @@ fun interface FormSubmitButtonClickListener {
* @param form the form to be submitted
* @since 3.12.1
*/
- fun onClicked(message: BaseMessage, form: Form)
+ fun onClicked(message: BaseMessage, form: MessageForm)
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/adapter/CarouselChildViewAdapter.kt b/uikit/src/main/java/com/sendbird/uikit/internal/adapter/CarouselChildViewAdapter.kt
index 5cfdfd07..41cf94ae 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/adapter/CarouselChildViewAdapter.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/adapter/CarouselChildViewAdapter.kt
@@ -5,15 +5,12 @@ import android.view.ViewGroup
import android.widget.LinearLayout.LayoutParams
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
-import com.sendbird.uikit.R
-import com.sendbird.uikit.internal.extensions.intToDp
import com.sendbird.uikit.internal.model.template_messages.Params
import com.sendbird.uikit.internal.model.template_messages.SizeType
import com.sendbird.uikit.internal.model.template_messages.ViewLifecycleHandler
import com.sendbird.uikit.internal.ui.messages.MessageTemplateView
-import kotlin.math.max
-internal class CarouselChildViewAdapter : RecyclerView.Adapter() {
+internal class CarouselChildViewAdapter(private val maxChildWidth: Int) : RecyclerView.Adapter() {
private val childTemplateParams: MutableList = mutableListOf()
internal var onChildViewCreated: ViewLifecycleHandler? = null
@@ -47,11 +44,12 @@ internal class CarouselChildViewAdapter : RecyclerView.Adapter onChildViewCreated?.invoke(view, viewParams) }
)
}
- private val Params.maxChildFixedWidthSize: Int?
- get() {
- return this.body.items
- .filter { it.width.type == SizeType.Fixed }
- .takeIf { it.isNotEmpty() }
- ?.maxOf {
- it.width.value +
- (it.viewStyle.margin?.left ?: 0) +
- (it.viewStyle.margin?.right ?: 0)
- }
- }
private val Params.hasFillWidth: Boolean
get() {
return this.body.items.any { it.width.type == SizeType.Flex && it.width.value == ViewGroup.LayoutParams.MATCH_PARENT }
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/ChannelExtensions.kt b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/ChannelExtensions.kt
index 7b207789..531ed961 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/ChannelExtensions.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/ChannelExtensions.kt
@@ -1,11 +1,43 @@
package com.sendbird.uikit.internal.extensions
import com.sendbird.android.channel.GroupChannel
-import com.sendbird.uikit.consts.StringSet
+import com.sendbird.android.message.BaseMessage
+import com.sendbird.uikit.internal.ui.messages.MESSAGE_FORM_VERSION
import com.sendbird.uikit.model.configurations.ChannelConfig
internal fun GroupChannel.shouldDisableInput(channelConfig: ChannelConfig): Boolean {
- return channelConfig.enableSuggestedReplies && this.lastMessage?.extendedMessagePayload?.get(StringSet.disable_chat_input) == true.toString()
+ val disabledChatInputMessages = disabledChatInputMessagesMap[url]
+ if (!disabledChatInputMessages.isNullOrEmpty()) {
+ // Defensive code for handling cases where the 'Message after submission' is not sent by the server due to an error when the form is submitted.
+ if (channelConfig.enableFormTypeMessage) {
+ val messageForm = disabledChatInputMessages.find { it.messageForm != null }?.messageForm
+ if (messageForm != null && messageForm.version <= MESSAGE_FORM_VERSION && !messageForm.isSubmitted) {
+ return true
+ }
+ }
+
+ // Defensive code for handling cases where 'Suggested reply' is being used and the server responds with 'disable_chat_input', but 'Suggested replies' are empty due to an error.
+ if (channelConfig.enableSuggestedReplies) {
+ val suggestedReplies = disabledChatInputMessages.find { it.suggestedReplies.isNotEmpty() }?.suggestedReplies
+ if (!suggestedReplies.isNullOrEmpty()) {
+ return true
+ }
+ }
+ return false
+ }
+ return false
+}
+
+// DisabledChatInputMessage determines whether to block the chat input using `disabled_chat_input` when sending multiple consecutive messages in a workflow.
+// In the future, it will be updated to handle `disabled_chat_input` through a Channel Event.
+private val disabledChatInputMessagesMap: MutableMap> = mutableMapOf()
+
+internal fun GroupChannel.saveDisabledChatInputMessages(messages: List) {
+ disabledChatInputMessagesMap[url] = messages
+}
+
+internal fun GroupChannel.clearDisabledChatInputMessages() {
+ disabledChatInputMessagesMap.remove(url)
}
internal val GroupChannel.containsBot: Boolean
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 a0924dcb..59a70f90 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
@@ -6,12 +6,14 @@ import com.sendbird.android.message.BaseFileMessage
import com.sendbird.android.message.BaseMessage
import com.sendbird.android.message.Emoji
import com.sendbird.android.message.FileMessage
-import com.sendbird.android.message.FormField
+import com.sendbird.android.message.MessageFormItem
import com.sendbird.android.message.MultipleFilesMessage
import com.sendbird.android.shadow.com.google.gson.JsonParser
import com.sendbird.uikit.R
+import com.sendbird.uikit.activities.adapter.MessageFormViewType
import com.sendbird.uikit.consts.StringSet
import com.sendbird.uikit.internal.singleton.MessageDisplayDataManager
+import com.sendbird.uikit.model.MessageList
import com.sendbird.uikit.log.Logger
import com.sendbird.uikit.model.EmojiManager
import com.sendbird.uikit.model.UserMessageDisplayData
@@ -84,21 +86,31 @@ internal fun BaseFileMessage.getName(context: Context): String {
}
internal fun List.clearLastValidations() {
- this.flatMap { message -> message.forms }
- .flatMap { form -> form.formFields }
- .forEach { formField -> formField.lastValidation = null }
+ this.flatMap { message -> message.messageForm?.items ?: emptyList() }
+ .forEach { messageFormItem -> messageFormItem.shouldCheckValidation = null }
}
internal val lastValidations: MutableMap = mutableMapOf()
-internal var FormField.lastValidation: Boolean?
- get() = lastValidations[this.identifier]
+internal var MessageFormItem.shouldCheckValidation: Boolean?
+ get() = lastValidations["$id"]
set(value) {
if (value == null) {
- lastValidations.remove(this.identifier)
+ lastValidations.remove("$id")
} else {
- lastValidations[this.identifier] = value
+ lastValidations["$id"] = value
}
}
+internal val MessageFormItem.isSubmittable: Boolean
+ get() = (this.required == false && this.draftValues == null) || (!(this.draftValues.isNullOrEmpty()) && this.draftValues?.all { this.isValid(it) } == true)
+
+internal fun MessageFormItem.MessageFormLayout.convertToViewType(): Int {
+ return when (this) {
+ MessageFormItem.MessageFormLayout.TEXT -> MessageFormViewType.TEXT.value
+ MessageFormItem.MessageFormLayout.TEXTAREA -> MessageFormViewType.TEXTAREA.value
+ MessageFormItem.MessageFormLayout.CHIP -> MessageFormViewType.CHIP.value
+ else -> MessageFormViewType.UNKNOWN.value
+ }
+}
private val emojiCategoriesMap: MutableMap> = mutableMapOf()
internal var BaseMessage.emojiCategories: List?
@@ -132,9 +144,6 @@ internal fun updateMessageEmojiCategories(messageList: List, emojiC
}
}
-private val FormField.identifier: String
- get() = "${this.messageId}_${this.key}"
-
@OptIn(AIChatBotExperimental::class)
internal var BaseMessage.shouldShowSuggestedReplies: Boolean
get() = this.extras[StringSet.should_show_suggested_replies] as? Boolean ?: false
@@ -161,3 +170,11 @@ internal val BaseMessage.isStreamMessage: Boolean
false
}
}
+
+internal val BaseMessage.disableChatInput: Boolean
+ get() = extendedMessagePayload[StringSet.disable_chat_input] == true.toString()
+
+internal fun MessageList.activeDisableInputMessageList(order: MessageList.Order): List {
+ val copied = if (order == MessageList.Order.DESC) this.toList() else this.toList().asReversed()
+ return copied.takeWhile { it.disableChatInput }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageTemplateExtensions.kt b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageTemplateExtensions.kt
index 53e486e3..2aa799f4 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageTemplateExtensions.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageTemplateExtensions.kt
@@ -3,19 +3,25 @@ package com.sendbird.uikit.internal.extensions
import com.sendbird.android.annotation.AIChatBotExperimental
import com.sendbird.android.channel.TemplateMessageData
import com.sendbird.android.message.BaseMessage
-import com.sendbird.android.shadow.com.google.gson.JsonParser
import com.sendbird.uikit.consts.StringSet
import com.sendbird.uikit.internal.model.template_messages.Params
import com.sendbird.uikit.internal.model.templates.MessageTemplateStatus
import com.sendbird.uikit.internal.singleton.MessageTemplateManager
import com.sendbird.uikit.internal.singleton.MessageTemplateParser
-internal const val MAX_CHILD_COUNT = 10
-
internal fun BaseMessage.isTemplateMessage(): Boolean {
return this.templateMessageData != null
}
+/**
+ * Check if the message is a valid template message.
+ * if the message is null or the type is not "default", it returns false.
+ * @return `true` if the message is a valid template message, `false` otherwise.
+ */
+internal fun TemplateMessageData?.isValid(): Boolean {
+ return this != null && MessageTemplateContainerType.from(type) != MessageTemplateContainerType.UNKNOWN
+}
+
internal fun BaseMessage.saveParamsFromTemplate() {
val templateMessageData = this.templateMessageData ?: return
val key = templateMessageData.key
@@ -42,13 +48,11 @@ internal fun TemplateMessageData.childTemplateKeys(): List {
return viewVariables.values.flatten().map { it.key }.distinct()
}
-internal val BaseMessage.messageTemplateContainerType: MessageTemplateContainerType
- get() = try {
- val uiObj = this.extendedMessagePayload[StringSet.ui]
- val containerType = JsonParser.parseString(uiObj).asJsonObject.get(StringSet.container_type).asString
- MessageTemplateContainerType.create(containerType)
- } catch (_: Exception) {
- MessageTemplateContainerType.DEFAULT
+internal val contentDisplayed: MutableMap = mutableMapOf()
+internal var BaseMessage.isContentDisplayed: Boolean
+ get() = contentDisplayed[messageId] ?: false
+ set(value) {
+ contentDisplayed[messageId] = value
}
@OptIn(AIChatBotExperimental::class)
@@ -74,13 +78,13 @@ internal var BaseMessage.messageTemplateParams: Params?
}
internal enum class MessageTemplateContainerType {
- DEFAULT, WIDE, CAROUSEL;
+ UNKNOWN, DEFAULT;
companion object {
- fun create(value: String?): MessageTemplateContainerType {
- return when (value) {
- "wide" -> WIDE
- else -> DEFAULT
+ fun from(value: String): MessageTemplateContainerType {
+ return when (value.lowercase()) {
+ StringSet.default -> DEFAULT
+ else -> UNKNOWN
}
}
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/StringExtensions.kt b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/StringExtensions.kt
index 150a982e..26f7141f 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/StringExtensions.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/StringExtensions.kt
@@ -17,3 +17,7 @@ internal fun String?.toDisplayText(default: String): String {
internal fun String.upperFirstChar(): String {
return this.replaceFirstChar { char -> if (char.isLowerCase()) char.titlecase(Locale.getDefault()) else char.toString() }
}
+
+internal infix fun List?.isEqualTo(other: List?): Boolean {
+ return this == other
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/ViewExtensions.kt b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/ViewExtensions.kt
index 362b2772..8ca94e68 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/ViewExtensions.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/ViewExtensions.kt
@@ -12,6 +12,7 @@ import android.os.Build
import android.util.TypedValue
import android.view.Gravity
import android.view.View
+import android.view.ViewGroup
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.ImageView
@@ -156,13 +157,13 @@ internal fun FeedbackView.drawFeedback(message: BaseMessage, listener: OnFeedbac
}
internal fun Context.createTemplateMessageLoadingView(): View {
- val maxWidth = resources.getDimensionPixelSize(R.dimen.sb_message_max_width)
+ val height = resources.getDimensionPixelSize(R.dimen.sb_template_message_loading_view_height)
return FrameLayout(this).apply {
- layoutParams = FrameLayout.LayoutParams(maxWidth, maxWidth)
+ layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height)
setBackgroundColor(Color.TRANSPARENT)
addView(
ProgressBar(context).apply {
- val size = resources.intToDp(36)
+ val size = resources.intToDp(42)
layoutParams = FrameLayout.LayoutParams(
size, size, Gravity.CENTER
)
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnFormValidationChangedListener.kt b/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnFormValidationChangedListener.kt
new file mode 100644
index 00000000..eff6d49a
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnFormValidationChangedListener.kt
@@ -0,0 +1,5 @@
+package com.sendbird.uikit.internal.interfaces
+
+internal fun interface OnFormValidationChangedListener {
+ fun onValidationChanged(isValid: Boolean)
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Params.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Params.kt
index 96bb2c0b..493f4138 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Params.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Params.kt
@@ -239,7 +239,7 @@ internal data class CarouselViewParams(
override val height: SizeSpec = SizeSpec(SizeType.Flex, WRAP_CONTENT),
override val viewStyle: ViewStyle = ViewStyle(),
val items: List,
- val spacing: Int = 10
+ val carouselStyle: CarouselStyle = CarouselStyle()
) : ViewParams()
internal object TemplateParamsCreator {
@@ -385,6 +385,9 @@ internal object TemplateParamsCreator {
padding = Padding(
6, 6, 12, 12
),
+ margin = Margin(
+ 0, 0, 50, 0
+ ),
radius = 16
),
items = textList
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Styles.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Styles.kt
index e035ead3..7fa78e35 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Styles.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Styles.kt
@@ -77,6 +77,12 @@ internal data class ViewStyle(
}
}
+@Serializable
+internal data class CarouselStyle(
+ val spacing: Int = 10,
+ val maxChildWidth: Int = 240 // default value
+)
+
@Serializable
internal data class Margin(
val top: Int = 0,
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/templates/MessageTemplate.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/templates/MessageTemplate.kt
index 583d5f8a..5c2bd144 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/model/templates/MessageTemplate.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/templates/MessageTemplate.kt
@@ -1,7 +1,6 @@
package com.sendbird.uikit.internal.model.templates
import com.sendbird.android.channel.SimpleTemplateData
-import com.sendbird.uikit.internal.extensions.MAX_CHILD_COUNT
import com.sendbird.uikit.internal.model.notifications.CSVColor
import com.sendbird.uikit.internal.model.notifications.NotificationThemeMode
import com.sendbird.uikit.internal.model.serializer.JsonElementToStringSerializer
@@ -16,7 +15,7 @@ import org.json.JSONObject
// TODO : Bind with [NotificationTemplate] after api spec finalize
@Serializable
-internal data class MessageTemplate constructor(
+internal data class MessageTemplate(
@SerialName(KeySet.key)
val templateKey: String,
@SerialName(KeySet.created_at)
@@ -75,7 +74,6 @@ internal data class MessageTemplate constructor(
val variableDataList = viewVariables[variable] ?: return@replace matchResult.value
val jsonArray = JSONArray()
variableDataList.forEach { childTemplateData ->
- if (jsonArray.length() >= MAX_CHILD_COUNT) return@forEach
val template = MessageTemplateManager.getTemplate(childTemplateData.key) ?: return@replace matchResult.value
jsonArray.put(JSONObject(template.getTemplateSyntax(childTemplateData.variables)))
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateMapper.kt b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateMapper.kt
index bd1c1cb4..86e9e863 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateMapper.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateMapper.kt
@@ -3,6 +3,7 @@ package com.sendbird.uikit.internal.singleton
import com.sendbird.android.message.BaseMessage
import com.sendbird.uikit.internal.extensions.childTemplateKeys
import com.sendbird.uikit.internal.extensions.isTemplateMessage
+import com.sendbird.uikit.internal.extensions.isValid
import com.sendbird.uikit.internal.extensions.messageTemplateStatus
import com.sendbird.uikit.internal.model.templates.MessageTemplateStatus
import com.sendbird.uikit.log.Logger
@@ -51,7 +52,14 @@ internal class MessageTemplateMapper(
}
}
- cachedTemplateMessages.forEach { it.messageTemplateStatus = MessageTemplateStatus.CACHED }
+ cachedTemplateMessages.forEach {
+ it.messageTemplateStatus = if (it.templateMessageData.isValid()) {
+ MessageTemplateStatus.CACHED
+ } else {
+ Logger.i("This template message is not supported. key=${it.templateMessageData}")
+ MessageTemplateStatus.NOT_APPLICABLE
+ }
+ }
Logger.d("3. filter not cached template keys result >> template messages[${templateMessages.size}], cached[${cachedTemplateMessages.size}], not cached[${notCachedTemplateMessages.size}]")
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/BaseNotificationView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/BaseNotificationView.kt
index b9420e1e..4ca5fa58 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/BaseNotificationView.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/BaseNotificationView.kt
@@ -15,6 +15,7 @@ import com.sendbird.android.message.BaseMessage
import com.sendbird.uikit.R
import com.sendbird.uikit.interfaces.OnNotificationTemplateActionHandler
import com.sendbird.uikit.internal.extensions.intToDp
+import com.sendbird.uikit.internal.extensions.isContentDisplayed
import com.sendbird.uikit.internal.interfaces.GetTemplateResultHandler
import com.sendbird.uikit.internal.model.notifications.NotificationThemeMode
import com.sendbird.uikit.internal.model.template_messages.KeySet
@@ -66,10 +67,13 @@ internal abstract class BaseNotificationView @JvmOverloads internal constructor(
)
}
}
- )
+ ).also {
+ message.isContentDisplayed = true
+ }
}
} catch (e: Throwable) {
Logger.w("${e.printStackTrace()}")
+ message.isContentDisplayed = false
createFallbackNotification(message, themeMode, onNotificationTemplateActionHandler)
}
parentView.removeAllViews()
@@ -88,6 +92,7 @@ internal abstract class BaseNotificationView @JvmOverloads internal constructor(
throw IllegalArgumentException("this message must have template key.")
}
if (!NotificationChannelManager.hasTemplate(templateKey)) {
+ message.isContentDisplayed = false
val layout = createLoadingView(!message.isFeedChannel, themeMode)
parentView.addView(layout)
}
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
deleted file mode 100644
index 1353552c..00000000
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormFieldView.kt
+++ /dev/null
@@ -1,162 +0,0 @@
-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.view.LayoutInflater
-import android.view.View
-import androidx.core.content.ContextCompat
-import androidx.core.content.res.ResourcesCompat
-import com.sendbird.android.message.Answer
-import com.sendbird.android.message.FormField
-import com.sendbird.android.message.FormFieldInputType
-import com.sendbird.uikit.R
-import com.sendbird.uikit.SendbirdUIKit
-import com.sendbird.uikit.databinding.SbViewFormFieldComponentBinding
-import com.sendbird.uikit.internal.extensions.lastValidation
-import com.sendbird.uikit.internal.extensions.setAppearance
-
-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_text_low_emphasis else R.color.onlight_text_low_emphasis)
- )
-
- 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_main else R.color.secondary_light)
- )
-
- 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.inputType == FormFieldInputType.PASSWORD) {
- binding.etFormField.transformationMethod = PasswordTransformationMethod()
- } else {
- binding.etFormField.transformationMethod = null
- }
- binding.etFormField.setText(formField.temporaryAnswer?.value ?: "")
- 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.value
- }
- }
-
- 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()
- }
-
- formField.temporaryAnswer = Answer(formField.key, s.toString())
- }
- }
-}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemChipView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemChipView.kt
new file mode 100644
index 00000000..b2c1c3c9
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemChipView.kt
@@ -0,0 +1,129 @@
+package com.sendbird.uikit.internal.ui.messages
+
+import android.content.Context
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.style.TextAppearanceSpan
+import android.util.AttributeSet
+import android.view.ContextThemeWrapper
+import android.view.LayoutInflater
+import android.view.View
+import com.google.android.material.chip.Chip
+import com.google.android.material.chip.ChipGroup
+import com.sendbird.android.message.MessageFormItem
+import com.sendbird.uikit.R
+import com.sendbird.uikit.SendbirdUIKit
+import com.sendbird.uikit.databinding.SbViewFormItemChipComponentBinding
+import com.sendbird.uikit.internal.extensions.intToDp
+import com.sendbird.uikit.internal.extensions.shouldCheckValidation
+import com.sendbird.uikit.internal.extensions.setAppearance
+import com.sendbird.uikit.internal.interfaces.OnFormValidationChangedListener
+import com.sendbird.uikit.internal.ui.widgets.TextChip
+
+internal class FormItemChipView @JvmOverloads internal constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : BaseMessageView(context, attrs, defStyle) {
+ override val binding: SbViewFormItemChipComponentBinding = SbViewFormItemChipComponentBinding.inflate(
+ LayoutInflater.from(getContext()),
+ this,
+ true
+ )
+ override val layout: View
+ get() = binding.root
+
+ private val tvFormItemTitleOptionalAppearance = if (SendbirdUIKit.isDarkMode()) {
+ R.style.SendbirdCaption3OnDark03
+ } else {
+ R.style.SendbirdCaption3OnLight03
+ }
+
+ var onValidationListener: OnFormValidationChangedListener? = null
+
+ init {
+ val isDarkMode = SendbirdUIKit.isDarkMode()
+ binding.tvFormItemTitle.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdCaption3OnDark02
+ else R.style.SendbirdCaption3OnLight02
+ )
+
+ binding.tvFormItemError.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdCaption4Error200
+ else R.style.SendbirdCaption4Error300
+ )
+ }
+
+ fun drawFormItem(messageFormItem: MessageFormItem, isEnabled: Boolean, shouldCheckValidation: Boolean?) {
+ val name = if (messageFormItem.required == true) {
+ messageFormItem.name
+ } else {
+ val title = "${messageFormItem.name} ${context.getString(R.string.sb_forms_optional)}"
+ SpannableString(title).apply {
+ setSpan(
+ TextAppearanceSpan(context, tvFormItemTitleOptionalAppearance),
+ messageFormItem.name?.length ?: 0,
+ title.length,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+ }
+ binding.tvFormItemTitle.text = name
+
+ messageFormItem.style?.resultCount?.max?.let { max ->
+ binding.chipGroupFormItem.isSingleSelection = max <= 1
+ }
+
+ val selectedItems = mutableListOf()
+ if (!isEnabled) {
+ binding.chipGroupFormItem.setOnCheckedChangeListener(null)
+ selectedItems.addAll(messageFormItem.submittedValues ?: emptyList())
+ updateFormItemState(true)
+ } else {
+ binding.chipGroupFormItem.setOnCheckedChangeListener(FormItemOnCheckedStateChangeListener(messageFormItem))
+ selectedItems.addAll(messageFormItem.draftValues ?: messageFormItem.style?.defaultOptions ?: emptyList())
+ if (selectedItems.isNotEmpty()) {
+ messageFormItem.draftValues = selectedItems
+ }
+ updateFormItemState(shouldCheckValidation ?: true)
+ }
+
+ binding.chipGroupFormItem.removeAllViews()
+ messageFormItem.style?.options?.forEach {
+ binding.chipGroupFormItem.addView(createChip(it, selectedItems, !isEnabled))
+ }
+ }
+
+ private fun createChip(chipText: String, selectedItems: List, isSubmitted: Boolean): Chip {
+ val isSelected = selectedItems.contains(chipText)
+ return TextChip(ContextThemeWrapper(context, R.style.Theme_MaterialComponents)).apply {
+ text = chipText
+ minHeight = resources.intToDp(32)
+ isChipEnabled = !isSubmitted
+ isChipSelected = isSelected
+ }
+ }
+
+ private fun updateFormItemState(isValid: Boolean) {
+ onValidationListener?.onValidationChanged(isValid)
+ if (!isValid) {
+ binding.tvFormItemError.visibility = VISIBLE
+ } else {
+ binding.tvFormItemError.visibility = GONE
+ }
+ }
+
+ private inner class FormItemOnCheckedStateChangeListener(
+ private val formItem: MessageFormItem
+ ) : ChipGroup.OnCheckedChangeListener {
+ override fun onCheckedChanged(chipGroup: ChipGroup?, id: Int) {
+ val checkedIds = chipGroup?.checkedChipIds
+ formItem.draftValues = checkedIds?.map { "${findViewById(it).text}" } ?: emptyList()
+ val isValid = formItem.draftValues?.all { formItem.isValid(it) } ?: true
+ formItem.shouldCheckValidation = isValid
+ updateFormItemState(isValid)
+ }
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemTextAreaView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemTextAreaView.kt
new file mode 100644
index 00000000..c3114059
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemTextAreaView.kt
@@ -0,0 +1,214 @@
+package com.sendbird.uikit.internal.ui.messages
+
+import android.content.Context
+import android.text.Editable
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.TextWatcher
+import android.text.style.TextAppearanceSpan
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import com.sendbird.android.message.MessageFormItem
+import com.sendbird.uikit.R
+import com.sendbird.uikit.SendbirdUIKit
+import com.sendbird.uikit.databinding.SbViewFormItemTextareaComponentBinding
+import com.sendbird.uikit.internal.extensions.shouldCheckValidation
+import com.sendbird.uikit.internal.extensions.setAppearance
+import com.sendbird.uikit.internal.interfaces.OnFormValidationChangedListener
+import java.util.concurrent.atomic.AtomicBoolean
+
+internal class FormItemTextAreaView @JvmOverloads internal constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : BaseMessageView(context, attrs, defStyle) {
+ override val binding: SbViewFormItemTextareaComponentBinding = SbViewFormItemTextareaComponentBinding.inflate(
+ LayoutInflater.from(getContext()),
+ this,
+ true
+ )
+ override val layout: View
+ get() = binding.root
+
+ private val etFormItemBackground = if (SendbirdUIKit.isDarkMode()) {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_normal_dark, null)
+ } else {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_normal_light, null)
+ }
+
+ private val etFormItemFocusedBackground = if (SendbirdUIKit.isDarkMode()) {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_focused_dark, null)
+ } else {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_focused_light, null)
+ }
+
+ private val etFormItemBackgroundError = if (SendbirdUIKit.isDarkMode()) {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_invalid_dark, null)
+ } else {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_invalid_light, null)
+ }
+
+ private val tvFormItemTitleOptionalAppearance = if (SendbirdUIKit.isDarkMode()) {
+ R.style.SendbirdCaption3OnDark03
+ } else {
+ R.style.SendbirdCaption3OnLight03
+ }
+
+ private var textWatcher: FormItemTextWatcher? = null
+ private var isValidationChecked: AtomicBoolean = AtomicBoolean(false)
+ private var messageFormItem: MessageFormItem? = null
+
+ var onValidationListener: OnFormValidationChangedListener? = null
+
+ init {
+ val isDarkMode = SendbirdUIKit.isDarkMode()
+ binding.tvFormItemTitle.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdCaption3OnDark02
+ else R.style.SendbirdCaption3OnLight02
+ )
+
+ binding.etFormItem.background = etFormItemBackground
+
+ binding.etFormItem.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdBody3OnDark01
+ else R.style.SendbirdBody3OnLight01
+ )
+
+ binding.etFormItem.setHintTextColor(
+ ContextCompat.getColor(context, if (isDarkMode) R.color.ondark_text_low_emphasis else R.color.onlight_text_low_emphasis)
+ )
+
+ binding.tvFormItemError.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_main else R.color.secondary_light)
+ )
+
+ binding.etAnswer.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdBody3OnDark01
+ else R.style.SendbirdBody3OnLight01
+ )
+
+ binding.etAnswer.setHintTextColor(
+ ContextCompat.getColor(context, if (isDarkMode) R.color.ondark_text_low_emphasis else R.color.onlight_text_low_emphasis)
+ )
+
+ binding.etAnswer.background = null
+ }
+
+ fun drawFormItem(messageFormItem: MessageFormItem, isEnabled: Boolean, shouldCheckValidation: Boolean?) {
+ this.messageFormItem = messageFormItem
+ textWatcher?.let { binding.etFormItem.removeTextChangedListener(it) }
+ binding.etFormItem.onFocusChangeListener = null
+ val name = if (messageFormItem.required == true) {
+ messageFormItem.name
+ } else {
+ val title = "${messageFormItem.name} ${context.getString(R.string.sb_forms_optional)}"
+ SpannableString(title).apply {
+ setSpan(
+ TextAppearanceSpan(context, tvFormItemTitleOptionalAppearance),
+ messageFormItem.name?.length ?: 0,
+ title.length,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+ }
+ binding.tvFormItemTitle.text = name
+
+ if (!isEnabled) {
+ binding.unansweredLayout.visibility = GONE
+ binding.answeredLayout.visibility = VISIBLE
+ val hasResponse = !(messageFormItem.submittedValues.isNullOrEmpty())
+ if (hasResponse) {
+ binding.etAnswer.setText(messageFormItem.submittedValues?.firstOrNull() ?: "")
+ } else {
+ binding.etAnswer.hint = context.getString(R.string.sb_forms_empty_response)
+ }
+ updateFormItemState(true)
+ } else {
+ binding.unansweredLayout.visibility = VISIBLE
+ binding.answeredLayout.visibility = GONE
+ binding.etFormItem.transformationMethod = null
+ binding.etFormItem.setText(messageFormItem.draftValues?.firstOrNull() ?: "")
+ textWatcher = FormItemTextWatcher(messageFormItem).also {
+ binding.etFormItem.addTextChangedListener(it)
+ }
+ binding.etFormItem.onFocusChangeListener = FormItemFocusChangeListener(messageFormItem)
+ messageFormItem.placeholder?.let { binding.etFormItem.hint = it }
+ updateFormItemState(shouldCheckValidation ?: true)
+ }
+ }
+
+ private fun updateFormItemState(isValid: Boolean) {
+ onValidationListener?.onValidationChanged(isValid)
+ if (!isValid) {
+ isValidationChecked.set(true)
+ binding.etFormItem.background = etFormItemBackgroundError
+ val hasResponse = binding.etFormItem.text.toString().isNotEmpty()
+ if (messageFormItem?.required == true && !hasResponse) {
+ binding.tvFormItemError.text = context.getString(R.string.sb_forms_required_form_item)
+ } else {
+ binding.tvFormItemError.text = context.getString(R.string.sb_forms_invalid_form_item)
+ }
+ binding.tvFormItemError.visibility = VISIBLE
+ } else {
+ binding.etFormItem.background = if (binding.etFormItem.hasFocus()) etFormItemFocusedBackground else etFormItemBackground
+ binding.tvFormItemError.visibility = GONE
+ }
+ }
+
+ internal fun setDraftValues(inputValue: String, isValidationChecked: Boolean, formItem: MessageFormItem) {
+ if (inputValue.isEmpty()) {
+ formItem.draftValues = null
+ formItem.shouldCheckValidation = null
+ updateFormItemState(true)
+ return
+ }
+ if (isValidationChecked) {
+ val isValid = formItem.isValid(inputValue)
+ formItem.shouldCheckValidation = isValid
+ updateFormItemState(isValid)
+ }
+ formItem.draftValues = listOf(inputValue)
+ }
+
+ private inner class FormItemTextWatcher(
+ private val formItem: MessageFormItem
+ ) : 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) {
+ setDraftValues(s.toString(), isValidationChecked.get(), formItem)
+ }
+ }
+
+ private inner class FormItemFocusChangeListener(
+ private val formItem: MessageFormItem
+ ) : OnFocusChangeListener {
+ override fun onFocusChange(v: View?, hasFocus: Boolean) {
+ if (hasFocus) {
+ v?.background = etFormItemFocusedBackground
+ } else {
+ isValidationChecked.set(true)
+ val text = binding.etFormItem.text.toString()
+ setDraftValues(text, true, formItem)
+ }
+ }
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemTextView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemTextView.kt
new file mode 100644
index 00000000..d15c2bb5
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/FormItemTextView.kt
@@ -0,0 +1,215 @@
+package com.sendbird.uikit.internal.ui.messages
+
+import android.content.Context
+import android.text.Editable
+import android.text.SpannableString
+import android.text.Spanned
+import android.text.TextWatcher
+import android.text.style.TextAppearanceSpan
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import com.sendbird.android.message.MessageFormItem
+import com.sendbird.uikit.R
+import com.sendbird.uikit.SendbirdUIKit
+import com.sendbird.uikit.databinding.SbViewFormItemTextComponentBinding
+import com.sendbird.uikit.internal.extensions.shouldCheckValidation
+import com.sendbird.uikit.internal.extensions.setAppearance
+import com.sendbird.uikit.internal.interfaces.OnFormValidationChangedListener
+import java.util.concurrent.atomic.AtomicBoolean
+
+internal class FormItemTextView @JvmOverloads internal constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : BaseMessageView(context, attrs, defStyle) {
+ override val binding: SbViewFormItemTextComponentBinding = SbViewFormItemTextComponentBinding.inflate(
+ LayoutInflater.from(getContext()),
+ this,
+ true
+ )
+ override val layout: View
+ get() = binding.root
+
+ private val etFormItemBackground = if (SendbirdUIKit.isDarkMode()) {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_normal_dark, null)
+ } else {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_normal_light, null)
+ }
+
+ private val etFormItemFocusedBackground = if (SendbirdUIKit.isDarkMode()) {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_focused_dark, null)
+ } else {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_focused_light, null)
+ }
+
+ private val etFormItemBackgroundError = if (SendbirdUIKit.isDarkMode()) {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_invalid_dark, null)
+ } else {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_edit_text_form_item_invalid_light, null)
+ }
+
+ private val tvFormItemTitleOptionalAppearance = if (SendbirdUIKit.isDarkMode()) {
+ R.style.SendbirdCaption3OnDark03
+ } else {
+ R.style.SendbirdCaption3OnLight03
+ }
+
+ private var textWatcher: FormItemTextWatcher? = null
+ private var isValidationChecked: AtomicBoolean = AtomicBoolean(false)
+ private var messageFormItem: MessageFormItem? = null
+
+ var onValidationListener: OnFormValidationChangedListener? = null
+
+ init {
+ val isDarkMode = SendbirdUIKit.isDarkMode()
+ binding.tvFormItemTitle.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdCaption3OnDark02
+ else R.style.SendbirdCaption3OnLight02
+ )
+
+ binding.etFormItem.background = etFormItemBackground
+
+ binding.etFormItem.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdBody3OnDark01
+ else R.style.SendbirdBody3OnLight01
+ )
+
+ binding.etFormItem.setHintTextColor(
+ ContextCompat.getColor(context, if (isDarkMode) R.color.ondark_text_low_emphasis else R.color.onlight_text_low_emphasis)
+ )
+
+ binding.tvFormItemError.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_main else R.color.secondary_light)
+ )
+
+ binding.etAnswer.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdBody3OnDark01
+ else R.style.SendbirdBody3OnLight01
+ )
+
+ binding.etAnswer.setHintTextColor(
+ ContextCompat.getColor(context, if (isDarkMode) R.color.ondark_text_low_emphasis else R.color.onlight_text_low_emphasis)
+ )
+
+ binding.etAnswer.background = null
+ }
+
+ fun drawFormItem(messageFormItem: MessageFormItem, isEnabled: Boolean, shouldCheckValidation: Boolean?) {
+ this.messageFormItem = messageFormItem
+ textWatcher?.let { binding.etFormItem.removeTextChangedListener(it) }
+ textWatcher = null
+ binding.etFormItem.onFocusChangeListener = null
+ val name = if (messageFormItem.required == true) {
+ messageFormItem.name
+ } else {
+ val title = "${messageFormItem.name} ${context.getString(R.string.sb_forms_optional)}"
+ SpannableString(title).apply {
+ setSpan(
+ TextAppearanceSpan(context, tvFormItemTitleOptionalAppearance),
+ messageFormItem.name?.length ?: 0,
+ title.length,
+ Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
+ )
+ }
+ }
+ binding.tvFormItemTitle.text = name
+
+ if (!isEnabled) {
+ binding.unansweredLayout.visibility = GONE
+ binding.answeredLayout.visibility = VISIBLE
+ val hasResponse = !(messageFormItem.submittedValues.isNullOrEmpty())
+ if (hasResponse) {
+ binding.etAnswer.setText(messageFormItem.submittedValues?.firstOrNull() ?: "")
+ } else {
+ binding.etAnswer.hint = context.getString(R.string.sb_forms_empty_response)
+ }
+ updateFormItemState(true)
+ } else {
+ binding.unansweredLayout.visibility = VISIBLE
+ binding.answeredLayout.visibility = GONE
+ binding.etFormItem.transformationMethod = null
+ binding.etFormItem.setText(messageFormItem.draftValues?.firstOrNull() ?: "")
+ textWatcher = FormItemTextWatcher(messageFormItem).also {
+ binding.etFormItem.addTextChangedListener(it)
+ }
+ binding.etFormItem.onFocusChangeListener = FormItemFocusChangeListener(messageFormItem)
+ messageFormItem.placeholder?.let { binding.etFormItem.hint = it }
+ updateFormItemState(shouldCheckValidation ?: true)
+ }
+ }
+
+ private fun updateFormItemState(isValid: Boolean) {
+ onValidationListener?.onValidationChanged(isValid)
+ if (!isValid) {
+ isValidationChecked.set(true)
+ binding.etFormItem.background = etFormItemBackgroundError
+ val hasResponse = binding.etFormItem.text.toString().isNotEmpty()
+ if (messageFormItem?.required == true && !hasResponse) {
+ binding.tvFormItemError.text = context.getString(R.string.sb_forms_required_form_item)
+ } else {
+ binding.tvFormItemError.text = context.getString(R.string.sb_forms_invalid_form_item)
+ }
+ binding.tvFormItemError.visibility = VISIBLE
+ } else {
+ binding.etFormItem.background = if (binding.etFormItem.hasFocus()) etFormItemFocusedBackground else etFormItemBackground
+ binding.tvFormItemError.visibility = GONE
+ }
+ }
+
+ internal fun setDraftValues(inputValue: String, isValidationChecked: Boolean, formItem: MessageFormItem) {
+ if (inputValue.isEmpty()) {
+ formItem.draftValues = null
+ formItem.shouldCheckValidation = null
+ updateFormItemState(true)
+ return
+ }
+ if (isValidationChecked) {
+ val isValid = formItem.isValid(inputValue)
+ formItem.shouldCheckValidation = isValid
+ updateFormItemState(isValid)
+ }
+ formItem.draftValues = listOf(inputValue)
+ }
+
+ private inner class FormItemTextWatcher(
+ private val formItem: MessageFormItem
+ ) : 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) {
+ setDraftValues(s.toString(), isValidationChecked.get(), formItem)
+ }
+ }
+
+ private inner class FormItemFocusChangeListener(
+ private val formItem: MessageFormItem
+ ) : OnFocusChangeListener {
+ override fun onFocusChange(v: View?, hasFocus: Boolean) {
+ if (hasFocus) {
+ v?.background = etFormItemFocusedBackground
+ } else {
+ isValidationChecked.set(true)
+ val text = binding.etFormItem.text.toString()
+ setDraftValues(text, true, formItem)
+ }
+ }
+ }
+}
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
index 5b01af44..c4af776a 100644
--- 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
@@ -5,20 +5,25 @@ 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.android.message.MessageForm
+import com.sendbird.android.message.SendingStatus
import com.sendbird.uikit.R
import com.sendbird.uikit.SendbirdUIKit
-import com.sendbird.uikit.activities.adapter.FormFieldAdapter
+import com.sendbird.uikit.activities.adapter.FormItemAdapter
+import com.sendbird.uikit.consts.MessageGroupType
import com.sendbird.uikit.databinding.SbViewFormMessageComponentBinding
import com.sendbird.uikit.internal.extensions.setAppearance
import com.sendbird.uikit.model.MessageListUIParams
import com.sendbird.uikit.utils.DrawableUtils
+import com.sendbird.uikit.utils.MessageUtils
import com.sendbird.uikit.utils.ViewUtils
+internal const val MESSAGE_FORM_VERSION = 1
+
internal class FormMessageView @JvmOverloads internal constructor(
context: Context,
attrs: AttributeSet? = null,
@@ -29,7 +34,10 @@ internal class FormMessageView @JvmOverloads internal constructor(
private val sentAtAppearance: Int
private val nicknameAppearance: Int
private val messageAppearance: Int
- private val formFieldAdapter: FormFieldAdapter = FormFieldAdapter()
+ private val formItemAdapter: FormItemAdapter = FormItemAdapter {
+ setSubmitButtonEnabled(if (messageForm?.isSubmitted == true) false else it)
+ }
+ private var messageForm: MessageForm? = null
override val layout: View
get() = binding.root
@@ -38,7 +46,6 @@ internal class FormMessageView @JvmOverloads internal constructor(
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
@@ -65,82 +72,102 @@ internal class FormMessageView @JvmOverloads internal constructor(
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_extra_light
+ binding.rvFormItems.adapter = formItemAdapter
+ binding.rvFormItems.layoutManager = LinearLayoutManager(context)
+ binding.rvFormItems.addItemDecoration(
+ ItemSpacingDecoration(resources.getDimensionPixelSize(R.dimen.sb_size_12))
)
- binding.tvMessageFormDisabled.setLinkTextColor(linkTextColor)
- binding.tvMessageFormDisabled.clickedLinkBackgroundColor = ContextCompat.getColor(context, clickedLinkBackgroundColor)
+ binding.rvFormItems.itemAnimator = null
} finally {
a.recycle()
}
}
fun drawFormMessage(message: BaseMessage, messageListUIParams: MessageListUIParams) {
- val form = message.forms.firstOrNull() ?: return
- formFieldAdapter.setFormFields(form)
+ val messageGroupType = messageListUIParams.messageGroupType
+ val isSent = message.sendingStatus == SendingStatus.SUCCEEDED
+ val showProfile =
+ messageGroupType == MessageGroupType.GROUPING_TYPE_SINGLE || messageGroupType == MessageGroupType.GROUPING_TYPE_TAIL
+ val showNickname =
+ (messageGroupType == MessageGroupType.GROUPING_TYPE_SINGLE || messageGroupType == MessageGroupType.GROUPING_TYPE_HEAD) &&
+ (!messageListUIParams.shouldUseQuotedView() || !MessageUtils.hasParentMessage(message))
+ val showSentAt =
+ isSent && (messageGroupType == MessageGroupType.GROUPING_TYPE_TAIL || messageGroupType == MessageGroupType.GROUPING_TYPE_SINGLE)
+
+ binding.ivProfileView.visibility = if (showProfile) VISIBLE else INVISIBLE
+ binding.tvNickname.visibility = if (showNickname) VISIBLE else GONE
+ binding.tvSentAt.visibility = if (showSentAt) VISIBLE else GONE
+
+ val paddingTop =
+ resources.getDimensionPixelSize(if (messageGroupType == MessageGroupType.GROUPING_TYPE_TAIL || messageGroupType == MessageGroupType.GROUPING_TYPE_BODY) R.dimen.sb_size_1 else R.dimen.sb_size_8)
+ val paddingBottom =
+ resources.getDimensionPixelSize(if (messageGroupType == MessageGroupType.GROUPING_TYPE_HEAD || messageGroupType == MessageGroupType.GROUPING_TYPE_BODY) R.dimen.sb_size_1 else R.dimen.sb_size_8)
+ binding.root.setPadding(binding.root.paddingLeft, paddingTop, binding.root.paddingRight, paddingBottom)
+
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) {
+ ViewUtils.drawNickname(binding.tvNickname, message, messageUIConfig, false)
+ ViewUtils.drawProfile(binding.ivProfileView, message)
+ ViewUtils.drawSentAt(binding.tvSentAt, message, messageUIConfig)
+
+ val form = message.messageForm ?: return
+ messageForm = form
+
+ if (messageListUIParams.channelConfig.enableFormTypeMessage && form.version <= MESSAGE_FORM_VERSION) {
binding.formEnabledLayout.visibility = VISIBLE
binding.tvMessageFormDisabled.visibility = GONE
+ formItemAdapter.setMessageForm(form)
+ setSubmitButtonEnabled(!form.isSubmitted)
} else {
+ binding.tvMessageFormDisabled.setAppearance(
+ context,
+ if (SendbirdUIKit.isDarkMode()) R.style.SendbirdBody3OnDark03 else R.style.SendbirdBody3OnLight03
+ )
binding.formEnabledLayout.visibility = GONE
binding.tvMessageFormDisabled.visibility = VISIBLE
}
+ }
- ViewUtils.drawTextMessage(
- binding.tvMessageFormDisabled,
- message,
- messageUIConfig,
- false,
- null,
- null
- )
-
- if (form.isSubmitted) {
- setSubmitButtonVisibility(View.GONE)
+ private fun setSubmitButtonEnabled(enabled: Boolean) {
+ val isDarkMode = SendbirdUIKit.isDarkMode()
+ binding.buttonSubmit.isClickable = enabled
+ binding.buttonSubmit.isEnabled = enabled
+ if (!enabled) {
+ binding.buttonSubmit.background = if (isDarkMode) {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_submit_disabled_button_dark, null)
+ } else {
+ ResourcesCompat.getDrawable(resources, R.drawable.sb_shape_submit_disabled_button_light, null)
+ }
+ binding.buttonSubmit.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdButtonOnDark04 else R.style.SendbirdButtonOnLight04
+ )
+ if (messageForm?.isSubmitted == true) binding.buttonSubmit.text = context.getString(R.string.sb_forms_submitted_successfully)
} else {
- setSubmitButtonVisibility(View.VISIBLE)
- }
- ViewUtils.drawNickname(binding.tvNickname, message, messageUIConfig, false)
- ViewUtils.drawProfile(binding.ivProfileView, message)
- ViewUtils.drawSentAt(binding.tvSentAt, message, messageUIConfig)
- }
+ 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)
+ }
- private fun setSubmitButtonVisibility(visibility: Int) {
- if (visibility !in setOf(View.VISIBLE, View.GONE)) return
- binding.buttonSubmit.visibility = visibility
+ binding.buttonSubmit.setAppearance(
+ context,
+ if (isDarkMode) R.style.SendbirdButtonOnLight01 else R.style.SendbirdButtonOnDark01
+ )
+ binding.buttonSubmit.text = context.getString(R.string.sb_forms_submit)
+ }
}
fun setSubmitButtonClickListener(listener: OnClickListener?) {
binding.buttonSubmit.setOnClickListener { view ->
- val isSubmittable = formFieldAdapter.isSubmittable()
- formFieldAdapter.updateValidation()
+ val isSubmittable = formItemAdapter.isSubmittable()
+ formItemAdapter.updateValidation()
if (!isSubmittable) {
return@setOnClickListener
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MessageTemplateView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MessageTemplateView.kt
index 27e0284d..90f6aaa4 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MessageTemplateView.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MessageTemplateView.kt
@@ -18,6 +18,7 @@ internal class MessageTemplateView @JvmOverloads internal constructor(
defStyle: Int = 0,
autoAdjustHeightWhenInvisible: Boolean = true,
) : RoundCornerLayout(context, attrs, defStyle, autoAdjustHeightWhenInvisible) {
+ var maxWidth: Int = Int.MAX_VALUE
init {
this.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent))
this.radius = 0f
@@ -58,4 +59,18 @@ internal class MessageTemplateView @JvmOverloads internal constructor(
if (cacheKey != null) viewCachePool?.cacheView(cacheKey, view)
this.addView(view)
}
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+
+ var totalWidth = measuredWidth
+
+ if (totalWidth > maxWidth) {
+ totalWidth = maxWidth
+ val newWidthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY)
+ super.onMeasure(newWidthMeasureSpec, heightMeasureSpec)
+ }
+
+ setMeasuredDimension(totalWidth, measuredHeight)
+ }
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherTemplateMessageView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherTemplateMessageView.kt
index 815c3853..3b02745c 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherTemplateMessageView.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherTemplateMessageView.kt
@@ -4,35 +4,31 @@ import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.View
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
+import androidx.annotation.VisibleForTesting
+import com.sendbird.android.channel.TemplateContainerOptions
import com.sendbird.android.message.BaseMessage
import com.sendbird.android.message.FeedbackStatus
-import com.sendbird.android.message.SendingStatus
import com.sendbird.uikit.R
-import com.sendbird.uikit.consts.MessageGroupType
import com.sendbird.uikit.consts.ReplyType
import com.sendbird.uikit.databinding.SbViewOtherTemplateMessageComponentBinding
+import com.sendbird.uikit.interfaces.OnItemClickListener
import com.sendbird.uikit.interfaces.OnMessageTemplateActionHandler
import com.sendbird.uikit.internal.extensions.ERR_MESSAGE_TEMPLATE_NOT_APPLICABLE
-import com.sendbird.uikit.internal.extensions.MessageTemplateContainerType
+import com.sendbird.uikit.internal.extensions.createFallbackViewParams
import com.sendbird.uikit.internal.extensions.createTemplateMessageLoadingView
import com.sendbird.uikit.internal.extensions.drawFeedback
import com.sendbird.uikit.internal.extensions.hasParentMessage
-import com.sendbird.uikit.internal.extensions.messageTemplateContainerType
+import com.sendbird.uikit.internal.extensions.isSuggestedRepliesVisible
+import com.sendbird.uikit.internal.extensions.messageTemplateParams
+import com.sendbird.uikit.internal.extensions.messageTemplateStatus
import com.sendbird.uikit.internal.extensions.saveParamsFromTemplate
+import com.sendbird.uikit.internal.extensions.shouldShowSuggestedReplies
import com.sendbird.uikit.internal.extensions.toContextThemeWrapper
import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener
-import com.sendbird.uikit.internal.model.template_messages.Params
-import com.sendbird.uikit.internal.model.template_messages.ViewType
-import com.sendbird.uikit.internal.extensions.createFallbackViewParams
import com.sendbird.uikit.internal.model.templates.MessageTemplateStatus
-import com.sendbird.uikit.internal.extensions.messageTemplateParams
-import com.sendbird.uikit.internal.extensions.messageTemplateStatus
import com.sendbird.uikit.internal.utils.TemplateViewCachePool
import com.sendbird.uikit.log.Logger
import com.sendbird.uikit.model.MessageListUIParams
-import com.sendbird.uikit.utils.MessageUtils
import com.sendbird.uikit.utils.ViewUtils
internal class OtherTemplateMessageView @JvmOverloads internal constructor(
@@ -40,19 +36,23 @@ internal class OtherTemplateMessageView @JvmOverloads internal constructor(
attrs: AttributeSet? = null,
defStyle: Int = 0
) : BaseMessageView(context, attrs, defStyle) {
- override val binding: SbViewOtherTemplateMessageComponentBinding
+ override val binding: SbViewOtherTemplateMessageComponentBinding = SbViewOtherTemplateMessageComponentBinding.inflate(
+ LayoutInflater.from(context.toContextThemeWrapper(defStyle)), this, true
+ )
override val layout: View
get() = binding.root
private val sentAtAppearance: Int
private val nicknameAppearance: Int
var onFeedbackRatingClickListener: OnFeedbackRatingClickListener? = null
+ var onSuggestedRepliesClickListener: OnItemClickListener? = null
+
+ private val suggestedRepliesViewStub: SuggestedRepliesView? by lazy {
+ binding.suggestedRepliesViewStub.inflate() as? SuggestedRepliesView
+ }
init {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.MessageView, defStyle, 0)
try {
- binding = SbViewOtherTemplateMessageComponentBinding.inflate(
- LayoutInflater.from(context.toContextThemeWrapper(defStyle)), this, true
- )
sentAtAppearance = a.getResourceId(
R.styleable.MessageView_sb_message_time_text_appearance,
R.style.SendbirdCaption4OnLight03
@@ -67,33 +67,25 @@ internal class OtherTemplateMessageView @JvmOverloads internal constructor(
}
fun drawMessage(message: BaseMessage, params: MessageListUIParams, viewCachePool: TemplateViewCachePool, handler: OnMessageTemplateActionHandler?) {
- val messageGroupType = params.messageGroupType
- val isSent = message.sendingStatus == SendingStatus.SUCCEEDED
- val showProfile =
- messageGroupType == MessageGroupType.GROUPING_TYPE_SINGLE || messageGroupType == MessageGroupType.GROUPING_TYPE_TAIL
- val showNickname =
- (messageGroupType == MessageGroupType.GROUPING_TYPE_SINGLE || messageGroupType == MessageGroupType.GROUPING_TYPE_HEAD) &&
- (!params.shouldUseQuotedView() || !MessageUtils.hasParentMessage(message))
+ val messageContainerOptions = message.templateMessageData?.containerOptions ?: TemplateContainerOptions()
+ val showProfile = messageContainerOptions.profile
+ val showNickname = messageContainerOptions.nickname
+ val shouldShowSentAt = messageContainerOptions.time
binding.ivProfileView.visibility = if (showProfile) VISIBLE else INVISIBLE
binding.tvNickname.visibility = if (showNickname) VISIBLE else GONE
- val shouldShowSentAt = isSent && (messageGroupType == MessageGroupType.GROUPING_TYPE_TAIL || messageGroupType == MessageGroupType.GROUPING_TYPE_SINGLE)
+ binding.tvSentAt.visibility = if (shouldShowSentAt) VISIBLE else GONE
+
messageUIConfig?.let {
it.otherSentAtTextUIConfig.mergeFromTextAppearance(context, sentAtAppearance)
it.otherNicknameTextUIConfig.mergeFromTextAppearance(context, nicknameAppearance)
val background = it.otherMessageBackground
if (background != null) binding.messageTemplateView.background = background
}
- ViewUtils.drawNickname(binding.tvNickname, message, messageUIConfig, false)
- ViewUtils.drawProfile(binding.ivProfileView, message)
- ViewUtils.drawSentAt(binding.tvSentAt, message, messageUIConfig)
- ViewUtils.drawSentAt(binding.tvSentAtForWideContainer, message, messageUIConfig)
- val paddingTop =
- resources.getDimensionPixelSize(if (messageGroupType == MessageGroupType.GROUPING_TYPE_TAIL || messageGroupType == MessageGroupType.GROUPING_TYPE_BODY) R.dimen.sb_size_1 else R.dimen.sb_size_8)
- val paddingBottom =
- resources.getDimensionPixelSize(if (messageGroupType == MessageGroupType.GROUPING_TYPE_HEAD || messageGroupType == MessageGroupType.GROUPING_TYPE_BODY) R.dimen.sb_size_1 else R.dimen.sb_size_8)
- binding.root.setPaddingRelative(binding.root.paddingStart, paddingTop, binding.root.paddingEnd, paddingBottom)
- drawTemplateView(message, viewCachePool, shouldShowSentAt, handler)
+ if (showNickname) ViewUtils.drawNickname(binding.tvNickname, message, messageUIConfig, false)
+ if (showProfile) ViewUtils.drawProfile(binding.ivProfileView, message)
+ if (shouldShowSentAt) ViewUtils.drawSentAt(binding.tvSentAt, message, messageUIConfig)
+ drawTemplateView(message, viewCachePool, handler)
val shouldShowFeedback = params.channelConfig.enableFeedback &&
!(message.hasParentMessage() && params.channelConfig.replyType == ReplyType.THREAD)
@@ -106,23 +98,41 @@ internal class OtherTemplateMessageView @JvmOverloads internal constructor(
} else {
binding.feedback.visibility = View.GONE
}
+
+ val shouldShowSuggestedReplies = message.shouldShowSuggestedReplies
+ message.isSuggestedRepliesVisible = shouldShowSuggestedReplies
+ if (shouldShowSuggestedReplies) {
+ suggestedRepliesViewStub?.let {
+ it.visibility = View.VISIBLE
+ it.drawSuggestedReplies(message, params.channelConfig.suggestedRepliesDirection)
+ it.onItemClickListener = OnItemClickListener { v, position, data ->
+ onSuggestedRepliesClickListener?.onItemClick(v, position, data)
+ }
+ }
+ } else {
+ suggestedRepliesViewStub?.visibility = View.GONE
+ }
}
- private fun drawTemplateView(
+ @VisibleForTesting
+ internal fun drawTemplateView(
message: BaseMessage,
viewCachePool: TemplateViewCachePool,
- shouldShowSentAt: Boolean,
handler: OnMessageTemplateActionHandler?
) {
Logger.d("drawTemplateView() messageId = ${message.messageId}, status = ${message.messageTemplateStatus}")
val params = when (val status = message.messageTemplateStatus) {
null, MessageTemplateStatus.NOT_APPLICABLE -> {
Logger.e("MessageTemplateStatus should not be null or NOT_APPLICABLE. messageId = ${message.messageId}, status = $status")
- val errorMessage = context.getString(R.string.sb_text_template_message_fallback_error).format(ERR_MESSAGE_TEMPLATE_NOT_APPLICABLE)
+ val errorMessage = context.getString(R.string.sb_text_template_message_fallback_error).format(
+ ERR_MESSAGE_TEMPLATE_NOT_APPLICABLE
+ )
context.createFallbackViewParams(errorMessage)
}
+ MessageTemplateStatus.FAILED_TO_PARSE, MessageTemplateStatus.FAILED_TO_FETCH -> {
+ context.createFallbackViewParams(message)
+ }
MessageTemplateStatus.LOADING -> {
- changeContainerType(MessageTemplateContainerType.DEFAULT, shouldShowSentAt)
val loadingView = context.createTemplateMessageLoadingView()
binding.messageTemplateView.removeAllViews()
binding.messageTemplateView.addView(loadingView)
@@ -130,24 +140,13 @@ internal class OtherTemplateMessageView @JvmOverloads internal constructor(
}
MessageTemplateStatus.CACHED -> {
// Params could be null if it's failed to parse template (e.g. there's a parent template but no child templates)
- val params = message.messageTemplateParams ?: kotlin.run {
+ val params = message.messageTemplateParams ?: run {
message.saveParamsFromTemplate()
message.messageTemplateParams
}
- val containerType = when {
- params == null -> MessageTemplateContainerType.DEFAULT
- params.hasCarouselView() -> MessageTemplateContainerType.CAROUSEL
- else -> message.messageTemplateContainerType
- }
-
- changeContainerType(containerType, shouldShowSentAt, params == null)
params ?: context.createFallbackViewParams(message)
}
- MessageTemplateStatus.FAILED_TO_PARSE, MessageTemplateStatus.FAILED_TO_FETCH -> {
- changeContainerType(MessageTemplateContainerType.DEFAULT, shouldShowSentAt)
- context.createFallbackViewParams(message)
- }
}
val cacheKey = "${message.messageId}_${message.messageTemplateStatus}"
@@ -155,89 +154,16 @@ internal class OtherTemplateMessageView @JvmOverloads internal constructor(
params,
cacheKey,
viewCachePool,
- onViewCreated = { view, params ->
- params.action?.register(view, message) { view, action, message ->
+ onViewCreated = { v, p ->
+ p.action?.register(v, message) { view, action, message ->
handler?.onHandleAction(view, action, message)
}
},
- onChildViewCreated = { view, params ->
- params.action?.register(view, message) { view, action, message ->
+ onChildViewCreated = { v, p ->
+ p.action?.register(v, message) { view, action, message ->
handler?.onHandleAction(view, action, message)
}
}
)
}
-
- private fun changeContainerType(type: MessageTemplateContainerType, shouldShowSentAt: Boolean, widthWrapContent: Boolean = true) {
- setContentPanelConstraintByType(type, widthWrapContent)
- val radius = when (type) {
- MessageTemplateContainerType.CAROUSEL -> 0F
- else -> context.resources.getDimensionPixelSize(R.dimen.sb_size_12).toFloat()
- }
- setContentPanelRadius(radius)
- setSentAtVisibility(type, shouldShowSentAt)
- }
-
- private fun setContentPanelConstraintByType(type: MessageTemplateContainerType, widthWrapContent: Boolean = true) {
- val margin = context.resources.getDimensionPixelSize(R.dimen.sb_size_12)
- val defaultWidth = if (widthWrapContent) {
- ConstraintSet.WRAP_CONTENT
- } else {
- context.resources.getDimensionPixelSize(R.dimen.sb_message_max_width)
- }
-
- val contentPanelId = binding.messageTemplateView.id
- binding.root.changeConstraintSet { set ->
- when (type) {
- MessageTemplateContainerType.DEFAULT -> {
- set.constrainWidth(contentPanelId, defaultWidth)
- set.connect(contentPanelId, ConstraintSet.START, binding.profileRightPadding.id, ConstraintSet.END, 0)
- set.clear(contentPanelId, ConstraintSet.END)
- }
- MessageTemplateContainerType.WIDE -> {
- set.constrainWidth(contentPanelId, 0)
- set.connect(contentPanelId, ConstraintSet.START, binding.profileRightPadding.id, ConstraintSet.END, 0)
- set.connect(contentPanelId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, margin)
- }
- MessageTemplateContainerType.CAROUSEL -> {
- set.constrainWidth(contentPanelId, ConstraintSet.MATCH_CONSTRAINT)
- set.connect(contentPanelId, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, 0)
- set.connect(contentPanelId, ConstraintSet.END, ConstraintSet.PARENT_ID, ConstraintSet.END, 0)
- }
- }
- }
- }
-
- private fun setContentPanelRadius(radius: Float) {
- binding.messageTemplateView.radius = radius
- }
-
- private fun setSentAtVisibility(containerType: MessageTemplateContainerType, shouldShowSentAt: Boolean) {
- if (shouldShowSentAt) {
- when (containerType) {
- MessageTemplateContainerType.DEFAULT -> {
- binding.tvSentAt.visibility = VISIBLE
- binding.tvSentAtForWideContainer.visibility = INVISIBLE
- }
- MessageTemplateContainerType.WIDE, MessageTemplateContainerType.CAROUSEL -> {
- binding.tvSentAt.visibility = INVISIBLE
- binding.tvSentAtForWideContainer.visibility = VISIBLE
- }
- }
- } else {
- binding.tvSentAt.visibility = INVISIBLE
- binding.tvSentAtForWideContainer.visibility = INVISIBLE
- }
- }
-
- private fun ConstraintLayout.changeConstraintSet(block: (ConstraintSet) -> Unit) {
- val constraintSet = ConstraintSet()
- constraintSet.clone(this)
- block(constraintSet)
- constraintSet.applyTo(this)
- }
-}
-
-private fun Params.hasCarouselView(): Boolean {
- return body.items.any { it.type == ViewType.CarouselView }
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedRepliesView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedRepliesView.kt
index 46dd88da..479fde81 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedRepliesView.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/SuggestedRepliesView.kt
@@ -57,33 +57,18 @@ internal class SuggestedRepliesView @JvmOverloads internal constructor(
when (direction) {
SuggestedRepliesDirection.VERTICAL -> {
layoutManager?.orientation = LinearLayoutManager.VERTICAL
-
- binding.rvSuggestedReplies.setPaddingRelative(
- 0,
- binding.rvSuggestedReplies.paddingTop,
- binding.rvSuggestedReplies.paddingEnd,
- binding.rvSuggestedReplies.paddingBottom
- )
-
layoutParams.setMargins(
resources.getDimensionPixelSize(R.dimen.sb_size_42),
resources.getDimensionPixelSize(R.dimen.sb_size_20),
- layoutParams.rightMargin,
+ resources.getDimensionPixelSize(R.dimen.sb_size_12),
layoutParams.bottomMargin
)
}
SuggestedRepliesDirection.HORIZONTAL -> {
layoutManager?.orientation = LinearLayoutManager.HORIZONTAL
- binding.rvSuggestedReplies.setPaddingRelative(
- resources.getDimensionPixelSize(R.dimen.sb_size_38),
- binding.rvSuggestedReplies.paddingTop,
- binding.rvSuggestedReplies.paddingEnd,
- binding.rvSuggestedReplies.paddingBottom
- )
-
layoutParams.setMargins(
- layoutParams.leftMargin,
+ resources.getDimensionPixelSize(R.dimen.sb_size_50),
resources.getDimensionPixelSize(R.dimen.sb_size_8),
layoutParams.rightMargin,
layoutParams.bottomMargin
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/notifications/FeedNotificationListComponent.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/notifications/FeedNotificationListComponent.kt
index 229c8088..54795ab6 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/notifications/FeedNotificationListComponent.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/notifications/FeedNotificationListComponent.kt
@@ -15,6 +15,7 @@ import com.sendbird.uikit.interfaces.OnMessageListUpdateHandler
import com.sendbird.uikit.interfaces.OnNotificationCategorySelectListener
import com.sendbird.uikit.interfaces.OnNotificationTemplateActionHandler
import com.sendbird.uikit.internal.extensions.intToDp
+import com.sendbird.uikit.internal.extensions.isContentDisplayed
import com.sendbird.uikit.internal.extensions.setTypeface
import com.sendbird.uikit.internal.interfaces.OnNotificationViewedDetectedListener
import com.sendbird.uikit.internal.model.notifications.NotificationConfig
@@ -22,7 +23,6 @@ import com.sendbird.uikit.internal.ui.widgets.InnerLinearLayoutManager
import com.sendbird.uikit.log.Logger
import com.sendbird.uikit.model.Action
import com.sendbird.uikit.utils.DrawableUtils
-import java.lang.Exception
import java.util.concurrent.atomic.AtomicBoolean
/**
@@ -107,8 +107,13 @@ internal open class FeedNotificationListComponent @JvmOverloads constructor(
val copiedList = adapter?.getItems()?.toList()
if (copiedList != null) {
try {
- val items = range.mapNotNull { i -> copiedList[i] }
- it.onNotificationViewedDetected(items)
+ val items = range.mapNotNull { i ->
+ val message = copiedList[i]
+ if (!message.isContentDisplayed) null else message
+ }
+ if (items.isNotEmpty()) {
+ it.onNotificationViewedDetected(items)
+ }
} catch (ignore: Exception) {
}
}
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
index 6c6af95c..9fa9d71c 100644
--- 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
@@ -16,7 +16,7 @@ internal class FormMessageViewHolder internal constructor(
var onSubmitClickListener: FormSubmitButtonClickListener? = null
override fun bind(channel: BaseChannel, message: BaseMessage, messageListUIParams: MessageListUIParams) {
if (message !is UserMessage) return
- val form = message.forms.firstOrNull() ?: return
+ val form = message.messageForm ?: return
binding.formsMessageView.messageUIConfig = messageUIConfig
binding.formsMessageView.drawFormMessage(message, messageListUIParams)
binding.formsMessageView.setSubmitButtonClickListener {
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherTemplateMessageViewHolder.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherTemplateMessageViewHolder.kt
index baff3c71..7a169e1f 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherTemplateMessageViewHolder.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherTemplateMessageViewHolder.kt
@@ -6,13 +6,14 @@ import com.sendbird.android.message.BaseMessage
import com.sendbird.uikit.activities.viewholder.MessageViewHolder
import com.sendbird.uikit.consts.ClickableViewIdentifier
import com.sendbird.uikit.databinding.SbViewOtherTemplateMessageBinding
+import com.sendbird.uikit.interfaces.OnItemClickListener
import com.sendbird.uikit.interfaces.OnMessageTemplateActionHandler
import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener
import com.sendbird.uikit.internal.ui.messages.OtherTemplateMessageView
import com.sendbird.uikit.internal.utils.TemplateViewCachePool
import com.sendbird.uikit.model.MessageListUIParams
-internal class OtherTemplateMessageViewHolder constructor(
+internal class OtherTemplateMessageViewHolder(
val binding: SbViewOtherTemplateMessageBinding,
messageListUIParams: MessageListUIParams
) : MessageViewHolder(binding.root, messageListUIParams) {
@@ -24,6 +25,7 @@ internal class OtherTemplateMessageViewHolder constructor(
var onMessageTemplateActionHandler: OnMessageTemplateActionHandler? = null
var onFeedbackRatingClickListener: OnFeedbackRatingClickListener? = null
+ var onSuggestedRepliesClickListener: OnItemClickListener? = null
override fun bind(channel: BaseChannel, message: BaseMessage, params: MessageListUIParams) {
binding.otherMessageView.messageUIConfig = messageUIConfig
@@ -31,9 +33,8 @@ internal class OtherTemplateMessageViewHolder constructor(
onMessageTemplateActionHandler?.onHandleAction(view, action, message)
}
- messageView.onFeedbackRatingClickListener = OnFeedbackRatingClickListener { message, rating ->
- this.onFeedbackRatingClickListener?.onFeedbackClicked(message, rating)
- }
+ messageView.onFeedbackRatingClickListener = onFeedbackRatingClickListener
+ messageView.onSuggestedRepliesClickListener = onSuggestedRepliesClickListener
}
override fun getClickableViewMap(): Map {
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/TemplateViews.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/TemplateViews.kt
index 9e2f4e15..c46b96e1 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/TemplateViews.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/TemplateViews.kt
@@ -2,6 +2,7 @@ package com.sendbird.uikit.internal.ui.widgets
import android.annotation.SuppressLint
import android.content.Context
+import android.graphics.Color
import android.graphics.Typeface
import android.text.TextUtils
import android.util.AttributeSet
@@ -204,10 +205,9 @@ internal class CarouselView @JvmOverloads constructor(
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RoundCornerLayout(context, attrs, defStyleAttr) {
- private val maxChildrenCount = 10 // (it's default value, not available to change at this point)
val recyclerView: RecyclerView
private var itemDecoration: CarouselViewItemDecoration? = null
- private val startPadding: Int = context.resources.intToDp(12 + 26 + 12) // left padding of profile + profile width + right padding of profile
+
init {
layoutParams = LayoutParams(
LayoutParams.WRAP_CONTENT,
@@ -218,9 +218,13 @@ internal class CarouselView @JvmOverloads constructor(
// If padding is touched, the event should be dispatched to the parent view.
override fun onTouchEvent(e: MotionEvent): Boolean {
val layoutManager = this.layoutManager as? LinearLayoutManager
- val isTouchEventInPadding = e.x < startPadding
- if (isTouchEventInPadding && layoutManager?.findFirstVisibleItemPosition() == 0) {
- return false
+ layoutManager?.let {
+ val firstVisibleItemPosition = it.findFirstVisibleItemPosition()
+ val lastVisibleItemPosition = it.findLastVisibleItemPosition()
+ val isTouchEventInPadding = (e.x < paddingStart) || (e.x > width - paddingEnd)
+ if (isTouchEventInPadding && (firstVisibleItemPosition == 0 || lastVisibleItemPosition == it.itemCount - 1)) {
+ return false
+ }
}
return super.onTouchEvent(e)
@@ -231,30 +235,32 @@ internal class CarouselView @JvmOverloads constructor(
LayoutParams.WRAP_CONTENT
)
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
-
- setPaddingRelative(startPadding, paddingTop, paddingEnd, paddingBottom)
clipToPadding = false
CarouselLeftSnapHelper().attachToRecyclerView(this)
}
- recyclerView.adapter = CarouselChildViewAdapter()
this.addView(recyclerView)
}
fun apply(params: CarouselViewParams, orientation: Orientation, onChildViewCreated: ViewLifecycleHandler?) {
- val spaceInPixel = context.resources.intToDp(params.spacing)
+ val spaceInPixel = context.resources.intToDp(params.carouselStyle.spacing)
itemDecoration?.let { recyclerView.removeItemDecoration(it) }
itemDecoration = CarouselViewItemDecoration(spaceInPixel).also {
recyclerView.addItemDecoration(it)
}
- val adapter = recyclerView.adapter as? CarouselChildViewAdapter ?: return
- adapter.onChildViewCreated = onChildViewCreated
- adapter.setChildTemplateParams(params.items.take(maxChildrenCount)) // platform synced
+ CarouselChildViewAdapter(context.resources.intToDp(params.carouselStyle.maxChildWidth)).apply {
+ recyclerView.adapter = this
+ this.onChildViewCreated = onChildViewCreated
+ this.setChildTemplateParams(params.items)
+ }
params.applyLayoutParams(context, layoutParams, orientation)
- // Currently, viewStyle is not used in CarouselView.
- // params.viewStyle.apply(this, true)
+ // apply ViewStyle to RecyclerView but Recycler is not a ViewRoundable instance.
+ // So, we need to apply the style to parent view directly. (workaround)
+ params.viewStyle.apply(recyclerView)
+ setRadiusIntSize(params.viewStyle.radius ?: 0)
+ setBorder(params.viewStyle.borderWidth ?: 0, params.viewStyle.borderColor ?: Color.TRANSPARENT)
}
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/TextChip.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/TextChip.kt
new file mode 100644
index 00000000..ebf362dd
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/TextChip.kt
@@ -0,0 +1,64 @@
+package com.sendbird.uikit.internal.ui.widgets
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Typeface
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.View
+import androidx.core.content.ContextCompat
+import androidx.core.content.res.ResourcesCompat
+import com.google.android.material.chip.Chip
+import com.sendbird.uikit.R
+import com.sendbird.uikit.SendbirdUIKit
+import com.sendbird.uikit.internal.extensions.intToDp
+
+internal class TextChip @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : Chip(context, attrs, defStyleAttr) {
+ private val isDarkMode = SendbirdUIKit.isDarkMode()
+ private val chipEnabledBackgroundColor: ColorStateList? = ContextCompat.getColorStateList(context, if (isDarkMode) R.color.selector_form_chip_background_dark else R.color.selector_form_chip_background_light)
+ private val chipDisabledBackgroundColor: ColorStateList? = ContextCompat.getColorStateList(context, if (isDarkMode) R.color.onlight_chip_disabled else R.color.ondark_chip_disabled)
+ private val chipEnabledTextColor: ColorStateList? = ContextCompat.getColorStateList(context, if (isDarkMode) R.color.selector_form_chip_text_dark else R.color.selector_form_chip_text_light)
+ private val chipDisabledTextColor: ColorStateList? = ContextCompat.getColorStateList(context, if (isDarkMode) R.color.selector_form_chip_disabled_text_dark else R.color.selector_form_chip_disabled_text_light)
+ private val chipEnabledStrokeColor: ColorStateList? = ContextCompat.getColorStateList(context, if (isDarkMode) R.color.selector_form_chip_stroke_dark else R.color.selector_form_chip_stroke_light)
+ private val closeSubmittedIconTint: ColorStateList? = ContextCompat.getColorStateList(context, if (isDarkMode) R.color.secondary_200 else R.color.secondary_300)
+
+ var isChipEnabled: Boolean = true
+ set(value) {
+ chipBackgroundColor = if (value) chipEnabledBackgroundColor else chipDisabledBackgroundColor
+ setTextColor(if (value) chipEnabledTextColor else chipDisabledTextColor)
+ closeIcon = if (value) null else ResourcesCompat.getDrawable(resources, R.drawable.icon_done, null)
+ chipStrokeColor = if (value) chipEnabledStrokeColor else null
+ chipStrokeWidth = if (value) resources.intToDp(1).toFloat() else 0f
+ closeIconTint = if (value) null else closeSubmittedIconTint
+ closeIconStartPadding = if (value) 0f else resources.intToDp(4).toFloat()
+ isEnabled = value
+ field = value
+ }
+
+ var isChipSelected: Boolean = false
+ set(value) {
+ isChecked = value
+ isCloseIconVisible = value
+ field = value
+ }
+
+ init {
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
+ textAlignment = View.TEXT_ALIGNMENT_CENTER
+ setTypeface(typeface, Typeface.BOLD)
+ textStartPadding = 0f
+ textEndPadding = 0f
+ ellipsize = TextUtils.TruncateAt.END
+ chipStartPadding = resources.intToDp(12).toFloat()
+ chipEndPadding = resources.intToDp(12).toFloat()
+ checkedIcon = null
+ isCheckable = true
+ isClickable = true
+ setEnsureMinTouchTargetSize(false)
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/utils/NotificationViewedTracker.kt b/uikit/src/main/java/com/sendbird/uikit/internal/utils/NotificationViewedTracker.kt
index 70e3e3b5..c01ffe1c 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/utils/NotificationViewedTracker.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/utils/NotificationViewedTracker.kt
@@ -1,6 +1,7 @@
package com.sendbird.uikit.internal.utils
import android.graphics.Rect
+import android.view.View
import android.view.ViewTreeObserver.OnGlobalLayoutListener
import androidx.recyclerview.widget.RecyclerView
import com.sendbird.uikit.internal.extensions.runOnUiThread
@@ -23,6 +24,24 @@ internal class NotificationViewedTracker(
private var initialDataLoaded = false
private var isRunning = false
var onNotificationViewedDetected: (() -> Unit)? = null
+ private val onChildAttachStateChangeListener by lazy {
+ object : RecyclerView.OnChildAttachStateChangeListener {
+ override fun onChildViewAttachedToWindow(view: View) {
+ if (!initialDataLoaded && recyclerView.childCount > 0) {
+ initialDataLoaded = true
+ startSchedule()
+ }
+ }
+
+ override fun onChildViewDetachedFromWindow(view: View) {
+ // this callback is called just before the view is detached.
+ // so child count never be 0.
+ if (initialDataLoaded && recyclerView.childCount <= 1) {
+ initialDataLoaded = false
+ }
+ }
+ }
+ }
override fun onScrollStateChanged(view: RecyclerView, scrollState: Int) {
if (view.childCount <= 0) return
@@ -62,6 +81,7 @@ internal class NotificationViewedTracker(
Logger.d(">> NotificationViewedTracker::start()")
if (isRunning) return
recyclerView.addOnScrollListener(this)
+ recyclerView.addOnChildAttachStateChangeListener(onChildAttachStateChangeListener)
/**
* If it is organized in tabs, the View's Visibility is View.Visible, but it may not actually be visible on the screen.
@@ -78,6 +98,7 @@ internal class NotificationViewedTracker(
isRunning = false
recyclerView.removeOnScrollListener(this)
recyclerView.viewTreeObserver.removeOnGlobalLayoutListener(this)
+ recyclerView.removeOnChildAttachStateChangeListener(onChildAttachStateChangeListener)
cancelSchedule()
}
@@ -88,7 +109,7 @@ internal class NotificationViewedTracker(
if (initialDelay > 0L) {
scheduler.schedule({
notifyNotificationViewed()
- }, debounce, TimeUnit.MILLISECONDS)
+ }, initialDelay, TimeUnit.MILLISECONDS)
return
} else {
notifyNotificationViewed()
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 7fcf9c97..12e1d4d5 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
@@ -7,7 +7,7 @@
import com.sendbird.android.channel.GroupChannel;
import com.sendbird.android.message.BaseMessage;
-import com.sendbird.android.message.Form;
+import com.sendbird.android.message.MessageForm;
import com.sendbird.android.message.SendingStatus;
import com.sendbird.uikit.activities.adapter.MessageListAdapter;
import com.sendbird.uikit.consts.StringSet;
@@ -240,7 +240,7 @@ public void setSuggestedRepliesClickListener(@Nullable OnItemClickListener messageList = activeDisableInputMessageList(cachedMessages, order);
+ ChannelExtensionsKt.saveDisabledChatInputMessages(channel, messageList);
+ }
+ }
+ }
}
messageList.setValue(new ChannelMessageData(traceName, finalMessageList));
diff --git a/uikit/src/main/res/color/selector_form_chip_background_dark.xml b/uikit/src/main/res/color/selector_form_chip_background_dark.xml
new file mode 100644
index 00000000..87ad591e
--- /dev/null
+++ b/uikit/src/main/res/color/selector_form_chip_background_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/color/selector_form_chip_background_light.xml b/uikit/src/main/res/color/selector_form_chip_background_light.xml
new file mode 100644
index 00000000..2308af5c
--- /dev/null
+++ b/uikit/src/main/res/color/selector_form_chip_background_light.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/color/selector_form_chip_disabled_text_dark.xml b/uikit/src/main/res/color/selector_form_chip_disabled_text_dark.xml
new file mode 100644
index 00000000..10fd151a
--- /dev/null
+++ b/uikit/src/main/res/color/selector_form_chip_disabled_text_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/color/selector_form_chip_disabled_text_light.xml b/uikit/src/main/res/color/selector_form_chip_disabled_text_light.xml
new file mode 100644
index 00000000..6d7d05f3
--- /dev/null
+++ b/uikit/src/main/res/color/selector_form_chip_disabled_text_light.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/color/selector_form_chip_stroke_dark.xml b/uikit/src/main/res/color/selector_form_chip_stroke_dark.xml
new file mode 100644
index 00000000..f835cca5
--- /dev/null
+++ b/uikit/src/main/res/color/selector_form_chip_stroke_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/color/selector_form_chip_stroke_light.xml b/uikit/src/main/res/color/selector_form_chip_stroke_light.xml
new file mode 100644
index 00000000..27c9a4c1
--- /dev/null
+++ b/uikit/src/main/res/color/selector_form_chip_stroke_light.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/color/selector_form_chip_text_dark.xml b/uikit/src/main/res/color/selector_form_chip_text_dark.xml
new file mode 100644
index 00000000..aa66c58e
--- /dev/null
+++ b/uikit/src/main/res/color/selector_form_chip_text_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/color/selector_form_chip_text_light.xml b/uikit/src/main/res/color/selector_form_chip_text_light.xml
new file mode 100644
index 00000000..d468f92e
--- /dev/null
+++ b/uikit/src/main/res/color/selector_form_chip_text_light.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/drawable/sb_shape_edit_text_form_item_focused_dark.xml b/uikit/src/main/res/drawable/sb_shape_edit_text_form_item_focused_dark.xml
new file mode 100644
index 00000000..ee3e2704
--- /dev/null
+++ b/uikit/src/main/res/drawable/sb_shape_edit_text_form_item_focused_dark.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/uikit/src/main/res/drawable/sb_shape_edit_text_form_item_focused_light.xml b/uikit/src/main/res/drawable/sb_shape_edit_text_form_item_focused_light.xml
new file mode 100644
index 00000000..7da72910
--- /dev/null
+++ b/uikit/src/main/res/drawable/sb_shape_edit_text_form_item_focused_light.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
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_item_invalid_dark.xml
similarity index 100%
rename from uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_dark.xml
rename to uikit/src/main/res/drawable/sb_shape_edit_text_form_item_invalid_dark.xml
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_item_invalid_light.xml
similarity index 100%
rename from uikit/src/main/res/drawable/sb_shape_edit_text_form_field_invalid_light.xml
rename to uikit/src/main/res/drawable/sb_shape_edit_text_form_item_invalid_light.xml
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_item_normal_dark.xml
similarity index 100%
rename from uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_dark.xml
rename to uikit/src/main/res/drawable/sb_shape_edit_text_form_item_normal_dark.xml
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_item_normal_light.xml
similarity index 100%
rename from uikit/src/main/res/drawable/sb_shape_edit_text_form_field_normal_light.xml
rename to uikit/src/main/res/drawable/sb_shape_edit_text_form_item_normal_light.xml
diff --git a/uikit/src/main/res/drawable/sb_shape_form_chip_not_selected_background_dark.xml b/uikit/src/main/res/drawable/sb_shape_form_chip_not_selected_background_dark.xml
new file mode 100644
index 00000000..1da5e164
--- /dev/null
+++ b/uikit/src/main/res/drawable/sb_shape_form_chip_not_selected_background_dark.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/uikit/src/main/res/drawable/sb_shape_form_chip_not_selected_background_light.xml b/uikit/src/main/res/drawable/sb_shape_form_chip_not_selected_background_light.xml
new file mode 100644
index 00000000..f5d8efd7
--- /dev/null
+++ b/uikit/src/main/res/drawable/sb_shape_form_chip_not_selected_background_light.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/uikit/src/main/res/drawable/sb_shape_form_chip_selected_background_dark.xml b/uikit/src/main/res/drawable/sb_shape_form_chip_selected_background_dark.xml
new file mode 100644
index 00000000..d498e668
--- /dev/null
+++ b/uikit/src/main/res/drawable/sb_shape_form_chip_selected_background_dark.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
diff --git a/uikit/src/main/res/drawable/sb_shape_form_chip_selected_background_light.xml b/uikit/src/main/res/drawable/sb_shape_form_chip_selected_background_light.xml
new file mode 100644
index 00000000..5b9fd64c
--- /dev/null
+++ b/uikit/src/main/res/drawable/sb_shape_form_chip_selected_background_light.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
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
index 83140c12..04fc9160 100644
--- 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
@@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
-
+
diff --git a/uikit/src/main/res/drawable/sb_shape_submit_disabled_button_dark.xml b/uikit/src/main/res/drawable/sb_shape_submit_disabled_button_dark.xml
new file mode 100644
index 00000000..c3e08132
--- /dev/null
+++ b/uikit/src/main/res/drawable/sb_shape_submit_disabled_button_dark.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/uikit/src/main/res/drawable/sb_shape_submit_disabled_button_light.xml b/uikit/src/main/res/drawable/sb_shape_submit_disabled_button_light.xml
new file mode 100644
index 00000000..ccec7ef6
--- /dev/null
+++ b/uikit/src/main/res/drawable/sb_shape_submit_disabled_button_light.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
diff --git a/uikit/src/main/res/drawable/selector_form_default_dark.xml b/uikit/src/main/res/drawable/selector_form_default_dark.xml
new file mode 100644
index 00000000..ff62730a
--- /dev/null
+++ b/uikit/src/main/res/drawable/selector_form_default_dark.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/uikit/src/main/res/drawable/selector_form_default_light.xml b/uikit/src/main/res/drawable/selector_form_default_light.xml
new file mode 100644
index 00000000..1e809d22
--- /dev/null
+++ b/uikit/src/main/res/drawable/selector_form_default_light.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ 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_item_chip.xml
similarity index 66%
rename from uikit/src/main/res/layout/sb_view_form_field.xml
rename to uikit/src/main/res/layout/sb_view_form_item_chip.xml
index cfd6cffb..fa103510 100644
--- a/uikit/src/main/res/layout/sb_view_form_field.xml
+++ b/uikit/src/main/res/layout/sb_view_form_item_chip.xml
@@ -1,6 +1,6 @@
-
diff --git a/uikit/src/main/res/layout/sb_view_form_item_chip_component.xml b/uikit/src/main/res/layout/sb_view_form_item_chip_component.xml
new file mode 100644
index 00000000..0d66b33b
--- /dev/null
+++ b/uikit/src/main/res/layout/sb_view_form_item_chip_component.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uikit/src/main/res/layout/sb_view_form_item_text.xml b/uikit/src/main/res/layout/sb_view_form_item_text.xml
new file mode 100644
index 00000000..9e2e39d6
--- /dev/null
+++ b/uikit/src/main/res/layout/sb_view_form_item_text.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_item_text_component.xml
similarity index 62%
rename from uikit/src/main/res/layout/sb_view_form_field_component.xml
rename to uikit/src/main/res/layout/sb_view_form_item_text_component.xml
index e68b1f0a..fd397bc5 100644
--- a/uikit/src/main/res/layout/sb_view_form_field_component.xml
+++ b/uikit/src/main/res/layout/sb_view_form_item_text_component.xml
@@ -6,87 +6,88 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
-
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
+ app:constraint_referenced_ids="tvFormItemTitle"/>
+ app:layout_constraintTop_toBottomOf="@id/barrierFormItemTitle">
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/sb_size_6">
+
+
+ app:layout_constraintStart_toStartOf="@id/etFormItem"
+ app:layout_constraintTop_toBottomOf="@id/etFormItem"/>
-
+
+
+ app:layout_constraintTop_toTopOf="parent"
+ tools:ignore="Autofill" />
+
+ app:layout_constraintEnd_toEndOf="parent" />
diff --git a/uikit/src/main/res/layout/sb_view_form_item_textarea.xml b/uikit/src/main/res/layout/sb_view_form_item_textarea.xml
new file mode 100644
index 00000000..f558ba2b
--- /dev/null
+++ b/uikit/src/main/res/layout/sb_view_form_item_textarea.xml
@@ -0,0 +1,6 @@
+
+
diff --git a/uikit/src/main/res/layout/sb_view_form_item_textarea_component.xml b/uikit/src/main/res/layout/sb_view_form_item_textarea_component.xml
new file mode 100644
index 00000000..14a683d9
--- /dev/null
+++ b/uikit/src/main/res/layout/sb_view_form_item_textarea_component.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
index 02ab79b4..2c1aba73 100644
--- a/uikit/src/main/res/layout/sb_view_form_message_component.xml
+++ b/uikit/src/main/res/layout/sb_view_form_message_component.xml
@@ -5,7 +5,6 @@
android:layout_height="wrap_content"
android:paddingStart="@dimen/sb_size_12"
android:paddingEnd="@dimen/sb_size_12"
- android:paddingBottom="@dimen/sb_size_16"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
@@ -43,41 +42,52 @@
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/sb_size_4"
app:layout_constraintStart_toEndOf="@id/contentPanel"
+ app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
/>
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintWidth_max="@dimen/sb_forms_message_width">
+
+ android:ellipsize="end"
+ android:maxLines="1"
+ android:minHeight="@dimen/sb_size_36"
+ android:text="@string/sb_forms_submit"
+ android:textAllCaps="false"
+ android:textSize="@dimen/sb_text_size_14"
+ app:layout_constraintBottom_toBottomOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/rvFormItems" />
-
-
-
-
+ app:layout_constraintEnd_toEndOf="parent"
+ >
+
-
+
+
-
-
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/headerContainer"
+ tools:layout_height="100dp"
+ />
-
-
+ android:layout_marginStart="@dimen/sb_size_50"
+ app:layout_constraintTop_toBottomOf="@id/messageTemplateView"
+ app:layout_constraintStart_toStartOf="@id/messageTemplateView"
+ tools:text="14:20 PM"
+ />
+
+
diff --git a/uikit/src/main/res/values/colors.xml b/uikit/src/main/res/values/colors.xml
index c0c468ae..449a57df 100755
--- a/uikit/src/main/res/values/colors.xml
+++ b/uikit/src/main/res/values/colors.xml
@@ -55,10 +55,14 @@
@color/onlight_02
@color/onlight_03
@color/onlight_04
+ #323232
+ #2a2a2a
@color/ondark_01
@color/ondark_02
@color/ondark_03
@color/ondark_04
+ #f7f7f7
+ #ffffff
#fff2b6
diff --git a/uikit/src/main/res/values/dimens.xml b/uikit/src/main/res/values/dimens.xml
index 8ecddd54..c21a02af 100644
--- a/uikit/src/main/res/values/dimens.xml
+++ b/uikit/src/main/res/values/dimens.xml
@@ -159,6 +159,7 @@
99dp
100dp
+ 300dp
400dp
240dp
120dp
diff --git a/uikit/src/main/res/values/strings.xml b/uikit/src/main/res/values/strings.xml
index 7188e07e..220f83c1 100644
--- a/uikit/src/main/res/values/strings.xml
+++ b/uikit/src/main/res/values/strings.xml
@@ -227,7 +227,11 @@
Submit
- Please check the value
+ Submitted successfully
+ Please check the value
+ This field is required.
+ Form type messages are not available in this version.
+ No response
Submit failed
(optional)