From 334383def40ddf4ee3e4844d314734f1c45fae38 Mon Sep 17 00:00:00 2001 From: "sha.sdk_deployment" Date: Fri, 7 Jun 2024 08:46:44 +0000 Subject: [PATCH] Added v3.17.1 --- CHANGELOG.md | 4 +- gradle.properties | 2 +- .../CustomizationHomeActivity.kt | 19 +- .../customization/OpenChannelRepository.kt | 32 ++++ .../ModerationGroupChannelSample.kt | 171 +++++++++++++++++ .../moderation/ModerationOpenChannelSample.kt | 173 ++++++++++++++++++ .../src/main/res/layout/activity_login.xml | 4 +- uikit-samples/src/main/res/values/strings.xml | 15 ++ uikit/build.gradle | 2 +- .../com/sendbird/uikit/consts/StringSet.kt | 1 + .../internal/extensions/ChannelExtensions.kt | 3 + .../internal/extensions/MessageExtensions.kt | 14 ++ .../model/template_messages/Styles.kt | 7 +- .../singleton/BaseSharedPreference.kt | 6 + .../singleton/NotificationChannelManager.kt | 8 +- .../NotificationTemplateRepository.kt | 18 +- .../ui/messages/OtherUserMessageView.kt | 11 +- .../ui/widgets/MessageTemplateImageView.kt | 57 +++--- .../internal/ui/widgets/RoundCornerLayout.kt | 60 +++--- .../components/BaseMessageListComponent.java | 9 +- .../sendbird/uikit/utils/MessageUtils.java | 1 + 21 files changed, 543 insertions(+), 74 deletions(-) create mode 100644 uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/OpenChannelRepository.kt create mode 100644 uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/moderation/ModerationGroupChannelSample.kt create mode 100644 uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/moderation/ModerationOpenChannelSample.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index a4ad1969..8e696dee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Changelog -### v3.17.0 (May 23, 2024) with Chat SDK `v4.16.3` +### v3.17.1 (Jun 7, 2024) with Chat SDK `v4.16.4` +* Fixed an intermittent crash due to variable initialization when entering a notification channel. +* Optimized ChatBot streaming message animation.### v3.17.0 (May 23, 2024) with Chat SDK `v4.16.3` * Deprecated `notifyStatusUpdated(GroupChannel, StatusFrameView)` in `MessageThreadInputComponent` * Added a new interface to set the enable state of the message input field * Added `boolean tryToChangeEnableInputView(boolean, String)` in `ChannelFragment`, `MessageThreadFragment`, `OpenChannelFragment` diff --git a/gradle.properties b/gradle.properties index 26c9b6f3..3006f32e 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.17.0 +UIKIT_VERSION = 3.17.1 UIKIT_VERSION_CODE = 1 diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/CustomizationHomeActivity.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/CustomizationHomeActivity.kt index d332713b..4ee2481e 100644 --- a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/CustomizationHomeActivity.kt +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/CustomizationHomeActivity.kt @@ -29,6 +29,8 @@ import com.sendbird.uikit.samples.customization.global.showAdapterProvidersSampl import com.sendbird.uikit.samples.customization.global.showFragmentProvidersSample import com.sendbird.uikit.samples.customization.global.showModuleProvidersSample import com.sendbird.uikit.samples.customization.global.showViewModelProvidersSample +import com.sendbird.uikit.samples.customization.moderation.showModerationGroupChannelSample +import com.sendbird.uikit.samples.customization.moderation.showModerationOpenChannelSample import com.sendbird.uikit.samples.customization.userlist.showCustomMemberContextMenuSample import com.sendbird.uikit.samples.customization.userlist.showUserItemDataSourceSample import com.sendbird.uikit.samples.customization.userlist.showUserItemFilteringSample @@ -177,7 +179,22 @@ class CustomizationHomeActivity : ComponentActivity() { CustomizationItem( title = getString(R.string.text_title_custom_member_context_menu), description = getString(R.string.text_desc_custom_member_context_menu), - ) { showCustomMemberContextMenuSample(this) } + ) { showCustomMemberContextMenuSample(this) }, + // endregion + + // region user list customization + CustomizationItem( + isHeader = true, + title = getString(R.string.text_title_moderation) + ), + CustomizationItem( + title = getString(R.string.text_title_moderation_group_channel), + description = getString(R.string.text_moderation_custom_group_channel_sample), + ) { showModerationGroupChannelSample(activity = this) }, + CustomizationItem( + title = getString(R.string.text_title_moderation_open_channel), + description = getString(R.string.text_moderation_custom_open_channel_sample), + ) { showModerationOpenChannelSample(activity = this) }, // endregion ) diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/OpenChannelRepository.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/OpenChannelRepository.kt new file mode 100644 index 00000000..84d3c64d --- /dev/null +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/OpenChannelRepository.kt @@ -0,0 +1,32 @@ +package com.sendbird.uikit.samples.customization + +import android.app.Activity +import com.sendbird.android.channel.OpenChannel +import com.sendbird.android.params.OpenChannelListQueryParams +import com.sendbird.uikit.samples.common.widgets.WaitingDialog +import com.sendbird.uikit.utils.ContextUtils +import java.util.concurrent.Executors + +internal object OpenChannelRepository { + private val worker = Executors.newSingleThreadExecutor() + private var channelCache = mutableListOf() + + fun getRandomChannel(activity: Activity, callback: (OpenChannel) -> Unit) { + if (channelCache.isNotEmpty()) { + callback(channelCache.random()) + return + } + WaitingDialog.show(activity) + worker.submit { + OpenChannel.createOpenChannelListQuery(OpenChannelListQueryParams()).next { channels, e -> + WaitingDialog.dismiss() + if (e != null || channels.isNullOrEmpty()) { + ContextUtils.toastError(activity, "No channels") + return@next + } + channelCache.addAll(channels) + activity.runOnUiThread { callback(channelCache.random()) } + } + } + } +} diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/moderation/ModerationGroupChannelSample.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/moderation/ModerationGroupChannelSample.kt new file mode 100644 index 00000000..1eb43ea6 --- /dev/null +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/moderation/ModerationGroupChannelSample.kt @@ -0,0 +1,171 @@ +package com.sendbird.uikit.samples.customization.moderation + +import android.app.Activity +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.sendbird.android.channel.ReportCategory +import com.sendbird.android.exception.SendbirdException +import com.sendbird.android.message.BaseMessage +import com.sendbird.android.message.SendingStatus +import com.sendbird.uikit.activities.ChannelActivity +import com.sendbird.uikit.activities.viewholder.MessageType +import com.sendbird.uikit.activities.viewholder.MessageViewHolderFactory +import com.sendbird.uikit.fragments.ChannelFragment +import com.sendbird.uikit.interfaces.OnCompleteHandler +import com.sendbird.uikit.interfaces.providers.ChannelFragmentProvider +import com.sendbird.uikit.interfaces.providers.ChannelViewModelProvider +import com.sendbird.uikit.model.DialogListItem +import com.sendbird.uikit.providers.FragmentProviders +import com.sendbird.uikit.providers.ViewModelProviders +import com.sendbird.uikit.samples.R +import com.sendbird.uikit.samples.customization.GroupChannelRepository +import com.sendbird.uikit.utils.MessageUtils +import com.sendbird.uikit.vm.ChannelViewModel +import com.sendbird.uikit.vm.ViewModelFactory +import java.util.Arrays +import java.util.Objects + +fun showModerationGroupChannelSample(activity: Activity) { + setModerationGroupChannelViewModelProviders() + + FragmentProviders.channel = ChannelFragmentProvider { channelUrl, args -> + ChannelFragment.Builder(channelUrl).withArguments(args) + .setCustomFragment(ModerationGroupChannelFragment()) + .setUseHeader(true) + .build() + } + + GroupChannelRepository.getRandomChannel(activity) { channel -> + activity.startActivity( + ChannelActivity.newIntent(activity, channel.url) + ) + } +} + +class ModerationGroupChannelFragment : ChannelFragment() { + override fun makeMessageContextMenu(message: BaseMessage): MutableList { + val items: MutableList = super.makeMessageContextMenu(message) + val status = message.sendingStatus + if (status == SendingStatus.PENDING) return items + + val type = MessageViewHolderFactory.getMessageType(message) + val report = DialogListItem(R.string.text_report, R.drawable.icon_error) + + when (type) { + MessageType.VIEW_TYPE_USER_MESSAGE_ME -> if (status == SendingStatus.SUCCEEDED) { + items.add(report) + } + MessageType.VIEW_TYPE_USER_MESSAGE_OTHER -> { + items.add(report) + } + else -> {} + } + + return items + } + + override fun onMessageContextMenuItemClicked(message: BaseMessage, view: View, position: Int, item: DialogListItem): Boolean { + val key = item.key + + if (key == R.string.text_report) { + showSelectReportCategory(message) + return true + } + + super.onMessageContextMenuItemClicked(message, view, position, item) + + return false + } + + private fun showSelectReportCategory(message: BaseMessage) { + if (context == null) return + + val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext()) + var category: ReportCategory + + builder.setTitle(getString(R.string.text_choose_report_category_dialog)) + .setNegativeButton(R.string.text_cancel) { dialog, which -> + dialog.dismiss() + } + .setItems( + arrayOf( + getString(R.string.text_report_suspicious), + getString(R.string.text_report_harassing), + getString(R.string.text_report_spam), + getString(R.string.text_report_inappropriate) + )) { dialog, which -> + when (which) { + 0 -> { + category = ReportCategory.SUSPICIOUS + } + + 1 -> { + category = ReportCategory.HARASSING + } + + 2 -> { + category = ReportCategory.SPAM + } + + else -> { + category = ReportCategory.INAPPROPRIATE + } + } + + reportMessage(message, category, "") + } + + val dialog: AlertDialog = builder.create() + dialog.show() + } + + private fun reportMessage(message: BaseMessage, reportCategory: ReportCategory, reason: String) { + (viewModel as ModerationGroupChannelViewModel).reportMessage( + message, + reportCategory, + reason + ) { e: SendbirdException? -> + if (e == null) { + toastSuccess(R.string.sb_view_toast_success_description, false) + } else { + toastError(R.string.text_report_error, false) + } + } + } +} + +class ModerationGroupChannelViewModel(channelUrl: String) : ChannelViewModel(channelUrl, null) { + fun reportMessage(message: BaseMessage, reportCategory: ReportCategory, reason: String, handler: OnCompleteHandler?) { + if (channel == null) return + channel?.let { + it.reportMessage(message, reportCategory, reason) { e: SendbirdException? -> handler?.onComplete(e) } + } + } +} + + +fun setModerationGroupChannelViewModelProviders() { + @Suppress("UNCHECKED_CAST") + class ModerationGroupChannelViewModelFactory(private vararg val params: Any?) : ViewModelFactory(*params) { + override fun create(modelClass: Class): T { + if (modelClass == ModerationGroupChannelViewModel::class.java) { + return ModerationGroupChannelViewModel( + (Objects.requireNonNull(params)[0] as String) + ) as T + } + + return super.create(modelClass) + } + } + + ViewModelProviders.channel = ChannelViewModelProvider { owner, channelUrl, _, _ -> + ViewModelProvider( + owner, + ModerationGroupChannelViewModelFactory(channelUrl) + )[channelUrl, ModerationGroupChannelViewModel::class.java] + } +} diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/moderation/ModerationOpenChannelSample.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/moderation/ModerationOpenChannelSample.kt new file mode 100644 index 00000000..d794dc8e --- /dev/null +++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/moderation/ModerationOpenChannelSample.kt @@ -0,0 +1,173 @@ +package com.sendbird.uikit.samples.customization.moderation + +import android.app.Activity +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import com.sendbird.android.channel.ReportCategory +import com.sendbird.android.exception.SendbirdException +import com.sendbird.android.message.BaseMessage +import com.sendbird.android.message.SendingStatus +import com.sendbird.uikit.activities.OpenChannelActivity +import com.sendbird.uikit.activities.viewholder.MessageType +import com.sendbird.uikit.activities.viewholder.MessageViewHolderFactory +import com.sendbird.uikit.fragments.OpenChannelFragment +import com.sendbird.uikit.interfaces.OnCompleteHandler +import com.sendbird.uikit.interfaces.providers.OpenChannelFragmentProvider +import com.sendbird.uikit.interfaces.providers.OpenChannelViewModelProvider +import com.sendbird.uikit.model.DialogListItem +import com.sendbird.uikit.providers.FragmentProviders +import com.sendbird.uikit.providers.ViewModelProviders +import com.sendbird.uikit.samples.R +import com.sendbird.uikit.samples.customization.OpenChannelRepository +import com.sendbird.uikit.utils.MessageUtils +import com.sendbird.uikit.vm.OpenChannelViewModel +import com.sendbird.uikit.vm.ViewModelFactory +import java.util.Arrays +import java.util.Objects + +fun showModerationOpenChannelSample(activity: Activity) { + setModerationOpenChannelViewModelProviders() + + FragmentProviders.openChannel = OpenChannelFragmentProvider { channelUrl, args -> + OpenChannelFragment.Builder(channelUrl).withArguments(args) + .setCustomFragment(ModerationOpenChannelFragment()) + .setUseHeader(true) + .build() + + } + + OpenChannelRepository.getRandomChannel(activity) { channel -> + activity.startActivity( + OpenChannelActivity.newIntent(activity, OpenChannelActivity::class.java, channel.url) + ) + } + +} + + +fun setModerationOpenChannelViewModelProviders() { + @Suppress("UNCHECKED_CAST") + class ModerationOpenChannelViewModelFactory(private vararg val params: Any?) : ViewModelFactory(*params) { + override fun create(modelClass: Class): T { + if (modelClass == ModerationOpenChannelViewModel::class.java) { + return ModerationOpenChannelViewModel( + (Objects.requireNonNull(params)[0] as String) + ) as T + } + return super.create(modelClass) + } + } + + ViewModelProviders.openChannel = OpenChannelViewModelProvider { owner, channelUrl, _ -> + ViewModelProvider( + owner, + ModerationOpenChannelViewModelFactory(channelUrl) + )[channelUrl, ModerationOpenChannelViewModel::class.java] + } +} + +class ModerationOpenChannelFragment : OpenChannelFragment() { + override fun makeMessageContextMenu(message: BaseMessage): MutableList { + val items: MutableList = super.makeMessageContextMenu(message) + val status = message.sendingStatus + if (status == SendingStatus.PENDING) return items + + val type = MessageViewHolderFactory.getMessageType(message) + val report = DialogListItem(R.string.text_report, R.drawable.icon_error) + + when (type) { + MessageType.VIEW_TYPE_USER_MESSAGE_ME -> if (status == SendingStatus.SUCCEEDED) { + items.add(report) + } + MessageType.VIEW_TYPE_USER_MESSAGE_OTHER -> { + items.add(report) + } + else -> {} + } + + return items + } + + override fun onMessageContextMenuItemClicked(message: BaseMessage, view: View, position: Int, item: DialogListItem): Boolean { + val key = item.key + + if (key == R.string.text_report) { + showSelectReportCategory(message) + return true + } + + super.onMessageContextMenuItemClicked(message, view, position, item) + + return false + } + + private fun showSelectReportCategory(message: BaseMessage) { + if (context == null) return + + val builder: AlertDialog.Builder = AlertDialog.Builder(requireContext()) + var category: ReportCategory + + builder.setTitle(getString(R.string.text_choose_report_category_dialog)) + .setNegativeButton(R.string.text_cancel) { dialog, which -> + dialog.dismiss() + } + .setItems( + arrayOf( + getString(R.string.text_report_suspicious), + getString(R.string.text_report_harassing), + getString(R.string.text_report_spam), + getString(R.string.text_report_inappropriate) + )) { dialog, which -> + when (which) { + 0 -> { + category = ReportCategory.SUSPICIOUS + } + + 1 -> { + category = ReportCategory.HARASSING + } + + 2 -> { + category = ReportCategory.SPAM + } + + else -> { + category = ReportCategory.INAPPROPRIATE + } + } + + reportMessage(message, category, "") + } + + val dialog: AlertDialog = builder.create() + dialog.show() + } + + private fun reportMessage(message: BaseMessage, reportCategory: ReportCategory, reason: String) { + (viewModel as ModerationOpenChannelViewModel).reportMessage( + message, + reportCategory, + reason + ) { e: SendbirdException? -> + if (e == null) { + toastSuccess(R.string.sb_view_toast_success_description, module.params.shouldUseOverlayMode()) + } else { + toastError(R.string.text_report_error, module.params.shouldUseOverlayMode()) + } + } + } +} + + +class ModerationOpenChannelViewModel(channelUrl: String) : OpenChannelViewModel(channelUrl, null) { + fun reportMessage(message: BaseMessage, reportCategory: ReportCategory, reason: String, handler: OnCompleteHandler?) { + if (channel == null) return + channel?.let { + it.reportMessage(message, reportCategory, reason) { e: SendbirdException? -> handler?.onComplete(e) } + } + } +} diff --git a/uikit-samples/src/main/res/layout/activity_login.xml b/uikit-samples/src/main/res/layout/activity_login.xml index 7bbc24c5..bb740f55 100644 --- a/uikit-samples/src/main/res/layout/activity_login.xml +++ b/uikit-samples/src/main/res/layout/activity_login.xml @@ -170,7 +170,9 @@ android:hint="@string/text_hint_user_id" android:maxLines="1" android:textCursorDrawable="@drawable/shape_cursor_drawable" - android:textSize="@dimen/sb_text_size_16"/> + android:textSize="@dimen/sb_text_size_16"> + + diff --git a/uikit-samples/src/main/res/values/strings.xml b/uikit-samples/src/main/res/values/strings.xml index 45ff0422..9f20e7d9 100644 --- a/uikit-samples/src/main/res/values/strings.xml +++ b/uikit-samples/src/main/res/values/strings.xml @@ -130,12 +130,27 @@ Retrieve user data from external sources. Customize member context menu Provide a way to make custom member context menus + Create random group channel, test record feature. + Create random open channel, test record feature. + + + Report + Moderation Feature Customization + Moderation feature for Group Channel + Moderation feature for Open Channel + Suspicious + Harassing + Spam + Inappropriate + Couldn\'t report message. #%d. %s Resend Delete + Cancel Custom Header Thumbs Up Thumbs Down + Choose the report category diff --git a/uikit/build.gradle b/uikit/build.gradle index d568cab0..a7406ea7 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.16.3' + api 'com.sendbird.sdk:sendbird-chat:4.16.4' 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/consts/StringSet.kt b/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt index bf9cfde0..1852adeb 100644 --- a/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt +++ b/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt @@ -106,6 +106,7 @@ object StringSet { const val DEFAULT_CHANNEL_COVER_URL = "https://static.sendbird.com/sample/cover/cover_" const val Voice_message = "Voice_message" const val message = "message" + const val stream = "stream" // attributes list const val reactions = "reactions" 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 2c02101b..7b207789 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 @@ -7,3 +7,6 @@ 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() } + +internal val GroupChannel.containsBot: Boolean + get() = this.hasBot || this.hasAiBot 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 137c79aa..5a482b72 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 @@ -7,6 +7,7 @@ import com.sendbird.android.message.BaseMessage import com.sendbird.android.message.FileMessage import com.sendbird.android.message.FormField import com.sendbird.android.message.MultipleFilesMessage +import com.sendbird.android.shadow.com.google.gson.JsonParser import com.sendbird.uikit.R import com.sendbird.uikit.consts.StringSet import com.sendbird.uikit.internal.singleton.MessageDisplayDataManager @@ -105,3 +106,16 @@ internal var BaseMessage.shouldShowSuggestedReplies: Boolean set(value) { this.extras[StringSet.should_show_suggested_replies] = value } + +internal val BaseMessage.isStreamMessage: Boolean + get() { + val data = this.data + if (data.isBlank()) { + return false + } + return try { + JsonParser.parseString(data).asJsonObject[StringSet.stream].asBoolean + } catch (e: Exception) { + false + } + } 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 9b4b72cd..1468c52f 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 @@ -2,7 +2,6 @@ package com.sendbird.uikit.internal.model.template_messages import android.graphics.Color import android.graphics.PorterDuff -import android.graphics.drawable.GradientDrawable import android.util.TypedValue import android.view.View import android.widget.ImageView @@ -60,13 +59,9 @@ internal data class ViewStyle( val padding: Padding? = null ) { fun apply(view: View, useRipple: Boolean = false): ViewStyle { - val resources = view.context.resources backgroundImageUrl?.let { view.loadToBackground(it, radius ?: 0, useRipple) } if (backgroundColor != null || (borderWidth != null && borderWidth > 0)) { - view.background = GradientDrawable().apply { - backgroundColor?.let { setColor(it) } - cornerRadius = resources.intToDp(radius ?: 0).toFloat() - } + view.setBackgroundColor(backgroundColor ?: Color.TRANSPARENT) } margin?.apply(view) diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/BaseSharedPreference.kt b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/BaseSharedPreference.kt index 2567af98..e91dfc19 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/BaseSharedPreference.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/BaseSharedPreference.kt @@ -46,5 +46,11 @@ internal class BaseSharedPreference( fun getLong(key: String): Long = preferences.getLong(key, 0L) + fun putInt(key: String, value: Int) { + preferences.edit().putInt(key, value).apply() + } + + fun getInt(key: String): Int = preferences.getInt(key, 0) + fun contains(key: String): Boolean = preferences.contains(key) } 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 8ee8dee2..279226eb 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 @@ -32,7 +32,9 @@ internal object NotificationChannelManager { * 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. */ + @Synchronized internal fun checkAndInit(context: Context) { + Logger.i(">> NotificationChannelManager::checkAndInit() isInitialized=${isInitialized.get()}") if (!isInitialized.get()) { init(context) } @@ -41,10 +43,12 @@ internal object NotificationChannelManager { @JvmStatic @Synchronized fun init(context: Context) { - if (isInitialized.getAndSet(true)) return + Logger.d("++ NotificationChannelManager init start ${Thread.currentThread().name}, isInitialized=${isInitialized.get()}") + if (isInitialized.get()) return worker.submit { - templateRepository = NotificationTemplateRepository(context.applicationContext) channelSettingsRepository = NotificationChannelRepository(context.applicationContext) + templateRepository = NotificationTemplateRepository(context.applicationContext) + isInitialized.set(true) }.get() } diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/NotificationTemplateRepository.kt b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/NotificationTemplateRepository.kt index 4bb328df..e7e9254c 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/NotificationTemplateRepository.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/NotificationTemplateRepository.kt @@ -14,7 +14,9 @@ import java.util.concurrent.atomic.AtomicReference private const val TEMPLATE_KEY_PREFIX = "SB_TEMPLATE_" private const val LAST_UPDATED_TEMPLATE_LIST_TOKEN = "LAST_UPDATED_TEMPLATE_LIST_AT" +private const val TEMPLATE_COUNT = "TEMPLATE_COUNT" private const val PREFERENCE_FILE_NAME = "com.sendbird.notifications.templates" +private const val MAX_CACHED_TEMPLATE_COUNT = 1000 internal class NotificationTemplateRepository(context: Context) { private val templateCache: MutableMap = ConcurrentHashMap() @@ -34,21 +36,34 @@ internal class NotificationTemplateRepository(context: Context) { } init { + checkCountLimit() preferences.loadAll({ key -> key.startsWith(TEMPLATE_KEY_PREFIX) }, { key, value -> templateCache[key] = NotificationTemplate.fromJson(value.toString()) - }) + }).also { + preferences.putInt(TEMPLATE_COUNT, templateCache.size) + } + } + + private fun checkCountLimit() { + val count = preferences.getInt(TEMPLATE_COUNT) + Logger.d("++ cached template count = $count") + if (count > MAX_CACHED_TEMPLATE_COUNT) { + clearAll() + } } private fun getTemplateKey(key: String) = "${TEMPLATE_KEY_PREFIX}$key" @WorkerThread + @Synchronized private fun saveToCache(template: NotificationTemplate) { Logger.d(">> NotificationTemplateRepository::saveToCache() key=${template.templateKey}") val key = getTemplateKey(template.templateKey) templateCache[key] = template preferences.putString(key, template.toString()) + preferences.putInt(TEMPLATE_COUNT, templateCache.size) } fun needToUpdateTemplateList(latestUpdatedToken: String?): Boolean { @@ -89,6 +104,7 @@ internal class NotificationTemplateRepository(context: Context) { latch.await() error?.let { throw it } return result.get().also { + Logger.i("++ request response template list size=${it.templates.size}") it?.templates?.forEach { template -> // convert list to map saveToCache(template) 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 ba943a88..5ebe4908 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 @@ -18,6 +18,7 @@ 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.extensions.isStreamMessage import com.sendbird.uikit.internal.extensions.shouldShowSuggestedReplies import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener import com.sendbird.uikit.internal.ui.widgets.OnLinkLongClickListener @@ -185,7 +186,15 @@ internal class OtherUserMessageView @JvmOverloads internal constructor( ViewUtils.drawNickname(binding.tvNickname, message, messageUIConfig, false) if (enableOgTag) ViewUtils.drawOgtag(binding.ovOgtag, message.ogMetaData) ViewUtils.drawReactionEnabled(binding.rvEmojiReactionList, channel, params.channelConfig) - ViewUtils.drawProfile(binding.ivProfileView, message) + + val skipUpdateProfile: Boolean = message.sender?.isBot == true && + message.isStreamMessage && + binding.ivProfileView.drawable != null + + if (!skipUpdateProfile) { + ViewUtils.drawProfile(binding.ivProfileView, message) + } + ViewUtils.drawSentAt(binding.tvSentAt, message, messageUIConfig) val paddingTop = diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/MessageTemplateImageView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/MessageTemplateImageView.kt index f8245a6a..78188aa2 100644 --- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/MessageTemplateImageView.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/MessageTemplateImageView.kt @@ -2,7 +2,6 @@ package com.sendbird.uikit.internal.ui.widgets import android.content.Context import android.graphics.Canvas -import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.graphics.RectF @@ -30,15 +29,23 @@ internal open class MessageTemplateImageView @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttr: Int = 0 ) : AppCompatImageView(context, attrs, defStyleAttr), ViewRoundable { - private lateinit var rectF: RectF private val path: Path = Path() - private var strokePaint: Paint? = null + private val rectF = RectF() override var radius: Float = 0F private var imageRatio: Float = 0F private var targetWidth: Int = 0 private var targetHeight: Int = 0 var viewParams: ViewParams? = null + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + setWillNotDraw(false) + } + fun setSize(width: Int, height: Int) { if (width > 0 && height > 0) { this.targetWidth = width @@ -48,25 +55,15 @@ internal open class MessageTemplateImageView @JvmOverloads constructor( } } - init { - setBorder(0, Color.TRANSPARENT) - } - override fun setRadiusIntSize(radius: Int) { this.radius = context.resources.intToDp(radius).toFloat() + invalidate() } final override fun setBorder(borderWidth: Int, @ColorInt borderColor: Int) { - if (borderWidth <= 0) - strokePaint = null - else { - strokePaint = Paint().apply { - style = Paint.Style.STROKE - isAntiAlias = true - strokeWidth = context.resources.intToDp(borderWidth).toFloat() - color = borderColor - } - } + borderPaint.color = borderColor + borderPaint.strokeWidth = context.resources.intToDp(borderWidth).toFloat() + invalidate() } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { @@ -79,29 +76,29 @@ internal open class MessageTemplateImageView @JvmOverloads constructor( post { this.visibility = visibility } - - rectF = RectF(0f, 0f, w.toFloat(), h.toFloat()) - resetPath() } override fun draw(canvas: Canvas) { + rectF.set(0f, 0f, width.toFloat(), height.toFloat()) + + // clip the imageview with round corner + path.reset() + path.addRoundRect(rectF, radius, radius, Path.Direction.CW) val save = canvas.save() canvas.clipPath(path) + + // draw the buffer and restore canvas settings. super.draw(canvas) - strokePaint?.let { - val inlineWidth = it.strokeWidth - rectF.set(inlineWidth / 2, inlineWidth / 2, width - inlineWidth / 2, height - inlineWidth / 2) - canvas.drawRoundRect(rectF, radius, radius, it) + // draw border + val hasBorder = borderPaint.strokeWidth > 0 + if (hasBorder) { + val halfBorder: Float = borderPaint.strokeWidth / 2 + rectF.inset(halfBorder, halfBorder) + canvas.drawRect(rectF, borderPaint) } canvas.restoreToCount(save) } - private fun resetPath() { - path.reset() - path.addRoundRect(rectF, radius, radius, Path.Direction.CW) - path.close() - } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { super.onMeasure(widthMeasureSpec, heightMeasureSpec) diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/RoundCornerLayout.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/RoundCornerLayout.kt index 64eebc69..42f25814 100755 --- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/RoundCornerLayout.kt +++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/RoundCornerLayout.kt @@ -2,7 +2,6 @@ package com.sendbird.uikit.internal.ui.widgets import android.content.Context import android.graphics.Canvas -import android.graphics.Color import android.graphics.Paint import android.graphics.Path import android.graphics.RectF @@ -21,28 +20,25 @@ internal open class RoundCornerLayout @JvmOverloads constructor( ) : LinearLayout(context, attrs, defStyleAttr), ViewRoundable { private val rectF: RectF = RectF() private val path: Path = Path() - private var strokePaint: Paint? = null override var radius: Float = 0F + private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.STROKE + } - init { - setBorder(0, Color.TRANSPARENT) + override fun onAttachedToWindow() { + super.onAttachedToWindow() + setWillNotDraw(false) } override fun setRadiusIntSize(radius: Int) { this.radius = context.resources.intToDp(radius).toFloat() + invalidate() } final override fun setBorder(borderWidth: Int, @ColorInt borderColor: Int) { - if (borderWidth <= 0) - strokePaint = null - else { - strokePaint = Paint().apply { - style = Paint.Style.STROKE - isAntiAlias = true - strokeWidth = context.resources.intToDp(borderWidth).toFloat() - color = borderColor - } - } + borderPaint.color = borderColor + borderPaint.strokeWidth = context.resources.intToDp(borderWidth).toFloat() + invalidate() } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { @@ -57,25 +53,33 @@ internal open class RoundCornerLayout @JvmOverloads constructor( this.visibility = visibility } } - rectF.set(0f, 0f, w.toFloat(), h.toFloat()) - resetPath() } override fun draw(canvas: Canvas) { - val save = canvas.save() - canvas.clipPath(path) - super.draw(canvas) - strokePaint?.let { - val inlineWidth = it.strokeWidth - rectF.set(inlineWidth / 2, inlineWidth / 2, width - inlineWidth / 2, height - inlineWidth / 2) - canvas.drawRoundRect(rectF, radius, radius, it) + rectF.set(0f, 0f, width.toFloat(), height.toFloat()) + var save: Int? = null + if (radius > 0) { + path.reset() + path.addRoundRect(rectF, radius, radius, Path.Direction.CW) + save = canvas.save() + canvas.clipPath(path) } - canvas.restoreToCount(save) + + val hasBorder = borderPaint.strokeWidth > 0 + val halfBorder: Float = borderPaint.strokeWidth / 2 + if (radius > 0 || hasBorder) { + rectF.inset(halfBorder, halfBorder) + } + + super.draw(canvas) + save?.let { canvas.restoreToCount(it) } } - private fun resetPath() { - path.reset() - path.addRoundRect(rectF, radius, radius, Path.Direction.CW) - path.close() + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + val hasBorder = borderPaint.strokeWidth > 0 + if (hasBorder) { + canvas.drawRoundRect(rectF, radius, radius, borderPaint) + } } } 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 34c3cc2b..c02363cb 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 @@ -33,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.extensions.ChannelExtensionsKt; import com.sendbird.uikit.internal.interfaces.OnFeedbackRatingClickListener; import com.sendbird.uikit.internal.ui.widgets.InnerLinearLayoutManager; import com.sendbird.uikit.internal.ui.widgets.MessageRecyclerView; @@ -60,6 +61,7 @@ abstract public class BaseMessageListComponent onScrollEndReaches(direction, messageRecyclerView)); @@ -484,6 +486,11 @@ public void notifyChannelChanged(@NonNull GroupChannel channel) { if (params.shouldUseBanner()) { drawFrozenBanner(channel.isFrozen()); } + if (ChannelExtensionsKt.getContainsBot(channel)) { + messageRecyclerView.getRecyclerView().setItemAnimator(null); + } else { + messageRecyclerView.getRecyclerView().setItemAnimator(itemAnimator); + } } /** diff --git a/uikit/src/main/java/com/sendbird/uikit/utils/MessageUtils.java b/uikit/src/main/java/com/sendbird/uikit/utils/MessageUtils.java index 9177572c..1285c0af 100644 --- a/uikit/src/main/java/com/sendbird/uikit/utils/MessageUtils.java +++ b/uikit/src/main/java/com/sendbird/uikit/utils/MessageUtils.java @@ -12,6 +12,7 @@ import com.sendbird.android.message.MessageMetaArray; import com.sendbird.android.message.SendingStatus; import com.sendbird.android.message.UserMessage; +import com.sendbird.android.shadow.com.google.gson.JsonParser; import com.sendbird.android.user.User; import com.sendbird.uikit.activities.viewholder.MessageType; import com.sendbird.uikit.activities.viewholder.MessageViewHolderFactory;