diff --git a/CHANGELOG.md b/CHANGELOG.md index 3376f45e..d664cc37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,13 @@ # Changelog +### v3.13.0 (Feb 1, 2024) with Chat SDK `v4.14.2` +* A feedback feature has been added to give opinions on the message. + * Added `enableFeedback` in `ChannelConfig`. + * Added `OnFeedbackRatingClickListener` which is a callback to be invoked when a feedback rating is clicked. + * Added `getFeedbackRatingClickListener()` and `setFeedbackRatingClickListener(OnFeedbackRatingClickListener)` in `BaseMessageListAdapter`. + * Added `setOnFeedbackRatingClickListener(OnFeedbackRatingClickListener)` and `onFeedbackRatingClicked(BaseMessage, FeedbackRating)` in `BaseMessageListComponent`. + * Added `onFeedbackRatingClicked(BaseMessage, FeedbackRating)` in `ChannelFragment`. + * Added `submitFeedback(BaseMessage, FeedbackRating, String)` and `removeFeedback(BaseMessage)` in `ChannelViewModel`. + * Added `onFeedbackSubmitted()`, `onFeedbackUpdated()` and `onFeedbackDelete` in `ChannelViewModel`. They allow you to observe feedback events for submitting, updating and deleting feedback. ### v3.12.1 (Jan 18, 2024) with Chat SDK `v4.14.1` * Fix memory leaks in UIKit. ### v3.12.0 (Jan, 2024) with Chat SDK `v4.13.0` diff --git a/gradle.properties b/gradle.properties index c407b144..5e6ef987 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,5 +16,5 @@ org.gradle.jvmargs=-Xmx1536m # https://developer.android.com/topic/libraries/support-library/androidx-rn android.useAndroidX=true -UIKIT_VERSION = 3.12.1 +UIKIT_VERSION = 3.13.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 7c6834bb..b04eb722 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 @@ -65,6 +65,58 @@ class BaseApplication : MultiDexApplication() { } } }, context) + + // set theme mode + SendbirdUIKit.setDefaultThemeMode(PreferenceUtils.themeMode) + // register push notification + SendbirdPushHelper.registerPushHandler(MyFirebaseMessagingService()) + // set logger + SendbirdUIKit.setLogLevel(SendbirdUIKit.LogLevel.ALL) + } + + /** + * In a sample app, different contextual settings are used in a single app. + * These are only used in the sample, because if the app kills and resurrects due to low memory, the last used sample settings should be preserved. + */ + fun setupConfigurations() { + when (PreferenceUtils.selectedSampleType) { + SampleType.Basic -> { + // set whether to use user profile + UIKitConfig.common.enableUsingDefaultUserProfile = true + // set whether to use typing indicators in channel list + UIKitConfig.groupChannelListConfig.enableTypingIndicator = true + // set whether to use read/delivery receipt in channel list + UIKitConfig.groupChannelListConfig.enableMessageReceiptStatus = true + // set whether to use user mention + UIKitConfig.groupChannelConfig.enableMention = true + // set reply type + UIKitConfig.groupChannelConfig.replyType = ReplyType.THREAD + UIKitConfig.groupChannelConfig.threadReplySelectType = ThreadReplySelectType.THREAD + // set whether to use voice message + UIKitConfig.groupChannelConfig.enableVoiceMessage = true + // set typing indicator types + UIKitConfig.groupChannelConfig.typingIndicatorTypes = setOf(TypingIndicatorType.BUBBLE, TypingIndicatorType.TEXT) + // set whether to use feedback + UIKitConfig.groupChannelConfig.enableFeedback = true + // set custom params + SendbirdUIKit.setCustomParamsHandler(object : CustomParamsHandler { + override fun onBeforeCreateOpenChannel(params: OpenChannelCreateParams) { + // You can set OpenChannelCreateParams globally before creating a open channel. + params.customType = StringSet.SB_COMMUNITY_TYPE + } + }) + } + SampleType.Notification -> {} + SampleType.Customization -> {} + SampleType.AiChatBot -> { + // set typing indicator types + UIKitConfig.groupChannelConfig.typingIndicatorTypes = setOf(TypingIndicatorType.BUBBLE) + // set whether to use feedback + UIKitConfig.groupChannelConfig.enableFeedback = true + } + else -> { + } + } } } @@ -78,49 +130,4 @@ class BaseApplication : MultiDexApplication() { // setup uikit configurations setupConfigurations() } - - /** - * In a sample app, different contextual settings are used in a single app. - * These are only used in the sample, because if the app kills and resurrects due to low memory, the last used sample settings should be preserved. - */ - private fun setupConfigurations() { - // set theme mode - SendbirdUIKit.setDefaultThemeMode(PreferenceUtils.themeMode) - // register push notification - SendbirdPushHelper.registerPushHandler(MyFirebaseMessagingService()) - // set logger - SendbirdUIKit.setLogLevel(SendbirdUIKit.LogLevel.ALL) - - when (PreferenceUtils.selectedSampleType) { - SampleType.Basic -> { - // set whether to use user profile - UIKitConfig.common.enableUsingDefaultUserProfile = true - // set whether to use typing indicators in channel list - UIKitConfig.groupChannelListConfig.enableTypingIndicator = true - // set whether to use read/delivery receipt in channel list - UIKitConfig.groupChannelListConfig.enableMessageReceiptStatus = true - // set whether to use user mention - UIKitConfig.groupChannelConfig.enableMention = true - // set reply type - UIKitConfig.groupChannelConfig.replyType = ReplyType.THREAD - UIKitConfig.groupChannelConfig.threadReplySelectType = ThreadReplySelectType.THREAD - // set whether to use voice message - UIKitConfig.groupChannelConfig.enableVoiceMessage = true - // set typing indicator types - UIKitConfig.groupChannelConfig.typingIndicatorTypes = setOf(TypingIndicatorType.BUBBLE, TypingIndicatorType.TEXT) - - // set custom params - SendbirdUIKit.setCustomParamsHandler(object : CustomParamsHandler { - override fun onBeforeCreateOpenChannel(params: OpenChannelCreateParams) { - // You can set OpenChannelCreateParams globally before creating a open channel. - params.customType = StringSet.SB_COMMUNITY_TYPE - } - }) - } - SampleType.Notification -> {} - SampleType.Customization -> {} - else -> { - } - } - } } diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/preferences/PreferenceUtils.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/preferences/PreferenceUtils.kt index 37021da9..81fc378f 100644 --- a/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/preferences/PreferenceUtils.kt +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/preferences/PreferenceUtils.kt @@ -2,6 +2,7 @@ package com.sendbird.uikit.samples.common.preferences import android.content.Context import com.sendbird.uikit.SendbirdUIKit.ThemeMode +import com.sendbird.uikit.samples.BaseApplication import com.sendbird.uikit.samples.common.consts.SampleType /** @@ -69,6 +70,7 @@ internal object PreferenceUtils { } else { pref.putString(PREFERENCE_KEY_LATEST_USED_SAMPLE, value.name) } + BaseApplication.setupConfigurations() } fun clearAll() = pref.clear() diff --git a/uikit/build.gradle b/uikit/build.gradle index 8c64f6bf..7e212fa6 100644 --- a/uikit/build.gradle +++ b/uikit/build.gradle @@ -64,7 +64,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // Sendbird - api 'com.sendbird.sdk:sendbird-chat:4.14.1' + api 'com.sendbird.sdk:sendbird-chat:4.14.2' implementation 'com.github.bumptech.glide:glide:4.13.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.13.0' diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BaseMessageListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BaseMessageListAdapter.java index 25194ea5..f0913d02 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BaseMessageListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BaseMessageListAdapter.java @@ -31,6 +31,7 @@ import com.sendbird.uikit.interfaces.OnIdentifiableItemLongClickListener; import com.sendbird.uikit.interfaces.OnItemClickListener; import com.sendbird.uikit.interfaces.OnMessageListUpdateHandler; +import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener; import com.sendbird.uikit.internal.singleton.MessageDisplayDataManager; import com.sendbird.uikit.internal.ui.viewholders.MyUserMessageViewHolder; import com.sendbird.uikit.internal.ui.viewholders.OtherUserMessageViewHolder; @@ -70,6 +71,9 @@ abstract public class BaseMessageListAdapter extends BaseMessageAdapter mentionClickListener; + @Nullable + protected OnFeedbackRatingClickListener feedbackRatingClickListener; + @NonNull private final MessageListUIParams messageListUIParams; @Nullable @@ -286,6 +290,12 @@ public void onBindViewHolder(@NonNull MessageViewHolder holder, final int positi mentionClickListener.onItemClick(view, messagePosition, mentionedUser); } }); + + otherUserMessageViewHolder.setOnFeedbackRatingClickListener((message, rating) -> { + if (feedbackRatingClickListener != null) { + feedbackRatingClickListener.onFeedbackClicked(message, rating); + } + }); } if (channel != null) { @@ -554,6 +564,27 @@ public OnItemClickListener getMentionClickListener() { return mentionClickListener; } + /** + * Returns a callback to be invoked when the feedback rating is clicked. + * + * @return {@link OnFeedbackRatingClickListener} to be invoked when the feedback rating is clicked. + * since 3.13.0 + */ + @Nullable + public OnFeedbackRatingClickListener getFeedbackRatingClickListener() { + return feedbackRatingClickListener; + } + + /** + * Register a callback to be invoked when the feedback rating is clicked. + * + * @param feedbackRatingClickListener The callback that will run + * since 3.13.0 + */ + public void setFeedbackRatingClickListener(@Nullable OnFeedbackRatingClickListener feedbackRatingClickListener) { + this.feedbackRatingClickListener = feedbackRatingClickListener; + } + /** * Sets {@link MessageDisplayDataProvider}, which is used to generate data before they are sent or rendered. * The generated value is primarily used when the view is rendered. diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageDiffCallback.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageDiffCallback.java index 1f069f0b..5b9310e5 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageDiffCallback.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageDiffCallback.java @@ -135,6 +135,14 @@ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { return false; } + if (oldMessage.getMyFeedbackStatus() != newMessage.getMyFeedbackStatus()) { + return false; + } + + if (oldMessage.getMyFeedback() != newMessage.getMyFeedback()) { + return false; + } + if (oldMessage instanceof TypingIndicatorMessage && newMessage instanceof TypingIndicatorMessage) { return ((TypingIndicatorMessage) oldMessage).getTypingUsers().equals(((TypingIndicatorMessage) newMessage).getTypingUsers()) ; } 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 fc0d33bd..28a388ba 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java @@ -20,7 +20,10 @@ import com.sendbird.android.channel.GroupChannel; import com.sendbird.android.channel.Role; +import com.sendbird.android.exception.SendbirdException; import com.sendbird.android.message.BaseMessage; +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.SendingStatus; @@ -37,6 +40,7 @@ import com.sendbird.uikit.activities.adapter.SuggestedMentionListAdapter; import com.sendbird.uikit.activities.viewholder.MessageType; import com.sendbird.uikit.activities.viewholder.MessageViewHolderFactory; +import com.sendbird.uikit.consts.DialogEditTextParams; import com.sendbird.uikit.consts.KeyboardDisplayType; import com.sendbird.uikit.consts.ReplyType; import com.sendbird.uikit.consts.StringSet; @@ -45,6 +49,7 @@ import com.sendbird.uikit.interfaces.LoadingDialogHandler; import com.sendbird.uikit.interfaces.MessageDisplayDataProvider; import com.sendbird.uikit.interfaces.OnConsumableClickListener; +import com.sendbird.uikit.interfaces.OnEditTextResultListener; import com.sendbird.uikit.interfaces.OnEmojiReactionClickListener; import com.sendbird.uikit.interfaces.OnEmojiReactionLongClickListener; import com.sendbird.uikit.interfaces.OnInputModeChangedListener; @@ -265,6 +270,7 @@ protected void onBindMessageListComponent(@NonNull MessageListComponent messageL messageListComponent.setOnEmojiReactionMoreButtonClickListener(emojiReactionMoreButtonClickListener != null ? emojiReactionMoreButtonClickListener : (view, position, message) -> showEmojiListDialog(message)); messageListComponent.setSuggestedRepliesClickListener((view, position, data) -> onSuggestedRepliesClicked(data)); messageListComponent.setFormSubmitButtonClickListener(this::onFormSubmitButtonClicked); + messageListComponent.setOnFeedbackRatingClickListener(this::onFeedbackRatingClicked); messageListComponent.setOnTooltipClickListener(tooltipClickListener != null ? tooltipClickListener : this::onMessageTooltipClicked); messageListComponent.setOnQuoteReplyMessageLongClickListener(this::onQuoteReplyMessageLongClicked); @@ -380,6 +386,38 @@ protected void onBindMessageListComponent(@NonNull MessageListComponent messageL } } }); + + viewModel.onFeedbackSubmitted().observe(getViewLifecycleOwner(), result -> { + if (result == null) return; + final BaseMessage message = result.first; + final SendbirdException e = result.second; + if (e == null) { + if (message != null) { + showUpdateFeedbackCommentDialog(message); + } + } else { + toastError(R.string.sb_text_toast_failure_feedback_submit); + } + }); + + viewModel.onFeedbackUpdated().observe(getViewLifecycleOwner(), result -> { + if (result == null) return; + final SendbirdException e = result.second; + + if (e == null) { + toastSuccess(R.string.sb_text_toast_success_feedback_update); + } else { + toastError(R.string.sb_text_toast_failure_feedback_update); + } + }); + + viewModel.onFeedbackDeleted().observe(getViewLifecycleOwner(), result -> { + if (result == null) return; + final SendbirdException e = result.second; + if (e != null) { + toastError(R.string.sb_text_toast_failure_feedback_delete); + } + }); } /** @@ -540,6 +578,37 @@ protected void onFormSubmitButtonClicked(@NonNull BaseMessage message, @NonNull }); } + /** + * Called when the feedback rating of the message is clicked. + * + * @param message The message that contains feedback + * @param feedbackRating The clicked feedback rating + * since 3.13.0 + */ + protected void onFeedbackRatingClicked(@NonNull BaseMessage message, @NonNull FeedbackRating feedbackRating) { + Feedback currentFeedback = message.getMyFeedback(); + if (currentFeedback != null) { + DialogListItem[] dialogListItems = { + new DialogListItem(R.string.sb_text_feedback_edit_comment), + new DialogListItem(R.string.sb_text_feedback_remove_comment, 0, true) + }; + + DialogUtils.showListBottomDialog( + requireContext(), + dialogListItems, + (view, position, data) -> { + if (position == 0) { + showUpdateFeedbackCommentDialog(message); + } else if (position == 1) { + getViewModel().removeFeedback(message); + } + } + ); + } else { + getViewModel().submitFeedback(message, feedbackRating, null); + } + } + /** * Find the same message as the message ID and move it to the matching message. * @@ -835,6 +904,33 @@ private void redirectMessageThreadIfNeeded(@Nullable Bundle args) { } } + private void showUpdateFeedbackCommentDialog(@NonNull BaseMessage message) { + final boolean hasFeedbackComment = message.getMyFeedback() != null && message.getMyFeedback().getComment() != null; + final String positiveButtonText = hasFeedbackComment ? getString(R.string.sb_text_button_save) : getString(R.string.sb_text_button_submit); + final OnEditTextResultListener listener = text -> { + final Feedback feedback = message.getMyFeedback(); + if (feedback == null) return; + getViewModel().submitFeedback(message, feedback.getRating(), text); + }; + + final DialogEditTextParams params = new DialogEditTextParams(getString(R.string.sb_text_feedback_comment_hint)); + final Feedback currentFeedback = message.getMyFeedback(); + if (currentFeedback != null) { + params.setText(currentFeedback.getComment()); + } + params.setEnableSingleLine(true); + DialogUtils.showInputDialog( + requireContext(), + getString(R.string.sb_text_feedback_comment_title), + params, + listener, + positiveButtonText, + null, + getString(R.string.sb_text_button_cancel), + null + ); + } + @SuppressWarnings("unused") public static class Builder { @NonNull diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/NotificationExtensions.kt b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/NotificationExtensions.kt index 0b8aa71d..4a9f4415 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/extensions/NotificationExtensions.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/NotificationExtensions.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.fragment.app.Fragment import com.sendbird.android.params.MessageListParams import com.sendbird.uikit.internal.model.notifications.NotificationConfig +import com.sendbird.uikit.internal.singleton.NotificationChannelManager.checkAndInit import com.sendbird.uikit.internal.singleton.NotificationChannelManager.getGlobalNotificationChannelSettings import com.sendbird.uikit.internal.ui.notifications.ChatNotificationChannelModule import com.sendbird.uikit.internal.ui.notifications.FeedNotificationChannelModule @@ -13,6 +14,7 @@ import com.sendbird.uikit.vm.ChatNotificationChannelViewModel import com.sendbird.uikit.vm.FeedNotificationChannelViewModel internal fun Fragment.createFeedNotificationChannelModule(args: Bundle): FeedNotificationChannelModule { + checkAndInit(requireContext()) val config = getGlobalNotificationChannelSettings()?.let { NotificationConfig.from(it) } @@ -20,6 +22,7 @@ internal fun Fragment.createFeedNotificationChannelModule(args: Bundle): FeedNot } internal fun Fragment.createChatNotificationChannelModule(args: Bundle): ChatNotificationChannelModule { + checkAndInit(requireContext()) val config = getGlobalNotificationChannelSettings()?.let { NotificationConfig.from(it) } 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 7612af18..719efa74 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 @@ -20,8 +20,13 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners import com.bumptech.glide.request.RequestOptions import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition +import com.sendbird.android.message.BaseMessage +import com.sendbird.android.message.FeedbackRating +import com.sendbird.android.message.FeedbackStatus import com.sendbird.uikit.R import com.sendbird.uikit.consts.StringSet +import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener +import com.sendbird.uikit.widgets.FeedbackView @Suppress("DEPRECATION") internal fun TextView.setAppearance(context: Context, res: Int) { @@ -134,3 +139,16 @@ internal fun View.setBackgroundColorAndRadii(colorStateList: ColorStateList?, ra drawable.color = colorStateList background = drawable } + +internal fun FeedbackView.drawFeedback(message: BaseMessage, shouldHideFeedback: Boolean, listener: OnFeedbackRatingClickListener?) { + this.visibility = if (shouldHideFeedback || message.myFeedbackStatus == FeedbackStatus.NOT_APPLICABLE) { + View.GONE + } else { + View.VISIBLE + } + + this.drawFeedback(message.myFeedback) + this.onFeedbackRatingClickListener = { feedbackRating: FeedbackRating -> + listener?.onFeedbackClicked(message, feedbackRating) + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnFeedbackRatingClickListener.kt b/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnFeedbackRatingClickListener.kt new file mode 100644 index 00000000..298ba6c3 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/internal/interfaces/OnFeedbackRatingClickListener.kt @@ -0,0 +1,20 @@ +package com.sendbird.uikit.internal.interfaces + +import com.sendbird.android.message.BaseMessage +import com.sendbird.android.message.FeedbackRating + +/** + * Interface definition for a callback to be invoked when a feedback rating is clicked. + * + * @since 3.13.0 + */ +fun interface OnFeedbackRatingClickListener { + /** + * Called when a feedback rating is clicked. + * + * @param message the message that contains Feedback. + * @param rating the feedback rating. + * @since 3.13.0 + */ + fun onFeedbackClicked(message: BaseMessage, rating: FeedbackRating) +} diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/KeySet.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/KeySet.kt index e85fa377..0e21b8a6 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/KeySet.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/KeySet.kt @@ -94,6 +94,7 @@ internal object KeySet { const val enable_multiple_files_message = "enable_multiple_files_message" const val enable_suggested_replies = "enable_suggested_replies" const val enable_form_type_message = "enable_form_type_message" + const val enable_feedback = "enable_feedback" const val reply_type = "reply_type" const val thread_reply_select_type = "thread_reply_select_type" const val enable_message_receipt_status = "enable_message_receipt_status" diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/NotificationChannelManager.kt b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/NotificationChannelManager.kt index 9da7472f..8ee8dee2 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/NotificationChannelManager.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/NotificationChannelManager.kt @@ -28,7 +28,18 @@ internal object NotificationChannelManager { private lateinit var templateRepository: NotificationTemplateRepository private lateinit var channelSettingsRepository: NotificationChannelRepository + /** + * To avoid sending an unintended exception, if the NotificationChannelManager hasn't been initialized it tries to initialize automatically. + * This is very defensive code and only works when creating a Fragment and attempting to reference NotificationChannelManager in exceptional cases. + */ + internal fun checkAndInit(context: Context) { + if (!isInitialized.get()) { + init(context) + } + } + @JvmStatic + @Synchronized fun init(context: Context) { if (isInitialized.getAndSet(true)) return worker.submit { diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherUserMessageView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherUserMessageView.kt index 51f44b70..8e0d3309 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherUserMessageView.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherUserMessageView.kt @@ -12,8 +12,12 @@ import com.sendbird.android.message.SendingStatus import com.sendbird.android.user.User import com.sendbird.uikit.R import com.sendbird.uikit.consts.MessageGroupType +import com.sendbird.uikit.consts.ReplyType import com.sendbird.uikit.databinding.SbViewOtherUserMessageComponentBinding import com.sendbird.uikit.interfaces.OnItemClickListener +import com.sendbird.uikit.internal.extensions.drawFeedback +import com.sendbird.uikit.internal.extensions.hasParentMessage +import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener import com.sendbird.uikit.internal.ui.widgets.OnLinkLongClickListener import com.sendbird.uikit.model.MessageListUIParams import com.sendbird.uikit.model.TextUIConfig @@ -38,6 +42,7 @@ internal class OtherUserMessageView @JvmOverloads internal constructor( private val sentAtAppearance: Int private val nicknameAppearance: Int var mentionClickListener: OnItemClickListener? = null + var onFeedbackRatingClickListener: OnFeedbackRatingClickListener? = null init { val a = context.theme.obtainStyledAttributes(attrs, R.styleable.MessageView_User, defStyle, 0) @@ -188,5 +193,12 @@ internal class OtherUserMessageView @JvmOverloads internal constructor( binding.quoteReplyPanel.visibility = GONE } ViewUtils.drawThreadInfo(binding.threadInfo, message, params) + + val shouldHideFeedback = !params.channelConfig.enableFeedback || + (message.hasParentMessage() && params.channelConfig.replyType == ReplyType.THREAD) + + binding.feedback.drawFeedback(message, shouldHideFeedback) { _, rating -> + onFeedbackRatingClickListener?.onFeedbackClicked(message, rating) + } } } diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/reactions/DialogView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/reactions/DialogView.kt index 11bf7930..8bc40d87 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/reactions/DialogView.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/reactions/DialogView.kt @@ -170,12 +170,14 @@ internal class DialogView @JvmOverloads constructor( fun setEditText(params: DialogEditTextParams?, editTextResultListener: OnEditTextResultListener?) { params?.let { binding.etInputText.visibility = VISIBLE - it.hintText?.isNotEmpty()?.let { - binding.etInputText.hint = params.hintText + it.hintText.takeUnless { text -> text.isNullOrEmpty() }?.let { text -> + binding.etInputText.hint = text } - it.text?.isNotEmpty()?.let { - binding.etInputText.hint = params.text + + it.text.takeUnless { text -> text.isNullOrEmpty() }?.let { text -> + binding.etInputText.setText(text) } + binding.etInputText.isSingleLine = params.enableSingleLine it.ellipsis?.let { binding.etInputText.ellipsize = params.ellipsis diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherUserMessageViewHolder.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherUserMessageViewHolder.kt index e257119a..7263b916 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherUserMessageViewHolder.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherUserMessageViewHolder.kt @@ -11,6 +11,7 @@ import com.sendbird.uikit.consts.ClickableViewIdentifier import com.sendbird.uikit.databinding.SbViewOtherUserMessageBinding import com.sendbird.uikit.interfaces.OnItemClickListener import com.sendbird.uikit.interfaces.OnItemLongClickListener +import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener import com.sendbird.uikit.model.MessageListUIParams internal class OtherUserMessageViewHolder internal constructor( @@ -51,4 +52,8 @@ internal class OtherUserMessageViewHolder internal constructor( fun setOnMentionClickListener(listener: OnItemClickListener?) { binding.otherMessageView.mentionClickListener = listener } + + fun setOnFeedbackRatingClickListener(listener: OnFeedbackRatingClickListener?) { + binding.otherMessageView.onFeedbackRatingClickListener = listener + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/model/configurations/ChannelConfig.kt b/uikit/src/main/java/com/sendbird/uikit/model/configurations/ChannelConfig.kt index cd333f3f..452ea518 100644 --- a/uikit/src/main/java/com/sendbird/uikit/model/configurations/ChannelConfig.kt +++ b/uikit/src/main/java/com/sendbird/uikit/model/configurations/ChannelConfig.kt @@ -43,6 +43,8 @@ data class ChannelConfig internal constructor( private var _enableSuggestedReplies: Boolean = false, @SerialName(KeySet.enable_form_type_message) private var _enableFormTypeMessage: Boolean = false, + @SerialName(KeySet.enable_feedback) + private var _enableFeedback: Boolean = false, @SerialName(KeySet.thread_reply_select_type) @Serializable(with = ThreadReplySelectTypeAsStringSerializer::class) private var _threadReplySelectType: ThreadReplySelectType = ThreadReplySelectType.THREAD, @@ -78,7 +80,9 @@ data class ChannelConfig internal constructor( @Transient private var enableSuggestedRepliesMutable: Boolean? = null, @Transient - private var enableFormTypeMessageMutable: Boolean? = null + private var enableFormTypeMessageMutable: Boolean? = null, + @Transient + private var enableFeedbackMutable: Boolean? = null ) : Parcelable { companion object { /** @@ -356,6 +360,26 @@ data class ChannelConfig internal constructor( enableFormTypeMessageMutable = value } + var enableFeedback: Boolean + /** + * Returns a value that determines whether to use feedback of the message or not. + * true, if channel displays feedback in the message list if it contains feedback data. + * false, otherwise. + * + * @return true if the feedback is enabled, false otherwise + * @since 3.13.0 + */ + get() = enableFeedbackMutable ?: _enableFeedback + /** + * Sets whether to use feedback or not. + * + * @param value true if the feedback is enabled, false otherwise + * @since 3.13.0 + */ + set(value) { + enableFeedbackMutable = value + } + @JvmSynthetic internal fun merge(config: ChannelConfig): ChannelConfig { this._enableOgTag = config._enableOgTag @@ -367,6 +391,7 @@ data class ChannelConfig internal constructor( this._enableMultipleFilesMessage = config._enableMultipleFilesMessage this._enableSuggestedReplies = config._enableSuggestedReplies this._enableFormTypeMessage = config._enableFormTypeMessage + this._enableFeedback = config._enableFeedback this._threadReplySelectType = config._threadReplySelectType this._replyType = config._replyType this.input.merge(config.input) diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/BaseMessageListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/BaseMessageListComponent.java index 35449ca7..34c3cc2b 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/BaseMessageListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/BaseMessageListComponent.java @@ -19,6 +19,7 @@ import com.sendbird.android.channel.GroupChannel; import com.sendbird.android.message.BaseMessage; +import com.sendbird.android.message.FeedbackRating; import com.sendbird.android.message.SendingStatus; import com.sendbird.android.user.User; import com.sendbird.uikit.R; @@ -32,6 +33,7 @@ import com.sendbird.uikit.interfaces.OnItemLongClickListener; import com.sendbird.uikit.interfaces.OnMessageListUpdateHandler; import com.sendbird.uikit.interfaces.OnPagedDataLoader; +import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener; import com.sendbird.uikit.internal.ui.widgets.InnerLinearLayoutManager; import com.sendbird.uikit.internal.ui.widgets.MessageRecyclerView; import com.sendbird.uikit.internal.ui.widgets.PagerRecyclerView; @@ -68,6 +70,10 @@ abstract public class BaseMessageListComponent messageProfileClickListener; @Nullable private OnItemClickListener messageMentionClickListener; + + @Nullable + private OnFeedbackRatingClickListener feedbackRatingClickListener; + @Nullable private OnItemLongClickListener messageLongClickListener; @Nullable @@ -159,6 +165,10 @@ public void setAdapter(@NonNull LA adapter) { this.adapter.setMentionClickListener(this::onMessageMentionClicked); } + if (this.adapter.getFeedbackRatingClickListener() == null) { + this.adapter.setFeedbackRatingClickListener(this::onFeedbackRatingClicked); + } + if (messageRecyclerView == null) return; messageRecyclerView.getRecyclerView().setAdapter(this.adapter); } @@ -329,6 +339,16 @@ public void setOnMessageMentionClickListener(@Nullable OnItemClickListener this.messageMentionClickListener = messageMentionClickListener; } + /** + * Register a callback to be invoked when the feedback rating of the message is clicked. + * + * @param feedbackRatingClickListener The callback that will run + * since 3.13.0 + */ + public void setOnFeedbackRatingClickListener(@Nullable OnFeedbackRatingClickListener feedbackRatingClickListener) { + this.feedbackRatingClickListener = feedbackRatingClickListener; + } + /** * Register a callback to be invoked when the message is long-clicked. * @@ -544,6 +564,16 @@ protected void onMessageMentionClicked(@NonNull View view, int position, @NonNul messageMentionClickListener.onItemClick(view, position, user); } + /** + * Called when the feedback rating of the message is clicked. + * + * @param rating The clicked feedback rating + * since 3.13.0 + */ + protected void onFeedbackRatingClicked(@NonNull BaseMessage message, @NonNull FeedbackRating rating) { + if (feedbackRatingClickListener != null) feedbackRatingClickListener.onFeedbackClicked(message, rating); + } + /** * Called when the item of the message list is long-clicked. * diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java index 4226521d..3119a29a 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java @@ -1,5 +1,7 @@ package com.sendbird.uikit.vm; +import android.util.Pair; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.UiThread; @@ -21,6 +23,8 @@ import com.sendbird.android.handler.MessageCollectionHandler; import com.sendbird.android.handler.MessageCollectionInitHandler; import com.sendbird.android.message.BaseMessage; +import com.sendbird.android.message.Feedback; +import com.sendbird.android.message.FeedbackRating; import com.sendbird.android.message.FileMessage; import com.sendbird.android.message.SendingStatus; import com.sendbird.android.params.MessageCollectionCreateParams; @@ -32,7 +36,6 @@ import com.sendbird.uikit.consts.StringSet; import com.sendbird.uikit.consts.TypingIndicatorType; import com.sendbird.uikit.interfaces.OnCompleteHandler; -import com.sendbird.uikit.internal.extensions.MessageExtensionsKt; import com.sendbird.uikit.internal.wrappers.MessageCollectionImpl; import com.sendbird.uikit.internal.wrappers.MessageCollectionWrapper; import com.sendbird.uikit.internal.wrappers.SendbirdChatImpl; @@ -81,6 +84,12 @@ public class ChannelViewModel extends BaseMessageListViewModel { private final MutableLiveData statusFrame = new MutableLiveData<>(); @NonNull private final MutableLiveData hugeGapDetected = new MutableLiveData<>(); + @NonNull + private final MutableLiveData> feedbackSubmitted = new MutableLiveData<>(); + @NonNull + private final MutableLiveData> feedbackUpdated = new MutableLiveData<>(); + @NonNull + private final MutableLiveData> feedbackDeleted = new MutableLiveData<>(); @Nullable private MessageListParams messageListParams; @Nullable @@ -463,6 +472,39 @@ public LiveData getStatusFrame() { return statusFrame; } + + /** + * Returns LiveData that can be observed for the result of submitting feedback. + * + * @return The BaseMessage that feedback is submitted. + * since 3.13.0 + */ + public LiveData> onFeedbackSubmitted() { + return feedbackSubmitted; + } + + /** + * Returns LiveData that can be observed for the result of updating feedback. + * + * @return The BaseMessage that feedback is updated. + * since 3.13.0 + */ + @NonNull + public LiveData> onFeedbackUpdated() { + return feedbackUpdated; + } + + /** + * Returns LiveData that can be observed for the result of deleting feedback. + * + * @return The BaseMessage that feedback is deleted. + * since 3.13.0 + */ + @NonNull + public LiveData> onFeedbackDeleted() { + return feedbackDeleted; + } + @Override public boolean hasNext() { return collection == null || collection.getHasNext(); @@ -809,6 +851,49 @@ public MessageListParams createMessageListParams() { return messageListParams; } + /** + * Submits feedback for the message. + * + * @param message The message for feedback. + * @param rating The rating for the message. + * @param comment The comment for the message. + * since 3.13.0 + */ + public void submitFeedback(@NonNull BaseMessage message, @NonNull FeedbackRating rating, @Nullable String comment) { + // If using BaseMessage without copying it, the properties of the message are updated immediately when updating the feedback, + // so the UI is not updated because the changes are not caught in the diff callback. + BaseMessage copiedMessage = BaseMessage.clone(message); + if (copiedMessage == null) return; + + Feedback currentFeedback = copiedMessage.getMyFeedback(); + if (currentFeedback == null) { + copiedMessage.submitFeedback(rating, comment, (feedback, e) -> { + feedbackSubmitted.postValue(Pair.create(copiedMessage, e)); + }); + } else { + copiedMessage.updateFeedback(rating, comment, (feedback, e) -> { + feedbackUpdated.postValue(Pair.create(copiedMessage, e)); + }); + } + } + + /** + * Removes feedback for the message. + * + * @param message The message for removing feedback. + * since 3.13.0 + */ + public void removeFeedback(@NonNull BaseMessage message) { + // If using BaseMessage without copying it, the properties of the message are updated immediately when updating the feedback, + // so the UI is not updated because the changes are not caught in the diff callback. + BaseMessage copiedMessage = BaseMessage.clone(message); + if (copiedMessage == null) return; + + copiedMessage.deleteFeedback(e -> { + feedbackDeleted.postValue(Pair.create(copiedMessage, e)); + }); + } + @VisibleForTesting @NonNull MessageCollectionWrapper createMessageCollection(long startingPoint, @NonNull MessageListParams params, @NonNull GroupChannel channel, @NonNull MessageCollectionHandler handler) { diff --git a/uikit/src/main/java/com/sendbird/uikit/widgets/FeedbackView.kt b/uikit/src/main/java/com/sendbird/uikit/widgets/FeedbackView.kt new file mode 100644 index 00000000..a123b406 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/widgets/FeedbackView.kt @@ -0,0 +1,113 @@ +package com.sendbird.uikit.widgets + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.FrameLayout +import com.sendbird.android.message.Feedback +import com.sendbird.android.message.FeedbackRating +import com.sendbird.uikit.R +import com.sendbird.uikit.SendbirdUIKit +import com.sendbird.uikit.databinding.SbViewFeedbackBinding + +internal class FeedbackView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = R.attr.sb_widget_feedback +) : FrameLayout(context, attrs, defStyleAttr) { + val binding: SbViewFeedbackBinding = SbViewFeedbackBinding.inflate( + LayoutInflater.from(getContext()), + this, + true + ) + var onFeedbackRatingClickListener: ((rating: FeedbackRating) -> Unit)? = null + + init { + val typedArray = context.theme.obtainStyledAttributes( + attrs, + R.styleable.Feedback, + defStyleAttr, + R.style.Widget_Sendbird_Feedback + ) + + try { + val isDarkMode = SendbirdUIKit.isDarkMode() + val goodFeedbackBackgroundResource = typedArray.getResourceId( + R.styleable.Feedback_sb_feedback_good_background, + if (isDarkMode) { + R.drawable.sb_feedback_background_dark + } else { + R.drawable.sb_feedback_background_light + } + ) + + val badFeedbackBackgroundResource = typedArray.getResourceId( + R.styleable.Feedback_sb_feedback_bad_background, + if (isDarkMode) { + R.drawable.sb_feedback_background_dark + } else { + R.drawable.sb_feedback_background_light + } + ) + + val goodFeedbackButtonResource = typedArray.getResourceId( + R.styleable.Feedback_sb_feedback_good_button, + if (isDarkMode) { + R.drawable.selector_feedback_good_button_dark + } else { + R.drawable.selector_feedback_good_button_light + } + ) + + val badFeedbackButtonResource = typedArray.getResourceId( + R.styleable.Feedback_sb_feedback_bad_button, + if (isDarkMode) { + R.drawable.selector_feedback_bad_button_dark + } else { + R.drawable.selector_feedback_bad_button_light + } + ) + + binding.goodFeedbackLayout.setBackgroundResource(goodFeedbackBackgroundResource) + binding.badFeedbackLayout.setBackgroundResource(badFeedbackBackgroundResource) + binding.ivGoodFeedback.setBackgroundResource(goodFeedbackButtonResource) + binding.ivBadFeedback.setBackgroundResource(badFeedbackButtonResource) + } finally { + typedArray.recycle() + } + + binding.goodFeedbackLayout.setOnClickListener { + onFeedbackRatingClickListener?.invoke(FeedbackRating.Good) + } + + binding.badFeedbackLayout.setOnClickListener { + onFeedbackRatingClickListener?.invoke(FeedbackRating.Bad) + } + + drawFeedback(null) + } + + fun drawFeedback(feedback: Feedback?) { + val (isGoodFeedbackEnabled, isGoodFeedbackSelected) = when (feedback?.rating) { + null -> true to false // the feedback is submittable but not submitted state. + FeedbackRating.Good -> true to true + FeedbackRating.Bad -> false to false + } + + val (isBadFeedbackEnabled, isBadFeedbackSelected) = when (feedback?.rating) { + null -> true to false // the feedback is submittable but not submitted state. + FeedbackRating.Good -> false to false + FeedbackRating.Bad -> true to true + } + + binding.goodFeedbackLayout.isEnabled = isGoodFeedbackEnabled + binding.goodFeedbackLayout.isSelected = isGoodFeedbackSelected + binding.ivGoodFeedback.isEnabled = isGoodFeedbackEnabled + binding.ivGoodFeedback.isSelected = isGoodFeedbackSelected + + binding.badFeedbackLayout.isEnabled = isBadFeedbackEnabled + binding.badFeedbackLayout.isSelected = isBadFeedbackSelected + binding.ivBadFeedback.isEnabled = isBadFeedbackEnabled + binding.ivBadFeedback.isSelected = isBadFeedbackSelected + } +} diff --git a/uikit/src/main/res/drawable-hdpi/icon_bad.png b/uikit/src/main/res/drawable-hdpi/icon_bad.png new file mode 100644 index 00000000..2843a327 Binary files /dev/null and b/uikit/src/main/res/drawable-hdpi/icon_bad.png differ diff --git a/uikit/src/main/res/drawable-hdpi/icon_good.png b/uikit/src/main/res/drawable-hdpi/icon_good.png new file mode 100644 index 00000000..9b887670 Binary files /dev/null and b/uikit/src/main/res/drawable-hdpi/icon_good.png differ diff --git a/uikit/src/main/res/drawable-mdpi/icon_bad.png b/uikit/src/main/res/drawable-mdpi/icon_bad.png new file mode 100644 index 00000000..a7fe15ab Binary files /dev/null and b/uikit/src/main/res/drawable-mdpi/icon_bad.png differ diff --git a/uikit/src/main/res/drawable-mdpi/icon_good.png b/uikit/src/main/res/drawable-mdpi/icon_good.png new file mode 100644 index 00000000..414f72fc Binary files /dev/null and b/uikit/src/main/res/drawable-mdpi/icon_good.png differ diff --git a/uikit/src/main/res/drawable-xhdpi/icon_bad.png b/uikit/src/main/res/drawable-xhdpi/icon_bad.png new file mode 100644 index 00000000..38bc85b2 Binary files /dev/null and b/uikit/src/main/res/drawable-xhdpi/icon_bad.png differ diff --git a/uikit/src/main/res/drawable-xhdpi/icon_good.png b/uikit/src/main/res/drawable-xhdpi/icon_good.png new file mode 100644 index 00000000..9e83648a Binary files /dev/null and b/uikit/src/main/res/drawable-xhdpi/icon_good.png differ diff --git a/uikit/src/main/res/drawable-xxhdpi/icon_bad.png b/uikit/src/main/res/drawable-xxhdpi/icon_bad.png new file mode 100644 index 00000000..53fb52ec Binary files /dev/null and b/uikit/src/main/res/drawable-xxhdpi/icon_bad.png differ diff --git a/uikit/src/main/res/drawable-xxhdpi/icon_good.png b/uikit/src/main/res/drawable-xxhdpi/icon_good.png new file mode 100644 index 00000000..4519560a Binary files /dev/null and b/uikit/src/main/res/drawable-xxhdpi/icon_good.png differ diff --git a/uikit/src/main/res/drawable-xxxhdpi/icon_bad.png b/uikit/src/main/res/drawable-xxxhdpi/icon_bad.png new file mode 100644 index 00000000..c0e11c5e Binary files /dev/null and b/uikit/src/main/res/drawable-xxxhdpi/icon_bad.png differ diff --git a/uikit/src/main/res/drawable-xxxhdpi/icon_good.png b/uikit/src/main/res/drawable-xxxhdpi/icon_good.png new file mode 100644 index 00000000..13734426 Binary files /dev/null and b/uikit/src/main/res/drawable-xxxhdpi/icon_good.png differ diff --git a/uikit/src/main/res/drawable/sb_feedback_background_dark.xml b/uikit/src/main/res/drawable/sb_feedback_background_dark.xml new file mode 100644 index 00000000..3caba58a --- /dev/null +++ b/uikit/src/main/res/drawable/sb_feedback_background_dark.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/uikit/src/main/res/drawable/sb_feedback_background_light.xml b/uikit/src/main/res/drawable/sb_feedback_background_light.xml new file mode 100644 index 00000000..1453c2e0 --- /dev/null +++ b/uikit/src/main/res/drawable/sb_feedback_background_light.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/uikit/src/main/res/drawable/selector_feedback_bad_button_dark.xml b/uikit/src/main/res/drawable/selector_feedback_bad_button_dark.xml new file mode 100644 index 00000000..6a61766e --- /dev/null +++ b/uikit/src/main/res/drawable/selector_feedback_bad_button_dark.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/uikit/src/main/res/drawable/selector_feedback_bad_button_light.xml b/uikit/src/main/res/drawable/selector_feedback_bad_button_light.xml new file mode 100644 index 00000000..77c904b0 --- /dev/null +++ b/uikit/src/main/res/drawable/selector_feedback_bad_button_light.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/uikit/src/main/res/drawable/selector_feedback_good_button_dark.xml b/uikit/src/main/res/drawable/selector_feedback_good_button_dark.xml new file mode 100644 index 00000000..f14e06b6 --- /dev/null +++ b/uikit/src/main/res/drawable/selector_feedback_good_button_dark.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/uikit/src/main/res/drawable/selector_feedback_good_button_light.xml b/uikit/src/main/res/drawable/selector_feedback_good_button_light.xml new file mode 100644 index 00000000..8175345a --- /dev/null +++ b/uikit/src/main/res/drawable/selector_feedback_good_button_light.xml @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/uikit/src/main/res/layout/sb_view_feedback.xml b/uikit/src/main/res/layout/sb_view_feedback.xml new file mode 100644 index 00000000..b973003b --- /dev/null +++ b/uikit/src/main/res/layout/sb_view_feedback.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + diff --git a/uikit/src/main/res/layout/sb_view_other_user_message_component.xml b/uikit/src/main/res/layout/sb_view_other_user_message_component.xml index 6bf8532e..18faa382 100644 --- a/uikit/src/main/res/layout/sb_view_other_user_message_component.xml +++ b/uikit/src/main/res/layout/sb_view_other_user_message_component.xml @@ -153,4 +153,15 @@ app:layout_constraintTop_toBottomOf="@id/contentPanel" /> + + diff --git a/uikit/src/main/res/values/attrs.xml b/uikit/src/main/res/values/attrs.xml index f8d9f8a9..dac03dbf 100644 --- a/uikit/src/main/res/values/attrs.xml +++ b/uikit/src/main/res/values/attrs.xml @@ -120,6 +120,7 @@ + @@ -380,6 +381,13 @@ + + + + + + + diff --git a/uikit/src/main/res/values/strings.xml b/uikit/src/main/res/values/strings.xml index 77b34cd9..109322fd 100644 --- a/uikit/src/main/res/values/strings.xml +++ b/uikit/src/main/res/values/strings.xml @@ -51,6 +51,10 @@ Can\'t read this notification. No notifications Something went wrong. + Provide additional feedback (optional) + Leave a comment + Edit comment + Remove feedback Moderations @@ -81,6 +85,10 @@ Uploading… Downloading… File saved + Changes saved. + Couldn\'t submit. Try again. + Couldn\'t save. Try again. + Couldn\'t delete. Try again. Edit Invite Cancel @@ -91,6 +99,7 @@ Selected Add OK + Submit diff --git a/uikit/src/main/res/values/styles.xml b/uikit/src/main/res/values/styles.xml index ed2b9e65..f3a39a51 100755 --- a/uikit/src/main/res/values/styles.xml +++ b/uikit/src/main/res/values/styles.xml @@ -279,6 +279,7 @@ @style/Widget.Sendbird.Message.Other.VoiceMessage @style/Widget.Sendbird.Message.Me.MultipleFilesMessage @style/Widget.Sendbird.Message.Other.MultipleFilesMessage + @style/Widget.Sendbird.Feedback + + + +