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"> +