diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt index 15f21a6..6083d73 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatScreen.kt @@ -1351,6 +1351,7 @@ fun ChatScreen( members = state.chatMembers, bans = state.chatBans, selfUserId = state.selfUserId, + selfRole = state.chatRole, canManageMembers = state.canManageMembers, canTransferOwnership = state.chatRole.equals("owner", ignoreCase = true), onPromoteMember = onPromoteMember, @@ -3469,6 +3470,7 @@ private fun ChatInfoTabContent( members: List, bans: List, selfUserId: Long?, + selfRole: String?, canManageMembers: Boolean, canTransferOwnership: Boolean, onPromoteMember: (Long) -> Unit, @@ -3484,6 +3486,7 @@ private fun ChatInfoTabContent( members = members, bans = bans, selfUserId = selfUserId, + selfRole = selfRole, canManageMembers = canManageMembers, canTransferOwnership = canTransferOwnership, onPromoteMember = onPromoteMember, @@ -3729,6 +3732,7 @@ private fun ChatMembersTabContent( members: List, bans: List, selfUserId: Long?, + selfRole: String?, canManageMembers: Boolean, canTransferOwnership: Boolean, onPromoteMember: (Long) -> Unit, @@ -3804,20 +3808,29 @@ private fun ChatMembersTabContent( } val isSelf = selfUserId != null && member.userId == selfUserId - if (canManageMembers && !member.role.equals("owner", ignoreCase = true) && !isSelf) { + val actorRole = selfRole?.lowercase(Locale.getDefault()).orEmpty() + val targetRole = member.role.lowercase(Locale.getDefault()) + val canManageTarget = canManageMembers && + !isSelf && + targetRole != "owner" && + !(actorRole == "admin" && targetRole == "admin") + val canPromote = canManageTarget && targetRole == "member" + val canDemote = canManageTarget && targetRole == "admin" && actorRole == "owner" + val canTransfer = canTransferOwnership && !isSelf && targetRole != "owner" + if (canManageTarget || canTransfer) { Row( modifier = Modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - if (member.role.equals("member", ignoreCase = true)) { + if (canPromote) { AssistChip( onClick = { onPromoteMember(member.userId) }, label = { Text("Promote") }, ) } - if (member.role.equals("admin", ignoreCase = true)) { + if (canDemote) { AssistChip( onClick = { pendingAction = PendingMemberAction( @@ -3829,7 +3842,7 @@ private fun ChatMembersTabContent( label = { Text("Demote") }, ) } - if (canTransferOwnership) { + if (canTransfer) { AssistChip( onClick = { pendingAction = PendingMemberAction( @@ -3841,26 +3854,28 @@ private fun ChatMembersTabContent( label = { Text("Transfer owner") }, ) } - AssistChip( - onClick = { - pendingAction = PendingMemberAction( - title = "Ban member", - body = "Ban ${member.name.ifBlank { "@${member.username ?: member.userId}" }}?", - onConfirm = { onBanMember(member.userId) }, - ) - }, - label = { Text("Ban") }, - ) - AssistChip( - onClick = { - pendingAction = PendingMemberAction( - title = "Kick member", - body = "Kick ${member.name.ifBlank { "@${member.username ?: member.userId}" }} from chat?", - onConfirm = { onKickMember(member.userId) }, - ) - }, - label = { Text("Kick") }, - ) + if (canManageTarget) { + AssistChip( + onClick = { + pendingAction = PendingMemberAction( + title = "Ban member", + body = "Ban ${member.name.ifBlank { "@${member.username ?: member.userId}" }}?", + onConfirm = { onBanMember(member.userId) }, + ) + }, + label = { Text("Ban") }, + ) + AssistChip( + onClick = { + pendingAction = PendingMemberAction( + title = "Kick member", + body = "Kick ${member.name.ifBlank { "@${member.username ?: member.userId}" }} from chat?", + onConfirm = { onKickMember(member.userId) }, + ) + }, + label = { Text("Kick") }, + ) + } } } } diff --git a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatViewModel.kt b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatViewModel.kt index 2a26ac9..6ab0e8a 100644 --- a/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatViewModel.kt +++ b/android/app/src/main/java/ru/daemonlord/messenger/ui/chat/ChatViewModel.kt @@ -508,34 +508,22 @@ class ChatViewModel @Inject constructor( } fun promoteMember(userId: Long) { - if (userId == uiState.value.selfUserId) { - _uiState.update { it.copy(errorMessage = "You cannot change your own role from this screen.") } - return - } + if (!ensureCanManageTarget(userId = userId, action = "promote")) return updateMemberRole(userId = userId, role = "admin") } fun demoteMember(userId: Long) { - if (userId == uiState.value.selfUserId) { - _uiState.update { it.copy(errorMessage = "You cannot change your own role from this screen.") } - return - } + if (!ensureCanManageTarget(userId = userId, action = "demote")) return updateMemberRole(userId = userId, role = "member") } fun transferOwnership(userId: Long) { - if (userId == uiState.value.selfUserId) { - _uiState.update { it.copy(errorMessage = "Transfer ownership to another member.") } - return - } + if (!ensureCanManageTarget(userId = userId, action = "transfer_ownership", ownerOnly = true)) return updateMemberRole(userId = userId, role = "owner") } fun kickMember(userId: Long) { - if (userId == uiState.value.selfUserId) { - _uiState.update { it.copy(errorMessage = "You cannot kick yourself.") } - return - } + if (!ensureCanManageTarget(userId = userId, action = "kick")) return viewModelScope.launch { when (val result = chatRepository.removeMember(chatId = chatId, userId = userId)) { is AppResult.Success -> refreshMembersAndBans() @@ -545,10 +533,7 @@ class ChatViewModel @Inject constructor( } fun banMember(userId: Long) { - if (userId == uiState.value.selfUserId) { - _uiState.update { it.copy(errorMessage = "You cannot ban yourself.") } - return - } + if (!ensureCanManageTarget(userId = userId, action = "ban")) return viewModelScope.launch { when (val result = chatRepository.banMember(chatId = chatId, userId = userId)) { is AppResult.Success -> refreshMembersAndBans() @@ -940,6 +925,45 @@ class ChatViewModel @Inject constructor( return uiState.value.messages.firstOrNull { it.id == messageId } } + private fun ensureCanManageTarget( + userId: Long, + action: String, + ownerOnly: Boolean = false, + ): Boolean { + val state = uiState.value + val selfId = state.selfUserId + if (selfId != null && userId == selfId) { + _uiState.update { it.copy(errorMessage = "You cannot apply this action to yourself.") } + return false + } + + val actorRole = state.chatRole?.lowercase() + if (actorRole != "owner" && actorRole != "admin") { + _uiState.update { it.copy(errorMessage = "You don't have enough permissions.") } + return false + } + if (ownerOnly && actorRole != "owner") { + _uiState.update { it.copy(errorMessage = "Only owner can perform this action.") } + return false + } + + val targetRole = state.chatMembers.firstOrNull { it.userId == userId }?.role?.lowercase() + if (targetRole == "owner") { + _uiState.update { it.copy(errorMessage = "You cannot manage owner account.") } + return false + } + if (actorRole == "admin" && (targetRole == "admin" || targetRole == "owner")) { + _uiState.update { it.copy(errorMessage = "Admin cannot manage admins or owner.") } + return false + } + + if (action == "transfer_ownership" && targetRole == "owner") { + _uiState.update { it.copy(errorMessage = "Choose another member for ownership transfer.") } + return false + } + return true + } + private fun AppError.toUiMessage(): String { return when (this) { AppError.Network -> "Network error."