Merge pull request #3759 from vector-im/feature/bca/improve_leave_space

Feature/bca/improve leave space
This commit is contained in:
Valere 2021-09-16 11:18:50 +02:00 committed by GitHub
commit 8b980e5227
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1224 additions and 91 deletions

1
changelog.d/3692.feature Normal file
View File

@ -0,0 +1 @@
Allow to also leave rooms when leaving a space

View File

@ -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

View File

@ -319,6 +319,7 @@
<activity android:name=".features.spaces.SpaceCreationActivity" />
<activity android:name=".features.spaces.manage.SpaceManageActivity" />
<activity android:name=".features.spaces.people.SpacePeopleActivity" />
<activity android:name=".features.spaces.leave.SpaceLeaveAdvancedActivity" />
<!-- Services -->
<service

View File

@ -146,6 +146,7 @@ import im.vector.app.features.spaces.create.CreateSpaceAdd3pidInvitesFragment
import im.vector.app.features.spaces.create.CreateSpaceDefaultRoomsFragment
import im.vector.app.features.spaces.create.CreateSpaceDetailsFragment
import im.vector.app.features.spaces.explore.SpaceDirectoryFragment
import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedFragment
import im.vector.app.features.spaces.manage.SpaceAddRoomFragment
import im.vector.app.features.spaces.manage.SpaceManageRoomsFragment
import im.vector.app.features.spaces.manage.SpaceSettingsFragment
@ -828,4 +829,9 @@ interface FragmentModule {
@IntoMap
@FragmentKey(RoomJoinRuleChooseRestrictedFragment::class)
fun bindRoomJoinRuleChooseRestrictedFragment(fragment: RoomJoinRuleChooseRestrictedFragment): Fragment
@Binds
@IntoMap
@FragmentKey(SpaceLeaveAdvancedFragment::class)
fun bindSpaceLeaveAdvancedFragment(fragment: SpaceLeaveAdvancedFragment): Fragment
}

View File

@ -84,10 +84,12 @@ 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
import im.vector.app.features.spaces.invite.SpaceInviteBottomSheet
import im.vector.app.features.spaces.leave.SpaceLeaveAdvancedActivity
import im.vector.app.features.spaces.manage.SpaceManageActivity
import im.vector.app.features.spaces.share.ShareSpaceBottomSheet
import im.vector.app.features.terms.ReviewTermsActivity
@ -171,6 +173,7 @@ interface ScreenComponent {
fun inject(activity: SpaceExploreActivity)
fun inject(activity: SpaceManageActivity)
fun inject(activity: RoomJoinRuleActivity)
fun inject(activity: SpaceLeaveAdvancedActivity)
/* ==========================================================================================
* BottomSheets
@ -199,6 +202,7 @@ interface ScreenComponent {
fun inject(bottomSheet: SpaceInviteBottomSheet)
fun inject(bottomSheet: JoinReplacementRoomBottomSheet)
fun inject(bottomSheet: MigrateRoomBottomSheet)
fun inject(bottomSheet: LeaveSpaceBottomSheet)
/* ==========================================================================================
* Others

View File

@ -0,0 +1,196 @@
/*
* 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 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 com.jakewharton.rxbinding3.widget.checkedChanges
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
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.spaces.leave.SpaceLeaveAdvancedActivity
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<BottomSheetLeaveSpaceBinding>() {
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
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()
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()
.observeOn(AndroidSchedulers.mainThread())
.subscribe {
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)
)
}
}
}
.disposeOnDestroyView()
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))
}
}
}
}

View File

@ -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 SpaceLeaveViewAction : VectorViewModelAction {
object SetAutoLeaveAll : SpaceLeaveViewAction()
object SetAutoLeaveNone : SpaceLeaveViewAction()
object SetAutoLeaveSelected : SpaceLeaveViewAction()
object LeaveSpace : SpaceLeaveViewAction()
}

View File

@ -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_NONE,
val leavingState: Async<Unit> = Uninitialized
) : MvRxState {
constructor(args: SpaceBottomSheetSettingsArgs) : this(spaceId = args.spaceId)
enum class LeaveMode {
LEAVE_ALL, LEAVE_NONE, LEAVE_SELECTED
}
}

View File

@ -0,0 +1,171 @@
/*
* 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.Uninitialized
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<SpaceMenuState, SpaceLeaveViewAction, EmptyViewEvents>(initialState) {
@AssistedFactory
interface Factory {
fun create(initialState: SpaceMenuState): SpaceMenuViewModel
}
companion object : MvRxViewModelFactory<SpaceMenuViewModel, SpaceMenuState> {
@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)) }
if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) {
// switch to home?
appStateHandler.setCurrentSpace(null, session)
}
}
}
}.disposeOnClear()
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: SpaceLeaveViewAction) {
when (action) {
SpaceLeaveViewAction.SetAutoLeaveAll -> setState {
copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_ALL, leavingState = Uninitialized)
}
SpaceLeaveViewAction.SetAutoLeaveNone -> setState {
copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_NONE, leavingState = Uninitialized)
}
SpaceLeaveViewAction.SetAutoLeaveSelected -> setState {
copy(leaveMode = SpaceMenuState.LeaveMode.LEAVE_SELECTED, leavingState = Uninitialized)
}
SpaceLeaveViewAction.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
session.getRoomSummaries(
roomSummaryQueryParams {
excludeType = null
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(initialState.spaceId)
memberships = listOf(Membership.JOIN)
}
).forEach {
try {
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)) }
}
}
}
}

View File

@ -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<BottomSheetSpaceSettingsBinding>() {
class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetSpaceSettingsBinding>(), 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<BottomS
fun onShareSpaceSelected(spaceId: String)
}
val settingsViewModel: SpaceMenuViewModel by fragmentViewModel()
var interactionListener: InteractionListener? = null
override val showExpanded = true
@ -91,41 +79,6 @@ class SpaceSettingsMenuBottomSheet : VectorBaseBottomSheetDialogFragment<BottomS
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val session = activeSessionHolder.getSafeActiveSession() ?: return
val roomSummary = session.getRoomSummary(spaceArgs.spaceId)
roomSummary?.toMatrixItem()?.let {
avatarRenderer.render(it, views.spaceAvatarImageView)
}
views.spaceNameView.text = roomSummary?.displayName
views.spaceDescription.setTextOrHide(roomSummary?.topic?.takeIf { it.isNotEmpty() })
val room = session.getRoom(spaceArgs.spaceId) ?: return
PowerLevelsObservableFactory(room)
.createObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { powerLevelContent ->
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<BottomS
}
views.leaveSpace.views.bottomSheetActionClickableZone.debouncedClicks {
val spaceSummary = activeSessionHolder.getSafeActiveSession()?.getRoomSummary(spaceArgs.spaceId)
?: return@debouncedClicks
val warningMessage: CharSequence? = if (spaceSummary.otherMemberIds.isEmpty()) {
span(getString(R.string.space_leave_prompt_msg_only_you)) {
textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
}
} else if (isLastAdmin) {
span(getString(R.string.space_leave_prompt_msg_as_admin)) {
textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
}
} else if (!spaceSummary.isPublic) {
span(getString(R.string.space_leave_prompt_msg_private)) {
textColor = colorProvider.getColorFromAttribute(R.attr.colorError)
}
} else {
null
}
MaterialAlertDialogBuilder(requireContext(), R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive)
.setMessage(warningMessage)
.setTitle(getString(R.string.space_leave_prompt_msg))
.setPositiveButton(R.string.leave) { _, _ ->
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<BottomS
}
}
}
override fun create(initialState: SpaceMenuState): SpaceMenuViewModel {
return viewModelFactory.create(initialState)
}
}

View File

@ -0,0 +1,103 @@
/*
* 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.leave
import com.airbnb.epoxy.TypedEpoxyController
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import im.vector.app.R
import im.vector.app.core.epoxy.loadingItem
import im.vector.app.core.epoxy.noResultItem
import im.vector.app.core.resources.StringProvider
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.spaces.manage.roomSelectionItem
import io.reactivex.functions.Predicate
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class SelectChildrenController @Inject constructor(
val avatarRenderer: AvatarRenderer,
val stringProvider: StringProvider
) : TypedEpoxyController<SpaceLeaveAdvanceViewState>() {
interface Listener {
fun onItemSelected(roomSummary: RoomSummary)
}
var listener: Listener? = null
private val matchFilter = RoomSearchMatchFilter()
override fun buildModels(data: SpaceLeaveAdvanceViewState?) {
val children = data?.allChildren ?: return
val host = this
when (children) {
Uninitialized -> return
is Loading -> {
loadingItem {
id("loading")
}
}
is Success -> {
matchFilter.filter = data.currentFilter
val roomList = children.invoke().filter { matchFilter.test(it) }
if (roomList.isEmpty()) {
noResultItem {
id("empty")
text(host.stringProvider.getString(R.string.no_result_placeholder))
}
} else {
roomList.forEach { item ->
roomSelectionItem {
id(item.roomId)
matrixItem(item.toMatrixItem())
avatarRenderer(host.avatarRenderer)
selected(data.selectedRooms.contains(item.roomId))
itemClickListener {
host.listener?.onItemSelected(item)
}
}
}
}
}
is Fail -> {
// errorWithRetryItem {
// id("failed_to_load")
// }
}
}
}
class RoomSearchMatchFilter : Predicate<RoomSummary> {
var filter: String = ""
override fun test(roomSummary: RoomSummary): Boolean {
if (filter.isEmpty()) {
// No filter
return true
}
// if filter is "Jo Do", it should match "John Doe"
return filter.split(" ").all {
roomSummary.name.contains(it, ignoreCase = true).orFalse()
|| roomSummary.topic.contains(it, ignoreCase = true).orFalse()
}
}
}
}

View File

@ -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.leave
import im.vector.app.core.platform.VectorViewModelAction
sealed class SpaceLeaveAdvanceViewAction : VectorViewModelAction {
data class ToggleSelection(val roomId: String) : SpaceLeaveAdvanceViewAction()
data class UpdateFilter(val filter: String) : SpaceLeaveAdvanceViewAction()
object DoLeave : SpaceLeaveAdvanceViewAction()
object ClearError : SpaceLeaveAdvanceViewAction()
}

View File

@ -0,0 +1,36 @@
/*
* 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.leave
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import im.vector.app.features.spaces.SpaceBottomSheetSettingsArgs
import org.matrix.android.sdk.api.session.room.model.RoomSummary
data class SpaceLeaveAdvanceViewState(
val spaceId: String,
val spaceSummary: RoomSummary? = null,
val allChildren: Async<List<RoomSummary>> = Uninitialized,
val selectedRooms: List<String> = emptyList(),
val currentFilter: String = "",
val leaveState: Async<Unit> = Uninitialized
) : MvRxState {
constructor(args: SpaceBottomSheetSettingsArgs) : this(
spaceId = args.spaceId
)
}

View File

@ -0,0 +1,134 @@
/*
* 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.leave
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.core.view.isGone
import androidx.core.view.isVisible
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.error.ErrorFormatter
import im.vector.app.core.extensions.commitTransaction
import im.vector.app.core.extensions.hideKeyboard
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.ToolbarConfigurable
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivitySimpleLoadingBinding
import im.vector.app.features.spaces.SpaceBottomSheetSettingsArgs
import javax.inject.Inject
class SpaceLeaveAdvancedActivity : VectorBaseActivity<ActivitySimpleLoadingBinding>(),
SpaceLeaveAdvancedViewModel.Factory,
ToolbarConfigurable {
override fun getBinding(): ActivitySimpleLoadingBinding = ActivitySimpleLoadingBinding.inflate(layoutInflater)
val leaveViewModel: SpaceLeaveAdvancedViewModel by viewModel()
@Inject lateinit var viewModelFactory: SpaceLeaveAdvancedViewModel.Factory
@Inject lateinit var errorFormatter: ErrorFormatter
override fun create(initialState: SpaceLeaveAdvanceViewState) = viewModelFactory.create(initialState)
override fun injectWith(injector: ScreenComponent) {
super.injectWith(injector)
injector.inject(this)
}
override fun showWaitingView(text: String?) {
hideKeyboard()
views.waitingView.waitingStatusText.isGone = views.waitingView.waitingStatusText.text.isNullOrBlank()
super.showWaitingView(text)
}
override fun hideWaitingView() {
views.waitingView.waitingStatusText.setTextOrHide(null)
views.waitingView.waitingHorizontalProgress.progress = 0
views.waitingView.waitingHorizontalProgress.isVisible = false
super.hideWaitingView()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val args = intent?.getParcelableExtra<SpaceBottomSheetSettingsArgs>(MvRx.KEY_ARG)
if (isFirstCreation()) {
val simpleName = SpaceLeaveAdvancedFragment::class.java.simpleName
if (supportFragmentManager.findFragmentByTag(simpleName) == null) {
supportFragmentManager.commitTransaction {
replace(
R.id.simpleFragmentContainer,
SpaceLeaveAdvancedFragment::class.java,
Bundle().apply { this.putParcelable(MvRx.KEY_ARG, args) },
simpleName
)
}
}
}
}
override fun initUiAndData() {
super.initUiAndData()
waitingView = views.waitingView.waitingView
leaveViewModel.subscribe(this) { state ->
when (state.leaveState) {
is Loading -> {
showWaitingView()
}
is Success -> {
setResult(RESULT_OK)
finish()
}
is Fail -> {
hideWaitingView()
MaterialAlertDialogBuilder(this)
.setTitle(R.string.dialog_title_error)
.setMessage(errorFormatter.toHumanReadable(state.leaveState.error))
.setPositiveButton(R.string.ok) { _, _ ->
leaveViewModel.handle(SpaceLeaveAdvanceViewAction.ClearError)
}
.show()
}
else -> {
hideWaitingView()
}
}
}
}
companion object {
fun newIntent(context: Context, spaceId: String): Intent {
return Intent(context, SpaceLeaveAdvancedActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, SpaceBottomSheetSettingsArgs(spaceId))
}
}
}
override fun configure(toolbar: MaterialToolbar) {
configureToolbar(toolbar)
}
}

View File

@ -0,0 +1,78 @@
/*
* 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.leave
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.airbnb.mvrx.activityViewModel
import com.airbnb.mvrx.withState
import com.jakewharton.rxbinding3.appcompat.queryTextChanges
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.databinding.FragmentSpaceLeaveAdvancedBinding
import io.reactivex.rxkotlin.subscribeBy
import org.matrix.android.sdk.api.session.room.model.RoomSummary
import java.util.concurrent.TimeUnit
import javax.inject.Inject
class SpaceLeaveAdvancedFragment @Inject constructor(
val controller: SelectChildrenController
) : VectorBaseFragment<FragmentSpaceLeaveAdvancedBinding>(),
SelectChildrenController.Listener {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?) =
FragmentSpaceLeaveAdvancedBinding.inflate(layoutInflater, container, false)
val viewModel: SpaceLeaveAdvancedViewModel by activityViewModel()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupToolbar(views.toolbar)
controller.listener = this
views.roomList.configureWith(controller)
views.spaceLeaveCancel.debouncedClicks { requireActivity().finish() }
views.spaceLeaveButton.debouncedClicks {
viewModel.handle(SpaceLeaveAdvanceViewAction.DoLeave)
}
views.publicRoomsFilter.queryTextChanges()
.debounce(100, TimeUnit.MILLISECONDS)
.subscribeBy {
viewModel.handle(SpaceLeaveAdvanceViewAction.UpdateFilter(it.toString()))
}
.disposeOnDestroyView()
}
override fun onDestroyView() {
controller.listener = null
views.roomList.cleanup()
super.onDestroyView()
}
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
controller.setData(state)
}
override fun onItemSelected(roomSummary: RoomSummary) {
viewModel.handle(SpaceLeaveAdvanceViewAction.ToggleSelection(roomSummary.roomId))
}
}

View File

@ -0,0 +1,141 @@
/*
* 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.leave
import androidx.lifecycle.viewModelScope
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.Uninitialized
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 kotlinx.coroutines.launch
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.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.rx.rx
import timber.log.Timber
class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
@Assisted val initialState: SpaceLeaveAdvanceViewState,
private val session: Session,
private val appStateHandler: AppStateHandler
) : VectorViewModel<SpaceLeaveAdvanceViewState, SpaceLeaveAdvanceViewAction, EmptyViewEvents>(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.getRoom(it)?.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(leaveState = Fail(failure)) }
}
}
}
SpaceLeaveAdvanceViewAction.ClearError -> {
setState { copy(leaveState = Uninitialized) }
}
}
}
init {
val spaceSummary = session.getRoomSummary(initialState.spaceId)
setState { copy(spaceSummary = spaceSummary) }
session.getRoom(initialState.spaceId)?.let { room ->
room.rx().liveRoomSummary().subscribe {
it.getOrNull()?.let {
if (it.membership == Membership.LEAVE) {
setState { copy(leaveState = Success(Unit)) }
if (appStateHandler.safeActiveSpaceId() == initialState.spaceId) {
// switch to home?
appStateHandler.setCurrentSpace(null, session)
}
}
}
}
}
viewModelScope.launch {
val children = session.getRoomSummaries(
roomSummaryQueryParams {
includeType = null
memberships = listOf(Membership.JOIN)
activeSpaceFilter = ActiveSpaceFilter.ActiveSpace(initialState.spaceId)
roomCategoryFilter = RoomCategoryFilter.ONLY_ROOMS
}
)
setState {
copy(allChildren = Success(children))
}
}
}
@AssistedFactory
interface Factory {
fun create(initialState: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel
}
companion object : MvRxViewModelFactory<SpaceLeaveAdvancedViewModel, SpaceLeaveAdvanceViewState> {
override fun create(viewModelContext: ViewModelContext, state: SpaceLeaveAdvanceViewState): SpaceLeaveAdvancedViewModel? {
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")
}
}
}

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:colorBackground"
android:orientation="vertical">
<TextView
android:id="@+id/bottom_leave_space_warning_text"
style="@style/Widget.Vector.TextView.Subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="20dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:layout_marginBottom="8dp"
android:textColor="?vctr_content_primary"
tools:text="@string/space_leave_prompt_msg_with_name" />
<RadioGroup
android:id="@+id/autoLeaveRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="?dialogPreferredPadding"
android:paddingTop="12dp"
android:paddingEnd="?dialogPreferredPadding"
android:paddingBottom="12dp">
<RadioButton
android:id="@+id/leave_all"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/leave_all_rooms_and_spaces"
tools:checked="true" />
<RadioButton
android:id="@+id/leave_none"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="180dp"
android:text="@string/dont_leave_any" />
<RadioButton
android:id="@+id/leave_selected"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minWidth="180dp"
android:text="@string/leave_specific_ones" />
</RadioGroup>
<TextView
android:id="@+id/inlineErrorText"
style="@style/Widget.Vector.TextView.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/layout_horizontal_margin"
android:layout_marginTop="4dp"
android:layout_marginEnd="@dimen/layout_horizontal_margin"
android:textColor="?colorError"
tools:visibility="visible"
tools:text="@string/error_no_network"
android:visibility="gone" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="16dp">
<ProgressBar
android:id="@+id/leaveProgress"
style="?android:attr/progressBarStyle"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/leaveButton"
style="@style/Widget.Vector.Button.Destructive"
android:layout_width="match_parent"
android:layout_height="56dp"
android:layout_gravity="center_horizontal"
android:text="@string/leave_space" />
</FrameLayout>
<Button
android:id="@+id/cancelButton"
style="@style/Widget.Vector.Button.Text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginStart="16dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
android:text="@string/cancel" />
</LinearLayout>

View File

@ -0,0 +1,97 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/coordinatorLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/roomList"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:colorBackground"
android:overScrollMode="always"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:listitem="@layout/item_room_to_add_in_space" />
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- minHeight="0dp" is important to collapse on scroll -->
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:minHeight="0dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:orientation="vertical">
<TextView
android:id="@+id/appBarTitle"
style="@style/Widget.Vector.TextView.HeadlineMedium"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:ellipsize="end"
android:gravity="start|center"
android:maxLines="1"
android:text="@string/pick_tings_to_leave"
android:textColor="?vctr_content_primary"
android:textStyle="bold" />
</LinearLayout>
</com.google.android.material.appbar.MaterialToolbar>
<androidx.appcompat.widget.SearchView
android:id="@+id/publicRoomsFilter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/addRoomToSpaceToolbar"
app:queryHint="@string/search_hint_room_name" />
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:id="@+id/spacePreviewButtonBar"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="?vctr_system"
android:elevation="2dp"
android:orientation="horizontal"
android:padding="8dp">
<Button
android:id="@+id/spaceLeaveCancel"
style="@style/Widget.Vector.Button.Text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:text="@string/cancel" />
<Button
android:id="@+id/spaceLeaveButton"
style="@style/Widget.Vector.Button.Destructive"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/leave_space" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1165,6 +1165,7 @@
<string name="space_explore_activity_title">استكشِف الغُرف</string>
<string name="space_add_child_title">أضف غُرف</string>
<string name="leave_space">غادر المساحة</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">هل أنت متأكد أنك تريد مغادرة المساحة؟</string>
<string name="space_leave_prompt_msg_only_you">انت الشخص الوحيد هنا إذا غادرت، فلن يتمكن أي شخص من الانضمام في المستقبل، بما في ذلك أنت.</string>
<string name="space_leave_prompt_msg_private">هذه المساحة ليست عامة. لن تتمكن من الانضمام مرة أخرى بدون دعوة.</string>

View File

@ -2737,6 +2737,7 @@
<string name="you_are_invited">Jste zváni</string>
<string name="spaces_beta_welcome_to_spaces">Vítejte v prostorech!</string>
<string name="space_add_existing_rooms">Přidat existující místnosti a prostor</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Jste si jisti, že chcete opustit tento prostor\?</string>
<string name="leave_space">Opustit prostor</string>
<string name="space_add_child_title">Přidat místnosti</string>

View File

@ -2770,6 +2770,7 @@
<string name="command_description_leave_room">Verlasse den Raum mit der angegebenen ID (oder den aktuellen Raum, wenn keine ID angegeben wird)</string>
<string name="search_hint_room_name">Name suchen</string>
<string name="you_are_invited">Du wurdest eingeladen</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Bist du dir sicher, dass du den Space verlassen willst\?</string>
<string name="leave_space">Space verlassen</string>
<string name="space_add_child_title">Räume hinzufügen</string>

View File

@ -2674,6 +2674,7 @@
<string name="space_leave_prompt_msg_as_admin">Vi estas administranto de ĉi tiu aro. Certigu, ke vi transdonis administrajn rajtojn al alia ano, antaŭ ol vi vere foriros.</string>
<string name="space_leave_prompt_msg_private">Ĉi tiu aro ne estas publika. Vi ne povos ree aliĝi sen invito.</string>
<string name="space_leave_prompt_msg_only_you">Vi estas la sola persono ĉi tie. Se vi foriros, neniu plu povos aliĝi, inkluzive vin mem.</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Ĉu vi certe volas foriri de la aro\?</string>
<string name="leave_space">Foriri de aro</string>
<string name="space_add_child_title">Aldoni ĉambrojn</string>

View File

@ -2619,6 +2619,7 @@ Por favor permite el acceso en la próxima ventana emergente para descubrir usua
<string name="labs_use_restricted_join_rule">Espacio Experimental - Sala Restringida.</string>
<string name="you_are_invited">Estas invitado</string>
<string name="space_add_rooms">Añadir salas</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Estas seguro de que quieres salir de este espacio\?</string>
<string name="leave_space">Salir de este espacio</string>
<string name="space_add_child_title">Añadir salas</string>

View File

@ -2679,6 +2679,7 @@
<string name="spaces_beta_welcome_to_spaces_desc">Kogukonnakeskused on uus võimalus siduda jututubasid ja inimesi.</string>
<string name="spaces_beta_welcome_to_spaces">Tere tulemast kasutama kogukonnakeskuseid!</string>
<string name="space_add_existing_rooms">Lisa olemasolevaid jututubasid ja kogukonnakeskuseid</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Kas oled kindel, et soovid lahkuda kogukonnakeskusest\?</string>
<string name="leave_space">Lahku kogukonnakeskusest</string>
<string name="space_add_child_title">Lisa jututuba</string>

View File

@ -2679,6 +2679,7 @@
<string name="spaces_beta_welcome_to_spaces_desc">فضاها شیوه‌ای جدید برای گروه‌بندی اتاق‌ها و افراد است.</string>
<string name="spaces_beta_welcome_to_spaces">به فضاها خوش آمدید!</string>
<string name="space_add_existing_rooms">افزودن فضا و اتاق‌های موجود</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">مطمئنید که می‌خواهید فضا را ترک کنید؟</string>
<string name="leave_space">ترک فضا</string>
<string name="space_add_child_title">افزودن اتاق</string>

View File

@ -2748,6 +2748,7 @@
<string name="space_leave_prompt_msg_as_admin">Vous êtes admin de cet espace, assurez-vous davoir transféré les droits dadmin à un autre membre avant de partir.</string>
<string name="space_leave_prompt_msg_private">Cet espace nest pas public. Vous ne pourrez pas le rejoindre sans invitation.</string>
<string name="space_leave_prompt_msg_only_you">Vous êtes la seule personne ici. Si vous partez, personne ne pourra entrer à lavenir, même pas vous.</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Voulez-vous vraiment quitter lespace\?</string>
<string name="leave_space">Quitter lespace</string>
<string name="space_add_child_title">Ajouter des salons</string>

View File

@ -2708,6 +2708,7 @@
<string name="spaces_beta_welcome_to_spaces_desc">Les espaces sont une nouvelle manière de regrouper les salons et les gens.</string>
<string name="spaces_beta_welcome_to_spaces">Bienvenue dans les espaces !</string>
<string name="space_add_existing_rooms">Ajouter des salons et espaces existants</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Voulez-vous vraiment quitter lespace \?</string>
<string name="leave_space">Quitter lespace</string>
<string name="space_add_child_title">Ajouter des salons</string>

View File

@ -2490,6 +2490,7 @@ Ha nem te állítottad be a visszaállítási metódust, akkor egy támadó pró
<string name="space_leave_prompt_msg_as_admin">Most te vagy a tér adminisztrátora, bizonyosodj meg arról, hogy kineveztél mást adminisztrátornak mielőtt elhagyod.</string>
<string name="space_leave_prompt_msg_private">Ez a tér nem nyilvános. Kilépés után csak újabb meghívóval lehet újra belépni.</string>
<string name="space_leave_prompt_msg_only_you">Csak te van itt. Ha kilépsz, akkor a jövőben senki nem tud majd ide belépni, beleértve téged is.</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Biztos el akarod hagyni a teret\?</string>
<string name="leave_space">Tér elhagyása</string>
<string name="space_add_child_title">Szobák hozzáadása</string>

View File

@ -2732,6 +2732,7 @@
<string name="spaces_beta_welcome_to_spaces_desc">Gli Spazi sono un nuovo modo per raggruppare stanze e contatti.</string>
<string name="spaces_beta_welcome_to_spaces">Benvenuto negli Spazi!</string>
<string name="space_add_existing_rooms">Aggiungi stanze e Spazi esistenti</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Vuoi veramente uscire dallo Spazio\?</string>
<string name="leave_space">Esci dallo Spazio</string>
<string name="space_add_child_title">Aggiungi stanze</string>

View File

@ -2815,6 +2815,7 @@
<string name="space_leave_prompt_msg_as_admin">Você é admin deste espaço, assegure-se que você tem transferido direito de admin a um outro membro antes de sair.</string>
<string name="space_leave_prompt_msg_private">Este espaço não é público. Você não vai ser capaz de se rejuntar sem um convite.</string>
<string name="space_leave_prompt_msg_only_you">Você é a única pessoa aqui. Se você sair, ninguém vai ser capaz de se juntar no futuro, incluindo você.</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Você tem certeza que você quer sair do espaço\?</string>
<string name="leave_space">Sair de Espaço</string>
<string name="space_add_child_title">Adicionar salas</string>

View File

@ -2868,6 +2868,7 @@
<string name="space_leave_prompt_msg_as_admin">Вы являетесь администратором этого пространства, перед уходом убедитесь, что передали права администратора другому пользователю.</string>
<string name="space_leave_prompt_msg_private">Это пространство не является публичным. Вы не сможете присоединиться к нему без приглашения.</string>
<string name="space_leave_prompt_msg_only_you">Вы здесь единственный человек. Если вы уйдёте, никто не сможет присоединиться в будущем, включая вас.</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Вы уверены, что хотите покинуть пространство\?</string>
<string name="leave_space">Покинуть пространство</string>
<string name="space_add_child_title">Добавить комнаты</string>

View File

@ -2668,6 +2668,7 @@
<string name="you_are_invited">Jeni ftuar</string>
<string name="spaces_beta_welcome_to_spaces">Mirë se vini te Hapësira!</string>
<string name="space_add_existing_rooms">Shtoni dhoma ekzistuese dhe hapësira</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Jeni i sigurt se doni të dilni nga hapësira\?</string>
<string name="leave_space">Braktiseni Hapësirën</string>
<string name="space_add_child_title">Shtoni dhoma</string>

View File

@ -2737,6 +2737,7 @@
<string name="spaces_beta_welcome_to_spaces_desc">Utrymmen är ett nytt sätt att gruppera rum och personer.</string>
<string name="spaces_beta_welcome_to_spaces">Välkommen till utrymmen!</string>
<string name="space_add_existing_rooms">Lägg till existerande rum och utrymme</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Är du säker på att du vill lämna utrymmet\?</string>
<string name="leave_space">Lämna utrymme</string>
<string name="space_add_child_title">Lägg till rum</string>

View File

@ -2602,6 +2602,7 @@
<string name="space_leave_prompt_msg_as_admin">你是此空间的管理员,请确保你在离开前已将管理权限转让给另一位成员。</string>
<string name="space_leave_prompt_msg_private">此空间并非公开空间。你将无法在没有邀请的情况下重新加入。</string>
<string name="space_leave_prompt_msg_only_you">你是这唯一的人。如果你离开,包括你在内的所有人都将无法加入此空间。</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">你确定你想要离开此空间吗?</string>
<string name="leave_space">离开空间</string>
<string name="space_add_child_title">添加聊天室</string>

View File

@ -2628,6 +2628,7 @@
<string name="spaces_beta_welcome_to_spaces_desc">空間是將聊天室與人們分組的新方式。</string>
<string name="spaces_beta_welcome_to_spaces">歡迎使用空間!</string>
<string name="space_add_existing_rooms">新增既有的聊天室與空間</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">您確定您想要離開空間嗎?</string>
<string name="leave_space">離開空間</string>
<string name="space_add_child_title">新增聊天室</string>

View File

@ -3479,11 +3479,17 @@
<string name="space_explore_activity_title">Explore rooms</string>
<string name="space_add_child_title">Add rooms</string>
<string name="leave_space">Leave Space</string>
<string name="space_leave_prompt_msg_with_name">Are you sure you want to leave %s?</string>
<!-- TO BE REMOVED -->
<string name="space_leave_prompt_msg">Are you sure you want to leave the space?</string>
<string name="space_leave_prompt_msg_only_you">You are the only person here. If you leave, no one will be able to join in the future, including you.</string>
<string name="space_leave_prompt_msg_private">This space is not public. You will not be able to rejoin without an invite.</string>
<string name="space_leave_prompt_msg_as_admin">You are admin of this space, ensure that you have transferred admin right to another member before leaving.</string>
<string name="space_leave_prompt_msg_private">You won\'t be able to rejoin unless you are re-invited.</string>
<string name="space_leave_prompt_msg_as_admin">You\'re the only admin of this space. Leaving it will mean no one has control over it.</string>
<string name="leave_all_rooms_and_spaces">Leave all rooms and spaces</string>
<string name="you_will_leave_all_in">You will leave all rooms and spaces in %s.</string>
<string name="dont_leave_any">Dont leave any rooms and spaces</string>
<string name="leave_specific_ones">Leave specific rooms and spaces…</string>
<string name="pick_tings_to_leave">Pick things to leave</string>
<string name="space_add_existing_rooms">Add existing rooms and space</string>
<string name="space_add_rooms">Add rooms</string>