1
0
mirror of https://github.com/vector-im/riotX-android synced 2025-10-05 15:52:47 +02:00

change (leave room) : warn on last admin when leaving rooms

This commit is contained in:
ganfra
2025-07-29 16:34:23 +02:00
parent 2d21c15e3b
commit 864346c3c0
14 changed files with 163 additions and 96 deletions

View File

@@ -25,7 +25,6 @@ import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.R
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
@@ -45,6 +44,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.home.room.list.widget.NotifsFabMenuView
import im.vector.app.features.matrixto.OriginOfMatrixTo
import im.vector.app.features.notifications.NotificationDrawerManager
import im.vector.app.features.room.LeaveRoomPrompt
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
@@ -422,7 +422,7 @@ class RoomListFragment :
}
}
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
private suspend fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
when (quickAction) {
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
roomListViewModel.handle(RoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY))
@@ -451,26 +451,11 @@ class RoomListFragment :
}
}
private fun promptLeaveRoom(roomId: String) {
val isPublicRoom = roomListViewModel.isPublicRoom(roomId)
val message = buildString {
append(getString(CommonStrings.room_participants_leave_prompt_msg))
if (!isPublicRoom) {
append("\n\n")
append(getString(CommonStrings.room_participants_leave_private_warning))
}
private suspend fun promptLeaveRoom(roomId: String) {
val warning = roomListViewModel.getLeaveRoomWarning(roomId)
LeaveRoomPrompt.show(requireContext(), warning) {
roomListViewModel.handle(RoomListAction.LeaveRoom(roomId))
}
MaterialAlertDialogBuilder(
requireContext(),
if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive
)
.setTitle(CommonStrings.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(CommonStrings.action_leave) { _, _ ->
roomListViewModel.handle(RoomListAction.LeaveRoom(roomId))
}
.setNegativeButton(CommonStrings.action_cancel, null)
.show()
}
override fun invalidate() = withState(roomListViewModel) { state ->

View File

@@ -26,6 +26,8 @@ import im.vector.app.features.analytics.extensions.toAnalyticsJoinedRoom
import im.vector.app.features.analytics.plan.JoinedRoom
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.invite.AutoAcceptInvites
import im.vector.app.features.room.LeaveRoomPrompt
import im.vector.app.features.room.getLeaveRoomWarning
import im.vector.app.features.settings.VectorPreferences
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -41,7 +43,6 @@ import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.localecho.RoomLocalEcho
import org.matrix.android.sdk.api.session.room.model.tag.RoomTag
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.api.session.room.state.isPublic
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.flow.flow
import timber.log.Timber
@@ -150,10 +151,11 @@ class RoomListViewModel @AssistedInject constructor(
}
}
fun isPublicRoom(roomId: String): Boolean {
return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
suspend fun getLeaveRoomWarning(roomId: String): LeaveRoomPrompt.Warning {
return session.getLeaveRoomWarning(roomId)
}
// PRIVATE METHODS *****************************************************************************
private fun handleSelectRoom(action: RoomListAction.SelectRoom) = withState {

View File

@@ -18,7 +18,6 @@ import androidx.recyclerview.widget.ConcatAdapter.Config.StableIdMode
import androidx.recyclerview.widget.LinearLayoutManager
import com.airbnb.epoxy.OnModelBuildFinishedListener
import com.airbnb.mvrx.fragmentViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import im.vector.app.core.epoxy.LayoutManagerStateRestorer
import im.vector.app.core.extensions.cleanup
@@ -36,7 +35,7 @@ import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedA
import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
import im.vector.app.features.home.room.list.home.header.HomeRoomsHeadersController
import im.vector.app.features.home.room.list.home.invites.InvitesActivity
import im.vector.lib.strings.CommonStrings
import im.vector.app.features.room.LeaveRoomPrompt
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.matrix.android.sdk.api.session.room.model.RoomSummary
@@ -103,7 +102,7 @@ class HomeRoomListFragment :
}
}
private fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
private suspend fun handleQuickActions(quickAction: RoomListQuickActionsSharedAction) {
when (quickAction) {
is RoomListQuickActionsSharedAction.NotificationsAllNoisy -> {
roomListViewModel.handle(HomeRoomListAction.ChangeRoomNotificationState(quickAction.roomId, RoomNotificationState.ALL_MESSAGES_NOISY))
@@ -185,26 +184,11 @@ class HomeRoomListFragment :
concatAdapter.addAdapter(roomsAdapter)
}
private fun promptLeaveRoom(roomId: String) {
val isPublicRoom = roomListViewModel.isPublicRoom(roomId)
val message = buildString {
append(getString(CommonStrings.room_participants_leave_prompt_msg))
if (!isPublicRoom) {
append("\n\n")
append(getString(CommonStrings.room_participants_leave_private_warning))
}
private suspend fun promptLeaveRoom(roomId: String) {
val warning = roomListViewModel.getLeaveRoomWarning(roomId)
LeaveRoomPrompt.show(requireContext(), warning) {
roomListViewModel.handle(HomeRoomListAction.LeaveRoom(roomId))
}
MaterialAlertDialogBuilder(
requireContext(),
if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive
)
.setTitle(CommonStrings.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(CommonStrings.action_leave) { _, _ ->
roomListViewModel.handle(HomeRoomListAction.LeaveRoom(roomId))
}
.setNegativeButton(CommonStrings.action_cancel, null)
.show()
}
private fun onInvitesCounterClicked() {

View File

@@ -26,6 +26,8 @@ import im.vector.app.features.analytics.extensions.toTrackingValue
import im.vector.app.features.analytics.plan.UserProperties
import im.vector.app.features.displayname.getBestName
import im.vector.app.features.home.room.list.home.header.HomeRoomFilter
import im.vector.app.features.room.LeaveRoomPrompt
import im.vector.app.features.room.getLeaveRoomWarning
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -331,8 +333,8 @@ class HomeRoomListViewModel @AssistedInject constructor(
filteredPagedRoomSummariesLive.queryParams = getFilteredQueryParams(newFilter, filteredPagedRoomSummariesLive.queryParams)
}
fun isPublicRoom(roomId: String): Boolean {
return session.getRoom(roomId)?.stateService()?.isPublic().orFalse()
suspend fun getLeaveRoomWarning(roomId: String): LeaveRoomPrompt.Warning {
return session.getLeaveRoomWarning(roomId)
}
private fun handleSelectRoom(action: HomeRoomListAction.SelectRoom) = withState {

View File

@@ -7,6 +7,37 @@
package im.vector.app.features.powerlevel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import org.matrix.android.sdk.api.session.room.Room
import org.matrix.android.sdk.api.session.room.members.roomMemberQueryParams
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
import org.matrix.android.sdk.api.session.room.powerlevels.Role
import org.matrix.android.sdk.flow.flow
fun Role.isOwner() = this == Role.Creator || this == Role.SuperAdmin
fun Room.membersByRoleFlow(): Flow<Map<Role, List<RoomMemberSummary>>> {
val roomMembersFlow = flow().liveRoomMembers(roomMemberQueryParams())
val roomPowerLevelsFlow = PowerLevelsFlowFactory(this).createFlow()
return combine(roomMembersFlow, roomPowerLevelsFlow) { roomMembers, roomPowerLevels ->
roomMembers.groupBy { roomPowerLevels.getSuggestedRole(it.userId) }
}.distinctUntilChanged()
}
fun Room.isLastAdminFlow(userId: String): Flow<Boolean> {
return membersByRoleFlow().map { membersByRole ->
val creatorMembers = membersByRole[Role.Creator].orEmpty()
val superAdminMembers = membersByRole[Role.SuperAdmin].orEmpty()
val adminMembers = membersByRole[Role.Admin].orEmpty()
val joinedAdmins = (adminMembers + creatorMembers + superAdminMembers).filter { it.membership == Membership.JOIN }
if (joinedAdmins.size == 1) {
joinedAdmins.first().userId == userId
} else {
false
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package im.vector.app.features.room
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.features.powerlevel.isLastAdminFlow
import im.vector.app.features.room.LeaveRoomPrompt.Warning
import im.vector.lib.strings.CommonStrings
import im.vector.lib.ui.styles.R
import kotlinx.coroutines.flow.first
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.state.isPublic
object LeaveRoomPrompt {
enum class Warning {
LAST_ADMIN,
PRIVATE_ROOM,
NONE
};
fun show(
context: Context,
warning: Warning,
onLeaveClick: () -> Unit
) {
val hasWarning = warning != Warning.NONE
val message = buildString {
append(context.getString(CommonStrings.room_participants_leave_prompt_msg))
if (hasWarning) append("\n\n")
when (warning) {
Warning.LAST_ADMIN -> append(context.getString(CommonStrings.room_participants_leave_last_admin))
Warning.PRIVATE_ROOM -> append(context.getString(CommonStrings.room_participants_leave_private_warning))
Warning.NONE -> Unit
}
}
MaterialAlertDialogBuilder(
context,
if (hasWarning) R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive else 0
)
.setTitle(CommonStrings.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(CommonStrings.action_leave) { _, _ ->
onLeaveClick()
}
.setNegativeButton(CommonStrings.action_cancel, null)
.show()
}
}
suspend fun Session.getLeaveRoomWarning(roomId: String): Warning {
val room = getRoom(roomId) ?: return Warning.NONE
val isLastAdmin = room.isLastAdminFlow(myUserId).first()
return when {
isLastAdmin -> Warning.LAST_ADMIN
!room.stateService().isPublic() -> Warning.PRIVATE_ROOM
else -> Warning.NONE
}
}

View File

@@ -45,6 +45,7 @@ import im.vector.app.features.home.room.detail.upgrade.MigrateRoomBottomSheet
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedAction
import im.vector.app.features.home.room.list.actions.RoomListQuickActionsSharedActionViewModel
import im.vector.app.features.navigation.SettingsActivityPayload
import im.vector.app.features.room.LeaveRoomPrompt
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
@@ -320,25 +321,16 @@ class RoomProfileFragment :
}
override fun onLeaveRoomClicked() {
val isPublicRoom = roomProfileViewModel.isPublicRoom()
val message = buildString {
append(getString(CommonStrings.room_participants_leave_prompt_msg))
if (!isPublicRoom) {
append("\n\n")
append(getString(CommonStrings.room_participants_leave_private_warning))
withState(roomProfileViewModel){ state ->
val warning = when {
state.isLastAdmin -> LeaveRoomPrompt.Warning.LAST_ADMIN
state.roomSummary()?.isPublic == false -> LeaveRoomPrompt.Warning.PRIVATE_ROOM
else -> LeaveRoomPrompt.Warning.NONE
}
LeaveRoomPrompt.show(requireContext(), warning){
roomProfileViewModel.handle(RoomProfileAction.LeaveRoom)
}
}
MaterialAlertDialogBuilder(
requireContext(),
if (isPublicRoom) 0 else im.vector.lib.ui.styles.R.style.ThemeOverlay_Vector_MaterialAlertDialog_Destructive
)
.setTitle(CommonStrings.room_participants_leave_prompt_title)
.setMessage(message)
.setPositiveButton(CommonStrings.action_leave) { _, _ ->
roomProfileViewModel.handle(RoomProfileAction.LeaveRoom)
}
.setNegativeButton(CommonStrings.action_cancel, null)
.show()
}
override fun onRoomAliasesClicked() {

View File

@@ -20,6 +20,7 @@ import im.vector.app.features.analytics.AnalyticsTracker
import im.vector.app.features.analytics.plan.Interaction
import im.vector.app.features.home.ShortcutCreator
import im.vector.app.features.powerlevel.PowerLevelsFlowFactory
import im.vector.app.features.powerlevel.isLastAdminFlow
import im.vector.app.features.session.coroutineScope
import im.vector.lib.strings.CommonStrings
import kotlinx.coroutines.Dispatchers
@@ -72,6 +73,14 @@ class RoomProfileViewModel @AssistedInject constructor(
observePermissions()
observePowerLevels()
observeCryptoSettings(flowRoom)
observeIsLastAdmin()
}
private fun observeIsLastAdmin() {
room.isLastAdminFlow(session.myUserId)
.onEach { isLastAdmin ->
setState { copy(isLastAdmin = isLastAdmin) }
}.launchIn(viewModelScope)
}
private fun observeCryptoSettings(flowRoom: FlowRoom) {

View File

@@ -30,6 +30,7 @@ data class RoomProfileViewState(
val encryptToVerifiedDeviceOnly: Async<Boolean> = Uninitialized,
val globalCryptoConfig: Async<GlobalCryptoConfig> = Uninitialized,
val unverifiedDevicesInTheRoom: Async<Boolean> = Uninitialized,
val isLastAdmin: Boolean = false
) : MavericksState {
constructor(args: RoomProfileArgs) : this(roomId = args.roomId)

View File

@@ -21,7 +21,7 @@ data class SpaceLeaveAdvanceViewState(
val currentFilter: String = "",
val leaveState: Async<Unit> = Uninitialized,
val isFilteringEnabled: Boolean = false,
val isLastOwner: Boolean = false
val isLastAdmin: Boolean = false
) : MavericksState {
constructor(args: SpaceBottomSheetSettingsArgs) : this(

View File

@@ -50,27 +50,12 @@ class SpaceLeaveAdvancedFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
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(CommonStrings.space_leave_prompt_msg_only_you)
state.isLastOwner -> getString(CommonStrings.space_leave_prompt_msg_as_admin)
!summary.isPublic -> getString(CommonStrings.space_leave_prompt_msg_private)
else -> null
}
views.spaceLeavePromptDescription.isVisible = warningMessage != null
views.spaceLeavePromptDescription.text = warningMessage
}
views.spaceLeavePromptTitle.text = getString(CommonStrings.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "")
}
views.roomList.configureWith(controller)
@@ -107,6 +92,19 @@ class SpaceLeaveAdvancedFragment :
override fun invalidate() = withState(viewModel) { state ->
super.invalidate()
state.spaceSummary?.let { summary ->
val warningMessage: CharSequence? = when {
summary.otherMemberIds.isEmpty() -> getString(CommonStrings.space_leave_prompt_msg_only_you)
state.isLastAdmin -> getString(CommonStrings.space_leave_prompt_msg_as_admin)
!summary.isPublic -> getString(CommonStrings.space_leave_prompt_msg_private)
else -> null
}
views.spaceLeavePromptDescription.isVisible = warningMessage != null
views.spaceLeavePromptDescription.text = warningMessage
}
views.spaceLeavePromptTitle.text = getString(CommonStrings.space_leave_prompt_msg_with_name, state.spaceSummary?.name ?: "")
if (state.isFilteringEnabled) {
views.appBarLayout.setExpanded(false)
}

View File

@@ -20,7 +20,7 @@ import im.vector.app.core.di.MavericksAssistedViewModelFactory
import im.vector.app.core.di.hiltMavericksViewModelFactory
import im.vector.app.core.platform.EmptyViewEvents
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.powerlevel.isOwner
import im.vector.app.features.powerlevel.isLastAdminFlow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
@@ -29,7 +29,6 @@ import org.matrix.android.sdk.api.query.RoomCategoryFilter
import org.matrix.android.sdk.api.query.SpaceFilter
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.getRoom
import org.matrix.android.sdk.api.session.room.getRoomPowerLevels
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.roomSummaryQueryParams
import org.matrix.android.sdk.flow.flow
@@ -44,20 +43,13 @@ class SpaceLeaveAdvancedViewModel @AssistedInject constructor(
init {
val space = session.getRoom(initialState.spaceId)
val spaceSummary = space?.roomSummary()
val roomPowerLevels = space?.getRoomPowerLevels()
roomPowerLevels?.let {
val isOwner = roomPowerLevels.getSuggestedRole(session.myUserId).isOwner()
val otherOwnersCount = spaceSummary?.otherMemberIds
?.map { roomPowerLevels.getSuggestedRole(it) }
?.count { it.isOwner() }
?: 0
val isLastOwner = isOwner && otherOwnersCount == 0
setState {
copy(isLastOwner = isLastOwner)
}
}
space?.isLastAdminFlow(session.myUserId)
?.onEach { isLastAdmin ->
setState { copy(isLastAdmin = isLastAdmin) }
}?.launchIn(viewModelScope)
val spaceSummary = space?.roomSummary()
setState { copy(spaceSummary = spaceSummary) }
session.getRoom(initialState.spaceId)
?.flow()