From 588c725254a533c796cfc5d7d208bf8f37f1042c Mon Sep 17 00:00:00 2001 From: Doo Rim Date: Wed, 3 Aug 2022 11:14:34 +0900 Subject: [PATCH] Added 3.1.0 --- CHANGELOG.md | 14 + README.md | 2 +- gradle.properties | 2 +- uikit-custom-sample/build.gradle | 4 +- .../src/main/AndroidManifest.xml | 1 + .../uikit/customsample/HomeActivity.java | 47 ++ .../uikit/customsample/SettingsFragment.java | 195 +++--- .../CustomBannedUserListFragment.java | 2 +- .../community/CreateCommunityActivity.java | 175 +++--- .../layout/view_custom_moderation_list.xml | 2 +- .../src/main/res/values-ko-rKR/strings.xml | 5 +- .../src/main/res/values/strings.xml | 1 + uikit-sample/build.gradle | 4 +- uikit-sample/src/main/AndroidManifest.xml | 1 + .../uikit_messaging_android/HomeActivity.java | 47 ++ .../SettingsFragment.java | 179 +++--- .../openchannel/OpenChannelListFragment.java | 1 + .../community/CreateCommunityActivity.java | 171 +++-- uikit-sample/src/main/res/values/strings.xml | 1 + uikit/build.gradle | 6 +- uikit/src/main/AndroidManifest.xml | 29 +- .../OpenChannelBannedUserListActivity.java | 86 +++ .../OpenChannelModerationActivity.java | 86 +++ ...enChannelMutedParticipantListActivity.java | 86 +++ .../OpenChannelOperatorListActivity.java | 86 +++ .../OpenChannelRegisterOperatorActivity.java | 86 +++ .../adapter/BannedUserListAdapter.java | 36 ++ .../adapter/ChannelListAdapter.java | 2 - .../adapter/EmojiReactionListAdapter.java | 2 - .../adapter/EmojiReactionUserListAdapter.java | 2 - .../activities/adapter/MemberListAdapter.java | 37 ++ .../adapter/MessageSearchAdapter.java | 2 - .../adapter/MutedMemberListAdapter.java | 37 ++ .../OpenChannelBannedUserListAdapter.java | 51 ++ ...penChannelMutedParticipantListAdapter.java | 53 ++ .../OpenChannelOperatorListAdapter.java | 51 ++ ...penChannelRegisterOperatorListAdapter.java | 45 ++ .../adapter/OperatorListAdapter.java | 36 ++ .../adapter/ParticipantListAdapter.java | 42 ++ .../adapter/SelectUserListAdapter.java | 4 +- .../adapter/UserTypeDiffCallback.java | 106 ++++ .../adapter/UserTypeListAdapter.java | 114 +--- .../fragments/BannedUserListFragment.java | 15 +- .../uikit/fragments/ChannelFragment.java | 154 ++--- .../uikit/fragments/ChannelListFragment.java | 2 - .../fragments/ChannelSettingsFragment.java | 79 +-- .../fragments/CreateChannelFragment.java | 5 + .../uikit/fragments/InviteUserFragment.java | 6 +- .../uikit/fragments/MemberListFragment.java | 7 +- .../fragments/MessageSearchFragment.java | 6 + .../uikit/fragments/ModerationFragment.java | 14 +- .../fragments/MutedMemberListFragment.java | 16 +- .../OpenChannelBannedUserListFragment.java | 591 ++++++++++++++++++ .../uikit/fragments/OpenChannelFragment.java | 157 ++--- .../OpenChannelModerationFragment.java | 387 ++++++++++++ ...enChannelMutedParticipantListFragment.java | 591 ++++++++++++++++++ .../OpenChannelOperatorListFragment.java | 590 +++++++++++++++++ .../OpenChannelRegisterOperatorFragment.java | 474 ++++++++++++++ .../OpenChannelSettingsFragment.java | 111 ++-- .../uikit/fragments/OperatorListFragment.java | 6 +- .../fragments/ParticipantListFragment.java | 84 ++- .../uikit/fragments/PermissionFragment.java | 172 ++--- .../uikit/fragments/PhotoViewFragment.java | 12 +- .../fragments/RegisterOperatorFragment.java | 5 + .../uikit/fragments/UIKitFragmentFactory.java | 90 ++- .../sendbird/uikit/modules/BaseModule.java | 2 - .../uikit/modules/MessageSearchModule.java | 2 - .../OpenChannelBannedUserListModule.java | 261 ++++++++ .../modules/OpenChannelModerationModule.java | 232 +++++++ .../uikit/modules/OpenChannelModule.java | 2 - ...OpenChannelMutedParticipantListModule.java | 261 ++++++++ .../OpenChannelOperatorListModule.java | 260 ++++++++ .../OpenChannelRegisterOperatorModule.java | 207 ++++++ .../components/BannedUserListComponent.java | 14 + .../components/ChannelHeaderComponent.java | 3 - .../CreateChannelUserListComponent.java | 3 - .../modules/components/HeaderComponent.java | 5 - .../components/InviteUserListComponent.java | 3 - .../components/MemberListComponent.java | 17 +- .../components/MessageInputComponent.java | 2 - .../components/MessageListComponent.java | 2 - .../components/ModerationListComponent.java | 4 +- .../components/MutedMemberListComponent.java | 17 +- .../OpenChannelBannedUserListComponent.java | 63 ++ .../OpenChannelHeaderComponent.java | 2 - .../OpenChannelMessageInputComponent.java | 29 +- .../OpenChannelMessageListComponent.java | 2 - .../OpenChannelModerationListComponent.java | 211 +++++++ ...nChannelMutedParticipantListComponent.java | 54 ++ .../OpenChannelOperatorListComponent.java | 54 ++ ...nChannelRegisterOperatorListComponent.java | 40 ++ .../OpenChannelSettingsMenuComponent.java | 31 +- .../components/OperatorListComponent.java | 17 +- .../components/ParticipantListComponent.java | 17 +- .../components/StateHeaderComponent.java | 3 - .../components/UserTypeListComponent.java | 16 +- .../com/sendbird/uikit/utils/DialogUtils.java | 24 + .../com/sendbird/uikit/utils/IntentUtils.java | 22 +- .../sendbird/uikit/utils/PermissionUtils.java | 65 +- .../sendbird/uikit/vm/ChannelViewModel.java | 15 +- .../uikit/vm/MessageSearchViewModel.java | 5 +- .../uikit/vm/ModerationViewModel.java | 2 +- .../OpenChannelBannedUserListViewModel.java | 38 ++ .../vm/OpenChannelModerationViewModel.java | 168 +++++ ...nChannelMutedParticipantListViewModel.java | 40 ++ .../vm/OpenChannelOperatorListViewModel.java | 40 ++ .../OpenChannelRegisterOperatorViewModel.java | 40 ++ .../uikit/vm/OpenChannelUserViewModel.java | 585 +++++++++++++++++ .../uikit/vm/OpenChannelViewModel.java | 31 +- .../uikit/vm/ParticipantViewModel.java | 323 +--------- .../sendbird/uikit/vm/ViewModelFactory.java | 10 + .../uikit/vm/queries/MutedUserListQuery.java | 54 ++ .../sendbird/uikit/widgets/DialogView.java | 6 +- .../sendbird/uikit/widgets/UserPreview.java | 39 +- uikit/src/main/res/layout/sb_view_dialog.xml | 10 +- .../sb_view_open_channel_settings_menu.xml | 6 + uikit/src/main/res/values/attrs.xml | 5 + uikit/src/main/res/values/strings.xml | 17 +- uikit/src/main/res/values/styles.xml | 99 ++- uikit/src/main/res/values/styles_dark.xml | 98 ++- uikit/src/main/res/values/styles_overlay.xml | 2 +- 121 files changed, 7725 insertions(+), 1368 deletions(-) create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelBannedUserListActivity.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelModerationActivity.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelMutedParticipantListActivity.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelOperatorListActivity.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelRegisterOperatorActivity.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelBannedUserListAdapter.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelMutedParticipantListAdapter.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelOperatorListAdapter.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelRegisterOperatorListAdapter.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/activities/adapter/UserTypeDiffCallback.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelBannedUserListFragment.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelModerationFragment.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelMutedParticipantListFragment.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelOperatorListFragment.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelRegisterOperatorFragment.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelBannedUserListModule.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelModerationModule.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelMutedParticipantListModule.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelOperatorListModule.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelRegisterOperatorModule.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelBannedUserListComponent.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelModerationListComponent.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMutedParticipantListComponent.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelOperatorListComponent.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelRegisterOperatorListComponent.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelBannedUserListViewModel.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelModerationViewModel.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelMutedParticipantListViewModel.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelOperatorListViewModel.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelRegisterOperatorViewModel.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelUserViewModel.java create mode 100644 uikit/src/main/java/com/sendbird/uikit/vm/queries/MutedUserListQuery.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 767a41b4..844deebc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # Changelog +### v3.1.0 (Aug 3, 2022) with Core SDK `v4.0.5` +* Support Android 13 + * Set the `maxSdkVersion` of `android.permission.READ_EXTERNAL_STORAGE` to `32` +* Removed `android.permission.REQUEST_INSTALL_PACKAGES` permission +* Support moderation in OpenChannel + * Added `MODERATIONS` in `OpenChannelSettingsMenuComponent.Menu` + * Added `OpenChannelModerationActivity`, `OpenChannelModerationFragment`, `OpenChannelModerationModule`, `OpenChannelModerationViewModel` + * Added `OpenChannelOperatorListActivity`, `OpenChannelOperatorListFragment`, `OpenChannelOperatorListModule`, `OpenChannelOperatorListViewModel`, `OpenChannelOperatorListAdapter` + * Added `OpenChannelRegisterOperatorActivity`, `OpenChannelRegisterOperatorFragment`, `OpenChannelRegisterOperatorModule`, `OpenChannelRegisterOperatorViewModel`, `OpenChannelRegisterOperatorAdapter` + * Added `OpenChannelBannedUserListActivity`, `OpenChannelBannedUserListFragment`, `OpenChannelBannedUserListModule`, `OpenChannelBannedUserListViewModel`, `OpenChannelBannedUserListAdapter` + * Added `OpenChannelMutedParticipantListActivity`, `OpenChannelMutedParticipantListFragment`, `OpenChannelMutedParticipantListModule`, `OpenChannelMutedParticipantListViewModel`, `OpenChannelMutedParticipantListAdapter` + * Added `newOpenChannelModerationFragment()`, `newOpenChannelOperatorListFragment()`, `newOpenChannelRegisterOperatorFragment()`, `newOpenChannelMutedParticipantListFragment()`, `newOpenChannelBannedUserListFragment()` in `UIKitFragmentFactory` +* Improved stability + ### v3.0.0 (Jul 12, 2022) with Core SDK `v4.0.4` * Support `modules` and `components` in the UIKit * Added `setEditedTextMarkUIConfig(TextUIConfig, TextUIConfig)` in `OpenChannelFragment.Builder` diff --git a/README.md b/README.md index 324cabbd..e5186f72 100644 --- a/README.md +++ b/README.md @@ -112,4 +112,4 @@ dependencies { After saving your `build.gradle` file, click the **Sync** button to apply all the changes. -
\ No newline at end of file +
diff --git a/gradle.properties b/gradle.properties index a02b9e9c..c6b95cb9 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.0.0 +UIKIT_VERSION = 3.1.0 UIKIT_VERSION_CODE = 1 \ No newline at end of file diff --git a/uikit-custom-sample/build.gradle b/uikit-custom-sample/build.gradle index 22ee277a..adae15b2 100644 --- a/uikit-custom-sample/build.gradle +++ b/uikit-custom-sample/build.gradle @@ -2,12 +2,12 @@ apply plugin: 'com.android.application' apply plugin: 'com.google.gms.google-services' android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { applicationId "com.sendbird.uikit.customsample" minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 versionCode 1 versionName "1.0" multiDexEnabled true diff --git a/uikit-custom-sample/src/main/AndroidManifest.xml b/uikit-custom-sample/src/main/AndroidManifest.xml index c779eaad..14493ece 100644 --- a/uikit-custom-sample/src/main/AndroidManifest.xml +++ b/uikit-custom-sample/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" package="com.sendbird.uikit.customsample"> + requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}); + @NonNull + private final ActivityResultLauncher appSettingLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), intent -> {}); + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -50,6 +67,18 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { binding.tvUnreadCount.setTextAppearance(this, R.style.SendbirdCaption3OnDark01); binding.tvUnreadCount.setBackgroundResource(R.drawable.shape_badge_background); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + final String permission = Manifest.permission.POST_NOTIFICATIONS; + if (ContextCompat.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED) { + return; + } + if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { + showPermissionRationalePopup(permission); + return; + } + requestPermissionLauncher.launch(permission); + } } @Override @@ -127,4 +156,22 @@ public void onError(@NonNull SendbirdException e) { } }); } + + private void showPermissionRationalePopup(@NonNull String permission) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); + builder.setMessage(String.format(Locale.US, getString(R.string.sb_text_need_to_allow_permission_notification), ContextUtils.getApplicationName(this))); + builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + appSettingLauncher.launch(intent); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(this, com.sendbird.uikit.R.color.secondary_300)); + } } \ No newline at end of file diff --git a/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/SettingsFragment.java b/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/SettingsFragment.java index 2188efa6..5384c545 100644 --- a/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/SettingsFragment.java +++ b/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/SettingsFragment.java @@ -5,9 +5,7 @@ import android.Manifest; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.LayoutInflater; @@ -15,6 +13,8 @@ import android.view.ViewGroup; import android.widget.TextView; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -54,18 +54,56 @@ * Displays a settings screen. */ public class SettingsFragment extends Fragment { - private String[] REQUIRED_PERMISSIONS; - - private static final int STORAGE_PERMISSIONS_REQUEST_CODE = 1001; - private static final int PERMISSION_SETTINGS_REQUEST_ID = 2000; - private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 2001; - private static final int PICK_IMAGE_ACTIVITY_REQUEST_CODE = 2002; + private final String[] REQUIRED_PERMISSIONS = PermissionUtils.CAMERA_PERMISSION; private AppCompatImageView profileImageView; private TextView nicknameTextView; private SwitchCompat disturbSwitch; private Uri mediaUri; + private final ActivityResultLauncher appSettingLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), intent -> { + if (getContext() == null) return; + final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), REQUIRED_PERMISSIONS); + if (hasPermission) { + showMediaSelectDialog(); + } + }); + private final ActivityResultLauncher permissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionResults -> { + if (PermissionUtils.getNotGrantedPermissions(permissionResults).isEmpty()) { + showMediaSelectDialog(); + } + }); + + private final ActivityResultLauncher takeCameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || getContext() == null) return; + final Uri mediaUri = this.mediaUri; + if (mediaUri != null) { + final File file = FileUtils.uriToFile(getContext().getApplicationContext(), mediaUri); + updateUserProfileImage(file); + } + }); + private final ActivityResultLauncher getContentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null || getContext() == null) return; + final Uri mediaUri = intent.getData(); + if (mediaUri != null) { + final File file = FileUtils.uriToFile(getContext().getApplicationContext(), mediaUri); + updateUserProfileImage(file); + } + }); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + SendbirdUIKit.connect(null); + } + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -84,77 +122,6 @@ public void onDestroy() { SendbirdChat.setAutoBackgroundDetection(true); } - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - SendbirdChat.setAutoBackgroundDetection(true); - if (resultCode != RESULT_OK) return; - - if (requestCode == PERMISSION_SETTINGS_REQUEST_ID) { - if (getContext() == null) return; - final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), REQUIRED_PERMISSIONS); - if (hasPermission) { - showMediaSelectDialog(); - } - return; - } - - switch (requestCode) { - case CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE: - break; - case PICK_IMAGE_ACTIVITY_REQUEST_CODE: - if (data != null) { - this.mediaUri = data.getData(); - } - break; - } - - if (this.mediaUri != null && getContext() != null) { - final File file = FileUtils.uriToFile(getContext().getApplicationContext(), mediaUri); - updateUserProfileImage(file); - } - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == STORAGE_PERMISSIONS_REQUEST_CODE && grantResults.length == REQUIRED_PERMISSIONS.length) { - boolean isAllGranted = true; - for (int result : grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - isAllGranted = false; - break; - } - } - - if (isAllGranted) { - showMediaSelectDialog(); - } else { - if (getActivity() == null) return; - String[] notGranted = PermissionUtils.getNotGrantedPermissions(getActivity(), permissions); - List deniedList = PermissionUtils.getShowRequestPermissionRationale(getActivity(), permissions); - if (deniedList.size() == 0 && getActivity() != null) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getActivity().getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); - builder.setMessage(getPermissionGuideMessage(getActivity(), notGranted[0])); - builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Uri.parse("package:" + getActivity().getPackageName())); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivityForResult(intent, PERMISSION_SETTINGS_REQUEST_ID); - }); - AlertDialog dialog = builder.create(); - dialog.show(); - if (getActivity() != null) { - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(getActivity(), com.sendbird.uikit.R.color.secondary_300)); - } - } - } - } - } - private void initPage(@NonNull View view) { if (getContext() == null) { return; @@ -168,15 +135,6 @@ private void initPage(@NonNull View view) { showEditProfileDialog(); }); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } else { - REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - boolean useDoNotDisturb = true; if (getArguments() != null) { useDoNotDisturb = getArguments().getBoolean(StringSet.SETTINGS_USE_DO_NOT_DISTURB, true); @@ -254,11 +212,48 @@ private void showEditProfileDialog() { return; } - requestPermissions(REQUIRED_PERMISSIONS, STORAGE_PERMISSIONS_REQUEST_CODE); + requestPermission(REQUIRED_PERMISSIONS); } }); } + private void requestPermission(@NonNull String[] permissions) { + if (getContext() == null || getActivity() == null) return; + // 1. check permission + final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), permissions); + if (hasPermission) { + showMediaSelectDialog(); + return; + } + + // 2. determine whether rationale popup should show + final List deniedList = PermissionUtils.getExplicitDeniedPermissionList(getActivity(), permissions); + if (!deniedList.isEmpty()) { + showPermissionRationalePopup(deniedList.get(0)); + return; + } + // 3. request permission + this.permissionLauncher.launch(permissions); + } + + private void showPermissionRationalePopup(@NonNull String permission) { + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); + builder.setTitle(getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); + builder.setMessage(getPermissionGuideMessage(requireContext(), permission)); + builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.parse("package:" + requireContext().getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + appSettingLauncher.launch(intent); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(requireContext(), com.sendbird.uikit.R.color.secondary_300)); + } + private void updateUserNickname(@NonNull String nickname) { if (getContext() == null) return; WaitingDialog.show(getContext()); @@ -321,11 +316,10 @@ private void showMediaSelectDialog() { items, (v, p, item) -> { try { final int key = item.getKey(); - SendbirdChat.setAutoBackgroundDetection(false); if (key == com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_camera) { takeCamera(); } else if (key == com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_gallery) { - pickImage(); + takePhoto(); } } catch (Exception e) { Logger.e(e); @@ -334,20 +328,19 @@ private void showMediaSelectDialog() { } private void takeCamera() { - if (getContext() == null) { - return; - } - - this.mediaUri = FileUtils.createPictureImageUri(getContext()); - Intent intent = IntentUtils.getCameraIntent(getContext(), mediaUri); - if (IntentUtils.hasIntent(getContext(), intent)) { - startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE); + SendbirdChat.setAutoBackgroundDetection(false); + this.mediaUri = FileUtils.createPictureImageUri(requireContext()); + if (mediaUri == null) return; + Intent intent = IntentUtils.getCameraIntent(requireContext(), mediaUri); + if (IntentUtils.hasIntent(requireContext(), intent)) { + takeCameraLauncher.launch(intent); } } - private void pickImage() { + private void takePhoto() { + SendbirdChat.setAutoBackgroundDetection(false); Intent intent = IntentUtils.getImageGalleryIntent(); - startActivityForResult(intent, PICK_IMAGE_ACTIVITY_REQUEST_CODE); + getContentLauncher.launch(intent); } private static String getPermissionGuideMessage(@NonNull Context context, @NonNull String permission) { diff --git a/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/groupchannel/fragments/CustomBannedUserListFragment.java b/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/groupchannel/fragments/CustomBannedUserListFragment.java index 4f055b39..e817179e 100644 --- a/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/groupchannel/fragments/CustomBannedUserListFragment.java +++ b/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/groupchannel/fragments/CustomBannedUserListFragment.java @@ -29,7 +29,7 @@ protected void onConfigureParams(@NonNull BannedUserListModule module, @NonNull HeaderComponent.Params headerParams = module.getHeaderComponent().getParams(); if (isFragmentAlive()) { - headerParams.setTitle(requireContext().getString(R.string.sb_text_menu_banned_members)); + headerParams.setTitle(requireContext().getString(R.string.sb_text_menu_banned_users)); headerParams.setLeftButtonIcon(ResourcesCompat.getDrawable(getResources(), R.drawable.icon_arrow_left, null)); headerParams.setUseRightButton(false); } diff --git a/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/openchannel/community/CreateCommunityActivity.java b/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/openchannel/community/CreateCommunityActivity.java index 5cb68219..606fcfcd 100644 --- a/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/openchannel/community/CreateCommunityActivity.java +++ b/uikit-custom-sample/src/main/java/com/sendbird/uikit/customsample/openchannel/community/CreateCommunityActivity.java @@ -3,22 +3,21 @@ import android.Manifest; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.text.Editable; import android.text.TextWatcher; import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.view.ContextThemeWrapper; -import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.bumptech.glide.Glide; @@ -49,12 +48,7 @@ * Displays a create open channel screen used for community. */ public class CreateCommunityActivity extends AppCompatActivity { - private String[] REQUIRED_PERMISSIONS; - - private static final int STORAGE_PERMISSIONS_REQUEST_CODE = 1001; - private static final int PERMISSION_SETTINGS_REQUEST_ID = 2000; - private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 2001; - private static final int PICK_IMAGE_ACTIVITY_REQUEST_CODE = 2002; + private final String[] REQUIRED_PERMISSIONS = PermissionUtils.CAMERA_PERMISSION; private ActivityCreateCommunityBinding binding; @NonNull @@ -62,12 +56,47 @@ public class CreateCommunityActivity extends AppCompatActivity { private Uri mediaUri; private File mediaFile; + private final ActivityResultLauncher appSettingLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), intent -> { + final boolean hasPermission = PermissionUtils.hasPermissions(this, REQUIRED_PERMISSIONS); + if (hasPermission) { + showMediaSelectDialog(); + } + }); + private final ActivityResultLauncher permissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionResults -> { + if (PermissionUtils.getNotGrantedPermissions(permissionResults).isEmpty()) { + showMediaSelectDialog(); + } + }); + + private final ActivityResultLauncher takeCameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK) return; + if (mediaUri != null) { + mediaFile = FileUtils.uriToFile(getApplicationContext(), mediaUri); + updateChannelCover(); + } + }); + private final ActivityResultLauncher getContentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null) return; + this.mediaUri = intent.getData(); + if (mediaUri != null) { + this.mediaFile = FileUtils.uriToFile(getApplicationContext(), mediaUri); + updateChannelCover(); + } + }); + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); int themeResId = SendbirdUIKit.getDefaultThemeMode().getResId(); setTheme(themeResId); - ActivityCreateCommunityBinding binding = ActivityCreateCommunityBinding.inflate(getLayoutInflater()); + this.binding = ActivityCreateCommunityBinding.inflate(getLayoutInflater()); View view = binding.getRoot(); setContentView(view); @@ -90,7 +119,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { return; } - ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, STORAGE_PERMISSIONS_REQUEST_CODE); + requestPermission(REQUIRED_PERMISSIONS); }); binding.etTitle.addTextChangedListener(new TextWatcher() { @Override @@ -105,15 +134,49 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { public void afterTextChanged(Editable s) {} }); binding.clearButton.setOnClickListener(v -> binding.etTitle.setText("")); + SendbirdUIKit.connect(null); + } - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } else { - REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; + @Override + protected void onDestroy() { + super.onDestroy(); + SendbirdChat.setAutoBackgroundDetection(true); + } + + private void requestPermission(@NonNull String[] permissions) { + // 1. check permission + final boolean hasPermission = PermissionUtils.hasPermissions(this, permissions); + if (hasPermission) { + showMediaSelectDialog(); + return; } + + // 2. determine whether rationale popup should show + final List deniedList = PermissionUtils.getExplicitDeniedPermissionList(this, permissions); + if (!deniedList.isEmpty()) { + showPermissionRationalePopup(deniedList.get(0)); + return; + } + // 3. request permission + this.permissionLauncher.launch(permissions); + } + + private void showPermissionRationalePopup(@NonNull String permission) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); + builder.setMessage(getPermissionGuideMessage(this, permission)); + builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + appSettingLauncher.launch(intent); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(this, com.sendbird.uikit.R.color.secondary_300)); } private void createCommunityChannel() { @@ -142,37 +205,6 @@ private void createCommunityChannel() { }); } - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - SendbirdChat.setAutoBackgroundDetection(true); - - if (resultCode != RESULT_OK) return; - - if (requestCode == PERMISSION_SETTINGS_REQUEST_ID) { - final boolean hasPermission = PermissionUtils.hasPermissions(this, REQUIRED_PERMISSIONS); - if (hasPermission) { - showMediaSelectDialog(); - } - return; - } - - switch (requestCode) { - case CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE: - break; - case PICK_IMAGE_ACTIVITY_REQUEST_CODE: - if (data != null) { - this.mediaUri = data.getData(); - } - break; - } - - if (this.mediaUri != null) { - mediaFile = FileUtils.uriToFile(getApplicationContext(), mediaUri); - updateChannelCover(); - } - } - private void updateChannelCover() { Glide.with(binding.getRoot().getContext()) .load(mediaUri) @@ -183,43 +215,6 @@ private void updateChannelCover() { .into(binding.ivChannelCover); } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == STORAGE_PERMISSIONS_REQUEST_CODE && grantResults.length == REQUIRED_PERMISSIONS.length) { - boolean isAllGranted = true; - for (int result : grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - isAllGranted = false; - break; - } - } - - if (isAllGranted) { - showMediaSelectDialog(); - } else { - String[] notGranted = PermissionUtils.getNotGrantedPermissions(this, permissions); - List deniedList = PermissionUtils.getShowRequestPermissionRationale(this, permissions); - if (deniedList.size() == 0) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); - builder.setMessage(getPermissionGuideMessage(this, notGranted[0])); - builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Uri.parse("package:" + getPackageName())); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivityForResult(intent, PERMISSION_SETTINGS_REQUEST_ID); - }); - AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(this, com.sendbird.uikit.R.color.secondary_300)); - } - } - } - } - private void showMediaSelectDialog() { DialogListItem delete = new DialogListItem(R.string.text_remove_photo, 0, true); DialogListItem camera = new DialogListItem(com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_camera); @@ -238,7 +233,7 @@ private void showMediaSelectDialog() { if (key == com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_camera) { takeCamera(); } else if (key == com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_gallery) { - pickImage(); + takePhoto(); } else { removeFile(); } @@ -253,13 +248,13 @@ private void takeCamera() { if (mediaUri == null) return; Intent intent = IntentUtils.getCameraIntent(this, mediaUri); if (IntentUtils.hasIntent(this, intent)) { - startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE); + takeCameraLauncher.launch(intent); } } - private void pickImage() { + private void takePhoto() { Intent intent = IntentUtils.getImageGalleryIntent(); - startActivityForResult(intent, PICK_IMAGE_ACTIVITY_REQUEST_CODE); + getContentLauncher.launch(intent); } private void removeFile() { diff --git a/uikit-custom-sample/src/main/res/layout/view_custom_moderation_list.xml b/uikit-custom-sample/src/main/res/layout/view_custom_moderation_list.xml index c3122a7b..4f03c99c 100644 --- a/uikit-custom-sample/src/main/res/layout/view_custom_moderation_list.xml +++ b/uikit-custom-sample/src/main/res/layout/view_custom_moderation_list.xml @@ -47,7 +47,7 @@ android:layout_width="match_parent" android:layout_height="@dimen/sb_size_56" android:gravity="center_vertical" - android:text="@string/sb_text_menu_banned_members" + android:text="@string/sb_text_menu_banned_users" android:textAppearance="@style/SendbirdBody1OnLight01" android:paddingLeft="@dimen/sb_size_16" android:paddingRight="@dimen/sb_size_16"/> diff --git a/uikit-custom-sample/src/main/res/values-ko-rKR/strings.xml b/uikit-custom-sample/src/main/res/values-ko-rKR/strings.xml index f34fa32d..9d49bfea 100644 --- a/uikit-custom-sample/src/main/res/values-ko-rKR/strings.xml +++ b/uikit-custom-sample/src/main/res/values-ko-rKR/strings.xml @@ -124,11 +124,11 @@ 관리자 관리자 음소거된 멤버 - 접근 금지된 멤버 + 접근 금지된 유저 채널 일시 정지하기 관리자가 없습니다. 음소거된 멤버가 없습니다. - 접근 금지된 멤버가 없습니다. + 접근 금지된 유저가 없습니다. 선택 추가 @@ -186,4 +186,5 @@ Open channel 1 on 1, Group chat with members Live streams, Open community chat + %s\n푸시 알림을 받기위해 알림설정이 필요합니다. 권한을 받기위해 설정으로 이동하세요. \ No newline at end of file diff --git a/uikit-custom-sample/src/main/res/values/strings.xml b/uikit-custom-sample/src/main/res/values/strings.xml index 098f128f..400fb3b5 100644 --- a/uikit-custom-sample/src/main/res/values/strings.xml +++ b/uikit-custom-sample/src/main/res/values/strings.xml @@ -39,4 +39,5 @@ Leave this channel? Okay %d seleted + %s\nneeds permission to receive push notification. Go to Settings to allow access \ No newline at end of file diff --git a/uikit-sample/build.gradle b/uikit-sample/build.gradle index f4c6e357..01b2056d 100644 --- a/uikit-sample/build.gradle +++ b/uikit-sample/build.gradle @@ -3,12 +3,12 @@ apply plugin: 'com.google.gms.google-services' version = UIKIT_VERSION android { - compileSdkVersion 32 + compileSdkVersion 33 defaultConfig { applicationId "com.sendbird.uikit.sample" minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 versionCode 1 versionName UIKIT_VERSION multiDexEnabled true diff --git a/uikit-sample/src/main/AndroidManifest.xml b/uikit-sample/src/main/AndroidManifest.xml index a8316e88..da4c7bc8 100644 --- a/uikit-sample/src/main/AndroidManifest.xml +++ b/uikit-sample/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {}); + @NonNull + private final ActivityResultLauncher appSettingLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), intent -> {}); + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -50,6 +67,18 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { binding.tvUnreadCount.setTextAppearance(this, R.style.SendbirdCaption3OnDark01); binding.tvUnreadCount.setBackgroundResource(R.drawable.shape_badge_background); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + final String permission = Manifest.permission.POST_NOTIFICATIONS; + if (ContextCompat.checkSelfPermission(this, permission) == PermissionChecker.PERMISSION_GRANTED) { + return; + } + if (ActivityCompat.shouldShowRequestPermissionRationale(this, permission)) { + showPermissionRationalePopup(permission); + return; + } + requestPermissionLauncher.launch(permission); + } } @Override @@ -126,4 +155,22 @@ public void onError(SendbirdException e) { } }); } + + private void showPermissionRationalePopup(@NonNull String permission) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); + builder.setMessage(String.format(Locale.US, getString(R.string.sb_text_need_to_allow_permission_notification), ContextUtils.getApplicationName(this))); + builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + appSettingLauncher.launch(intent); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(this, com.sendbird.uikit.R.color.secondary_300)); + } } \ No newline at end of file diff --git a/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/SettingsFragment.java b/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/SettingsFragment.java index a71bfd49..5a4fff69 100644 --- a/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/SettingsFragment.java +++ b/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/SettingsFragment.java @@ -5,9 +5,7 @@ import android.Manifest; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.view.LayoutInflater; @@ -17,6 +15,8 @@ import android.view.View; import android.view.ViewGroup; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -57,17 +57,55 @@ * Displays a settings screen. */ public class SettingsFragment extends Fragment { - private String[] REQUIRED_PERMISSIONS; - - private static final int STORAGE_PERMISSIONS_REQUEST_CODE = 1001; - private static final int PERMISSION_SETTINGS_REQUEST_ID = 2000; - private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 2001; - private static final int PICK_IMAGE_ACTIVITY_REQUEST_CODE = 2002; + private final String[] REQUIRED_PERMISSIONS = PermissionUtils.CAMERA_PERMISSION; private FragmentSettingsBinding binding; private final StateHeaderComponent headerComponent = new StateHeaderComponent(); private Uri mediaUri; + private final ActivityResultLauncher appSettingLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), intent -> { + if (getContext() == null) return; + final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), REQUIRED_PERMISSIONS); + if (hasPermission) { + showMediaSelectDialog(); + } + }); + private final ActivityResultLauncher permissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionResults -> { + if (PermissionUtils.getNotGrantedPermissions(permissionResults).isEmpty()) { + showMediaSelectDialog(); + } + }); + + private final ActivityResultLauncher takeCameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || getContext() == null) return; + final Uri mediaUri = this.mediaUri; + if (mediaUri != null) { + final File file = FileUtils.uriToFile(getContext().getApplicationContext(), mediaUri); + updateUserProfileImage(file); + } + }); + private final ActivityResultLauncher getContentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null || getContext() == null) return; + final Uri mediaUri = intent.getData(); + if (mediaUri != null) { + final File file = FileUtils.uriToFile(getContext().getApplicationContext(), mediaUri); + updateUserProfileImage(file); + } + }); + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + SendbirdUIKit.connect(null); + } + @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -127,75 +165,41 @@ public void onDestroy() { SendbirdChat.setAutoBackgroundDetection(true); } - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - SendbirdChat.setAutoBackgroundDetection(true); - - if (resultCode != RESULT_OK) return; - - if (requestCode == PERMISSION_SETTINGS_REQUEST_ID) { - if (getContext() == null) return; - final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), REQUIRED_PERMISSIONS); - if (hasPermission) { - showMediaSelectDialog(); - } + private void requestPermission(@NonNull String[] permissions) { + if (getContext() == null || getActivity() == null) return; + // 1. check permission + final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), permissions); + if (hasPermission) { + showMediaSelectDialog(); return; } - switch (requestCode) { - case CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE: - break; - case PICK_IMAGE_ACTIVITY_REQUEST_CODE: - if (data != null) { - this.mediaUri = data.getData(); - } - break; - } - - if (this.mediaUri != null && getContext() != null) { - final File file = FileUtils.uriToFile(getContext().getApplicationContext(), mediaUri); - updateUserProfileImage(file); + // 2. determine whether rationale popup should show + final List deniedList = PermissionUtils.getExplicitDeniedPermissionList(getActivity(), permissions); + if (!deniedList.isEmpty()) { + showPermissionRationalePopup(deniedList.get(0)); + return; } + // 3. request permission + this.permissionLauncher.launch(permissions); } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (requestCode == STORAGE_PERMISSIONS_REQUEST_CODE && grantResults.length == REQUIRED_PERMISSIONS.length) { - boolean isAllGranted = true; - for (int result : grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - isAllGranted = false; - break; - } - } - - if (isAllGranted) { - showMediaSelectDialog(); - } else { - if (getContext() == null || getActivity() == null) return; - String[] notGranted = PermissionUtils.getNotGrantedPermissions(getContext(), permissions); - List deniedList = PermissionUtils.getShowRequestPermissionRationale(getActivity(), permissions); - if (deniedList.size() == 0 && getActivity() != null) { - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); - builder.setTitle(getActivity().getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); - builder.setMessage(getPermissionGuideMessage(getActivity(), notGranted[0])); - builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Uri.parse("package:" + getActivity().getPackageName())); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivityForResult(intent, PERMISSION_SETTINGS_REQUEST_ID); - }); - AlertDialog dialog = builder.create(); - dialog.show(); - if (getContext() == null) return; - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(getContext(), com.sendbird.uikit.R.color.secondary_300)); - } - } - } + private void showPermissionRationalePopup(@NonNull String permission) { + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); + builder.setTitle(getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); + builder.setMessage(getPermissionGuideMessage(requireContext(), permission)); + builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.parse("package:" + requireContext().getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + appSettingLauncher.launch(intent); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(requireContext(), com.sendbird.uikit.R.color.secondary_300)); } private void initPage() { @@ -220,15 +224,6 @@ private void initPage() { binding.tvHomeName.setTextColor(getResources().getColor(isDark ? R.color.ondark_01 : R.color.onlight_01)); binding.signOutDivider.setBackgroundResource(isDark ? R.drawable.sb_line_divider_dark : R.drawable.sb_line_divider_light); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } else { - REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - boolean useHeader = true; boolean useDoNotDisturb = true; if (getArguments() != null) { @@ -350,7 +345,7 @@ private void showEditProfileDialog() { return; } - requestPermissions(REQUIRED_PERMISSIONS, STORAGE_PERMISSIONS_REQUEST_CODE); + requestPermission(REQUIRED_PERMISSIONS); } }); } @@ -435,11 +430,10 @@ private void showMediaSelectDialog() { items, (v, p, item) -> { try { final int key = item.getKey(); - SendbirdChat.setAutoBackgroundDetection(false); if (key == com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_camera) { takeCamera(); } else if (key == com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_gallery) { - pickImage(); + takePhoto(); } } catch (Exception e) { Logger.e(e); @@ -448,20 +442,19 @@ private void showMediaSelectDialog() { } private void takeCamera() { - if (getContext() == null) { - return; - } - - this.mediaUri = FileUtils.createPictureImageUri(getContext()); - Intent intent = IntentUtils.getCameraIntent(getContext(), mediaUri); - if (IntentUtils.hasIntent(getContext(), intent)) { - startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE); + SendbirdChat.setAutoBackgroundDetection(false); + this.mediaUri = FileUtils.createPictureImageUri(requireContext()); + if (mediaUri == null) return; + Intent intent = IntentUtils.getCameraIntent(requireContext(), mediaUri); + if (IntentUtils.hasIntent(requireContext(), intent)) { + takeCameraLauncher.launch(intent); } } - private void pickImage() { + private void takePhoto() { + SendbirdChat.setAutoBackgroundDetection(false); Intent intent = IntentUtils.getImageGalleryIntent(); - startActivityForResult(intent, PICK_IMAGE_ACTIVITY_REQUEST_CODE); + getContentLauncher.launch(intent); } private static String getPermissionGuideMessage(@NonNull Context context, @NonNull String permission) { diff --git a/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/openchannel/OpenChannelListFragment.java b/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/openchannel/OpenChannelListFragment.java index d609b1cd..cd11817c 100644 --- a/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/openchannel/OpenChannelListFragment.java +++ b/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/openchannel/OpenChannelListFragment.java @@ -105,6 +105,7 @@ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); SendbirdChat.addConnectionHandler(CONNECTION_HANDLER_ID, this); + SendbirdUIKit.connect(null); } @Override diff --git a/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/openchannel/community/CreateCommunityActivity.java b/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/openchannel/community/CreateCommunityActivity.java index c031b76e..006be5b2 100644 --- a/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/openchannel/community/CreateCommunityActivity.java +++ b/uikit-sample/src/main/java/com/sendbird/uikit_messaging_android/openchannel/community/CreateCommunityActivity.java @@ -3,22 +3,21 @@ import android.Manifest; import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.provider.Settings; import android.text.Editable; import android.text.TextWatcher; import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.view.ContextThemeWrapper; -import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import com.bumptech.glide.Glide; @@ -51,12 +50,7 @@ * Displays a create open channel screen used for community. */ public class CreateCommunityActivity extends AppCompatActivity { - private String[] REQUIRED_PERMISSIONS; - - private static final int STORAGE_PERMISSIONS_REQUEST_CODE = 1001; - private static final int PERMISSION_SETTINGS_REQUEST_ID = 2000; - private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 2001; - private static final int PICK_IMAGE_ACTIVITY_REQUEST_CODE = 2002; + private final String[] REQUIRED_PERMISSIONS = PermissionUtils.CAMERA_PERMISSION; private ActivityCreateCommunityBinding binding; @NonNull @@ -64,6 +58,41 @@ public class CreateCommunityActivity extends AppCompatActivity { private Uri mediaUri; private File mediaFile; + private final ActivityResultLauncher appSettingLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), intent -> { + final boolean hasPermission = PermissionUtils.hasPermissions(this, REQUIRED_PERMISSIONS); + if (hasPermission) { + showMediaSelectDialog(); + } + }); + private final ActivityResultLauncher permissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionResults -> { + if (PermissionUtils.getNotGrantedPermissions(permissionResults).isEmpty()) { + showMediaSelectDialog(); + } + }); + + private final ActivityResultLauncher takeCameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK) return; + if (this.mediaUri != null) { + mediaFile = FileUtils.uriToFile(getApplicationContext(), mediaUri); + updateChannelCover(); + } + }); + private final ActivityResultLauncher getContentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null) return; + this.mediaUri = intent.getData(); + if (this.mediaUri != null) { + this.mediaFile = FileUtils.uriToFile(getApplicationContext(), mediaUri); + updateChannelCover(); + } + }); + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -102,7 +131,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { return; } - ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, STORAGE_PERMISSIONS_REQUEST_CODE); + requestPermission(REQUIRED_PERMISSIONS); }); binding.etTitle.addTextChangedListener(new TextWatcher() { @Override @@ -117,15 +146,43 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { public void afterTextChanged(Editable s) {} }); binding.clearButton.setOnClickListener(v -> binding.etTitle.setText("")); + SendbirdUIKit.connect(null); + } - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } else { - REQUIRED_PERMISSIONS = new String[]{Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; + private void requestPermission(@NonNull String[] permissions) { + // 1. check permission + final boolean hasPermission = PermissionUtils.hasPermissions(this, permissions); + if (hasPermission) { + showMediaSelectDialog(); + return; + } + + // 2. determine whether rationale popup should show + final List deniedList = PermissionUtils.getExplicitDeniedPermissionList(this, permissions); + if (!deniedList.isEmpty()) { + showPermissionRationalePopup(deniedList.get(0)); + return; } + // 3. request permission + this.permissionLauncher.launch(permissions); + } + + private void showPermissionRationalePopup(@NonNull String permission) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle(getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); + builder.setMessage(getPermissionGuideMessage(this, permission)); + builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.parse("package:" + getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + appSettingLauncher.launch(intent); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(this, com.sendbird.uikit.R.color.secondary_300)); } private void createCommunityChannel() { @@ -154,37 +211,6 @@ private void createCommunityChannel() { }); } - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - SendbirdChat.setAutoBackgroundDetection(true); - - if (resultCode != RESULT_OK) return; - - if (requestCode == PERMISSION_SETTINGS_REQUEST_ID) { - final boolean hasPermission = PermissionUtils.hasPermissions(this, REQUIRED_PERMISSIONS); - if (hasPermission) { - showMediaSelectDialog(); - } - return; - } - - switch (requestCode) { - case CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE: - break; - case PICK_IMAGE_ACTIVITY_REQUEST_CODE: - if (data != null) { - this.mediaUri = data.getData(); - } - break; - } - - if (this.mediaUri != null) { - mediaFile = FileUtils.uriToFile(getApplicationContext(), mediaUri); - updateChannelCover(); - } - } - private void updateChannelCover() { Glide.with(binding.background.getContext()) .load(mediaUri) @@ -195,44 +221,6 @@ private void updateChannelCover() { .into(binding.ivChannelCover); } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - if (requestCode == STORAGE_PERMISSIONS_REQUEST_CODE && grantResults.length == REQUIRED_PERMISSIONS.length) { - boolean isAllGranted = true; - for (int result : grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - isAllGranted = false; - break; - } - } - - if (isAllGranted) { - showMediaSelectDialog(); - } else { - String[] notGranted = PermissionUtils.getNotGrantedPermissions(this, permissions); - List deniedList = PermissionUtils.getShowRequestPermissionRationale(this, permissions); - if (deniedList.size() == 0) { - AlertDialog.Builder builder = new AlertDialog.Builder(this); - builder.setTitle(getString(com.sendbird.uikit.R.string.sb_text_dialog_permission_title)); - builder.setMessage(getPermissionGuideMessage(this, notGranted[0])); - builder.setPositiveButton(com.sendbird.uikit.R.string.sb_text_go_to_settings, (dialogInterface, i) -> { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Uri.parse("package:" + getPackageName())); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivityForResult(intent, PERMISSION_SETTINGS_REQUEST_ID); - }); - AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(this, com.sendbird.uikit.R.color.secondary_300)); - } - } - } - } - private void showMediaSelectDialog() { DialogListItem delete = new DialogListItem(R.string.text_remove_photo, 0, true); DialogListItem camera = new DialogListItem(com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_camera); @@ -247,11 +235,10 @@ private void showMediaSelectDialog() { DialogUtils.showListBottomDialog(this, items, (view, position, item) -> { try { final int key = item.getKey(); - SendbirdChat.setAutoBackgroundDetection(false); if (key == com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_camera) { takeCamera(); } else if (key == com.sendbird.uikit.R.string.sb_text_channel_settings_change_channel_image_gallery) { - pickImage(); + takePhoto(); } else { removeFile(); } @@ -262,17 +249,19 @@ private void showMediaSelectDialog() { } private void takeCamera() { + SendbirdChat.setAutoBackgroundDetection(false); this.mediaUri = FileUtils.createPictureImageUri(this); if (mediaUri == null) return; Intent intent = IntentUtils.getCameraIntent(this, mediaUri); if (IntentUtils.hasIntent(this, intent)) { - startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE); + takeCameraLauncher.launch(intent); } } - private void pickImage() { + private void takePhoto() { + SendbirdChat.setAutoBackgroundDetection(false); Intent intent = IntentUtils.getImageGalleryIntent(); - startActivityForResult(intent, PICK_IMAGE_ACTIVITY_REQUEST_CODE); + getContentLauncher.launch(intent); } private void removeFile() { diff --git a/uikit-sample/src/main/res/values/strings.xml b/uikit-sample/src/main/res/values/strings.xml index 30654629..e5d6d42f 100644 --- a/uikit-sample/src/main/res/values/strings.xml +++ b/uikit-sample/src/main/res/values/strings.xml @@ -37,4 +37,5 @@ %s participants Couldn\'t update user information. Couldn\'t update disturb setting. + %s\nneeds permission to receive push notification. Go to Settings to allow access diff --git a/uikit/build.gradle b/uikit/build.gradle index 8baf9e53..364e5851 100644 --- a/uikit/build.gradle +++ b/uikit/build.gradle @@ -2,12 +2,12 @@ apply plugin: 'com.android.library' version = UIKIT_VERSION android { - compileSdkVersion 32 + compileSdkVersion 33 version = UIKIT_VERSION defaultConfig { minSdkVersion 21 - targetSdkVersion 32 + targetSdkVersion 33 versionCode Integer.parseInt(UIKIT_VERSION_CODE) versionName UIKIT_VERSION @@ -62,7 +62,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // Sendbird - api 'com.sendbird.sdk:sendbird-chat:4.0.4' + api 'com.sendbird.sdk:sendbird-chat:4.0.5' implementation 'com.github.bumptech.glide:glide:4.13.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.13.0' diff --git a/uikit/src/main/AndroidManifest.xml b/uikit/src/main/AndroidManifest.xml index c49d69c4..3e688a74 100644 --- a/uikit/src/main/AndroidManifest.xml +++ b/uikit/src/main/AndroidManifest.xml @@ -3,13 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" package="com.sendbird.uikit"> - - + + + - @@ -97,6 +94,26 @@ android:name=".activities.ChannelPushSettingActivity" android:configChanges="orientation|screenSize|keyboardHidden" android:launchMode="singleTop" /> + + + + + cls, @NonNull String channelUrl) { + Intent intent = new Intent(context, cls); + intent.putExtra(StringSet.KEY_CHANNEL_URL, channelUrl); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(SendbirdUIKit.isDarkMode() ? R.style.AppTheme_Dark_Sendbird : R.style.AppTheme_Sendbird); + setContentView(R.layout.sb_activity); + + String url = getIntent().getStringExtra(StringSet.KEY_CHANNEL_URL); + if (TextUtils.isEmpty(url)) { + ContextUtils.toastError(this, R.string.sb_text_error_get_channel); + } else { + Fragment fragment = createFragment(); + FragmentManager manager = getSupportFragmentManager(); + manager.popBackStack(); + manager.beginTransaction() + .replace(R.id.sb_fragment_container, fragment) + .commit(); + } + } + + /** + * It will be called when the {@link OpenChannelBannedUserListActivity} is being created. + * The data contained in Intent is delivered to Fragment's Bundle. + * + * @return {@link com.sendbird.uikit.fragments.OpenChannelBannedUserListFragment} + * @since 3.1.0 + */ + @NonNull + protected Fragment createFragment() { + final Bundle args = getIntent() != null && getIntent().getExtras() != null ? getIntent().getExtras() : new Bundle(); + return SendbirdUIKit.getFragmentFactory().newOpenChannelBannedUserListFragment(args.getString(StringSet.KEY_CHANNEL_URL, ""), args); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelModerationActivity.java b/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelModerationActivity.java new file mode 100644 index 00000000..919ac59e --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelModerationActivity.java @@ -0,0 +1,86 @@ +package com.sendbird.uikit.activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.utils.ContextUtils; +import com.sendbird.uikit.utils.TextUtils; + +/** + * Activity displays moderation menu of the open channel. + * + * @since 3.1.0 + */ +public class OpenChannelModerationActivity extends AppCompatActivity { + + /** + * Create an intent for a {@link OpenChannelModerationActivity}. + * + * @param context A Context of the application package implementing this class. + * @param channelUrl the url of the channel will be implemented. + * @return OpenChannelModerationActivity Intent. + * @since 3.1.0 + */ + @NonNull + public static Intent newIntent(@NonNull Context context, @NonNull String channelUrl) { + return newIntentFromCustomActivity(context, OpenChannelModerationActivity.class, channelUrl); + } + + /** + * Create an intent for a custom activity. The custom activity must inherit {@link OpenChannelModerationActivity}. + * + * @param context A Context of the application package implementing this class. + * @param cls The activity class that is to be used for the intent. + * @param channelUrl the url of the channel will be implemented. + * @return Returns a newly created Intent that can be used to launch the activity. + * @since 3.1.0 + */ + @NonNull + public static Intent newIntentFromCustomActivity(@NonNull Context context, @NonNull Class cls, @NonNull String channelUrl) { + Intent intent = new Intent(context, cls); + intent.putExtra(StringSet.KEY_CHANNEL_URL, channelUrl); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(SendbirdUIKit.isDarkMode() ? R.style.AppTheme_Dark_Sendbird : R.style.AppTheme_Sendbird); + setContentView(R.layout.sb_activity); + + String url = getIntent().getStringExtra(StringSet.KEY_CHANNEL_URL); + if (TextUtils.isEmpty(url)) { + ContextUtils.toastError(this, R.string.sb_text_error_get_channel); + } else { + Fragment fragment = createFragment(); + FragmentManager manager = getSupportFragmentManager(); + manager.popBackStack(); + manager.beginTransaction() + .replace(R.id.sb_fragment_container, fragment) + .commit(); + } + } + + /** + * It will be called when the {@link OpenChannelModerationActivity} is being created. + * The data contained in Intent is delivered to Fragment's Bundle. + * + * @return {@link com.sendbird.uikit.fragments.OpenChannelModerationFragment} + * @since 3.1.0 + */ + @NonNull + protected Fragment createFragment() { + final Bundle args = getIntent() != null && getIntent().getExtras() != null ? getIntent().getExtras() : new Bundle(); + return SendbirdUIKit.getFragmentFactory().newOpenChannelModerationFragment(args.getString(StringSet.KEY_CHANNEL_URL, ""), args); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelMutedParticipantListActivity.java b/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelMutedParticipantListActivity.java new file mode 100644 index 00000000..6eb1c6f8 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelMutedParticipantListActivity.java @@ -0,0 +1,86 @@ +package com.sendbird.uikit.activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.utils.ContextUtils; +import com.sendbird.uikit.utils.TextUtils; + +/** + * Activity displays a list of muted participants from a channel. + * + * @since 3.1.0 + */ +public class OpenChannelMutedParticipantListActivity extends AppCompatActivity { + + /** + * Create an intent for a {@link OpenChannelMutedParticipantListActivity}. + * + * @param context A Context of the application package implementing this class. + * @param channelUrl the url of the channel will be implemented. + * @return OpenChannelMutedParticipantListActivity Intent. + * @since 3.1.0 + */ + @NonNull + public static Intent newIntent(@NonNull Context context, @NonNull String channelUrl) { + return newIntentFromCustomActivity(context, OpenChannelMutedParticipantListActivity.class, channelUrl); + } + + /** + * Create an intent for a custom activity. The custom activity must inherit {@link OpenChannelMutedParticipantListActivity}. + * + * @param context A Context of the application package implementing this class. + * @param cls The activity class that is to be used for the intent. + * @param channelUrl the url of the channel will be implemented. + * @return Returns a newly created Intent that can be used to launch the activity. + * @since 3.1.0 + */ + @NonNull + public static Intent newIntentFromCustomActivity(@NonNull Context context, @NonNull Class cls, @NonNull String channelUrl) { + Intent intent = new Intent(context, cls); + intent.putExtra(StringSet.KEY_CHANNEL_URL, channelUrl); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(SendbirdUIKit.isDarkMode() ? R.style.AppTheme_Dark_Sendbird : R.style.AppTheme_Sendbird); + setContentView(R.layout.sb_activity); + + String url = getIntent().getStringExtra(StringSet.KEY_CHANNEL_URL); + if (TextUtils.isEmpty(url)) { + ContextUtils.toastError(this, R.string.sb_text_error_get_channel); + } else { + Fragment fragment = createFragment(); + FragmentManager manager = getSupportFragmentManager(); + manager.popBackStack(); + manager.beginTransaction() + .replace(R.id.sb_fragment_container, fragment) + .commit(); + } + } + + /** + * It will be called when the {@link OpenChannelMutedParticipantListActivity} is being created. + * The data contained in Intent is delivered to Fragment's Bundle. + * + * @return {@link com.sendbird.uikit.fragments.OpenChannelMutedParticipantListFragment} + * @since 3.1.0 + */ + @NonNull + protected Fragment createFragment() { + final Bundle args = getIntent() != null && getIntent().getExtras() != null ? getIntent().getExtras() : new Bundle(); + return SendbirdUIKit.getFragmentFactory().newOpenChannelMutedParticipantListFragment(args.getString(StringSet.KEY_CHANNEL_URL, ""), args); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelOperatorListActivity.java b/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelOperatorListActivity.java new file mode 100644 index 00000000..b21a96f4 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelOperatorListActivity.java @@ -0,0 +1,86 @@ +package com.sendbird.uikit.activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.utils.ContextUtils; +import com.sendbird.uikit.utils.TextUtils; + +/** + * Activity displays a list of operators from open channel. + * + * @since 3.1.0 + */ +public class OpenChannelOperatorListActivity extends AppCompatActivity { + + /** + * Create an intent for a {@link OpenChannelOperatorListActivity}. + * + * @param context A Context of the application package implementing this class. + * @param channelUrl the url of the channel will be implemented. + * @return OpenChannelOperatorListActivity Intent. + * @since 3.1.0 + */ + @NonNull + public static Intent newIntent(@NonNull Context context, @NonNull String channelUrl) { + return newIntentFromCustomActivity(context, OpenChannelOperatorListActivity.class, channelUrl); + } + + /** + * Create an intent for a custom activity. The custom activity must inherit {@link OpenChannelOperatorListActivity}. + * + * @param context A Context of the application package implementing this class. + * @param cls The activity class that is to be used for the intent. + * @param channelUrl the url of the channel will be implemented. + * @return Returns a newly created Intent that can be used to launch the activity. + * @since 3.1.0 + */ + @NonNull + public static Intent newIntentFromCustomActivity(@NonNull Context context, @NonNull Class cls, @NonNull String channelUrl) { + Intent intent = new Intent(context, cls); + intent.putExtra(StringSet.KEY_CHANNEL_URL, channelUrl); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(SendbirdUIKit.isDarkMode() ? R.style.AppTheme_Dark_Sendbird : R.style.AppTheme_Sendbird); + setContentView(R.layout.sb_activity); + + String url = getIntent().getStringExtra(StringSet.KEY_CHANNEL_URL); + if (TextUtils.isEmpty(url)) { + ContextUtils.toastError(this, R.string.sb_text_error_get_channel); + } else { + Fragment fragment = createFragment(); + FragmentManager manager = getSupportFragmentManager(); + manager.popBackStack(); + manager.beginTransaction() + .replace(R.id.sb_fragment_container, fragment) + .commit(); + } + } + + /** + * It will be called when the {@link OpenChannelOperatorListActivity} is being created. + * The data contained in Intent is delivered to Fragment's Bundle. + * + * @return {@link com.sendbird.uikit.fragments.OpenChannelOperatorListFragment} + * @since 3.1.0 + */ + @NonNull + protected Fragment createFragment() { + final Bundle args = getIntent() != null && getIntent().getExtras() != null ? getIntent().getExtras() : new Bundle(); + return SendbirdUIKit.getFragmentFactory().newOpenChannelOperatorListFragment(args.getString(StringSet.KEY_CHANNEL_URL, ""), args); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelRegisterOperatorActivity.java b/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelRegisterOperatorActivity.java new file mode 100644 index 00000000..d69a7c87 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/OpenChannelRegisterOperatorActivity.java @@ -0,0 +1,86 @@ +package com.sendbird.uikit.activities; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.utils.ContextUtils; +import com.sendbird.uikit.utils.TextUtils; + +/** + * Activity displays a list of users and provide a current user to register other users as operator. + * + * @since 3.1.0 + */ +public class OpenChannelRegisterOperatorActivity extends AppCompatActivity { + + /** + * Create an intent for a {@link OpenChannelRegisterOperatorActivity}. + * + * @param context A Context of the application package implementing this class. + * @param channelUrl the url of the channel will be implemented. + * @return OpenChannelRegisterOperatorActivity Intent. + * @since 3.1.0 + */ + @NonNull + public static Intent newIntent(@NonNull Context context, @NonNull String channelUrl) { + return newIntentFromCustomActivity(context, OpenChannelRegisterOperatorActivity.class, channelUrl); + } + + /** + * Create an intent for a custom activity. The custom activity must inherit {@link OpenChannelRegisterOperatorActivity}. + * + * @param context A Context of the application package implementing this class. + * @param cls The activity class that is to be used for the intent. + * @param channelUrl the url of the channel will be implemented. + * @return Returns a newly created Intent that can be used to launch the activity. + * @since 3.1.0 + */ + @NonNull + public static Intent newIntentFromCustomActivity(@NonNull Context context, @NonNull Class cls, @NonNull String channelUrl) { + Intent intent = new Intent(context, cls); + intent.putExtra(StringSet.KEY_CHANNEL_URL, channelUrl); + return intent; + } + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setTheme(SendbirdUIKit.isDarkMode() ? R.style.AppTheme_Dark_Sendbird : R.style.AppTheme_Sendbird); + setContentView(R.layout.sb_activity); + + String url = getIntent().getStringExtra(StringSet.KEY_CHANNEL_URL); + if (TextUtils.isEmpty(url)) { + ContextUtils.toastError(this, R.string.sb_text_error_get_channel); + } else { + Fragment fragment = createFragment(); + FragmentManager manager = getSupportFragmentManager(); + manager.popBackStack(); + manager.beginTransaction() + .replace(R.id.sb_fragment_container, fragment) + .commit(); + } + } + + /** + * It will be called when the {@link OpenChannelRegisterOperatorActivity} is being created. + * The data contained in Intent is delivered to Fragment's Bundle. + * + * @return {@link com.sendbird.uikit.fragments.OpenChannelRegisterOperatorFragment} + * @since 3.1.0 + */ + @NonNull + protected Fragment createFragment() { + final Bundle args = getIntent() != null && getIntent().getExtras() != null ? getIntent().getExtras() : new Bundle(); + return SendbirdUIKit.getFragmentFactory().newOpenChannelRegisterOperatorFragment(args.getString(StringSet.KEY_CHANNEL_URL, ""), args); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BannedUserListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BannedUserListAdapter.java index a3f23fea..49bf6346 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BannedUserListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/BannedUserListAdapter.java @@ -1,9 +1,45 @@ package com.sendbird.uikit.activities.adapter; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.channel.Role; import com.sendbird.android.user.User; +import java.util.List; + /** * BannedUserListAdapter provides a binding from a {@link User} set to views that are displayed within a RecyclerView. */ public class BannedUserListAdapter extends UserTypeListAdapter { + @NonNull + private Role myRole = Role.NONE; + + /** + * Sets the {@link List } to be displayed. + * + * @param userList list to be displayed + * @param myRole The role of the current user + */ + public void setItems(@NonNull List userList, @NonNull Role myRole) { + final UserTypeDiffCallback diffCallback = UserTypeDiffCallback.createFromGroupChannel(getItems(), userList, this.myRole, myRole); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + + setUsers(userList); + this.myRole = myRole; + diffResult.dispatchUpdatesTo(this); + } + + @Override + protected boolean isCurrentUserOperator() { + return myRole == Role.OPERATOR; + } + + @NonNull + @Override + protected String getItemViewDescription(@NonNull Context context, @NonNull User user) { + return ""; + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/ChannelListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/ChannelListAdapter.java index 4d9359b9..c99cb51d 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/ChannelListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/ChannelListAdapter.java @@ -2,7 +2,6 @@ import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; -import android.annotation.SuppressLint; import android.content.Context; import android.util.TypedValue; import android.view.LayoutInflater; @@ -158,7 +157,6 @@ public OnItemLongClickListener getOnItemLongClickListener() { * * @return The {@link List} in this adapter. */ - @SuppressLint("KotlinPropertyAccess") @Override @NonNull public List getItems() { diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/EmojiReactionListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/EmojiReactionListAdapter.java index e81479d0..d4f6bb73 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/EmojiReactionListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/EmojiReactionListAdapter.java @@ -2,7 +2,6 @@ import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; -import android.annotation.SuppressLint; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -258,7 +257,6 @@ public void setUseMoreButton(boolean useMoreButton) { * @return true if the view is using more button, false otherwise * @since 1.1.2 */ - @SuppressLint("KotlinPropertyAccess") public boolean useMoreButton() { return useMoreButton; } diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/EmojiReactionUserListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/EmojiReactionUserListAdapter.java index 272640d6..26853cd9 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/EmojiReactionUserListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/EmojiReactionUserListAdapter.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.activities.adapter; -import android.annotation.SuppressLint; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -103,7 +102,6 @@ public User getItem(int position) { * @return The {@link List} in this adapter. * @since 1.1.0 */ - @SuppressLint("KotlinPropertyAccess") @Override @NonNull public List getItems() { diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MemberListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MemberListAdapter.java index 39cbc63c..3d15b94f 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MemberListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MemberListAdapter.java @@ -1,6 +1,15 @@ package com.sendbird.uikit.activities.adapter; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.channel.Role; import com.sendbird.android.user.Member; +import com.sendbird.uikit.R; + +import java.util.List; /** * MemberListAdapter provides a binding from a {@link Member} type data set to views that are displayed within a RecyclerView. @@ -8,4 +17,32 @@ * @since 3.0.0 */ public class MemberListAdapter extends UserTypeListAdapter { + @NonNull + private Role myRole = Role.NONE; + + /** + * Sets the {@link List } to be displayed. + * + * @param userList list to be displayed + * @param myRole The role of the current user + */ + public void setItems(@NonNull List userList, @NonNull Role myRole) { + final UserTypeDiffCallback diffCallback = UserTypeDiffCallback.createFromGroupChannel(getItems(), userList, this.myRole, myRole); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + + setUsers(userList); + this.myRole = myRole; + diffResult.dispatchUpdatesTo(this); + } + + @Override + protected boolean isCurrentUserOperator() { + return myRole == Role.OPERATOR; + } + + @NonNull + @Override + protected String getItemViewDescription(@NonNull Context context, @NonNull Member member) { + return member.getRole() == Role.OPERATOR ? context.getString(R.string.sb_text_operator) : ""; + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageSearchAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageSearchAdapter.java index 971c5929..a9cc62d7 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageSearchAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MessageSearchAdapter.java @@ -2,7 +2,6 @@ import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; -import android.annotation.SuppressLint; import android.content.Context; import android.util.TypedValue; import android.view.LayoutInflater; @@ -87,7 +86,6 @@ public BaseMessage getItem(int position) { * * @return The {@link List} in this adapter. */ - @SuppressLint("KotlinPropertyAccess") @Override @NonNull public List getItems() { diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MutedMemberListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MutedMemberListAdapter.java index 17d18ac0..85379725 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MutedMemberListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/MutedMemberListAdapter.java @@ -1,6 +1,15 @@ package com.sendbird.uikit.activities.adapter; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.channel.Role; import com.sendbird.android.user.Member; +import com.sendbird.uikit.R; + +import java.util.List; /** * MutedMemberListAdapter provides a binding from a {@link Member} type data set to views that are displayed within a RecyclerView. @@ -8,4 +17,32 @@ * @since 3.0.0 */ public class MutedMemberListAdapter extends UserTypeListAdapter { + @NonNull + private Role myRole = Role.NONE; + + /** + * Sets the {@link List } to be displayed. + * + * @param userList list to be displayed + * @param myRole The role of the current user + */ + public void setItems(@NonNull List userList, @NonNull Role myRole) { + final UserTypeDiffCallback diffCallback = UserTypeDiffCallback.createFromGroupChannel(getItems(), userList, this.myRole, myRole); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + + setUsers(userList); + this.myRole = myRole; + diffResult.dispatchUpdatesTo(this); + } + + @Override + protected boolean isCurrentUserOperator() { + return myRole == Role.OPERATOR; + } + + @NonNull + @Override + protected String getItemViewDescription(@NonNull Context context, @NonNull Member member) { + return member.getRole() == Role.OPERATOR ? context.getString(R.string.sb_text_operator) : ""; + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelBannedUserListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelBannedUserListAdapter.java new file mode 100644 index 00000000..5cad0c84 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelBannedUserListAdapter.java @@ -0,0 +1,51 @@ +package com.sendbird.uikit.activities.adapter; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; + +import java.util.List; + +/** + * OpenChannelBannedUserListAdapter provides a binding from a {@link User} set to views that are displayed within a RecyclerView. + * + * @since 3.1.0 + */ +public class OpenChannelBannedUserListAdapter extends UserTypeListAdapter { + @Nullable + private OpenChannel openChannel; + + /** + * Sets the {@link List } to be displayed. + * + * @param userList list to be displayed + * @param openChannel The latest open channel + * @since 3.1.0 + */ + public void setItems(@NonNull List userList, @NonNull OpenChannel openChannel) { + final UserTypeDiffCallback diffCallback = UserTypeDiffCallback.createFromOpenChannel(getItems(), userList, this.openChannel, openChannel); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + + setUsers(userList); + this.openChannel = OpenChannel.clone(openChannel); + diffResult.dispatchUpdatesTo(this); + } + + @Override + protected boolean isCurrentUserOperator() { + if (this.openChannel == null) return false; + return this.openChannel.isOperator(SendbirdChat.getCurrentUser()); + } + + @NonNull + @Override + protected String getItemViewDescription(@NonNull Context context, @NonNull User user) { + return ""; + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelMutedParticipantListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelMutedParticipantListAdapter.java new file mode 100644 index 00000000..6192476c --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelMutedParticipantListAdapter.java @@ -0,0 +1,53 @@ +package com.sendbird.uikit.activities.adapter; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.R; + +import java.util.List; + +/** + * OpenChannelMutedParticipantListAdapter provides a binding from a {@link User} type data set to views that are displayed within a RecyclerView. + * + * @since 3.1.0 + */ +public class OpenChannelMutedParticipantListAdapter extends UserTypeListAdapter { + @Nullable + private OpenChannel openChannel; + + /** + * Sets the {@link List } to be displayed. + * + * @param userList list to be displayed + * @param openChannel The latest open channel + * @since 3.1.0 + */ + public void setItems(@NonNull List userList, @NonNull OpenChannel openChannel) { + final UserTypeDiffCallback diffCallback = UserTypeDiffCallback.createFromOpenChannel(getItems(), userList, this.openChannel, openChannel); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + + setUsers(userList); + this.openChannel = OpenChannel.clone(openChannel); + diffResult.dispatchUpdatesTo(this); + } + + @Override + protected boolean isCurrentUserOperator() { + if (this.openChannel == null) return false; + return this.openChannel.isOperator(SendbirdChat.getCurrentUser()); + } + + @NonNull + @Override + protected String getItemViewDescription(@NonNull Context context, @NonNull User user) { + if (this.openChannel == null) return ""; + return this.openChannel.isOperator(user) ? context.getString(R.string.sb_text_operator) : ""; + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelOperatorListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelOperatorListAdapter.java new file mode 100644 index 00000000..4dd488f1 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelOperatorListAdapter.java @@ -0,0 +1,51 @@ +package com.sendbird.uikit.activities.adapter; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; + +import java.util.List; + +/** + * OpenChannelOperatorListAdapter provides a binding from a {@link User} type data set to views that are displayed within a RecyclerView. + * + * @since 3.1.0 + */ +public class OpenChannelOperatorListAdapter extends UserTypeListAdapter { + @Nullable + private OpenChannel openChannel; + + /** + * Sets the {@link List } to be displayed. + * + * @param userList list to be displayed + * @param openChannel The latest open channel + * @since 3.1.0 + */ + public void setItems(@NonNull List userList, @NonNull OpenChannel openChannel) { + final UserTypeDiffCallback diffCallback = UserTypeDiffCallback.createFromOpenChannel(getItems(), userList, this.openChannel, openChannel); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + + setUsers(userList); + this.openChannel = OpenChannel.clone(openChannel); + diffResult.dispatchUpdatesTo(this); + } + + @Override + protected boolean isCurrentUserOperator() { + if (this.openChannel == null) return false; + return this.openChannel.isOperator(SendbirdChat.getCurrentUser()); + } + + @NonNull + @Override + protected String getItemViewDescription(@NonNull Context context, @NonNull User user) { + return ""; + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelRegisterOperatorListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelRegisterOperatorListAdapter.java new file mode 100644 index 00000000..b20da1e2 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OpenChannelRegisterOperatorListAdapter.java @@ -0,0 +1,45 @@ +package com.sendbird.uikit.activities.adapter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.interfaces.OnItemClickListener; +import com.sendbird.uikit.interfaces.UserInfo; +import com.sendbird.uikit.utils.UserUtils; + +/** + * OpenChannelRegisterOperatorListAdapter provides a binding from a {@link User} type data set to views that are displayed within a RecyclerView. + * + * @since 3.1.0 + */ +public class OpenChannelRegisterOperatorListAdapter extends SelectUserListAdapter { + @Nullable + private final OpenChannel openChannel; + + public OpenChannelRegisterOperatorListAdapter(@Nullable OpenChannel openChannel) { + this(openChannel, null); + } + + public OpenChannelRegisterOperatorListAdapter(@Nullable OpenChannel openChannel, @Nullable OnItemClickListener listener) { + super(listener); + this.openChannel = openChannel; + } + + @Override + protected boolean isDisabled(@NonNull User user) { + return openChannel != null && openChannel.isOperator(user); + } + + @Override + protected boolean isSelected(@NonNull User user) { + return selectedUserIdList.contains(user.getUserId()); + } + + @NonNull + @Override + protected UserInfo toUserInfo(@NonNull User user) { + return UserUtils.toUserInfo(user); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OperatorListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OperatorListAdapter.java index d24d7f35..2945f1ab 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OperatorListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/OperatorListAdapter.java @@ -1,11 +1,47 @@ package com.sendbird.uikit.activities.adapter; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.channel.Role; import com.sendbird.android.user.User; +import java.util.List; + /** * OperatorListAdapter provides a binding from a {@link User} type data set to views that are displayed within a RecyclerView. * * @since 3.0.0 */ public class OperatorListAdapter extends UserTypeListAdapter { + @NonNull + private Role myRole = Role.NONE; + + /** + * Sets the {@link List } to be displayed. + * + * @param userList list to be displayed + * @param myRole The role of the current user + */ + public void setItems(@NonNull List userList, @NonNull Role myRole) { + final UserTypeDiffCallback diffCallback = UserTypeDiffCallback.createFromGroupChannel(getItems(), userList, this.myRole, myRole); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + + setUsers(userList); + this.myRole = myRole; + diffResult.dispatchUpdatesTo(this); + } + + @Override + protected boolean isCurrentUserOperator() { + return myRole == Role.OPERATOR; + } + + @NonNull + @Override + protected String getItemViewDescription(@NonNull Context context, @NonNull User user) { + return ""; + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/ParticipantListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/ParticipantListAdapter.java index 6942d0a6..4b0adc5b 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/ParticipantListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/ParticipantListAdapter.java @@ -1,6 +1,17 @@ package com.sendbird.uikit.activities.adapter; +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.OpenChannel; import com.sendbird.android.user.User; +import com.sendbird.uikit.R; + +import java.util.List; /** * ParticipantsListAdapter provides a binding from a {@link User} type data set to views that are displayed within a RecyclerView. @@ -8,4 +19,35 @@ * @since 3.0.0 */ public class ParticipantListAdapter extends UserTypeListAdapter { + @Nullable + private OpenChannel openChannel; + + /** + * Sets the {@link List } to be displayed. + * + * @param userList list to be displayed + * @param openChannel The latest open channel + * @since 3.1.0 + */ + public void setItems(@NonNull List userList, @NonNull OpenChannel openChannel) { + final UserTypeDiffCallback diffCallback = UserTypeDiffCallback.createFromOpenChannel(getItems(), userList, this.openChannel, openChannel); + final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); + + setUsers(userList); + this.openChannel = OpenChannel.clone(openChannel); + diffResult.dispatchUpdatesTo(this); + } + + @Override + protected boolean isCurrentUserOperator() { + if (this.openChannel == null) return false; + return this.openChannel.isOperator(SendbirdChat.getCurrentUser()); + } + + @NonNull + @Override + protected String getItemViewDescription(@NonNull Context context, @NonNull User user) { + if (this.openChannel == null) return ""; + return this.openChannel.isOperator(user) ? context.getString(R.string.sb_text_operator) : ""; + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/SelectUserListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/SelectUserListAdapter.java index 55b5f051..6b443965 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/SelectUserListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/SelectUserListAdapter.java @@ -2,7 +2,6 @@ import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; -import android.annotation.SuppressLint; import android.view.LayoutInflater; import android.view.ViewGroup; @@ -45,7 +44,7 @@ public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewT * Constructor */ public SelectUserListAdapter() { - setHasStableIds(true); + this(null); } /** @@ -69,7 +68,6 @@ public T getItem(int position) { return userList.get(position); } - @SuppressLint("KotlinPropertyAccess") @Override @NonNull public List getItems() { diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/UserTypeDiffCallback.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/UserTypeDiffCallback.java new file mode 100644 index 00000000..19890db6 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/UserTypeDiffCallback.java @@ -0,0 +1,106 @@ +package com.sendbird.uikit.activities.adapter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.channel.Role; +import com.sendbird.android.user.Member; +import com.sendbird.android.user.User; + +import java.util.List; + +class UserTypeDiffCallback extends DiffUtil.Callback { + @NonNull + private final List oldUserList; + @NonNull + private final List newUserList; + @NonNull + private final Role oldMyRole; + @NonNull + private final Role newMyRole; + @Nullable + private final OpenChannel oldOpenChannel; + @Nullable + private final OpenChannel newOpenChannel; + + static UserTypeDiffCallback createFromOpenChannel(@NonNull List oldUserList, @NonNull List newUserList, @Nullable OpenChannel oldOpenChannel, @Nullable OpenChannel newOpenChannel) { + return new UserTypeDiffCallback<>(oldUserList, newUserList, + oldOpenChannel != null && oldOpenChannel.isOperator(SendbirdChat.getCurrentUser()) ? Role.OPERATOR : Role.NONE, + newOpenChannel != null && newOpenChannel.isOperator(SendbirdChat.getCurrentUser()) ? Role.OPERATOR : Role.NONE, + oldOpenChannel, newOpenChannel); + } + + static UserTypeDiffCallback createFromGroupChannel(@NonNull List oldUserList, @NonNull List newUserList, @NonNull Role oldMyRole, @NonNull Role newMyRole) { + return new UserTypeDiffCallback<>(oldUserList, newUserList, oldMyRole, newMyRole, null, null); + } + + private UserTypeDiffCallback(@NonNull List oldUserList, @NonNull List newUserList, @NonNull Role oldMyRole, @NonNull Role newMyRole, @Nullable OpenChannel oldOpenChannel, @Nullable OpenChannel newOpenChannel) { + this.oldUserList = oldUserList; + this.newUserList = newUserList; + this.oldMyRole = oldMyRole; + this.newMyRole = newMyRole; + this.oldOpenChannel = oldOpenChannel; + this.newOpenChannel = newOpenChannel; + } + + @Override + public int getOldListSize() { + return oldUserList.size(); + } + + @Override + public int getNewListSize() { + return newUserList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + final T oldUser = oldUserList.get(oldItemPosition); + final T newUser = newUserList.get(newItemPosition); + + return oldUser.equals(newUser) && oldMyRole.equals(newMyRole); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + final T oldUser = oldUserList.get(oldItemPosition); + final T newUser = newUserList.get(newItemPosition); + + if (!areItemsTheSame(oldItemPosition, newItemPosition)) { + return false; + } + + final String oldNickname = oldUser.getNickname(); + final String newNickname = newUser.getNickname(); + if (!newNickname.equals(oldNickname)) { + return false; + } + + if (newUser instanceof Member && oldUser instanceof Member) { + final Member oldMember = (Member) oldUser; + final Member newMember = (Member) newUser; + if (oldMember.isMuted() != newMember.isMuted()) { + return false; + } + + if (oldMember.getRole() != newMember.getRole()) { + return false; + } + } + + if (oldOpenChannel != null && newOpenChannel != null) { + final boolean oldIsOperator = oldOpenChannel.isOperator(oldUser); + final boolean newIsOperator = newOpenChannel.isOperator(newUser); + if (oldIsOperator != newIsOperator) { + return false; + } + } + + final String oldProfileUrl = oldUser.getProfileUrl(); + final String newProfileUrl = newUser.getProfileUrl(); + return newProfileUrl.equals(oldProfileUrl); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/UserTypeListAdapter.java b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/UserTypeListAdapter.java index b55e0abb..2f63f44c 100644 --- a/uikit/src/main/java/com/sendbird/uikit/activities/adapter/UserTypeListAdapter.java +++ b/uikit/src/main/java/com/sendbird/uikit/activities/adapter/UserTypeListAdapter.java @@ -10,10 +10,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.view.ContextThemeWrapper; -import androidx.recyclerview.widget.DiffUtil; -import com.sendbird.android.channel.Role; import com.sendbird.android.user.Member; +import com.sendbird.android.user.RestrictedUser; +import com.sendbird.android.user.RestrictionType; import com.sendbird.android.user.User; import com.sendbird.uikit.R; import com.sendbird.uikit.activities.viewholder.BaseViewHolder; @@ -29,7 +29,7 @@ /** * Adapters provides a binding from a {@link User} set to views that are displayed within a RecyclerView. */ -public class UserTypeListAdapter extends BaseAdapter> { +abstract public class UserTypeListAdapter extends BaseAdapter> { @NonNull final private List users = new ArrayList<>(); @Nullable @@ -38,8 +38,6 @@ public class UserTypeListAdapter extends BaseAdapter longClickListener; @Nullable private OnItemClickListener actionItemClickListener; - @NonNull - private Role myRole = Role.NONE; @Nullable private OnItemClickListener profileClickListener; @@ -198,17 +196,32 @@ public OnItemClickListener getOnProfileClickListener() { * Sets the {@link List} to be displayed. * * @param userList list to be displayed + * @since 3.1.0 */ - public void setItems(@NonNull List userList, @NonNull Role myRole) { - final UserTypeDiffCallback diffCallback = new UserTypeDiffCallback<>(this.users, userList, this.myRole, myRole); - final DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(diffCallback); - + protected void setUsers(@NonNull List userList) { this.users.clear(); this.users.addAll(userList); - this.myRole = myRole; - diffResult.dispatchUpdatesTo(this); } + /** + * Returns whether the current user is operator or not. + * + * @return {@code true} if the current user is operator, {@code false} otherwise + * @since 3.1.0 + */ + abstract protected boolean isCurrentUserOperator(); + + /** + * Returns the description to be shown in item view. + * + * @param context Context for item view + * @param user The user to be checked if showing operator badge + * @return Text to be shown as description in item view + * @since 3.1.0 + */ + @NonNull + abstract protected String getItemViewDescription(@NonNull Context context, @NonNull T user); + private class UserPreviewHolder extends BaseViewHolder { @NonNull private final SbViewUserPreviewBinding binding; @@ -250,81 +263,14 @@ private class UserPreviewHolder extends BaseViewHolder { @Override public void bind(@NonNull T user) { - binding.userViewHolder.useActionMenu(myRole == Role.OPERATOR && actionItemClickListener != null); + boolean isMuted = false; if (user instanceof Member) { - UserPreview.drawMember(binding.userViewHolder, (Member) user); - } else { - UserPreview.drawMemberFromUser(binding.userViewHolder, user); + isMuted = ((Member) user).isMuted(); + } else if (user instanceof RestrictedUser) { + isMuted = ((RestrictedUser) user).getRestrictionInfo().getRestrictionType().equals(RestrictionType.MUTED); } - } - } - - private static class UserTypeDiffCallback extends DiffUtil.Callback { - @NonNull - private final List oldUserList; - @NonNull - private final List newUserList; - @NonNull - private final Role oldMyRole; - @NonNull - private final Role newMyRole; - - UserTypeDiffCallback(@NonNull List oldUserList, @NonNull List newUserList, @NonNull Role oldMyRole, @NonNull Role newMyRole) { - this.oldUserList = oldUserList; - this.newUserList = newUserList; - this.oldMyRole = oldMyRole; - this.newMyRole = newMyRole; - } - - @Override - public int getOldListSize() { - return oldUserList.size(); - } - - @Override - public int getNewListSize() { - return newUserList.size(); - } - - @Override - public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { - final T oldUser = oldUserList.get(oldItemPosition); - final T newUser = newUserList.get(newItemPosition); - - return oldUser.equals(newUser) && oldMyRole.equals(newMyRole); - } - - @Override - public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { - final T oldUser = oldUserList.get(oldItemPosition); - final T newUser = newUserList.get(newItemPosition); - - if (!areItemsTheSame(oldItemPosition, newItemPosition)) { - return false; - } - - final String oldNickname = oldUser.getNickname(); - final String newNickname = newUser.getNickname(); - if (!newNickname.equals(oldNickname)) { - return false; - } - - final String oldProfileUrl = oldUser.getProfileUrl(); - final String newProfileUrl = newUser.getProfileUrl(); - - if (newUser instanceof Member && oldUser instanceof Member) { - final Member oldMember = (Member) oldUser; - final Member newMember = (Member) newUser; - if (oldMember.isMuted() != newMember.isMuted()) { - return false; - } - - if (oldMember.getRole() != newMember.getRole()) { - return false; - } - } - - return newProfileUrl.equals(oldProfileUrl); + binding.userViewHolder.useActionMenu(isCurrentUserOperator() && actionItemClickListener != null); + UserPreview.drawUser(binding.userViewHolder, user, getItemViewDescription(binding.getRoot().getContext(), user), isMuted); } } } diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/BannedUserListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/BannedUserListFragment.java index 6d516655..c2d3024d 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/BannedUserListFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/BannedUserListFragment.java @@ -101,14 +101,13 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull BannedUserLis @Override protected void onReady(@NonNull ReadyStatus status, @NonNull BannedUserListModule module, @NonNull BannedUserListViewModel viewModel) { Logger.d(">> BannedUserListFragment::onReady status=%s", status); - if (status != ReadyStatus.READY) { + final GroupChannel channel = viewModel.getChannel(); + if (status == ReadyStatus.ERROR || channel == null) { final StatusComponent statusComponent = module.getStatusComponent(); statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); return; } - final GroupChannel channel = viewModel.getChannel(); - if (channel == null) return; if (channel.getMyRole() != Role.OPERATOR) shouldActivityFinish(); viewModel.loadInitial(); } @@ -486,9 +485,9 @@ public Builder setOnHeaderRightButtonClickListener(@NonNull View.OnClickListener } /** - * Sets the channel user list adapter. + * Sets the banned user list adapter. * - * @param adapter the adapter for the channel user list. + * @param adapter the adapter for the banned user list. * @return This Builder object to allow for chaining of calls to set methods. * @since 3.0.0 */ @@ -499,7 +498,7 @@ public Builder setBannedUserListAdapter(T adap } /** - * Sets the click listener on the item of channel user list. + * Sets the click listener on the item of banned user list. * * @param itemClickListener The callback that will run. * @return This Builder object to allow for chaining of calls to set methods. @@ -512,7 +511,7 @@ public Builder setOnItemClickListener(@NonNull OnItemClickListener itemCli } /** - * Sets the long click listener on the item of channel user list. + * Sets the long click listener on the item of banned user list. * * @param itemLongClickListener The callback that will run. * @return This Builder object to allow for chaining of calls to set methods. @@ -525,7 +524,7 @@ public Builder setOnItemLongClickListener(@NonNull OnItemLongClickListener } /** - * Sets the action item click listener on the item of channel user list. + * Sets the action item click listener on the item of banned user list. * * @param actionItemClickListener The callback that will run. * @return This Builder object to allow for chaining of calls to set methods. 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 e532356c..3ffe2bd4 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelFragment.java @@ -2,7 +2,6 @@ import static android.app.Activity.RESULT_OK; -import android.Manifest; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -15,6 +14,8 @@ import android.view.View; import android.widget.EditText; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -84,6 +85,7 @@ import com.sendbird.uikit.utils.FileUtils; import com.sendbird.uikit.utils.IntentUtils; import com.sendbird.uikit.utils.MessageUtils; +import com.sendbird.uikit.utils.PermissionUtils; import com.sendbird.uikit.utils.ReactionUtils; import com.sendbird.uikit.utils.SoftInputUtils; import com.sendbird.uikit.utils.TextUtils; @@ -107,12 +109,6 @@ * Fragment that provides chat in {@code GroupChannel} */ public class ChannelFragment extends BaseModuleFragment { - private static final int CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE = 2001; - private static final int PICK_IMAGE_PERMISSIONS_REQUEST_CODE = 2002; - private static final int PICK_FILE_PERMISSIONS_REQUEST_CODE = 2003; - private static final int PERMISSION_REQUEST_ALL = 2005; - private static final int PERMISSION_REQUEST_STORAGE = 2006; - @Nullable private View.OnClickListener headerLeftButtonClickListener; @Nullable @@ -175,6 +171,28 @@ public class ChannelFragment extends BaseModuleFragment getContentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null) return; + final Uri mediaUri = intent.getData(); + if (mediaUri != null && isFragmentAlive()) { + sendFileMessage(mediaUri); + } + }); + private final ActivityResultLauncher takeCameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK) return; + final Uri mediaUri = ChannelFragment.this.mediaUri; + if (mediaUri != null && isFragmentAlive()) { + sendFileMessage(mediaUri); + } + }); + @NonNull @Override protected ChannelModule onCreateModule(@NonNull Bundle args) { @@ -329,7 +347,7 @@ protected void onBindMessageListComponent(@NonNull MessageListComponent messageL messageListComponent.notifyOtherMessageReceived(anchorDialogShowing.get()); if (eventSource.equals(StringSet.EVENT_MESSAGE_SENT)) { final MessageListParams messageListParams = viewModel.getMessageListParams(); - final BaseMessage latestMessage = adapter.getItem(messageListParams.getReverse() ? 0 : adapter.getItemCount() - 1); + final BaseMessage latestMessage = adapter.getItem(messageListParams != null && messageListParams.getReverse() ? 0 : adapter.getItemCount() - 1); if (latestMessage instanceof FileMessage) { // Download from files already sent for quick image loading. FileDownloader.downloadThumbnail(context, (FileMessage) latestMessage); @@ -1049,28 +1067,13 @@ protected void showMediaSelectDialog() { */ public void takeCamera() { SendbirdChat.setAutoBackgroundDetection(false); - checkPermission(PERMISSION_REQUEST_ALL, new PermissionFragment.IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return new String[]{Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - return new String[]{Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { - if (getContext() == null) return; - mediaUri = FileUtils.createPictureImageUri(getContext()); - if (mediaUri == null) return; - Intent intent = IntentUtils.getCameraIntent(getContext(), mediaUri); - if (IntentUtils.hasIntent(getContext(), intent)) { - startActivityForResult(intent, CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE); - } + requestPermission(PermissionUtils.CAMERA_PERMISSION, () -> { + if (getContext() == null) return; + this.mediaUri = FileUtils.createPictureImageUri(getContext()); + if (mediaUri == null) return; + Intent intent = IntentUtils.getCameraIntent(getContext(), mediaUri); + if (IntentUtils.hasIntent(getContext(), intent)) { + takeCameraLauncher.launch(intent); } }); } @@ -1082,23 +1085,17 @@ public void onPermissionGranted(int requestCode) { */ public void takePhoto() { SendbirdChat.setAutoBackgroundDetection(false); - checkPermission(PERMISSION_REQUEST_STORAGE, new PermissionFragment.IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; - } - return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { + Logger.d("++ build sdk int=%s", Build.VERSION.SDK_INT); + final String[] permissions = PermissionUtils.GET_CONTENT_PERMISSION; + if (permissions.length > 0) { + requestPermission(permissions, () -> { Intent intent = IntentUtils.getGalleryIntent(); - startActivityForResult(intent, PICK_IMAGE_PERMISSIONS_REQUEST_CODE); - } - }); + getContentLauncher.launch(intent); + }); + } else { + Intent intent = IntentUtils.getGalleryIntent(); + getContentLauncher.launch(intent); + } } /** @@ -1108,46 +1105,15 @@ public void onPermissionGranted(int requestCode) { */ public void takeFile() { SendbirdChat.setAutoBackgroundDetection(false); - checkPermission(PERMISSION_REQUEST_STORAGE, new PermissionFragment.IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; - } - return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { + final String[] permissions = PermissionUtils.GET_CONTENT_PERMISSION; + if (permissions.length > 0) { + requestPermission(permissions, () -> { Intent intent = IntentUtils.getFileChooserIntent(); - startActivityForResult(intent, PICK_FILE_PERMISSIONS_REQUEST_CODE); - } - }); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - SendbirdChat.setAutoBackgroundDetection(true); - - if (resultCode != RESULT_OK) return; - - Uri mediaUri = this.mediaUri; - switch (requestCode) { - case CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE: - break; - case PICK_IMAGE_PERMISSIONS_REQUEST_CODE: - case PICK_FILE_PERMISSIONS_REQUEST_CODE: - if (data != null) { - mediaUri = data.getData(); - } - break; - } - - if (mediaUri != null && isFragmentAlive()) { - sendFileMessage(mediaUri); + getContentLauncher.launch(intent); + }); + } else { + Intent intent = IntentUtils.getFileChooserIntent(); + getContentLauncher.launch(intent); } } @@ -1309,19 +1275,7 @@ protected void saveFileMessage(@NonNull FileMessage message) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { download(message); } else { - checkPermission(PERMISSION_REQUEST_STORAGE, new PermissionFragment.IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { - download(message); - } - }); + requestPermission(PermissionUtils.GET_CONTENT_PERMISSION, () -> download(message)); } } @@ -2027,8 +1981,8 @@ public Builder setMentionUIConfig(@Nullable TextUIConfig configSentFromMe, @Null /** * Sets the UI configuration of edited text mark. * - * @param configSentFromMe the UI configuration of edited text mark in the message that was sent from me. - * @param configSentFromOthers the UI configuration of edited text mark in the message that was sent from others. + * @param configSentFromMe the UI configuration of edited text mark in the message that was sent from me. + * @param configSentFromOthers the UI configuration of edited text mark in the message that was sent from others. * @return This Builder object to allow for chaining of calls to set methods. * @since 3.0.0 */ diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelListFragment.java index 3779c4cd..60db02b9 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelListFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelListFragment.java @@ -40,8 +40,6 @@ import com.sendbird.uikit.widgets.SelectChannelTypeView; import com.sendbird.uikit.widgets.StatusFrameView; -import java.util.Objects; - /** * Fragment displaying the list of channels. */ diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelSettingsFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelSettingsFragment.java index 16525e2b..40e5668c 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelSettingsFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ChannelSettingsFragment.java @@ -2,14 +2,14 @@ import static android.app.Activity.RESULT_OK; -import android.Manifest; import android.content.Intent; import android.content.res.ColorStateList; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -44,6 +44,7 @@ import com.sendbird.uikit.utils.DialogUtils; import com.sendbird.uikit.utils.FileUtils; import com.sendbird.uikit.utils.IntentUtils; +import com.sendbird.uikit.utils.PermissionUtils; import com.sendbird.uikit.vm.ChannelSettingsViewModel; import com.sendbird.uikit.vm.ViewModelFactory; @@ -53,8 +54,6 @@ * Fragment displaying the information of {@code GroupChannel}. */ public class ChannelSettingsFragment extends BaseModuleFragment { - private static final int CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE = 2001; - private static final int PICK_IMAGE_PERMISSIONS_REQUEST_CODE = 2002; @Nullable private Uri mediaUri; @@ -67,6 +66,28 @@ public class ChannelSettingsFragment extends BaseModuleFragment getContentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null) return; + final Uri mediaUri = intent.getData(); + if (mediaUri != null && isFragmentAlive()) { + processPickedImage(mediaUri); + } + }); + private final ActivityResultLauncher takeCameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK) return; + final Uri mediaUri = ChannelSettingsFragment.this.mediaUri; + if (mediaUri != null && isFragmentAlive()) { + processPickedImage(mediaUri); + } + }); + @NonNull @Override protected ChannelSettingsModule onCreateModule(@NonNull Bundle args) { @@ -84,32 +105,13 @@ protected ChannelSettingsViewModel onCreateViewModel() { return new ViewModelProvider(this, new ViewModelFactory(getChannelUrl())).get(getChannelUrl(), ChannelSettingsViewModel.class); } - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - SendbirdChat.setAutoBackgroundDetection(true); - - if (resultCode != RESULT_OK) return; - - switch (requestCode) { - case CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE: - break; - case PICK_IMAGE_PERMISSIONS_REQUEST_CODE: - if (data != null) { - mediaUri = data.getData(); - } - break; - } - - if (mediaUri == null) return; - - final Uri finalMediaUri = mediaUri; + private void processPickedImage(@NonNull Uri uri) { TaskQueue.addTask(new JobResultTask() { @Override @Nullable public File call() { if (!isFragmentAlive()) return null; - return FileUtils.uriToFile(requireContext(), finalMediaUri); + return FileUtils.uriToFile(requireContext(), uri); } @Override @@ -267,24 +269,7 @@ private void showChannelInfoEditDialog() { getString(R.string.sb_text_button_cancel), null); } else if (key == R.string.sb_text_channel_settings_change_channel_image) { Logger.dev("change channel image"); - checkPermission(PICK_IMAGE_PERMISSIONS_REQUEST_CODE, new IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return new String[]{Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - return new String[]{Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { - showMediaSelectDialog(); - } - }); + requestPermission(PermissionUtils.CAMERA_PERMISSION, this::showMediaSelectDialog); } }); } @@ -379,7 +364,7 @@ private void showMediaSelectDialog() { if (key == R.string.sb_text_channel_settings_change_channel_image_camera) { takeCamera(); } else if (key == R.string.sb_text_channel_settings_change_channel_image_gallery) { - pickImage(); + takePhoto(); } } catch (Exception e) { Logger.e(e); @@ -394,13 +379,13 @@ private void takeCamera() { if (mediaUri == null) return; Intent intent = IntentUtils.getCameraIntent(requireActivity(), mediaUri); if (IntentUtils.hasIntent(requireContext(), intent)) { - startActivityForResult(intent, CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE); + takeCameraLauncher.launch(intent); } } - private void pickImage() { + private void takePhoto() { Intent intent = IntentUtils.getImageGalleryIntent(); - startActivityForResult(intent, PICK_IMAGE_PERMISSIONS_REQUEST_CODE); + getContentLauncher.launch(intent); } public static class Builder { diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/CreateChannelFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/CreateChannelFragment.java index 8b5af5d2..90385b16 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/CreateChannelFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/CreateChannelFragment.java @@ -92,6 +92,11 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull CreateChannel @Override protected void onReady(@NonNull ReadyStatus status, @NonNull CreateChannelModule module, @NonNull CreateChannelViewModel viewModel) { Logger.d(">> CreateChannelFragment::onReady()"); + if (status != ReadyStatus.READY) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } viewModel.loadInitial(); } diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/InviteUserFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/InviteUserFragment.java index 73e907c6..8847845e 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/InviteUserFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/InviteUserFragment.java @@ -92,7 +92,11 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull InviteUserMod protected void onReady(@NonNull ReadyStatus status, @NonNull InviteUserModule module, @NonNull InviteUserViewModel viewModel) { Logger.d(">> InviteUserFragment::onReady(ReadyStatus=%s)", status); final GroupChannel channel = viewModel.getChannel(); - if (channel == null) return; + if (status != ReadyStatus.READY || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } module.getInviteUserListComponent().notifyDisabledUserIds(getDisabledUserIds(channel)); viewModel.loadInitial(); diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/MemberListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/MemberListFragment.java index 71df68ab..a71ed15c 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/MemberListFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/MemberListFragment.java @@ -94,7 +94,12 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull MemberListMod @Override protected void onReady(@NonNull ReadyStatus status, @NonNull MemberListModule module, @NonNull MemberListViewModel viewModel) { Logger.d(">> MemberListFragment::onReady(ReadyStatus=%s)", status); - + final GroupChannel channel = viewModel.getChannel(); + if (status == ReadyStatus.ERROR || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } viewModel.getChannelDeleted().observe(getViewLifecycleOwner(), isDeleted -> { if (isDeleted) shouldActivityFinish(); }); diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/MessageSearchFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/MessageSearchFragment.java index 92b8d658..12af3553 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/MessageSearchFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/MessageSearchFragment.java @@ -89,6 +89,12 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull MessageSearch @Override protected void onReady(@NonNull ReadyStatus status, @NonNull MessageSearchModule module, @NonNull MessageSearchViewModel viewModel) { Logger.d(">> MessageSearchFragment::onReady(ReadyStatus=%s)", status); + final GroupChannel channel = viewModel.getChannel(); + if (status == ReadyStatus.ERROR || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } } /** diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/ModerationFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/ModerationFragment.java index 66339c71..12e361f8 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/ModerationFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ModerationFragment.java @@ -14,6 +14,7 @@ import com.sendbird.android.channel.GroupChannel; import com.sendbird.android.channel.Role; import com.sendbird.android.user.MemberState; +import com.sendbird.uikit.R; import com.sendbird.uikit.SendbirdUIKit; import com.sendbird.uikit.activities.BannedUserListActivity; import com.sendbird.uikit.activities.MutedMemberListActivity; @@ -73,13 +74,18 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull ModerationMod protected void onReady(@NonNull ReadyStatus status, @NonNull ModerationModule module, @NonNull ModerationViewModel viewModel) { Logger.d(">> ModerationFragment::onReady status=%s", status); - final ModerationListComponent moderationListComponent = getModule().getModerationListComponent(); - final GroupChannel channel = viewModel.getChannel(); - if (channel != null) { - moderationListComponent.notifyChannelChanged(channel); + if (status == ReadyStatus.ERROR || channel == null) { + if (isFragmentAlive()) { + toastError(R.string.sb_text_error_get_channel); + shouldActivityFinish(); + } + return; } + final ModerationListComponent moderationListComponent = getModule().getModerationListComponent(); + moderationListComponent.notifyChannelChanged(channel); + viewModel.getMyMemberStateChanges().observe(getViewLifecycleOwner(), memberState -> { if (memberState == MemberState.NONE) shouldActivityFinish(); }); diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/MutedMemberListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/MutedMemberListFragment.java index ae0722e7..444cd127 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/MutedMemberListFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/MutedMemberListFragment.java @@ -16,7 +16,6 @@ import com.sendbird.android.user.Member; import com.sendbird.uikit.R; import com.sendbird.uikit.SendbirdUIKit; -import com.sendbird.uikit.activities.RegisterOperatorActivity; import com.sendbird.uikit.activities.adapter.MutedMemberListAdapter; import com.sendbird.uikit.consts.StringSet; import com.sendbird.uikit.interfaces.LoadingDialogHandler; @@ -35,7 +34,7 @@ import com.sendbird.uikit.widgets.StatusFrameView; /** - * Fragment displaying the operators of the channel. + * Fragment displaying muted members of the channel. * * @since 1.2.0 */ @@ -97,7 +96,12 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull MutedMemberLi protected void onReady(@NonNull ReadyStatus status, @NonNull MutedMemberListModule module, @NonNull MutedMemberListViewModel viewModel) { Logger.d(">> MutedMemberListFragment::onReady(ReadyStatus=%s)", status); final GroupChannel channel = viewModel.getChannel(); - if (channel != null && channel.getMyRole() != Role.OPERATOR) shouldActivityFinish(); + if (status == ReadyStatus.ERROR || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } + if (channel.getMyRole() != Role.OPERATOR) shouldActivityFinish(); viewModel.getOperatorUnregistered().observe(getViewLifecycleOwner(), isDismissed -> { if (isDismissed) shouldActivityFinish(); }); @@ -119,11 +123,7 @@ protected void onBindHeaderComponent(@NonNull HeaderComponent headerComponent, @ Logger.d(">> MutedMemberListFragment::onBindHeaderComponent()"); headerComponent.setOnLeftButtonClickListener(headerLeftButtonClickListener != null ? headerLeftButtonClickListener : v -> shouldActivityFinish()); - headerComponent.setOnRightButtonClickListener(headerRightButtonClickListener != null ? headerRightButtonClickListener : v -> { - if (isFragmentAlive() && getContext() != null && channel != null) { - startActivity(RegisterOperatorActivity.newIntent(getContext(), channel.getUrl())); - } - }); + headerComponent.setOnRightButtonClickListener(headerRightButtonClickListener); } /** diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelBannedUserListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelBannedUserListFragment.java new file mode 100644 index 00000000..24b63a8a --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelBannedUserListFragment.java @@ -0,0 +1,591 @@ +package com.sendbird.uikit.fragments; + +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.lifecycle.ViewModelProvider; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.activities.adapter.OpenChannelBannedUserListAdapter; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.interfaces.LoadingDialogHandler; +import com.sendbird.uikit.interfaces.OnItemClickListener; +import com.sendbird.uikit.interfaces.OnItemLongClickListener; +import com.sendbird.uikit.log.Logger; +import com.sendbird.uikit.model.DialogListItem; +import com.sendbird.uikit.model.ReadyStatus; +import com.sendbird.uikit.modules.OpenChannelBannedUserListModule; +import com.sendbird.uikit.modules.components.HeaderComponent; +import com.sendbird.uikit.modules.components.OpenChannelBannedUserListComponent; +import com.sendbird.uikit.modules.components.StatusComponent; +import com.sendbird.uikit.utils.DialogUtils; +import com.sendbird.uikit.vm.OpenChannelBannedUserListViewModel; +import com.sendbird.uikit.vm.ViewModelFactory; +import com.sendbird.uikit.widgets.StatusFrameView; + +/** + * Fragment displaying the banned user list of the channel. + * + * @since 3.1.0 + */ +public class OpenChannelBannedUserListFragment extends BaseModuleFragment { + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OpenChannelBannedUserListAdapter adapter; + @Nullable + private OnItemClickListener itemClickListener; + @Nullable + private OnItemLongClickListener itemLongClickListener; + @Nullable + private OnItemClickListener actionItemClickListener; + @Nullable + private OnItemClickListener profileClickListener; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + @NonNull + @Override + protected OpenChannelBannedUserListModule onCreateModule(@NonNull Bundle args) { + return new OpenChannelBannedUserListModule(requireContext()); + } + + @Override + protected void onConfigureParams(@NonNull OpenChannelBannedUserListModule module, @NonNull Bundle args) { + if (loadingDialogHandler != null) { + module.setOnLoadingDialogHandler(loadingDialogHandler); + } + } + + @NonNull + @Override + protected OpenChannelBannedUserListViewModel onCreateViewModel() { + return new ViewModelProvider(this, new ViewModelFactory(getChannelUrl())).get(getChannelUrl(), OpenChannelBannedUserListViewModel.class); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getModule().getStatusComponent().notifyStatusChanged(StatusFrameView.Status.LOADING); + } + + @Override + protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull OpenChannelBannedUserListModule module, @NonNull OpenChannelBannedUserListViewModel viewModel) { + Logger.d(">> OpenChannelBannedUserListFragment::onBeforeReady status=%s", status); + module.getBannedUserListComponent().setPagedDataLoader(viewModel); + if (this.adapter != null) { + module.getBannedUserListComponent().setAdapter(adapter); + } + final OpenChannel channel = viewModel.getChannel(); + onBindHeaderComponent(module.getHeaderComponent(), viewModel, channel); + onBindBannedUserListComponent(module.getBannedUserListComponent(), viewModel, channel); + onBindStatusComponent(module.getStatusComponent(), viewModel, channel); + + viewModel.getOperatorUpdated().observe(getViewLifecycleOwner(), updatedChannel -> { + if (!updatedChannel.isOperator(SendbirdChat.getCurrentUser())) { + shouldActivityFinish(); + } + }); + viewModel.getChannelDeleted().observe(getViewLifecycleOwner(), isDeleted -> { + if (isDeleted) shouldActivityFinish(); + }); + } + + @Override + protected void onReady(@NonNull ReadyStatus status, @NonNull OpenChannelBannedUserListModule module, @NonNull OpenChannelBannedUserListViewModel viewModel) { + Logger.d(">> OpenChannelBannedUserListFragment::onReady status=%s", status); + final OpenChannel channel = viewModel.getChannel(); + if (status != ReadyStatus.READY || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } + + if (!channel.isOperator(SendbirdChat.getCurrentUser())) shouldActivityFinish(); + viewModel.loadInitial(); + + viewModel.getUserBanned().observe(getViewLifecycleOwner(), restrictedUser -> { + if (restrictedUser.getUserId().equals(SendbirdUIKit.getAdapter().getUserInfo().getUserId())) { + shouldActivityFinish(); + } else { + viewModel.loadInitial(); + } + }); + viewModel.getUserUnbanned().observe(getViewLifecycleOwner(), user -> viewModel.loadInitial()); + } + + /** + * Called to bind events to the HeaderComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelBannedUserListModule, OpenChannelBannedUserListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param headerComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindHeaderComponent(@NonNull HeaderComponent headerComponent, @NonNull OpenChannelBannedUserListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelBannedUserListFragment::onBindHeaderComponent()"); + headerComponent.setOnLeftButtonClickListener(headerLeftButtonClickListener != null ? headerLeftButtonClickListener : v -> shouldActivityFinish()); + headerComponent.setOnRightButtonClickListener(headerRightButtonClickListener); + } + + /** + * Called to bind events to the OpenChannelBannedUserListComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelBannedUserListModule, OpenChannelBannedUserListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param listComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindBannedUserListComponent(@NonNull OpenChannelBannedUserListComponent listComponent, @NonNull OpenChannelBannedUserListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelBannedUserListFragment::onBindOpenChannelBannedUserListComponent()"); + + listComponent.setOnItemClickListener(itemClickListener); + listComponent.setOnItemLongClickListener(itemLongClickListener); + listComponent.setOnActionItemClickListener(actionItemClickListener != null ? actionItemClickListener : this::onActionItemClicked); + listComponent.setOnProfileClickListener(profileClickListener != null ? profileClickListener : this::onProfileClicked); + + viewModel.getUserList().observe(getViewLifecycleOwner(), users -> { + Logger.dev("++ observing result participants size : %s", users.size()); + if (openChannel != null) { + listComponent.notifyDataSetChanged(users, openChannel); + } + }); + } + + /** + * Called to bind events to the StatusComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelBannedUserListModule, OpenChannelBannedUserListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param statusComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindStatusComponent(@NonNull StatusComponent statusComponent, @NonNull OpenChannelBannedUserListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelBannedUserListFragment::onBindStatusComponent()"); + statusComponent.setOnActionButtonClickListener(v -> { + statusComponent.notifyStatusChanged(StatusFrameView.Status.LOADING); + shouldAuthenticate(); + }); + + viewModel.getStatusFrame().observe(getViewLifecycleOwner(), statusComponent::notifyStatusChanged); + } + + /** + * Called when the user profile has been clicked. + * + * @param view The view that was clicked. + * @param position The position that was clicked. + * @param user The user data that was clicked. + * @since 3.1.0 + */ + protected void onProfileClicked(@NonNull View view, int position, @NonNull User user) { + if (getContext() == null) return; + DialogUtils.showUserProfileDialog(getContext(), user, false, null, getModule().getLoadingDialogHandler()); + } + + /** + * Called when the action has been clicked. + * + * @param view The view that was clicked. + * @param position The position that was clicked. + * @param user The user data that was clicked. + * @since 3.1.0 + */ + protected void onActionItemClicked(@NonNull View view, int position, @NonNull User user) { + if (getContext() == null) return; + DialogListItem[] items; + DialogListItem unbanMember = new DialogListItem(R.string.sb_text_unban_participant); + items = new DialogListItem[]{unbanMember}; + + DialogUtils.showListDialog(getContext(), user.getNickname(), + items, + (v, p, key) -> { + shouldShowLoadingDialog(); + getViewModel().unbanUser(user.getUserId(), e -> { + shouldDismissLoadingDialog(); + if (e != null) { + toastError(R.string.sb_text_error_unban_participant); + } + }); + } + ); + } + + /** + * It will be called when the loading dialog needs displaying. + * + * @return True if the callback has consumed the event, false otherwise. + * @since 3.1.0 + */ + protected boolean shouldShowLoadingDialog() { + if (getContext() != null) { + return getModule().shouldShowLoadingDialog(getContext()); + } + return false; + } + + /** + * It will be called when the loading dialog needs dismissing. + * + * @since 3.1.0 + */ + protected void shouldDismissLoadingDialog() { + getModule().shouldDismissLoadingDialog(); + } + + /** + * Returns the URL of the channel with the required data to use this fragment. + * + * @return The URL of a channel this fragment is currently associated with + * @since 3.1.0 + */ + @NonNull + protected String getChannelUrl() { + final Bundle args = getArguments() == null ? new Bundle() : getArguments(); + return args.getString(StringSet.KEY_CHANNEL_URL, ""); + } + + public static class Builder { + @NonNull + private final Bundle bundle; + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OpenChannelBannedUserListAdapter adapter; + @Nullable + private OnItemClickListener itemClickListener; + @Nullable + private OnItemLongClickListener itemLongClickListener; + @Nullable + private OnItemClickListener actionItemClickListener; + @Nullable + private OnItemClickListener profileClickListener; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl) { + this(channelUrl, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param themeMode {@link SendbirdUIKit.ThemeMode} + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @NonNull SendbirdUIKit.ThemeMode themeMode) { + this(channelUrl, themeMode.getResId()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param customThemeResId the resource identifier for custom theme. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @StyleRes int customThemeResId) { + bundle = new Bundle(); + bundle.putInt(StringSet.KEY_THEME_RES_ID, customThemeResId); + bundle.putString(StringSet.KEY_CHANNEL_URL, channelUrl); + } + + /** + * Sets arguments to this fragment. + * + * @param args the arguments supplied when the fragment was instantiated. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder withArguments(@NonNull Bundle args) { + this.bundle.putAll(args); + return this; + } + + /** + * Sets the title of the header. + * + * @param title text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderTitle(@NonNull String title) { + bundle.putString(StringSet.KEY_HEADER_TITLE, title); + return this; + } + + /** + * Sets whether the header is used. + * + * @param useHeader true if the header is used, false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeader(boolean useHeader) { + bundle.putBoolean(StringSet.KEY_USE_HEADER, useHeader); + return this; + } + + /** + * Sets whether the right button of the header is used. + * + * @param useHeaderRightButton true if the right button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderRightButton(boolean useHeaderRightButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_RIGHT_BUTTON, useHeaderRightButton); + return this; + } + + /** + * Sets whether the left button of the header is used. + * + * @param useHeaderLeftButton true if the left button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderLeftButton(boolean useHeaderLeftButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_LEFT_BUTTON, useHeaderLeftButton); + return this; + } + + /** + * Sets the icon on the left button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderLeftButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the icon on the right button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderRightButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_RIGHT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_RIGHT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the icon when the data is not exists. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setEmptyIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_EMPTY_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_EMPTY_ICON_TINT, tint); + return this; + } + + /** + * Sets the text when the data is not exists + * + * @param resId the resource identifier of text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setEmptyText(@StringRes int resId) { + bundle.putInt(StringSet.KEY_EMPTY_TEXT_RES_ID, resId); + return this; + } + + /** + * Sets the text when error occurs + * + * @param resId the resource identifier of text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setErrorText(@StringRes int resId) { + bundle.putInt(StringSet.KEY_ERROR_TEXT_RES_ID, resId); + return this; + } + + /** + * Sets the click listener on the left button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderLeftButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerLeftButtonClickListener = listener; + return this; + } + + /** + * Sets the click listener on the right button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderRightButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerRightButtonClickListener = listener; + return this; + } + + /** + * Sets the banned user list adapter. + * + * @param adapter the adapter for the banned user list. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setBannedUserListAdapter(T adapter) { + this.adapter = adapter; + return this; + } + + /** + * Sets the click listener on the item of banned user list. + * + * @param itemClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnItemClickListener(@NonNull OnItemClickListener itemClickListener) { + this.itemClickListener = itemClickListener; + return this; + } + + /** + * Sets the long click listener on the item of banned user list. + * + * @param itemLongClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnItemLongClickListener(@NonNull OnItemLongClickListener itemLongClickListener) { + this.itemLongClickListener = itemLongClickListener; + return this; + } + + /** + * Sets the action item click listener on the item of banned user list. + * + * @param actionItemClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnActionItemClickListener(@NonNull OnItemClickListener actionItemClickListener) { + this.actionItemClickListener = actionItemClickListener; + return this; + } + + /** + * Sets the click listener on the profile of message. + * + * @param profileClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnProfileClickListener(@NonNull OnItemClickListener profileClickListener) { + this.profileClickListener = profileClickListener; + return this; + } + + /** + * Sets whether the user profile uses. + * + * @param useUserProfile true if the user profile is shown when the profile image clicked, false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseUserProfile(boolean useUserProfile) { + bundle.putBoolean(StringSet.KEY_USE_USER_PROFILE, useUserProfile); + return this; + } + + /** + * Sets the custom loading dialog handler + * + * @param loadingDialogHandler Interface definition for a callback to be invoked before when the loading dialog is called. + * @return This Builder object to allow for chaining of calls to set methods. + * @see LoadingDialogHandler + * @since 3.1.0 + */ + @NonNull + public Builder setLoadingDialogHandler(@NonNull LoadingDialogHandler loadingDialogHandler) { + this.loadingDialogHandler = loadingDialogHandler; + return this; + } + + /** + * Creates an {@link OpenChannelBannedUserListFragment} with the arguments supplied to this + * builder. + * + * @return The {@link OpenChannelBannedUserListFragment} applied to the {@link Bundle}. + * @since 3.1.0 + */ + @NonNull + public OpenChannelBannedUserListFragment build() { + OpenChannelBannedUserListFragment fragment = new OpenChannelBannedUserListFragment(); + fragment.setArguments(bundle); + fragment.headerLeftButtonClickListener = headerLeftButtonClickListener; + fragment.headerRightButtonClickListener = headerRightButtonClickListener; + fragment.adapter = adapter; + fragment.itemClickListener = itemClickListener; + fragment.itemLongClickListener = itemLongClickListener; + fragment.actionItemClickListener = actionItemClickListener; + fragment.profileClickListener = profileClickListener; + fragment.loadingDialogHandler = loadingDialogHandler; + return fragment; + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelFragment.java index 019fbcc2..5b13e3d4 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelFragment.java @@ -2,7 +2,6 @@ import static android.app.Activity.RESULT_OK; -import android.Manifest; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; @@ -15,6 +14,8 @@ import android.view.View; import android.widget.EditText; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -71,6 +72,7 @@ import com.sendbird.uikit.utils.FileUtils; import com.sendbird.uikit.utils.IntentUtils; import com.sendbird.uikit.utils.MessageUtils; +import com.sendbird.uikit.utils.PermissionUtils; import com.sendbird.uikit.utils.SoftInputUtils; import com.sendbird.uikit.utils.TextUtils; import com.sendbird.uikit.vm.FileDownloader; @@ -89,12 +91,6 @@ * Fragment that provides chat in {@code OpenChannel} */ public class OpenChannelFragment extends BaseModuleFragment { - private static final int CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE = 2001; - private static final int PICK_IMAGE_PERMISSIONS_REQUEST_CODE = 2002; - private static final int PICK_FILE_PERMISSIONS_REQUEST_CODE = 2003; - private static final int PERMISSION_REQUEST_ALL = 2005; - private static final int PERMISSION_REQUEST_STORAGE = 2006; - @Nullable private OpenChannelMessageListAdapter adapter; @Nullable @@ -141,6 +137,28 @@ public class OpenChannelFragment extends BaseModuleFragment getContentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null) return; + final Uri mediaUri = intent.getData(); + if (mediaUri != null && isFragmentAlive()) { + sendFileMessage(mediaUri); + } + }); + private final ActivityResultLauncher takeCameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK) return; + final Uri mediaUri = OpenChannelFragment.this.mediaUri; + if (mediaUri != null && isFragmentAlive()) { + sendFileMessage(mediaUri); + } + }); + @NonNull @Override protected OpenChannelModule onCreateModule(@NonNull Bundle args) { @@ -259,10 +277,10 @@ protected void onBindChannelHeaderComponent(@NonNull OpenChannelHeaderComponent Logger.d(">> OpenChannelFragment::onBindChannelHeaderComponent()"); if (channel == null) return; - boolean isOperator = channel.isOperator(SendbirdChat.getCurrentUser()); headerComponent.setOnLeftButtonClickListener(headerLeftButtonClickListener != null ? headerLeftButtonClickListener : v -> shouldActivityFinish()); headerComponent.setOnRightButtonClickListener(headerRightButtonClickListener != null ? headerRightButtonClickListener : v -> { if (!isFragmentAlive()) return; + boolean isOperator = channel.isOperator(SendbirdChat.getCurrentUser()); if (isOperator) { Intent intent = OpenChannelSettingsActivity.newIntent(requireContext(), channel.getUrl()); startActivity(intent); @@ -363,6 +381,13 @@ protected void onBindMessageInputComponent(@NonNull OpenChannelMessageInputCompo inputComponent.requestInputMode(MessageInputView.Mode.DEFAULT); } }); + viewModel.getMyMutedInfo().observe(getViewLifecycleOwner(), isMuted -> { + if (viewModel.getChannel() == null) return; + inputComponent.notifyMyMutedStateChanged(viewModel.getChannel(), isMuted); + if (isMuted) { + inputComponent.requestInputMode(MessageInputView.Mode.DEFAULT); + } + }); } /** @@ -551,28 +576,14 @@ protected void showMediaSelectDialog() { */ public void takeCamera() { SendbirdChat.setAutoBackgroundDetection(false); - checkPermission(PERMISSION_REQUEST_ALL, new IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return new String[]{Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - return new String[]{Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { - if (!isFragmentAlive()) return; - mediaUri = FileUtils.createPictureImageUri(requireContext()); - if (mediaUri == null) return; - Intent intent = IntentUtils.getCameraIntent(requireContext(), mediaUri); - if (IntentUtils.hasIntent(requireContext(), intent)) { - startActivityForResult(intent, CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE); - } + String[] permissions = PermissionUtils.CAMERA_PERMISSION; + requestPermission(permissions, () -> { + if (getContext() == null) return; + this.mediaUri = FileUtils.createPictureImageUri(getContext()); + if (mediaUri == null) return; + Intent intent = IntentUtils.getCameraIntent(getContext(), mediaUri); + if (IntentUtils.hasIntent(getContext(), intent)) { + takeCameraLauncher.launch(intent); } }); } @@ -584,23 +595,17 @@ public void onPermissionGranted(int requestCode) { */ public void takePhoto() { SendbirdChat.setAutoBackgroundDetection(false); - checkPermission(PERMISSION_REQUEST_STORAGE, new IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; - } - return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { + Logger.d("++ build sdk int=%s", Build.VERSION.SDK_INT); + final String[] permissions = PermissionUtils.GET_CONTENT_PERMISSION; + if (permissions.length > 0) { + requestPermission(permissions, () -> { Intent intent = IntentUtils.getGalleryIntent(); - startActivityForResult(intent, PICK_IMAGE_PERMISSIONS_REQUEST_CODE); - } - }); + getContentLauncher.launch(intent); + }); + } else { + Intent intent = IntentUtils.getGalleryIntent(); + getContentLauncher.launch(intent); + } } /** @@ -610,45 +615,15 @@ public void onPermissionGranted(int requestCode) { */ public void takeFile() { SendbirdChat.setAutoBackgroundDetection(false); - checkPermission(PERMISSION_REQUEST_STORAGE, new IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; - } - return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { + final String[] permissions = PermissionUtils.GET_CONTENT_PERMISSION; + if (permissions.length > 0) { + requestPermission(permissions, () -> { Intent intent = IntentUtils.getFileChooserIntent(); - startActivityForResult(intent, PICK_FILE_PERMISSIONS_REQUEST_CODE); - } - }); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - SendbirdChat.setAutoBackgroundDetection(true); - - if (resultCode != RESULT_OK) return; - - switch (requestCode) { - case CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE: - break; - case PICK_IMAGE_PERMISSIONS_REQUEST_CODE: - case PICK_FILE_PERMISSIONS_REQUEST_CODE: - if (data != null) { - this.mediaUri = data.getData(); - } - break; - } - - if (this.mediaUri != null && isFragmentAlive()) { - sendFileMessage(mediaUri); + getContentLauncher.launch(intent); + }); + } else { + Intent intent = IntentUtils.getFileChooserIntent(); + getContentLauncher.launch(intent); } } @@ -966,19 +941,7 @@ protected void saveFileMessage(@NonNull FileMessage message) { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { download(message); } else { - checkPermission(PERMISSION_REQUEST_STORAGE, new PermissionFragment.IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - return new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { - download(message); - } - }); + requestPermission(PermissionUtils.GET_CONTENT_PERMISSION, () -> download(message)); } } diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelModerationFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelModerationFragment.java new file mode 100644 index 00000000..3b3ff91f --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelModerationFragment.java @@ -0,0 +1,387 @@ +package com.sendbird.uikit.fragments; + +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.lifecycle.ViewModelProvider; + +import com.sendbird.android.channel.BaseChannel; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.activities.OpenChannelBannedUserListActivity; +import com.sendbird.uikit.activities.OpenChannelMutedParticipantListActivity; +import com.sendbird.uikit.activities.OpenChannelOperatorListActivity; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.interfaces.LoadingDialogHandler; +import com.sendbird.uikit.interfaces.OnMenuItemClickListener; +import com.sendbird.uikit.log.Logger; +import com.sendbird.uikit.model.ReadyStatus; +import com.sendbird.uikit.modules.OpenChannelModerationModule; +import com.sendbird.uikit.modules.components.HeaderComponent; +import com.sendbird.uikit.modules.components.OpenChannelModerationListComponent; +import com.sendbird.uikit.vm.OpenChannelModerationViewModel; +import com.sendbird.uikit.vm.ViewModelFactory; + +/** + * Fragment displaying the menu list to control the open channel. + * It will be displayed if the participant is an operator. + * + * @since 3.1.0 + */ +public class OpenChannelModerationFragment extends BaseModuleFragment { + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OnMenuItemClickListener menuItemClickListener; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + @NonNull + @Override + protected OpenChannelModerationModule onCreateModule(@NonNull Bundle args) { + return new OpenChannelModerationModule(requireContext()); + } + + @Override + protected void onConfigureParams(@NonNull OpenChannelModerationModule module, @NonNull Bundle args) { + if (loadingDialogHandler != null) module.setOnLoadingDialogHandler(loadingDialogHandler); + } + + @NonNull + @Override + protected OpenChannelModerationViewModel onCreateViewModel() { + return new ViewModelProvider(this, new ViewModelFactory(getChannelUrl())).get(getChannelUrl(), OpenChannelModerationViewModel.class); + } + + @Override + protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull OpenChannelModerationModule module, @NonNull OpenChannelModerationViewModel viewModel) { + Logger.d(">> OpenChannelModerationFragment::onBeforeReady status=%s", status); + onBindHeaderComponent(module.getHeaderComponent(), viewModel, viewModel.getChannel()); + onBindModerationListComponent(module.getModerationListComponent(), viewModel, viewModel.getChannel()); + } + + @Override + protected void onReady(@NonNull ReadyStatus status, @NonNull OpenChannelModerationModule module, @NonNull OpenChannelModerationViewModel viewModel) { + Logger.d(">> OpenChannelModerationFragment::onReady status=%s", status); + final OpenChannel channel = viewModel.getChannel(); + if (status == ReadyStatus.ERROR || channel == null) { + if (isFragmentAlive()) { + toastError(R.string.sb_text_error_get_channel); + shouldActivityFinish(); + } + return; + } + + viewModel.getChannelDeleted().observe(getViewLifecycleOwner(), url -> shouldActivityFinish()); + viewModel.getCurrentUserBanned().observe(getViewLifecycleOwner(), isBanned -> { + if (isBanned) shouldActivityFinish(); + }); + viewModel.getCurrentUserRegisteredOperator().observe(getViewLifecycleOwner(), isOperator -> { + if (!isOperator) shouldActivityFinish(); + }); + } + + /** + * Called to bind events to the HeaderComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelModerationModule, OpenChannelModerationViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param headerComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindHeaderComponent(@NonNull HeaderComponent headerComponent, @NonNull OpenChannelModerationViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelModerationFragment::onBindHeaderComponent()"); + headerComponent.setOnLeftButtonClickListener(headerLeftButtonClickListener != null ? headerLeftButtonClickListener : v -> shouldActivityFinish()); + headerComponent.setOnRightButtonClickListener(headerRightButtonClickListener); + } + + /** + * Called to bind events to the ModerationListComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelModerationModule, OpenChannelModerationViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param moderationListComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindModerationListComponent(@NonNull OpenChannelModerationListComponent moderationListComponent, @NonNull OpenChannelModerationViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelModerationFragment::onBindModerationListComponent()"); + if (openChannel == null) return; + moderationListComponent.setOnMenuItemClickListener((view, menu, data) -> { + Logger.dev("++ %s item clicked", menu.name()); + if (menuItemClickListener != null) { + return menuItemClickListener.onMenuItemClicked(view, menu, openChannel); + } + if (getContext() == null) return false; + switch (menu) { + case OPERATORS: + startActivity(OpenChannelOperatorListActivity.newIntent(getContext(), openChannel.getUrl())); + break; + case MUTED_PARTICIPANTS: + startActivity(OpenChannelMutedParticipantListActivity.newIntent(getContext(), openChannel.getUrl())); + break; + case BANNED_PARTICIPANTS: + startActivity(OpenChannelBannedUserListActivity.newIntent(getContext(), openChannel.getUrl())); + break; + default: + return false; + } + return true; + }); + } + + /** + * It will be called when the loading dialog needs displaying. + * + * @return True if the callback has consumed the event, false otherwise. + * @since 3.1.0 + */ + protected boolean shouldShowLoadingDialog() { + if (getContext() != null) { + return getModule().shouldShowLoadingDialog(requireContext()); + } + return false; + } + + /** + * It will be called when the loading dialog needs dismissing. + * + * @since 3.1.0 + */ + protected void shouldDismissLoadingDialog() { + getModule().shouldDismissLoadingDialog(); + } + + /** + * Returns the URL of the channel with the required data to use this fragment. + * + * @return The URL of a channel this fragment is currently associated with + * @since 3.1.0 + */ + @NonNull + protected String getChannelUrl() { + final Bundle args = getArguments() == null ? new Bundle() : getArguments(); + return args.getString(StringSet.KEY_CHANNEL_URL, ""); + } + + public static class Builder { + @NonNull + private final Bundle bundle; + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OnMenuItemClickListener menuItemClickListener; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl) { + this(channelUrl, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param themeMode {@link SendbirdUIKit.ThemeMode} + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @NonNull SendbirdUIKit.ThemeMode themeMode) { + this(channelUrl, themeMode.getResId()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param customThemeResId the resource identifier for custom theme. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @StyleRes int customThemeResId) { + bundle = new Bundle(); + bundle.putInt(StringSet.KEY_THEME_RES_ID, customThemeResId); + bundle.putString(StringSet.KEY_CHANNEL_URL, channelUrl); + } + + /** + * Sets arguments to this fragment. + * + * @param args the arguments supplied when the fragment was instantiated. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder withArguments(@NonNull Bundle args) { + this.bundle.putAll(args); + return this; + } + + /** + * Sets the title of the header. + * + * @param title text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderTitle(@NonNull String title) { + bundle.putString(StringSet.KEY_HEADER_TITLE, title); + return this; + } + + /** + * Sets whether the header is used. + * + * @param useHeader true if the header is used, false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeader(boolean useHeader) { + bundle.putBoolean(StringSet.KEY_USE_HEADER, useHeader); + return this; + } + + /** + * Sets whether the left button of the header is used. + * + * @param useHeaderLeftButton true if the left button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderLeftButton(boolean useHeaderLeftButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_LEFT_BUTTON, useHeaderLeftButton); + return this; + } + + /** + * Sets the icon on the left button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderLeftButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the click listener on the left button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderLeftButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerLeftButtonClickListener = listener; + return this; + } + + /** + * Sets whether the right button of the header is used. + * + * @param useHeaderRightButton true if the right button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderRightButton(boolean useHeaderRightButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_RIGHT_BUTTON, useHeaderRightButton); + return this; + } + + /** + * Sets the icon on the right button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderRightButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_RIGHT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_RIGHT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the click listener on the right button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderRightButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerRightButtonClickListener = listener; + return this; + } + + /** + * Sets the click listener on the menu item is clicked. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnMenuItemClickListener(@NonNull OnMenuItemClickListener listener) { + this.menuItemClickListener = listener; + return this; + } + + /** + * Sets the custom loading dialog handler + * + * @param loadingDialogHandler Interface definition for a callback to be invoked before when the loading dialog is called. + * @see LoadingDialogHandler + * @since 3.1.0 + */ + @NonNull + public Builder setLoadingDialogHandler(@NonNull LoadingDialogHandler loadingDialogHandler) { + this.loadingDialogHandler = loadingDialogHandler; + return this; + } + + /** + * Creates an {@link OpenChannelModerationFragment} with the arguments supplied to this + * builder. + * + * @return The {@link OpenChannelModerationFragment} applied to the {@link Bundle}. + * @since 3.1.0 + */ + @NonNull + public OpenChannelModerationFragment build() { + OpenChannelModerationFragment fragment = new OpenChannelModerationFragment(); + fragment.setArguments(bundle); + fragment.headerLeftButtonClickListener = headerLeftButtonClickListener; + fragment.headerRightButtonClickListener = headerRightButtonClickListener; + fragment.menuItemClickListener = menuItemClickListener; + fragment.loadingDialogHandler = loadingDialogHandler; + return fragment; + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelMutedParticipantListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelMutedParticipantListFragment.java new file mode 100644 index 00000000..9059818b --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelMutedParticipantListFragment.java @@ -0,0 +1,591 @@ +package com.sendbird.uikit.fragments; + +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.lifecycle.ViewModelProvider; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.activities.adapter.OpenChannelMutedParticipantListAdapter; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.interfaces.LoadingDialogHandler; +import com.sendbird.uikit.interfaces.OnItemClickListener; +import com.sendbird.uikit.interfaces.OnItemLongClickListener; +import com.sendbird.uikit.log.Logger; +import com.sendbird.uikit.model.DialogListItem; +import com.sendbird.uikit.model.ReadyStatus; +import com.sendbird.uikit.modules.OpenChannelMutedParticipantListModule; +import com.sendbird.uikit.modules.components.HeaderComponent; +import com.sendbird.uikit.modules.components.OpenChannelMutedParticipantListComponent; +import com.sendbird.uikit.modules.components.StatusComponent; +import com.sendbird.uikit.utils.DialogUtils; +import com.sendbird.uikit.vm.OpenChannelMutedParticipantListViewModel; +import com.sendbird.uikit.vm.ViewModelFactory; +import com.sendbird.uikit.widgets.StatusFrameView; + +/** + * Fragment displaying muted participants of the channel. + * + * @since 3.1.0 + */ +public class OpenChannelMutedParticipantListFragment extends BaseModuleFragment { + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OpenChannelMutedParticipantListAdapter adapter; + @Nullable + private OnItemClickListener itemClickListener; + @Nullable + private OnItemLongClickListener itemLongClickListener; + @Nullable + private OnItemClickListener actionItemClickListener; + @Nullable + private OnItemClickListener profileClickListener; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + @NonNull + @Override + protected OpenChannelMutedParticipantListModule onCreateModule(@NonNull Bundle args) { + return new OpenChannelMutedParticipantListModule(requireContext()); + } + + @Override + protected void onConfigureParams(@NonNull OpenChannelMutedParticipantListModule module, @NonNull Bundle args) { + if (loadingDialogHandler != null) module.setOnLoadingDialogHandler(loadingDialogHandler); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getModule().getStatusComponent().notifyStatusChanged(StatusFrameView.Status.LOADING); + } + + @NonNull + @Override + protected OpenChannelMutedParticipantListViewModel onCreateViewModel() { + return new ViewModelProvider(this, new ViewModelFactory(getChannelUrl())).get(getChannelUrl(), OpenChannelMutedParticipantListViewModel.class); + } + + @Override + protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull OpenChannelMutedParticipantListModule module, @NonNull OpenChannelMutedParticipantListViewModel viewModel) { + Logger.d(">> OpenChannelMutedParticipantListFragment::onBeforeReady status=%s", status); + module.getMutedParticipantListComponent().setPagedDataLoader(viewModel); + if (this.adapter != null) { + module.getMutedParticipantListComponent().setAdapter(adapter); + } + final OpenChannel channel = viewModel.getChannel(); + onBindHeaderComponent(module.getHeaderComponent(), viewModel, channel); + onBindMutedParticipantListComponent(module.getMutedParticipantListComponent(), viewModel, channel); + onBindStatusComponent(module.getStatusComponent(), viewModel, channel); + } + + @Override + protected void onReady(@NonNull ReadyStatus status, @NonNull OpenChannelMutedParticipantListModule module, @NonNull OpenChannelMutedParticipantListViewModel viewModel) { + Logger.d(">> OpenChannelMutedParticipantListFragment::onReady status=%s", status); + final OpenChannel channel = viewModel.getChannel(); + if (status != ReadyStatus.READY || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } + + if (!channel.isOperator(SendbirdChat.getCurrentUser())) shouldActivityFinish(); + viewModel.loadInitial(); + + viewModel.getChannelDeleted().observe(getViewLifecycleOwner(), isDeleted -> { + if (isDeleted) shouldActivityFinish(); + }); + viewModel.getUserBanned().observe(getViewLifecycleOwner(), restrictedUser -> { + if (restrictedUser.getUserId().equals(SendbirdUIKit.getAdapter().getUserInfo().getUserId())) { + shouldActivityFinish(); + } + }); + viewModel.getOperatorUpdated().observe(getViewLifecycleOwner(), updatedChannel -> { + if (!updatedChannel.isOperator(SendbirdChat.getCurrentUser())) { + shouldActivityFinish(); + } else { + viewModel.loadInitial(); + } + }); + viewModel.getUserMuted().observe(getViewLifecycleOwner(), restrictedUser -> viewModel.loadInitial()); + viewModel.getUserUnmuted().observe(getViewLifecycleOwner(), user -> viewModel.loadInitial()); + } + + /** + * Called to bind events to the HeaderComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelMutedParticipantListModule, OpenChannelMutedParticipantListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param headerComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindHeaderComponent(@NonNull HeaderComponent headerComponent, @NonNull OpenChannelMutedParticipantListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelMutedParticipantListFragment::onBindHeaderComponent()"); + + headerComponent.setOnLeftButtonClickListener(headerLeftButtonClickListener != null ? headerLeftButtonClickListener : v -> shouldActivityFinish()); + headerComponent.setOnRightButtonClickListener(headerRightButtonClickListener); + } + + /** + * Called to bind events to the OpenChannelMutedParticipantListComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelMutedParticipantListModule, OpenChannelMutedParticipantListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param listComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindMutedParticipantListComponent(@NonNull OpenChannelMutedParticipantListComponent listComponent, @NonNull OpenChannelMutedParticipantListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelMutedParticipantListFragment::onBindOpenChannelMutedParticipantListComponent()"); + + listComponent.setOnItemClickListener(itemClickListener); + listComponent.setOnItemLongClickListener(itemLongClickListener); + listComponent.setOnActionItemClickListener(actionItemClickListener != null ? actionItemClickListener : this::onActionItemClicked); + listComponent.setOnProfileClickListener(profileClickListener != null ? profileClickListener : this::onProfileClicked); + + viewModel.getUserList().observe(getViewLifecycleOwner(), users -> { + Logger.dev("++ observing result participants size : %s", users.size()); + if (openChannel != null) { + listComponent.notifyDataSetChanged(users, openChannel); + } + }); + } + + /** + * Called to bind events to the StatusComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelMutedParticipantListModule, OpenChannelMutedParticipantListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param statusComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindStatusComponent(@NonNull StatusComponent statusComponent, @NonNull OpenChannelMutedParticipantListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelMutedParticipantListFragment::onBindStatusComponent()"); + + statusComponent.setOnActionButtonClickListener(v -> { + statusComponent.notifyStatusChanged(StatusFrameView.Status.LOADING); + shouldAuthenticate(); + }); + + viewModel.getStatusFrame().observe(getViewLifecycleOwner(), statusComponent::notifyStatusChanged); + } + + /** + * Called when the action has been clicked. + * + * @param view The view that was clicked. + * @param position The position that was clicked. + * @param user The participant data that was clicked. + * @since 3.1.0 + */ + protected void onActionItemClicked(@NonNull View view, int position, @NonNull User user) { + if (getContext() == null) return; + Logger.d(">> OpenChannelMutedParticipantListFragment::onActionItemClicked()"); + DialogListItem[] items; + DialogListItem unMute = new DialogListItem(R.string.sb_text_unmute_participant); + items = new DialogListItem[]{unMute}; + DialogUtils.showListDialog(getContext(), user.getNickname(), + items, + (v, p, key) -> { + shouldShowLoadingDialog(); + getViewModel().unmuteUser(user.getUserId(), e -> { + shouldDismissLoadingDialog(); + if (e != null) { + toastError(R.string.sb_text_error_unmute_participant); + } + }); + } + ); + } + + /** + * Called when the user profile has been clicked. + * + * @param view The view that was clicked. + * @param position The position that was clicked. + * @param user The user data that was clicked. + * @since 3.1.0 + */ + protected void onProfileClicked(@NonNull View view, int position, @NonNull User user) { + if (getContext() == null) return; + DialogUtils.showUserProfileDialog(getContext(), user, false, null, getModule().getLoadingDialogHandler()); + } + + /** + * It will be called when the loading dialog needs displaying. + * + * @return True if the callback has consumed the event, false otherwise. + * @since 3.1.0 + */ + protected boolean shouldShowLoadingDialog() { + if (getContext() != null) { + return getModule().shouldShowLoadingDialog(getContext()); + } + return false; + } + + /** + * It will be called when the loading dialog needs dismissing. + * + * @since 3.1.0 + */ + protected void shouldDismissLoadingDialog() { + getModule().shouldDismissLoadingDialog(); + } + + /** + * Returns the URL of the channel with the required data to use this fragment. + * + * @return The URL of a channel this fragment is currently associated with + * @since 3.1.0 + */ + @NonNull + protected String getChannelUrl() { + final Bundle args = getArguments() == null ? new Bundle() : getArguments(); + return args.getString(StringSet.KEY_CHANNEL_URL, ""); + } + + public static class Builder { + @NonNull + private final Bundle bundle; + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OpenChannelMutedParticipantListAdapter adapter; + @Nullable + private OnItemClickListener itemClickListener; + @Nullable + private OnItemLongClickListener itemLongClickListener; + @Nullable + private OnItemClickListener actionItemClickListener; + @Nullable + private OnItemClickListener profileClickListener; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl) { + this(channelUrl, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param themeMode {@link SendbirdUIKit.ThemeMode} + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @NonNull SendbirdUIKit.ThemeMode themeMode) { + this(channelUrl, themeMode.getResId()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param customThemeResId the resource identifier for custom theme. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @StyleRes int customThemeResId) { + bundle = new Bundle(); + bundle.putInt(StringSet.KEY_THEME_RES_ID, customThemeResId); + bundle.putString(StringSet.KEY_CHANNEL_URL, channelUrl); + } + + /** + * Sets arguments to this fragment. + * + * @param args the arguments supplied when the fragment was instantiated. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder withArguments(@NonNull Bundle args) { + this.bundle.putAll(args); + return this; + } + + /** + * Sets the title of the header. + * + * @param title text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderTitle(@NonNull String title) { + bundle.putString(StringSet.KEY_HEADER_TITLE, title); + return this; + } + + /** + * Sets whether the header is used. + * + * @param useHeader true if the header is used, false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeader(boolean useHeader) { + bundle.putBoolean(StringSet.KEY_USE_HEADER, useHeader); + return this; + } + + /** + * Sets whether the right button of the header is used. + * + * @param useHeaderRightButton true if the right button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderRightButton(boolean useHeaderRightButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_RIGHT_BUTTON, useHeaderRightButton); + return this; + } + + /** + * Sets whether the left button of the header is used. + * + * @param useHeaderLeftButton true if the left button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderLeftButton(boolean useHeaderLeftButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_LEFT_BUTTON, useHeaderLeftButton); + return this; + } + + /** + * Sets the icon on the left button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderLeftButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the icon on the right button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderRightButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_RIGHT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_RIGHT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the icon when the data is not exists. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setEmptyIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_EMPTY_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_EMPTY_ICON_TINT, tint); + return this; + } + + /** + * Sets the text when the data is not exists + * + * @param resId the resource identifier of text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setEmptyText(@StringRes int resId) { + bundle.putInt(StringSet.KEY_EMPTY_TEXT_RES_ID, resId); + return this; + } + + /** + * Sets the text when error occurs + * + * @param resId the resource identifier of text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setErrorText(@StringRes int resId) { + bundle.putInt(StringSet.KEY_ERROR_TEXT_RES_ID, resId); + return this; + } + + /** + * Sets the click listener on the left button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderLeftButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerLeftButtonClickListener = listener; + return this; + } + + /** + * Sets the click listener on the right button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderRightButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerRightButtonClickListener = listener; + return this; + } + + /** + * Sets the muted participant list adapter + * + * @param adapter the adapter for the channel's muted participant list. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setMutedOperatorListAdapter(T adapter) { + this.adapter = adapter; + return this; + } + + /** + * Sets the click listener on the item of channel user list. + * + * @param itemClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnItemClickListener(@NonNull OnItemClickListener itemClickListener) { + this.itemClickListener = itemClickListener; + return this; + } + + /** + * Sets the long click listener on the item of channel user list. + * + * @param itemLongClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnItemLongClickListener(@NonNull OnItemLongClickListener itemLongClickListener) { + this.itemLongClickListener = itemLongClickListener; + return this; + } + + /** + * Sets the action item click listener on the item of channel user list. + * + * @param actionItemClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnActionItemClickListener(@NonNull OnItemClickListener actionItemClickListener) { + this.actionItemClickListener = actionItemClickListener; + return this; + } + + /** + * Sets the click listener on the profile of message. + * + * @param profileClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnProfileClickListener(@NonNull OnItemClickListener profileClickListener) { + this.profileClickListener = profileClickListener; + return this; + } + + /** + * Sets whether the user profile uses. + * + * @param useUserProfile true if the user profile is shown when the profile image clicked, false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseUserProfile(boolean useUserProfile) { + bundle.putBoolean(StringSet.KEY_USE_USER_PROFILE, useUserProfile); + return this; + } + + /** + * Sets the custom loading dialog handler + * + * @param loadingDialogHandler Interface definition for a callback to be invoked before when the loading dialog is called. + * @return This Builder object to allow for chaining of calls to set methods. + * @see LoadingDialogHandler + * @since 3.1.0 + */ + @NonNull + public Builder setLoadingDialogHandler(@NonNull LoadingDialogHandler loadingDialogHandler) { + this.loadingDialogHandler = loadingDialogHandler; + return this; + } + + /** + * Creates an {@link OpenChannelMutedParticipantListFragment} with the arguments supplied to this + * builder. + * + * @return The {@link OpenChannelMutedParticipantListFragment} applied to the {@link Bundle}. + * @since 3.1.0 + */ + @NonNull + public OpenChannelMutedParticipantListFragment build() { + OpenChannelMutedParticipantListFragment fragment = new OpenChannelMutedParticipantListFragment(); + fragment.setArguments(bundle); + fragment.headerLeftButtonClickListener = headerLeftButtonClickListener; + fragment.headerRightButtonClickListener = headerRightButtonClickListener; + fragment.adapter = adapter; + fragment.itemClickListener = itemClickListener; + fragment.itemLongClickListener = itemLongClickListener; + fragment.actionItemClickListener = actionItemClickListener; + fragment.profileClickListener = profileClickListener; + fragment.loadingDialogHandler = loadingDialogHandler; + return fragment; + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelOperatorListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelOperatorListFragment.java new file mode 100644 index 00000000..21d4e4e4 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelOperatorListFragment.java @@ -0,0 +1,590 @@ +package com.sendbird.uikit.fragments; + +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.lifecycle.ViewModelProvider; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.activities.OpenChannelRegisterOperatorActivity; +import com.sendbird.uikit.activities.adapter.OpenChannelOperatorListAdapter; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.interfaces.LoadingDialogHandler; +import com.sendbird.uikit.interfaces.OnItemClickListener; +import com.sendbird.uikit.interfaces.OnItemLongClickListener; +import com.sendbird.uikit.log.Logger; +import com.sendbird.uikit.model.DialogListItem; +import com.sendbird.uikit.model.ReadyStatus; +import com.sendbird.uikit.modules.OpenChannelOperatorListModule; +import com.sendbird.uikit.modules.components.HeaderComponent; +import com.sendbird.uikit.modules.components.OpenChannelOperatorListComponent; +import com.sendbird.uikit.modules.components.StatusComponent; +import com.sendbird.uikit.utils.DialogUtils; +import com.sendbird.uikit.vm.OpenChannelOperatorListViewModel; +import com.sendbird.uikit.vm.ViewModelFactory; +import com.sendbird.uikit.widgets.StatusFrameView; + +/** + * Fragment displaying the operators of the open channel. + * + * @since 3.1.0 + */ +public class OpenChannelOperatorListFragment extends BaseModuleFragment { + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OpenChannelOperatorListAdapter adapter; + @Nullable + private OnItemClickListener itemClickListener; + @Nullable + private OnItemLongClickListener itemLongClickListener; + @Nullable + private OnItemClickListener actionItemClickListener; + @Nullable + private OnItemClickListener profileClickListener; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + @NonNull + @Override + protected OpenChannelOperatorListModule onCreateModule(@NonNull Bundle args) { + return new OpenChannelOperatorListModule(requireContext()); + } + + @Override + protected void onConfigureParams(@NonNull OpenChannelOperatorListModule module, @NonNull Bundle args) { + if (loadingDialogHandler != null) module.setOnLoadingDialogHandler(loadingDialogHandler); + } + + @NonNull + @Override + protected OpenChannelOperatorListViewModel onCreateViewModel() { + return new ViewModelProvider(this, new ViewModelFactory(getChannelUrl())).get(getChannelUrl(), OpenChannelOperatorListViewModel.class); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getModule().getStatusComponent().notifyStatusChanged(StatusFrameView.Status.LOADING); + } + + @Override + protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull OpenChannelOperatorListModule module, @NonNull OpenChannelOperatorListViewModel viewModel) { + Logger.d(">> OpenChannelOperatorListFragment::onBeforeReady status=%s", status); + module.getOperatorListComponent().setPagedDataLoader(viewModel); + if (this.adapter != null) { + module.getOperatorListComponent().setAdapter(adapter); + } + final OpenChannel channel = viewModel.getChannel(); + onBindHeaderComponent(module.getHeaderComponent(), viewModel, channel); + onBindOperatorListComponent(module.getOperatorListComponent(), viewModel, channel); + onBindStatusComponent(module.getStatusComponent(), viewModel, channel); + } + + @Override + protected void onReady(@NonNull ReadyStatus status, @NonNull OpenChannelOperatorListModule module, @NonNull OpenChannelOperatorListViewModel viewModel) { + Logger.d(">> OpenChannelOperatorListFragment::onReady status=%s", status); + final OpenChannel channel = viewModel.getChannel(); + if (status != ReadyStatus.READY || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } + + if (!channel.isOperator(SendbirdChat.getCurrentUser())) shouldActivityFinish(); + viewModel.loadInitial(); + + viewModel.getChannelDeleted().observe(getViewLifecycleOwner(), isDeleted -> { + if (isDeleted) shouldActivityFinish(); + }); + viewModel.getUserBanned().observe(getViewLifecycleOwner(), restrictedUser -> { + if (restrictedUser.getUserId().equals(SendbirdUIKit.getAdapter().getUserInfo().getUserId())) { + shouldActivityFinish(); + } + }); + viewModel.getOperatorUpdated().observe(getViewLifecycleOwner(), updatedChannel -> { + if (!updatedChannel.isOperator(SendbirdChat.getCurrentUser())) { + shouldActivityFinish(); + } else { + viewModel.loadInitial(); + } + }); + } + + /** + * Called to bind events to the HeaderComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelOperatorListModule, OpenChannelOperatorListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param headerComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindHeaderComponent(@NonNull HeaderComponent headerComponent, @NonNull OpenChannelOperatorListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelOperatorListFragment::onBindHeaderComponent()"); + headerComponent.setOnLeftButtonClickListener(headerLeftButtonClickListener != null ? headerLeftButtonClickListener : v -> shouldActivityFinish()); + headerComponent.setOnRightButtonClickListener(headerRightButtonClickListener != null ? headerRightButtonClickListener : v -> { + if (isFragmentAlive() && getContext() != null && openChannel != null) { + startActivity(OpenChannelRegisterOperatorActivity.newIntent(getContext(), openChannel.getUrl())); + } + }); + } + + /** + * Called to bind events to the OpenChannelOperatorListComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelOperatorListModule, OpenChannelOperatorListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param listComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindOperatorListComponent(@NonNull OpenChannelOperatorListComponent listComponent, @NonNull OpenChannelOperatorListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelOperatorListFragment::onBindOpenChannelOperatorListComponent()"); + + listComponent.setOnItemClickListener(itemClickListener); + listComponent.setOnItemLongClickListener(itemLongClickListener); + listComponent.setOnActionItemClickListener(actionItemClickListener != null ? actionItemClickListener : this::onActionItemClicked); + listComponent.setOnProfileClickListener(profileClickListener != null ? profileClickListener : this::onProfileClicked); + + viewModel.getUserList().observe(getViewLifecycleOwner(), users -> { + Logger.dev("++ observing result participants size : %s", users.size()); + if (openChannel != null) { + listComponent.notifyDataSetChanged(users, openChannel); + } + }); + } + + /** + * Called to bind events to the StatusComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelOperatorListModule, OpenChannelOperatorListViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param statusComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindStatusComponent(@NonNull StatusComponent statusComponent, @NonNull OpenChannelOperatorListViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelOperatorListFragment::onBindStatusComponent()"); + statusComponent.setOnActionButtonClickListener(v -> { + statusComponent.notifyStatusChanged(StatusFrameView.Status.LOADING); + shouldAuthenticate(); + }); + + viewModel.getStatusFrame().observe(getViewLifecycleOwner(), statusComponent::notifyStatusChanged); + } + + /** + * Called when the action has been clicked. + * + * @param view The view that was clicked. + * @param position The position that was clicked. + * @param user The member data that was clicked. + * @since 3.1.0 + */ + protected void onActionItemClicked(@NonNull View view, int position, @NonNull User user) { + if (getContext() == null) return; + Logger.d(">> OpenChannelOperatorListFragment::onActionItemClicked()"); + DialogListItem[] items; + DialogListItem removeOperator = new DialogListItem(R.string.sb_text_unregister_operator); + items = new DialogListItem[]{removeOperator}; + DialogUtils.showListDialog(getContext(), user.getNickname(), + items, + (v, p, key) -> { + shouldShowLoadingDialog(); + getViewModel().removeOperator(user.getUserId(), e -> { + shouldDismissLoadingDialog(); + if (e != null) { + toastError(R.string.sb_text_error_unregister_operator); + } + }); + } + ); + } + + /** + * Called when the user profile has been clicked. + * + * @param view The view that was clicked. + * @param position The position that was clicked. + * @param user The member data that was clicked. + * @since 3.1.0 + */ + protected void onProfileClicked(@NonNull View view, int position, @NonNull User user) { + if (getContext() == null) return; + DialogUtils.showUserProfileDialog(getContext(), user, false, null, getModule().getLoadingDialogHandler()); + } + + /** + * It will be called when the loading dialog needs displaying. + * + * @return True if the callback has consumed the event, false otherwise. + * @since 3.1.0 + */ + public boolean shouldShowLoadingDialog() { + if (!isFragmentAlive()) return false; + return getModule().shouldShowLoadingDialog(requireContext()); + } + + /** + * It will be called when the loading dialog needs dismissing. + * + * @since 3.1.0 + */ + public void shouldDismissLoadingDialog() { + getModule().shouldDismissLoadingDialog(); + } + + /** + * Returns the URL of the channel with the required data to use this fragment. + * + * @return The URL of a channel this fragment is currently associated with + * @since 3.1.0 + */ + @NonNull + protected String getChannelUrl() { + final Bundle args = getArguments() == null ? new Bundle() : getArguments(); + return args.getString(StringSet.KEY_CHANNEL_URL, ""); + } + + public static class Builder { + @NonNull + private final Bundle bundle; + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OpenChannelOperatorListAdapter adapter; + @Nullable + private OnItemClickListener itemClickListener; + @Nullable + private OnItemLongClickListener itemLongClickListener; + @Nullable + private OnItemClickListener actionItemClickListener; + @Nullable + private OnItemClickListener profileClickListener; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl) { + this(channelUrl, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param themeMode {@link SendbirdUIKit.ThemeMode} + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @NonNull SendbirdUIKit.ThemeMode themeMode) { + this(channelUrl, themeMode.getResId()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param customThemeResId the resource identifier for custom theme. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @StyleRes int customThemeResId) { + bundle = new Bundle(); + bundle.putInt(StringSet.KEY_THEME_RES_ID, customThemeResId); + bundle.putString(StringSet.KEY_CHANNEL_URL, channelUrl); + } + + /** + * Sets arguments to this fragment. + * + * @param args the arguments supplied when the fragment was instantiated. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder withArguments(@NonNull Bundle args) { + this.bundle.putAll(args); + return this; + } + + /** + * Sets the title of the header. + * + * @param title text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderTitle(@NonNull String title) { + bundle.putString(StringSet.KEY_HEADER_TITLE, title); + return this; + } + + /** + * Sets whether the header is used. + * + * @param useHeader true if the header is used, false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeader(boolean useHeader) { + bundle.putBoolean(StringSet.KEY_USE_HEADER, useHeader); + return this; + } + + /** + * Sets whether the right button of the header is used. + * + * @param useHeaderRightButton true if the right button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderRightButton(boolean useHeaderRightButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_RIGHT_BUTTON, useHeaderRightButton); + return this; + } + + /** + * Sets whether the left button of the header is used. + * + * @param useHeaderLeftButton true if the left button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderLeftButton(boolean useHeaderLeftButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_LEFT_BUTTON, useHeaderLeftButton); + return this; + } + + /** + * Sets the icon on the left button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderLeftButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the icon on the right button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderRightButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_RIGHT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_RIGHT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the icon when the data is not exists. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setEmptyIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_EMPTY_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_EMPTY_ICON_TINT, tint); + return this; + } + + /** + * Sets the text when the data is not exists + * + * @param resId the resource identifier of text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setEmptyText(@StringRes int resId) { + bundle.putInt(StringSet.KEY_EMPTY_TEXT_RES_ID, resId); + return this; + } + + /** + * Sets the text when error occurs + * + * @param resId the resource identifier of text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setErrorText(@StringRes int resId) { + bundle.putInt(StringSet.KEY_ERROR_TEXT_RES_ID, resId); + return this; + } + + /** + * Sets the click listener on the left button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderLeftButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerLeftButtonClickListener = listener; + return this; + } + + /** + * Sets the click listener on the right button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderRightButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerRightButtonClickListener = listener; + return this; + } + + /** + * Sets the channel user list adapter. + * + * @param adapter the adapter for the channel user list. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOperatorListAdapter(T adapter) { + this.adapter = adapter; + return this; + } + + /** + * Sets the click listener on the item of channel user list. + * + * @param itemClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnItemClickListener(@NonNull OnItemClickListener itemClickListener) { + this.itemClickListener = itemClickListener; + return this; + } + + /** + * Sets the long click listener on the item of channel user list. + * + * @param itemLongClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnItemLongClickListener(@NonNull OnItemLongClickListener itemLongClickListener) { + this.itemLongClickListener = itemLongClickListener; + return this; + } + + /** + * Sets the action item click listener on the item of channel user list. + * + * @param actionItemClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnActionItemClickListener(@NonNull OnItemClickListener actionItemClickListener) { + this.actionItemClickListener = actionItemClickListener; + return this; + } + + /** + * Sets the click listener on the profile of message. + * + * @param profileClickListener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnProfileClickListener(@NonNull OnItemClickListener profileClickListener) { + this.profileClickListener = profileClickListener; + return this; + } + + /** + * Sets whether the user profile uses. + * + * @param useUserProfile true if the user profile is shown when the profile image clicked, false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseUserProfile(boolean useUserProfile) { + bundle.putBoolean(StringSet.KEY_USE_USER_PROFILE, useUserProfile); + return this; + } + + /** + * Sets the custom loading dialog handler + * + * @param loadingDialogHandler Interface definition for a callback to be invoked before when the loading dialog is called. + * @return This Builder object to allow for chaining of calls to set methods. + * @see LoadingDialogHandler + * @since 3.1.0 + */ + @NonNull + public Builder setLoadingDialogHandler(@NonNull LoadingDialogHandler loadingDialogHandler) { + this.loadingDialogHandler = loadingDialogHandler; + return this; + } + + /** + * Creates an {@link OpenChannelOperatorListFragment} with the arguments supplied to this + * builder. + * + * @return The {@link OpenChannelOperatorListFragment} applied to the {@link Bundle}. + * @since 3.1.0 + */ + @NonNull + public OpenChannelOperatorListFragment build() { + OpenChannelOperatorListFragment fragment = new OpenChannelOperatorListFragment(); + fragment.setArguments(bundle); + fragment.headerLeftButtonClickListener = headerLeftButtonClickListener; + fragment.headerRightButtonClickListener = headerRightButtonClickListener; + fragment.adapter = adapter; + fragment.itemClickListener = itemClickListener; + fragment.itemLongClickListener = itemLongClickListener; + fragment.actionItemClickListener = actionItemClickListener; + fragment.profileClickListener = profileClickListener; + fragment.loadingDialogHandler = loadingDialogHandler; + return fragment; + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelRegisterOperatorFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelRegisterOperatorFragment.java new file mode 100644 index 00000000..fa9d909f --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelRegisterOperatorFragment.java @@ -0,0 +1,474 @@ +package com.sendbird.uikit.fragments; + +import static android.app.Activity.RESULT_OK; + +import android.content.Intent; +import android.content.res.ColorStateList; +import android.os.Bundle; +import android.view.View; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.lifecycle.ViewModelProvider; + +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.activities.OpenChannelOperatorListActivity; +import com.sendbird.uikit.activities.adapter.OpenChannelRegisterOperatorListAdapter; +import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.interfaces.OnUserSelectChangedListener; +import com.sendbird.uikit.interfaces.OnUserSelectionCompleteListener; +import com.sendbird.uikit.interfaces.PagedQueryHandler; +import com.sendbird.uikit.log.Logger; +import com.sendbird.uikit.model.ReadyStatus; +import com.sendbird.uikit.modules.OpenChannelRegisterOperatorModule; +import com.sendbird.uikit.modules.components.OpenChannelRegisterOperatorListComponent; +import com.sendbird.uikit.modules.components.SelectUserHeaderComponent; +import com.sendbird.uikit.modules.components.StatusComponent; +import com.sendbird.uikit.vm.OpenChannelRegisterOperatorViewModel; +import com.sendbird.uikit.vm.ViewModelFactory; +import com.sendbird.uikit.widgets.StatusFrameView; + +import java.util.List; + +/** + * Fragment displaying the participant list to be operator. + * + * @since 3.1.0 + */ +public class OpenChannelRegisterOperatorFragment extends BaseModuleFragment { + @Nullable + private PagedQueryHandler pagedQueryHandler; + @Nullable + private OpenChannelRegisterOperatorListAdapter adapter; + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OnUserSelectChangedListener userSelectChangedListener; + @Nullable + private OnUserSelectionCompleteListener userSelectionCompleteListener; + + @NonNull + @Override + protected OpenChannelRegisterOperatorModule onCreateModule(@NonNull Bundle args) { + return new OpenChannelRegisterOperatorModule(requireContext()); + } + + @Override + protected void onConfigureParams(@NonNull OpenChannelRegisterOperatorModule module, @NonNull Bundle args) { + } + + @NonNull + @Override + protected OpenChannelRegisterOperatorViewModel onCreateViewModel() { + return new ViewModelProvider(this, new ViewModelFactory(getChannelUrl(), pagedQueryHandler)).get(getChannelUrl(), OpenChannelRegisterOperatorViewModel.class); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + getModule().getStatusComponent().notifyStatusChanged(StatusFrameView.Status.LOADING); + } + + @Override + protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull OpenChannelRegisterOperatorModule module, @NonNull OpenChannelRegisterOperatorViewModel viewModel) { + Logger.d(">> OpenChannelRegisterOperatorFragment::onBeforeReady status=%s", status); + module.getRegisterOperatorListComponent().setPagedDataLoader(viewModel); + if (this.adapter != null) { + module.getRegisterOperatorListComponent().setAdapter(adapter); + } + final OpenChannel channel = viewModel.getChannel(); + onBindHeaderComponent(module.getHeaderComponent(), viewModel, channel); + onBindRegisterOperatorListComponent(module.getRegisterOperatorListComponent(), viewModel, channel); + onBindStatusComponent(module.getStatusComponent(), viewModel, channel); + } + + @Override + protected void onReady(@NonNull ReadyStatus status, @NonNull OpenChannelRegisterOperatorModule module, @NonNull OpenChannelRegisterOperatorViewModel viewModel) { + Logger.d(">> OpenChannelRegisterOperatorFragment::onReady status=%s", status); + final OpenChannel channel = viewModel.getChannel(); + if (status != ReadyStatus.READY || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } + viewModel.getChannelDeleted().observe(getViewLifecycleOwner(), isDeleted -> { + if (isDeleted) shouldActivityFinish(); + }); + + viewModel.loadInitial(); + } + + /** + * Called to bind events to the HeaderComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelRegisterOperatorModule, OpenChannelRegisterOperatorViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param headerComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindHeaderComponent(@NonNull SelectUserHeaderComponent headerComponent, @NonNull OpenChannelRegisterOperatorViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelRegisterOperatorFragment::onBindHeaderComponent()"); + headerComponent.setOnLeftButtonClickListener(headerLeftButtonClickListener != null ? headerLeftButtonClickListener : v -> shouldActivityFinish()); + headerComponent.setOnRightButtonClickListener(headerRightButtonClickListener != null ? headerRightButtonClickListener : v -> getModule().getRegisterOperatorListComponent().notifySelectionComplete()); + } + + /** + * Called to bind events to the OpenChannelRegisterOperatorListComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelRegisterOperatorModule, OpenChannelRegisterOperatorViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param listComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindRegisterOperatorListComponent(@NonNull OpenChannelRegisterOperatorListComponent listComponent, @NonNull OpenChannelRegisterOperatorViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelRegisterOperatorFragment::onBindRegisterOperatorListComponent()"); + if (openChannel != null) { + listComponent.setAdapter(new OpenChannelRegisterOperatorListAdapter(openChannel)); + } + listComponent.setOnUserSelectChangedListener(userSelectChangedListener != null ? userSelectChangedListener : (selectedUserIds, isSelected) -> getModule().getHeaderComponent().notifySelectedUserChanged(selectedUserIds.size())); + listComponent.setOnUserSelectionCompleteListener(userSelectionCompleteListener != null ? userSelectionCompleteListener : OpenChannelRegisterOperatorFragment.this::onUserSelectionCompleted); + viewModel.getUserList().observe(getViewLifecycleOwner(), listComponent::notifyDataSetChanged); + } + + /** + * Called to bind events to the StatusComponent. This is called from {@link #onBeforeReady(ReadyStatus, OpenChannelRegisterOperatorModule, OpenChannelRegisterOperatorViewModel)} regardless of the value of {@link ReadyStatus}. + * + * @param statusComponent The component to which the event will be bound + * @param viewModel A view model that provides the data needed for the fragment + * @param openChannel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + protected void onBindStatusComponent(@NonNull StatusComponent statusComponent, @NonNull OpenChannelRegisterOperatorViewModel viewModel, @Nullable OpenChannel openChannel) { + Logger.d(">> OpenChannelRegisterOperatorFragment::onBindStatusComponent()"); + statusComponent.setOnActionButtonClickListener(v -> { + statusComponent.notifyStatusChanged(StatusFrameView.Status.LOADING); + shouldAuthenticate(); + }); + + viewModel.getStatusFrame().observe(getViewLifecycleOwner(), statusComponent::notifyStatusChanged); + } + + /** + * Called when the user selection completed. + * + * @param selectedUsers selected user's ids. + * @since 3.1.0 + */ + protected void onUserSelectionCompleted(@NonNull List selectedUsers) { + Logger.d(">> RegisterOperators::onUserSelectComplete()"); + getViewModel().addOperators(selectedUsers, e -> { + if (e != null) { + toastError(R.string.sb_text_error_register_operator); + Logger.e(e); + return; + } + if (getActivity() != null) { + Intent intent = new Intent(getActivity(), OpenChannelOperatorListActivity.class); + getActivity().setResult(RESULT_OK, intent); + } + shouldActivityFinish(); + }); + } + + /** + * Returns the URL of the channel with the required data to use this fragment. + * + * @return The URL of a channel this fragment is currently associated with + * @since 3.1.0 + */ + @NonNull + protected String getChannelUrl() { + final Bundle args = getArguments() == null ? new Bundle() : getArguments(); + return args.getString(StringSet.KEY_CHANNEL_URL, ""); + } + + public static class Builder { + @NonNull + private final Bundle bundle; + @Nullable + private PagedQueryHandler pagedQueryHandler; + @Nullable + private OpenChannelRegisterOperatorListAdapter adapter; + @Nullable + private View.OnClickListener headerLeftButtonClickListener; + @Nullable + private View.OnClickListener headerRightButtonClickListener; + @Nullable + private OnUserSelectChangedListener userSelectChangedListener; + @Nullable + private OnUserSelectionCompleteListener userSelectionCompleteListener; + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl) { + this(channelUrl, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param themeMode {@link SendbirdUIKit.ThemeMode} + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @NonNull SendbirdUIKit.ThemeMode themeMode) { + this(channelUrl, themeMode.getResId()); + } + + /** + * Constructor + * + * @param channelUrl the url of the channel will be implemented. + * @param customThemeResId the resource identifier for custom theme. + * @since 3.1.0 + */ + public Builder(@NonNull String channelUrl, @StyleRes int customThemeResId) { + bundle = new Bundle(); + bundle.putInt(StringSet.KEY_THEME_RES_ID, customThemeResId); + bundle.putString(StringSet.KEY_CHANNEL_URL, channelUrl); + } + + /** + * Sets arguments to this fragment. + * + * @param args the arguments supplied when the fragment was instantiated. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder withArguments(@NonNull Bundle args) { + this.bundle.putAll(args); + return this; + } + + /** + * Sets the title of the header. + * + * @param title text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderTitle(@NonNull String title) { + bundle.putString(StringSet.KEY_HEADER_TITLE, title); + return this; + } + + /** + * Sets whether the header is used. + * + * @param useHeader true if the header is used, false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeader(boolean useHeader) { + bundle.putBoolean(StringSet.KEY_USE_HEADER, useHeader); + return this; + } + + /** + * Sets whether the right button of the header is used. + * + * @param useHeaderRightButton true if the right button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setUseHeaderRightButton(boolean useHeaderRightButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_RIGHT_BUTTON, useHeaderRightButton); + return this; + } + + /** + * Sets whether the left button of the header is used. + * + * @param useHeaderLeftButton true if the left button of the header is used, + * false otherwise. + * @return This Builder object to allow for chaining of calls to set methods. + */ + @NonNull + public Builder setUseHeaderLeftButton(boolean useHeaderLeftButton) { + bundle.putBoolean(StringSet.KEY_USE_HEADER_LEFT_BUTTON, useHeaderLeftButton); + return this; + } + + /** + * Sets the icon on the left button of the header. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setHeaderLeftButtonIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_HEADER_LEFT_BUTTON_ICON_TINT, tint); + return this; + } + + /** + * Sets the right button text of the header. + * + * @param rightButtonText text to be displayed to the right button. + * @return This Builder object to allow for chaining of calls to set methods. + */ + @NonNull + public Builder setRightButtonText(@NonNull String rightButtonText) { + bundle.putString(StringSet.KEY_HEADER_RIGHT_BUTTON_TEXT, rightButtonText); + return this; + } + + /** + * Sets the handler that loads the list of user. + * + * @param handler The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setCustomPagedQueryHandler(@NonNull PagedQueryHandler handler) { + this.pagedQueryHandler = handler; + return this; + } + + /** + * Sets the user list adapter. + * + * @param adapter the adapter for the user list. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setRegisterOperatorListAdapter(@NonNull OpenChannelRegisterOperatorListAdapter adapter) { + this.adapter = adapter; + return this; + } + + /** + * Sets the click listener on the left button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderLeftButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerLeftButtonClickListener = listener; + return this; + } + + /** + * Sets the click listener on the right button of the header. + * + * @param listener The callback that will run. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnHeaderRightButtonClickListener(@NonNull View.OnClickListener listener) { + this.headerRightButtonClickListener = listener; + return this; + } + + /** + * Sets the icon when the data is not exists. + * + * @param resId the resource identifier of the drawable. + * @param tint Color state list to use for tinting this resource, or null to clear the tint. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setEmptyIcon(@DrawableRes int resId, @Nullable ColorStateList tint) { + bundle.putInt(StringSet.KEY_EMPTY_ICON_RES_ID, resId); + bundle.putParcelable(StringSet.KEY_EMPTY_ICON_TINT, tint); + return this; + } + + /** + * Sets the text when the data is not exists + * + * @param resId the resource identifier of text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setEmptyText(@StringRes int resId) { + bundle.putInt(StringSet.KEY_EMPTY_TEXT_RES_ID, resId); + return this; + } + + /** + * Sets the text when error occurs + * + * @param resId the resource identifier of text to be displayed. + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setErrorText(@StringRes int resId) { + bundle.putInt(StringSet.KEY_ERROR_TEXT_RES_ID, resId); + return this; + } + + /** + * Register a callback to be invoked when the user is selected. + * + * @param userSelectChangedListener The callback that will run + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnUserSelectChangedListener(@Nullable OnUserSelectChangedListener userSelectChangedListener) { + this.userSelectChangedListener = userSelectChangedListener; + return this; + } + + /** + * Register a callback to be invoked when selecting users is completed. + * + * @param userSelectionCompleteListener The callback that will run + * @return This Builder object to allow for chaining of calls to set methods. + * @since 3.1.0 + */ + @NonNull + public Builder setOnUserSelectionCompleteListener(@Nullable OnUserSelectionCompleteListener userSelectionCompleteListener) { + this.userSelectionCompleteListener = userSelectionCompleteListener; + return this; + } + + /** + * Creates an {@link OpenChannelRegisterOperatorFragment} with the arguments supplied to this + * builder. + * + * @return The {@link OpenChannelRegisterOperatorFragment} applied to the {@link Bundle}. + * @since 3.1.0 + */ + @NonNull + public OpenChannelRegisterOperatorFragment build() { + OpenChannelRegisterOperatorFragment fragment = new OpenChannelRegisterOperatorFragment(); + fragment.setArguments(bundle); + fragment.pagedQueryHandler = pagedQueryHandler; + fragment.adapter = adapter; + fragment.headerLeftButtonClickListener = headerLeftButtonClickListener; + fragment.headerRightButtonClickListener = headerRightButtonClickListener; + fragment.userSelectChangedListener = userSelectChangedListener; + fragment.userSelectionCompleteListener = userSelectionCompleteListener; + return fragment; + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelSettingsFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelSettingsFragment.java index f1726edb..bd6e13e1 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelSettingsFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/OpenChannelSettingsFragment.java @@ -2,14 +2,14 @@ import static android.app.Activity.RESULT_OK; -import android.Manifest; import android.content.Intent; import android.content.res.ColorStateList; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.view.View; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -22,6 +22,7 @@ import com.sendbird.android.params.OpenChannelUpdateParams; import com.sendbird.uikit.R; import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.activities.OpenChannelModerationActivity; import com.sendbird.uikit.activities.ParticipantListActivity; import com.sendbird.uikit.consts.DialogEditTextParams; import com.sendbird.uikit.consts.StringSet; @@ -41,6 +42,7 @@ import com.sendbird.uikit.utils.DialogUtils; import com.sendbird.uikit.utils.FileUtils; import com.sendbird.uikit.utils.IntentUtils; +import com.sendbird.uikit.utils.PermissionUtils; import com.sendbird.uikit.vm.OpenChannelSettingsViewModel; import com.sendbird.uikit.vm.ViewModelFactory; @@ -50,9 +52,6 @@ * Fragment displaying the information of {@code OpenChannel}. */ public class OpenChannelSettingsFragment extends BaseModuleFragment { - private static final int CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE = 2001; - private static final int PICK_IMAGE_PERMISSIONS_REQUEST_CODE = 2002; - @Nullable private Uri mediaUri; @@ -64,6 +63,29 @@ public class OpenChannelSettingsFragment extends BaseModuleFragment menuItemClickListener; private LoadingDialogHandler loadingDialogHandler; + private final ActivityResultLauncher getContentLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null) return; + final Uri mediaUri = intent.getData(); + if (mediaUri != null && isFragmentAlive()) { + processPickedImage(mediaUri); + } + }); + private final ActivityResultLauncher takeCameraLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), result -> { + SendbirdChat.setAutoBackgroundDetection(true); + final Intent intent = result.getData(); + int resultCode = result.getResultCode(); + + if (resultCode != RESULT_OK || intent == null) return; + final Uri mediaUri = OpenChannelSettingsFragment.this.mediaUri; + if (mediaUri != null && isFragmentAlive()) { + processPickedImage(mediaUri); + } + }); + @NonNull @Override protected OpenChannelSettingsModule onCreateModule(@NonNull Bundle args) { @@ -81,32 +103,13 @@ protected OpenChannelSettingsViewModel onCreateViewModel() { return new ViewModelProvider(this, new ViewModelFactory(getChannelUrl())).get(getChannelUrl(), OpenChannelSettingsViewModel.class); } - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - super.onActivityResult(requestCode, resultCode, data); - SendbirdChat.setAutoBackgroundDetection(true); - - if (resultCode != RESULT_OK) return; - - switch (requestCode) { - case CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE: - break; - case PICK_IMAGE_PERMISSIONS_REQUEST_CODE: - if (data != null) { - mediaUri = data.getData(); - } - break; - } - - if (mediaUri == null) return; - - final Uri finalMediaUri = mediaUri; + private void processPickedImage(@NonNull Uri uri) { TaskQueue.addTask(new JobResultTask() { @Override @Nullable public File call() { if (!isFragmentAlive()) return null; - return FileUtils.uriToFile(requireContext(), finalMediaUri); + return FileUtils.uriToFile(requireContext(), uri); } @Override @@ -195,15 +198,23 @@ protected void onBindSettingsMenuComponent(@NonNull OpenChannelSettingsMenuCompo Logger.d(">> OpenChannelSettingsFragment::onBindSettingsMenuComponent()"); menuComponent.setOnMenuClickListener(menuItemClickListener != null ? menuItemClickListener : (view, position, menu) -> { - if (menu == OpenChannelSettingsMenuComponent.Menu.PARTICIPANTS) { + if (menu == OpenChannelSettingsMenuComponent.Menu.MODERATIONS) { + startModerationsActivity(); + } else if (menu == OpenChannelSettingsMenuComponent.Menu.PARTICIPANTS) { startParticipantsListActivity(); } else if (menu == OpenChannelSettingsMenuComponent.Menu.DELETE_CHANNEL) { - deleteChannel(); + showDeleteChannelDialog(); } }); viewModel.getChannelUpdated().observe(getViewLifecycleOwner(), menuComponent::notifyChannelChanged); } + private void startModerationsActivity() { + if (isFragmentAlive()) { + startActivity(OpenChannelModerationActivity.newIntent(requireContext(), getViewModel().getChannelUrl())); + } + } + private void startParticipantsListActivity() { if (isFragmentAlive()) { startActivity(ParticipantListActivity.newIntent(requireContext(), getViewModel().getChannelUrl())); @@ -231,7 +242,6 @@ private void showChannelInfoEditDialog() { DialogEditTextParams params = new DialogEditTextParams(getString(R.string.sb_text_channel_settings_change_channel_name_hint)); params.setEnableSingleLine(true); - assert getFragmentManager() != null; DialogUtils.showInputDialog( requireContext(), getString(R.string.sb_text_channel_settings_change_channel_name), @@ -240,24 +250,8 @@ private void showChannelInfoEditDialog() { getString(R.string.sb_text_button_cancel), null); } else if (key == R.string.sb_text_channel_settings_change_channel_image) { Logger.dev("change channel image"); - checkPermission(PICK_IMAGE_PERMISSIONS_REQUEST_CODE, new IPermissionHandler() { - @Override - @NonNull - public String[] getPermissions(int requestCode) { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return new String[]{Manifest.permission.CAMERA, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - return new String[]{Manifest.permission.CAMERA, - Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}; - } - - @Override - public void onPermissionGranted(int requestCode) { - showMediaSelectDialog(); - } - }); + String[] permissions = PermissionUtils.CAMERA_PERMISSION; + requestPermission(permissions, this::showMediaSelectDialog); } }); } @@ -278,7 +272,7 @@ private void showMediaSelectDialog() { if (key == R.string.sb_text_channel_settings_change_channel_image_camera) { takeCamera(); } else if (key == R.string.sb_text_channel_settings_change_channel_image_gallery) { - pickImage(); + takePhoto(); } } catch (Exception e) { Logger.e(e); @@ -287,19 +281,34 @@ private void showMediaSelectDialog() { }); } + private void showDeleteChannelDialog() { + if (getContext() == null) return; + DialogUtils.showWarningDialog( + requireContext(), + getString(R.string.sb_text_dialog_delete_channel), + getString(R.string.sb_text_dialog_delete_channel_message), + getString(R.string.sb_text_button_delete), + delete -> { + Logger.dev("delete"); + deleteChannel(); + }, + getString(R.string.sb_text_button_cancel), + cancel -> Logger.dev("cancel")); + } + private void takeCamera() { if (!isFragmentAlive()) return; mediaUri = FileUtils.createPictureImageUri(requireContext()); if (mediaUri == null) return; Intent intent = IntentUtils.getCameraIntent(requireActivity(), mediaUri); if (IntentUtils.hasIntent(requireContext(), intent)) { - startActivityForResult(intent, CAPTURE_IMAGE_PERMISSIONS_REQUEST_CODE); + takeCameraLauncher.launch(intent); } } - private void pickImage() { + private void takePhoto() { Intent intent = IntentUtils.getImageGalleryIntent(); - startActivityForResult(intent, PICK_IMAGE_PERMISSIONS_REQUEST_CODE); + getContentLauncher.launch(intent); } /** diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/OperatorListFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/OperatorListFragment.java index 8ce11bea..1fd5fb0e 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/OperatorListFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/OperatorListFragment.java @@ -97,6 +97,11 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull OperatorListM protected void onReady(@NonNull ReadyStatus status, @NonNull OperatorListModule module, @NonNull OperatorListViewModel viewModel) { Logger.d(">> OperatorListFragment::onReady(ReadyStatus=%s)", status); final GroupChannel channel = viewModel.getChannel(); + if (status != ReadyStatus.READY || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } viewModel.getOperatorUnregistered().observe(getViewLifecycleOwner(), isDismissed -> { if (isDismissed) shouldActivityFinish(); }); @@ -104,7 +109,6 @@ protected void onReady(@NonNull ReadyStatus status, @NonNull OperatorListModule if (isDeleted) shouldActivityFinish(); }); - if (channel == null) return; if (channel.getMyRole() != Role.OPERATOR) shouldActivityFinish(); viewModel.loadInitial(); } 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 bfbda32b..fb301637 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/ParticipantListFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/ParticipantListFragment.java @@ -11,16 +11,17 @@ import androidx.annotation.StyleRes; import androidx.lifecycle.ViewModelProvider; -import com.sendbird.android.SendbirdChat; import com.sendbird.android.channel.OpenChannel; -import com.sendbird.android.channel.Role; import com.sendbird.android.user.User; +import com.sendbird.uikit.R; import com.sendbird.uikit.SendbirdUIKit; import com.sendbird.uikit.activities.adapter.ParticipantListAdapter; import com.sendbird.uikit.consts.StringSet; +import com.sendbird.uikit.interfaces.OnCompleteHandler; import com.sendbird.uikit.interfaces.OnItemClickListener; import com.sendbird.uikit.interfaces.OnItemLongClickListener; import com.sendbird.uikit.log.Logger; +import com.sendbird.uikit.model.DialogListItem; import com.sendbird.uikit.model.ReadyStatus; import com.sendbird.uikit.modules.ParticipantListModule; import com.sendbird.uikit.modules.components.HeaderComponent; @@ -90,13 +91,22 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull ParticipantLi protected void onReady(@NonNull ReadyStatus status, @NonNull ParticipantListModule module, @NonNull ParticipantViewModel viewModel) { Logger.d(">> ParticipantListFragment::onReady(ReadyStatus=%s)", status); final OpenChannel channel = viewModel.getChannel(); + if (status != ReadyStatus.READY || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } + + viewModel.loadInitial(); + viewModel.getChannelDeleted().observe(getViewLifecycleOwner(), isDeleted -> { if (isDeleted) shouldActivityFinish(); }); - - if (channel != null) { - viewModel.loadInitial(); - } + viewModel.getUserBanned().observe(getViewLifecycleOwner(), restrictedUser -> { + if (restrictedUser.getUserId().equals(SendbirdUIKit.getAdapter().getUserInfo().getUserId())) { + shouldActivityFinish(); + } + }); } /** @@ -127,11 +137,12 @@ protected void onBindParticipantsListComponent(@NonNull ParticipantListComponent listComponent.setOnItemClickListener(itemClickListener); listComponent.setOnItemLongClickListener(itemLongClickListener); listComponent.setOnProfileClickListener(profileClickListener != null ? profileClickListener : this::onProfileClicked); - listComponent.setOnActionItemClickListener(actionItemClickListener); + listComponent.setOnActionItemClickListener(actionItemClickListener != null ? actionItemClickListener : (view, position, participant) -> onActionItemClicked(view, position, participant, channel)); viewModel.getUserList().observe(getViewLifecycleOwner(), users -> { - Logger.dev("++ observing result members size : %s", users.size()); - if (channel == null) return; - listComponent.notifyDataSetChanged(users, channel.isOperator(SendbirdChat.getCurrentUser()) ? Role.OPERATOR : Role.NONE); + Logger.dev("++ observing result participants size : %s", users.size()); + if (channel != null) { + listComponent.notifyDataSetChanged(users, channel); + } }); } @@ -165,6 +176,59 @@ protected void onProfileClicked(@NonNull View view, int position, @NonNull User DialogUtils.showUserProfileDialog(getContext(), user, false, null, null); } + /** + * Called when the action has been clicked. + * + * @param view The view that was clicked. + * @param position The position that was clicked. + * @param participant The participant data that was clicked. + * @param channel The {@code OpenChannel} that contains the data needed for this fragment + * @since 3.1.0 + */ + 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); + } + }); + } + /** * Returns the URL of the channel with the required data to use this fragment. * diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/PermissionFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/PermissionFragment.java index 140fca47..2bffb2ad 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/PermissionFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/PermissionFragment.java @@ -1,7 +1,5 @@ package com.sendbird.uikit.fragments; -import static android.app.Activity.RESULT_OK; - import android.Manifest; import android.app.Activity; import android.content.Context; @@ -13,6 +11,10 @@ import android.net.Uri; import android.provider.Settings; +import androidx.activity.result.ActivityResultCallback; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContract; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; @@ -26,91 +28,62 @@ import java.util.List; import java.util.Locale; +import java.util.Map; public abstract class PermissionFragment extends BaseFragment { - interface IPermissionHandler { - @NonNull - String[] getPermissions(int requestCode); - void onPermissionGranted(int requestCode); + interface PermissionHandler { + void onPermissionGranted(); } - private int requestCode; @Nullable - private IPermissionHandler handler; - private final int PERMISSION_SETTINGS_REQUEST_ID = 100; + private PermissionHandler handler; - void checkPermission(int requestCode, @NonNull IPermissionHandler handler) { - this.requestCode = requestCode; - this.handler = handler; - final String[] permissions = handler.getPermissions(requestCode); + @NonNull + private final ActivityResultLauncher appSettingLauncher = registerForActivityResult(new AppSettingActivityResult(), information -> { if (getContext() == null) return; - final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), permissions); + + final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), information.permission); + Logger.d("___ hasPermission=%s", hasPermission); if (hasPermission) { - handler.onPermissionGranted(requestCode); - return; + notifyPermissionResult(information.handler); } - - requestPermissions(permissions, requestCode); - } - - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { - if (handler == null) return; - final String[] requested = handler.getPermissions(requestCode); - if (requestCode == this.requestCode && grantResults.length == requested.length) { - boolean isAllGranted = true; - for (int result : grantResults) { - if (result != PackageManager.PERMISSION_GRANTED) { - isAllGranted = false; - break; - } + }); + @NonNull + private final ActivityResultLauncher requestPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), new ActivityResultCallback>() { + @Override + public void onActivityResult(@NonNull Map permissionResults) { + if (getContext() == null) return; + if (PermissionUtils.getNotGrantedPermissions(permissionResults).isEmpty()) { + notifyPermissionResult(handler); + handler = null; } + } + }); - if (isAllGranted) { - handler.onPermissionGranted(requestCode); - } else { - if (getContext() == null | getActivity() == null) return; - String[] notGranted = PermissionUtils.getNotGrantedPermissions(getContext(), permissions); - List deniedList = PermissionUtils.getShowRequestPermissionRationale(getActivity(), permissions); - if (deniedList.size() == 0) { - if (!isFragmentAlive()) return; - Drawable icon = getPermissionDrawable(requireActivity(), notGranted[0]); - AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); - builder.setTitle(requireContext().getString(R.string.sb_text_dialog_permission_title)); - builder.setMessage(getPermissionGuideMessage(requireContext(), notGranted[0])); - if (icon != null) { - builder.setIcon(icon); - } - builder.setPositiveButton(R.string.sb_text_go_to_settings, (dialogInterface, i) -> { - Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); - intent.addCategory(Intent.CATEGORY_DEFAULT); - intent.setData(Uri.parse("package:" + requireContext().getPackageName())); - intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); - intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - startActivityForResult(intent, PERMISSION_SETTINGS_REQUEST_ID); - }); - AlertDialog dialog = builder.create(); - dialog.show(); - dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(requireContext(), R.color.secondary_300)); - } - } + private void notifyPermissionResult(@Nullable PermissionHandler handler) { + if (handler != null) { + handler.onPermissionGranted(); } } - @Override - public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { - if (resultCode != RESULT_OK) return; - if (requestCode == PERMISSION_SETTINGS_REQUEST_ID) { - if (handler != null) { - String[] permissions = handler.getPermissions(this.requestCode); - if (getContext() == null) return; - final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), permissions); - if (hasPermission) { - handler.onPermissionGranted(requestCode); - } - } + void requestPermission(@NonNull String[] permissions, @NonNull PermissionHandler handler) { + if (getContext() == null || getActivity() == null) return; + this.handler = handler; + // 1. check permission + final boolean hasPermission = PermissionUtils.hasPermissions(getContext(), permissions); + if (hasPermission) { + notifyPermissionResult(handler); + return; } + + // 2. determine whether rationale popup should show + final List deniedList = PermissionUtils.getExplicitDeniedPermissionList(getActivity(), permissions); + if (!deniedList.isEmpty()) { + showPermissionRationalePopup(deniedList.get(0), handler); + return; + } + // 3. request permission + requestPermissionLauncher.launch(permissions); } @Nullable @@ -138,4 +111,59 @@ private static String getPermissionGuideMessage(@NonNull Context context, @NonNu } return String.format(Locale.US, context.getString(textResId), ContextUtils.getApplicationName(context)); } + + private void showPermissionRationalePopup(@NonNull String permission, @NonNull PermissionHandler handler) { + Drawable icon = getPermissionDrawable(requireActivity(), permission); + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); + builder.setTitle(requireContext().getString(R.string.sb_text_dialog_permission_title)); + builder.setMessage(getPermissionGuideMessage(requireContext(), permission)); + if (icon != null) { + builder.setIcon(icon); + } + builder.setPositiveButton(R.string.sb_text_go_to_settings, (dialogInterface, i) -> { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setData(Uri.parse("package:" + requireContext().getPackageName())); + intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + intent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); + appSettingLauncher.launch(new PermissionInformation(intent, permission, handler)); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(ContextCompat.getColor(requireContext(), R.color.secondary_300)); + } + + private static final class PermissionInformation { + @NonNull + private Intent intent; + @NonNull + private final String permission; + @NonNull + private final PermissionHandler handler; + + public PermissionInformation(@NonNull Intent intent, @NonNull String permission, @NonNull PermissionHandler handler) { + this.intent = intent; + this.permission = permission; + this.handler = handler; + } + } + + private static final class AppSettingActivityResult extends ActivityResultContract { + private PermissionInformation information; + + @NonNull + @Override + public Intent createIntent(@NonNull Context context, @NonNull PermissionInformation information) { + this.information = information; + return information.intent; + } + + @NonNull + @Override + public PermissionInformation parseResult(int resultCode, @Nullable Intent intent) { + if (intent != null) this.information.intent = intent; + return information; + } + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/PhotoViewFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/PhotoViewFragment.java index e28b079c..3ac251bf 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/PhotoViewFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/PhotoViewFragment.java @@ -45,7 +45,7 @@ import com.sendbird.uikit.utils.TextUtils; import com.sendbird.uikit.vm.FileDownloader; -public class PhotoViewFragment extends PermissionFragment implements PermissionFragment.IPermissionHandler, LoadingDialogHandler { +public class PhotoViewFragment extends PermissionFragment implements PermissionFragment.PermissionHandler, LoadingDialogHandler { @NonNull private final String[] REQUIRED_PERMISSIONS = {Manifest.permission.WRITE_EXTERNAL_STORAGE}; @@ -212,7 +212,7 @@ protected void onDrawPage() { if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { download(); } else { - checkPermission(0, (IPermissionHandler) this); + requestPermission(REQUIRED_PERMISSIONS, (PermissionHandler) this); } }); @@ -274,13 +274,7 @@ public void onAnimationEnd(Animator animation) { } @Override - @NonNull - public String[] getPermissions(int requestCode) { - return REQUIRED_PERMISSIONS; - } - - @Override - public void onPermissionGranted(int requestCode) { + public void onPermissionGranted() { download(); } diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/RegisterOperatorFragment.java b/uikit/src/main/java/com/sendbird/uikit/fragments/RegisterOperatorFragment.java index 6216fcac..f4b31749 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/RegisterOperatorFragment.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/RegisterOperatorFragment.java @@ -91,6 +91,11 @@ protected void onBeforeReady(@NonNull ReadyStatus status, @NonNull RegisterOpera protected void onReady(@NonNull ReadyStatus status, @NonNull RegisterOperatorModule module, @NonNull RegisterOperatorViewModel viewModel) { Logger.d(">> RegisterOperatorFragment::onReady(ReadyStatus=%s)", status); final GroupChannel channel = viewModel.getChannel(); + if (status != ReadyStatus.READY || channel == null) { + final StatusComponent statusComponent = module.getStatusComponent(); + statusComponent.notifyStatusChanged(StatusFrameView.Status.CONNECTION_ERROR); + return; + } viewModel.getChannelDeleted().observe(getViewLifecycleOwner(), isDeleted -> { if (isDeleted) shouldActivityFinish(); }); diff --git a/uikit/src/main/java/com/sendbird/uikit/fragments/UIKitFragmentFactory.java b/uikit/src/main/java/com/sendbird/uikit/fragments/UIKitFragmentFactory.java index d98f9ced..3d2bd318 100644 --- a/uikit/src/main/java/com/sendbird/uikit/fragments/UIKitFragmentFactory.java +++ b/uikit/src/main/java/com/sendbird/uikit/fragments/UIKitFragmentFactory.java @@ -33,7 +33,7 @@ public Fragment newChannelListFragment(@NonNull Bundle args) { /** * Returns the ChannelFragment. * - * @param channelUrl the channel url, + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link ChannelFragment} * @since 3.0.0 @@ -49,6 +49,7 @@ public Fragment newChannelFragment(@NonNull String channelUrl, @NonNull Bundle a /** * Returns the ChannelSettingsFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link ChannelSettingsFragment} * @since 3.0.0 @@ -64,6 +65,7 @@ public Fragment newChannelSettingsFragment(@NonNull String channelUrl, @NonNull /** * Returns the CreateChannelFragment. * + * @param channelType the channel type to be created. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link CreateChannelFragment} * @since 3.0.0 @@ -79,6 +81,7 @@ public Fragment newCreateChannelFragment(@NonNull CreatableChannelType channelTy /** * Returns the InviteUserFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link InviteUserFragment} * @since 3.0.0 @@ -94,6 +97,7 @@ public Fragment newInviteUserFragment(@NonNull String channelUrl, @NonNull Bundl /** * Returns the RegisterOperatorFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link RegisterOperatorFragment} * @since 3.0.0 @@ -109,6 +113,7 @@ public Fragment newRegisterOperatorFragment(@NonNull String channelUrl, @NonNull /** * Returns the MessageSearchFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link MessageSearchFragment} * @since 3.0.0 @@ -124,6 +129,7 @@ public Fragment newMessageSearchFragment(@NonNull String channelUrl, @NonNull Bu /** * Returns the ModerationFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link ModerationFragment} * @since 3.0.0 @@ -139,6 +145,7 @@ public Fragment newModerationFragment(@NonNull String channelUrl, @NonNull Bundl /** * Returns the MemberListFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link MemberListFragment} * @since 3.0.0 @@ -155,6 +162,7 @@ public Fragment newMemberListFragment(@NonNull String channelUrl, @NonNull Bundl /** * Returns the BannedUserListFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link BannedUserListFragment} * @since 3.0.0 @@ -171,6 +179,7 @@ public Fragment newBannedUserListFragment(@NonNull String channelUrl, @NonNull B /** * Returns the OperatorListFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link OperatorListFragment} * @since 3.0.0 @@ -187,6 +196,7 @@ public Fragment newOperatorListFragment(@NonNull String channelUrl, @NonNull Bun /** * Returns the MutedMemberListFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link MutedMemberListFragment} * @since 3.0.0 @@ -203,6 +213,7 @@ public Fragment newMutedMemberListFragment(@NonNull String channelUrl, @NonNull /** * Returns the ParticipantListFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link ParticipantListFragment} * @since 3.0.0 @@ -218,6 +229,7 @@ public Fragment newParticipantListFragment(@NonNull String channelUrl, @NonNull /** * Returns the OpenChannelSettingsFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link OpenChannelSettingsFragment} * @since 3.0.0 @@ -233,6 +245,7 @@ public Fragment newOpenChannelSettingsFragment(@NonNull String channelUrl, @NonN /** * Returns the OpenChannelSettingsFragment. * + * @param channelUrl the channel url for the target channel. * @param args the arguments supplied when the fragment was instantiated. * @return The {@link OpenChannelSettingsFragment} * @since 3.0.0 @@ -244,4 +257,79 @@ public Fragment newChannelPushSettingFragment(@NonNull String channelUrl, @NonNu .setUseHeader(true) .build(); } + + /** + * Returns the OpenChannelModerationFragment. + * + * @param channelUrl the channel url for the target channel. + * @param args the arguments supplied when the fragment was instantiated. + * @return The {@link OpenChannelModerationFragment} + * @since 3.1.0 + */ + @NonNull + public Fragment newOpenChannelModerationFragment(@NonNull String channelUrl, @NonNull Bundle args) { + return new OpenChannelModerationFragment.Builder(channelUrl) + .withArguments(args) + .build(); + } + + /** + * Returns the OpenChannelRegisterOperatorFragment. + * + * @param channelUrl the channel url for the target channel. + * @param args the arguments supplied when the fragment was instantiated. + * @return The {@link OpenChannelRegisterOperatorFragment} + * @since 3.1.0 + */ + @NonNull + public Fragment newOpenChannelRegisterOperatorFragment(@NonNull String channelUrl, @NonNull Bundle args) { + return new OpenChannelRegisterOperatorFragment.Builder(channelUrl) + .withArguments(args) + .build(); + } + + /** + * Returns the OpenChannelOperatorListFragment. + * + * @param channelUrl the channel url for the target channel. + * @param args the arguments supplied when the fragment was instantiated. + * @return The {@link OpenChannelOperatorListFragment} + * @since 3.1.0 + */ + @NonNull + public Fragment newOpenChannelOperatorListFragment(@NonNull String channelUrl, @NonNull Bundle args) { + return new OpenChannelOperatorListFragment.Builder(channelUrl) + .withArguments(args) + .build(); + } + + /** + * Returns the OpenChannelMutedParticipantListFragment. + * + * @param channelUrl the channel url for the target channel. + * @param args the arguments supplied when the fragment was instantiated. + * @return The {@link OpenChannelMutedParticipantListFragment} + * @since 3.1.0 + */ + @NonNull + public Fragment newOpenChannelMutedParticipantListFragment(@NonNull String channelUrl, @NonNull Bundle args) { + return new OpenChannelMutedParticipantListFragment.Builder(channelUrl) + .withArguments(args) + .build(); + } + + /** + * Returns the OpenChannelBannedUserListFragment. + * + * @param channelUrl the channel url for the target channel. + * @param args the arguments supplied when the fragment was instantiated. + * @return The {@link OpenChannelBannedUserListFragment} + * @since 3.1.0 + */ + @NonNull + public Fragment newOpenChannelBannedUserListFragment(@NonNull String channelUrl, @NonNull Bundle args) { + return new OpenChannelBannedUserListFragment.Builder(channelUrl) + .withArguments(args) + .build(); + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/BaseModule.java b/uikit/src/main/java/com/sendbird/uikit/modules/BaseModule.java index ea740770..69f2098a 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/BaseModule.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/BaseModule.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules; -import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.util.TypedValue; @@ -98,7 +97,6 @@ public int getTheme() { * @return true if the header is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseHeader() { return useHeader; } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/MessageSearchModule.java b/uikit/src/main/java/com/sendbird/uikit/modules/MessageSearchModule.java index a55643a0..cfd9df8f 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/MessageSearchModule.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/MessageSearchModule.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules; -import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.util.TypedValue; @@ -280,7 +279,6 @@ public void setUseSearchBar(boolean useSearchBar) { * @return true if the message search header is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseSearchBar() { return shouldUseHeader(); } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelBannedUserListModule.java b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelBannedUserListModule.java new file mode 100644 index 00000000..6693882e --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelBannedUserListModule.java @@ -0,0 +1,261 @@ +package com.sendbird.uikit.modules; + +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.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.view.ContextThemeWrapper; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.interfaces.LoadingDialogHandler; +import com.sendbird.uikit.modules.components.HeaderComponent; +import com.sendbird.uikit.modules.components.OpenChannelBannedUserListComponent; +import com.sendbird.uikit.modules.components.StatusComponent; +import com.sendbird.uikit.widgets.WaitingDialog; + +/** + * A module for open channel banned user list. + * All composed components are created when the module is created. After than those components can replace. + * + * @since 3.1.0 + */ +public class OpenChannelBannedUserListModule extends BaseModule { + @NonNull + private final Params params; + @NonNull + private HeaderComponent headerComponent; + @NonNull + private OpenChannelBannedUserListComponent bannedUserListComponent; + @NonNull + private StatusComponent statusComponent; + + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public OpenChannelBannedUserListModule(@NonNull Context context) { + this(context, new Params(context)); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param params The Parameter object that can customize a default Fragment. + * @since 3.1.0 + */ + public OpenChannelBannedUserListModule(@NonNull Context context, @NonNull Params params) { + this.params = params; + this.headerComponent = new HeaderComponent(); + this.headerComponent.getParams().setUseRightButton(false); + this.bannedUserListComponent = new OpenChannelBannedUserListComponent(); + this.statusComponent = new StatusComponent(); + } + + @Override + @NonNull + public View onCreateView(@NonNull Context context, @NonNull LayoutInflater inflater, @Nullable Bundle args) { + if (args != null) params.apply(context, args); + final Context moduleContext = new ContextThemeWrapper(context, params.getTheme()); + final LinearLayout parent = new LinearLayout(context); + parent.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.setOrientation(LinearLayout.VERTICAL); + + final TypedValue values = new TypedValue(); + if (params.shouldUseHeader()) { + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_header, values, true); + final Context headerThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater headerInflater = inflater.cloneInContext(headerThemeContext); + final View header = this.headerComponent.onCreateView(headerThemeContext, headerInflater, parent, args); + parent.addView(header); + } + + final FrameLayout innerContainer = new FrameLayout(context); + innerContainer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.addView(innerContainer); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_list, values, true); + final Context listThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater listInflater = inflater.cloneInContext(listThemeContext); + final View userListLayout = this.bannedUserListComponent.onCreateView(listThemeContext, listInflater, innerContainer, args); + innerContainer.addView(userListLayout); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_status, values, true); + final Context statusThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater statusInflater = inflater.cloneInContext(statusThemeContext); + final View statusLayout = statusComponent.onCreateView(statusThemeContext, statusInflater, innerContainer, args); + innerContainer.addView(statusLayout); + return parent; + } + + /** + * Sets the handler for the loading dialog. + * + * @param loadingDialogHandler Loading dialog handler to be used in this module + * @since 3.1.0 + */ + public void setOnLoadingDialogHandler(@Nullable LoadingDialogHandler loadingDialogHandler) { + this.loadingDialogHandler = loadingDialogHandler; + } + + /** + * Returns the handler for loading dialog. + * + * @return Loading dialog handler to be used in this module + * @since 3.1.0 + */ + @Nullable + public LoadingDialogHandler getLoadingDialogHandler() { + return loadingDialogHandler; + } + + /** + * It will be called when the loading dialog needs displaying. + * + * @return True if the callback has consumed the event, false otherwise. + * @since 3.1.0 + */ + public boolean shouldShowLoadingDialog(@NonNull Context context) { + if (loadingDialogHandler != null && loadingDialogHandler.shouldShowLoadingDialog()) { + return true; + } + + WaitingDialog.show(context); + return true; + } + + /** + * It will be called when the loading dialog needs dismissing. + * + * @since 3.1.0 + */ + public void shouldDismissLoadingDialog() { + if (loadingDialogHandler != null) { + loadingDialogHandler.shouldDismissLoadingDialog(); + return; + } + WaitingDialog.dismiss(); + } + + /** + * Sets a custom header component. + * + * @param component The header component to be used in this module + * @since 3.1.0 + */ + public void setHeaderComponent(@NonNull T component) { + this.headerComponent = component; + } + + /** + * Sets a custom list component. + * + * @param component The list component to be used in this module + * @since 3.1.0 + */ + public void setBannedUserListComponent(@NonNull T component) { + this.bannedUserListComponent = component; + } + + /** + * Sets a custom status component. + * + * @param component The status component to be used in this module + * @since 3.1.0 + */ + public void setStatusComponent(@NonNull T component) { + this.statusComponent = component; + } + + /** + * Returns the header component. + * + * @return The header component of this module + * @since 3.1.0 + */ + @NonNull + public HeaderComponent getHeaderComponent() { + return headerComponent; + } + + /** + * Returns the list component. + * + * @return The list component of this module + * @since 3.1.0 + */ + @NonNull + public OpenChannelBannedUserListComponent getBannedUserListComponent() { + return bannedUserListComponent; + } + + /** + * Returns the status component. + * + * @return The status component of this module + * @since 3.1.0 + */ + @NonNull + public StatusComponent getStatusComponent() { + return statusComponent; + } + + /** + * Returns a collection of parameters applied to this module. + * + * @return {@link Params} applied to this module. + * @since 3.1.0 + */ + @NonNull + public Params getParams() { + return params; + } + + public static class Params extends BaseModule.Params { + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public Params(@NonNull Context context) { + this(context, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeMode The theme of Sendbird UIKit to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @NonNull SendbirdUIKit.ThemeMode themeMode) { + super(context, themeMode, R.attr.sb_module_open_channel_banned_user_list); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeResId The theme resource ID to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @StyleRes int themeResId) { + super(context, themeResId, R.attr.sb_module_open_channel_banned_user_list); + } + } +} \ No newline at end of file diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelModerationModule.java b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelModerationModule.java new file mode 100644 index 00000000..14437d00 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelModerationModule.java @@ -0,0 +1,232 @@ +package com.sendbird.uikit.modules; + +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.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.view.ContextThemeWrapper; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.interfaces.LoadingDialogHandler; +import com.sendbird.uikit.modules.components.HeaderComponent; +import com.sendbird.uikit.modules.components.OpenChannelModerationListComponent; +import com.sendbird.uikit.widgets.WaitingDialog; + +/** + * A module for open channel moderation. + * All composed components are created when the module is created. After than those components can replace. + * + * @since 3.1.0 + */ +public class OpenChannelModerationModule extends BaseModule { + @NonNull + private final Params params; + @NonNull + private HeaderComponent headerComponent; + @NonNull + private OpenChannelModerationListComponent moderationListComponent; + + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public OpenChannelModerationModule(@NonNull Context context) { + this(context, new Params(context)); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param params The Parameter object that can customize a default Fragment. + * @since 3.1.0 + */ + public OpenChannelModerationModule(@NonNull Context context, @NonNull Params params) { + this.params = params; + this.headerComponent = new HeaderComponent(); + this.headerComponent.getParams().setUseRightButton(false); + this.moderationListComponent = new OpenChannelModerationListComponent(); + } + + @Override + @NonNull + public View onCreateView(@NonNull Context context, @NonNull LayoutInflater inflater, @Nullable Bundle args) { + if (args != null) params.apply(context, args); + final Context moduleContext = new ContextThemeWrapper(context, params.getTheme()); + + final LinearLayout parent = new LinearLayout(context); + parent.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.setOrientation(LinearLayout.VERTICAL); + parent.setBackgroundResource(SendbirdUIKit.isDarkMode() ? R.color.background_600 : R.color.background_50); + + final TypedValue values = new TypedValue(); + if (params.shouldUseHeader()) { + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_header, values, true); + final Context headerThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater headerInflater = inflater.cloneInContext(headerThemeContext); + final View header = this.headerComponent.onCreateView(headerThemeContext, headerInflater, parent, args); + parent.addView(header); + } + + final FrameLayout innerContainer = new FrameLayout(context); + innerContainer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.addView(innerContainer); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_moderation_list, values, true); + final Context listThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater listInflater = inflater.cloneInContext(listThemeContext); + final View moderationLayout = this.moderationListComponent.onCreateView(listThemeContext, listInflater, innerContainer, args); + innerContainer.addView(moderationLayout); + return parent; + } + + /** + * Sets a custom header component. + * + * @param component The header component to be used in this module + * @since 3.1.0 + */ + public void setHeaderComponent(@NonNull T component) { + this.headerComponent = component; + } + + /** + * Sets a custom list component. + * + * @param component The list component to be used in this module + * @since 3.1.0 + */ + public void setModerationListComponent(@NonNull T component) { + this.moderationListComponent = component; + } + + /** + * Returns the header component. + * + * @return The header component of this module + * @since 3.1.0 + */ + @NonNull + public HeaderComponent getHeaderComponent() { + return headerComponent; + } + + /** + * Returns the list component. + * + * @return The list component of this module + * @since 3.1.0 + */ + @NonNull + public OpenChannelModerationListComponent getModerationListComponent() { + return moderationListComponent; + } + + /** + * Sets the handler for the loading dialog. + * + * @param loadingDialogHandler Loading dialog handler to be used in this module + * @since 3.1.0 + */ + public void setOnLoadingDialogHandler(@Nullable LoadingDialogHandler loadingDialogHandler) { + this.loadingDialogHandler = loadingDialogHandler; + } + + /** + * Returns the handler for loading dialog. + * + * @return Loading dialog handler to be used in this module + * @since 3.1.0 + */ + @Nullable + public LoadingDialogHandler getLoadingDialogHandler() { + return loadingDialogHandler; + } + + /** + * It will be called when the loading dialog needs displaying. + * + * @return True if the callback has consumed the event, false otherwise. + * @since 3.1.0 + */ + public boolean shouldShowLoadingDialog(@NonNull Context context) { + if (loadingDialogHandler != null && loadingDialogHandler.shouldShowLoadingDialog()) { + return true; + } + + WaitingDialog.show(context); + return true; + } + + /** + * It will be called when the loading dialog needs dismissing. + * + * @since 3.1.0 + */ + public void shouldDismissLoadingDialog() { + if (loadingDialogHandler != null) { + loadingDialogHandler.shouldDismissLoadingDialog(); + return; + } + WaitingDialog.dismiss(); + } + + /** + * Returns a collection of parameters applied to this module. + * + * @return {@link Params} applied to this module. + * @since 3.1.0 + */ + @NonNull + public Params getParams() { + return params; + } + + public static class Params extends BaseModule.Params { + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public Params(@NonNull Context context) { + this(context, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeMode The theme of Sendbird UIKit to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @NonNull SendbirdUIKit.ThemeMode themeMode) { + super(context, themeMode, R.attr.sb_module_open_channel_moderation); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeResId The theme resource ID to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @StyleRes int themeResId) { + super(context, themeResId, R.attr.sb_module_open_channel_moderation); + } + } +} \ No newline at end of file diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelModule.java b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelModule.java index 7191f6c8..1ecdd819 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelModule.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelModule.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules; -import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.util.TypedValue; @@ -317,7 +316,6 @@ public void setUseOverlayMode(boolean useOverlayMode) { * * @return true if a overlay mode. is shown, false otherwise */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseOverlayMode() { return useOverlayMode; } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelMutedParticipantListModule.java b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelMutedParticipantListModule.java new file mode 100644 index 00000000..07a57353 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelMutedParticipantListModule.java @@ -0,0 +1,261 @@ +package com.sendbird.uikit.modules; + +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.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.view.ContextThemeWrapper; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.interfaces.LoadingDialogHandler; +import com.sendbird.uikit.modules.components.HeaderComponent; +import com.sendbird.uikit.modules.components.OpenChannelMutedParticipantListComponent; +import com.sendbird.uikit.modules.components.StatusComponent; +import com.sendbird.uikit.widgets.WaitingDialog; + +/** + * A module for open channel muted participant list. + * All composed components are created when the module is created. After than those components can replace. + * + * @since 3.1.0 + */ +public class OpenChannelMutedParticipantListModule extends BaseModule { + @NonNull + private final Params params; + @NonNull + private HeaderComponent headerComponent; + @NonNull + private OpenChannelMutedParticipantListComponent mutedParticipantListComponent; + @NonNull + private StatusComponent statusComponent; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public OpenChannelMutedParticipantListModule(@NonNull Context context) { + this(context, new Params(context)); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param params The Parameter object that can customize a default Fragment. + * @since 3.1.0 + */ + public OpenChannelMutedParticipantListModule(@NonNull Context context, @NonNull Params params) { + this.params = params; + this.headerComponent = new HeaderComponent(); + this.headerComponent.getParams().setUseRightButton(false); + this.mutedParticipantListComponent = new OpenChannelMutedParticipantListComponent(); + this.statusComponent = new StatusComponent(); + } + + @Override + @NonNull + public View onCreateView(@NonNull Context context, @NonNull LayoutInflater inflater, @Nullable Bundle args) { + if (args != null) params.apply(context, args); + final Context moduleContext = new ContextThemeWrapper(context, params.getTheme()); + + final LinearLayout parent = new LinearLayout(context); + parent.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.setOrientation(LinearLayout.VERTICAL); + + final TypedValue values = new TypedValue(); + if (params.shouldUseHeader()) { + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_header, values, true); + final Context headerThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater headerInflater = inflater.cloneInContext(headerThemeContext); + final View header = this.headerComponent.onCreateView(headerThemeContext, headerInflater, parent, args); + parent.addView(header); + } + + final FrameLayout innerContainer = new FrameLayout(context); + innerContainer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.addView(innerContainer); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_list, values, true); + final Context listThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater listInflater = inflater.cloneInContext(listThemeContext); + final View mutedParticipantListLayout = this.mutedParticipantListComponent.onCreateView(listThemeContext, listInflater, innerContainer, args); + innerContainer.addView(mutedParticipantListLayout); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_status, values, true); + final Context statusThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater statusInflater = inflater.cloneInContext(statusThemeContext); + final View statusLayout = statusComponent.onCreateView(statusThemeContext, statusInflater, innerContainer, args); + innerContainer.addView(statusLayout); + return parent; + } + + /** + * Sets the handler for the loading dialog. + * + * @param loadingDialogHandler Loading dialog handler to be used in this module + * @since 3.1.0 + */ + public void setOnLoadingDialogHandler(@Nullable LoadingDialogHandler loadingDialogHandler) { + this.loadingDialogHandler = loadingDialogHandler; + } + + /** + * Returns the handler for loading dialog. + * + * @return Loading dialog handler to be used in this module + * @since 3.1.0 + */ + @Nullable + public LoadingDialogHandler getLoadingDialogHandler() { + return loadingDialogHandler; + } + + /** + * It will be called when the loading dialog needs displaying. + * + * @return True if the callback has consumed the event, false otherwise. + * @since 3.1.0 + */ + public boolean shouldShowLoadingDialog(@NonNull Context context) { + if (loadingDialogHandler != null && loadingDialogHandler.shouldShowLoadingDialog()) { + return true; + } + + WaitingDialog.show(context); + return true; + } + + /** + * It will be called when the loading dialog needs dismissing. + * + * @since 3.1.0 + */ + public void shouldDismissLoadingDialog() { + if (loadingDialogHandler != null) { + loadingDialogHandler.shouldDismissLoadingDialog(); + return; + } + WaitingDialog.dismiss(); + } + + /** + * Sets a custom header component. + * + * @param component The header component to be used in this module + * @since 3.1.0 + */ + public void setHeaderComponent(@NonNull T component) { + this.headerComponent = component; + } + + /** + * Sets a custom list component. + * + * @param component The list component to be used in this module + * @since 3.1.0 + */ + public void setMutedParticipantListComponent(@NonNull T component) { + this.mutedParticipantListComponent = component; + } + + /** + * Sets a custom status component. + * + * @param component The status component to be used in this module + * @since 3.1.0 + */ + public void setStatusComponent(@NonNull T component) { + this.statusComponent = component; + } + + /** + * Returns the header component. + * + * @return The header component of this module + * @since 3.1.0 + */ + @NonNull + public HeaderComponent getHeaderComponent() { + return headerComponent; + } + + /** + * Returns the list component. + * + * @return The list component of this module + * @since 3.1.0 + */ + @NonNull + public OpenChannelMutedParticipantListComponent getMutedParticipantListComponent() { + return mutedParticipantListComponent; + } + + /** + * Returns the status component. + * + * @return The status component of this module + * @since 3.1.0 + */ + @NonNull + public StatusComponent getStatusComponent() { + return statusComponent; + } + + /** + * Returns a collection of parameters applied to this module. + * + * @return {@link Params} applied to this module. + * @since 3.1.0 + */ + @NonNull + public Params getParams() { + return params; + } + + public static class Params extends BaseModule.Params { + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public Params(@NonNull Context context) { + this(context, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeMode The theme of Sendbird UIKit to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @NonNull SendbirdUIKit.ThemeMode themeMode) { + super(context, themeMode, R.attr.sb_module_open_channel_muted_participant_list); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeResId The theme resource ID to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @StyleRes int themeResId) { + super(context, themeResId, R.attr.sb_module_open_channel_muted_participant_list); + } + } +} \ No newline at end of file diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelOperatorListModule.java b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelOperatorListModule.java new file mode 100644 index 00000000..8771750f --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelOperatorListModule.java @@ -0,0 +1,260 @@ +package com.sendbird.uikit.modules; + +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.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.view.ContextThemeWrapper; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.interfaces.LoadingDialogHandler; +import com.sendbird.uikit.modules.components.HeaderComponent; +import com.sendbird.uikit.modules.components.OpenChannelOperatorListComponent; +import com.sendbird.uikit.modules.components.StatusComponent; +import com.sendbird.uikit.widgets.WaitingDialog; + +/** + * A module for open channel operator list. + * All composed components are created when the module is created. After than those components can replace. + * + * @since 3.1.0 + */ +public class OpenChannelOperatorListModule extends BaseModule { + @NonNull + private final Params params; + @NonNull + private HeaderComponent headerComponent; + @NonNull + private OpenChannelOperatorListComponent operatorListComponent; + @NonNull + private StatusComponent statusComponent; + @Nullable + private LoadingDialogHandler loadingDialogHandler; + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public OpenChannelOperatorListModule(@NonNull Context context) { + this(context, new Params(context)); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param params The Parameter object that can customize a default Fragment. + * @since 3.1.0 + */ + public OpenChannelOperatorListModule(@NonNull Context context, @NonNull Params params) { + this.params = params; + this.headerComponent = new HeaderComponent(); + this.operatorListComponent = new OpenChannelOperatorListComponent(); + this.statusComponent = new StatusComponent(); + } + + @Override + @NonNull + public View onCreateView(@NonNull Context context, @NonNull LayoutInflater inflater, @Nullable Bundle args) { + if (args != null) params.apply(context, args); + final Context moduleContext = new ContextThemeWrapper(context, params.getTheme()); + + final LinearLayout parent = new LinearLayout(context); + parent.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.setOrientation(LinearLayout.VERTICAL); + + final TypedValue values = new TypedValue(); + if (params.shouldUseHeader()) { + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_header, values, true); + final Context headerThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater headerInflater = inflater.cloneInContext(headerThemeContext); + final View header = this.headerComponent.onCreateView(headerThemeContext, headerInflater, parent, args); + parent.addView(header); + } + + final FrameLayout innerContainer = new FrameLayout(context); + innerContainer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.addView(innerContainer); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_list, values, true); + final Context listThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater listInflater = inflater.cloneInContext(listThemeContext); + final View operatorListLayout = operatorListComponent.onCreateView(listThemeContext, listInflater, innerContainer, args); + innerContainer.addView(operatorListLayout); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_status, values, true); + final Context statusThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater statusInflater = inflater.cloneInContext(statusThemeContext); + final View statusLayout = statusComponent.onCreateView(statusThemeContext, statusInflater, innerContainer, args); + innerContainer.addView(statusLayout); + return parent; + } + + /** + * Sets the handler for the loading dialog. + * + * @param loadingDialogHandler Loading dialog handler to be used in this module + * @since 3.1.0 + */ + public void setOnLoadingDialogHandler(@Nullable LoadingDialogHandler loadingDialogHandler) { + this.loadingDialogHandler = loadingDialogHandler; + } + + /** + * Returns the handler for loading dialog. + * + * @return Loading dialog handler to be used in this module + * @since 3.1.0 + */ + @Nullable + public LoadingDialogHandler getLoadingDialogHandler() { + return loadingDialogHandler; + } + + /** + * It will be called when the loading dialog needs displaying. + * + * @return True if the callback has consumed the event, false otherwise. + * @since 3.1.0 + */ + public boolean shouldShowLoadingDialog(@NonNull Context context) { + if (loadingDialogHandler != null && loadingDialogHandler.shouldShowLoadingDialog()) { + return true; + } + + WaitingDialog.show(context); + return true; + } + + /** + * It will be called when the loading dialog needs dismissing. + * + * @since 3.1.0 + */ + public void shouldDismissLoadingDialog() { + if (loadingDialogHandler != null) { + loadingDialogHandler.shouldDismissLoadingDialog(); + return; + } + WaitingDialog.dismiss(); + } + + /** + * Sets a custom header component. + * + * @param component The header component to be used in this module + * @since 3.1.0 + */ + public void setHeaderComponent(@NonNull T component) { + this.headerComponent = component; + } + + /** + * Sets a custom list component. + * + * @param component The list component to be used in this module + * @since 3.1.0 + */ + public void setOperatorListComponent(@NonNull T component) { + this.operatorListComponent = component; + } + + /** + * Sets a custom status component. + * + * @param component The status component to be used in this module + * @since 3.1.0 + */ + public void setStatusComponent(@NonNull T component) { + this.statusComponent = component; + } + + /** + * Returns the header component. + * + * @return The header component of this module + * @since 3.1.0 + */ + @NonNull + public HeaderComponent getHeaderComponent() { + return headerComponent; + } + + /** + * Returns the list component. + * + * @return The list component of this module + * @since 3.1.0 + */ + @NonNull + public OpenChannelOperatorListComponent getOperatorListComponent() { + return operatorListComponent; + } + + /** + * Returns the status component. + * + * @return The status component of this module + * @since 3.1.0 + */ + @NonNull + public StatusComponent getStatusComponent() { + return statusComponent; + } + + /** + * Returns a collection of parameters applied to this module. + * + * @return {@link Params} applied to this module. + * @since 3.1.0 + */ + @NonNull + public Params getParams() { + return params; + } + + public static class Params extends BaseModule.Params { + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public Params(@NonNull Context context) { + this(context, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeMode The theme of Sendbird UIKit to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @NonNull SendbirdUIKit.ThemeMode themeMode) { + super(context, themeMode, R.attr.sb_module_open_channel_operator_list); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeResId The theme resource ID to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @StyleRes int themeResId) { + super(context, themeResId, R.attr.sb_module_open_channel_operator_list); + } + } +} \ No newline at end of file diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelRegisterOperatorModule.java b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelRegisterOperatorModule.java new file mode 100644 index 00000000..caf2555e --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/OpenChannelRegisterOperatorModule.java @@ -0,0 +1,207 @@ +package com.sendbird.uikit.modules; + +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.FrameLayout; +import android.widget.LinearLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StyleRes; +import androidx.appcompat.view.ContextThemeWrapper; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.SendbirdUIKit; +import com.sendbird.uikit.modules.components.OpenChannelRegisterOperatorListComponent; +import com.sendbird.uikit.modules.components.SelectUserHeaderComponent; +import com.sendbird.uikit.modules.components.StatusComponent; + +/** + * A module for open channel register operator. + * All composed components are created when the module is created. After than those components can replace. + * + * @since 3.1.0 + */ +public class OpenChannelRegisterOperatorModule extends BaseModule { + @NonNull + private final Params params; + @NonNull + private SelectUserHeaderComponent headerComponent; + @NonNull + private OpenChannelRegisterOperatorListComponent registerOperatorListComponent; + @NonNull + private StatusComponent statusComponent; + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public OpenChannelRegisterOperatorModule(@NonNull Context context) { + this(context, new Params(context)); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param params The Parameter object that can customize a default Fragment. + * @since 3.1.0 + */ + public OpenChannelRegisterOperatorModule(@NonNull Context context, @NonNull Params params) { + this.params = params; + this.headerComponent = new SelectUserHeaderComponent(); + this.headerComponent.getParams().setRightButtonText(context.getString(R.string.sb_text_button_add)); + this.registerOperatorListComponent = new OpenChannelRegisterOperatorListComponent(); + this.statusComponent = new StatusComponent(); + } + + @Override + @NonNull + public View onCreateView(@NonNull Context context, @NonNull LayoutInflater inflater, @Nullable Bundle args) { + if (args != null) params.apply(context, args); + final Context moduleContext = new ContextThemeWrapper(context, params.getTheme()); + final LinearLayout parent = new LinearLayout(context); + parent.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.setOrientation(LinearLayout.VERTICAL); + + final TypedValue values = new TypedValue(); + if (params.shouldUseHeader()) { + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_state_header, values, true); + final Context headerThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater headerInflater = inflater.cloneInContext(headerThemeContext); + final View header = this.headerComponent.onCreateView(headerThemeContext, headerInflater, parent, args); + parent.addView(header); + } + + final FrameLayout innerContainer = new FrameLayout(context); + innerContainer.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + parent.addView(innerContainer); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_list, values, true); + final Context listThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater listInflater = inflater.cloneInContext(listThemeContext); + final View participantListLayout = registerOperatorListComponent.onCreateView(listThemeContext, listInflater, innerContainer, args); + innerContainer.addView(participantListLayout); + + moduleContext.getTheme().resolveAttribute(R.attr.sb_component_status, values, true); + final Context statusThemeContext = new ContextThemeWrapper(moduleContext, values.resourceId); + final LayoutInflater statusInflater = inflater.cloneInContext(statusThemeContext); + final View statusLayout = statusComponent.onCreateView(statusThemeContext, statusInflater, innerContainer, args); + innerContainer.addView(statusLayout); + return parent; + } + + /** + * Sets a custom header component. + * + * @param component The header component to be used in this module + * @since 3.1.0 + */ + public void setHeaderComponent(@NonNull T component) { + this.headerComponent = component; + } + + /** + * Sets a custom list component. + * + * @param component The list component to be used in this module + * @since 3.1.0 + */ + public void setRegisterOperatorListComponent(@NonNull T component) { + this.registerOperatorListComponent = component; + } + + /** + * Sets a custom status component. + * + * @param component The status component to be used in this module + * @since 3.1.0 + */ + public void setStatusComponent(@NonNull T component) { + this.statusComponent = component; + } + + /** + * Returns the header component. + * + * @return The header component of this module + * @since 3.1.0 + */ + @NonNull + public SelectUserHeaderComponent getHeaderComponent() { + return headerComponent; + } + + /** + * Returns the list component. + * + * @return The list component of this module + * @since 3.1.0 + */ + @NonNull + public OpenChannelRegisterOperatorListComponent getRegisterOperatorListComponent() { + return registerOperatorListComponent; + } + + /** + * Returns the status component. + * + * @return The status component of this module + * @since 3.1.0 + */ + @NonNull + public StatusComponent getStatusComponent() { + return statusComponent; + } + + /** + * Returns a collection of parameters applied to this module. + * + * @return {@link Params} applied to this module. + * @since 3.1.0 + */ + @NonNull + public Params getParams() { + return params; + } + + public static class Params extends BaseModule.Params { + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @since 3.1.0 + */ + public Params(@NonNull Context context) { + this(context, SendbirdUIKit.getDefaultThemeMode()); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeMode The theme of Sendbird UIKit to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @NonNull SendbirdUIKit.ThemeMode themeMode) { + super(context, themeMode, R.attr.sb_module_open_channel_register_operator); + } + + /** + * Constructor + * + * @param context The {@code Context} this module is currently associated with + * @param themeResId The theme resource ID to be applied to this module + * @since 3.1.0 + */ + public Params(@NonNull Context context, @StyleRes int themeResId) { + super(context, themeResId, R.attr.sb_module_open_channel_register_operator); + } + } +} \ No newline at end of file diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/BannedUserListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/BannedUserListComponent.java index d67061f9..5b6ebcbf 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/BannedUserListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/BannedUserListComponent.java @@ -2,9 +2,12 @@ import androidx.annotation.NonNull; +import com.sendbird.android.channel.Role; import com.sendbird.android.user.User; import com.sendbird.uikit.activities.adapter.BannedUserListAdapter; +import java.util.List; + /** * This class creates and performs a view corresponding the banned user list area in Sendbird UIKit. * @@ -46,4 +49,15 @@ public void setAdapter(@NonNull T adapter) { protected BannedUserListAdapter getAdapter() { return this.adapter; } + + /** + * Notifies this component that the list of users is changed. + * + * @param userList The list of users to be displayed on this component + * @param myRole Role of the current user + * @since 3.0.0 + */ + public void notifyDataSetChanged(@NonNull List userList, @NonNull Role myRole) { + this.adapter.setItems(userList, myRole); + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/ChannelHeaderComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/ChannelHeaderComponent.java index 3163f12a..fb73b69a 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/ChannelHeaderComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/ChannelHeaderComponent.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; @@ -153,7 +152,6 @@ public void setUseProfileImage(boolean useProfileImage) { * * @return true if the typing indicator is used, false otherwise */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseTypingIndicator() { return useTypingIndicator; } @@ -163,7 +161,6 @@ public boolean shouldUseTypingIndicator() { * * @return true if the profile image is used, false otherwise */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseProfileImage() { return useProfileImage; } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/CreateChannelUserListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/CreateChannelUserListComponent.java index 9fa564b9..ca76b592 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/CreateChannelUserListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/CreateChannelUserListComponent.java @@ -1,7 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; - import androidx.annotation.NonNull; import com.sendbird.uikit.activities.adapter.CreateChannelUserListAdapter; @@ -22,7 +20,6 @@ public class CreateChannelUserListComponent extends SelectUserListComponenttrue if the right button of the header is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public void setUseRightButton(boolean useRightButton) { this.useRightButton = useRightButton; } @@ -225,7 +223,6 @@ public void setUseRightButton(boolean useRightButton) { * @param useLeftButton true if the left button of the header is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public void setUseLeftButton(boolean useLeftButton) { this.useLeftButton = useLeftButton; } @@ -289,7 +286,6 @@ public Drawable getRightButtonIcon() { * @return true if the right button of the header is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseRightButton() { return useRightButton; } @@ -300,7 +296,6 @@ public boolean shouldUseRightButton() { * @return true if the left button of the header is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseLeftButton() { return useLeftButton; } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/InviteUserListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/InviteUserListComponent.java index 6af88dd4..ab8fd913 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/InviteUserListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/InviteUserListComponent.java @@ -1,7 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; - import androidx.annotation.NonNull; import com.sendbird.uikit.activities.adapter.InviteUserListAdapter; @@ -34,7 +32,6 @@ public void setAdapter(@NonNull T adapter) { * @return The adapter applied to this list component * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") @NonNull @Override protected InviteUserListAdapter getAdapter() { diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/MemberListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/MemberListComponent.java index ebcff799..a562bcb4 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/MemberListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/MemberListComponent.java @@ -1,12 +1,13 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; - import androidx.annotation.NonNull; +import com.sendbird.android.channel.Role; import com.sendbird.android.user.Member; import com.sendbird.uikit.activities.adapter.MemberListAdapter; +import java.util.List; + /** * This class creates and performs a view corresponding the member list area in Sendbird UIKit. * @@ -22,7 +23,6 @@ public class MemberListComponent extends UserTypeListComponent { * @return The adapter applied to this list component * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") @NonNull @Override protected MemberListAdapter getAdapter() { @@ -40,4 +40,15 @@ public void setAdapter(@NonNull T adapter) { this.adapter = adapter; super.setAdapter(this.adapter); } + + /** + * Notifies this component that the list of users is changed. + * + * @param userList The list of users to be displayed on this component + * @param myRole Role of the current user + * @since 3.0.0 + */ + public void notifyDataSetChanged(@NonNull List userList, @NonNull Role myRole) { + this.adapter.setItems(userList, myRole); + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/MessageInputComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/MessageInputComponent.java index e3c156d7..25d282af 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/MessageInputComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/MessageInputComponent.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Typeface; @@ -646,7 +645,6 @@ public KeyboardDisplayType getKeyboardDisplayType() { * @return true if the left button of the input view is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseLeftButton() { return useLeftButton; } 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 7638de39..51bab06a 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 @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; @@ -791,7 +790,6 @@ public void setUseUserProfile(boolean useUserProfile) { * @return true if the user profile is shown, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseUserProfile() { return useUserProfile; } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/ModerationListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/ModerationListComponent.java index a563ffd1..3480908f 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/ModerationListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/ModerationListComponent.java @@ -143,7 +143,7 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla this.operators.setMenuType(SingleMenuItemView.Type.NEXT); this.operators.setIcon(R.drawable.icon_operator); - this.operators.setName(listThemeContext.getString(R.string.sb_text_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)); @@ -158,7 +158,7 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla this.bannedMembers.setMenuType(SingleMenuItemView.Type.NEXT); this.bannedMembers.setIcon(R.drawable.icon_ban); - this.bannedMembers.setName(listThemeContext.getString(R.string.sb_text_menu_banned_members)); + 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)); diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/MutedMemberListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/MutedMemberListComponent.java index c9a0ad35..6bc95171 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/MutedMemberListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/MutedMemberListComponent.java @@ -1,12 +1,13 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; - import androidx.annotation.NonNull; +import com.sendbird.android.channel.Role; import com.sendbird.android.user.Member; import com.sendbird.uikit.activities.adapter.MutedMemberListAdapter; +import java.util.List; + /** * This class creates and performs a view corresponding the muted member list area in Sendbird UIKit. * @@ -34,10 +35,20 @@ public void setAdapter(@NonNull T adapter) { * @return The adapter applied to this list component * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") @NonNull @Override protected MutedMemberListAdapter getAdapter() { return adapter; } + + /** + * Notifies this component that the list of users is changed. + * + * @param userList The list of users to be displayed on this component + * @param myRole Role of the current user + * @since 3.0.0 + */ + public void notifyDataSetChanged(@NonNull List userList, @NonNull Role myRole) { + this.adapter.setItems(userList, myRole); + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelBannedUserListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelBannedUserListComponent.java new file mode 100644 index 00000000..68cb49b2 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelBannedUserListComponent.java @@ -0,0 +1,63 @@ +package com.sendbird.uikit.modules.components; + +import androidx.annotation.NonNull; + +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.activities.adapter.OpenChannelBannedUserListAdapter; + +import java.util.List; + +/** + * This class creates and performs a view corresponding the banned user list area in Sendbird UIKit. + * + * @since 3.1.0 + */ +public class OpenChannelBannedUserListComponent extends UserTypeListComponent { + @NonNull + private OpenChannelBannedUserListAdapter adapter = new OpenChannelBannedUserListAdapter(); + + /** + * Constructor + * + * @since 3.1.0 + */ + public OpenChannelBannedUserListComponent() { + super(); + } + + /** + * Sets the banned user list adapter to provide child views on demand. The default is {@code new OpenChannelBannedUserListAdapter()}. + *

When adapter is changed, all existing views are recycled back to the pool. If the pool has only one adapter, it will be cleared.

+ * + * @param adapter The adapter to be applied to this list component + * @since 3.1.0 + */ + public void setAdapter(@NonNull T adapter) { + this.adapter = adapter; + super.setAdapter(this.adapter); + } + + /** + * Returns the banned user list adapter. + * + * @return The adapter applied to this list component + * @since 3.1.0 + */ + @NonNull + @Override + protected OpenChannelBannedUserListAdapter getAdapter() { + return this.adapter; + } + + /** + * Notifies this component that the list of users is changed. + * + * @param userList The list of users to be displayed on this component + * @param openChannel The latest open channel + * @since 3.1.0 + */ + public void notifyDataSetChanged(@NonNull List userList, @NonNull OpenChannel openChannel) { + this.adapter.setItems(userList, openChannel); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelHeaderComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelHeaderComponent.java index f0aa23a9..8e94e818 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelHeaderComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelHeaderComponent.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; @@ -148,7 +147,6 @@ public String getDescription() { * @return true if the profile image is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean useProfileImage() { return useProfileImage; } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMessageInputComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMessageInputComponent.java index f237c959..1b0d4bda 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMessageInputComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMessageInputComponent.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; @@ -52,6 +51,7 @@ public class OpenChannelMessageInputComponent { private OnInputModeChangedListener inputModeChangedListener; @Nullable private CharSequence hintText; + private boolean isMuted; /** * Constructor @@ -309,7 +309,7 @@ protected void onEditModeSaveButtonClicked(@NonNull View view) { /** * Notifies this component that the channel data has changed. * - * @param channel The latest group channel + * @param channel The latest open channel * @since 3.0.0 */ public void notifyChannelChanged(@NonNull OpenChannel channel) { @@ -322,7 +322,7 @@ public void notifyChannelChanged(@NonNull OpenChannel channel) { * Notifies this component that the data needed to draw the input has changed. * * @param message Message required for current input information - * @param channel The latest group channel + * @param channel The latest open channel * @since 3.0.0 */ public void notifyDataChanged(@Nullable BaseMessage message, @NonNull OpenChannel channel) { @@ -333,7 +333,7 @@ public void notifyDataChanged(@Nullable BaseMessage message, @NonNull OpenChanne * Notifies this component that the data needed to draw the input has changed. * * @param message Message required for current input information - * @param channel The latest group channel + * @param channel The latest open channel * @param defaultText Text set as initial value for input * @since 3.0.0 */ @@ -368,15 +368,31 @@ public void requestInputMode(@NonNull MessageInputView.Mode mode) { this.messageInputView.setInputMode(mode); } + /** + * Notifies this component that the muted state of the current user has changed. + * + * @param channel The latest open channel + * @param isMuted Whether the current user is muted or not + * @since 3.1.0 + */ + public void notifyMyMutedStateChanged(@NonNull OpenChannel channel, boolean isMuted) { + this.isMuted = isMuted; + if (messageInputView == null) return; + final MessageInputView inputView = this.messageInputView; + setHintMessageText(inputView, channel); + } + private void setHintMessageText(@NonNull MessageInputView inputView, @NonNull OpenChannel channel) { boolean isOperator = channel.isOperator(SendbirdChat.getCurrentUser()); boolean isFrozen = channel.isFrozen() && !isOperator; - inputView.setEnabled(!isFrozen); + inputView.setEnabled(!isMuted && !isFrozen); // set hint final Context context = inputView.getContext(); String hintText = this.hintText != null ? this.hintText.toString() : null; - if (isFrozen) { + if (isMuted) { + hintText = context.getString(R.string.sb_text_channel_input_text_hint_muted); + } else if (isFrozen) { hintText = context.getString(R.string.sb_text_channel_input_text_hint_frozen); } Logger.dev("++ hint text : " + hintText); @@ -527,7 +543,6 @@ public KeyboardDisplayType getKeyboardDisplayType() { * @return true if the left button of the input view is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseLeftButton() { return useLeftButton; } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMessageListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMessageListComponent.java index 8a55a19e..105d50f1 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMessageListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMessageListComponent.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; @@ -498,7 +497,6 @@ public void setEditedTextMarkUIConfig(@Nullable TextUIConfig configSentFromMe, @ * @return true if the user profile is shown, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseUserProfile() { return useUserProfile; } 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 new file mode 100644 index 00000000..b2c807ca --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelModerationListComponent.java @@ -0,0 +1,211 @@ +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.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.core.widget.NestedScrollView; + +import com.sendbird.uikit.R; +import com.sendbird.uikit.interfaces.OnMenuItemClickListener; +import com.sendbird.uikit.widgets.SingleMenuItemView; + +/** + * This class creates and performs a view corresponding the moderation list area in Sendbird UIKit. + * + * @since 3.1.0 + */ +public class OpenChannelModerationListComponent { + + /** + * Represents all moderation menus for open channel. + * + * @since 3.1.0 + */ + public enum ModerationMenu { + /** + * Menu to administrate operators + */ + OPERATORS, + /** + * Menu to administrate muted participants + */ + MUTED_PARTICIPANTS, + /** + * Menu to administrate banned participants + */ + BANNED_PARTICIPANTS, + } + + @NonNull + private final Params params; + @Nullable + private NestedScrollView nestedScrollView; + + @Nullable + private OnMenuItemClickListener menuItemClickListener; + + @SuppressWarnings("FieldCanBeLocal") + @Nullable + private SingleMenuItemView operators; + @Nullable + private SingleMenuItemView mutedParticipants; + @SuppressWarnings("FieldCanBeLocal") + @Nullable + private SingleMenuItemView bannedParticipants; + + /** + * Constructor + * + * @since 3.1.0 + */ + public OpenChannelModerationListComponent() { + this.params = new Params(); + } + + /** + * Returns the nested scroll view used in this component. + * + * @return {@link NestedScrollView} that this component creates and performs by default + * @since 3.1.0 + */ + @Nullable + protected NestedScrollView getNestedScrollView() { + return this.nestedScrollView; + } + + /** + * Returns a collection of parameters applied to this component. + * + * @return {@code Params} applied to this component + * @since 3.1.0 + */ + @NonNull + public Params getParams() { + return params; + } + + /** + * Returns the view created by {@link #onCreateView(Context, LayoutInflater, ViewGroup, Bundle)}. + * + * @return the topmost view containing this view + * @since 3.1.0 + */ + @Nullable + public View getRootView() { + return this.nestedScrollView; + } + + /** + * Called after the component was created to make views. + *

If this function is used override, {@link #getRootView()} must also be override.

+ * + * @param context The {@code Context} this component is currently associated with + * @param inflater The LayoutInflater object that can be used to inflate any views in the component + * @param parent The ViewGroup into which the new View will be added + * @param args The arguments supplied when the component was instantiated, if any + * @return Return the View for the UI. + * @since 3.1.0 + */ + @NonNull + public View onCreateView(@NonNull Context context, @NonNull LayoutInflater inflater, @NonNull ViewGroup parent, @Nullable Bundle args) { + if (args != null) params.apply(context, args); + final TypedValue values = new TypedValue(); + context.getTheme().resolveAttribute(R.attr.sb_component_moderation_list, values, true); + final Context listThemeContext = new ContextThemeWrapper(context, values.resourceId); + + final NestedScrollView listView = new NestedScrollView(listThemeContext); + final LinearLayout innerLayout = new LinearLayout(listThemeContext); + innerLayout.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + innerLayout.setOrientation(LinearLayout.VERTICAL); + listView.addView(innerLayout); + final int height = listThemeContext.getResources().getDimensionPixelSize(R.dimen.sb_size_56); + final LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, height); + + this.operators = new SingleMenuItemView(listThemeContext); + this.mutedParticipants = new SingleMenuItemView(listThemeContext); + this.bannedParticipants = new SingleMenuItemView(listThemeContext); + + this.operators.setMenuType(SingleMenuItemView.Type.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.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.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); + this.bannedParticipants.setLayoutParams(layoutParams); + this.bannedParticipants.setOnClickListener(v -> onMenuItemClicked(v, ModerationMenu.BANNED_PARTICIPANTS)); + + innerLayout.addView(operators); + innerLayout.addView(mutedParticipants); + innerLayout.addView(bannedParticipants); + this.nestedScrollView = listView; + return listView; + } + + /** + * Register a callback to be invoked when the item of the menu is clicked. + * + * @param menuItemClickListener The callback that will run + * @since 3.1.0 + */ + public void setOnMenuItemClickListener(@Nullable OnMenuItemClickListener menuItemClickListener) { + this.menuItemClickListener = menuItemClickListener; + } + + /** + * Called when the item of the menu list is clicked. + * + * @param view The View clicked + * @param menu The menu that the clicked item displays + * @since 3.1.0 + */ + protected void onMenuItemClicked(@NonNull View view, @NonNull ModerationMenu menu) { + if (menuItemClickListener != null) + menuItemClickListener.onMenuItemClicked(view, menu, null); + } + + /** + * 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. + *

Since the onCreateView configuring View uses the values of the set Params, we recommend that you set up for Params before the onCreateView is called.

+ * + * @see #getParams() + * @since 3.1.0 + */ + public static class Params { + protected Params() { + } + + /** + * Apply data that matches keys mapped to Params' properties. + * + * @param context The {@code Context} this component is currently associated with + * @param args The sets of arguments to apply at Params. + * @return This Params object that applied with given data. + * @since 3.1.0 + */ + @NonNull + protected Params apply(@NonNull Context context, @NonNull Bundle args) { + return this; + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMutedParticipantListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMutedParticipantListComponent.java new file mode 100644 index 00000000..1e906c42 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelMutedParticipantListComponent.java @@ -0,0 +1,54 @@ +package com.sendbird.uikit.modules.components; + +import androidx.annotation.NonNull; + +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.activities.adapter.OpenChannelMutedParticipantListAdapter; + +import java.util.List; + +/** + * This class creates and performs a view corresponding the muted participant list area in Sendbird UIKit. + * + * @since 3.1.0 + */ +public class OpenChannelMutedParticipantListComponent extends UserTypeListComponent { + @NonNull + private OpenChannelMutedParticipantListAdapter adapter = new OpenChannelMutedParticipantListAdapter(); + + /** + * Sets the muted participant list adapter to provide child views on demand. The default is {@code new OpenChannelMutedParticipantListAdapter()}. + *

When adapter is changed, all existing views are recycled back to the pool. If the pool has only one adapter, it will be cleared.

+ * + * @param adapter The adapter to be applied to this list component + * @since 3.1.0 + */ + public void setAdapter(@NonNull T adapter) { + this.adapter = adapter; + super.setAdapter(this.adapter); + } + + /** + * Returns the muted participant list adapter. + * + * @return The adapter applied to this list component + * @since 3.1.0 + */ + @NonNull + @Override + protected OpenChannelMutedParticipantListAdapter getAdapter() { + return adapter; + } + + /** + * Notifies this component that the list of users is changed. + * + * @param userList The list of users to be displayed on this component + * @param openChannel The latest open channel + * @since 3.1.0 + */ + public void notifyDataSetChanged(@NonNull List userList, @NonNull OpenChannel openChannel) { + this.adapter.setItems(userList, openChannel); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelOperatorListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelOperatorListComponent.java new file mode 100644 index 00000000..f5a1a5c1 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelOperatorListComponent.java @@ -0,0 +1,54 @@ +package com.sendbird.uikit.modules.components; + +import androidx.annotation.NonNull; + +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.user.User; +import com.sendbird.uikit.activities.adapter.OpenChannelOperatorListAdapter; + +import java.util.List; + +/** + * This class creates and performs a view corresponding the operator list area in Sendbird UIKit. + * + * @since 3.1.0 + */ +public class OpenChannelOperatorListComponent extends UserTypeListComponent { + @NonNull + private OpenChannelOperatorListAdapter adapter = new OpenChannelOperatorListAdapter(); + + /** + * Returns the operator list adapter. + * + * @return The adapter applied to this list component + * @since 3.1.0 + */ + @NonNull + @Override + protected OpenChannelOperatorListAdapter getAdapter() { + return adapter; + } + + /** + * Sets the operator list adapter to provide child views on demand. The default is {@code new OpenChannelOperatorListAdapter()}. + *

When adapter is changed, all existing views are recycled back to the pool. If the pool has only one adapter, it will be cleared.

+ * + * @param adapter The adapter to be applied to this list component + * @since 3.1.0 + */ + public void setAdapter(@NonNull T adapter) { + this.adapter = adapter; + super.setAdapter(this.adapter); + } + + /** + * Notifies this component that the list of users is changed. + * + * @param userList The list of users to be displayed on this component + * @param openChannel The latest open channel + * @since 3.1.0 + */ + public void notifyDataSetChanged(@NonNull List userList, @NonNull OpenChannel openChannel) { + this.adapter.setItems(userList, openChannel); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelRegisterOperatorListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelRegisterOperatorListComponent.java new file mode 100644 index 00000000..45b9cded --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OpenChannelRegisterOperatorListComponent.java @@ -0,0 +1,40 @@ +package com.sendbird.uikit.modules.components; + +import androidx.annotation.NonNull; + +import com.sendbird.android.user.User; +import com.sendbird.uikit.activities.adapter.OpenChannelRegisterOperatorListAdapter; + +/** + * This class creates and performs a view corresponding the participant list area when registering operators in Sendbird UIKit. + * + * @since 3.1.0 + */ +public class OpenChannelRegisterOperatorListComponent extends SelectUserListComponent { + @NonNull + private OpenChannelRegisterOperatorListAdapter adapter = new OpenChannelRegisterOperatorListAdapter(null); + + /** + * Returns the participant list adapter when registering operators. + * + * @return The adapter applied to this list component + * @since 3.1.0 + */ + @NonNull + @Override + protected OpenChannelRegisterOperatorListAdapter getAdapter() { + return adapter; + } + + /** + * Sets the participant list adapter when registering operators to provide child views on demand. The default is {@code new RegisterOperatorListAdapter()}. + *

When adapter is changed, all existing views are recycled back to the pool. If the pool has only one adapter, it will be cleared.

+ * + * @param adapter The adapter to be applied to this list component + * @since 3.1.0 + */ + public void setAdapter(@NonNull T adapter) { + this.adapter = adapter; + super.setAdapter(this.adapter); + } +} 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 56b28404..d12f7097 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 @@ -12,6 +12,7 @@ import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.view.ContextThemeWrapper; +import com.sendbird.android.SendbirdChat; import com.sendbird.android.channel.OpenChannel; import com.sendbird.uikit.R; import com.sendbird.uikit.SendbirdUIKit; @@ -26,7 +27,20 @@ */ public class OpenChannelSettingsMenuComponent { public enum Menu { - PARTICIPANTS, DELETE_CHANNEL + /** + * A menu of Moderations to users or members control. + * + * @since 3.1.0 + */ + MODERATIONS, + /** + * A menu for viewing the participants of the current channel. + */ + PARTICIPANTS, + /** + * A menu to delete the current channel. + */ + DELETE_CHANNEL } @NonNull @@ -88,8 +102,14 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla final LayoutInflater menuInflater = inflater.cloneInContext(menuThemeContext); final View view = menuInflater.inflate(R.layout.sb_view_open_channel_settings_menu, parent, false); - SingleMenuItemView participantsItemView = view.findViewById(R.id.participants); - SingleMenuItemView deleteItemView = view.findViewById(R.id.delete); + 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); @@ -99,6 +119,7 @@ public View onCreateView(@NonNull Context context, @NonNull LayoutInflater infla 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; @@ -113,6 +134,10 @@ 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); + SingleMenuItemView participantsItemView = menuView.findViewById(R.id.participants); participantsItemView.setDescription(ChannelUtils.makeMemberCountText(channel.getParticipantCount()).toString()); } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/OperatorListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/OperatorListComponent.java index f5e334e6..18a0dfca 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/OperatorListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/OperatorListComponent.java @@ -1,12 +1,13 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; - import androidx.annotation.NonNull; +import com.sendbird.android.channel.Role; import com.sendbird.android.user.User; import com.sendbird.uikit.activities.adapter.OperatorListAdapter; +import java.util.List; + /** * This class creates and performs a view corresponding the operator list area in Sendbird UIKit. * @@ -23,7 +24,6 @@ public class OperatorListComponent extends UserTypeListComponent { * @return The adapter applied to this list component * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") @NonNull @Override protected OperatorListAdapter getAdapter() { @@ -41,4 +41,15 @@ public void setAdapter(@NonNull T adapter) { this.adapter = adapter; super.setAdapter(this.adapter); } + + /** + * Notifies this component that the list of users is changed. + * + * @param userList The list of users to be displayed on this component + * @param myRole Role of the current user + * @since 3.0.0 + */ + public void notifyDataSetChanged(@NonNull List userList, @NonNull Role myRole) { + this.adapter.setItems(userList, myRole); + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/ParticipantListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/ParticipantListComponent.java index 5c6673db..b5f190e6 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/ParticipantListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/ParticipantListComponent.java @@ -1,12 +1,13 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; - import androidx.annotation.NonNull; +import com.sendbird.android.channel.OpenChannel; import com.sendbird.android.user.User; import com.sendbird.uikit.activities.adapter.ParticipantListAdapter; +import java.util.List; + /** * This class creates and performs a view corresponding the participant list area in Sendbird UIKit. * @@ -22,7 +23,6 @@ public class ParticipantListComponent extends UserTypeListComponent { * @return The adapter applied to this list component * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") @NonNull @Override protected ParticipantListAdapter getAdapter() { @@ -40,4 +40,15 @@ public void setAdapter(@NonNull T adapter) { this.adapter = adapter; super.setAdapter(this.adapter); } + + /** + * Notifies this component that the list of users is changed. + * + * @param userList The list of users to be displayed on this component + * @param openChannel The latest open channel + * @since 3.1.0 + */ + public void notifyDataSetChanged(@NonNull List userList, @NonNull OpenChannel openChannel) { + this.adapter.setItems(userList, openChannel); + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/StateHeaderComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/StateHeaderComponent.java index 4d32fa71..e51f8b56 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/StateHeaderComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/StateHeaderComponent.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; import android.content.Context; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; @@ -269,7 +268,6 @@ public String getRightButtonText() { * @return true if the right button of the header is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseRightButton() { return useRightButton; } @@ -280,7 +278,6 @@ public boolean shouldUseRightButton() { * @return true if the left button of the header is used, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseLeftButton() { return useLeftButton; } diff --git a/uikit/src/main/java/com/sendbird/uikit/modules/components/UserTypeListComponent.java b/uikit/src/main/java/com/sendbird/uikit/modules/components/UserTypeListComponent.java index 67201337..ebe547a7 100644 --- a/uikit/src/main/java/com/sendbird/uikit/modules/components/UserTypeListComponent.java +++ b/uikit/src/main/java/com/sendbird/uikit/modules/components/UserTypeListComponent.java @@ -1,6 +1,5 @@ package com.sendbird.uikit.modules.components; -import android.annotation.SuppressLint; import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; @@ -11,7 +10,6 @@ import androidx.annotation.Nullable; import androidx.recyclerview.widget.LinearLayoutManager; -import com.sendbird.android.channel.Role; import com.sendbird.android.user.User; import com.sendbird.uikit.R; import com.sendbird.uikit.activities.adapter.UserTypeListAdapter; @@ -147,17 +145,6 @@ public void setPagedDataLoader(@NonNull OnPagedDataLoader> pagedDataLoad if (recyclerView != null) recyclerView.setPager(pagedDataLoader); } - /** - * Notifies this component that the list of users is changed. - * - * @param userList The list of users to be displayed on this component - * @param myRole Role of the current user - */ - public void notifyDataSetChanged(@NonNull List userList, @NonNull Role myRole) { - if (this.recyclerView == null) return; - getAdapter().setItems(userList, myRole); - } - /** * Called when the profile view of the item is clicked. * @@ -188,7 +175,7 @@ protected void onActionItemClicked(@NonNull View view, int position, @NonNull T * * @param view The View long-clicked. * @param position The position long-clicked. - * @param user The channel that the long-clicked item displays + * @param user The user that the long-clicked item displays * @since 3.0.0 */ protected void onItemLongClicked(@NonNull View view, int position, @NonNull T user) { @@ -277,7 +264,6 @@ public void setUseUserProfile(boolean useUserProfile) { * @return true if the user profile is shown, false otherwise * @since 3.0.0 */ - @SuppressLint("KotlinPropertyAccess") public boolean shouldUseUserProfile() { return useUserProfile; } diff --git a/uikit/src/main/java/com/sendbird/uikit/utils/DialogUtils.java b/uikit/src/main/java/com/sendbird/uikit/utils/DialogUtils.java index 9f5264f7..32d5bd60 100644 --- a/uikit/src/main/java/com/sendbird/uikit/utils/DialogUtils.java +++ b/uikit/src/main/java/com/sendbird/uikit/utils/DialogUtils.java @@ -153,6 +153,29 @@ public static AlertDialog showWarningDialog(@NonNull Context context, @NonNull public static AlertDialog showWarningDialog(@NonNull Context context, @NonNull String title, + @NonNull String message, + @NonNull String warningButtonText, + @Nullable View.OnClickListener warningButtonListener, + @NonNull String negativeButtonText, + @Nullable View.OnClickListener negativeButtonListener) { + return showWarningDialog(context, title, message, warningButtonText, warningButtonListener, negativeButtonText, negativeButtonListener, false); + } + + @NonNull + public static AlertDialog showWarningDialog(@NonNull Context context, + @NonNull String title, + @NonNull String warningButtonText, + @Nullable View.OnClickListener warningButtonListener, + @NonNull String negativeButtonText, + @Nullable View.OnClickListener negativeButtonListener, + boolean useOverlay) { + return showWarningDialog(context, title, "", warningButtonText, warningButtonListener, negativeButtonText, negativeButtonListener, useOverlay); + } + + @NonNull + public static AlertDialog showWarningDialog(@NonNull Context context, + @NonNull String title, + @NonNull String message, @NonNull String warningButtonText, @Nullable View.OnClickListener warningButtonListener, @NonNull String negativeButtonText, @@ -174,6 +197,7 @@ public static AlertDialog showWarningDialog(@NonNull Context context, final Context themeWrapperContext = new ContextThemeWrapper(context, themeResId); final DialogView dialogView = new DialogView(themeWrapperContext); dialogView.setTitle(title); + dialogView.setMessage(message); final AlertDialog.Builder builder = new AlertDialog.Builder(context, R.style.Sendbird_Dialog); builder.setView(dialogView); diff --git a/uikit/src/main/java/com/sendbird/uikit/utils/IntentUtils.java b/uikit/src/main/java/com/sendbird/uikit/utils/IntentUtils.java index 14b0a6c2..480c46ec 100644 --- a/uikit/src/main/java/com/sendbird/uikit/utils/IntentUtils.java +++ b/uikit/src/main/java/com/sendbird/uikit/utils/IntentUtils.java @@ -44,14 +44,24 @@ public static Intent getGalleryIntent() { @NonNull private static Intent getGalleryIntent(@NonNull String[] mimetypes) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_GET_CONTENT); - intent.setType("*/*"); - intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes); - intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); - return Intent.createChooser(intent, null); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + Intent intent = new Intent(); + intent.setAction(Intent.ACTION_GET_CONTENT); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_MIME_TYPES, mimetypes); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); + return Intent.createChooser(intent, null); + } + + Intent intent = new Intent(MediaStore.ACTION_PICK_IMAGES); + if (mimetypes.length == 1) { + // ACTION_PICK_IMAGES supports only two mimetypes. + intent.setType(mimetypes[0]); + } + return intent; } + @NonNull public static Intent getFileChooserIntent() { Intent intent = new Intent(); diff --git a/uikit/src/main/java/com/sendbird/uikit/utils/PermissionUtils.java b/uikit/src/main/java/com/sendbird/uikit/utils/PermissionUtils.java index 58b833c3..a4d8377e 100644 --- a/uikit/src/main/java/com/sendbird/uikit/utils/PermissionUtils.java +++ b/uikit/src/main/java/com/sendbird/uikit/utils/PermissionUtils.java @@ -1,25 +1,56 @@ package com.sendbird.uikit.utils; +import android.Manifest; import android.app.Activity; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Build; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; -import androidx.core.content.PermissionChecker; +import androidx.core.content.ContextCompat; + +import com.sendbird.uikit.log.Logger; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.Map; +import java.util.Set; public class PermissionUtils { + public final static String[] CAMERA_PERMISSION = getCameraPermission(); + public final static String[] GET_CONTENT_PERMISSION = getGetContentPermission(); + private PermissionUtils() { } + private static String[] getCameraPermission() { + String[] permissions = new String[]{Manifest.permission.CAMERA, + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.READ_EXTERNAL_STORAGE}; + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + permissions = new String[]{Manifest.permission.CAMERA}; + } + return permissions; + } + + private static String[] getGetContentPermission() { + String[] permissions = new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissions = new String[] {}; + } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + permissions = new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}; + } + return permissions; + } + public static boolean hasPermissions(@NonNull Context context, @NonNull String... permissions) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { for (String permission : permissions) { - if (PermissionChecker.checkSelfPermission(context, permission) != PermissionChecker.PERMISSION_GRANTED) { + if (ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { return false; } } @@ -29,7 +60,7 @@ public static boolean hasPermissions(@NonNull Context context, @NonNull String.. @NonNull public static String[] getNotGrantedPermissions(@NonNull Context context, @NonNull String... permissions) { - ArrayList notGrantedPermissions = new ArrayList<>(); + List notGrantedPermissions = new ArrayList<>(); for (String perm : permissions) { if (!PermissionUtils.hasPermissions(context, perm)) { notGrantedPermissions.add(perm); @@ -39,15 +70,31 @@ public static String[] getNotGrantedPermissions(@NonNull Context context, @NonNu } @NonNull - public static List getShowRequestPermissionRationale(@NonNull Activity activity, @NonNull String... permissions) { + public static List getNotGrantedPermissions(@NonNull Map permissionResults) { + final Set keys = permissionResults.keySet(); + final List notGrantedList = new ArrayList<>(); + for (String permission : keys) { + Logger.d("permissionResults.get(%s) : %s", permission, permissionResults.get(permission)); + if (Boolean.FALSE.equals(permissionResults.get(permission))) { + notGrantedList.add(permission); + } + } + return notGrantedList; + } + + @NonNull + public static List getExplicitDeniedPermissionList(@NonNull Activity activity, @NonNull Collection permissions) { List result = new ArrayList<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - for (String permission : permissions) { - if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { - result.add(permission); - } + for (String permission : permissions) { + if (ActivityCompat.shouldShowRequestPermissionRationale(activity, permission)) { + result.add(permission); } } return result; } + + @NonNull + public static List getExplicitDeniedPermissionList(@NonNull Activity activity, @NonNull String... permissions) { + return getExplicitDeniedPermissionList(activity, Arrays.asList(permissions)); + } } 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 bcfee515..5c009da0 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/ChannelViewModel.java @@ -86,8 +86,8 @@ public class ChannelViewModel extends BaseViewModel implements OnPagedDataLoader private final MutableLiveData statusFrame = new MutableLiveData<>(); @NonNull private final MutableLiveData hugeGapDetected = new MutableLiveData<>(); - @NonNull - private final MessageListParams messageListParams; + @Nullable + private MessageListParams messageListParams; @Nullable private GroupChannel channel; @NonNull @@ -150,8 +150,7 @@ public ChannelViewModel(@NonNull String channelUrl, @Nullable MessageListParams super(); this.channel = null; this.channelUrl = channelUrl; - this.messageListParams = messageListParams == null ? createMessageListParams() : messageListParams; - this.messageListParams.setReverse(true); + this.messageListParams = messageListParams; this.memberFinder = new MemberFinder(channelUrl, SendbirdUIKit.getUserMentionConfig()); SendbirdChat.addChannelHandler(ID_CHANNEL_EVENT_HANDLER, new GroupChannelHandler() { @@ -218,7 +217,11 @@ private synchronized void initMessageCollection(final long startingPoint) { if (this.collection != null) { disposeMessageCollection(); } - this.collection = SendbirdChat.createMessageCollection(new MessageCollectionCreateParams(channel, messageListParams, startingPoint, new MessageCollectionHandler() { + if (this.messageListParams == null) { + this.messageListParams = createMessageListParams(); + } + this.messageListParams.setReverse(true); + this.collection = SendbirdChat.createMessageCollection(new MessageCollectionCreateParams(channel, this.messageListParams, startingPoint, new MessageCollectionHandler() { @UiThread @Override public void onMessagesAdded(@NonNull MessageContext context, @NonNull GroupChannel channel, @NonNull List messages) { @@ -452,7 +455,7 @@ public String getChannelUrl() { * @return {@link MessageListParams} used in this view model * @since 3.0.0 */ - @NonNull + @Nullable public MessageListParams getMessageListParams() { return messageListParams; } diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/MessageSearchViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/MessageSearchViewModel.java index e901f9c7..284f651b 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/MessageSearchViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/MessageSearchViewModel.java @@ -82,7 +82,6 @@ public String getChannelUrl() { */ @NonNull protected MessageSearchQuery createMessageSearchQuery(@NonNull String keyword) { - final long timestampFrom = channel == null ? 0 : Math.max(channel.getJoinedAt(), channel.getInvitedAt()); final MessageSearchQueryParams params = new MessageSearchQueryParams(keyword); if (query != null) { params.setAdvancedQuery(query.isAdvancedQuery()); @@ -92,11 +91,13 @@ protected MessageSearchQuery createMessageSearchQuery(@NonNull String keyword) { params.setMessageTimestampTo(query.getMessageTimestampTo()); params.setTargetFields(query.getTargetFields()); params.setOrder(query.getOrder()); + params.setMessageTimestampFrom(query.getMessageTimestampFrom()); } else { + final long timestampFrom = channel == null ? 0 : Math.max(channel.getJoinedAt(), channel.getInvitedAt()); + params.setMessageTimestampFrom(timestampFrom); params.setOrder(MessageSearchQuery.Order.TIMESTAMP); } params.setChannelUrl(channelUrl); - params.setMessageTimestampFrom(timestampFrom); params.setReverse(false); return SendbirdChat.createMessageSearchQuery(params); } diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/ModerationViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/ModerationViewModel.java index 42fb8697..1a415006 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/ModerationViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/ModerationViewModel.java @@ -187,7 +187,7 @@ public LiveData getIsChannelDeleted() { /** * Returns LiveData that can be observed whether the current user is banned. * - * @return LiveData holding the URL of the deleted channel + * @return LiveData holding the current user banned or not * @since 3.0.0 */ @NonNull diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelBannedUserListViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelBannedUserListViewModel.java new file mode 100644 index 00000000..c0bb8e57 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelBannedUserListViewModel.java @@ -0,0 +1,38 @@ +package com.sendbird.uikit.vm; + +import androidx.annotation.NonNull; + +import com.sendbird.android.channel.ChannelType; +import com.sendbird.android.user.User; +import com.sendbird.uikit.interfaces.PagedQueryHandler; +import com.sendbird.uikit.vm.queries.BannedUserListQuery; + +/** + * ViewModel preparing and managing data related with the list of banned users + * + * @since 3.1.0 + */ +public class OpenChannelBannedUserListViewModel extends OpenChannelUserViewModel { + /** + * Constructor + * + * @param channelUrl The URL of a channel this view model is currently associated with + * @since 3.1.0 + */ + public OpenChannelBannedUserListViewModel(@NonNull String channelUrl) { + super(channelUrl); + } + + /** + * Creates banned user list query. + * + * @param channelUrl The url of {@code OpenChannel} with banned users to be fetched by the query + * @return {@code PagedQueryHandler} to retrieve the list of users who are banned + * @since 3.1.0 + */ + @NonNull + @Override + protected PagedQueryHandler createQueryHandler(@NonNull String channelUrl) { + return new BannedUserListQuery(ChannelType.OPEN, channelUrl); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelModerationViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelModerationViewModel.java new file mode 100644 index 00000000..83067f26 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelModerationViewModel.java @@ -0,0 +1,168 @@ +package com.sendbird.uikit.vm; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.BaseChannel; +import com.sendbird.android.channel.ChannelType; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.handler.OpenChannelHandler; +import com.sendbird.android.message.BaseMessage; +import com.sendbird.android.user.RestrictedUser; +import com.sendbird.android.user.User; +import com.sendbird.uikit.interfaces.AuthenticateHandler; +import com.sendbird.uikit.log.Logger; + +/** + * ViewModel preparing and managing data related with the push setting of a channel + * + * @since 3.1.0 + */ +public class OpenChannelModerationViewModel extends BaseViewModel { + @NonNull + private final String CHANNEL_HANDLER_OPEN_CHANNEL_MODERATION = "CHANNEL_HANDLER_OPEN_CHANNEL_MODERATION" + System.currentTimeMillis(); + @NonNull + private final String channelUrl; + @Nullable + private OpenChannel channel; + @NonNull + private final MutableLiveData currentUserRegisteredOperator = new MutableLiveData<>(); + @NonNull + private final MutableLiveData channelDeleted = new MutableLiveData<>(); + @NonNull + private final MutableLiveData currentUserBanned = new MutableLiveData<>(); + + /** + * Constructor + * + * @param channelUrl The URL of a channel this view model is currently associated with + * @since 3.1.0 + */ + public OpenChannelModerationViewModel(@NonNull String channelUrl) { + super(); + this.channelUrl = channelUrl; + SendbirdChat.addChannelHandler(CHANNEL_HANDLER_OPEN_CHANNEL_MODERATION, new OpenChannelHandler() { + @Override + public void onMessageReceived(@NonNull BaseChannel baseChannel, @NonNull BaseMessage baseMessage) {} + + @Override + public void onChannelDeleted(@NonNull String channelUrl, @NonNull ChannelType channelType) { + if (isCurrentChannel(channelUrl)) { + Logger.i(">> OpenChannelModerationViewModel::onChannelDeleted()"); + Logger.d("++ deleted channel url : " + channelUrl); + OpenChannelModerationViewModel.this.channelDeleted.setValue(channelUrl); + } + } + + @Override + public void onOperatorUpdated(@NonNull BaseChannel channel) { + if (isCurrentChannel(channel.getUrl()) && channel instanceof OpenChannel) { + Logger.i(">> OpenChannelModerationViewModel::onOperatorUpdated()"); + OpenChannelModerationViewModel.this.currentUserRegisteredOperator.setValue(((OpenChannel) channel).isOperator(SendbirdChat.getCurrentUser())); + } + } + + @Override + public void onUserBanned(@NonNull BaseChannel channel, @NonNull RestrictedUser restrictedUser) { + final User currentUser = SendbirdChat.getCurrentUser(); + if (isCurrentChannel(channel.getUrl()) && currentUser != null) { + Logger.i(">> OpenChannelModerationViewModel::onUserBanned()"); + OpenChannelModerationViewModel.this.currentUserBanned.setValue(restrictedUser.getUserId().equals(currentUser.getUserId())); + } + } + }); + } + + /** + * Tries to connect Sendbird Server and retrieve a channel instance. + * + * @param handler Callback notifying the result of authentication + * @since 3.1.0 + */ + @Override + public void authenticate(@NonNull AuthenticateHandler handler) { + connect((user, e) -> { + if (user != null) { + OpenChannel.getChannel(channelUrl, (channel, e1) -> { + OpenChannelModerationViewModel.this.channel = channel; + if (e1 != null) { + handler.onAuthenticationFailed(); + } else { + handler.onAuthenticated(); + } + }); + } else { + handler.onAuthenticationFailed(); + } + }); + } + + @Override + protected void onCleared() { + super.onCleared(); + SendbirdChat.removeChannelHandler(CHANNEL_HANDLER_OPEN_CHANNEL_MODERATION); + } + + /** + * Returns LiveData that can be observed whether the current user is banned. + * + * @return LiveData holding whether the current user is banned or not + * @since 3.1.0 + */ + @NonNull + public LiveData getCurrentUserBanned() { + return currentUserBanned; + } + + /** + * Returns LiveData that can be observed if the current user is operator or not. + * + * @return LiveData holding whether the current user is registered as operator or not + * @since 3.1.0 + */ + @NonNull + public LiveData getCurrentUserRegisteredOperator() { + return currentUserRegisteredOperator; + } + + /** + * Returns LiveData that can be observed if the channel has been deleted. + * + * @return LiveData holding the URL of the deleted channel + * @since 3.1.0 + */ + @NonNull + public LiveData getChannelDeleted() { + return channelDeleted; + } + + /** + * Returns {@code OpenChannel}. If the authentication failed, {@code null} is returned. + * + * @return {@code OpenChannel} this view model is currently associated with + * @since 3.1.0 + */ + @Nullable + public OpenChannel getChannel() { + return channel; + } + + /** + * Returns URL of OpenChannel. + * + * @return The URL of a channel this view model is currently associated with + * @since 3.1.0 + */ + @NonNull + public String getChannelUrl() { + return channelUrl; + } + + private boolean isCurrentChannel(@NonNull String channelUrl) { + if (channel == null) return false; + return channelUrl.equals(channel.getUrl()); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelMutedParticipantListViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelMutedParticipantListViewModel.java new file mode 100644 index 00000000..ea752457 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelMutedParticipantListViewModel.java @@ -0,0 +1,40 @@ +package com.sendbird.uikit.vm; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.sendbird.android.channel.ChannelType; +import com.sendbird.android.user.User; +import com.sendbird.uikit.interfaces.PagedQueryHandler; +import com.sendbird.uikit.vm.queries.MutedUserListQuery; + +/** + * ViewModel preparing and managing data related with the list of muted participants + * + * @since 3.1.0 + */ +public class OpenChannelMutedParticipantListViewModel extends OpenChannelUserViewModel { + /** + * Constructor + * + * @param channelUrl The URL of a channel this view model is currently associated with + * @param queryHandler A callback to be invoked when a list of data is loaded. + * @since 3.1.0 + */ + public OpenChannelMutedParticipantListViewModel(@NonNull String channelUrl, @Nullable PagedQueryHandler queryHandler) { + super(channelUrl, queryHandler); + } + + /** + * Creates muted participant list query. + * + * @param channelUrl The url of {@code OpenChannel} with muted participants to be fetched by the query + * @return {@code PagedQueryHandler} to retrieve the list of participants who are muted + * @since 3.1.0 + */ + @NonNull + @Override + protected PagedQueryHandler createQueryHandler(@NonNull String channelUrl) { + return new MutedUserListQuery(ChannelType.OPEN, channelUrl); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelOperatorListViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelOperatorListViewModel.java new file mode 100644 index 00000000..1eef1939 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelOperatorListViewModel.java @@ -0,0 +1,40 @@ +package com.sendbird.uikit.vm; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.sendbird.android.channel.ChannelType; +import com.sendbird.android.user.User; +import com.sendbird.uikit.interfaces.PagedQueryHandler; +import com.sendbird.uikit.vm.queries.OperatorListQuery; + +/** + * ViewModel preparing and managing data related with the list of operators + * + * @since 3.1.0 + */ +public class OpenChannelOperatorListViewModel extends OpenChannelUserViewModel { + /** + * Constructor + * + * @param channelUrl The URL of a channel this view model is currently associated with + * @param queryHandler A callback to be invoked when a list of data is loaded. + * @since 3.1.0 + */ + public OpenChannelOperatorListViewModel(@NonNull String channelUrl, @Nullable PagedQueryHandler queryHandler) { + super(channelUrl, queryHandler); + } + + /** + * Creates operator list query. + * + * @param channelUrl The url of {@code OpenChannel} with operators to be fetched by the query + * @return {@code PagedQueryHandler} to retrieve the list of operators + * @since 3.1.0 + */ + @NonNull + @Override + protected PagedQueryHandler createQueryHandler(@NonNull String channelUrl) { + return new OperatorListQuery(ChannelType.OPEN, channelUrl); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelRegisterOperatorViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelRegisterOperatorViewModel.java new file mode 100644 index 00000000..daeb4266 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelRegisterOperatorViewModel.java @@ -0,0 +1,40 @@ +package com.sendbird.uikit.vm; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.sendbird.android.user.User; +import com.sendbird.uikit.interfaces.PagedQueryHandler; +import com.sendbird.uikit.vm.queries.ParticipantsListQuery; + +/** + * ViewModel preparing and managing data related with the push setting of a channel + * + * @since 3.1.0 + */ +public class OpenChannelRegisterOperatorViewModel extends OpenChannelUserViewModel { + + /** + * Constructor + * + * @param channelUrl The URL of a channel this view model is currently associated with + * @param queryHandler A callback to be invoked when a list of data is loaded. + * @since 3.1.0 + */ + public OpenChannelRegisterOperatorViewModel(@NonNull String channelUrl, @Nullable PagedQueryHandler queryHandler) { + super(channelUrl, queryHandler); + } + + /** + * Creates the list query of users who can be operator. + * + * @param channelUrl The url of {@code OpenChannel} with users to be fetched by the query + * @return {@code PagedQueryHandler} to retrieve the list of operators + * @since 3.1.0 + */ + @NonNull + @Override + protected PagedQueryHandler createQueryHandler(@NonNull String channelUrl) { + return new ParticipantsListQuery(channelUrl); + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelUserViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelUserViewModel.java new file mode 100644 index 00000000..d5b0e27f --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelUserViewModel.java @@ -0,0 +1,585 @@ +package com.sendbird.uikit.vm; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.BaseChannel; +import com.sendbird.android.channel.ChannelType; +import com.sendbird.android.channel.OpenChannel; +import com.sendbird.android.exception.SendbirdException; +import com.sendbird.android.handler.ConnectionHandler; +import com.sendbird.android.handler.OpenChannelHandler; +import com.sendbird.android.message.BaseMessage; +import com.sendbird.android.user.RestrictedUser; +import com.sendbird.android.user.User; +import com.sendbird.uikit.interfaces.AuthenticateHandler; +import com.sendbird.uikit.interfaces.OnCompleteHandler; +import com.sendbird.uikit.interfaces.OnPagedDataLoader; +import com.sendbird.uikit.interfaces.PagedQueryHandler; +import com.sendbird.uikit.log.Logger; +import com.sendbird.uikit.utils.TextUtils; +import com.sendbird.uikit.widgets.StatusFrameView; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * ViewModel preparing and managing data related with the list of users + * + * @since 3.1.0 + */ +public abstract class OpenChannelUserViewModel extends BaseViewModel implements OnPagedDataLoader> { + @NonNull + private final String CONNECTION_HANDLER_ID = getClass().getName() + System.currentTimeMillis(); + @NonNull + private final String OPEN_CHANNEL_HANDLER_USER_LIST = "OPEN_CHANNEL_HANDLER_USER_LIST" + System.currentTimeMillis(); + @NonNull + private final MutableLiveData statusFrame = new MutableLiveData<>(); + @NonNull + private final MutableLiveData> userList = new MutableLiveData<>(); + @NonNull + private final MutableLiveData channelDeleted = new MutableLiveData<>(); + @NonNull + private final MutableLiveData operatorUpdated = new MutableLiveData<>(); + @NonNull + private final MutableLiveData userBanned = new MutableLiveData<>(); + @NonNull + private final MutableLiveData userUnbanned = new MutableLiveData<>(); + @NonNull + private final MutableLiveData userMuted = new MutableLiveData<>(); + @NonNull + private final MutableLiveData userUnmuted = new MutableLiveData<>(); + @NonNull + private final String channelUrl; + @Nullable + private PagedQueryHandler query; + @Nullable + private OpenChannel channel; + private volatile boolean isInitialRequest = false; + @NonNull + private final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); + @Nullable + private Future currentFuture; + + /** + * Constructor + * + * @param channelUrl The URL of a channel this view model is currently associated with + * @since 3.1.0 + */ + public OpenChannelUserViewModel(@NonNull String channelUrl) { + this(channelUrl, null); + } + + /** + * Constructor + * + * @param channelUrl The URL of a channel this view model is currently associated with + * @param queryHandler A callback to be invoked when a list of data is loaded. + * @since 3.1.0 + */ + public OpenChannelUserViewModel(@NonNull String channelUrl, @Nullable PagedQueryHandler queryHandler) { + super(); + this.channelUrl = channelUrl; + this.query = queryHandler; + registerChannelHandler(); + } + + private void registerChannelHandler() { + SendbirdChat.addChannelHandler(OPEN_CHANNEL_HANDLER_USER_LIST, new OpenChannelHandler() { + @Override + public void onMessageReceived(@NonNull BaseChannel baseChannel, @NonNull BaseMessage baseMessage) {} + + @Override + public void onChannelDeleted(@NonNull String channelUrl, @NonNull ChannelType channelType) { + if (!isCurrentChannel(channelUrl)) return; + Logger.i(">> OpenChannelUserViewModel::onChannelDeleted()"); + channelDeleted.postValue(true); + } + + @Override + public void onOperatorUpdated(@NonNull BaseChannel channel) { + if (!isCurrentChannel(channel.getUrl())) return; + Logger.i(">> OpenChannelUserViewModel::onOperatorUpdated()"); + operatorUpdated.postValue((OpenChannel) channel); + } + + @Override + public void onUserMuted(@NonNull BaseChannel channel, @NonNull RestrictedUser restrictedUser) { + if (!isCurrentChannel(channel.getUrl())) return; + Logger.i(">> OpenChannelUserViewModel::onUserMuted()"); + userMuted.postValue(restrictedUser); + } + + @Override + public void onUserUnmuted(@NonNull BaseChannel channel, @NonNull User user) { + if (!isCurrentChannel(channel.getUrl())) return; + Logger.i(">> OpenChannelUserViewModel::onUserUnmuted()"); + userUnmuted.postValue(user); + } + + @Override + public void onUserBanned(@NonNull BaseChannel channel, @NonNull RestrictedUser user) { + if (!isCurrentChannel(channel.getUrl())) return; + Logger.i(">> OpenChannelUserViewModel::onUserBanned()"); + userBanned.postValue(user); + } + + @Override + public void onUserUnbanned(@NonNull BaseChannel channel, @NonNull User user) { + if (!isCurrentChannel(channel.getUrl())) return; + Logger.i(">> OpenChannelUserViewModel::onUserUnbanned()"); + userUnbanned.postValue(user); + } + }); + } + + @Override + protected void onCleared() { + super.onCleared(); + SendbirdChat.removeConnectionHandler(CONNECTION_HANDLER_ID); + SendbirdChat.removeChannelHandler(OPEN_CHANNEL_HANDLER_USER_LIST); + } + + private boolean isCurrentChannel(@NonNull String channelUrl) { + return channel != null && channelUrl.equals(channel.getUrl()); + } + + private void onResult(@Nullable List userList, @Nullable Exception e) { + if (e != null) { + Logger.e(e); + if (isInitialRequest) { + SendbirdChat.addConnectionHandler(CONNECTION_HANDLER_ID, new ConnectionHandler() { + @Override + public void onDisconnected(@NonNull String s) { + } + + @Override + public void onConnected(@NonNull String s) { + } + + @Override + public void onReconnectStarted() { + } + + @Override + public void onReconnectSucceeded() { + SendbirdChat.removeConnectionHandler(CONNECTION_HANDLER_ID); + loadInitial(); + } + + @Override + public void onReconnectFailed() { + } + }); + return; + } + changeAlertStatus(StatusFrameView.Status.ERROR); + notifyDataSetChanged(this.userList.getValue()); + } else { + Logger.d("__ added"); + final List newUsers = new ArrayList<>(); + if (userList != null) { + newUsers.addAll(userList); + } + final List origin = this.userList.getValue(); + if (origin != null) { + newUsers.addAll(0, origin); + } + applyUserList(newUsers); + } + isInitialRequest = false; + } + + /** + * Returns {@code OpenChannel}. If the authentication failed, {@code null} is returned. + * + * @return {@code OpenChannel} this view model is currently associated with + * @since 3.1.0 + */ + @Nullable + public OpenChannel getChannel() { + return channel; + } + + /** + * Returns URL of OpenChannel. + * + * @return The URL of a channel this view model is currently associated with + * @since 3.1.0 + */ + @NonNull + public String getChannelUrl() { + return channelUrl; + } + + /** + * Returns LiveData that can be observed if the channel has been deleted. + * + * @return LiveData holding whether the channel has been deleted + * @since 3.1.0 + */ + @NonNull + public LiveData getChannelDeleted() { + return channelDeleted; + } + + /** + * Returns LiveData that can be observed for the status of the result of fetching the user list. + * When the user list is fetched successfully, the status is {@link StatusFrameView.Status#NONE}. + * + * @return The Status for the user list + * @since 3.1.0 + */ + @NonNull + public LiveData getStatusFrame() { + return statusFrame; + } + + /** + * Returns LiveData that can be observed if operators are updated in the current open channel. + * + * @return LiveData holding the updated open channel + * @since 3.1.0 + */ + @NonNull + public MutableLiveData getOperatorUpdated() { + return operatorUpdated; + } + + /** + * Returns LiveData that can be observed if the user is banned in the current open channel. + * + * @return LiveData holding the user is banned in the current open channel + * @since 3.1.0 + */ + @NonNull + public MutableLiveData getUserBanned() { + return userBanned; + } + + /** + * Returns LiveData that can be observed if the user is unbanned in the current open channel. + * + * @return LiveData holding the user is unbanned in the current open channel + * @since 3.1.0 + */ + @NonNull + public MutableLiveData getUserUnbanned() { + return userUnbanned; + } + + /** + * Returns LiveData that can be observed if the user is muted in the current open channel. + * + * @return LiveData holding the user is muted in the current open channel + * @since 3.1.0 + */ + @NonNull + public MutableLiveData getUserMuted() { + return userMuted; + } + + /** + * Returns LiveData that can be observed if the user is unmuted in the current open channel. + * + * @return LiveData holding the user is unmuted in the current open channel + * @since 3.1.0 + */ + @NonNull + public MutableLiveData getUserUnmuted() { + return userUnmuted; + } + + private void changeAlertStatus(@NonNull StatusFrameView.Status status) { + if (!hasData() || status == StatusFrameView.Status.NONE) { + statusFrame.postValue(status); + } + } + + private boolean hasData() { + List origin = userList.getValue(); + return origin != null && origin.size() > 0; + } + + /** + * Returns LiveData that can be observed for the list of users. + * + * @return LiveData holding the latest list of users + * @since 3.1.0 + */ + @NonNull + public LiveData> getUserList() { + return userList; + } + + private void applyUserList(@NonNull List newUserList) { + changeAlertStatus(newUserList.size() == 0 ? StatusFrameView.Status.EMPTY : StatusFrameView.Status.NONE); + notifyDataSetChanged(newUserList); + } + + private void notifyDataSetChanged(@Nullable List list) { + userList.postValue(list == null ? new ArrayList<>() : list); + } + + /** + * Returns {@code false} as the user list do not support to load for the previous by default. + * + * @return Always {@code false} + * @since 3.1.0 + */ + @Override + public boolean hasPrevious() { + return false; + } + + @Override + public boolean hasNext() { + return query != null && query.hasMore(); + } + + /** + * Requests the list of Users for the first time. + * If there is no more pages to be read, an empty List (not null) returns. + * If the request is succeed, you can observe updated data through {@link #getUserList()}. + * + * @since 3.1.0 + */ + public synchronized boolean loadInitial() { + Logger.d(">> OpenChannelUserViewModel::loadInitial()"); + if (this.query == null) { + this.query = createQueryHandler(channelUrl); + } + if (currentFuture != null) currentFuture.cancel(true); + this.currentFuture = executorService.schedule(() -> { + List origin = userList.getValue(); + if (origin != null) { + origin.clear(); + } + isInitialRequest = true; + query.loadInitial(OpenChannelUserViewModel.this::onResult); + return true; + }, 500, TimeUnit.MILLISECONDS); + return true; + } + + /** + * Requests the list of Users. + * If there is no more pages to be read, an empty List (not null) returns. + * If the request is succeed, you can observe updated data through {@link #getUserList()}. + * + * @return Returns the queried list of Users if no error occurs + * @throws Exception Throws exception if getting the user list are failed + * @since 3.1.0 + */ + @NonNull + @Override + public List loadNext() throws Exception { + if (hasNext()) { + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference> result = new AtomicReference<>(); + final AtomicReference error = new AtomicReference<>(); + try { + if (query == null) return Collections.emptyList(); + query.loadMore((userList, e) -> { + try { + if (e != null) { + error.set(e); + return; + } + result.set(userList); + } finally { + latch.countDown(); + } + }); + latch.await(); + } catch (Exception e) { + error.set(e); + throw e; + } finally { + onResult(result.get(), error.get()); + } + return result.get(); + } + return Collections.emptyList(); + } + + /** + * Returns the empty list as the user list do not support to load for the previous by default. + * + * @return The empty list + * @since 3.1.0 + */ + @NonNull + @Override + public List loadPrevious() { + return Collections.emptyList(); + } + + /** + * Adds users with id in {@code userIds} as operators. + * + * @param userIds User IDs to be added as operators + * @param handler Callback handler called when this method is completed + * @since 3.1.0 + */ + public void addOperators(@NonNull List userIds, @Nullable OnCompleteHandler handler) { + if (channel == null) { + if (handler != null) handler.onComplete(new SendbirdException("channel instance not exists")); + return; + } + channel.addOperators(userIds, e -> { + if (handler != null) handler.onComplete(e); + }); + } + + /** + * Adds user with {@code userId} as operators. + * + * @param userId User ID to be added as operator + * @param handler Callback handler called when this method is completed + * @since 3.1.0 + */ + public void addOperator(@NonNull String userId, @Nullable OnCompleteHandler handler) { + if (channel == null) { + if (handler != null) handler.onComplete(new SendbirdException("channel instance not exists")); + return; + } + channel.addOperators(Collections.singletonList(userId), e -> { + if (handler != null) handler.onComplete(e); + }); + } + + /** + * Dismisses operator with {@code userId}. + * + * @param userId User ID to be dismissed from operator + * @param handler Callback handler called when this method is completed + * @since 3.1.0 + */ + public void removeOperator(@NonNull String userId, @Nullable OnCompleteHandler handler) { + if (channel == null) { + if (handler != null) handler.onComplete(new SendbirdException("channel instance not exists")); + return; + } + channel.removeOperators(Collections.singletonList(userId), e -> { + if (handler != null) handler.onComplete(e); + }); + } + + /** + * Mutes the user with {@code userId}. + * + * @param userId ID of the user to be muted + * @param handler Callback handler called when this method is completed + * @since 3.1.0 + */ + public void muteUser(@NonNull String userId, @Nullable OnCompleteHandler handler) { + if (channel == null) { + if (handler != null) handler.onComplete(new SendbirdException("channel instance not exists")); + return; + } + channel.muteUser(userId, e -> { + if (handler != null) handler.onComplete(e); + }); + } + + /** + * Unmutes the user with {@code userId}. + * + * @param userId ID of the user to be unmuted + * @param handler Callback handler called when this method is completed + * @since 3.1.0 + */ + public void unmuteUser(@NonNull String userId, @Nullable OnCompleteHandler handler) { + if (channel == null) { + if (handler != null) handler.onComplete(new SendbirdException("channel instance not exists")); + return; + } + channel.unmuteUser(userId, e -> { + if (handler != null) handler.onComplete(e); + }); + } + + /** + * Bans the user with {@code userId}. + * + * @param userId ID of the user to be banned + * @param handler Callback handler called when this method is completed + * @since 3.1.0 + */ + public void banUser(@NonNull String userId, @Nullable OnCompleteHandler handler) { + if (channel == null) { + if (handler != null) handler.onComplete(new SendbirdException("channel instance not exists")); + return; + } + channel.banUser(userId, -1, e -> { + if (handler != null) handler.onComplete(e); + }); + } + + /** + * Unbans the user with {@code userId}. + * + * @param userId ID of the user to be unbanned + * @param handler Callback handler called when this method is completed + * @since 3.1.0 + */ + public void unbanUser(@NonNull String userId, @Nullable OnCompleteHandler handler) { + if (channel == null) { + if (handler != null) handler.onComplete(new SendbirdException("channel instance not exists")); + return; + } + channel.unbanUser(userId, e -> { + if (handler != null) handler.onComplete(e); + }); + } + + /** + * Tries to connect Sendbird Server and retrieve a channel instance. + * + * @param handler Callback notifying the result of authentication + * @since 3.1.0 + */ + @Override + public void authenticate(@NonNull AuthenticateHandler handler) { + connect((user, e) -> { + if (user != null) { + if (TextUtils.isNotEmpty(channelUrl)) { + OpenChannel.getChannel(channelUrl, (channel, e1) -> { + OpenChannelUserViewModel.this.channel = channel; + if (e1 != null) { + handler.onAuthenticationFailed(); + } else { + handler.onAuthenticated(); + } + }); + } else { + handler.onAuthenticated(); + } + } else { + handler.onAuthenticationFailed(); + } + }); + } + + /** + * Create a query handler that is loading paged data. + * + * @param channelUrl channel's url + * @return A paged query handler. + * @since 3.1.0 + */ + @NonNull + protected abstract PagedQueryHandler createQueryHandler(@NonNull String channelUrl); +} diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelViewModel.java index 52c8f39e..4f490c88 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/OpenChannelViewModel.java @@ -33,7 +33,6 @@ import com.sendbird.uikit.log.Logger; import com.sendbird.uikit.model.FileInfo; import com.sendbird.uikit.model.MessageList; -import com.sendbird.uikit.utils.Available; import com.sendbird.uikit.widgets.StatusFrameView; import java.io.File; @@ -73,6 +72,8 @@ public class OpenChannelViewModel extends BaseViewModel implements OnPagedDataLo private final MutableLiveData messageLoadState = new MutableLiveData<>(); @NonNull private final MutableLiveData statusFrame = new MutableLiveData<>(); + @NonNull + private final MutableLiveData myMutedInfo = new MutableLiveData<>(); @Nullable private OpenChannel channel; @NonNull @@ -276,6 +277,9 @@ public void onUserMuted(@NonNull BaseChannel channel, @NonNull RestrictedUser us if (isCurrentChannel(channel.getUrl())) { Logger.i(">> OpenChannelViewModel::onUserMuted()"); channelUpdated.postValue((OpenChannel) channel); + if (SendbirdChat.getCurrentUser() != null && user.getUserId().equals(SendbirdChat.getCurrentUser().getUserId())) { + myMutedInfo.postValue(true); + } } } @@ -284,6 +288,9 @@ public void onUserUnmuted(@NonNull BaseChannel channel, @NonNull User user) { if (isCurrentChannel(channel.getUrl())) { Logger.i(">> OpenChannelViewModel::onUserUnmuted()"); channelUpdated.postValue((OpenChannel) channel); + if (SendbirdChat.getCurrentUser() != null && user.getUserId().equals(SendbirdChat.getCurrentUser().getUserId())) { + myMutedInfo.postValue(false); + } } } @@ -347,6 +354,17 @@ public LiveData onMessageDeleted() { return messageDeleted; } + /** + * Returns LiveData that can be observed if the current user is muted or not. + * + * @return LiveData holding the current user muted information + * @since 3.1.0 + */ + @NonNull + public LiveData getMyMutedInfo() { + return myMutedInfo; + } + /** * Returns {@code OpenChannel}. If the authentication failed, {@code null} is returned. * @@ -815,6 +833,8 @@ public void authenticate(@NonNull AuthenticateHandler handler) { handler.onAuthenticationFailed(); } else { handler.onAuthenticated(); + // TODO to be deleted after Chat SDK support + if (channel != null) getMyMutedInfo(channel); } }); } else { @@ -839,4 +859,13 @@ public void enterChannel(@NonNull OpenChannel channel, @NonNull OnCompleteHandle handler.onComplete(e); }); } + + // TODO to be deleted after Chat SDK support + private void getMyMutedInfo(@NonNull OpenChannel channel) { + channel.getMyMutedInfo((isMuted, description, startAt, endAt, remainingDuration, e) -> { + if (e == null) { + myMutedInfo.postValue(isMuted); + } + }); + } } diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/ParticipantViewModel.java b/uikit/src/main/java/com/sendbird/uikit/vm/ParticipantViewModel.java index 0896d46b..790cbe28 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/ParticipantViewModel.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/ParticipantViewModel.java @@ -2,56 +2,17 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.lifecycle.LiveData; -import androidx.lifecycle.MutableLiveData; -import com.sendbird.android.SendbirdChat; -import com.sendbird.android.channel.BaseChannel; -import com.sendbird.android.channel.ChannelType; -import com.sendbird.android.channel.OpenChannel; -import com.sendbird.android.handler.ConnectionHandler; -import com.sendbird.android.handler.OpenChannelHandler; -import com.sendbird.android.message.BaseMessage; -import com.sendbird.android.user.RestrictedUser; import com.sendbird.android.user.User; -import com.sendbird.uikit.interfaces.AuthenticateHandler; -import com.sendbird.uikit.interfaces.OnPagedDataLoader; import com.sendbird.uikit.interfaces.PagedQueryHandler; -import com.sendbird.uikit.log.Logger; import com.sendbird.uikit.vm.queries.ParticipantsListQuery; -import com.sendbird.uikit.widgets.StatusFrameView; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicReference; /** * ViewModel preparing and managing data related with the list of participants * * @since 3.0.0 */ -public class ParticipantViewModel extends BaseViewModel implements OnPagedDataLoader> { - @NonNull - private final String CONNECTION_HANDLER_ID = getClass().getName() + System.currentTimeMillis(); - @NonNull - private final String CHANNEL_HANDLER_MEMBER_LIST = "CHANNEL_HANDLER_MEMBER_LIST" + System.currentTimeMillis(); - @NonNull - private final MutableLiveData statusFrame = new MutableLiveData<>(); - @NonNull - private final MutableLiveData> userList = new MutableLiveData<>(); - @NonNull - private final MutableLiveData channelDeleted = new MutableLiveData<>(); - @NonNull - private final String channelUrl; - - @NonNull - private final PagedQueryHandler queryHandler; - @Nullable - private OpenChannel channel; - private volatile boolean isInitialRequest = false; - +public class ParticipantViewModel extends OpenChannelUserViewModel { /** * Constructor * @@ -60,287 +21,7 @@ public class ParticipantViewModel extends BaseViewModel implements OnPagedDataLo * @since 3.0.0 */ public ParticipantViewModel(@NonNull String channelUrl, @Nullable PagedQueryHandler queryHandler) { - super(); - this.channelUrl = channelUrl; - this.queryHandler = queryHandler == null ? createQueryHandler(channelUrl) : queryHandler; - SendbirdChat.addChannelHandler(CHANNEL_HANDLER_MEMBER_LIST, new OpenChannelHandler() { - @Override - public void onMessageReceived(@NonNull BaseChannel baseChannel, @NonNull BaseMessage baseMessage) { - } - - @Override - public void onChannelDeleted(@NonNull String channelUrl, @NonNull ChannelType channelType) { - if (isCurrentChannel(channelUrl)) { - Logger.i(">> UserViewModel::onChannelDeleted()"); - channelDeleted.postValue(true); - } - } - - @Override - public void onUserBanned(@NonNull BaseChannel channel, @NonNull RestrictedUser user) { - updateChannel(channel); - final User currentUser = SendbirdChat.getCurrentUser(); - if (isCurrentChannel(channel.getUrl()) && currentUser != null && - user.getUserId().equals(currentUser.getUserId())) { - Logger.i(">> UserViewModel::onUserBanned()"); - channelDeleted.postValue(true); - } - } - - @Override - public void onChannelChanged(@NonNull BaseChannel channel) { - updateChannel(channel); - } - }); - } - - @Override - protected void onCleared() { - super.onCleared(); - SendbirdChat.removeConnectionHandler(CONNECTION_HANDLER_ID); - SendbirdChat.removeChannelHandler(CHANNEL_HANDLER_MEMBER_LIST); - } - - private boolean isCurrentChannel(@NonNull String channelUrl) { - return channel != null && channelUrl.equals(channel.getUrl()); - } - - private void onResult(@NonNull List memberList, @Nullable Exception e) { - if (e != null) { - Logger.e(e); - if (isInitialRequest) { - SendbirdChat.addConnectionHandler(CONNECTION_HANDLER_ID, new ConnectionHandler() { - @Override - public void onConnected(@NonNull String s) { - - } - - @Override - public void onDisconnected(@NonNull String s) { - - } - - @Override - public void onReconnectStarted() { - - } - - @Override - public void onReconnectSucceeded() { - SendbirdChat.removeConnectionHandler(CONNECTION_HANDLER_ID); - loadInitial(); - } - - @Override - public void onReconnectFailed() { - - } - }); - return; - } - changeAlertStatus(StatusFrameView.Status.ERROR); - notifyDataSetChanged(this.userList.getValue()); - } else { - final List newUsers = new ArrayList<>(memberList); - final List origin = this.userList.getValue(); - if (origin != null) { - newUsers.addAll(0, origin); - } - applyUserList(newUsers); - } - isInitialRequest = false; - } - - /** - * Returns {@code OpenChannel}. If the authentication failed, {@code null} is returned. - * - * @return {@code OpenChannel} this view model is currently associated with - * @since 3.0.0 - */ - @Nullable - public OpenChannel getChannel() { - return channel; - } - - /** - * Returns URL of GroupChannel. - * - * @return The URL of a channel this view model is currently associated with - * @since 3.0.0 - */ - @NonNull - public String getChannelUrl() { - return channelUrl; - } - - /** - * Returns LiveData that can be observed if the channel has been deleted. - * - * @return LiveData holding whether {@code OpenChannel} has been deleted - * @since 3.0.0 - */ - @NonNull - public LiveData getChannelDeleted() { - return channelDeleted; - } - - private void updateChannel(@NonNull BaseChannel channel) { - if (isCurrentChannel(channel.getUrl())) { - Logger.i(">> UserViewModel::updateChannel()"); - loadInitial(); - } - } - - /** - * Returns LiveData that can be observed for the status of the result of fetching the user list. - * When the user list is fetched successfully, the status is {@link StatusFrameView.Status#NONE}. - * - * @return The Status for the user list - * @since 3.0.0 - */ - @NonNull - public LiveData getStatusFrame() { - return statusFrame; - } - - private void changeAlertStatus(@NonNull StatusFrameView.Status status) { - if (!hasData() || status == StatusFrameView.Status.NONE) { - statusFrame.postValue(status); - } - } - - private boolean hasData() { - List origin = userList.getValue(); - return origin != null && origin.size() > 0; - } - - /** - * Returns LiveData that can be observed for the list of participants. - * - * @return LiveData holding the latest list of participants - * @since 3.0.0 - */ - @NonNull - public LiveData> getUserList() { - return userList; - } - - private void applyUserList(@NonNull List newUserList) { - changeAlertStatus(newUserList.size() == 0 ? StatusFrameView.Status.EMPTY : StatusFrameView.Status.NONE); - notifyDataSetChanged(newUserList); - } - - private void notifyDataSetChanged(@Nullable List list) { - userList.postValue(list == null ? new ArrayList<>() : list); - } - - @Override - public boolean hasNext() { - return queryHandler.hasMore(); - } - - /** - * Returns {@code false} as the participant list do not support to load for the previous by default. - * - * @return Always {@code false} - * @since 3.0.0 - */ - @Override - public boolean hasPrevious() { - return false; - } - - /** - * Requests the list of Users for the first time. - * If there is no more pages to be read, an empty List (not null) returns. - * If the request is succeed, you can observe updated data through {@link #getUserList()}. - * - * @since 3.0.0 - */ - public synchronized void loadInitial() { - Logger.d(">> MemberListViewModel::loadInitial()"); - List origin = this.userList.getValue(); - if (origin != null) { - origin.clear(); - } - this.isInitialRequest = true; - queryHandler.loadInitial(ParticipantViewModel.this::onResult); - } - - /** - * Requests the list of Users. - * If there is no more pages to be read, an empty List (not null) returns. - * If the request is succeed, you can observe updated data through {@link #getUserList()}. - * - * @return Returns the queried list of Users if no error occurs - * @throws Exception Throws exception if getting the user list are failed - * @since 3.0.0 - */ - @NonNull - @Override - public List loadNext() throws Exception { - if (hasNext()) { - final CountDownLatch latch = new CountDownLatch(1); - final AtomicReference> result = new AtomicReference<>(); - final AtomicReference error = new AtomicReference<>(); - try { - queryHandler.loadMore((userList, e) -> { - try { - if (e != null) { - error.set(e); - return; - } - result.set(userList); - } finally { - latch.countDown(); - } - }); - latch.await(); - } catch (Exception e) { - error.set(e); - throw e; - } finally { - onResult(result.get(), error.get()); - } - return result.get(); - } - return Collections.emptyList(); - } - - /** - * Returns the empty list as the participant list do not support to load for the previous by default. - * - * @return The empty list - * @since 3.0.0 - */ - @NonNull - @Override - public List loadPrevious() { - return Collections.emptyList(); - } - - /** - * Tries to connect Sendbird Server and retrieve a channel instance. - * - * @param handler Callback notifying the result of authentication - * @since 3.0.0 - */ - @Override - public void authenticate(@NonNull AuthenticateHandler handler) { - connect((user, e) -> { - if (user != null) { - OpenChannel.getChannel(channelUrl, (channel, e1) -> { - ParticipantViewModel.this.channel = channel; - if (e1 != null) { - handler.onAuthenticationFailed(); - } else { - handler.onAuthenticated(); - } - }); - } else { - handler.onAuthenticationFailed(); - } - }); + super(channelUrl, queryHandler); } /** diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/ViewModelFactory.java b/uikit/src/main/java/com/sendbird/uikit/vm/ViewModelFactory.java index a322cc83..f5536cd4 100644 --- a/uikit/src/main/java/com/sendbird/uikit/vm/ViewModelFactory.java +++ b/uikit/src/main/java/com/sendbird/uikit/vm/ViewModelFactory.java @@ -71,6 +71,16 @@ public T create(@NonNull Class modelClass) { return (T) new RegisterOperatorViewModel((String) Objects.requireNonNull(params)[0], params.length > 1 ? (PagedQueryHandler) params[1] : null); } else if (modelClass.isAssignableFrom(ChannelPushSettingViewModel.class)) { return (T) new ChannelPushSettingViewModel((String) Objects.requireNonNull(params)[0]); + } else if (modelClass.isAssignableFrom(OpenChannelModerationViewModel.class)) { + return (T) new OpenChannelModerationViewModel((String) Objects.requireNonNull(params)[0]); + } else if (modelClass.isAssignableFrom(OpenChannelRegisterOperatorViewModel.class)) { + return (T) new OpenChannelRegisterOperatorViewModel((String) Objects.requireNonNull(params)[0], params.length > 1 ? (PagedQueryHandler) params[1] : null); + } else if (modelClass.isAssignableFrom(OpenChannelOperatorListViewModel.class)) { + return (T) new OpenChannelOperatorListViewModel((String) Objects.requireNonNull(params)[0], params.length > 1 ? (PagedQueryHandler) params[1] : null); + } else if (modelClass.isAssignableFrom(OpenChannelMutedParticipantListViewModel.class)) { + return (T) new OpenChannelMutedParticipantListViewModel((String) Objects.requireNonNull(params)[0], params.length > 1 ? (PagedQueryHandler) params[1] : null); + } else if (modelClass.isAssignableFrom(OpenChannelBannedUserListViewModel.class)) { + return (T) new OpenChannelBannedUserListViewModel((String) Objects.requireNonNull(params)[0]); } else { return super.create(modelClass); } diff --git a/uikit/src/main/java/com/sendbird/uikit/vm/queries/MutedUserListQuery.java b/uikit/src/main/java/com/sendbird/uikit/vm/queries/MutedUserListQuery.java new file mode 100644 index 00000000..e870fae6 --- /dev/null +++ b/uikit/src/main/java/com/sendbird/uikit/vm/queries/MutedUserListQuery.java @@ -0,0 +1,54 @@ +package com.sendbird.uikit.vm.queries; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.sendbird.android.SendbirdChat; +import com.sendbird.android.channel.ChannelType; +import com.sendbird.android.exception.SendbirdException; +import com.sendbird.android.params.MutedUserListQueryParams; +import com.sendbird.android.user.User; +import com.sendbird.uikit.interfaces.OnListResultHandler; +import com.sendbird.uikit.interfaces.PagedQueryHandler; + +import java.util.ArrayList; + +public class MutedUserListQuery implements PagedQueryHandler { + @NonNull + private final ChannelType channelType; + @NonNull + private final String channelUrl; + @Nullable + private com.sendbird.android.user.query.MutedUserListQuery query; + + public MutedUserListQuery(@NonNull ChannelType channelType, @NonNull String channelUrl) { + this.channelType = channelType; + this.channelUrl = channelUrl; + } + + @Override + public void loadInitial(@NonNull OnListResultHandler handler) { + MutedUserListQueryParams params = new MutedUserListQueryParams(channelType, channelUrl); + params.setLimit(30); + this.query = SendbirdChat.createMutedUserListQuery(params); + loadMore(handler); + } + + @Override + public void loadMore(@NonNull OnListResultHandler handler) { + if (this.query == null) { + handler.onResult(null, new SendbirdException("loadInitial must be called first.")); + return; + } + this.query.next((list, e) -> handler.onResult(list != null ? new ArrayList<>(list) : null, e)); + } + + @Override + public boolean hasMore() { + if (this.query != null) { + return this.query.getHasNext(); + } else { + return false; + } + } +} diff --git a/uikit/src/main/java/com/sendbird/uikit/widgets/DialogView.java b/uikit/src/main/java/com/sendbird/uikit/widgets/DialogView.java index 46fb230d..0ae9df06 100644 --- a/uikit/src/main/java/com/sendbird/uikit/widgets/DialogView.java +++ b/uikit/src/main/java/com/sendbird/uikit/widgets/DialogView.java @@ -64,7 +64,7 @@ public DialogView(@NonNull Context context, @Nullable AttributeSet attrs, int de backgroundBottomId = a.getResourceId(R.styleable.DialogView_sb_dialog_view_background_bottom, R.drawable.sb_top_rounded_rectangle_light); backgroundAnchorId = a.getResourceId(R.styleable.DialogView_sb_dialog_view_background_anchor, R.drawable.layer_dialog_anchor_background_light); int titleAppearance = a.getResourceId(R.styleable.DialogView_sb_dialog_view_title_appearance, R.style.SendbirdH1OnLight01); - int messageAppearance = a.getResourceId(R.styleable.DialogView_sb_dialog_view_message_appearance, R.style.SendbirdSubtitle2OnLight01); + int messageAppearance = a.getResourceId(R.styleable.DialogView_sb_dialog_view_message_appearance, R.style.SendbirdBody3OnLight02); int editTextAppearance = a.getResourceId(R.styleable.DialogView_sb_dialog_view_edit_text_appearance, R.style.SendbirdSubtitle2OnLight01); ColorStateList editTextTint = a.getColorStateList(R.styleable.DialogView_sb_dialog_view_edit_text_tint); int editTextCursorDrawable = a.getResourceId(R.styleable.DialogView_sb_dialog_view_edit_text_cursor_drawable, R.drawable.sb_message_input_cursor_light); @@ -137,8 +137,8 @@ public void setTitle(@Nullable CharSequence title) { binding.tvDialogTitle.setVisibility(VISIBLE); } - public void setMessage(int message) { - if (message == 0) { + public void setMessage(@Nullable CharSequence message) { + if (TextUtils.isEmpty(message)) { return; } binding.tvDialogMessage.setText(message); diff --git a/uikit/src/main/java/com/sendbird/uikit/widgets/UserPreview.java b/uikit/src/main/java/com/sendbird/uikit/widgets/UserPreview.java index 56b389f9..5c4f321a 100644 --- a/uikit/src/main/java/com/sendbird/uikit/widgets/UserPreview.java +++ b/uikit/src/main/java/com/sendbird/uikit/widgets/UserPreview.java @@ -17,9 +17,6 @@ import androidx.annotation.Nullable; import androidx.appcompat.content.res.AppCompatResources; -import com.sendbird.android.SendbirdChat; -import com.sendbird.android.channel.Role; -import com.sendbird.android.user.Member; import com.sendbird.android.user.User; import com.sendbird.uikit.R; import com.sendbird.uikit.SendbirdUIKit; @@ -116,47 +113,23 @@ public void setImageFromUrl(@Nullable String url) { ViewUtils.drawProfile(binding.ivProfile, url); } - public void setVisibleOverlay(int visiblility) { - binding.ivProfileOverlay.setVisibility(visiblility); + public void setVisibleOverlay(int visibility) { + binding.ivProfileOverlay.setVisibility(visibility); } public void enableActionMenu(boolean enabled) { binding.ivAction.setEnabled(enabled); } - public static void drawMember(@NonNull UserPreview preview, @NonNull Member member) { + public static void drawUser(@NonNull UserPreview preview, @NonNull User user, @NonNull final String description, boolean isMuted) { Context context = preview.getContext(); - boolean isOperatorMember = member.getRole() == Role.OPERATOR; - boolean isMe = member.getUserId().equals(SendbirdChat.getCurrentUser().getUserId()); - final String nickname = UserUtils.getDisplayName(context, member); - preview.setName(nickname); - - String description = isOperatorMember ? context.getString(R.string.sb_text_operator) : ""; - preview.setDescription(description); - preview.setImageFromUrl(member.getProfileUrl()); - preview.enableActionMenu(!isMe); - preview.setVisibleOverlay(member.isMuted() ? View.VISIBLE : View.GONE); - - if (isMe) { - String meBadge = nickname + context.getResources().getString(R.string.sb_text_user_list_badge_me); - final Spannable spannable = new SpannableString(meBadge); - int badgeAppearance = SendbirdUIKit.isDarkMode() ? R.style.SendbirdSubtitle2OnDark02 : R.style.SendbirdSubtitle2OnLight02; - int originLen = nickname.length(); - spannable.setSpan(new TextAppearanceSpan(context, badgeAppearance), - originLen, meBadge.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - preview.setName(spannable); - } - } - - public static void drawMemberFromUser(@NonNull UserPreview preview, @NonNull User user) { - Context context = preview.getContext(); - boolean isMe = user.getUserId().equals(SendbirdChat.getCurrentUser().getUserId()); + boolean isMe = user.getUserId().equals(SendbirdUIKit.getAdapter().getUserInfo().getUserId()); final String nickname = UserUtils.getDisplayName(context, user); preview.setName(nickname); - - preview.setDescription(""); + preview.setDescription(description); preview.setImageFromUrl(user.getProfileUrl()); preview.enableActionMenu(!isMe); + preview.setVisibleOverlay(isMuted ? View.VISIBLE : View.GONE); if (isMe) { String meBadge = nickname + context.getResources().getString(R.string.sb_text_user_list_badge_me); diff --git a/uikit/src/main/res/layout/sb_view_dialog.xml b/uikit/src/main/res/layout/sb_view_dialog.xml index aa300af9..b27d0c07 100644 --- a/uikit/src/main/res/layout/sb_view_dialog.xml +++ b/uikit/src/main/res/layout/sb_view_dialog.xml @@ -21,7 +21,7 @@ android:maxLines="1" android:ellipsize="end" android:layout_marginTop="@dimen/sb_size_20" - android:layout_marginBottom="@dimen/sb_size_20" + android:layout_marginBottom="@dimen/sb_size_16" android:visibility="gone"/> @@ -40,10 +40,9 @@ android:id="@+id/tvDialogMessage" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="@dimen/sb_size_20" android:layout_marginLeft="@dimen/sb_size_24" android:layout_marginRight="@dimen/sb_size_24" - android:layout_marginBottom="@dimen/sb_size_24" + android:layout_marginBottom="@dimen/sb_size_12" android:maxLines="3" android:ellipsize="end" android:visibility="gone"/> @@ -54,7 +53,7 @@ android:layout_height="wrap_content" android:layout_marginLeft="@dimen/sb_size_24" android:layout_marginRight="@dimen/sb_size_24" - android:layout_marginBottom="@dimen/sb_size_24" + android:layout_marginBottom="@dimen/sb_size_20" android:imeOptions="flagNoExtractUi" android:visibility="gone"/> @@ -63,7 +62,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="@dimen/sb_size_4" - android:layout_marginBottom="@dimen/sb_size_4" app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" android:visibility="gone"/> @@ -75,7 +73,7 @@ android:layout_height="wrap_content" android:gravity="end" android:orientation="horizontal" - android:paddingTop="@dimen/sb_size_8" + android:paddingTop="@dimen/sb_size_12" android:paddingBottom="@dimen/sb_size_12" android:paddingLeft="@dimen/sb_size_8" android:paddingRight="@dimen/sb_size_8" 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 index 08cf04aa..fd559810 100644 --- 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 @@ -5,6 +5,12 @@ android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android"> + + + + + + + diff --git a/uikit/src/main/res/values/strings.xml b/uikit/src/main/res/values/strings.xml index f188d17f..3b6d0240 100644 --- a/uikit/src/main/res/values/strings.xml +++ b/uikit/src/main/res/values/strings.xml @@ -64,6 +64,8 @@ Yesterday Delete this file? Delete this message? + Delete channel? + Once deleted, this channel can\'t be restored. Copied Uploading… Downloading… @@ -107,9 +109,13 @@ Couldn\'t send message. Try again. Message filtered. Couldn\'t mute member. Try again. + Couldn\'t mute participant. Try again. Couldn\'t ban member. Try again. + Couldn\'t ban participant. Try again. Couldn\'t unmute member. Try again. + Couldn\'t unmute participant. Try again. Couldn\'t unban member. Try again. + Couldn\'t unban participant. Try again. Couldn\'t open camera. @@ -132,18 +138,25 @@ Operator Operators Muted members - Banned members + Banned users Freeze channel No operator members No muted members - No banned members + No banned users + Muted participants + No operator participants + No muted participants Register as operator Mute + Mute Ban + Ban Unregister operator Unmute Unban + Unban + Unmute Message User ID diff --git a/uikit/src/main/res/values/styles.xml b/uikit/src/main/res/values/styles.xml index f4632bf0..eb1a80fa 100755 --- a/uikit/src/main/res/values/styles.xml +++ b/uikit/src/main/res/values/styles.xml @@ -23,6 +23,11 @@ @style/Module.ParticipantList @style/Module.RegisterOperator @style/Module.ChannelPushSetting + @style/Module.OpenChannelModeration + @style/Module.OpenChannelRegisterOperator + @style/Module.OpenChannelOperatorList + @style/Module.OpenChannelMutedParticipantList + @style/Module.OpenChannelBannedUserList @style/Component.ChannelMessageInput @style/Component.ChannelSettingsInfo @@ -121,6 +126,30 @@ @style/Component.Header.ChannelPushSetting @style/Component.ChannelPushSetting + + + + + @@ -475,6 +504,7 @@ @string/sb_text_user_list_empty @string/sb_text_error_get_user_list + @@ -490,6 +520,69 @@ @drawable/selector_radio_button_light + + + + + + + + + + + + + + + + + + + + + + + + @@ -475,6 +504,7 @@ @string/sb_text_user_list_empty @string/sb_text_error_get_user_list + @@ -490,6 +520,68 @@ @drawable/selector_radio_button_dark + + + + + + + + + + + + + + + + + +