From fe783b6220268f1baecf653ab2d6c99e6643f13b Mon Sep 17 00:00:00 2001 From: kanat Date: Thu, 29 Feb 2024 14:40:22 -0800 Subject: [PATCH] (sample-app) add ability to hide/show channel --- .../sample/feature/chat/info/ChatInfoItem.kt | 31 ++++++-- .../feature/chat/info/ChatInfoViewHolders.kt | 4 +- .../chat/info/group/GroupChatInfoFragment.kt | 66 +++++++++++++++-- .../chat/info/group/GroupChatInfoViewModel.kt | 74 +++++++++++++++---- .../common/ConfirmationDialogFragment.kt | 20 ++++- .../src/main/res/drawable/ic_hide.xml | 5 ++ .../src/main/res/drawable/ic_hide_3.xml | 5 ++ .../src/main/res/drawable/ic_show.xml | 5 ++ .../main/res/layout/chat_info_option_item.xml | 19 +++++ .../src/main/res/values/strings.xml | 6 ++ 10 files changed, 203 insertions(+), 32 deletions(-) create mode 100644 stream-chat-android-ui-components-sample/src/main/res/drawable/ic_hide.xml create mode 100644 stream-chat-android-ui-components-sample/src/main/res/drawable/ic_hide_3.xml create mode 100644 stream-chat-android-ui-components-sample/src/main/res/drawable/ic_show.xml diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/ChatInfoItem.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/ChatInfoItem.kt index 1a938a22382..851c56801f5 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/ChatInfoItem.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/ChatInfoItem.kt @@ -34,7 +34,7 @@ sealed class ChatInfoItem { data class MemberItem(val member: Member, val createdBy: User) : ChatInfoItem() data class MembersSeparator(val membersToShow: Int) : ChatInfoItem() data class ChannelName(val name: String) : ChatInfoItem() - object Separator : ChatInfoItem() + data object Separator : ChatInfoItem() sealed class Option : ChatInfoItem() { @@ -52,35 +52,37 @@ sealed class ChatInfoItem { open val showRightArrow: Boolean = true - object PinnedMessages : Option() { + open val checkedState: Boolean? = null + + data object PinnedMessages : Option() { override val iconResId: Int get() = R.drawable.stream_ui_ic_pin override val textResId: Int get() = R.string.chat_info_option_pinned_messages } - object SharedMedia : Option() { + data object SharedMedia : Option() { override val iconResId: Int get() = R.drawable.ic_media override val textResId: Int get() = R.string.chat_info_option_media } - object SharedFiles : Option() { + data object SharedFiles : Option() { override val iconResId: Int get() = R.drawable.ic_files override val textResId: Int get() = R.string.chat_info_option_files } - object SharedGroups : Option() { + data object SharedGroups : Option() { override val iconResId: Int get() = R.drawable.ic_new_group override val textResId: Int get() = R.string.chat_info_option_shared_groups } - object DeleteConversation : Option() { + data object DeleteConversation : Option() { override val iconResId: Int get() = R.drawable.ic_delete override val textResId: Int @@ -92,7 +94,7 @@ sealed class ChatInfoItem { override val showRightArrow: Boolean = false } - object LeaveGroup : Option() { + data object LeaveGroup : Option() { override val iconResId: Int get() = R.drawable.ic_leave_group override val textResId: Int @@ -100,6 +102,19 @@ sealed class ChatInfoItem { override val showRightArrow: Boolean = false } + data class HideChannel(var isHidden: Boolean) : Option() { + override val iconResId: Int + get() = R.drawable.ic_hide + override val textResId: Int + get() = R.string.chat_group_info_option_hide + + override val showRightArrow: Boolean = false + + override val checkedState: Boolean + get() = isHidden + } + + sealed class Stateful : Option() { abstract val isChecked: Boolean @@ -125,4 +140,4 @@ sealed class ChatInfoItem { } } } -} +} \ No newline at end of file diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/ChatInfoViewHolders.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/ChatInfoViewHolders.kt index 6965e2d4c95..e2397234490 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/ChatInfoViewHolders.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/ChatInfoViewHolders.kt @@ -88,7 +88,9 @@ class ChatInfoOptionViewHolder( binding.optionTextView.setTextColor(itemView.context.getColorFromRes(item.textColorResId)) binding.optionImageView.setImageResource(item.iconResId) binding.optionImageView.setColorFilter(itemView.context.getColorFromRes(item.tintResId)) - binding.optionArrowRight.isInvisible = !item.showRightArrow + binding.optionArrowRight.isVisible = item.showRightArrow + binding.optionCompound.isVisible = item.checkedState != null + binding.optionCompound.isChecked = item.checkedState == true } } diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/group/GroupChatInfoFragment.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/group/GroupChatInfoFragment.kt index aecbd464ff2..22e5c7f1b26 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/group/GroupChatInfoFragment.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/group/GroupChatInfoFragment.kt @@ -26,6 +26,8 @@ import androidx.fragment.app.viewModels import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs import io.getstream.chat.android.client.ChatClient +import io.getstream.chat.android.client.events.ChannelHiddenEvent +import io.getstream.chat.android.client.events.ChannelVisibleEvent import io.getstream.chat.android.client.events.NotificationChannelMutesUpdatedEvent import io.getstream.chat.android.client.subscribeFor import io.getstream.chat.android.state.utils.EventObserver @@ -43,9 +45,12 @@ import io.getstream.chat.ui.sample.feature.chat.info.group.users.GroupChatInfoAd import io.getstream.chat.ui.sample.feature.common.ConfirmationDialogFragment import io.getstream.chat.ui.sample.util.extensions.autoScrollToTop import io.getstream.chat.ui.sample.util.extensions.useAdjustResize +import io.getstream.log.taggedLogger class GroupChatInfoFragment : Fragment() { + private val logger by taggedLogger("GroupChatInfo-View") + private val args: GroupChatInfoFragmentArgs by navArgs() private val viewModel: GroupChatInfoViewModel by viewModels { ChatViewModelFactory(args.cid) } private val headerViewModel: MessageListHeaderViewModel by viewModels { @@ -101,6 +106,7 @@ class GroupChatInfoFragment : Fragment() { private fun bindGroupInfoViewModel() { subscribeForChannelMutesUpdatedEvents() + subscribeForChannelVisibilityEvents() setOnClickListeners() viewModel.events.observe( @@ -135,6 +141,7 @@ class GroupChatInfoFragment : Fragment() { ChatInfoItem.Separator, ChatInfoItem.ChannelName(state.channelName), ChatInfoItem.Option.Stateful.MuteChannel(isChecked = state.channelMuted), + ChatInfoItem.Option.HideChannel(isHidden = state.channelHidden), ChatInfoItem.Option.PinnedMessages, ChatInfoItem.Option.SharedMedia, ChatInfoItem.Option.SharedFiles, @@ -148,6 +155,7 @@ class GroupChatInfoFragment : Fragment() { when (it) { is GroupChatInfoViewModel.ErrorEvent.ChangeGroupNameError -> R.string.chat_group_info_error_change_name is GroupChatInfoViewModel.ErrorEvent.MuteChannelError -> R.string.chat_group_info_error_mute_channel + is GroupChatInfoViewModel.ErrorEvent.HideChannelError -> R.string.chat_group_info_error_hide_channel is GroupChatInfoViewModel.ErrorEvent.LeaveChannelError -> R.string.chat_group_info_error_leave_channel }.let(::showToast) }, @@ -156,14 +164,14 @@ class GroupChatInfoFragment : Fragment() { private fun setOnClickListeners() { adapter.setChatInfoStatefulOptionChangedListener { option, isChecked -> - viewModel.onAction( - when (option) { - is ChatInfoItem.Option.Stateful.MuteChannel -> GroupChatInfoViewModel.Action.MuteChannelClicked( - isChecked, - ) - else -> throw IllegalStateException("Chat info option $option is not supported!") - }, - ) + logger.d { "[onStatefulOptionChanged] option: $option, isChecked: $isChecked" } + + when (option) { + is ChatInfoItem.Option.Stateful.MuteChannel -> viewModel.onAction( + GroupChatInfoViewModel.Action.MuteChannelClicked(isChecked) + ) + else -> throw IllegalStateException("Chat info option $option is not supported!") + } } adapter.setChatInfoOptionClickListener { option -> when (option) { @@ -186,6 +194,9 @@ class GroupChatInfoFragment : Fragment() { } .show(parentFragmentManager, ConfirmationDialogFragment.TAG) } + is ChatInfoItem.Option.HideChannel -> prepareHideChannelClickedAction { + viewModel.onAction(it) + } else -> throw IllegalStateException("Group chat info option $option is not supported!") } } @@ -199,4 +210,43 @@ class GroupChatInfoFragment : Fragment() { viewModel.onAction(GroupChatInfoViewModel.Action.ChannelMutesUpdated(it.me.channelMutes)) } } + + private fun subscribeForChannelVisibilityEvents() { + ChatClient.instance().subscribeFor(viewLifecycleOwner) { + viewModel.onAction(GroupChatInfoViewModel.Action.ChannelHiddenUpdated( + cid = it.cid, + hidden = true, + clearHistory = it.clearHistory + )) + } + ChatClient.instance().subscribeFor(viewLifecycleOwner) { + viewModel.onAction(GroupChatInfoViewModel.Action.ChannelHiddenUpdated( + cid = it.cid, + hidden = false + )) + } + } + + private fun prepareHideChannelClickedAction( + onReady: (GroupChatInfoViewModel.Action.HideChannelClicked) -> Unit + ) { + val curValue = viewModel.state.value!!.channelHidden + val newValue = curValue.not() + val action = GroupChatInfoViewModel.Action.HideChannelClicked(newValue) + if (newValue) { + val channelName = viewModel.state.value!!.channelName + ConfirmationDialogFragment.newHideChannelInstance(requireContext(), channelName) + .apply { + confirmClickListener = ConfirmationDialogFragment.ConfirmClickListener { + onReady(action.copy(clearHistory = true)) + } + cancelClickListener = ConfirmationDialogFragment.CancelClickListener { + onReady(action) + } + } + .show(parentFragmentManager, ConfirmationDialogFragment.TAG) + } else { + onReady(action) + } + } } diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/group/GroupChatInfoViewModel.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/group/GroupChatInfoViewModel.kt index 0564bba36a8..d2ad4a2641a 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/group/GroupChatInfoViewModel.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/chat/info/group/GroupChatInfoViewModel.kt @@ -32,10 +32,13 @@ import io.getstream.chat.android.models.Message import io.getstream.chat.android.models.User import io.getstream.chat.android.state.extensions.watchChannelAsState import io.getstream.chat.android.state.utils.Event +import io.getstream.log.taggedLogger import io.getstream.result.Result import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch class GroupChatInfoViewModel( @@ -44,6 +47,8 @@ class GroupChatInfoViewModel( private val clientState: ClientState = chatClient.clientState, ) : ViewModel() { + private val logger by taggedLogger("GroupChatInfo-VM") + /** * Holds information about the current channel and is actively updated. */ @@ -74,16 +79,29 @@ class GroupChatInfoViewModel( createdBy = channelData.createdBy, ) } + + // TODO we use take(1), cause ChannelState.hidden seems to be not updated properly + _state.addSource(channelState.flatMapLatest { it.hidden }.distinctUntilChanged().take(1).asLiveData()) { hidden -> + logger.v { "[onHiddenChanged] hidden: $hidden" } + _state.value = _state.value?.copy( + channelHidden = hidden, + ) + } } fun onAction(action: Action) { - when (action) { - is Action.NameChanged -> changeGroupName(action.name) - is Action.MemberClicked -> handleMemberClick(action.member) - Action.MembersSeparatorClicked -> _state.value = _state.value!!.copy(shouldExpandMembers = true) - is Action.MuteChannelClicked -> switchGroupMute(action.isEnabled) - is Action.ChannelMutesUpdated -> updateChannelMuteStatus(action.channelMutes) - Action.LeaveChannelClicked -> leaveChannel() + logger.d { "[onAction] action: $action" } + viewModelScope.launch { + when (action) { + is Action.NameChanged -> changeGroupName(action.name) + is Action.MemberClicked -> handleMemberClick(action.member) + is Action.MembersSeparatorClicked -> _state.value = _state.value!!.copy(shouldExpandMembers = true) + is Action.MuteChannelClicked -> switchGroupMute(action.isEnabled) + is Action.HideChannelClicked -> switchGroupHide(action.isHidden, action.clearHistory) + is Action.ChannelMutesUpdated -> updateChannelMuteStatus(action.channelMutes) + is Action.ChannelHiddenUpdated -> updateChannelHideStatus(action.cid, action.hidden) + is Action.LeaveChannelClicked -> leaveChannel() + } } } @@ -130,7 +148,13 @@ class GroupChatInfoViewModel( } private fun updateChannelMuteStatus(channelMutes: List) { - _state.postValue(_state.value!!.copy(channelMuted = channelMutes.any { it.channel.cid == cid })) + _state.value = _state.value!!.copy(channelMuted = channelMutes.any { it.channel.cid == cid }) + } + + private fun updateChannelHideStatus(eventCid: String, hidden: Boolean) { + if (eventCid != cid) return + logger.v { "[updateChannelHideStatus] hidden: $hidden" } + _state.value = _state.value!!.copy(channelHidden = hidden) } private fun switchGroupMute(isEnabled: Boolean) { @@ -146,11 +170,26 @@ class GroupChatInfoViewModel( } } + private fun switchGroupHide(hide: Boolean, clearHistory: Boolean?) { + logger.v { "[switchGroupHide] hide: $hide, clearHistory: $clearHistory" } + viewModelScope.launch { + val result = if (hide) { + channelClient.hide(clearHistory = clearHistory == true).await() + } else { + channelClient.show().await() + } + if (result is Result.Failure) { + _errorEvents.postValue(Event(ErrorEvent.HideChannelError)) + } + } + } + data class State( val members: List, val createdBy: User, val channelName: String, val channelMuted: Boolean, + val channelHidden: Boolean, val shouldExpandMembers: Boolean?, val membersToShowCount: Int, val ownCapabilities: Set, @@ -159,21 +198,27 @@ class GroupChatInfoViewModel( sealed class Action { data class NameChanged(val name: String) : Action() data class MemberClicked(val member: Member) : Action() - object MembersSeparatorClicked : Action() + data object MembersSeparatorClicked : Action() data class MuteChannelClicked(val isEnabled: Boolean) : Action() + data class HideChannelClicked(val isHidden: Boolean, val clearHistory: Boolean? = null) : Action() data class ChannelMutesUpdated(val channelMutes: List) : Action() - object LeaveChannelClicked : Action() + + data class ChannelHiddenUpdated( + val cid: String, val hidden: Boolean, val clearHistory: Boolean? = null + ) : Action() + data object LeaveChannelClicked : Action() } sealed class UiEvent { data class ShowMemberOptions(val member: Member, val channelName: String) : UiEvent() - object RedirectToHome : UiEvent() + data object RedirectToHome : UiEvent() } sealed class ErrorEvent { - object ChangeGroupNameError : ErrorEvent() - object MuteChannelError : ErrorEvent() - object LeaveChannelError : ErrorEvent() + data object ChangeGroupNameError : ErrorEvent() + data object MuteChannelError : ErrorEvent() + data object HideChannelError : ErrorEvent() + data object LeaveChannelError : ErrorEvent() } companion object { @@ -184,6 +229,7 @@ class GroupChatInfoViewModel( createdBy = User(), channelName = "", channelMuted = false, + channelHidden = false, shouldExpandMembers = null, membersToShowCount = 0, emptySet(), diff --git a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/common/ConfirmationDialogFragment.kt b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/common/ConfirmationDialogFragment.kt index e9d40d9ca9c..236ab8b6907 100644 --- a/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/common/ConfirmationDialogFragment.kt +++ b/stream-chat-android-ui-components-sample/src/main/kotlin/io/getstream/chat/ui/sample/feature/common/ConfirmationDialogFragment.kt @@ -33,6 +33,7 @@ import io.getstream.chat.ui.sample.databinding.ConfirmationDialogFragmentBinding internal class ConfirmationDialogFragment : BottomSheetDialogFragment() { var confirmClickListener: ConfirmClickListener? = null + var cancelClickListener: CancelClickListener? = null private val iconResId: Int by lazy { requireArguments().getInt(ARG_ICON_RES_ID) } private val iconTintResId: Int by lazy { requireArguments().getInt(ARG_ICON_TINT_RES_ID) } @@ -48,6 +49,7 @@ internal class ConfirmationDialogFragment : BottomSheetDialogFragment() { override fun onDetach() { super.onDetach() confirmClickListener = null + cancelClickListener = null } override fun onCreateView( @@ -71,7 +73,10 @@ internal class ConfirmationDialogFragment : BottomSheetDialogFragment() { titleTextView.text = title descriptionTextView.text = description cancelButton.text = cancelText - cancelButton.setOnClickListener { dismiss() } + cancelButton.setOnClickListener { + cancelClickListener?.onClick() + dismiss() + } if (hasConfirmButton) { confirmButton.apply { isVisible = true @@ -96,6 +101,10 @@ internal class ConfirmationDialogFragment : BottomSheetDialogFragment() { fun onClick() } + fun interface CancelClickListener { + fun onClick() + } + companion object { const val TAG = "ConfirmationDialogFragment" private const val ARG_ICON_RES_ID = "icon_res_id" @@ -133,6 +142,15 @@ internal class ConfirmationDialogFragment : BottomSheetDialogFragment() { cancelText = context.getString(R.string.cancel), ) + fun newHideChannelInstance(context: Context, channelName: String): ConfirmationDialogFragment = newInstance( + iconResId = R.drawable.ic_hide, + iconTintResId = R.color.stream_ui_grey, + title = context.getString(R.string.chat_group_info_option_hide), + description = context.getString(R.string.chat_group_info_option_hide_description, channelName), + confirmText = context.getString(R.string.clear_history), + cancelText = context.getString(R.string.keep_history), + ) + fun newFlagMessageInstance(context: Context): ConfirmationDialogFragment = newInstance( iconResId = R.drawable.stream_ui_ic_flag, iconTintResId = R.color.red, diff --git a/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_hide.xml b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_hide.xml new file mode 100644 index 00000000000..290d0a180f8 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_hide.xml @@ -0,0 +1,5 @@ + + + diff --git a/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_hide_3.xml b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_hide_3.xml new file mode 100644 index 00000000000..c65684a023e --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_hide_3.xml @@ -0,0 +1,5 @@ + + + diff --git a/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_show.xml b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_show.xml new file mode 100644 index 00000000000..9b10da78344 --- /dev/null +++ b/stream-chat-android-ui-components-sample/src/main/res/drawable/ic_show.xml @@ -0,0 +1,5 @@ + + + diff --git a/stream-chat-android-ui-components-sample/src/main/res/layout/chat_info_option_item.xml b/stream-chat-android-ui-components-sample/src/main/res/layout/chat_info_option_item.xml index 4beca309b33..e94ad313983 100644 --- a/stream-chat-android-ui-components-sample/src/main/res/layout/chat_info_option_item.xml +++ b/stream-chat-android-ui-components-sample/src/main/res/layout/chat_info_option_item.xml @@ -47,6 +47,7 @@ app:layout_constraintHorizontal_chainStyle="spread_inside" app:layout_constraintStart_toEndOf="@id/optionImageView" app:layout_constraintTop_toTopOf="@id/optionImageView" + app:layout_constraintHorizontal_bias="0" tools:text="Notifications" /> @@ -56,6 +57,7 @@ android:layout_height="24dp" android:layout_marginEnd="@dimen/spacing_medium" android:src="@drawable/ic_icon_right" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="@id/optionImageView" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/optionTextView" @@ -64,6 +66,23 @@ tools:ignore="ContentDescription" /> + + Cancel Delete Leave + Clear History + Keep History Remove Ok @@ -102,6 +104,9 @@ Owner Mute Group + Hide Group + Show Group + Do you really want to hide the group %s? Leave Group %d more NAME @@ -115,6 +120,7 @@ Failed to remove member Failed to change channel name Failed to switch mute state + Failed to switch hide state Failed to leave channel Failed to add channel member