diff --git a/CHANGELOG.md b/CHANGELOG.md
index a8ce0850..c46270f9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,38 @@
# Changelog
+### v3.16.0 (Apr 25, 2024) with Chat SDK `v4.16.2`
+Support a way to customise the menu items in the `ChannelSettingsMenuComponent` and `OpenChannelSettingsMenuComponent`.
+- Added `createMenuView(Context, String, String, SingleMenuType, int, int)` in `ChannelSettingsMenuComponent`
+- Added `setMenuList(List
, MenuViewProvider) in `ChannelSettingsMenuComponent.Params`
+- Added `MenuViewProvider` to ChannelSettings that allows you to create and make custom menus.
+
+* Simple example for creating custom menu.
+```kotlin
+ModuleProviders.channelSettings = ChannelSettingsModuleProvider { context, _ ->
+ ChannelSettingsModule(context).apply {
+ val customMenuList = ChannelSettingsMenuComponent.defaultMenuSet.toMutableList().apply {
+ add(ChannelSettingsMenuComponent.Menu.CUSTOM)
+ }
+ val component = ChannelSettingsMenuComponent().apply {
+ // set the custom menu list.
+ params.setMenuList(customMenuList) { context, _ -> // create custom menu view.
+ createMenuView(
+ context,
+ "Go to Chat",
+ null,
+ SingleMenuType.NONE,
+ R.drawable.icon_chat,
+ 0
+ )
+ }
+ }
+ setChannelSettingsMenuComponent(component)
+ }
+}
+```
+
+- Added `getActionContextMenuTitle(Member, GroupChannel)`, `makeActionContextMenu(Member, GroupChannel)`, and `onActionContextMenuItemClicked(Member, DialogListItem, GroupChannel)` in `MemberListFragment`
+- Added `getActionContextMenuTitle(User, OpenChannel)`, `makeActionContextMenu(User, OpenChannel)`, and `onActionContextMenuItemClicked(User, DialogListItem, OpenChannel)` in `MemberListFragment`
+- Added `Message template` feature for `GroupChannel`.
### v3.15.0 (Mar 28, 2024) with Chat SDK `v4.16.0`
* Added `sendLogViewed(List)` in `FeedNotificationChannelViewModel`.
* Deprecated `sendLogImpression(List)` in `FeedNotificationChannelViewModel`.
diff --git a/gradle.properties b/gradle.properties
index 55f3e3a4..f38ee443 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.15.0
+UIKIT_VERSION = 3.16.0
UIKIT_VERSION_CODE = 1
diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/extensions/UIKitExtensions.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/extensions/UIKitExtensions.kt
index 49083c6c..021697d2 100644
--- a/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/extensions/UIKitExtensions.kt
+++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/common/extensions/UIKitExtensions.kt
@@ -40,7 +40,7 @@ internal fun SampleType?.getLogoDrawable(context: Context): Drawable? {
return when (this) {
null -> R.drawable.logo_sendbird
SampleType.Basic -> R.drawable.logo_sendbird
- SampleType.Notification -> R.drawable.logo_notifications
+ SampleType.Notification -> R.drawable.logo_business_messaging
SampleType.Customization -> R.drawable.logo_sendbird
SampleType.AiChatBot -> R.drawable.logo_sendbird
}.let { ContextCompat.getDrawable(context, it) }
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 2959ea3d..d332713b 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
@@ -22,10 +22,14 @@ import com.sendbird.uikit.samples.customization.channel.showNewMessageTypeSample
import com.sendbird.uikit.samples.customization.channellist.showChannelItemFilteringSample
import com.sendbird.uikit.samples.customization.channellist.showChannelItemUISample
import com.sendbird.uikit.samples.customization.channellist.showNewChannelItemTypeSample
+import com.sendbird.uikit.samples.customization.channelsettings.showAppendNewCustomGroupChannelSettingsMenuSample
+import com.sendbird.uikit.samples.customization.channelsettings.showCustomGroupChannelSettingsMenuSample
+import com.sendbird.uikit.samples.customization.channelsettings.showHidingChannelSettingsMenuSample
import com.sendbird.uikit.samples.customization.global.showAdapterProvidersSample
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.userlist.showCustomMemberContextMenuSample
import com.sendbird.uikit.samples.customization.userlist.showUserItemDataSourceSample
import com.sendbird.uikit.samples.customization.userlist.showUserItemFilteringSample
import com.sendbird.uikit.samples.customization.userlist.showUserItemSelectSample
@@ -130,6 +134,25 @@ class CustomizationHomeActivity : ComponentActivity() {
) { showChannelItemFilteringSample(this) },
// endregion
+ // region channel settings customization
+ CustomizationItem(
+ isHeader = true,
+ title = getString(R.string.text_list_title_channel_settings)
+ ),
+ CustomizationItem(
+ title = getString(R.string.text_title_custom_channel_settings_menu_sample),
+ description = getString(R.string.text_desc_custom_channel_settings_menu_sample)
+ ) { showCustomGroupChannelSettingsMenuSample(this) },
+ CustomizationItem(
+ title = getString(R.string.text_title_append_new_channel_settings_menu_sample),
+ description = getString(R.string.text_desc_append_new_channel_settings_menu_sample)
+ ) { showAppendNewCustomGroupChannelSettingsMenuSample(this) },
+ CustomizationItem(
+ title = getString(R.string.text_title_hide_channel_settings_menu_sample),
+ description = getString(R.string.text_desc_hide_channel_settings_menu_sample)
+ ) { showHidingChannelSettingsMenuSample(this) },
+ // endregion
+
// region user list customization
CustomizationItem(
isHeader = true,
@@ -150,7 +173,11 @@ class CustomizationHomeActivity : ComponentActivity() {
CustomizationItem(
title = getString(R.string.text_title_user_item_custom_datasource),
description = getString(R.string.text_desc_user_item_custom_datasource),
- ) { showUserItemDataSourceSample(this) }
+ ) { showUserItemDataSourceSample(this) },
+ CustomizationItem(
+ title = getString(R.string.text_title_custom_member_context_menu),
+ description = getString(R.string.text_desc_custom_member_context_menu),
+ ) { showCustomMemberContextMenuSample(this) }
// endregion
)
diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/channelsettings/GroupChannelSettingSample.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/channelsettings/GroupChannelSettingSample.kt
new file mode 100644
index 00000000..7d0adc1b
--- /dev/null
+++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/channelsettings/GroupChannelSettingSample.kt
@@ -0,0 +1,109 @@
+package com.sendbird.uikit.samples.customization.channelsettings
+
+import android.app.Activity
+import com.sendbird.uikit.activities.ChannelSettingsActivity
+import com.sendbird.uikit.consts.SingleMenuType
+import com.sendbird.uikit.interfaces.providers.ChannelSettingsModuleProvider
+import com.sendbird.uikit.modules.ChannelSettingsModule
+import com.sendbird.uikit.modules.components.ChannelSettingsMenuComponent
+import com.sendbird.uikit.providers.ModuleProviders
+import com.sendbird.uikit.samples.R
+import com.sendbird.uikit.samples.customization.GroupChannelRepository
+
+fun showAppendNewCustomGroupChannelSettingsMenuSample(activity: Activity) {
+ // You can customize the Group Channel settings menu using the following code.
+ // The following code is an example of how to customize the Group Channel settings menu.
+ // If you want to handle the CUSTOM menu click event, you should handle it yourself after creating a custom menu view.
+ ModuleProviders.channelSettings = ChannelSettingsModuleProvider { context, _ ->
+ val module = ChannelSettingsModule(context).apply {
+ val customMenuList = ChannelSettingsMenuComponent.defaultMenuSet.toMutableList().apply {
+ add(ChannelSettingsMenuComponent.Menu.CUSTOM)
+ }
+ val component = ChannelSettingsMenuComponent().apply {
+ // set the custom menu list.
+ params.setMenuList(customMenuList) { context, _ -> // create custom menu view.
+ createMenuView(
+ context,
+ "Go to Chat",
+ null,
+ SingleMenuType.NEXT,
+ R.drawable.icon_chat,
+ 0
+ ).apply {
+ // set the click event listener here.
+ setOnClickListener {
+ println(">>>>>> Go to Chat")
+ }
+ }
+ }
+ }
+ setChannelSettingsMenuComponent(component)
+ }
+ module
+ }
+
+ GroupChannelRepository.getRandomChannel(activity) { channel ->
+ activity.startActivity(ChannelSettingsActivity.newIntent(activity, channel.url))
+ }
+}
+
+fun showCustomGroupChannelSettingsMenuSample(activity: Activity) {
+ // You can customize the Group Channel settings menu using the following code.
+ // It shows how to make custom menu items in the Group Channel settings menu.
+ // If you want to handle the CUSTOM menu click event, you should handle it yourself after creating a custom menu view.
+ ModuleProviders.channelSettings = ChannelSettingsModuleProvider { context, _ ->
+ val module = ChannelSettingsModule(context).apply {
+ val component = ChannelSettingsMenuComponent().apply {
+ // set the custom menu list.
+ params.setMenuList(
+ listOf(
+ ChannelSettingsMenuComponent.Menu.CUSTOM,
+ ChannelSettingsMenuComponent.Menu.MEMBERS,
+ ChannelSettingsMenuComponent.Menu.LEAVE_CHANNEL,
+ )
+ ) { context, _ -> // create custom menu view.
+ createMenuView(
+ context,
+ "Go to Chat",
+ null,
+ SingleMenuType.NEXT,
+ R.drawable.icon_chat,
+ 0
+ ).apply {
+ // set the click event listener here.
+ setOnClickListener {
+ println(">>>>>> Go to Chat")
+ }
+ }
+ }
+ }
+ setChannelSettingsMenuComponent(component)
+ }
+ module
+ }
+
+ GroupChannelRepository.getRandomChannel(activity) { channel ->
+ activity.startActivity(ChannelSettingsActivity.newIntent(activity, channel.url))
+ }
+}
+
+fun showHidingChannelSettingsMenuSample(activity: Activity) {
+ // It shows how to hide the default menu items in the Group Channel settings menu.
+ ModuleProviders.channelSettings = ChannelSettingsModuleProvider { context, _ ->
+ val module = ChannelSettingsModule(context).apply {
+ val customMenuList = ChannelSettingsMenuComponent.defaultMenuSet.toMutableList().apply {
+ remove(ChannelSettingsMenuComponent.Menu.LEAVE_CHANNEL)
+ }
+ val component = ChannelSettingsMenuComponent().apply {
+ // hide LEAVE_CHANNEL menu.
+ params.setMenuList(customMenuList, null)
+ }
+ setChannelSettingsMenuComponent(component)
+ }
+ module
+ }
+
+ GroupChannelRepository.getRandomChannel(activity) { channel ->
+ activity.startActivity(ChannelSettingsActivity.newIntent(activity, channel.url))
+ }
+}
diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/userlist/MemberContextMenuSample.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/userlist/MemberContextMenuSample.kt
new file mode 100644
index 00000000..f44b046a
--- /dev/null
+++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/customization/userlist/MemberContextMenuSample.kt
@@ -0,0 +1,50 @@
+package com.sendbird.uikit.samples.customization.userlist
+
+import android.app.Activity
+import com.sendbird.android.channel.GroupChannel
+import com.sendbird.android.user.Member
+import com.sendbird.uikit.activities.MemberListActivity
+import com.sendbird.uikit.fragments.MemberListFragment
+import com.sendbird.uikit.interfaces.providers.MemberListFragmentProvider
+import com.sendbird.uikit.model.DialogListItem
+import com.sendbird.uikit.providers.FragmentProviders
+import com.sendbird.uikit.samples.R
+import com.sendbird.uikit.samples.customization.GroupChannelRepository
+
+fun showCustomMemberContextMenuSample(activity: Activity) {
+ GroupChannelRepository.getRandomChannel(activity) { channel ->
+ FragmentProviders.memberList = MemberListFragmentProvider { channelUrl, _ ->
+ MemberListFragment.Builder(channelUrl)
+ .setCustomFragment(CustomMemberListFragment())
+ .build()
+ }
+ activity.startActivity(MemberListActivity.newIntent(activity, channel.url))
+ }
+}
+
+class CustomMemberListFragment : MemberListFragment() {
+ override fun getActionContextMenuTitle(member: Member, channel: GroupChannel?): String {
+ return "Custom Context Menu"
+ }
+
+ override fun makeActionContextMenu(member: Member, channel: GroupChannel?): MutableList {
+ return super.makeActionContextMenu(member, channel).apply {
+ add(DialogListItem(R.string.text_menu_thumbs_up, R.drawable.icon_good))
+ add(DialogListItem(R.string.text_menu_thumbs_down, R.drawable.icon_bad))
+ }
+ }
+
+ override fun onActionContextMenuItemClicked(member: Member, item: DialogListItem, channel: GroupChannel?): Boolean {
+ return when (item.key) {
+ R.string.text_menu_thumbs_up -> {
+ println(">>>>>> Thumbs Up")
+ true
+ }
+ R.string.text_menu_thumbs_down -> {
+ println(">>>>>> Thumbs Down")
+ true
+ }
+ else -> super.onActionContextMenuItemClicked(member, item, channel)
+ }
+ }
+}
diff --git a/uikit-samples/src/main/java/com/sendbird/uikit/samples/notification/NotificationHomeActivity.kt b/uikit-samples/src/main/java/com/sendbird/uikit/samples/notification/NotificationHomeActivity.kt
index a6247fe5..f3b9d8e0 100644
--- a/uikit-samples/src/main/java/com/sendbird/uikit/samples/notification/NotificationHomeActivity.kt
+++ b/uikit-samples/src/main/java/com/sendbird/uikit/samples/notification/NotificationHomeActivity.kt
@@ -44,7 +44,6 @@ class NotificationHomeActivity : ThemeHomeActivity() {
getFeedChannelUrl()
)
)
-
}
btSignOut.setOnClickListener { logout() }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
diff --git a/uikit-samples/src/main/res/drawable-hdpi/logo_business_messaging.png b/uikit-samples/src/main/res/drawable-hdpi/logo_business_messaging.png
new file mode 100644
index 00000000..896253ae
Binary files /dev/null and b/uikit-samples/src/main/res/drawable-hdpi/logo_business_messaging.png differ
diff --git a/uikit-samples/src/main/res/drawable-hdpi/logo_sendbird_full.png b/uikit-samples/src/main/res/drawable-hdpi/logo_sendbird_full.png
index b72e745d..8eed7c18 100644
Binary files a/uikit-samples/src/main/res/drawable-hdpi/logo_sendbird_full.png and b/uikit-samples/src/main/res/drawable-hdpi/logo_sendbird_full.png differ
diff --git a/uikit-samples/src/main/res/drawable-mdpi/logo_business_messaging.png b/uikit-samples/src/main/res/drawable-mdpi/logo_business_messaging.png
new file mode 100644
index 00000000..5d647f02
Binary files /dev/null and b/uikit-samples/src/main/res/drawable-mdpi/logo_business_messaging.png differ
diff --git a/uikit-samples/src/main/res/drawable-mdpi/logo_sendbird_full.png b/uikit-samples/src/main/res/drawable-mdpi/logo_sendbird_full.png
index c9296976..d0637a3c 100644
Binary files a/uikit-samples/src/main/res/drawable-mdpi/logo_sendbird_full.png and b/uikit-samples/src/main/res/drawable-mdpi/logo_sendbird_full.png differ
diff --git a/uikit-samples/src/main/res/drawable-xhdpi/logo_business_messaging.png b/uikit-samples/src/main/res/drawable-xhdpi/logo_business_messaging.png
new file mode 100644
index 00000000..6a2eca36
Binary files /dev/null and b/uikit-samples/src/main/res/drawable-xhdpi/logo_business_messaging.png differ
diff --git a/uikit-samples/src/main/res/drawable-xhdpi/logo_sendbird_full.png b/uikit-samples/src/main/res/drawable-xhdpi/logo_sendbird_full.png
index e93da43f..5ca72688 100644
Binary files a/uikit-samples/src/main/res/drawable-xhdpi/logo_sendbird_full.png and b/uikit-samples/src/main/res/drawable-xhdpi/logo_sendbird_full.png differ
diff --git a/uikit-samples/src/main/res/drawable-xxhdpi/logo_business_messaging.png b/uikit-samples/src/main/res/drawable-xxhdpi/logo_business_messaging.png
new file mode 100644
index 00000000..b2ca27c9
Binary files /dev/null and b/uikit-samples/src/main/res/drawable-xxhdpi/logo_business_messaging.png differ
diff --git a/uikit-samples/src/main/res/drawable-xxhdpi/logo_sendbird_full.png b/uikit-samples/src/main/res/drawable-xxhdpi/logo_sendbird_full.png
index 7404501a..89b1818e 100644
Binary files a/uikit-samples/src/main/res/drawable-xxhdpi/logo_sendbird_full.png and b/uikit-samples/src/main/res/drawable-xxhdpi/logo_sendbird_full.png differ
diff --git a/uikit-samples/src/main/res/drawable-xxxhdpi/logo_business_messaging.png b/uikit-samples/src/main/res/drawable-xxxhdpi/logo_business_messaging.png
new file mode 100644
index 00000000..36749215
Binary files /dev/null and b/uikit-samples/src/main/res/drawable-xxxhdpi/logo_business_messaging.png differ
diff --git a/uikit-samples/src/main/res/drawable-xxxhdpi/logo_sendbird_full.png b/uikit-samples/src/main/res/drawable-xxxhdpi/logo_sendbird_full.png
index 9454a4ab..7c4a5357 100644
Binary files a/uikit-samples/src/main/res/drawable-xxxhdpi/logo_sendbird_full.png and b/uikit-samples/src/main/res/drawable-xxxhdpi/logo_sendbird_full.png differ
diff --git a/uikit-samples/src/main/res/layout/activity_login.xml b/uikit-samples/src/main/res/layout/activity_login.xml
index f0837977..f131f689 100644
--- a/uikit-samples/src/main/res/layout/activity_login.xml
+++ b/uikit-samples/src/main/res/layout/activity_login.xml
@@ -18,7 +18,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/sb_size_56"
- android:background="@drawable/logo_notifications"
+ android:background="@drawable/logo_business_messaging"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
@@ -32,7 +32,7 @@
android:textSize="@dimen/sb_text_size_24"
android:textStyle="bold"
android:textColor="@color/onlight_01"
- android:text="@string/text_title_login_activity"
+ android:text="@string/text_title_uikit_sample_app"
app:layout_constraintTop_toBottomOf="@id/logoImageView"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@@ -168,12 +168,10 @@
android:layout_height="wrap_content"
android:text="@string/text_use_feed_channel_only"
android:buttonTint="@color/primary_300"
- android:paddingStart="@dimen/sb_size_8"
android:layout_marginTop="@dimen/sb_size_24"
app:layout_constraintTop_toBottomOf="@id/nicknameLayout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@id/signInButton"
- style="@android:style/Widget.DeviceDefault.Light.CompoundButton.RadioButton"
/>
+ android:text="@string/text_business_messaging"/>
+ tools:ignore="MissingTranslation,ExtraTranslation">
채널
diff --git a/uikit-samples/src/main/res/values/strings.xml b/uikit-samples/src/main/res/values/strings.xml
index bbfbee0c..b53932a9 100644
--- a/uikit-samples/src/main/res/values/strings.xml
+++ b/uikit-samples/src/main/res/values/strings.xml
@@ -1,12 +1,12 @@
-
+
UIKitSamples
Sendbird UIKit sample
- Basic Usage
+ Basic Usages
Talk to an AI Chatbot
Customizations
- Sendbird Notification
+ Business Messaging sample
UIKit sample app
UI Kit v%1$s SDK v%2$s
@@ -58,7 +58,7 @@
Go to README
- Notification Home
+ Business Messaging
Chat and Feed channels
Feed channel only
@@ -108,6 +108,15 @@
%s and %s are typing…
Several people are typing…
+
+ Channel Settings Customization
+ Append New Channel Settings Menu
+ Append new channel Settings menus.
+ Hide Default Channel Settings Menu
+ Hide default channel Settings menus.
+ Change The Whole Channel Settings Menu
+ Change the whole channel Settings menus.
+
User List Customization
User Item UI
@@ -118,10 +127,14 @@
Implement filtering for users based on a specific criteria for display.
User Item Custom Data Source
Retrieve user data from external sources.
+ Customize member context menu
+ Provide a way to make custom member context menus
#%d. %s
Resend
Delete
Custom Header
+ Thumbs Up
+ Thumbs Down
diff --git a/uikit/build.gradle b/uikit/build.gradle
index cf89a04e..2c3d48bf 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.0'
+ api 'com.sendbird.sdk:sendbird-chat:4.16.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/SendbirdUIKit.java b/uikit/src/main/java/com/sendbird/uikit/SendbirdUIKit.java
index ca52e2d9..dee14b07 100644
--- a/uikit/src/main/java/com/sendbird/uikit/SendbirdUIKit.java
+++ b/uikit/src/main/java/com/sendbird/uikit/SendbirdUIKit.java
@@ -14,6 +14,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.StyleRes;
import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
import androidx.appcompat.content.res.AppCompatResources;
import com.sendbird.android.AppInfo;
@@ -33,6 +34,7 @@
import com.sendbird.android.params.GroupChannelCreateParams;
import com.sendbird.android.params.InitParams;
import com.sendbird.android.params.UserUpdateParams;
+import com.sendbird.android.template.MessageTemplateInfo;
import com.sendbird.android.user.User;
import com.sendbird.uikit.activities.ChannelActivity;
import com.sendbird.uikit.adapter.SendbirdUIKitAdapter;
@@ -44,6 +46,7 @@
import com.sendbird.uikit.interfaces.CustomUserListQueryHandler;
import com.sendbird.uikit.interfaces.UserInfo;
import com.sendbird.uikit.internal.singleton.MessageDisplayDataManager;
+import com.sendbird.uikit.internal.singleton.MessageTemplateManager;
import com.sendbird.uikit.internal.singleton.NotificationChannelManager;
import com.sendbird.uikit.internal.singleton.UIKitConfigRepository;
import com.sendbird.uikit.internal.tasks.JobResultTask;
@@ -64,9 +67,14 @@
import org.jetbrains.annotations.TestOnly;
import java.io.OutputStream;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
/**
@@ -244,6 +252,7 @@ static void clearAll() {
UIKitPrefs.clearAll();
NotificationChannelManager.clearAll();
MessageDisplayDataManager.clearAll();
+ MessageTemplateManager.clearAll();
}
/**
@@ -320,6 +329,7 @@ public void onInitSucceed() {
FileUtils.removeDeletableDir(context.getApplicationContext());
UIKitPrefs.init(context.getApplicationContext());
NotificationChannelManager.init(context.getApplicationContext());
+ MessageTemplateManager.init(context.getApplicationContext());
EmojiManager.init();
}
@@ -611,23 +621,7 @@ public Pair call() throws Exception {
updateEmojiList();
}
- final NotificationInfo notificationInfo = appInfo.getNotificationInfo();
- if (notificationInfo != null && notificationInfo.isEnabled()) {
- // Even if the request fails, it should not affect the result of the connection request.
- try {
- // if the cache exists or no need to update, blocking is released right away
- final String latestToken = notificationInfo.getTemplateListToken();
- NotificationChannelManager.requestTemplateListBlocking(latestToken);
- } catch (Exception ignore) {
- }
- try {
- // if the cache exists or no need to update, blocking is released right away
- final long settingsUpdatedAt = notificationInfo.getSettingsUpdatedAt();
- NotificationChannelManager.requestNotificationChannelSettingBlocking(settingsUpdatedAt);
- } catch (Exception ignore) {
- }
- }
-
+ fetchTemplatesBlocking(sendbirdChat);
if (SendbirdUIKit.uikitConfigRepo != null) {
try {
SendbirdUIKit.uikitConfigRepo.requestConfigurationsBlocking(sendbirdChat, appInfo.getUiKitConfigInfo());
@@ -653,6 +647,55 @@ public void onResultForUiThread(@Nullable Pair data, @N
});
}
+ @WorkerThread
+ @VisibleForTesting
+ static void fetchTemplatesBlocking(@NonNull SendbirdChatContract sendbirdChat) {
+ final AppInfo appInfo = sendbirdChat.getAppInfo();
+ if (appInfo != null) {
+ ExecutorService executor = Executors.newFixedThreadPool(3); // 3 threads for 3 requests
+ List> tasks = new ArrayList<>();
+
+ final NotificationInfo notificationInfo = appInfo.getNotificationInfo();
+ if (notificationInfo != null && notificationInfo.isEnabled()) {
+ // Even if the request fails, it should not affect the result of the connection request.
+ tasks.add(Executors.callable(() -> {
+ try {
+ // if the cache exists or no need to update, blocking is released right away
+ final String latestToken = notificationInfo.getTemplateListToken();
+ NotificationChannelManager.requestTemplateListBlocking(latestToken);
+ } catch (Exception ignore) {
+ }
+ }));
+ tasks.add(Executors.callable(() -> {
+ try {
+ // if the cache exists or no need to update, blocking is released right away
+ final long settingsUpdatedAt = notificationInfo.getSettingsUpdatedAt();
+ NotificationChannelManager.requestNotificationChannelSettingBlocking(settingsUpdatedAt);
+ } catch (Exception ignore) {
+ }
+ }));
+ }
+
+ final MessageTemplateInfo messageTemplateInfo = appInfo.getMessageTemplateInfo();
+ if (messageTemplateInfo != null && messageTemplateInfo.getToken() != null) { // `token == null` means there are no templates in server.
+ tasks.add(Executors.callable(() -> {
+ try {
+ final String latestToken = messageTemplateInfo.getToken();
+ MessageTemplateManager.syncMessageTemplateListBlocking(latestToken);
+ } catch (Exception ignore) {
+ }
+ }));
+ }
+
+ try {
+ executor.invokeAll(tasks);
+ } catch (InterruptedException ignore) {
+ } finally {
+ executor.shutdown();
+ }
+ }
+ }
+
@NonNull
private static Pair connectBlocking(@NonNull SendbirdChatContract sendbirdChat) throws InterruptedException {
AtomicReference result = new AtomicReference<>();
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 5b9310e5..e9ea2cb0 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
@@ -14,12 +14,15 @@
import com.sendbird.android.user.User;
import com.sendbird.uikit.consts.MessageGroupType;
import com.sendbird.uikit.consts.ReplyType;
+import com.sendbird.uikit.internal.extensions.MessageTemplateExtensionsKt;
+import com.sendbird.uikit.internal.model.templates.MessageTemplateStatus;
import com.sendbird.uikit.model.MessageListUIParams;
import com.sendbird.uikit.model.TypingIndicatorMessage;
import com.sendbird.uikit.utils.MessageUtils;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
class MessageDiffCallback extends DiffUtil.Callback {
@NonNull
@@ -85,12 +88,18 @@ public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
}
}
- Map oldExtendedMessagePayload = oldMessage.getExtendedMessagePayload();
- Map newExtendedMessagePayload = newMessage.getExtendedMessagePayload();
+ final Map oldExtendedMessagePayload = oldMessage.getExtendedMessagePayload();
+ final Map newExtendedMessagePayload = newMessage.getExtendedMessagePayload();
if (!oldExtendedMessagePayload.equals(newExtendedMessagePayload)) {
return false;
}
+ final MessageTemplateStatus oldMessageTemplateStatus = MessageTemplateExtensionsKt.getMessageTemplateStatus(oldMessage);
+ final MessageTemplateStatus newMessageTemplateStatus = MessageTemplateExtensionsKt.getMessageTemplateStatus(newMessage);
+ if (!Objects.equals(oldMessageTemplateStatus, newMessageTemplateStatus)) {
+ return false;
+ }
+
if (messageListUIParams.shouldUseMessageReceipt()) {
if (oldChannel.getUnreadMemberCount(newMessage) != newChannel.getUnreadMemberCount(newMessage)) {
return false;
diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageListAdapter.java
index eb34572c..92b9d386 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
@@ -2,6 +2,8 @@
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
+import android.view.ViewGroup;
+
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -9,24 +11,33 @@
import com.sendbird.android.channel.GroupChannel;
import com.sendbird.android.message.BaseMessage;
import com.sendbird.uikit.activities.viewholder.MessageViewHolder;
-import com.sendbird.uikit.interfaces.OnItemClickListener;
import com.sendbird.uikit.interfaces.FormSubmitButtonClickListener;
+import com.sendbird.uikit.interfaces.OnItemClickListener;
+import com.sendbird.uikit.interfaces.OnMessageTemplateActionHandler;
+import com.sendbird.uikit.internal.contracts.SendbirdUIKitContract;
+import com.sendbird.uikit.internal.contracts.SendbirdUIKitImpl;
+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.ui.viewholders.SuggestedRepliesViewHolder;
-import com.sendbird.uikit.internal.contracts.SendbirdUIKitImpl;
-import com.sendbird.uikit.internal.contracts.SendbirdUIKitContract;
+import com.sendbird.uikit.internal.utils.TemplateViewCachePool;
+import com.sendbird.uikit.log.Logger;
import com.sendbird.uikit.model.MessageListUIParams;
/**
* MessageListAdapter provides a binding from a {@link BaseMessage} type data set to views that are displayed within a RecyclerView.
*/
public class MessageListAdapter extends BaseMessageListAdapter {
+ private final TemplateViewCachePool templateViewCachePool = new TemplateViewCachePool();
@Nullable
protected OnItemClickListener suggestedRepliesClickListener;
@Nullable
protected FormSubmitButtonClickListener formSubmitButtonClickListener;
+ @Nullable
+ protected OnMessageTemplateActionHandler messageTemplateActionHandler;
+
public MessageListAdapter(boolean useMessageGroupUI) {
this(null, useMessageGroupUI);
}
@@ -54,6 +65,31 @@ public MessageListAdapter(@Nullable GroupChannel channel, @NonNull MessageListUI
sendbirdUIKit);
}
+ @NonNull
+ @Override
+ public MessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ MessageViewHolder viewHolder = super.onCreateViewHolder(parent, viewType);
+ if (viewHolder instanceof OtherTemplateMessageViewHolder) {
+ OtherTemplateMessageViewHolder otherTemplateMessageViewHolder = (OtherTemplateMessageViewHolder) viewHolder;
+ otherTemplateMessageViewHolder.setTemplateViewCachePool(templateViewCachePool);
+ otherTemplateMessageViewHolder.setOnMessageTemplateActionHandler((view, action, message) -> {
+ final OnMessageTemplateActionHandler finalListener = this.messageTemplateActionHandler;
+ if (finalListener != null) {
+ finalListener.onHandleAction(view, action, message);
+ }
+ });
+
+ otherTemplateMessageViewHolder.setOnFeedbackRatingClickListener((view, rating) -> {
+ final OnFeedbackRatingClickListener finalListener = this.feedbackRatingClickListener;
+ if (finalListener != null) {
+ finalListener.onFeedbackClicked(view, rating);
+ }
+ });
+ }
+
+ return viewHolder;
+ }
+
@Override
public void onBindViewHolder(@NonNull MessageViewHolder holder, int position) {
super.onBindViewHolder(holder, position);
@@ -119,4 +155,25 @@ public FormSubmitButtonClickListener getFormSubmitButtonClickListener() {
public void setFormSubmitButtonClickListener(@Nullable FormSubmitButtonClickListener formSubmitButtonClickListener) {
this.formSubmitButtonClickListener = formSubmitButtonClickListener;
}
+
+ /**
+ * Register a callback to be invoked when the message template action is clicked.
+ *
+ * @return {@link OnMessageTemplateActionHandler} to be invoked when the message template action is clicked.
+ * since 3.16.0
+ */
+ @Nullable
+ public OnMessageTemplateActionHandler getMessageTemplateActionHandler() {
+ return messageTemplateActionHandler;
+ }
+
+ /**
+ * Register a callback to be invoked when the message template action is clicked.
+ *
+ * @param messageTemplateActionHandler handler
+ * since 3.16.0
+ */
+ public void setMessageTemplateActionHandler(@Nullable OnMessageTemplateActionHandler messageTemplateActionHandler) {
+ this.messageTemplateActionHandler = messageTemplateActionHandler;
+ }
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageType.java b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageType.java
index 2ddbe9f4..005e1c95 100644
--- a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageType.java
+++ b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageType.java
@@ -119,7 +119,13 @@ public enum MessageType {
*
* since 3.11.0
*/
- VIEW_TYPE_TYPING_INDICATOR(21);
+ VIEW_TYPE_TYPING_INDICATOR(21),
+
+ /**
+ * @since 3.16.0
+ */
+ VIEW_TYPE_TEMPLATE_MESSAGE_OTHER(22);
+
final int value;
MessageType(int value) {
diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageViewHolderFactory.java b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MessageViewHolderFactory.java
index b16a1a83..86d9a114 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
@@ -29,6 +29,7 @@
import com.sendbird.uikit.databinding.SbViewOtherFileMessageBinding;
import com.sendbird.uikit.databinding.SbViewOtherFileVideoMessageBinding;
import com.sendbird.uikit.databinding.SbViewOtherMultipleFilesMessageBinding;
+import com.sendbird.uikit.databinding.SbViewOtherTemplateMessageBinding;
import com.sendbird.uikit.databinding.SbViewOtherUserMessageBinding;
import com.sendbird.uikit.databinding.SbViewOtherVoiceMessageBinding;
import com.sendbird.uikit.databinding.SbViewParentMessageInfoHolderBinding;
@@ -36,6 +37,8 @@
import com.sendbird.uikit.databinding.SbViewTimeLineMessageBinding;
import com.sendbird.uikit.databinding.SbViewTypingIndicatorMessageBinding;
import com.sendbird.uikit.internal.extensions.MessageExtensionsKt;
+import com.sendbird.uikit.internal.extensions.MessageTemplateExtensionsKt;
+import com.sendbird.uikit.internal.model.templates.MessageTemplateStatus;
import com.sendbird.uikit.internal.ui.viewholders.AdminMessageViewHolder;
import com.sendbird.uikit.internal.ui.viewholders.FormMessageViewHolder;
import com.sendbird.uikit.internal.ui.viewholders.MyFileMessageViewHolder;
@@ -52,6 +55,7 @@
import com.sendbird.uikit.internal.ui.viewholders.OtherFileMessageViewHolder;
import com.sendbird.uikit.internal.ui.viewholders.OtherImageFileMessageViewHolder;
import com.sendbird.uikit.internal.ui.viewholders.OtherMultipleFilesMessageViewHolder;
+import com.sendbird.uikit.internal.ui.viewholders.OtherTemplateMessageViewHolder;
import com.sendbird.uikit.internal.ui.viewholders.OtherUserMessageViewHolder;
import com.sendbird.uikit.internal.ui.viewholders.OtherVideoFileMessageViewHolder;
import com.sendbird.uikit.internal.ui.viewholders.OtherVoiceMessageViewHolder;
@@ -65,8 +69,6 @@
import com.sendbird.uikit.model.TypingIndicatorMessage;
import com.sendbird.uikit.utils.MessageUtils;
-import java.util.Map;
-
/**
* A Factory manages a type of messages.
*/
@@ -234,6 +236,9 @@ public static MessageViewHolder createViewHolder(@NonNull LayoutInflater inflate
case VIEW_TYPE_TYPING_INDICATOR:
holder = new TypingIndicatorViewHolder(SbViewTypingIndicatorMessageBinding.inflate(inflater, parent, false), messageListUIParams);
break;
+ case VIEW_TYPE_TEMPLATE_MESSAGE_OTHER:
+ holder = new OtherTemplateMessageViewHolder(SbViewOtherTemplateMessageBinding.inflate(inflater, parent, false), messageListUIParams);
+ break;
default:
// unknown message type
if (viewType == MessageType.VIEW_TYPE_UNKNOWN_MESSAGE_ME) {
@@ -265,6 +270,19 @@ public static int getViewType(@NonNull BaseMessage message) {
public static MessageType getMessageType(@NonNull BaseMessage message) {
MessageType type;
+ MessageTemplateStatus messageTemplateStatus = MessageTemplateExtensionsKt.getMessageTemplateStatus(message);
+ if (messageTemplateStatus != null) {
+ switch (messageTemplateStatus) {
+ case CACHED:
+ case LOADING:
+ case FAILED_TO_FETCH:
+ case FAILED_TO_PARSE:
+ return MessageType.VIEW_TYPE_TEMPLATE_MESSAGE_OTHER;
+ case NOT_APPLICABLE:
+ break;
+ }
+ }
+
if (message.getChannelType() == ChannelType.GROUP && !message.getForms().isEmpty()) {
return MessageType.VIEW_TYPE_FORM_TYPE_MESSAGE;
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MyMessageViewHolder.kt b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MyMessageViewHolder.kt
index e27df60f..07c96403 100644
--- a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MyMessageViewHolder.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/MyMessageViewHolder.kt
@@ -40,6 +40,7 @@ open class MyMessageViewHolder(
@CallSuper
override fun bind(channel: BaseChannel, message: BaseMessage, params: MessageListUIParams) {
+ binding.root.messageUIConfig = messageUIConfig
binding.root.drawMessage(channel, message, params)
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/OtherMessageViewHolder.kt b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/OtherMessageViewHolder.kt
index 4ee3ebfd..2148c257 100644
--- a/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/OtherMessageViewHolder.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/activities/viewholder/OtherMessageViewHolder.kt
@@ -27,19 +27,20 @@ import com.sendbird.uikit.model.MessageListUIParams
@MessageViewHolderExperimental
open class OtherMessageViewHolder(
parent: ViewGroup,
- open val contentView: View,
+ open val contentView: View? = null,
messageListUIParams: MessageListUIParams,
- private val binding: SbViewOtherMessageBinding = SbViewOtherMessageBinding.inflate(
+ protected val binding: SbViewOtherMessageBinding = SbViewOtherMessageBinding.inflate(
LayoutInflater.from(parent.context.toComponentListContextThemeWrapper())
)
) : MessageViewHolder(binding.root, messageListUIParams), EmojiReactionHandler {
init {
- binding.root.attachContentView(contentView)
+ contentView?.let { binding.root.attachContentView(it) }
}
@CallSuper
override fun bind(channel: BaseChannel, message: BaseMessage, params: MessageListUIParams) {
+ binding.root.messageUIConfig = messageUIConfig
binding.root.drawMessage(channel, message, params)
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/consts/SingleMenuType.kt b/uikit/src/main/java/com/sendbird/uikit/consts/SingleMenuType.kt
new file mode 100644
index 00000000..e0f55e5b
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/consts/SingleMenuType.kt
@@ -0,0 +1,30 @@
+package com.sendbird.uikit.consts
+
+/**
+ * Single menu type
+ *
+ * @constructor Create empty Single menu type
+ * @since 3.16.0
+ */
+enum class SingleMenuType(private val value: Int) {
+ /**
+ * A type that has an action button to redirect next page.
+ */
+ NEXT(0),
+
+ /**
+ * A type that has a switch button to toggle some action.
+ */
+ SWITCH(1),
+
+ /**
+ * A type that has no next action.
+ */
+ NONE(2);
+
+ companion object {
+ // TODO (Remove : after all codes are converted as kotlin this annotation doesn't need)
+ @JvmStatic
+ fun from(value: Int): SingleMenuType = values().firstOrNull { it.value == value } ?: NONE
+ }
+}
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 bad43f8f..996c3d33 100644
--- a/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/consts/StringSet.kt
@@ -105,6 +105,7 @@ object StringSet {
const val LABEL_COPY_TEXT = "COPY_TEXT"
const val DEFAULT_CHANNEL_COVER_URL = "https://static.sendbird.com/sample/cover/cover_"
const val Voice_message = "Voice_message"
+ const val message = "message"
// attributes list
const val reactions = "reactions"
@@ -127,6 +128,7 @@ object StringSet {
const val EVENT_MESSAGE_RECEIVED = "EVENT_MESSAGE_RECEIVED"
const val EVENT_MESSAGE_UPDATED = "EVENT_MESSAGE_UPDATED"
const val EVENT_TYPING_STATUS_UPDATED = "EVENT_TYPING_STATUS_UPDATED"
+ const val EVENT_MESSAGE_TEMPLATE_UPDATED = "EVENT_MESSAGE_TEMPLATE_UPDATED"
const val INVALID_URL = "INVALID_URL"
const val photo = "photo"
const val photos = "photos"
@@ -144,6 +146,13 @@ object StringSet {
const val uikit = "uikit"
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"
+
// Config
const val none = "none"
const val quote_reply = "quote_reply"
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 28a388ba..23e2449f 100644
--- a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java
+++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java
@@ -1,9 +1,11 @@
package com.sendbird.uikit.fragments;
import android.app.ActivityOptions;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.res.ColorStateList;
+import android.net.Uri;
import android.os.Bundle;
import android.text.Editable;
import android.view.View;
@@ -56,9 +58,11 @@
import com.sendbird.uikit.interfaces.OnInputTextChangedListener;
import com.sendbird.uikit.interfaces.OnItemClickListener;
import com.sendbird.uikit.interfaces.OnItemLongClickListener;
+import com.sendbird.uikit.interfaces.OnMessageTemplateActionHandler;
import com.sendbird.uikit.internal.extensions.MessageExtensionsKt;
import com.sendbird.uikit.internal.model.VoicePlayerManager;
import com.sendbird.uikit.log.Logger;
+import com.sendbird.uikit.model.Action;
import com.sendbird.uikit.model.DialogListItem;
import com.sendbird.uikit.model.ReadyStatus;
import com.sendbird.uikit.model.TextUIConfig;
@@ -72,6 +76,7 @@
import com.sendbird.uikit.providers.ViewModelProviders;
import com.sendbird.uikit.utils.ChannelUtils;
import com.sendbird.uikit.utils.DialogUtils;
+import com.sendbird.uikit.utils.IntentUtils;
import com.sendbird.uikit.utils.MessageUtils;
import com.sendbird.uikit.utils.TextUtils;
import com.sendbird.uikit.vm.ChannelViewModel;
@@ -142,6 +147,9 @@ public class ChannelFragment extends BaseMessageListFragment {
@@ -931,6 +940,68 @@ private void showUpdateFeedbackCommentDialog(@NonNull BaseMessage message) {
);
}
+ private void handleTemplateMessageAction(@NonNull View view, @NonNull Action action, @NonNull BaseMessage message) {
+ switch (action.type) {
+ case StringSet.web:
+ handleWebAction(view, action, message);
+ break;
+ case StringSet.custom:
+ handleCustomAction(view, action, message);
+ break;
+ default:
+ break;
+ }
+ }
+
+ /**
+ * If an Action is registered in a specific view, it is called when a click event occurs.
+ *
+ * @param action the registered Action data
+ * @param message a clicked message
+ * since 3.16.0
+ */
+ protected void handleWebAction(@NonNull View view, @NonNull Action action, @NonNull BaseMessage message) {
+ Logger.d(">> ChannelFragment::handleWebAction() action=%s", action);
+ final Intent intent = IntentUtils.getWebViewerIntent(action.data);
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Logger.e(e);
+ }
+ }
+
+ /**
+ * If an Action is registered in a specific view, it is called when a click event occurs.
+ *
+ * @param action the registered Action data
+ * @param message a clicked message
+ * since 3.16.0
+ */
+ protected void handleCustomAction(@NonNull View view, @NonNull Action action, @NonNull BaseMessage message) {
+ Logger.d(">> ChannelFragment::handleCustomAction() action=%s", action);
+ try {
+ final String data = action.data;
+ if (TextUtils.isNotEmpty(data)) {
+ final Uri uri = Uri.parse(data);
+ Logger.d("++ uri = %s", uri);
+ final String scheme = uri.getScheme();
+ Logger.d("++ scheme=%s", scheme);
+ Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+ boolean hasIntent = IntentUtils.hasIntent(requireContext(), intent);
+ if (!hasIntent) {
+ final String alterData = action.alterData;
+ if (alterData != null) {
+ intent = new Intent(Intent.ACTION_VIEW, Uri.parse(alterData));
+ }
+ }
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(intent);
+ }
+ } catch (Exception e) {
+ Logger.w(e);
+ }
+ }
+
@SuppressWarnings("unused")
public static class Builder {
@NonNull
@@ -996,6 +1067,10 @@ public static class Builder {
private View.OnClickListener voiceRecorderButtonClickListener;
@Nullable
private OnItemClickListener messageMentionClickListener;
+
+ @Nullable
+ private OnMessageTemplateActionHandler messageTemplateActionHandler;
+
@Nullable
private ChannelFragment customFragment;
@@ -2000,6 +2075,11 @@ public Builder setOnMessageMentionClickListener(@NonNull OnItemClickListener contextMenuList = makeActionContextMenu(member, channel);
+ final int size = contextMenuList.size();
+ final DialogListItem[] items = contextMenuList.toArray(new DialogListItem[size]);
+ final String title = getActionContextMenuTitle(member, channel);
+ DialogUtils.showListDialog(getContext(), title, items, (v, p, item) ->
+ onActionContextMenuItemClicked(member, item, channel));
+ }
+
+ /**
+ * Returns the title of the member context menu.
+ *
+ * @param member a member to make the context menu
+ * @param channel a channel to make the context menu
+ * @return the title of the member context menu
+ * since 3.16.0
+ */
+ @NonNull
+ protected String getActionContextMenuTitle(@NonNull Member member, @Nullable GroupChannel channel) {
+ return member.getNickname();
+ }
+
+ /**
+ * Makes the context menu for the member.
+ *
+ * @param member a member to make the context menu
+ * @param channel a channel to make the context menu
+ * @return a list of {@link DialogListItem} to show the context menu
+ * since 3.16.0
+ */
+ @NonNull
+ protected List makeActionContextMenu(@NonNull Member member, @Nullable GroupChannel channel) {
+ if (getContext() == null || channel == null) return new ArrayList<>();
boolean isMuted = member.isMuted();
boolean isOperator = member.getRole() == Role.OPERATOR;
- DialogListItem registerOperator = new DialogListItem(isOperator ? R.string.sb_text_unregister_operator : R.string.sb_text_register_operator);
- DialogListItem muteMember = new DialogListItem(isMuted ? R.string.sb_text_unmute_member : R.string.sb_text_mute_member);
- DialogListItem banMember = new DialogListItem(R.string.sb_text_ban_member, 0, true);
- DialogListItem[] items = !channel.isBroadcast() ?
+ final DialogListItem registerOperator = new DialogListItem(isOperator ? R.string.sb_text_unregister_operator : R.string.sb_text_register_operator);
+ final DialogListItem muteMember = new DialogListItem(isMuted ? R.string.sb_text_unmute_member : R.string.sb_text_mute_member);
+ final DialogListItem banMember = new DialogListItem(R.string.sb_text_ban_member, 0, true);
+ final DialogListItem[] items = !channel.isBroadcast() ?
new DialogListItem[]{registerOperator, muteMember, banMember} :
new DialogListItem[]{registerOperator, banMember};
+ return new ArrayList<>(Arrays.asList(items));
+ }
+ /**
+ * Called when the context menu item has been clicked.
+ *
+ * @param member a member to make the context menu
+ * @param item a context menu item
+ * @param channel a channel to make the context menu
+ * @return True if the callback has consumed the event, false otherwise.
+ * since 3.16.0
+ */
+ protected boolean onActionContextMenuItemClicked(@NonNull Member member, @NonNull DialogListItem item, @Nullable GroupChannel channel) {
+ Logger.d(">> MemberListFragment::onActionContextMenuItemClicked(member=%s, item.key=%s)", member, item.getKey());
+ final Context context = getContext();
+ if (context == null || channel == null) return false;
final MemberListModule module = getModule();
final MemberListViewModel viewModel = getViewModel();
- DialogUtils.showListDialog(getContext(), member.getNickname(),
- items, (v, p, item) -> {
- final int key = item.getKey();
- final OnCompleteHandler handler = e -> {
- module.shouldDismissLoadingDialog();
- if (e != null) {
- int errorTextResId = R.string.sb_text_error_register_operator;
- if (key == R.string.sb_text_unregister_operator) {
- errorTextResId = R.string.sb_text_error_unregister_operator;
- } else if (key == R.string.sb_text_mute_member) {
- errorTextResId = R.string.sb_text_error_mute_member;
- } else if (key == R.string.sb_text_unmute_member) {
- errorTextResId = R.string.sb_text_error_unmute_member;
- } else if (key == R.string.sb_text_ban_member) {
- errorTextResId = R.string.sb_text_error_ban_member;
- }
- toastError(errorTextResId);
- }
- };
- if (getContext() == null) return;
- module.shouldShowLoadingDialog(getContext());
- if (key == R.string.sb_text_register_operator) {
- viewModel.addOperator(member.getUserId(), handler);
- } else if (key == R.string.sb_text_unregister_operator) {
- viewModel.removeOperator(member.getUserId(), handler);
+ final int key = item.getKey();
+ final OnCompleteHandler handler = e -> {
+ module.shouldDismissLoadingDialog();
+ if (e != null) {
+ int errorTextResId = R.string.sb_text_error_register_operator;
+ if (key == R.string.sb_text_unregister_operator) {
+ errorTextResId = R.string.sb_text_error_unregister_operator;
} else if (key == R.string.sb_text_mute_member) {
- viewModel.muteUser(member.getUserId(), handler);
+ errorTextResId = R.string.sb_text_error_mute_member;
} else if (key == R.string.sb_text_unmute_member) {
- viewModel.unmuteUser(member.getUserId(), handler);
+ errorTextResId = R.string.sb_text_error_unmute_member;
} else if (key == R.string.sb_text_ban_member) {
- viewModel.banUser(member.getUserId(), handler);
+ errorTextResId = R.string.sb_text_error_ban_member;
}
- });
+ toastError(errorTextResId);
+ }
+ };
+ if (key == R.string.sb_text_register_operator) {
+ viewModel.addOperator(member.getUserId(), handler);
+ } else if (key == R.string.sb_text_unregister_operator) {
+ viewModel.removeOperator(member.getUserId(), handler);
+ } else if (key == R.string.sb_text_mute_member) {
+ viewModel.muteUser(member.getUserId(), handler);
+ } else if (key == R.string.sb_text_unmute_member) {
+ viewModel.unmuteUser(member.getUserId(), handler);
+ } else if (key == R.string.sb_text_ban_member) {
+ viewModel.banUser(member.getUserId(), handler);
+ } else {
+ return false;
+ }
+ module.shouldShowLoadingDialog(context);
+ return true;
}
/**
diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/ParticipantListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/ParticipantListFragment.java
index 6753ec09..80368ae0 100644
--- a/uikit/src/main/java/com/sendbird/uikit/fragments/ParticipantListFragment.java
+++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ParticipantListFragment.java
@@ -1,5 +1,6 @@
package com.sendbird.uikit.fragments;
+import android.content.Context;
import android.content.res.ColorStateList;
import android.os.Bundle;
import android.view.View;
@@ -33,6 +34,10 @@
import com.sendbird.uikit.vm.ParticipantViewModel;
import com.sendbird.uikit.widgets.StatusFrameView;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
/**
* Fragment displaying the operators of the channel.
*
@@ -170,7 +175,7 @@ protected void onBindStatusComponent(@NonNull StatusComponent statusComponent, @
*
* @param view The view that was clicked.
* @param position The position that was clicked.
- * @param user The member data that was clicked.
+ * @param user The user data that was clicked.
* since 1.2.2
*/
protected void onProfileClicked(@NonNull View view, int position, @NonNull User user) {
@@ -191,46 +196,13 @@ protected void onProfileClicked(@NonNull View view, int position, @NonNull User
*/
protected void onActionItemClicked(@NonNull View view, int position, @NonNull User participant, @Nullable OpenChannel channel) {
if (getContext() == null || channel == null) return;
- boolean isOperator = channel.isOperator(participant);
- DialogListItem registerOperator = new DialogListItem(isOperator ? R.string.sb_text_unregister_operator : R.string.sb_text_register_operator);
- // mute menu is always shown as 'Mute' because there is no way to know user's muted info
- DialogListItem muteParticipant = new DialogListItem(R.string.sb_text_mute_participant);
- DialogListItem banParticipant = new DialogListItem(R.string.sb_text_ban_participant, 0, true);
- DialogListItem[] items = new DialogListItem[]{registerOperator, muteParticipant, banParticipant};
- final ParticipantListModule module = getModule();
- final ParticipantViewModel viewModel = getViewModel();
- DialogUtils.showListDialog(getContext(), participant.getNickname(),
- items, (v, p, item) -> {
- final int key = item.getKey();
- final OnCompleteHandler handler = e -> {
- module.shouldDismissLoadingDialog();
- if (e != null) {
- int errorTextResId = R.string.sb_text_error_register_operator;
- if (key == R.string.sb_text_unregister_operator) {
- errorTextResId = R.string.sb_text_error_unregister_operator;
- } else if (key == R.string.sb_text_mute_participant) {
- errorTextResId = R.string.sb_text_error_mute_participant;
- } else if (key == R.string.sb_text_ban_participant) {
- errorTextResId = R.string.sb_text_error_ban_participant;
- }
- toastError(errorTextResId);
- } else {
- viewModel.loadInitial();
- }
- };
- if (getContext() == null) return;
- module.shouldShowLoadingDialog(getContext());
- if (key == R.string.sb_text_register_operator) {
- viewModel.addOperator(participant.getUserId(), handler);
- } else if (key == R.string.sb_text_unregister_operator) {
- viewModel.removeOperator(participant.getUserId(), handler);
- } else if (key == R.string.sb_text_mute_participant) {
- viewModel.muteUser(participant.getUserId(), handler);
- } else if (key == R.string.sb_text_ban_participant) {
- viewModel.banUser(participant.getUserId(), handler);
- }
- });
+ final List contextMenuList = makeActionContextMenu(participant, channel);
+ final int size = contextMenuList.size();
+ final DialogListItem[] items = contextMenuList.toArray(new DialogListItem[size]);
+ final String title = getActionContextMenuTitle(participant, channel);
+ DialogUtils.showListDialog(getContext(), title, items, (v, p, item) ->
+ onActionContextMenuItemClicked(participant, item, channel));
}
/**
@@ -245,6 +217,86 @@ protected String getChannelUrl() {
return args.getString(StringSet.KEY_CHANNEL_URL, "");
}
+ /**
+ * Returns the title of the user context menu.
+ *
+ * @param participant a user to make the context menu
+ * @param channel a channel to make the context menu
+ * @return the title of the user context menu
+ * since 3.16.0
+ */
+ @NonNull
+ protected String getActionContextMenuTitle(@NonNull User participant, @Nullable OpenChannel channel) {
+ return participant.getNickname();
+ }
+
+ /**
+ * Makes the context menu for the user.
+ *
+ * @param participant a user to make the context menu
+ * @param channel a channel to make the context menu
+ * @return a list of {@link DialogListItem} to show the context menu
+ * since 3.16.0
+ */
+ @NonNull
+ protected List makeActionContextMenu(@NonNull User participant, @Nullable OpenChannel channel) {
+ if (getContext() == null || channel == null) return new ArrayList<>();
+ boolean isOperator = channel.isOperator(participant);
+ final DialogListItem registerOperator = new DialogListItem(isOperator ? R.string.sb_text_unregister_operator : R.string.sb_text_register_operator);
+ // mute menu is always shown as 'Mute' because there is no way to know user's muted info
+ final DialogListItem muteParticipant = new DialogListItem(R.string.sb_text_mute_participant);
+ final DialogListItem banParticipant = new DialogListItem(R.string.sb_text_ban_participant, 0, true);
+ final DialogListItem[] items = new DialogListItem[]{registerOperator, muteParticipant, banParticipant};
+ return new ArrayList<>(Arrays.asList(items));
+ }
+
+ /**
+ * Called when the context menu item has been clicked.
+ *
+ * @param participant a user to make the context menu
+ * @param item a context menu item
+ * @param channel a channel to make the context menu
+ * @return True if the callback has consumed the event, false otherwise.
+ * since 3.16.0
+ */
+ protected boolean onActionContextMenuItemClicked(@NonNull User participant, @NonNull DialogListItem item, @Nullable OpenChannel channel) {
+ Logger.d(">> ParticipantListFragment::onActionContextMenuItemClicked(Participant=%s, item.key=%s)", participant, item.getKey());
+ final Context context = getContext();
+ if (context == null || channel == null) return false;
+ final ParticipantListModule module = getModule();
+ final ParticipantViewModel viewModel = getViewModel();
+ final int key = item.getKey();
+ final OnCompleteHandler handler = e -> {
+ module.shouldDismissLoadingDialog();
+ if (e != null) {
+ int errorTextResId = R.string.sb_text_error_register_operator;
+ if (key == R.string.sb_text_unregister_operator) {
+ errorTextResId = R.string.sb_text_error_unregister_operator;
+ } else if (key == R.string.sb_text_mute_participant) {
+ errorTextResId = R.string.sb_text_error_mute_participant;
+ } else if (key == R.string.sb_text_ban_participant) {
+ errorTextResId = R.string.sb_text_error_ban_participant;
+ }
+ toastError(errorTextResId);
+ } else {
+ viewModel.loadInitial();
+ }
+ };
+ module.shouldShowLoadingDialog(getContext());
+ if (key == R.string.sb_text_register_operator) {
+ viewModel.addOperator(participant.getUserId(), handler);
+ } else if (key == R.string.sb_text_unregister_operator) {
+ viewModel.removeOperator(participant.getUserId(), handler);
+ } else if (key == R.string.sb_text_mute_participant) {
+ viewModel.muteUser(participant.getUserId(), handler);
+ } else if (key == R.string.sb_text_ban_participant) {
+ viewModel.banUser(participant.getUserId(), handler);
+ } else {
+ return false;
+ }
+ return true;
+ }
+
/**
* This is a Builder that is able to create the fragment of participants list.
* The builder provides options how the channel is showing and working. Also you can set the event handler what you want to override.
diff --git a/uikit/src/main/java/com/sendbird/uikit/interfaces/CustomMenuProvider.kt b/uikit/src/main/java/com/sendbird/uikit/interfaces/CustomMenuProvider.kt
new file mode 100644
index 00000000..3757eb45
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/interfaces/CustomMenuProvider.kt
@@ -0,0 +1,21 @@
+package com.sendbird.uikit.interfaces
+
+import android.content.Context
+import android.view.View
+
+/**
+ * A provider interface for customizing the menu view.
+ *
+ * @since 3.16.0
+ */
+fun interface MenuViewProvider {
+ /**
+ * Provide menu view.
+ *
+ * @param context The context in which the theme is set
+ * @param position The position of the current custom menu item
+ * @return The menu view
+ * @since 3.16.0
+ */
+ fun provideMenuView(context: Context, position: Int): View
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/interfaces/OnMessageTemplateActionHandler.kt b/uikit/src/main/java/com/sendbird/uikit/interfaces/OnMessageTemplateActionHandler.kt
new file mode 100644
index 00000000..f036f092
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/interfaces/OnMessageTemplateActionHandler.kt
@@ -0,0 +1,22 @@
+package com.sendbird.uikit.interfaces
+
+import android.view.View
+import com.sendbird.android.message.BaseMessage
+import com.sendbird.uikit.model.Action
+
+/**
+ * Interface definition for a callback to be invoked when a item is invoked with an event.
+ *
+ * @since 3.16.0
+ */
+fun interface OnMessageTemplateActionHandler {
+ /**
+ * If an Action is registered in a specific view, it is called when a click event occurs.
+ *
+ * @param view the view that was clicked.
+ * @param action the registered Action data
+ * @param message the clicked message
+ * @since 3.16.0
+ */
+ fun onHandleAction(view: View, action: Action, message: BaseMessage)
+}
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
new file mode 100644
index 00000000..40252d69
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/adapter/CarouselChildViewAdapter.kt
@@ -0,0 +1,90 @@
+package com.sendbird.uikit.internal.adapter
+
+import android.content.Context
+import android.view.ViewGroup
+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
+
+internal class CarouselChildViewAdapter : RecyclerView.Adapter() {
+ private val childTemplateParams: MutableList = mutableListOf()
+ internal var onChildViewCreated: ViewLifecycleHandler? = null
+
+ fun setChildTemplateParams(newParams: List) {
+ val oldParams = childTemplateParams.toList()
+ val diffResult = DiffUtil.calculateDiff(
+ ParamsDiffCallback(oldParams, newParams)
+ )
+ childTemplateParams.clear()
+ childTemplateParams.addAll(newParams)
+ diffResult.dispatchUpdatesTo(this)
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CarouselChildItemViewHolder {
+ return CarouselChildItemViewHolder(parent.context)
+ }
+
+ override fun getItemCount(): Int {
+ return childTemplateParams.size
+ }
+
+ override fun onBindViewHolder(holder: CarouselChildItemViewHolder, position: Int) {
+ holder.bind(childTemplateParams[position])
+ }
+
+ inner class CarouselChildItemViewHolder(
+ context: Context,
+ private val contentView: MessageTemplateView = MessageTemplateView(
+ context,
+ autoAdjustHeightWhenInvisible = false
+ )
+ ) : RecyclerView.ViewHolder(contentView) {
+ fun bind(params: Params) {
+ val maxChildFixedWidthSize = params.maxChildFixedWidthSize
+ val width = if (maxChildFixedWidthSize != null) {
+ contentView.context.resources.intToDp(maxChildFixedWidthSize)
+ } else {
+ contentView.context.resources.getDimensionPixelSize(R.dimen.sb_message_max_width)
+ }
+
+ contentView.layoutParams = contentView.layoutParams.apply {
+ this.width = width
+ }
+
+ contentView.draw(
+ params,
+ onViewCreated = { view, viewParams -> 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 }
+ }
+ }
+
+ private class ParamsDiffCallback(
+ private val oldParams: List,
+ private val newParams: List
+ ) : DiffUtil.Callback() {
+ override fun getOldListSize(): Int = oldParams.size
+
+ override fun getNewListSize(): Int = newParams.size
+
+ override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ return oldParams[oldItemPosition] == newParams[newItemPosition]
+ }
+
+ override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
+ return oldParams[oldItemPosition] == newParams[newItemPosition]
+ }
+ }
+}
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 1c085640..5a8bf12b 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
@@ -15,6 +15,7 @@ import com.sendbird.uikit.utils.MessageUtils
internal fun BaseMessage.hasParentMessage() = parentMessageId != 0L
internal fun BaseMessage.getDisplayMessage(): String {
+ if (this.isTemplateMessage()) return StringSet.message
return when (val data = MessageDisplayDataManager.getOrNull(this)) {
is UserMessageDisplayData -> data.message ?: message
else -> {
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
new file mode 100644
index 00000000..53e486e3
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/extensions/MessageTemplateExtensions.kt
@@ -0,0 +1,90 @@
+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
+}
+
+internal fun BaseMessage.saveParamsFromTemplate() {
+ val templateMessageData = this.templateMessageData ?: return
+ val key = templateMessageData.key
+ val template = MessageTemplateManager.getTemplate(key)
+ if (template != null) {
+ val syntax = template.getTemplateSyntax(
+ templateMessageData.variables,
+ templateMessageData.viewVariables
+ )
+
+ try {
+ val params = MessageTemplateParser.parse(syntax)
+ this.messageTemplateStatus = MessageTemplateStatus.CACHED
+ this.messageTemplateParams = params
+ } catch (e: Exception) {
+ this.messageTemplateStatus = MessageTemplateStatus.FAILED_TO_PARSE
+ }
+ } else {
+ this.messageTemplateStatus = MessageTemplateStatus.FAILED_TO_FETCH
+ }
+}
+
+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
+ }
+
+@OptIn(AIChatBotExperimental::class)
+internal var BaseMessage.messageTemplateStatus: MessageTemplateStatus?
+ get() = extras[StringSet.message_template_status] as? MessageTemplateStatus
+ set(value) {
+ if (value == null) {
+ extras.remove(StringSet.message_template_status)
+ } else {
+ extras[StringSet.message_template_status] = value
+ }
+ }
+
+@OptIn(AIChatBotExperimental::class)
+internal var BaseMessage.messageTemplateParams: Params?
+ get() = extras[StringSet.message_template_params] as? Params
+ set(value) {
+ if (value == null) {
+ extras.remove(StringSet.message_template_params)
+ } else {
+ extras[StringSet.message_template_params] = value
+ }
+ }
+
+internal enum class MessageTemplateContainerType {
+ DEFAULT, WIDE, CAROUSEL;
+
+ companion object {
+ fun create(value: String?): MessageTemplateContainerType {
+ return when (value) {
+ "wide" -> WIDE
+ else -> DEFAULT
+ }
+ }
+ }
+}
+
+internal const val ERR_MESSAGE_TEMPLATE_NOT_APPLICABLE = "NOT_APPLICABLE"
+internal const val ERR_MESSAGE_TEMPLATE_UNKNOWN = "UNKNOWN"
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 719efa74..107b41c3 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
@@ -3,15 +3,19 @@ package com.sendbird.uikit.internal.extensions
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.ColorStateList
+import android.graphics.Color
import android.graphics.Typeface
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.RippleDrawable
import android.os.Build
import android.util.TypedValue
+import android.view.Gravity
import android.view.View
import android.widget.EditText
+import android.widget.FrameLayout
import android.widget.ImageView
+import android.widget.ProgressBar
import android.widget.TextView
import androidx.core.content.ContextCompat
import com.bumptech.glide.Glide
@@ -26,6 +30,10 @@ 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.internal.model.notifications.NotificationThemeMode
+import com.sendbird.uikit.internal.model.template_messages.Params
+import com.sendbird.uikit.internal.model.template_messages.TemplateViewGenerator
+import com.sendbird.uikit.utils.DrawableUtils
import com.sendbird.uikit.widgets.FeedbackView
@Suppress("DEPRECATION")
@@ -152,3 +160,37 @@ internal fun FeedbackView.drawFeedback(message: BaseMessage, shouldHideFeedback:
listener?.onFeedbackClicked(message, feedbackRating)
}
}
+
+internal fun Context.createTemplateMessageLoadingView(): View {
+ val maxWidth = resources.getDimensionPixelSize(R.dimen.sb_message_max_width)
+ return FrameLayout(this).apply {
+ layoutParams = FrameLayout.LayoutParams(maxWidth, maxWidth)
+ setBackgroundColor(Color.TRANSPARENT)
+ addView(
+ ProgressBar(context).apply {
+ val size = resources.intToDp(36)
+ layoutParams = FrameLayout.LayoutParams(
+ size, size, Gravity.CENTER
+ )
+ val loading = DrawableUtils.setTintList(
+ context,
+ R.drawable.sb_progress,
+ ColorStateList.valueOf(TemplateViewGenerator.getSpinnerColor(NotificationThemeMode.Default))
+ )
+ this.indeterminateDrawable = loading
+ }
+ )
+ }
+}
+
+internal fun Context.createFallbackViewParams(message: BaseMessage): Params {
+ return createFallbackViewParams(message.message)
+}
+
+internal fun Context.createFallbackViewParams(message: String): Params {
+ return TemplateViewGenerator.createMessageTemplateDefaultViewParam(
+ message,
+ this.getString(R.string.sb_text_template_message_fallback_title),
+ this.getString(R.string.sb_text_template_message_fallback_description),
+ )
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Enums.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Enums.kt
index 1f8458b3..21ed926e 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Enums.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/Enums.kt
@@ -22,7 +22,10 @@ internal enum class ViewType {
ImageButton,
@SerialName(KeySet.text)
- Text
+ Text,
+
+ @SerialName(KeySet.carouselView)
+ CarouselView
;
companion object {
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 94d0d779..3deecdcf 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
@@ -57,6 +57,7 @@ internal object KeySet {
const val vertical = "vertical"
const val sub_data = "sub_data"
const val sub_type = "sub_type"
+ const val carouselView = "carouselView"
// notifications
const val key = "key"
@@ -66,6 +67,7 @@ internal object KeySet {
const val ui_template = "ui_template"
const val color_variables = "color_variables"
const val template_variables = "template_variables"
+ const val variables = "variables"
const val light = "light"
const val dark = "dark"
const val default = "default"
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 e750d439..4ed5a4bd 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
@@ -29,8 +29,8 @@ internal data class ActionData constructor(
) {
fun register(
view: View,
- onNotificationTemplateActionHandler: OnNotificationTemplateActionHandler?,
- message: BaseMessage
+ message: BaseMessage,
+ onNotificationTemplateActionHandler: OnNotificationTemplateActionHandler?
) {
onNotificationTemplateActionHandler?.let { callback ->
view.setOnClickListener {
@@ -232,3 +232,15 @@ internal data class ImageButtonViewParams constructor(
val metaData: MetaData? = null,
val imageStyle: ImageStyle = ImageStyle()
) : ViewParams()
+
+@Serializable
+@SerialName(KeySet.carouselView)
+internal data class CarouselViewParams constructor(
+ override val type: ViewType,
+ override val action: ActionData? = null,
+ override val width: SizeSpec = SizeSpec(SizeType.Flex, FILL_PARENT),
+ override val height: SizeSpec = SizeSpec(SizeType.Flex, WRAP_CONTENT),
+ override val viewStyle: ViewStyle = ViewStyle(),
+ val items: List,
+ val spacing: Int = 10
+) : ViewParams()
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/TemplateViewGenerator.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/TemplateViewGenerator.kt
index 1fac3797..cb1e2e5f 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/TemplateViewGenerator.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/template_messages/TemplateViewGenerator.kt
@@ -9,6 +9,7 @@ import com.sendbird.android.message.BaseMessage
import com.sendbird.uikit.SendbirdUIKit
import com.sendbird.uikit.internal.model.notifications.NotificationThemeMode
import com.sendbird.uikit.internal.ui.widgets.Box
+import com.sendbird.uikit.internal.ui.widgets.CarouselView
import com.sendbird.uikit.internal.ui.widgets.Image
import com.sendbird.uikit.internal.ui.widgets.ImageButton
import com.sendbird.uikit.internal.ui.widgets.Text
@@ -17,13 +18,16 @@ import com.sendbird.uikit.internal.ui.widgets.TextButton
internal typealias ViewLifecycleHandler = (view: View, viewParams: ViewParams) -> Unit
internal object TemplateViewGenerator {
+
+ @Throws(RuntimeException::class)
fun inflateViews(
context: Context,
params: Params,
- onViewCreated: ViewLifecycleHandler? = null
+ onViewCreated: ViewLifecycleHandler? = null,
+ onChildViewCreated: ViewLifecycleHandler? = null
): View {
when (params.version) {
- 1 -> {
+ 1, 2 -> {
return LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
@@ -32,7 +36,7 @@ internal object TemplateViewGenerator {
)
params.body.items.forEach {
addView(
- generateView(context, it, Orientation.Column, onViewCreated)
+ generateView(context, it, Orientation.Column, onViewCreated, onChildViewCreated)
)
}
}
@@ -47,7 +51,8 @@ internal object TemplateViewGenerator {
context: Context,
viewParams: ViewParams,
orientation: Orientation,
- onViewCreated: ViewLifecycleHandler? = null
+ onViewCreated: ViewLifecycleHandler? = null,
+ onChildViewCreated: ViewLifecycleHandler? = null
): View {
return when (viewParams) {
is BoxViewParams -> createBoxView(context, viewParams, orientation, onViewCreated)
@@ -55,6 +60,7 @@ internal object TemplateViewGenerator {
is TextViewParams -> createTextView(context, viewParams, orientation, onViewCreated)
is ButtonViewParams -> createButtonView(context, viewParams, orientation, onViewCreated)
is ImageButtonViewParams -> createImageButtonView(context, viewParams, orientation, onViewCreated)
+ is CarouselViewParams -> createCarouselView(context, viewParams, orientation, onViewCreated, onChildViewCreated)
}
}
@@ -128,6 +134,19 @@ internal object TemplateViewGenerator {
}
}
+ private fun createCarouselView(
+ context: Context,
+ params: CarouselViewParams,
+ orientation: Orientation,
+ onViewCreated: ViewLifecycleHandler? = null,
+ onChildViewCreated: ViewLifecycleHandler? = null
+ ): ViewGroup {
+ return CarouselView(context).apply {
+ onViewCreated?.invoke(this, params)
+ apply(params, orientation, onChildViewCreated)
+ }
+ }
+
@JvmStatic
fun createDefaultViewParam(
message: BaseMessage,
@@ -180,6 +199,63 @@ internal object TemplateViewGenerator {
)
}
+ @JvmStatic
+ fun createMessageTemplateDefaultViewParam(
+ message: String,
+ defaultFallbackTitle: String,
+ defaultFallbackDescription: String
+ ): Params {
+ val hasFallbackMessage = message.isNotEmpty()
+ val textList = mutableListOf(
+ TextViewParams(
+ type = ViewType.Text,
+ width = SizeSpec(SizeType.Flex, WRAP_CONTENT),
+ height = SizeSpec(SizeType.Flex, WRAP_CONTENT),
+ textStyle = TextStyle(
+ size = 14,
+ color = getTitleColor(NotificationThemeMode.Default)
+ ),
+ text = message.takeIf { it.isNotEmpty() } ?: defaultFallbackTitle,
+ )
+ )
+
+ if (!hasFallbackMessage) {
+ textList.add(
+ TextViewParams(
+ type = ViewType.Text,
+ width = SizeSpec(SizeType.Flex, WRAP_CONTENT),
+ height = SizeSpec(SizeType.Flex, WRAP_CONTENT),
+ textStyle = TextStyle(
+ size = 14,
+ color = getDescTextColor(NotificationThemeMode.Default)
+ ),
+ text = defaultFallbackDescription
+ )
+ )
+ }
+ return Params(
+ version = 1,
+ body = Body(
+ items = listOf(
+ BoxViewParams(
+ type = ViewType.Box,
+ orientation = Orientation.Column,
+ width = SizeSpec(SizeType.Flex, WRAP_CONTENT),
+ height = SizeSpec(SizeType.Flex, WRAP_CONTENT),
+ viewStyle = ViewStyle(
+ backgroundColor = getBackgroundColor(NotificationThemeMode.Default),
+ padding = Padding(
+ 6, 6, 12, 12
+ ),
+ radius = 16
+ ),
+ items = textList
+ ),
+ )
+ )
+ )
+ }
+
private fun getBackgroundColor(themeMode: NotificationThemeMode): Int {
val color = when (themeMode) {
NotificationThemeMode.Light -> "#EEEEEE"
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
new file mode 100644
index 00000000..583d5f8a
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/templates/MessageTemplate.kt
@@ -0,0 +1,97 @@
+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
+import com.sendbird.uikit.internal.model.template_messages.KeySet
+import com.sendbird.uikit.internal.singleton.JsonParser
+import com.sendbird.uikit.internal.singleton.MessageTemplateManager
+import com.sendbird.uikit.log.Logger
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import org.json.JSONArray
+import org.json.JSONObject
+
+// TODO : Bind with [NotificationTemplate] after api spec finalize
+@Serializable
+internal data class MessageTemplate constructor(
+ @SerialName(KeySet.key)
+ val templateKey: String,
+ @SerialName(KeySet.created_at)
+ val createdAt: Long,
+ @SerialName(KeySet.updated_at)
+ val updatedAt: Long,
+ val name: String? = null,
+ @SerialName(KeySet.ui_template)
+ @Serializable(with = JsonElementToStringSerializer::class)
+ private val _uiTemplate: String,
+ @SerialName(KeySet.color_variables)
+ private val _colorVariables: Map
+) {
+
+ fun getTemplateSyntax(
+ variables: Map,
+ viewVariables: Map> = emptyMap()
+ ): String {
+ return _uiTemplate
+ .replaceVariables(variables)
+ .replaceViewVariables(viewVariables)
+ }
+
+ private fun String.replaceVariables(variables: Map): String {
+ val regex = "\\{([^{}]+)\\}".toRegex()
+ return regex.replace(this) { matchResult ->
+ val variable = matchResult.groups[1]?.value
+ var converted = false
+
+ // 1. lookup and convert color variables first
+ var convertedResult = _colorVariables[variable]?.let {
+ Logger.i("++ color variable key=$variable, value=$it")
+ converted = true
+ val csvColor = CSVColor(it)
+ csvColor.getColorHexString(NotificationThemeMode.Default)
+ } ?: matchResult.value
+
+ // 2. If color variables didn't convert, convert data variables then.
+ if (!converted && variables.isNotEmpty()) {
+ convertedResult = variables[variable]?.let {
+ Logger.i("++ data variable key=$variable, value=$it")
+ it
+ } ?: convertedResult
+ }
+ convertedResult
+ }
+ }
+
+ /**
+ * If there is problem while replacing view variables, it will return original string and it will be failed to parse to Params. It's intended.
+ */
+ private fun String.replaceViewVariables(viewVariables: Map>): String {
+ val regex = """\"\{@([^{}]+)\}\"""".toRegex() // find `"{@variable}"` pattern including `"`
+ return regex.replace(this) { matchResult ->
+ val variable = matchResult.groups[1]?.value ?: return@replace matchResult.value
+ 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)))
+ }
+
+ jsonArray.toString()
+ }
+ }
+
+ override fun toString(): String {
+ return JsonParser.toJsonString(this)
+ }
+
+ companion object {
+ @JvmStatic
+ fun fromJson(value: String): MessageTemplate {
+ return JsonParser.fromJson(value)
+ }
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/model/templates/MessageTemplateStatus.kt b/uikit/src/main/java/com/sendbird/uikit/internal/model/templates/MessageTemplateStatus.kt
new file mode 100644
index 00000000..1321fba3
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/model/templates/MessageTemplateStatus.kt
@@ -0,0 +1,9 @@
+package com.sendbird.uikit.internal.model.templates
+
+internal enum class MessageTemplateStatus {
+ NOT_APPLICABLE,
+ CACHED,
+ LOADING,
+ FAILED_TO_PARSE,
+ FAILED_TO_FETCH
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateManager.kt b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateManager.kt
new file mode 100644
index 00000000..10e8b93b
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateManager.kt
@@ -0,0 +1,115 @@
+package com.sendbird.uikit.internal.singleton
+
+import android.content.Context
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.sendbird.android.exception.SendbirdException
+import com.sendbird.android.params.MessageTemplateListParams
+import com.sendbird.uikit.internal.model.templates.MessageTemplate
+import com.sendbird.uikit.log.Logger
+import java.util.concurrent.Executors
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * This class is used to manage message templates related data which is used for [com.sendbird.android.channel.GroupChannel].
+ * It doesn't manage the templates for Notification. For Notification, use [NotificationChannelManager].
+ */
+internal object MessageTemplateManager {
+ internal lateinit var instance: MessageTemplateManagerImpl
+ @VisibleForTesting
+ internal val isInitialized: AtomicBoolean = AtomicBoolean()
+
+ internal fun checkAndInit(context: Context) {
+ if (!isInitialized.get()) {
+ init(context)
+ }
+ }
+
+ @JvmStatic
+ @Synchronized
+ fun init(context: Context) {
+ val messageTemplateRepository = MessageTemplateRepository(context.applicationContext)
+ instance = MessageTemplateManagerImpl(messageTemplateRepository)
+ isInitialized.set(true)
+ }
+
+ @JvmStatic
+ fun hasTemplate(key: String?): Boolean {
+ key ?: return false
+ return instance.hasTemplate(key)
+ }
+
+ @JvmStatic
+ fun getTemplate(key: String?): MessageTemplate? {
+ key ?: return null
+ return instance.getTemplate(key)
+ }
+
+ @WorkerThread
+ @JvmStatic
+ @Throws(SendbirdException::class)
+ fun syncMessageTemplateListBlocking(latestToken: String?) = instance.syncMessageTemplateListBlocking(latestToken)
+
+ @JvmStatic
+ @Throws(SendbirdException::class)
+ fun getMessageTemplatesBlocking(
+ keys: List
+ ): List = instance.getMessageTemplatesBlocking(keys)
+
+ @JvmStatic
+ fun clearAll() = instance.clearAll()
+
+ @VisibleForTesting
+ internal fun isInstanceInitialized() = this::instance.isInitialized
+}
+
+internal class MessageTemplateManagerImpl(private val messageTemplateRepository: MessageTemplateRepository) {
+ private val worker = Executors.newSingleThreadExecutor()
+ fun hasTemplate(key: String): Boolean = messageTemplateRepository.getTemplate(key) != null
+
+ @WorkerThread
+ @Throws(SendbirdException::class)
+ fun syncMessageTemplateListBlocking(latestToken: String?) {
+ if (latestToken == null) return // it means there's no template in the server.
+
+ // 1. check updated time with server.
+ val lastTemplateToken = messageTemplateRepository.lastCachedToken
+ if (lastTemplateToken.isNotEmpty() && lastTemplateToken == latestToken) {
+ Logger.d("++ skip request template list. The template list is already up-to-date.")
+ return
+ }
+
+ // 2. call api
+ messageTemplateRepository.requestMessageTemplatesBlocking()
+ }
+
+ @Throws(SendbirdException::class)
+ @VisibleForTesting
+ internal fun getMessageTemplatesBlocking(keys: List): List {
+ Logger.d("MessageTemplateManager::getMessageTemplatesBlocking(keys: ${keys.joinToString()})")
+ val cachedTemplates = mutableListOf()
+ val uncachedKeys = keys.filter {
+ val template = messageTemplateRepository.getTemplate(it)
+ if (template != null) cachedTemplates.add(template)
+ template == null
+ }
+
+ Logger.d("MessageTemplateManager::getMessageTemplatesBlocking uncachedKeys: ${uncachedKeys.joinToString()}")
+ if (uncachedKeys.isEmpty()) {
+ return cachedTemplates
+ }
+
+ return cachedTemplates + messageTemplateRepository.requestMessageTemplatesBlocking(
+ params = MessageTemplateListParams(limit = 100, keys = keys)
+ )
+ }
+
+ fun getTemplate(key: String): MessageTemplate? {
+ return messageTemplateRepository.getTemplate(key)
+ }
+
+ fun clearAll() {
+ Logger.d("MessageTemplateManager::clearAll()")
+ messageTemplateRepository.clearAll()
+ }
+}
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
new file mode 100644
index 00000000..bd1c1cb4
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateMapper.kt
@@ -0,0 +1,100 @@
+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.messageTemplateStatus
+import com.sendbird.uikit.internal.model.templates.MessageTemplateStatus
+import com.sendbird.uikit.log.Logger
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+/**
+ * Map [BaseMessage] and [com.sendbird.uikit.internal.model.templates.MessageTemplate].
+ */
+internal class MessageTemplateMapper(
+ private val worker: ExecutorService = Executors.newCachedThreadPool()
+) {
+ /**
+ * Returns updated messages immediately.
+ * Then, it requests uncached MessageTemplates and will call onFetchCompleteHandler with fetched messages.
+ */
+ fun mapTemplate(messages: List, onFetchCompleteHandler: (updatedMessages: List) -> Unit): List {
+ val startedAt = System.currentTimeMillis()
+ try {
+ // 1. filter mutable template message status
+ val mutableTemplateMessages = messages.filter {
+ it.messageTemplateStatus == null
+ }
+
+ Logger.d("1. filter mutable template message status result >> total[${messages.size}], mutable[${mutableTemplateMessages.size}]")
+ if (mutableTemplateMessages.isEmpty()) {
+ return mutableTemplateMessages
+ }
+
+ // 2. filter template message
+ val (templateMessages, notTemplateMessage) = mutableTemplateMessages.partition { it.isTemplateMessage() }
+ templateMessages.forEach { it.messageTemplateStatus = MessageTemplateStatus.LOADING }
+ notTemplateMessage.forEach { it.messageTemplateStatus = MessageTemplateStatus.NOT_APPLICABLE }
+
+ Logger.d("2. filter template message result >> mutable[${mutableTemplateMessages.size}], template messages[${templateMessages.size}], not template messages[${notTemplateMessage.size}]")
+ if (templateMessages.isEmpty()) {
+ return mutableTemplateMessages
+ }
+
+ // 3. filter not cached template keys
+ val (cachedTemplateMessages, notCachedTemplateMessages) = templateMessages.partition {
+ val templateMessageData = it.templateMessageData ?: return@partition false
+ val hasParentTemplate = MessageTemplateManager.hasTemplate(templateMessageData.key)
+ hasParentTemplate && templateMessageData.childTemplateKeys().all { key ->
+ MessageTemplateManager.hasTemplate(key)
+ }
+ }
+
+ cachedTemplateMessages.forEach { it.messageTemplateStatus = MessageTemplateStatus.CACHED }
+
+ Logger.d("3. filter not cached template keys result >> template messages[${templateMessages.size}], cached[${cachedTemplateMessages.size}], not cached[${notCachedTemplateMessages.size}]")
+
+ if (notCachedTemplateMessages.isEmpty()) {
+ return mutableTemplateMessages
+ }
+
+ // 4. fetch not cached templates
+ worker.submit {
+ val parentTemplateKeys = notCachedTemplateMessages.mapNotNull {
+ it.templateMessageData?.key
+ }.filter { key -> MessageTemplateManager.hasTemplate(key).not() }
+
+ val childTemplateKeys = notCachedTemplateMessages.mapNotNull {
+ it.templateMessageData?.childTemplateKeys()
+ }.flatten().filter { key -> MessageTemplateManager.hasTemplate(key).not() }
+
+ val notCachedTemplateKeys = (parentTemplateKeys + childTemplateKeys).distinct()
+ try {
+ MessageTemplateManager.getMessageTemplatesBlocking(notCachedTemplateKeys)
+ val (fetchedMessages, notFetchedMessages) = notCachedTemplateMessages.partition { message ->
+ val templateMessageData = message.templateMessageData ?: return@partition false
+ val hasParentTemplate = MessageTemplateManager.hasTemplate(templateMessageData.key)
+ hasParentTemplate && templateMessageData.childTemplateKeys().all { key ->
+ MessageTemplateManager.hasTemplate(key)
+ }
+ }
+ Logger.d("4. fetch not cached templates result >> fetched messages[${fetchedMessages.size}], not fetched messages[${notFetchedMessages.size}]")
+ fetchedMessages.forEach { it.messageTemplateStatus = MessageTemplateStatus.CACHED }
+ notFetchedMessages.forEach { it.messageTemplateStatus = MessageTemplateStatus.FAILED_TO_FETCH }
+ } catch (e: Exception) {
+ Logger.d("4. fetch not cached templates result >> failed to fetch templates >> ${e.message}")
+ notCachedTemplateMessages.forEach { message ->
+ message.messageTemplateStatus = MessageTemplateStatus.FAILED_TO_FETCH
+ }
+ }
+
+ onFetchCompleteHandler(notCachedTemplateMessages)
+ }
+
+ return mutableTemplateMessages
+ } finally {
+ Logger.d("mapTemplate[size:${messages.size}] took ${System.currentTimeMillis() - startedAt}ms")
+ }
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateParser.kt b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateParser.kt
index 420381ee..fe539a04 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateParser.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateParser.kt
@@ -41,7 +41,7 @@ internal object MessageTemplateParser {
@Throws(Exception::class)
fun parse(jsonTemplate: String): Params {
return when (val version = JSONObject(jsonTemplate).getInt(KeySet.version)) {
- 1 -> parseParams(jsonTemplate)
+ 1, 2 -> parseParams(jsonTemplate)
else -> throw RuntimeException("unsupported version. current version = $version")
}
}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateRepository.kt b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateRepository.kt
new file mode 100644
index 00000000..27c797fe
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/singleton/MessageTemplateRepository.kt
@@ -0,0 +1,120 @@
+package com.sendbird.uikit.internal.singleton
+
+import android.content.Context
+import androidx.annotation.WorkerThread
+import com.sendbird.android.SendbirdChat
+import com.sendbird.android.exception.SendbirdException
+import com.sendbird.android.params.MessageTemplateListParams
+import com.sendbird.uikit.internal.model.templates.MessageTemplate
+import com.sendbird.uikit.log.Logger
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.concurrent.thread
+
+private const val MESSAGE_TEMPLATE_KEY_PREFIX = "SB_MESSAGE_TEMPLATE_"
+private const val MESSAGE_TEMPLATE_LAST_UPDATED_TOKEN = "MESSAGE_TEMPLATE_LAST_UPDATED_TOKEN"
+private const val PREFERENCE_FILE_NAME = "com.sendbird.message.templates"
+
+/**
+ * This class is used to store templates which is used for [com.sendbird.android.channel.GroupChannel].
+ * It doesn't manage the templates for Notification. For Notification, use [NotificationTemplateRepository].
+ */
+internal class MessageTemplateRepository(context: Context) {
+ private val templateCache: MutableMap = ConcurrentHashMap()
+ private val preferences = BaseSharedPreference(context.applicationContext, PREFERENCE_FILE_NAME)
+ internal var lastCachedToken: String = ""
+ get() {
+ return field.ifEmpty {
+ field = preferences.optString(MESSAGE_TEMPLATE_LAST_UPDATED_TOKEN)
+ field
+ }
+ }
+ private set(value) {
+ if (value != field) {
+ field = value
+ preferences.putString(MESSAGE_TEMPLATE_LAST_UPDATED_TOKEN, value)
+ }
+ }
+
+ private val initialTemplateLoadLock = Any()
+
+ init {
+ thread {
+ synchronized(initialTemplateLoadLock) {
+ preferences.loadAll(
+ predicate = { key ->
+ key.startsWith(MESSAGE_TEMPLATE_KEY_PREFIX)
+ },
+ onEach = { key, value ->
+ templateCache[key] = MessageTemplate.fromJson(value.toString())
+ }
+ )
+ }
+ }
+ }
+
+ @WorkerThread
+ private fun saveToCache(template: MessageTemplate) {
+ Logger.d(">> MessageTemplateRepository::saveToCache() key=${template.templateKey}")
+ val key = template.templateKey.toMessageTemplateKey()
+ templateCache[key] = template
+ preferences.putString(key, template.toString())
+ }
+
+ fun getTemplate(key: String): MessageTemplate? {
+ Logger.d(">> MessageTemplateRepository::getTemplate() key=$key")
+ return synchronized(initialTemplateLoadLock) {
+ templateCache[key.toMessageTemplateKey()]
+ }
+ }
+
+ @WorkerThread
+ @Throws(SendbirdException::class)
+ fun requestMessageTemplatesBlocking(
+ params: MessageTemplateListParams = MessageTemplateListParams(limit = 100)
+ ): List {
+ Logger.d(">> MessageTemplateRepository::requestTemplateList()")
+ val latch = CountDownLatch(1)
+ var error: SendbirdException? = null
+ val result: AtomicReference> = AtomicReference()
+ val hasNoFilter = params.keys.isNullOrEmpty()
+ val token = if (hasNoFilter) lastCachedToken else null
+ SendbirdChat.getMessageTemplatesByToken(token, params) { messageTemplatesResult, e ->
+ error = e
+ try {
+ if (hasNoFilter) {
+ // cache the token only when there is no filter
+ messageTemplatesResult?.token.takeUnless { it.isNullOrEmpty() }?.let { token ->
+ lastCachedToken = token
+ }
+ }
+ val templateList = messageTemplatesResult?.templates?.map {
+ MessageTemplate.fromJson(it.template)
+ }
+ result.set(templateList)
+ } catch (e: Throwable) {
+ error = SendbirdException("message template list data is not valid", e)
+ } finally {
+ latch.countDown()
+ }
+ }
+ latch.await(10, TimeUnit.SECONDS)
+ error?.let { throw it }
+ return result.get().also {
+ it?.forEach { template ->
+ // convert list to map
+ saveToCache(template)
+ }
+ }
+ }
+
+ fun clearAll() {
+ lastCachedToken = ""
+ templateCache.clear()
+ preferences.clearAll()
+ }
+
+ private fun String.toMessageTemplateKey() = MESSAGE_TEMPLATE_KEY_PREFIX + this
+}
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 2c89198f..651fb3ac 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
@@ -45,17 +45,21 @@ internal abstract class BaseNotificationView @JvmOverloads internal constructor(
e?.let { throw e }
jsonTemplate?.let {
val viewParams: Params = MessageTemplateParser.parse(jsonTemplate)
- TemplateViewGenerator.inflateViews(context, viewParams) { view, params ->
- params.action?.register(
- view,
- { v, action, message ->
+ TemplateViewGenerator.inflateViews(
+ context,
+ viewParams,
+ onViewCreated = { view, params ->
+ params.action?.register(
+ view,
+ message
+ ) { v, action, message ->
sendNotificationStats(templateKey, message)
onNotificationTemplateActionHandler?.onHandleAction(
v, action, message
)
- }, message
- )
- }
+ }
+ }
+ )
}
} catch (e: Throwable) {
Logger.w("${e.printStackTrace()}")
@@ -100,9 +104,13 @@ internal abstract class BaseNotificationView @JvmOverloads internal constructor(
context.getString(R.string.sb_text_notification_fallback_description),
themeMode
).run {
- TemplateViewGenerator.inflateViews(context, this) { view, params ->
- params.action?.register(view, onNotificationTemplateActionHandler, message)
- }
+ TemplateViewGenerator.inflateViews(
+ context,
+ this,
+ onViewCreated = { view, params ->
+ params.action?.register(view, message, onNotificationTemplateActionHandler)
+ }
+ )
}
}
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
new file mode 100644
index 00000000..27e0284d
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MessageTemplateView.kt
@@ -0,0 +1,61 @@
+package com.sendbird.uikit.internal.ui.messages
+
+import android.content.Context
+import android.util.AttributeSet
+import androidx.core.content.ContextCompat
+import com.sendbird.uikit.R
+import com.sendbird.uikit.internal.extensions.ERR_MESSAGE_TEMPLATE_UNKNOWN
+import com.sendbird.uikit.internal.extensions.createFallbackViewParams
+import com.sendbird.uikit.internal.model.template_messages.Params
+import com.sendbird.uikit.internal.model.template_messages.TemplateViewGenerator
+import com.sendbird.uikit.internal.model.template_messages.ViewLifecycleHandler
+import com.sendbird.uikit.internal.ui.widgets.RoundCornerLayout
+import com.sendbird.uikit.internal.utils.TemplateViewCachePool
+
+internal class MessageTemplateView @JvmOverloads internal constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0,
+ autoAdjustHeightWhenInvisible: Boolean = true,
+) : RoundCornerLayout(context, attrs, defStyle, autoAdjustHeightWhenInvisible) {
+ init {
+ this.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent))
+ this.radius = 0f
+
+ layoutParams = LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT
+ )
+ }
+
+ @Synchronized
+ internal fun draw(
+ params: Params,
+ cacheKey: String? = null,
+ viewCachePool: TemplateViewCachePool? = null,
+ onViewCreated: ViewLifecycleHandler? = null,
+ onChildViewCreated: ViewLifecycleHandler? = null
+ ) {
+ if (this.childCount > 0) {
+ this.removeAllViews()
+ }
+
+ if (cacheKey != null) {
+ val cachedView = viewCachePool?.getScrappedView(cacheKey)
+ if (cachedView != null) {
+ this.addView(cachedView)
+ return
+ }
+ }
+
+ val view = try {
+ TemplateViewGenerator.inflateViews(context, params, onViewCreated, onChildViewCreated)
+ } catch (e: Exception) {
+ val errorMessage = context.getString(R.string.sb_text_template_message_fallback_error).format(ERR_MESSAGE_TEMPLATE_UNKNOWN)
+ val fallbackParams = context.createFallbackViewParams(errorMessage)
+ TemplateViewGenerator.inflateViews(context, fallbackParams, onViewCreated, onChildViewCreated)
+ }
+ if (cacheKey != null) viewCachePool?.cacheView(cacheKey, view)
+ this.addView(view)
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MyMessageView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MyMessageView.kt
index 55daef4a..99074a95 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MyMessageView.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/MyMessageView.kt
@@ -20,7 +20,7 @@ import com.sendbird.uikit.utils.ViewUtils
internal class MyMessageView @JvmOverloads internal constructor(
context: Context,
attrs: AttributeSet? = null,
- defStyle: Int = 0
+ defStyle: Int = R.attr.sb_widget_my_message
) : BaseMessageView(context, attrs, defStyle) {
override val binding: SbViewMyMessageComponentBinding
override val layout: View
@@ -50,6 +50,8 @@ internal class MyMessageView @JvmOverloads internal constructor(
)
binding.contentPanel.background =
DrawableUtils.setTintList(context, messageBackground, messageBackgroundTint)
+ binding.customContentPanel.background =
+ DrawableUtils.setTintList(context, messageBackground, messageBackgroundTint)
binding.emojiReactionListBackground.setBackgroundResource(emojiReactionListBackground)
} finally {
a.recycle()
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherMessageView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherMessageView.kt
index 6a2b6a1e..60d81ef3 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherMessageView.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherMessageView.kt
@@ -21,7 +21,7 @@ import com.sendbird.uikit.utils.ViewUtils
internal class OtherMessageView @JvmOverloads internal constructor(
context: Context,
attrs: AttributeSet? = null,
- defStyle: Int = 0
+ defStyle: Int = R.attr.sb_widget_other_message
) : BaseMessageView(context, attrs, defStyle) {
override val binding: SbViewOtherMessageComponentBinding
override val layout: View
@@ -55,6 +55,8 @@ internal class OtherMessageView @JvmOverloads internal constructor(
)
binding.contentPanel.background =
DrawableUtils.setTintList(context, messageBackground, messageBackgroundTint)
+ binding.customContentPanel.background =
+ DrawableUtils.setTintList(context, messageBackground, messageBackgroundTint)
binding.emojiReactionListBackground.setBackgroundResource(emojiReactionListBackground)
} finally {
a.recycle()
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
new file mode 100644
index 00000000..0291ddce
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/messages/OtherTemplateMessageView.kt
@@ -0,0 +1,237 @@
+package com.sendbird.uikit.internal.ui.messages
+
+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 com.sendbird.android.message.BaseMessage
+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.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.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.saveParamsFromTemplate
+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(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = 0
+) : BaseMessageView(context, attrs, defStyle) {
+ override val binding: SbViewOtherTemplateMessageComponentBinding
+ override val layout: View
+ get() = binding.root
+ private val sentAtAppearance: Int
+ private val nicknameAppearance: Int
+ var onFeedbackRatingClickListener: OnFeedbackRatingClickListener? = null
+
+ 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
+ )
+ nicknameAppearance = a.getResourceId(
+ R.styleable.MessageView_sb_message_sender_name_text_appearance,
+ R.style.SendbirdCaption1OnLight02
+ )
+ } finally {
+ a.recycle()
+ }
+ }
+
+ 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))
+
+ 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)
+ 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.setPadding(binding.root.paddingLeft, paddingTop, binding.root.paddingRight, paddingBottom)
+ drawTemplateView(message, viewCachePool, shouldShowSentAt, handler)
+
+ val shouldHideFeedback = !params.channelConfig.enableFeedback ||
+ (message.hasParentMessage() && params.channelConfig.replyType == ReplyType.THREAD)
+
+ binding.feedback.drawFeedback(message, shouldHideFeedback) { _, rating ->
+ onFeedbackRatingClickListener?.onFeedbackClicked(message, rating)
+ }
+ }
+
+ private 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)
+ context.createFallbackViewParams(errorMessage)
+ }
+ MessageTemplateStatus.LOADING -> {
+ changeContainerType(MessageTemplateContainerType.DEFAULT, shouldShowSentAt)
+ val loadingView = context.createTemplateMessageLoadingView()
+ binding.messageTemplateView.removeAllViews()
+ binding.messageTemplateView.addView(loadingView)
+ return
+ }
+ 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 {
+ 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}"
+ binding.messageTemplateView.draw(
+ params,
+ cacheKey,
+ viewCachePool,
+ onViewCreated = { view, params ->
+ params.action?.register(view, message) { view, action, message ->
+ handler?.onHandleAction(view, action, message)
+ }
+ },
+ onChildViewCreated = { view, params ->
+ params.action?.register(view, 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/viewholders/OtherTemplateMessageViewHolder.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherTemplateMessageViewHolder.kt
new file mode 100644
index 00000000..baff3c71
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/viewholders/OtherTemplateMessageViewHolder.kt
@@ -0,0 +1,44 @@
+package com.sendbird.uikit.internal.ui.viewholders
+
+import android.view.View
+import com.sendbird.android.channel.BaseChannel
+import com.sendbird.android.message.BaseMessage
+import com.sendbird.uikit.activities.viewholder.MessageViewHolder
+import com.sendbird.uikit.consts.ClickableViewIdentifier
+import com.sendbird.uikit.databinding.SbViewOtherTemplateMessageBinding
+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(
+ val binding: SbViewOtherTemplateMessageBinding,
+ messageListUIParams: MessageListUIParams
+) : MessageViewHolder(binding.root, messageListUIParams) {
+ lateinit var templateViewCachePool: TemplateViewCachePool
+
+ private val messageView: OtherTemplateMessageView
+ get() = binding.otherMessageView
+
+ var onMessageTemplateActionHandler: OnMessageTemplateActionHandler? = null
+
+ var onFeedbackRatingClickListener: OnFeedbackRatingClickListener? = null
+
+ override fun bind(channel: BaseChannel, message: BaseMessage, params: MessageListUIParams) {
+ binding.otherMessageView.messageUIConfig = messageUIConfig
+ messageView.drawMessage(message, params, templateViewCachePool) { view, action, message ->
+ onMessageTemplateActionHandler?.onHandleAction(view, action, message)
+ }
+
+ messageView.onFeedbackRatingClickListener = OnFeedbackRatingClickListener { message, rating ->
+ this.onFeedbackRatingClickListener?.onFeedbackClicked(message, rating)
+ }
+ }
+
+ override fun getClickableViewMap(): Map {
+ return mapOf(
+ ClickableViewIdentifier.Profile.name to messageView.binding.ivProfileView,
+ )
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/CarouselViewItemDecoration.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/CarouselViewItemDecoration.kt
new file mode 100644
index 00000000..c786ea6d
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/CarouselViewItemDecoration.kt
@@ -0,0 +1,15 @@
+package com.sendbird.uikit.internal.ui.widgets
+
+import android.graphics.Rect
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+class CarouselViewItemDecoration(private val space: Int) : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
+ super.getItemOffsets(outRect, view, parent, state)
+
+ if (parent.getChildAdapterPosition(view) != parent.adapter?.itemCount?.minus(1)) {
+ outRect.right = space
+ }
+ }
+}
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 613a1303..64eebc69 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
@@ -16,7 +16,8 @@ import com.sendbird.uikit.internal.interfaces.ViewRoundable
internal open class RoundCornerLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
+ defStyleAttr: Int = 0,
+ private val autoAdjustHeightWhenInvisible: Boolean = true
) : LinearLayout(context, attrs, defStyleAttr), ViewRoundable {
private val rectF: RectF = RectF()
private val path: Path = Path()
@@ -50,9 +51,11 @@ internal open class RoundCornerLayout @JvmOverloads constructor(
// In the template message syntax, the views that are not drawn have to hide.
// onSizeChanged() and onLayout() do not update the view even if the visibility changes, so the status of the view must be updated once again.
// Logger.i("-- parent view's width=${(parent as View).width}, x=$x, measureWidth=$width, visible=$visibility")
- val visibility = if (x <= -width || x >= (parent as View).width) GONE else VISIBLE
- post {
- this.visibility = visibility
+ if (autoAdjustHeightWhenInvisible) {
+ val visibility = if (x <= -width || x >= (parent as View).width) GONE else VISIBLE
+ post {
+ this.visibility = visibility
+ }
}
rectF.set(0f, 0f, w.toFloat(), h.toFloat())
resetPath()
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/SingleMenuItemView.kt b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/SingleMenuItemView.kt
index 2baf7ea6..fbd4fbdc 100644
--- a/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/SingleMenuItemView.kt
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/ui/widgets/SingleMenuItemView.kt
@@ -13,6 +13,7 @@ import androidx.annotation.DrawableRes
import androidx.appcompat.content.res.AppCompatResources
import com.sendbird.uikit.R
import com.sendbird.uikit.SendbirdUIKit
+import com.sendbird.uikit.consts.SingleMenuType
import com.sendbird.uikit.databinding.SbViewSingleMenuItemBinding
import com.sendbird.uikit.internal.extensions.setAppearance
import com.sendbird.uikit.utils.DrawableUtils
@@ -26,29 +27,6 @@ internal class SingleMenuItemView @JvmOverloads constructor(
val layout: View
get() = this
- enum class Type(var value: Int) {
- /**
- * A type that has an action button to redirect next page.
- */
- NEXT(0),
-
- /**
- * A type that has a switch button to toggle some action.
- */
- SWITCH(1),
-
- /**
- * A type that has no next action.
- */
- NONE(2);
-
- companion object {
- // TODO (Remove : after all codes are converted as kotlin this annotation doesn't need)
- @JvmStatic
- fun from(value: Int): Type = values().firstOrNull { it.value == value } ?: NONE
- }
- }
-
override fun setOnClickListener(listener: OnClickListener?) = binding.vgMenuItem.setOnClickListener(listener)
override fun setOnLongClickListener(listener: OnLongClickListener?) =
binding.vgMenuItem.setOnLongClickListener(listener)
@@ -76,14 +54,14 @@ internal class SingleMenuItemView @JvmOverloads constructor(
binding.scSwitch.isChecked = checked
}
- fun setMenuType(type: Type) {
+ fun setMenuType(type: SingleMenuType) {
when (type) {
- Type.NEXT -> {
+ SingleMenuType.NEXT -> {
binding.scSwitch.visibility = GONE
binding.ivNext.visibility = VISIBLE
binding.tvDescription.visibility = VISIBLE
}
- Type.SWITCH -> {
+ SingleMenuType.SWITCH -> {
binding.scSwitch.visibility = VISIBLE
binding.ivNext.visibility = GONE
binding.tvDescription.visibility = GONE
@@ -100,6 +78,10 @@ internal class SingleMenuItemView @JvmOverloads constructor(
binding.tvDescription.text = description
}
+ fun setIconVisibility(visibility: Int) {
+ binding.ivIcon.visibility = visibility
+ }
+
init {
val a = context.theme.obtainStyledAttributes(attrs, R.styleable.SingleMenuItemView, defStyle, 0)
try {
@@ -144,9 +126,42 @@ internal class SingleMenuItemView @JvmOverloads constructor(
)
binding.scSwitch.trackTintList = AppCompatResources.getColorStateList(context, switchTrackTint)
binding.scSwitch.thumbTintList = AppCompatResources.getColorStateList(context, switchThumbTint)
- setMenuType(Type.from(type))
+ setMenuType(SingleMenuType.from(type))
+
+ layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT).apply {
+ height = context.resources.getDimensionPixelSize(R.dimen.sb_size_56)
+ }
} finally {
a.recycle()
}
}
+
+ companion object {
+ @JvmStatic
+ fun createMenuView(
+ context: Context,
+ title: String,
+ description: String?,
+ type: SingleMenuType,
+ @DrawableRes iconResId: Int,
+ @ColorRes iconTintResId: Int
+ ): View {
+ return SingleMenuItemView(context).apply {
+ setName(title)
+ setMenuType(type)
+ description?.let { setDescription(it) }
+
+ if (iconResId != 0) {
+ setIcon(iconResId)
+ setIconVisibility(VISIBLE)
+ } else {
+ setIconVisibility(GONE)
+ }
+
+ if (iconTintResId != 0) {
+ setIconTint(AppCompatResources.getColorStateList(context, iconTintResId))
+ }
+ }
+ }
+ }
}
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 7afc4ac7..75b39e13 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
@@ -5,19 +5,26 @@ import android.content.Context
import android.text.TextUtils
import android.util.AttributeSet
import android.view.Gravity
+import android.view.MotionEvent
import android.widget.LinearLayout
import android.widget.TextView
import androidx.appcompat.widget.AppCompatTextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
import com.sendbird.uikit.R
+import com.sendbird.uikit.internal.adapter.CarouselChildViewAdapter
import com.sendbird.uikit.internal.extensions.addRipple
import com.sendbird.uikit.internal.extensions.intToDp
import com.sendbird.uikit.internal.extensions.setAppearance
import com.sendbird.uikit.internal.model.template_messages.BoxViewParams
import com.sendbird.uikit.internal.model.template_messages.ButtonViewParams
+import com.sendbird.uikit.internal.model.template_messages.CarouselViewParams
import com.sendbird.uikit.internal.model.template_messages.ImageButtonViewParams
import com.sendbird.uikit.internal.model.template_messages.ImageViewParams
import com.sendbird.uikit.internal.model.template_messages.Orientation
import com.sendbird.uikit.internal.model.template_messages.TextViewParams
+import com.sendbird.uikit.internal.model.template_messages.ViewLifecycleHandler
+import com.sendbird.uikit.internal.utils.CarouselLeftSnapHelper
@SuppressLint("ViewConstructor")
internal open class Text @JvmOverloads constructor(
@@ -180,3 +187,63 @@ internal open class Box @JvmOverloads constructor(
params.viewStyle.apply(this)
}
}
+
+@SuppressLint("ClickableViewAccessibility")
+internal class CarouselView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : RoundCornerLayout(context, attrs, defStyleAttr) {
+ val recyclerView: RecyclerView
+ 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,
+ LayoutParams.WRAP_CONTENT
+ )
+
+ recyclerView = object : RecyclerView(context) {
+ // 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
+ }
+
+ return super.onTouchEvent(e)
+ }
+ }.apply {
+ layoutParams = LayoutParams(
+ LayoutParams.MATCH_PARENT,
+ LayoutParams.WRAP_CONTENT
+ )
+ layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+
+ setPadding(startPadding, paddingTop, paddingRight, 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)
+ 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)
+ params.applyLayoutParams(context, layoutParams, orientation)
+
+ // Currently, viewStyle is not used in CarouselView.
+ // params.viewStyle.apply(this, true)
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/utils/CarouselLeftSnapHelper.kt b/uikit/src/main/java/com/sendbird/uikit/internal/utils/CarouselLeftSnapHelper.kt
new file mode 100644
index 00000000..6e18a9af
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/utils/CarouselLeftSnapHelper.kt
@@ -0,0 +1,72 @@
+package com.sendbird.uikit.internal.utils
+
+import android.view.View
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.LinearSnapHelper
+import androidx.recyclerview.widget.OrientationHelper
+import androidx.recyclerview.widget.RecyclerView
+import kotlin.math.abs
+
+internal class CarouselLeftSnapHelper : LinearSnapHelper() {
+ private var horizontalHelper: OrientationHelper? = null
+ private fun getHorizontalHelper(layoutManager: RecyclerView.LayoutManager): OrientationHelper {
+ val helper = horizontalHelper
+ return if (helper == null) {
+ OrientationHelper.createHorizontalHelper(layoutManager)
+ } else {
+ if (helper.layoutManager !== layoutManager) {
+ OrientationHelper.createHorizontalHelper(layoutManager).also {
+ horizontalHelper = it
+ }
+ } else {
+ helper
+ }
+ }
+ }
+
+ override fun findSnapView(layoutManager: RecyclerView.LayoutManager?): View? {
+ if (layoutManager !is LinearLayoutManager) return null
+ if (layoutManager.findLastVisibleItemPosition() == layoutManager.itemCount - 1) {
+ return null
+ }
+
+ return findLeftClosestView(layoutManager, getHorizontalHelper(layoutManager))
+ }
+
+ override fun calculateDistanceToFinalSnap(layoutManager: RecyclerView.LayoutManager, targetView: View): IntArray? {
+ if (layoutManager.canScrollVertically()) return super.calculateDistanceToFinalSnap(layoutManager, targetView)
+ val out = IntArray(2)
+ out[0] = distanceToLeft(targetView, getHorizontalHelper(layoutManager))
+ out[1] = 0 // vertical position always zero
+ return out
+ }
+
+ private fun distanceToLeft(targetView: View, orientationHelper: OrientationHelper): Int {
+ val childLeft: Int = orientationHelper.getDecoratedStart(targetView)
+ val containerLeft: Int = orientationHelper.startAfterPadding
+ return childLeft - containerLeft
+ }
+
+ private fun findLeftClosestView(
+ layoutManager: RecyclerView.LayoutManager,
+ helper: OrientationHelper
+ ): View? {
+ val childCount = layoutManager.childCount
+ if (childCount == 0) {
+ return null
+ }
+ var closestChild: View? = null
+ var absClosest = Int.MAX_VALUE
+ for (i in 0 until childCount) {
+ val child = layoutManager.getChildAt(i) ?: continue
+ val distanceToLeft = distanceToLeft(child, helper)
+ val absDistance = abs(distanceToLeft)
+ if (absDistance < absClosest) {
+ absClosest = absDistance
+ closestChild = child
+ }
+ }
+
+ return closestChild
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/internal/utils/TemplateViewCachePool.kt b/uikit/src/main/java/com/sendbird/uikit/internal/utils/TemplateViewCachePool.kt
new file mode 100644
index 00000000..10f7efdc
--- /dev/null
+++ b/uikit/src/main/java/com/sendbird/uikit/internal/utils/TemplateViewCachePool.kt
@@ -0,0 +1,28 @@
+package com.sendbird.uikit.internal.utils
+
+import android.view.View
+import com.sendbird.uikit.log.Logger
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Be careful that this TemplateViewCachePool is not referenced by multiple objects.
+ * Consider java's garbage collection policy so that when a component with a cache pool is freed from memory,
+ * this cache pool is also freed.
+ */
+internal class TemplateViewCachePool {
+ private val viewCachePool: MutableMap> = ConcurrentHashMap()
+
+ /**
+ * get scrapped view which is not attached to any parent from cache pool
+ */
+ internal fun getScrappedView(key: String): View? {
+ val views = viewCachePool[key]
+ return views?.firstOrNull { it.parent == null }.also {
+ Logger.d("key: $key, view cache ${if (it != null) "hit" else "missed"}")
+ }
+ }
+
+ internal fun cacheView(key: String, view: View) {
+ viewCachePool.getOrPut(key) { mutableListOf() }.add(view)
+ }
+}
diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/ChannelSettingsMenuComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/ChannelSettingsMenuComponent.java
index 742dcafc..72c0b047 100644
--- a/uikit/src/main/java/com/sendbird/uikit/modules/components/ChannelSettingsMenuComponent.java
+++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/ChannelSettingsMenuComponent.java
@@ -6,7 +6,10 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.view.ContextThemeWrapper;
@@ -15,16 +18,24 @@
import com.sendbird.android.channel.Role;
import com.sendbird.uikit.R;
import com.sendbird.uikit.SendbirdUIKit;
+import com.sendbird.uikit.consts.SingleMenuType;
import com.sendbird.uikit.consts.StringSet;
+import com.sendbird.uikit.interfaces.MenuViewProvider;
import com.sendbird.uikit.interfaces.OnItemClickListener;
import com.sendbird.uikit.internal.ui.widgets.SingleMenuItemView;
+import com.sendbird.uikit.log.Logger;
import com.sendbird.uikit.model.configurations.ChannelSettingConfig;
import com.sendbird.uikit.model.configurations.UIKitConfig;
import com.sendbird.uikit.utils.ChannelUtils;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
/**
* This class creates and performs a view corresponding the channel settings menu area in Sendbird UIKit.
- *
* since 3.0.0
*/
public class ChannelSettingsMenuComponent {
@@ -52,9 +63,28 @@ public enum Menu {
/**
* A menu to search messages in the current channel.
*/
- SEARCH_IN_CHANNEL
+ SEARCH_IN_CHANNEL,
+
+ /**
+ * A custom menu.
+ */
+ CUSTOM,
}
+ /**
+ * A collection of default menus.
+ * since 3.16.0
+ */
+ public final static List defaultMenuSet = Collections.unmodifiableList(
+ Arrays.asList(
+ Menu.MODERATIONS,
+ Menu.NOTIFICATIONS,
+ Menu.MEMBERS,
+ Menu.SEARCH_IN_CHANNEL,
+ Menu.LEAVE_CHANNEL
+ )
+ );
+
@NonNull
private final Params params;
@Nullable
@@ -63,9 +93,11 @@ public enum Menu {
@Nullable
protected OnItemClickListener menuClickListener;
+ @NonNull
+ private final Map defaultMenuViews = new HashMap<>();
+
/**
* Constructor
- *
* since 3.0.0
*/
public ChannelSettingsMenuComponent() {
@@ -111,46 +143,42 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla
final TypedValue values = new TypedValue();
context.getTheme().resolveAttribute(R.attr.sb_component_channel_settings_menu, values, true);
final Context menuThemeContext = new ContextThemeWrapper(context, values.resourceId);
- final LayoutInflater menuInflater = inflater.cloneInContext(menuThemeContext);
- final View view = menuInflater.inflate(R.layout.sb_view_channel_settings_menu, parent, false);
-
- final SingleMenuItemView moderationsItemView = view.findViewById(R.id.moderations);
- moderationsItemView.setName(context.getString(R.string.sb_text_channel_settings_moderations));
- moderationsItemView.setMenuType(SingleMenuItemView.Type.NEXT);
- moderationsItemView.setIcon(R.drawable.icon_moderations);
- moderationsItemView.setVisibility(View.GONE);
-
- final SingleMenuItemView notificationItemView = view.findViewById(R.id.notification);
- notificationItemView.setName(context.getString(R.string.sb_text_channel_settings_notification));
- notificationItemView.setMenuType(SingleMenuItemView.Type.NEXT);
- notificationItemView.setIcon(R.drawable.icon_notifications);
-
- final SingleMenuItemView membersItemView = view.findViewById(R.id.members);
- membersItemView.setName(context.getString(R.string.sb_text_channel_settings_members));
- membersItemView.setMenuType(SingleMenuItemView.Type.NEXT);
- membersItemView.setIcon(R.drawable.icon_members);
-
- final SingleMenuItemView messageSearchItemView = view.findViewById(R.id.messageSearch);
- messageSearchItemView.setName(context.getString(R.string.sb_text_channel_settings_message_search));
- messageSearchItemView.setMenuType(SingleMenuItemView.Type.NONE);
- messageSearchItemView.setIcon(R.drawable.icon_search);
- messageSearchItemView.setVisibility(
- ChannelSettingConfig.getEnableMessageSearch(params.channelSettingConfig) ? View.VISIBLE : View.GONE);
-
- final SingleMenuItemView leaveItemView = view.findViewById(R.id.leave);
- leaveItemView.setName(context.getString(R.string.sb_text_channel_settings_leave_channel));
- leaveItemView.setMenuType(SingleMenuItemView.Type.NONE);
- leaveItemView.setIcon(R.drawable.icon_leave);
- leaveItemView.setIconTint(SendbirdUIKit.getDefaultThemeMode().getErrorTintColorStateList(context));
-
- moderationsItemView.setOnClickListener(v -> onMenuClicked(v, Menu.MODERATIONS));
- notificationItemView.setOnClickListener(v -> onMenuClicked(v, Menu.NOTIFICATIONS));
- notificationItemView.setOnActionMenuClickListener(v -> onMenuClicked(v, Menu.NOTIFICATIONS));
- membersItemView.setOnClickListener(v -> onMenuClicked(v, Menu.MEMBERS));
- messageSearchItemView.setOnClickListener(v -> onMenuClicked(v, Menu.SEARCH_IN_CHANNEL));
- leaveItemView.setOnClickListener(v -> onMenuClicked(v, Menu.LEAVE_CHANNEL));
- this.menuView = view;
- return view;
+ final LinearLayout layout = new LinearLayout(menuThemeContext);
+ layout.setOrientation(LinearLayout.VERTICAL);
+
+ for (int i = 0; i < params.getMenuList().size(); i++) {
+ final Menu menu = params.getMenuList().get(i);
+ final View menuView = createMenuView(menuThemeContext, menu, i);
+ if (menu != Menu.CUSTOM) {
+ menuView.setOnClickListener(v -> onMenuClicked(v, menu));
+ defaultMenuViews.put(menu, (SingleMenuItemView) menuView);
+ }
+ layout.addView(menuView);
+ }
+ this.menuView = layout;
+ return layout;
+ }
+
+ /**
+ * Creates a custom menu view.
+ *
+ * @param context The {@code Context} this component is currently associated with
+ * @param title The title of the menu
+ * @param type The type of the menu
+ * @param iconResId The icon resource id of the menu
+ * @return The custom menu view
+ * since 3.16.0
+ */
+ @NonNull
+ public View createMenuView(
+ @NonNull Context context,
+ @NonNull String title,
+ @Nullable String description,
+ @NonNull SingleMenuType type,
+ @DrawableRes int iconResId,
+ @ColorRes int iconTintResId
+ ) {
+ return SingleMenuItemView.createMenuView(context, title, description, type, iconResId, iconTintResId);
}
/**
@@ -162,22 +190,32 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla
public void notifyChannelChanged(@NonNull GroupChannel channel) {
if (this.menuView == null) return;
+ final SingleMenuItemView membersItemView = defaultMenuViews.get(Menu.MEMBERS);
+ if (membersItemView != null) {
+ membersItemView.setDescription(ChannelUtils.makeMemberCountText(channel.getMemberCount()).toString());
+ }
- final SingleMenuItemView membersItemView = menuView.findViewById(R.id.members);
- membersItemView.setDescription(ChannelUtils.makeMemberCountText(channel.getMemberCount()).toString());
- GroupChannel.PushTriggerOption pushTriggerOption = channel.getMyPushTriggerOption();
- final SingleMenuItemView notificationItemView = menuView.findViewById(R.id.notification);
- notificationItemView.setDescription(ChannelUtils.makePushSettingStatusText(menuView.getContext(), pushTriggerOption));
+ final SingleMenuItemView notificationItemView = defaultMenuViews.get(Menu.NOTIFICATIONS);
+ if (notificationItemView != null) {
+ final GroupChannel.PushTriggerOption pushTriggerOption = channel.getMyPushTriggerOption();
+ notificationItemView.setDescription(ChannelUtils.makePushSettingStatusText(menuView.getContext(), pushTriggerOption));
+ }
- final SingleMenuItemView moderationsItemView = menuView.findViewById(R.id.moderations);
- moderationsItemView.setVisibility(channel.getMyRole() == Role.OPERATOR ? View.VISIBLE : View.GONE);
- final SingleMenuItemView messageSearchItemView = menuView.findViewById(R.id.messageSearch);
- messageSearchItemView.setVisibility(
- ChannelSettingConfig.getEnableMessageSearch(params.channelSettingConfig) ? View.VISIBLE : View.GONE);
+ final SingleMenuItemView moderationsItemView = defaultMenuViews.get(Menu.MODERATIONS);
+ if (moderationsItemView != null) {
+ moderationsItemView.setVisibility(channel.getMyRole() == Role.OPERATOR ? View.VISIBLE : View.GONE);
+ }
+ final SingleMenuItemView messageSearchItemView = defaultMenuViews.get(Menu.SEARCH_IN_CHANNEL);
+ if (messageSearchItemView != null) {
+ messageSearchItemView.setVisibility(
+ ChannelSettingConfig.getEnableMessageSearch(params.channelSettingConfig) ? View.VISIBLE : View.GONE);
+ }
}
/**
* Register a callback to be invoked when the item of the menu is clicked.
+ * The click event about the {@link Menu#CUSTOM} menu won’t be called.
+ * If you want to handle the {@link Menu#CUSTOM} menu, you should handle it yourself after creating a custom menu view.
*
* @param menuClickListener The callback that will run
* @see Menu
@@ -199,6 +237,57 @@ protected void onMenuClicked(@NonNull View view, @NonNull Menu menu) {
if (this.menuClickListener != null) this.menuClickListener.onItemClick(view, 0, menu);
}
+ @NonNull
+ private View createMenuView(@NonNull Context context, @NonNull Menu menu, int position) {
+ if (menu == Menu.CUSTOM) {
+ final MenuViewProvider provider = params.getCustomMenuViewProvider();
+ if (provider != null) {
+ return provider.provideMenuView(context, position);
+ }
+ Logger.d("MenuViewProvider is not set. Creating a default View.");
+ return new View(context);
+ }
+
+ return createDefaultMenuView(context, menu);
+ }
+
+ @NonNull
+ private View createDefaultMenuView(@NonNull Context context, @NonNull Menu menu) {
+ final SingleMenuItemView menuView = new SingleMenuItemView(context);
+ switch (menu) {
+ case MODERATIONS:
+ menuView.setName(context.getString(R.string.sb_text_channel_settings_moderations));
+ menuView.setMenuType(SingleMenuType.NEXT);
+ menuView.setIcon(R.drawable.icon_moderations);
+ menuView.setVisibility(View.GONE);
+ break;
+ case NOTIFICATIONS:
+ menuView.setName(context.getString(R.string.sb_text_channel_settings_notification));
+ menuView.setMenuType(SingleMenuType.NEXT);
+ menuView.setIcon(R.drawable.icon_notifications);
+ break;
+ case MEMBERS:
+ menuView.setName(context.getString(R.string.sb_text_channel_settings_members));
+ menuView.setMenuType(SingleMenuType.NEXT);
+ menuView.setIcon(R.drawable.icon_members);
+ break;
+ case LEAVE_CHANNEL:
+ menuView.setName(context.getString(R.string.sb_text_channel_settings_leave_channel));
+ menuView.setMenuType(SingleMenuType.NONE);
+ menuView.setIcon(R.drawable.icon_leave);
+ menuView.setIconTint(SendbirdUIKit.getDefaultThemeMode().getErrorTintColorStateList(context));
+ break;
+ case SEARCH_IN_CHANNEL:
+ menuView.setName(context.getString(R.string.sb_text_channel_settings_message_search));
+ menuView.setMenuType(SingleMenuType.NONE);
+ menuView.setIcon(R.drawable.icon_search);
+ menuView.setVisibility(
+ ChannelSettingConfig.getEnableMessageSearch(params.channelSettingConfig) ? View.VISIBLE : View.GONE);
+ break;
+ }
+ return menuView;
+ }
+
/**
* A collection of parameters, which can be applied to a default View. The values of params are not dynamically applied at runtime.
* Params cannot be created directly, and it is automatically created together when components are created.
@@ -211,6 +300,12 @@ public static class Params {
@NonNull
private ChannelSettingConfig channelSettingConfig = UIKitConfig.getGroupChannelSettingConfig();
+ @NonNull
+ private List menuList = defaultMenuSet;
+
+ @Nullable
+ private MenuViewProvider customMenuViewProvider;
+
/**
* Constructor
*
@@ -251,6 +346,40 @@ public ChannelSettingConfig getChannelSettingConfig() {
return channelSettingConfig;
}
+ /**
+ * Sets the list of menus to be displayed in the channel settings menu.
+ * If the CUSTOM menu is included in the list, you must set the {@link MenuViewProvider} to create a custom menu view.
+ *
+ * @param menuList A list of settings menus in guaranteed with order.
+ * @param provider The provider to create custom menu view.
+ * since 3.16.0
+ */
+ public void setMenuList(@NonNull List menuList, @Nullable MenuViewProvider provider) {
+ this.menuList = menuList;
+ this.customMenuViewProvider = provider;
+ }
+
+ /**
+ * Returns the list of menus to be displayed in the channel settings menu.
+ *
+ * @return A list of settings menus in guaranteed with order.
+ * since 3.16.0
+ */
+ @NonNull
+ public List getMenuList() {
+ return menuList;
+ }
+
+ /**
+ * Returns the {@link MenuViewProvider} to create a custom menu view.
+ *
+ * @return The provider to create custom menu view.
+ * since 3.16.0
+ */
+ @Nullable
+ public MenuViewProvider getCustomMenuViewProvider() {
+ return customMenuViewProvider;
+ }
/**
* Apply data that matches keys mapped to Params' properties.
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 749185e2..7fcf9c97 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
@@ -14,6 +14,8 @@
import com.sendbird.uikit.interfaces.OnItemClickListener;
import com.sendbird.uikit.interfaces.OnItemLongClickListener;
import com.sendbird.uikit.interfaces.FormSubmitButtonClickListener;
+import com.sendbird.uikit.interfaces.OnMessageTemplateActionHandler;
+import com.sendbird.uikit.model.Action;
import com.sendbird.uikit.model.MessageListUIParams;
import com.sendbird.uikit.providers.AdapterProviders;
@@ -35,6 +37,9 @@ public class MessageListComponent extends BaseMessageListComponent onMenuItemClicked(v, ModerationMenu.OPERATORS));
- this.mutedMembers.setMenuType(SingleMenuItemView.Type.NEXT);
+ this.mutedMembers.setMenuType(SingleMenuType.NEXT);
this.mutedMembers.setIcon(R.drawable.icon_mute);
this.mutedMembers.setName(listThemeContext.getString(R.string.sb_text_menu_muted_members));
this.mutedMembers.setNextActionDrawable(R.drawable.icon_chevron_right);
@@ -156,14 +157,14 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla
this.mutedMembers.setLayoutParams(layoutParams);
this.mutedMembers.setOnClickListener(v -> onMenuItemClicked(v, ModerationMenu.MUTED_MEMBERS));
- this.bannedMembers.setMenuType(SingleMenuItemView.Type.NEXT);
+ this.bannedMembers.setMenuType(SingleMenuType.NEXT);
this.bannedMembers.setIcon(R.drawable.icon_ban);
this.bannedMembers.setName(listThemeContext.getString(R.string.sb_text_menu_banned_users));
this.bannedMembers.setNextActionDrawable(R.drawable.icon_chevron_right);
this.bannedMembers.setLayoutParams(layoutParams);
this.bannedMembers.setOnClickListener(v -> onMenuItemClicked(v, ModerationMenu.BANNED_MEMBERS));
- this.frozenState.setMenuType(SingleMenuItemView.Type.SWITCH);
+ this.frozenState.setMenuType(SingleMenuType.SWITCH);
this.frozenState.setIcon(R.drawable.icon_freeze);
this.frozenState.setName(listThemeContext.getString(R.string.sb_text_menu_freeze_channel));
this.frozenState.setNextActionDrawable(R.drawable.icon_chevron_right);
diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelModerationListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelModerationListComponent.java
index f8f55bd1..71f84089 100644
--- a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelModerationListComponent.java
+++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelModerationListComponent.java
@@ -14,6 +14,7 @@
import androidx.core.widget.NestedScrollView;
import com.sendbird.uikit.R;
+import com.sendbird.uikit.consts.SingleMenuType;
import com.sendbird.uikit.interfaces.OnMenuItemClickListener;
import com.sendbird.uikit.internal.ui.widgets.SingleMenuItemView;
@@ -133,21 +134,21 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla
this.mutedParticipants = new SingleMenuItemView(listThemeContext);
this.bannedParticipants = new SingleMenuItemView(listThemeContext);
- this.operators.setMenuType(SingleMenuItemView.Type.NEXT);
+ this.operators.setMenuType(SingleMenuType.NEXT);
this.operators.setIcon(R.drawable.icon_operator);
this.operators.setName(listThemeContext.getString(R.string.sb_text_menu_operators));
this.operators.setNextActionDrawable(R.drawable.icon_chevron_right);
this.operators.setLayoutParams(layoutParams);
this.operators.setOnClickListener(v -> onMenuItemClicked(v, ModerationMenu.OPERATORS));
- this.mutedParticipants.setMenuType(SingleMenuItemView.Type.NEXT);
+ this.mutedParticipants.setMenuType(SingleMenuType.NEXT);
this.mutedParticipants.setIcon(R.drawable.icon_mute);
this.mutedParticipants.setName(listThemeContext.getString(R.string.sb_text_menu_muted_participants));
this.mutedParticipants.setNextActionDrawable(R.drawable.icon_chevron_right);
this.mutedParticipants.setLayoutParams(layoutParams);
this.mutedParticipants.setOnClickListener(v -> onMenuItemClicked(v, ModerationMenu.MUTED_PARTICIPANTS));
- this.bannedParticipants.setMenuType(SingleMenuItemView.Type.NEXT);
+ this.bannedParticipants.setMenuType(SingleMenuType.NEXT);
this.bannedParticipants.setIcon(R.drawable.icon_ban);
this.bannedParticipants.setName(listThemeContext.getString(R.string.sb_text_menu_banned_users));
this.bannedParticipants.setNextActionDrawable(R.drawable.icon_chevron_right);
diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelSettingsMenuComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelSettingsMenuComponent.java
index 33dd80ea..f23a5f67 100644
--- a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelSettingsMenuComponent.java
+++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelSettingsMenuComponent.java
@@ -1,12 +1,16 @@
package com.sendbird.uikit.modules.components;
+
import android.content.Context;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.LinearLayout;
+import androidx.annotation.ColorRes;
+import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.content.res.AppCompatResources;
@@ -16,10 +20,19 @@
import com.sendbird.android.channel.OpenChannel;
import com.sendbird.uikit.R;
import com.sendbird.uikit.SendbirdUIKit;
+import com.sendbird.uikit.consts.SingleMenuType;
+import com.sendbird.uikit.interfaces.MenuViewProvider;
import com.sendbird.uikit.interfaces.OnItemClickListener;
import com.sendbird.uikit.internal.ui.widgets.SingleMenuItemView;
+import com.sendbird.uikit.log.Logger;
import com.sendbird.uikit.utils.ChannelUtils;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
/**
* This class creates and performs a view corresponding the open channel settings menu area in Sendbird UIKit.
*
@@ -29,7 +42,6 @@ public class OpenChannelSettingsMenuComponent {
public enum Menu {
/**
* A menu of Moderations to users or members control.
- *
* since 3.1.0
*/
MODERATIONS,
@@ -40,9 +52,26 @@ public enum Menu {
/**
* A menu to delete the current channel.
*/
- DELETE_CHANNEL
+ DELETE_CHANNEL,
+ /**
+ * A custom menu.
+ * since 3.16.0
+ */
+ CUSTOM,
}
+ /**
+ * A collection of default menus.
+ * since 3.16.0
+ */
+ public final static List defaultMenuSet = Collections.unmodifiableList(
+ Arrays.asList(
+ Menu.MODERATIONS,
+ Menu.PARTICIPANTS,
+ Menu.DELETE_CHANNEL
+ )
+ );
+
@NonNull
private final Params params;
@Nullable
@@ -51,6 +80,9 @@ public enum Menu {
@Nullable
protected OnItemClickListener menuClickListener;
+ @NonNull
+ private final Map defaultMenuViews = new HashMap<>();
+
/**
* Constructor
*
@@ -99,31 +131,41 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla
final TypedValue values = new TypedValue();
context.getTheme().resolveAttribute(R.attr.sb_component_open_channel_settings_menu, values, true);
final Context menuThemeContext = new ContextThemeWrapper(context, values.resourceId);
- final LayoutInflater menuInflater = inflater.cloneInContext(menuThemeContext);
-
- final View view = menuInflater.inflate(R.layout.sb_view_open_channel_settings_menu, parent, false);
- final SingleMenuItemView moderationsItemView = view.findViewById(R.id.moderations);
- final SingleMenuItemView participantsItemView = view.findViewById(R.id.participants);
- final SingleMenuItemView deleteItemView = view.findViewById(R.id.delete);
-
- moderationsItemView.setName(context.getString(R.string.sb_text_channel_settings_moderations));
- moderationsItemView.setMenuType(SingleMenuItemView.Type.NEXT);
- moderationsItemView.setIcon(R.drawable.icon_moderations);
- moderationsItemView.setVisibility(View.GONE);
-
- participantsItemView.setName(context.getString(R.string.sb_text_header_participants));
- participantsItemView.setMenuType(SingleMenuItemView.Type.NEXT);
- participantsItemView.setIcon(R.drawable.icon_members);
- deleteItemView.setName(context.getString(R.string.sb_text_channel_settings_delete_channel));
- deleteItemView.setMenuType(SingleMenuItemView.Type.NONE);
- deleteItemView.setIcon(R.drawable.icon_delete);
- deleteItemView.setIconTint(AppCompatResources.getColorStateList(context, SendbirdUIKit.isDarkMode() ? R.color.error_200 : R.color.error_300));
-
- moderationsItemView.setOnClickListener(v -> onMenuClicked(v, Menu.MODERATIONS));
- participantsItemView.setOnClickListener(v -> onMenuClicked(v, Menu.PARTICIPANTS));
- deleteItemView.setOnClickListener(v -> onMenuClicked(v, Menu.DELETE_CHANNEL));
- this.menuView = view;
- return view;
+ final LinearLayout layout = new LinearLayout(menuThemeContext);
+ layout.setOrientation(LinearLayout.VERTICAL);
+ for (int i = 0; i < params.getMenuList().size(); i++) {
+ final Menu menu = params.getMenuList().get(i);
+ final View menuView = createMenuView(menuThemeContext, menu, i);
+ if (menu != Menu.CUSTOM) {
+ menuView.setOnClickListener(v -> onMenuClicked(v, menu));
+ defaultMenuViews.put(menu, (SingleMenuItemView) menuView);
+ }
+ layout.addView(menuView);
+ }
+ this.menuView = layout;
+ return layout;
+ }
+
+ /**
+ * Creates a custom menu view.
+ *
+ * @param context The {@code Context} this component is currently associated with
+ * @param title The title of the menu
+ * @param type The type of the menu
+ * @param iconResId The icon resource id of the menu
+ * @return The custom menu view
+ * since 3.16.0
+ */
+ @NonNull
+ public View createMenuView(
+ @NonNull Context context,
+ @NonNull String title,
+ @Nullable String description,
+ @NonNull SingleMenuType type,
+ @DrawableRes int iconResId,
+ @ColorRes int iconTintResId
+ ) {
+ return SingleMenuItemView.createMenuView(context, title, description, type, iconResId, iconTintResId);
}
/**
@@ -135,15 +177,21 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla
public void notifyChannelChanged(@NonNull OpenChannel channel) {
if (this.menuView == null) return;
- final SingleMenuItemView moderationsItemView = menuView.findViewById(R.id.moderations);
- moderationsItemView.setVisibility(channel.isOperator(SendbirdChat.getCurrentUser()) ? View.VISIBLE : View.GONE);
+ final SingleMenuItemView moderationsItemView = defaultMenuViews.get(Menu.MODERATIONS);
+ if (moderationsItemView != null) {
+ moderationsItemView.setVisibility(channel.isOperator(SendbirdChat.getCurrentUser()) ? View.VISIBLE : View.GONE);
+ }
- SingleMenuItemView participantsItemView = menuView.findViewById(R.id.participants);
- participantsItemView.setDescription(ChannelUtils.makeMemberCountText(channel.getParticipantCount()).toString());
+ final SingleMenuItemView participantsItemView = defaultMenuViews.get(Menu.PARTICIPANTS);
+ if (participantsItemView != null) {
+ participantsItemView.setDescription(ChannelUtils.makeMemberCountText(channel.getParticipantCount()).toString());
+ }
}
/**
* Register a callback to be invoked when the item of the menu is clicked.
+ * The click event about the {@link Menu#CUSTOM} menu won’t be called.
+ * If you want to handle the {@link Menu#CUSTOM} menu, you should handle it yourself after creating a custom menu view.
*
* @param menuClickListener The callback that will run
* since 3.0.0
@@ -163,6 +211,45 @@ protected void onMenuClicked(@NonNull View view, @NonNull Menu menu) {
if (this.menuClickListener != null) this.menuClickListener.onItemClick(view, 0, menu);
}
+ @NonNull
+ private View createMenuView(@NonNull Context context, @NonNull Menu menu, int position) {
+ if (menu == Menu.CUSTOM) {
+ final MenuViewProvider provider = params.getCustomMenuViewProvider();
+ if (provider != null) {
+ return provider.provideMenuView(context, position);
+ }
+ Logger.d("MenuViewProvider is not set. Creating a default View.");
+ return new View(context);
+ }
+
+ return createDefaultMenuView(context, menu);
+ }
+
+ @NonNull
+ private View createDefaultMenuView(@NonNull Context context, @NonNull Menu menu) {
+ final SingleMenuItemView menuView = new SingleMenuItemView(context);
+ switch (menu) {
+ case MODERATIONS:
+ menuView.setName(context.getString(R.string.sb_text_channel_settings_moderations));
+ menuView.setMenuType(SingleMenuType.NEXT);
+ menuView.setIcon(R.drawable.icon_moderations);
+ menuView.setVisibility(View.GONE);
+ break;
+ case PARTICIPANTS:
+ menuView.setName(context.getString(R.string.sb_text_header_participants));
+ menuView.setMenuType(SingleMenuType.NEXT);
+ menuView.setIcon(R.drawable.icon_members);
+ break;
+ case DELETE_CHANNEL:
+ menuView.setName(context.getString(R.string.sb_text_channel_settings_delete_channel));
+ menuView.setMenuType(SingleMenuType.NONE);
+ menuView.setIcon(R.drawable.icon_delete);
+ menuView.setIconTint(AppCompatResources.getColorStateList(context, SendbirdUIKit.isDarkMode() ? R.color.error_200 : R.color.error_300));
+ break;
+ }
+ return menuView;
+ }
+
/**
* A collection of parameters, which can be applied to a default View. The values of params are not dynamically applied at runtime.
* Params cannot be created directly, and it is automatically created together when components are created.
@@ -172,6 +259,12 @@ protected void onMenuClicked(@NonNull View view, @NonNull Menu menu) {
* since 3.0.0
*/
public static class Params {
+ @NonNull
+ private List menuList = defaultMenuSet;
+
+ @Nullable
+ private MenuViewProvider customMenuViewProvider;
+
/**
* Constructor
*
@@ -180,6 +273,41 @@ public static class Params {
protected Params() {
}
+ /**
+ * Sets the list of menus to be displayed in the channel settings menu.
+ * If the CUSTOM menu is included in the list, you must set the {@link MenuViewProvider} to create a custom menu view.
+ *
+ * @param menuList A list of settings menus in guaranteed with order.
+ * @param provider The provider to create custom menu view.
+ * since 3.16.0
+ */
+ public void setMenuList(@NonNull List menuList, @Nullable MenuViewProvider provider) {
+ this.menuList = menuList;
+ this.customMenuViewProvider = provider;
+ }
+
+ /**
+ * Returns the list of menus to be displayed in the channel settings menu.
+ *
+ * @return A list of settings menus in guaranteed with order.
+ * since 3.16.0
+ */
+ @NonNull
+ public List getMenuList() {
+ return menuList;
+ }
+
+ /**
+ * Returns the {@link MenuViewProvider} to create a custom menu view.
+ *
+ * @return The provider to create custom menu view.
+ * since 3.16.0
+ */
+ @Nullable
+ public MenuViewProvider getCustomMenuViewProvider() {
+ return customMenuViewProvider;
+ }
+
/**
* Apply data that matches keys mapped to Params' properties.
*
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 f2252e17..59321917 100644
--- a/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java
+++ b/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java
@@ -31,17 +31,19 @@
import com.sendbird.android.params.MessageListParams;
import com.sendbird.android.params.common.MessagePayloadFilter;
import com.sendbird.android.user.User;
+import com.sendbird.uikit.SendbirdUIKit;
import com.sendbird.uikit.consts.MessageLoadState;
import com.sendbird.uikit.consts.ReplyType;
import com.sendbird.uikit.consts.StringSet;
import com.sendbird.uikit.consts.TypingIndicatorType;
import com.sendbird.uikit.interfaces.OnCompleteHandler;
-import com.sendbird.uikit.internal.contracts.MessageCollectionImpl;
import com.sendbird.uikit.internal.contracts.MessageCollectionContract;
-import com.sendbird.uikit.internal.contracts.SendbirdChatImpl;
+import com.sendbird.uikit.internal.contracts.MessageCollectionImpl;
import com.sendbird.uikit.internal.contracts.SendbirdChatContract;
-import com.sendbird.uikit.internal.contracts.SendbirdUIKitImpl;
+import com.sendbird.uikit.internal.contracts.SendbirdChatImpl;
import com.sendbird.uikit.internal.contracts.SendbirdUIKitContract;
+import com.sendbird.uikit.internal.contracts.SendbirdUIKitImpl;
+import com.sendbird.uikit.internal.singleton.MessageTemplateMapper;
import com.sendbird.uikit.log.Logger;
import com.sendbird.uikit.model.SuggestedRepliesMessage;
import com.sendbird.uikit.model.TypingIndicatorMessage;
@@ -60,6 +62,8 @@
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
+import kotlin.Unit;
+
/**
* ViewModel preparing and managing data related with the list of messages in a channel
*
@@ -102,6 +106,9 @@ public class ChannelViewModel extends BaseMessageListViewModel {
@NonNull
private final ChannelConfig channelConfig;
+ @NonNull
+ private final MessageTemplateMapper messageTemplateMapper = new MessageTemplateMapper();
+
/**
* Class that holds message data in a channel.
*
@@ -540,7 +547,13 @@ synchronized void notifyDataSetChanged(@NonNull String traceName) {
Logger.d("-- ChannelViewModel::notifyDataSetChanged() event is ignored. traceName=%s", traceName);
return;
}
+
+ List messages = cachedMessages.toList();
+ // The reason why updates message template status here instead of buildMessageList(),
+ // it's difficult for customers to handle message template values by themselves when they override the `buildMessageList()` for their message list customization.
+ processMessageTemplate(messages, traceName);
final List finalMessageList = buildMessageList();
+
if (finalMessageList.size() == 0) {
statusFrame.setValue(StatusFrameView.Status.EMPTY);
} else {
@@ -550,6 +563,19 @@ synchronized void notifyDataSetChanged(@NonNull String traceName) {
messageList.setValue(new ChannelMessageData(traceName, finalMessageList));
}
+ private void processMessageTemplate(@NonNull List messages, @NonNull String traceName) {
+ Logger.d("[MessageTemplate] traceName: " + traceName);
+ final List updatedTemplateMessages = messageTemplateMapper.mapTemplate(messages, (updatedMessages) -> {
+ cachedMessages.updateAll(updatedMessages);
+ SendbirdUIKit.runOnUIThread(() -> notifyDataSetChanged(StringSet.EVENT_MESSAGE_TEMPLATE_UPDATED));
+ return Unit.INSTANCE;
+ });
+
+ if (!updatedTemplateMessages.isEmpty()) {
+ cachedMessages.updateAll(updatedTemplateMessages);
+ }
+ }
+
boolean shouldIgnoreEvent(@NonNull String traceName) {
if (collection == null) return true;
// even though a pending message is added, if the message is sent from the Thread page it shouldn't scroll to the first.
diff --git a/uikit/src/main/res/layout/sb_view_open_channel_settings_menu.xml b/uikit/src/main/res/layout/sb_view_open_channel_settings_menu.xml
deleted file mode 100644
index a171c9d6..00000000
--- a/uikit/src/main/res/layout/sb_view_open_channel_settings_menu.xml
+++ /dev/null
@@ -1,25 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/uikit/src/main/res/layout/sb_view_other_message_component.xml b/uikit/src/main/res/layout/sb_view_other_message_component.xml
index 15540d63..0440b62c 100644
--- a/uikit/src/main/res/layout/sb_view_other_message_component.xml
+++ b/uikit/src/main/res/layout/sb_view_other_message_component.xml
@@ -74,12 +74,12 @@
android:id="@+id/customContentPanel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ app:layout_constraintWidth_max="@dimen/sb_message_max_width"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="@+id/contentBarrier"
app:layout_constraintBottom_toTopOf="@id/rvEmojiReactionList" />
-
+
diff --git a/uikit/src/main/res/layout/sb_view_other_template_message_component.xml b/uikit/src/main/res/layout/sb_view_other_template_message_component.xml
new file mode 100644
index 00000000..191b6cef
--- /dev/null
+++ b/uikit/src/main/res/layout/sb_view_other_template_message_component.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/uikit/src/main/res/layout/sb_view_single_menu_item.xml b/uikit/src/main/res/layout/sb_view_single_menu_item.xml
index 1ec6d97f..7db7a470 100644
--- a/uikit/src/main/res/layout/sb_view_single_menu_item.xml
+++ b/uikit/src/main/res/layout/sb_view_single_menu_item.xml
@@ -53,6 +53,7 @@
android:layout_height="@dimen/sb_size_24"
android:scaleType="centerCrop"
android:importantForAccessibility="no"
+ android:layout_marginEnd="@dimen/sb_size_16"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/tvName"
app:layout_constraintStart_toStartOf="parent"
@@ -62,8 +63,7 @@
android:id="@+id/tvName"
android:layout_width="@dimen/sb_size_0"
android:layout_height="wrap_content"
- android:layout_marginLeft="@dimen/sb_size_16"
- android:layout_marginRight="@dimen/sb_size_16"
+ android:layout_marginEnd="@dimen/sb_size_16"
android:gravity="center_vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/vgAction"
diff --git a/uikit/src/main/res/values/strings.xml b/uikit/src/main/res/values/strings.xml
index 109322fd..7188e07e 100644
--- a/uikit/src/main/res/values/strings.xml
+++ b/uikit/src/main/res/values/strings.xml
@@ -230,4 +230,9 @@
Please check the value
Submit failed
(optional)
+
+
+ (Template error)
+ Can\'t read this message.
+ Error occurred while rendering template message. Reason: %s