diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index 391140b9f3..4b0dd1f0a3 100644 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -162,7 +162,7 @@ Formatter\.formatShortFileSize===1 # android\.text\.TextUtils ### This is not a rule, but a warning: the number of "enum class" has changed. For Json classes, it is mandatory that they have `@JsonClass(generateAdapter = false)`. If the enum is not used as a Json class, change the value in file forbidden_strings_in_code.txt -enum class===105 +enum class===106 ### Do not import temporary legacy classes import org.matrix.android.sdk.internal.legacy.riot===3 diff --git a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt index 2ec330efe6..843ec56e5b 100644 --- a/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt +++ b/vector/src/main/java/im/vector/app/core/di/ScreenComponent.kt @@ -84,6 +84,7 @@ import im.vector.app.features.settings.devices.DeviceVerificationInfoBottomSheet import im.vector.app.features.share.IncomingShareActivity import im.vector.app.features.signout.soft.SoftLogoutActivity import im.vector.app.features.spaces.InviteRoomSpaceChooserBottomSheet +import im.vector.app.features.spaces.LeaveSpaceBottomSheet import im.vector.app.features.spaces.SpaceCreationActivity import im.vector.app.features.spaces.SpaceExploreActivity import im.vector.app.features.spaces.SpaceSettingsMenuBottomSheet @@ -199,6 +200,7 @@ interface ScreenComponent { fun inject(bottomSheet: SpaceInviteBottomSheet) fun inject(bottomSheet: JoinReplacementRoomBottomSheet) fun inject(bottomSheet: MigrateRoomBottomSheet) + fun inject(bottomSheet: LeaveSpaceBottomSheet) /* ========================================================================================== * Others 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 new file mode 100644 index 0000000000..909c84fdbf --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/LeaveSpaceBottomSheet.kt @@ -0,0 +1,167 @@ +/* + * 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.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 com.airbnb.mvrx.Loading +import com.airbnb.mvrx.args +import com.airbnb.mvrx.parentFragmentViewModel +import com.airbnb.mvrx.withState +import com.jakewharton.rxbinding3.widget.checkedChanges +import im.vector.app.R +import im.vector.app.core.di.ScreenComponent +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 io.reactivex.android.schedulers.AndroidSchedulers +import kotlinx.parcelize.Parcelize +import me.gujun.android.span.span +import org.matrix.android.sdk.api.util.toMatrixItem +import javax.inject.Inject + +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 + + override fun injectWith(injector: ScreenComponent) { + injector.inject(this) + } + + @Parcelize + data class Args( + val spaceId: String + ) : Parcelable + + override val showExpanded = true + + private val spaceArgs: SpaceBottomSheetSettingsArgs by args() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + views.autoLeaveRadioGroup.checkedChanges() + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { + when (it) { + views.leaveAll.id -> { + settingsViewModel.handle(SpaceMenuViewAction.SetAutoLeaveAll) + } + views.leaveNone.id -> { + settingsViewModel.handle(SpaceMenuViewAction.SetAutoLeaveNone) + } + views.leaveSelected.id -> { + settingsViewModel.handle(SpaceMenuViewAction.SetAutoLeaveSelected) + } + } + } + .disposeOnDestroyView() + + views.leaveButton.debouncedClicks { + settingsViewModel.handle(SpaceMenuViewAction.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, 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) + + 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 + } + + 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) + } + } + } + + 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/SpaceMenuState.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuState.kt new file mode 100644 index 0000000000..23ce9c90b8 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuState.kt @@ -0,0 +1,39 @@ +/* + * 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 com.airbnb.mvrx.Async +import com.airbnb.mvrx.MvRxState +import com.airbnb.mvrx.Uninitialized +import org.matrix.android.sdk.api.session.room.model.RoomSummary + +data class SpaceMenuState( + val spaceId: String, + val spaceSummary: RoomSummary? = null, + val canEditSettings: Boolean = false, + val canInvite: Boolean = false, + val canAddChild: Boolean = false, + val isLastAdmin: Boolean = false, + val leaveMode: LeaveMode = LeaveMode.LEAVE_ALL, + val leavingState: Async = Uninitialized +) : MvRxState { + constructor(args: SpaceBottomSheetSettingsArgs) : this(spaceId = args.spaceId) + + enum class LeaveMode { + LEAVE_ALL, LEAVE_NONE, LEAVE_SELECTED + } +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewAction.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewAction.kt new file mode 100644 index 0000000000..1c5de35861 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewAction.kt @@ -0,0 +1,26 @@ +/* + * 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 im.vector.app.core.platform.VectorViewModelAction + +sealed class SpaceMenuViewAction : VectorViewModelAction { + object SetAutoLeaveAll : SpaceMenuViewAction() + object SetAutoLeaveNone : SpaceMenuViewAction() + object SetAutoLeaveSelected : SpaceMenuViewAction() + object LeaveSpace : SpaceMenuViewAction() +} diff --git a/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt new file mode 100644 index 0000000000..52ebfc4a60 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/spaces/SpaceMenuViewModel.kt @@ -0,0 +1,160 @@ +/* + * 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 com.airbnb.mvrx.ActivityViewModelContext +import com.airbnb.mvrx.Fail +import com.airbnb.mvrx.FragmentViewModelContext +import com.airbnb.mvrx.Loading +import com.airbnb.mvrx.MvRxViewModelFactory +import com.airbnb.mvrx.Success +import com.airbnb.mvrx.ViewModelContext +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import im.vector.app.AppStateHandler +import im.vector.app.core.platform.EmptyViewEvents +import im.vector.app.core.platform.VectorViewModel +import im.vector.app.features.powerlevel.PowerLevelsObservableFactory +import im.vector.app.features.session.coroutineScope +import kotlinx.coroutines.launch +import org.matrix.android.sdk.api.query.ActiveSpaceFilter +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.room.model.Membership +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.rx.rx +import timber.log.Timber + +class SpaceMenuViewModel @AssistedInject constructor( + @Assisted val initialState: SpaceMenuState, + val session: Session, + val appStateHandler: AppStateHandler +) : VectorViewModel(initialState) { + + @AssistedFactory + interface Factory { + fun create(initialState: SpaceMenuState): SpaceMenuViewModel + } + + companion object : MvRxViewModelFactory { + + @JvmStatic + override fun create(viewModelContext: ViewModelContext, state: SpaceMenuState): SpaceMenuViewModel? { + val factory = when (viewModelContext) { + is FragmentViewModelContext -> viewModelContext.fragment as? Factory + is ActivityViewModelContext -> viewModelContext.activity as? Factory + } + return factory?.create(state) ?: error("You should let your activity/fragment implements Factory interface") + } + } + + init { + val roomSummary = session.getRoomSummary(initialState.spaceId) + + setState { + copy(spaceSummary = roomSummary) + } + + session.getRoom(initialState.spaceId)?.let { room -> + + room.rx().liveRoomSummary().subscribe { + it.getOrNull()?.let { + if (it.membership == Membership.LEAVE) { + setState { copy(leavingState = Success(Unit)) } + // switch to home? + appStateHandler.setCurrentSpace(null, session) + } + } + } + PowerLevelsObservableFactory(room) + .createObservable() + .subscribe { + val powerLevelsHelper = PowerLevelsHelper(it) + + val canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId) + val canAddChild = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD) + + val canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR) + val canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME) + val canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC) + + val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin + val otherAdminCount = roomSummary?.otherMemberIds + ?.map { powerLevelsHelper.getUserRole(it) } + ?.count { it is Role.Admin } + ?: 0 + val isLastAdmin = isAdmin && otherAdminCount == 0 + + setState { + copy( + canEditSettings = canChangeAvatar || canChangeName || canChangeTopic, + canInvite = canInvite, + canAddChild = canAddChild, + isLastAdmin = isLastAdmin + ) + } + } + .disposeOnClear() + } + } + + override fun handle(action: SpaceMenuViewAction) { + when (action) { + SpaceMenuViewAction.SetAutoLeaveAll -> setState { copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_ALL) } + SpaceMenuViewAction.SetAutoLeaveNone -> setState { copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_NONE) } + SpaceMenuViewAction.SetAutoLeaveSelected -> setState { copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_SELECTED) } + SpaceMenuViewAction.LeaveSpace -> handleLeaveSpace() + } + } + + private fun handleLeaveSpace() = withState { state -> + + setState { copy(leavingState = Loading()) } + + session.coroutineScope.launch { + try { + if (state.leaveMode == SpaceMenuState.LeaveMode.LEAVE_NONE) { + session.getRoom(initialState.spaceId)?.leave(null) + } else if (state.leaveMode == SpaceMenuState.LeaveMode.LEAVE_ALL) { + // need to find all child rooms that i have joined + try { + session.getRoomSummaries( + roomSummaryQueryParams { + excludeType = null + activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(initialState.spaceId) + memberships = listOf(Membership.JOIN) + } + ).forEach { + session.getRoom(it.roomId)?.leave(null) + } + } catch (failure: Throwable) { + // silently ignore? + Timber.e(failure, "Fail to leave sub rooms/spaces") + } + session.getRoom(initialState.spaceId)?.leave(null) + } + + // We observe the membership and to dismiss when we have remote echo of leaving + } catch (failure: Throwable) { + setState { copy(leavingState = Fail(failure)) } + } + } + } +} 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 37c5088123..fa035bebc9 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 @@ -22,35 +22,24 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.view.isVisible +import com.airbnb.mvrx.Success import com.airbnb.mvrx.args -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import im.vector.app.R -import im.vector.app.core.di.ActiveSessionHolder +import com.airbnb.mvrx.fragmentViewModel +import com.airbnb.mvrx.withState import im.vector.app.core.di.ScreenComponent 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.databinding.BottomSheetSpaceSettingsBinding import im.vector.app.features.home.AvatarRenderer import im.vector.app.features.navigation.Navigator -import im.vector.app.features.powerlevel.PowerLevelsObservableFactory import im.vector.app.features.rageshake.BugReporter import im.vector.app.features.rageshake.ReportType import im.vector.app.features.roomprofile.RoomProfileActivity -import im.vector.app.features.session.coroutineScope -import im.vector.app.features.settings.VectorPreferences import im.vector.app.features.spaces.manage.ManageType import im.vector.app.features.spaces.manage.SpaceManageActivity -import io.reactivex.android.schedulers.AndroidSchedulers -import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize -import me.gujun.android.span.span import org.matrix.android.sdk.api.extensions.orFalse -import org.matrix.android.sdk.api.session.events.model.EventType -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.util.toMatrixItem -import timber.log.Timber import javax.inject.Inject @Parcelize @@ -58,15 +47,12 @@ data class SpaceBottomSheetSettingsArgs( val spaceId: String ) : Parcelable -// XXX make proper view model before leaving beta -class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment() { +class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment(), SpaceMenuViewModel.Factory { @Inject lateinit var navigator: Navigator - @Inject lateinit var activeSessionHolder: ActiveSessionHolder @Inject lateinit var avatarRenderer: AvatarRenderer - @Inject lateinit var vectorPreferences: VectorPreferences @Inject lateinit var bugReporter: BugReporter - @Inject lateinit var colorProvider: ColorProvider + @Inject lateinit var viewModelFactory: SpaceMenuViewModel.Factory private val spaceArgs: SpaceBottomSheetSettingsArgs by args() @@ -74,6 +60,8 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment - val powerLevelsHelper = PowerLevelsHelper(powerLevelContent) - val canInvite = powerLevelsHelper.isUserAbleToInvite(session.myUserId) - val canAddChild = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_SPACE_CHILD) - - val canChangeAvatar = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_AVATAR) - val canChangeName = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_NAME) - val canChangeTopic = powerLevelsHelper.isUserAllowedToSend(session.myUserId, true, EventType.STATE_ROOM_TOPIC) - - views.spaceSettings.isVisible = canChangeAvatar || canChangeName || canChangeTopic - - views.invitePeople.isVisible = canInvite || roomSummary?.isPublic.orFalse() - views.addRooms.isVisible = canAddChild - - val isAdmin = powerLevelsHelper.getUserRole(session.myUserId) is Role.Admin - val otherAdminCount = roomSummary?.otherMemberIds - ?.map { powerLevelsHelper.getUserRole(it) } - ?.count { it is Role.Admin } - ?: 0 - isLastAdmin = isAdmin && otherAdminCount == 0 - }.disposeOnDestroyView() - views.spaceBetaTag.debouncedClicks { bugReporter.openBugReportScreen(requireActivity(), ReportType.SPACE_BETA_FEEDBACK) } @@ -154,42 +107,29 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment - session.coroutineScope.launch { - try { - session.getRoom(spaceArgs.spaceId)?.leave(null) - } catch (failure: Throwable) { - Timber.e(failure, "Failed to leave space") - } - } - dismiss() - } - .setNegativeButton(R.string.cancel, null) - .show() + LeaveSpaceBottomSheet.newInstance(spaceArgs.spaceId).show(childFragmentManager, "LOGOUT") } } + override fun invalidate() = withState(settingsViewModel) { state -> + super.invalidate() + + if (state.leavingState is Success) { + dismiss() + } + + state.spaceSummary?.toMatrixItem()?.let { + avatarRenderer.render(it, views.spaceAvatarImageView) + } + views.spaceNameView.text = state.spaceSummary?.displayName + views.spaceDescription.setTextOrHide(state.spaceSummary?.topic?.takeIf { it.isNotEmpty() }) + + views.spaceSettings.isVisible = state.canEditSettings + + views.invitePeople.isVisible = state.canInvite || state.spaceSummary?.isPublic.orFalse() + views.addRooms.isVisible = state.canAddChild + } + companion object { fun newInstance(spaceId: String, interactionListener: InteractionListener): SpaceSettingsMenuBottomSheet { return SpaceSettingsMenuBottomSheet().apply { @@ -198,4 +138,8 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment + + + + + + + + + + + + + + + + + + + + +