android: prevent self member actions and add member action confirmations
This commit is contained in:
@@ -1350,6 +1350,7 @@ fun ChatScreen(
|
|||||||
},
|
},
|
||||||
members = state.chatMembers,
|
members = state.chatMembers,
|
||||||
bans = state.chatBans,
|
bans = state.chatBans,
|
||||||
|
selfUserId = state.selfUserId,
|
||||||
canManageMembers = state.canManageMembers,
|
canManageMembers = state.canManageMembers,
|
||||||
canTransferOwnership = state.chatRole.equals("owner", ignoreCase = true),
|
canTransferOwnership = state.chatRole.equals("owner", ignoreCase = true),
|
||||||
onPromoteMember = onPromoteMember,
|
onPromoteMember = onPromoteMember,
|
||||||
@@ -3467,6 +3468,7 @@ private fun ChatInfoTabContent(
|
|||||||
onForceCycleSpeedAudioSourceHandled: (String) -> Unit,
|
onForceCycleSpeedAudioSourceHandled: (String) -> Unit,
|
||||||
members: List<ChatMemberItem>,
|
members: List<ChatMemberItem>,
|
||||||
bans: List<ChatBanItem>,
|
bans: List<ChatBanItem>,
|
||||||
|
selfUserId: Long?,
|
||||||
canManageMembers: Boolean,
|
canManageMembers: Boolean,
|
||||||
canTransferOwnership: Boolean,
|
canTransferOwnership: Boolean,
|
||||||
onPromoteMember: (Long) -> Unit,
|
onPromoteMember: (Long) -> Unit,
|
||||||
@@ -3481,6 +3483,7 @@ private fun ChatInfoTabContent(
|
|||||||
ChatMembersTabContent(
|
ChatMembersTabContent(
|
||||||
members = members,
|
members = members,
|
||||||
bans = bans,
|
bans = bans,
|
||||||
|
selfUserId = selfUserId,
|
||||||
canManageMembers = canManageMembers,
|
canManageMembers = canManageMembers,
|
||||||
canTransferOwnership = canTransferOwnership,
|
canTransferOwnership = canTransferOwnership,
|
||||||
onPromoteMember = onPromoteMember,
|
onPromoteMember = onPromoteMember,
|
||||||
@@ -3725,6 +3728,7 @@ private val urlRegex = Regex("""https?://[^\s]+""")
|
|||||||
private fun ChatMembersTabContent(
|
private fun ChatMembersTabContent(
|
||||||
members: List<ChatMemberItem>,
|
members: List<ChatMemberItem>,
|
||||||
bans: List<ChatBanItem>,
|
bans: List<ChatBanItem>,
|
||||||
|
selfUserId: Long?,
|
||||||
canManageMembers: Boolean,
|
canManageMembers: Boolean,
|
||||||
canTransferOwnership: Boolean,
|
canTransferOwnership: Boolean,
|
||||||
onPromoteMember: (Long) -> Unit,
|
onPromoteMember: (Long) -> Unit,
|
||||||
@@ -3734,6 +3738,13 @@ private fun ChatMembersTabContent(
|
|||||||
onTransferOwnership: (Long) -> Unit,
|
onTransferOwnership: (Long) -> Unit,
|
||||||
onUnbanMember: (Long) -> Unit,
|
onUnbanMember: (Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
data class PendingMemberAction(
|
||||||
|
val title: String,
|
||||||
|
val body: String,
|
||||||
|
val onConfirm: () -> Unit,
|
||||||
|
)
|
||||||
|
var pendingAction by remember { mutableStateOf<PendingMemberAction?>(null) }
|
||||||
|
|
||||||
if (members.isEmpty() && bans.isEmpty()) {
|
if (members.isEmpty() && bans.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -3792,7 +3803,8 @@ private fun ChatMembersTabContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (canManageMembers && !member.role.equals("owner", ignoreCase = true)) {
|
val isSelf = selfUserId != null && member.userId == selfUserId
|
||||||
|
if (canManageMembers && !member.role.equals("owner", ignoreCase = true) && !isSelf) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -3807,22 +3819,46 @@ private fun ChatMembersTabContent(
|
|||||||
}
|
}
|
||||||
if (member.role.equals("admin", ignoreCase = true)) {
|
if (member.role.equals("admin", ignoreCase = true)) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onDemoteMember(member.userId) },
|
onClick = {
|
||||||
|
pendingAction = PendingMemberAction(
|
||||||
|
title = "Demote admin",
|
||||||
|
body = "Demote ${member.name.ifBlank { "@${member.username ?: member.userId}" }} to member?",
|
||||||
|
onConfirm = { onDemoteMember(member.userId) },
|
||||||
|
)
|
||||||
|
},
|
||||||
label = { Text("Demote") },
|
label = { Text("Demote") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (canTransferOwnership) {
|
if (canTransferOwnership) {
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onTransferOwnership(member.userId) },
|
onClick = {
|
||||||
|
pendingAction = PendingMemberAction(
|
||||||
|
title = "Transfer ownership",
|
||||||
|
body = "Transfer ownership to ${member.name.ifBlank { "@${member.username ?: member.userId}" }}?",
|
||||||
|
onConfirm = { onTransferOwnership(member.userId) },
|
||||||
|
)
|
||||||
|
},
|
||||||
label = { Text("Transfer owner") },
|
label = { Text("Transfer owner") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onBanMember(member.userId) },
|
onClick = {
|
||||||
|
pendingAction = PendingMemberAction(
|
||||||
|
title = "Ban member",
|
||||||
|
body = "Ban ${member.name.ifBlank { "@${member.username ?: member.userId}" }}?",
|
||||||
|
onConfirm = { onBanMember(member.userId) },
|
||||||
|
)
|
||||||
|
},
|
||||||
label = { Text("Ban") },
|
label = { Text("Ban") },
|
||||||
)
|
)
|
||||||
AssistChip(
|
AssistChip(
|
||||||
onClick = { onKickMember(member.userId) },
|
onClick = {
|
||||||
|
pendingAction = PendingMemberAction(
|
||||||
|
title = "Kick member",
|
||||||
|
body = "Kick ${member.name.ifBlank { "@${member.username ?: member.userId}" }} from chat?",
|
||||||
|
onConfirm = { onKickMember(member.userId) },
|
||||||
|
)
|
||||||
|
},
|
||||||
label = { Text("Kick") },
|
label = { Text("Kick") },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -3871,6 +3907,24 @@ private fun ChatMembersTabContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
pendingAction?.let { action ->
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { pendingAction = null },
|
||||||
|
title = { Text(action.title) },
|
||||||
|
text = { Text(action.body) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
action.onConfirm()
|
||||||
|
pendingAction = null
|
||||||
|
},
|
||||||
|
) { Text(stringResource(id = R.string.common_confirm)) }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { pendingAction = null }) { Text(stringResource(id = R.string.common_cancel)) }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntry> {
|
private fun buildChatInfoEntries(messages: List<MessageItem>): List<ChatInfoEntry> {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.update
|
|||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import ru.daemonlord.messenger.core.notifications.ActiveChatTracker
|
import ru.daemonlord.messenger.core.notifications.ActiveChatTracker
|
||||||
import ru.daemonlord.messenger.core.notifications.NotificationDispatcher
|
import ru.daemonlord.messenger.core.notifications.NotificationDispatcher
|
||||||
|
import ru.daemonlord.messenger.core.token.TokenRepository
|
||||||
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
|
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
|
||||||
import ru.daemonlord.messenger.domain.chat.usecase.ObserveChatUseCase
|
import ru.daemonlord.messenger.domain.chat.usecase.ObserveChatUseCase
|
||||||
import ru.daemonlord.messenger.domain.chat.usecase.ObserveChatsUseCase
|
import ru.daemonlord.messenger.domain.chat.usecase.ObserveChatsUseCase
|
||||||
@@ -64,6 +65,7 @@ class ChatViewModel @Inject constructor(
|
|||||||
private val handleRealtimeEventsUseCase: HandleRealtimeEventsUseCase,
|
private val handleRealtimeEventsUseCase: HandleRealtimeEventsUseCase,
|
||||||
private val activeChatTracker: ActiveChatTracker,
|
private val activeChatTracker: ActiveChatTracker,
|
||||||
private val notificationDispatcher: NotificationDispatcher,
|
private val notificationDispatcher: NotificationDispatcher,
|
||||||
|
private val tokenRepository: TokenRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val chatId: Long = checkNotNull(savedStateHandle["chatId"])
|
private val chatId: Long = checkNotNull(savedStateHandle["chatId"])
|
||||||
@@ -506,18 +508,34 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun promoteMember(userId: Long) {
|
fun promoteMember(userId: Long) {
|
||||||
|
if (userId == uiState.value.selfUserId) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "You cannot change your own role from this screen.") }
|
||||||
|
return
|
||||||
|
}
|
||||||
updateMemberRole(userId = userId, role = "admin")
|
updateMemberRole(userId = userId, role = "admin")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun demoteMember(userId: Long) {
|
fun demoteMember(userId: Long) {
|
||||||
|
if (userId == uiState.value.selfUserId) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "You cannot change your own role from this screen.") }
|
||||||
|
return
|
||||||
|
}
|
||||||
updateMemberRole(userId = userId, role = "member")
|
updateMemberRole(userId = userId, role = "member")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun transferOwnership(userId: Long) {
|
fun transferOwnership(userId: Long) {
|
||||||
|
if (userId == uiState.value.selfUserId) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "Transfer ownership to another member.") }
|
||||||
|
return
|
||||||
|
}
|
||||||
updateMemberRole(userId = userId, role = "owner")
|
updateMemberRole(userId = userId, role = "owner")
|
||||||
}
|
}
|
||||||
|
|
||||||
fun kickMember(userId: Long) {
|
fun kickMember(userId: Long) {
|
||||||
|
if (userId == uiState.value.selfUserId) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "You cannot kick yourself.") }
|
||||||
|
return
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = chatRepository.removeMember(chatId = chatId, userId = userId)) {
|
when (val result = chatRepository.removeMember(chatId = chatId, userId = userId)) {
|
||||||
is AppResult.Success -> refreshMembersAndBans()
|
is AppResult.Success -> refreshMembersAndBans()
|
||||||
@@ -527,6 +545,10 @@ class ChatViewModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun banMember(userId: Long) {
|
fun banMember(userId: Long) {
|
||||||
|
if (userId == uiState.value.selfUserId) {
|
||||||
|
_uiState.update { it.copy(errorMessage = "You cannot ban yourself.") }
|
||||||
|
return
|
||||||
|
}
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
when (val result = chatRepository.banMember(chatId = chatId, userId = userId)) {
|
when (val result = chatRepository.banMember(chatId = chatId, userId = userId)) {
|
||||||
is AppResult.Success -> refreshMembersAndBans()
|
is AppResult.Success -> refreshMembersAndBans()
|
||||||
@@ -807,11 +829,13 @@ class ChatViewModel @Inject constructor(
|
|||||||
private fun refresh() {
|
private fun refresh() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
|
val selfUserId = tokenRepository.getActiveUserId()
|
||||||
when (val result = syncRecentMessagesUseCase(chatId = chatId)) {
|
when (val result = syncRecentMessagesUseCase(chatId = chatId)) {
|
||||||
is AppResult.Success -> _uiState.update { it.copy(isLoading = false) }
|
is AppResult.Success -> _uiState.update { it.copy(isLoading = false, selfUserId = selfUserId) }
|
||||||
is AppResult.Error -> _uiState.update {
|
is AppResult.Error -> _uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
selfUserId = selfUserId,
|
||||||
errorMessage = result.reason.toUiMessage(),
|
errorMessage = result.reason.toUiMessage(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import ru.daemonlord.messenger.domain.chat.model.ChatBanItem
|
|||||||
|
|
||||||
data class MessageUiState(
|
data class MessageUiState(
|
||||||
val chatId: Long = 0L,
|
val chatId: Long = 0L,
|
||||||
|
val selfUserId: Long? = null,
|
||||||
val isLoading: Boolean = true,
|
val isLoading: Boolean = true,
|
||||||
val isLoadingMore: Boolean = false,
|
val isLoadingMore: Boolean = false,
|
||||||
val isSending: Boolean = false,
|
val isSending: Boolean = false,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<string name="toast_night_mode_enabled">Включен ночной режим.</string>
|
<string name="toast_night_mode_enabled">Включен ночной режим.</string>
|
||||||
|
|
||||||
<string name="common_cancel">Отмена</string>
|
<string name="common_cancel">Отмена</string>
|
||||||
|
<string name="common_confirm">Подтвердить</string>
|
||||||
<string name="common_close">Закрыть</string>
|
<string name="common_close">Закрыть</string>
|
||||||
<string name="common_delete">Удалить</string>
|
<string name="common_delete">Удалить</string>
|
||||||
<string name="common_send">Отправить</string>
|
<string name="common_send">Отправить</string>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
<string name="chats_not_found">No chats found</string>
|
<string name="chats_not_found">No chats found</string>
|
||||||
|
|
||||||
<string name="common_cancel">Cancel</string>
|
<string name="common_cancel">Cancel</string>
|
||||||
|
<string name="common_confirm">Confirm</string>
|
||||||
<string name="common_close">Close</string>
|
<string name="common_close">Close</string>
|
||||||
<string name="common_delete">Delete</string>
|
<string name="common_delete">Delete</string>
|
||||||
<string name="common_send">Send</string>
|
<string name="common_send">Send</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user