From 38641c88b71ac3300829a3bb3304712adc1f6662 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 18 May 2022 20:51:41 +0200 Subject: [PATCH 1/3] Fix usage of System.currentTimeMillis(). This a bit mocky but anyway it's better to use SystemClock.elapsedRealtime() for this case. --- .../java/im/vector/lib/core/utils/flow/TimingOperators.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt b/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt index 2efb439ace..aeb5ae7914 100644 --- a/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt +++ b/library/core-utils/src/main/java/im/vector/lib/core/utils/flow/TimingOperators.kt @@ -16,6 +16,7 @@ package im.vector.lib.core.utils.flow +import android.os.SystemClock import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel @@ -68,10 +69,10 @@ fun Flow.chunk(durationInMillis: Long): Flow> { @ExperimentalCoroutinesApi fun Flow.throttleFirst(windowDuration: Long): Flow = flow { - var windowStartTime = System.currentTimeMillis() + var windowStartTime = SystemClock.elapsedRealtime() var emitted = false collect { value -> - val currentTime = System.currentTimeMillis() + val currentTime = SystemClock.elapsedRealtime() val delta = currentTime - windowStartTime if (delta >= windowDuration) { windowStartTime += delta / windowDuration * windowDuration From 18842b5e3d2a89b6b5eba691ad87f6b10d6a461a Mon Sep 17 00:00:00 2001 From: fedrunov <66663241+fedrunov@users.noreply.github.com> Date: Thu, 19 May 2022 11:49:08 +0200 Subject: [PATCH 2/3] leaving space aligned with ios (#5942) --- changelog.d/5728.misc | 1 + .../app/ui/robot/space/SpaceMenuRobot.kt | 3 +- .../utils/ToggleableAppBarLayoutBehavior.kt | 67 ++++++ .../features/spaces/LeaveSpaceBottomSheet.kt | 194 ------------------ .../spaces/SpaceSettingsMenuBottomSheet.kt | 3 +- .../leave/SpaceLeaveAdvanceViewAction.kt | 3 + .../leave/SpaceLeaveAdvanceViewState.kt | 5 +- .../leave/SpaceLeaveAdvancedFragment.kt | 115 +++++++++-- .../leave/SpaceLeaveAdvancedViewModel.kt | 119 +++++++---- .../res/layout/bottom_sheet_leave_space.xml | 105 ---------- .../layout/fragment_space_leave_advanced.xml | 113 +++++++--- vector/src/main/res/menu/menu_space_leave.xml | 13 ++ vector/src/main/res/values/strings.xml | 18 +- 13 files changed, 372 insertions(+), 387 deletions(-) create mode 100644 changelog.d/5728.misc create mode 100644 vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt delete mode 100644 vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt delete mode 100644 vector/src/main/res/layout/bottom_sheet_leave_space.xml create mode 100644 vector/src/main/res/menu/menu_space_leave.xml diff --git a/changelog.d/5728.misc b/changelog.d/5728.misc new file mode 100644 index 0000000000..6e463fa76f --- /dev/null +++ b/changelog.d/5728.misc @@ -0,0 +1 @@ +leaving space experience changed to be aligned with iOS diff --git a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt index 4d35e3c550..289c6e21b4 100644 --- a/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt +++ b/vector/src/androidTest/java/im/vector/app/ui/robot/space/SpaceMenuRobot.kt @@ -104,11 +104,10 @@ class SpaceMenuRobot { fun leaveSpace() { clickOnSheet(R.id.leaveSpace) - waitUntilDialogVisible(ViewMatchers.withId(R.id.leaveButton)) - clickOn(R.id.leave_selected) waitUntilActivityVisible { waitUntilViewVisible(ViewMatchers.withId(R.id.roomList)) } + clickOn(R.id.spaceLeaveSelectAll) clickOn(R.id.spaceLeaveButton) waitUntilViewVisible(ViewMatchers.withId(R.id.groupListView)) } diff --git a/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt b/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt new file mode 100644 index 0000000000..c829313256 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/ToggleableAppBarLayoutBehavior.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.vector.app.core.utils + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.google.android.material.appbar.AppBarLayout + +/** + * [AppBarLayout.Behavior] subclass with a possibility to disable behavior. + * Useful for cases when in some view state we want prevent toolbar from collapsing/expanding by scroll events + */ +class ToggleableAppBarLayoutBehavior : AppBarLayout.Behavior { + constructor() : super() + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + + var isEnabled = true + + override fun onStartNestedScroll(parent: CoordinatorLayout, + child: AppBarLayout, + directTargetChild: View, + target: View, + nestedScrollAxes: Int, + type: Int): Boolean { + return isEnabled && super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type) + } + + override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, + child: AppBarLayout, + target: View, + dxConsumed: Int, + dyConsumed: Int, + dxUnconsumed: Int, + dyUnconsumed: Int, + type: Int, + consumed: IntArray) { + if (!isEnabled) return + super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed) + } + + override fun onNestedPreScroll(coordinatorLayout: CoordinatorLayout, + child: AppBarLayout, + target: View, + dx: Int, + dy: Int, + consumed: IntArray, + type: Int) { + if (!isEnabled) return + super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type) + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt deleted file mode 100644 index a292b64ddd..0000000000 --- a/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (c) 2021 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package im.vector.app.features.spaces - -import android.app.Activity -import android.graphics.Typeface -import android.os.Bundle -import android.os.Parcelable -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.text.toSpannable -import androidx.core.view.isInvisible -import androidx.core.view.isVisible -import androidx.lifecycle.lifecycleScope -import com.airbnb.mvrx.Fail -import com.airbnb.mvrx.Loading -import com.airbnb.mvrx.args -import com.airbnb.mvrx.parentFragmentViewModel -import com.airbnb.mvrx.withState -import dagger.hilt.android.AndroidEntryPoint -import im.vector.app.R -import im.vector.app.core.error.ErrorFormatter -import im.vector.app.core.extensions.registerStartForActivityResult -import im.vector.app.core.extensions.setTextOrHide -import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment -import im.vector.app.core.resources.ColorProvider -import im.vector.app.core.utils.styleMatchingText -import im.vector.app.databinding.BottomSheetLeaveSpaceBinding -import im.vector.app.features.displayname.getBestName -import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.parcelize.Parcelize -import me.gujun.android.span.span -import org.matrix.android.sdk.api.util.toMatrixItem -import reactivecircus.flowbinding.android.widget.checkedChanges -import javax.inject.Inject - -@AndroidEntryPoint -class LeaveSpaceBottomSheet : VectorBaseBottomSheetDialogFragment() { - - val settingsViewModel: SpaceMenuViewModel by parentFragmentViewModel() - - override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetLeaveSpaceBinding { - return BottomSheetLeaveSpaceBinding.inflate(inflater, container, false) - } - - @Inject lateinit var colorProvider: ColorProvider - @Inject lateinit var errorFormatter: ErrorFormatter - - @Parcelize - data class Args( - val spaceId: String - ) : Parcelable - - override val showExpanded = true - - private val spaceArgs: SpaceBottomSheetSettingsArgs by args() - - private val cherryPickLeaveActivityResult = registerStartForActivityResult { activityResult -> - if (activityResult.resultCode == Activity.RESULT_OK) { - // nothing actually? - } else { - // move back to default - settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - views.autoLeaveRadioGroup.checkedChanges() - .onEach { - when (it) { - views.leaveAll.id -> { - settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveAll) - } - views.leaveNone.id -> { - settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveNone) - } - views.leaveSelected.id -> { - settingsViewModel.handle(SpaceLeaveViewAction.SetAutoLeaveSelected) - // launch dedicated activity - cherryPickLeaveActivityResult.launch( - SpaceLeaveAdvancedActivity.newIntent(requireContext(), spaceArgs.spaceId) - ) - } - } - } - .launchIn(viewLifecycleOwner.lifecycleScope) - - views.leaveButton.debouncedClicks { - settingsViewModel.handle(SpaceLeaveViewAction.LeaveSpace) - } - - views.cancelButton.debouncedClicks { - dismiss() - } - } - - override fun invalidate() = withState(settingsViewModel) { state -> - super.invalidate() - - val spaceSummary = state.spaceSummary ?: return@withState - val bestName = spaceSummary.toMatrixItem().getBestName() - val commonText = getString(R.string.space_leave_prompt_msg_with_name, bestName) - .toSpannable().styleMatchingText(bestName, Typeface.BOLD) - - val warningMessage: CharSequence = if (spaceSummary.otherMemberIds.isEmpty()) { - span { - +commonText - +"\n\n" - span(getString(R.string.space_leave_prompt_msg_only_you)) { - textColor = colorProvider.getColorFromAttribute(R.attr.colorError) - } - } - } else if (state.isLastAdmin) { - span { - +commonText - +"\n\n" - span(getString(R.string.space_leave_prompt_msg_as_admin)) { - textColor = colorProvider.getColorFromAttribute(R.attr.colorError) - } - } - } else if (!spaceSummary.isPublic) { - span { - +commonText - +"\n\n" - span(getString(R.string.space_leave_prompt_msg_private)) { - textColor = colorProvider.getColorFromAttribute(R.attr.colorError) - } - } - } else { - commonText - } - - views.bottomLeaveSpaceWarningText.setTextOrHide(warningMessage) - - views.inlineErrorText.setTextOrHide(null) - if (state.leavingState is Loading) { - views.leaveButton.isInvisible = true - views.cancelButton.isInvisible = true - views.leaveProgress.isVisible = true - } else { - views.leaveButton.isInvisible = false - views.cancelButton.isInvisible = false - views.leaveProgress.isVisible = false - if (state.leavingState is Fail) { - views.inlineErrorText.setTextOrHide(errorFormatter.toHumanReadable(state.leavingState.error)) - } - } - - val hasChildren = (spaceSummary.spaceChildren?.size ?: 0) > 0 - if (hasChildren) { - views.autoLeaveRadioGroup.isVisible = true - when (state.leaveMode) { - SpaceMenuState.LeaveMode.LEAVE_ALL -> { - views.autoLeaveRadioGroup.check(views.leaveAll.id) - } - SpaceMenuState.LeaveMode.LEAVE_NONE -> { - views.autoLeaveRadioGroup.check(views.leaveNone.id) - } - SpaceMenuState.LeaveMode.LEAVE_SELECTED -> { - views.autoLeaveRadioGroup.check(views.leaveSelected.id) - } - } - } else { - views.autoLeaveRadioGroup.isVisible = false - } - } - - companion object { - - fun newInstance(spaceId: String): LeaveSpaceBottomSheet { - return LeaveSpaceBottomSheet().apply { - setArguments(SpaceBottomSheetSettingsArgs(spaceId)) - } - } - } -} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt index 78eab5b97f..94aa7e19b8 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceSettingsMenuBottomSheet.kt @@ -35,6 +35,7 @@ import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.navigation.Navigator import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.roomprofile.RoomProfileActivity +import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceManageActivity import kotlinx.parcelize.Parcelize @@ -109,7 +110,7 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment> = Uninitialized, val selectedRooms: List = emptyList(), val currentFilter: String = "", - val leaveState: Async = Uninitialized + val leaveState: Async = Uninitialized, + val isFilteringEnabled: Boolean = false, + val isLastAdmin: Boolean = false ) : MavericksState { + constructor(args: SpaceBottomSheetSettingsArgs) : this( spaceId = args.spaceId ) diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt index 53c7481acb..308572a30f 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedFragment.kt @@ -18,20 +18,23 @@ package im.vector.app.features.spaces.leave import android.os.Bundle import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuItem import android.view.View import android.view.ViewGroup -import androidx.lifecycle.lifecycleScope +import androidx.appcompat.widget.SearchView +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.isVisible +import com.airbnb.mvrx.Success import com.airbnb.mvrx.activityViewModel import com.airbnb.mvrx.withState +import im.vector.app.R import im.vector.app.core.extensions.cleanup import im.vector.app.core.extensions.configureWith import im.vector.app.core.platform.VectorBaseFragment +import im.vector.app.core.utils.ToggleableAppBarLayoutBehavior import im.vector.app.databinding.FragmentSpaceLeaveAdvancedBinding -import kotlinx.coroutines.flow.debounce -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach import org.matrix.android.sdk.api.session.room.model.RoomSummary -import reactivecircus.flowbinding.appcompat.queryTextChanges import javax.inject.Inject class SpaceLeaveAdvancedFragment @Inject constructor( @@ -44,11 +47,33 @@ class SpaceLeaveAdvancedFragment @Inject constructor( val viewModel: SpaceLeaveAdvancedViewModel by activityViewModel() + override fun getMenuRes() = R.menu.menu_space_leave + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - setupToolbar(views.toolbar) - .allowBack() + controller.listener = this + + withState(viewModel) { state -> + setupToolbar(views.toolbar) + .setSubtitle(state.spaceSummary?.name) + .allowBack() + + state.spaceSummary?.let { summary -> + val warningMessage: CharSequence? = when { + summary.otherMemberIds.isEmpty() -> getString(R.string.space_leave_prompt_msg_only_you) + state.isLastAdmin -> getString(R.string.space_leave_prompt_msg_as_admin) + !summary.isPublic -> getString(R.string.space_leave_prompt_msg_private) + else -> null + } + + views.spaceLeavePromptDescription.isVisible = warningMessage != null + views.spaceLeavePromptDescription.text = warningMessage + } + + views.spaceLeavePromptTitle.text = getString(R.string.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "") + } + views.roomList.configureWith(controller) views.spaceLeaveCancel.debouncedClicks { requireActivity().finish() } @@ -56,12 +81,23 @@ class SpaceLeaveAdvancedFragment @Inject constructor( viewModel.handle(SpaceLeaveAdvanceViewAction.DoLeave) } - views.publicRoomsFilter.queryTextChanges() - .debounce(100) - .onEach { - viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it.toString())) - } - .launchIn(viewLifecycleOwner.lifecycleScope) + views.spaceLeaveSelectGroup.setOnCheckedChangeListener { _, optionId -> + when (optionId) { + R.id.spaceLeaveSelectAll -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectAll) + R.id.spaceLeaveSelectNone -> viewModel.handle(SpaceLeaveAdvanceViewAction.SelectNone) + } + } + } + + override fun onPrepareOptionsMenu(menu: Menu) { + menu.findItem(R.id.menu_space_leave_search)?.let { searchItem -> + searchItem.bind( + onExpanded = { viewModel.handle(SpaceLeaveAdvanceViewAction.SetFilteringEnabled(isEnabled = true)) }, + onCollapsed = { viewModel.handle(SpaceLeaveAdvanceViewAction.SetFilteringEnabled(isEnabled = false)) }, + onTextChanged = { viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it)) } + ) + } + super.onPrepareOptionsMenu(menu) } override fun onDestroyView() { @@ -72,10 +108,63 @@ class SpaceLeaveAdvancedFragment @Inject constructor( override fun invalidate() = withState(viewModel) { state -> super.invalidate() + + if (state.isFilteringEnabled) { + views.appBarLayout.setExpanded(false) + } + + updateAppBarBehaviorState(state) + updateRadioButtonsState(state) + controller.setData(state) } override fun onItemSelected(roomSummary: RoomSummary) { viewModel.handle(SpaceLeaveAdvanceViewAction.ToggleSelection(roomSummary.roomId)) } + + private fun updateAppBarBehaviorState(state: SpaceLeaveAdvanceViewState) { + val behavior = (views.appBarLayout.layoutParams as CoordinatorLayout.LayoutParams).behavior as ToggleableAppBarLayoutBehavior + behavior.isEnabled = !state.isFilteringEnabled + } + + private fun updateRadioButtonsState(state: SpaceLeaveAdvanceViewState) { + (state.allChildren as? Success)?.invoke()?.size?.let { allChildrenCount -> + when (state.selectedRooms.size) { + 0 -> views.spaceLeaveSelectNone.isChecked = true + allChildrenCount -> views.spaceLeaveSelectAll.isChecked = true + else -> views.spaceLeaveSelectSemi.isChecked = true + } + } + } + + private fun MenuItem.bind( + onExpanded: () -> Unit, + onCollapsed: () -> Unit, + onTextChanged: (String) -> Unit) { + setOnActionExpandListener(object : MenuItem.OnActionExpandListener { + override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + onExpanded() + return true + } + + override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + onCollapsed() + return true + } + }) + + val searchView = actionView as SearchView + + searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + onTextChanged(newText ?: "") + return true + } + }) + } } diff --git a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt index 3f5a27f696..2ab417ac55 100644 --- a/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/spaces/leave/SpaceLeaveAdvancedViewModel.kt @@ -36,9 +36,14 @@ import okhttp3.internal.toImmutableList import org.matrix.android.sdk.api.query.ActiveSpaceFilter import org.matrix.android.sdk.api.query.RoomCategoryFilter import org.matrix.android.sdk.api.session.Session +import org.matrix.android.sdk.api.session.events.model.EventType +import org.matrix.android.sdk.api.session.events.model.toModel import org.matrix.android.sdk.api.session.getRoom -import org.matrix.android.sdk.api.session.getRoomSummary +import org.matrix.android.sdk.api.session.room.getStateEvent import org.matrix.android.sdk.api.session.room.model.Membership +import org.matrix.android.sdk.api.session.room.model.PowerLevelsContent +import org.matrix.android.sdk.api.session.room.powerlevels.PowerLevelsHelper +import org.matrix.android.sdk.api.session.room.powerlevels.Role import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams import org.matrix.android.sdk.flow.flow import org.matrix.android.sdk.flow.unwrap @@ -50,52 +55,24 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor( private val appStateHandler: AppStateHandler ) : VectorViewModel(initialState) { - override fun handle(action: SpaceLeaveAdvanceViewAction) = withState { state -> - when (action) { - is SpaceLeaveAdvanceViewAction.ToggleSelection -> { - val existing = state.selectedRooms.toMutableList() - if (existing.contains(action.roomId)) { - existing.remove(action.roomId) - } else { - existing.add(action.roomId) - } - setState { - copy( - selectedRooms = existing.toImmutableList() - ) - } - } - is SpaceLeaveAdvanceViewAction.UpdateFilter -> { - setState { copy(currentFilter = action.filter) } - } - SpaceLeaveAdvanceViewAction.DoLeave -> { - setState { copy(leaveState = Loading()) } - viewModelScope.launch { - try { - state.selectedRooms.forEach { - try { - session.roomService().leaveRoom(it) - } catch (failure: Throwable) { - // silently ignore? - Timber.e(failure, "Fail to leave sub rooms/spaces") - } - } + init { + val space = session.getRoom(initialState.spaceId) + val spaceSummary = space?.roomSummary() - session.spaceService().leaveSpace(initialState.spaceId) - // We observe the membership and to dismiss when we have remote echo of leaving - } catch (failure: Throwable) { - setState { copy(leaveState = Fail(failure)) } - } - } - } - SpaceLeaveAdvanceViewAction.ClearError -> { - setState { copy(leaveState = Uninitialized) } + val powerLevelsEvent = space?.getStateEvent(EventType.STATE_ROOM_POWER_LEVELS) + powerLevelsEvent?.content?.toModel()?.let { powerLevelsContent -> + val powerLevelsHelper = PowerLevelsHelper(powerLevelsContent) + val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin + val otherAdminCount = spaceSummary?.otherMemberIds + ?.map { powerLevelsHelper.getUserRole(it) } + ?.count { it is Role.Admin } + ?: 0 + val isLastAdmin = isAdmin && otherAdminCount == 0 + setState { + copy(isLastAdmin = isLastAdmin) } } - } - init { - val spaceSummary = session.getRoomSummary(initialState.spaceId) setState { copy(spaceSummary = spaceSummary) } session.getRoom(initialState.spaceId)?.let { room -> room.flow().liveRoomSummary() @@ -127,6 +104,62 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor( } } + override fun handle(action: SpaceLeaveAdvanceViewAction) { + when (action) { + is SpaceLeaveAdvanceViewAction.UpdateFilter -> setState { copy(currentFilter = action.filter) } + SpaceLeaveAdvanceViewAction.ClearError -> setState { copy(leaveState = Uninitialized) } + SpaceLeaveAdvanceViewAction.SelectNone -> setState { copy(selectedRooms = emptyList()) } + is SpaceLeaveAdvanceViewAction.SetFilteringEnabled -> setState { copy(isFilteringEnabled = action.isEnabled) } + is SpaceLeaveAdvanceViewAction.ToggleSelection -> handleSelectionToggle(action) + SpaceLeaveAdvanceViewAction.DoLeave -> handleLeave() + SpaceLeaveAdvanceViewAction.SelectAll -> handleSelectAll() + } + } + + private fun handleSelectAll() = withState { state -> + val filteredRooms = (state.allChildren as? Success)?.invoke()?.filter { + it.name.contains(state.currentFilter, true) + } + filteredRooms?.let { + setState { copy(selectedRooms = it.map { it.roomId }) } + } + } + + private fun handleLeave() = withState { state -> + setState { copy(leaveState = Loading()) } + viewModelScope.launch { + try { + state.selectedRooms.forEach { + try { + session.roomService().leaveRoom(it) + } catch (failure: Throwable) { + // silently ignore? + Timber.e(failure, "Fail to leave sub rooms/spaces") + } + } + + session.spaceService().leaveSpace(initialState.spaceId) + // We observe the membership and to dismiss when we have remote echo of leaving + } catch (failure: Throwable) { + setState { copy(leaveState = Fail(failure)) } + } + } + } + + private fun handleSelectionToggle(action: SpaceLeaveAdvanceViewAction.ToggleSelection) = withState { state -> + val existing = state.selectedRooms.toMutableList() + if (existing.contains(action.roomId)) { + existing.remove(action.roomId) + } else { + existing.add(action.roomId) + } + setState { + copy( + selectedRooms = existing.toImmutableList(), + ) + } + } + @AssistedFactory interface Factory : MavericksAssistedViewModelFactory { override fun create(initialState: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel diff --git a/vector/src/main/res/layout/bottom_sheet_leave_space.xml b/vector/src/main/res/layout/bottom_sheet_leave_space.xml deleted file mode 100644 index 9e5a7c7ebf..0000000000 --- a/vector/src/main/res/layout/bottom_sheet_leave_space.xml +++ /dev/null @@ -1,105 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - -