Skip to content

Commit 9bbf44c

Browse files
authored
feat: UI for promoting next user to admin role (WPB-25278) (#4819)
1 parent a672d8c commit 9bbf44c

13 files changed

Lines changed: 702 additions & 3 deletions

File tree

app/src/main/kotlin/com/wire/android/di/accountScoped/ConversationModule.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import com.wire.kalium.logic.feature.conversation.IsOneToOneConversationCreatedU
3636
import com.wire.kalium.logic.feature.conversation.JoinConversationViaCodeUseCase
3737
import com.wire.kalium.logic.feature.conversation.CheckConversationLeaveConditionsUseCase
3838
import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase
39+
import com.wire.kalium.logic.feature.conversation.ObserveEligibleMembersForConversationAdminRoleUseCase
3940
import com.wire.kalium.logic.feature.conversation.NotifyConversationIsOpenUseCase
4041
import com.wire.kalium.logic.feature.conversation.ObserveArchivedUnreadConversationsCountUseCase
4142
import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase
@@ -209,6 +210,13 @@ class ConversationModule {
209210
fun provideCheckConversationLeaveConditionsUseCase(conversationScope: ConversationScope): CheckConversationLeaveConditionsUseCase =
210211
conversationScope.checkConversationLeaveConditions
211212

213+
@ViewModelScoped
214+
@Provides
215+
fun provideObserveEligibleMembersForConversationAdminRoleUseCase(
216+
conversationScope: ConversationScope
217+
): ObserveEligibleMembersForConversationAdminRoleUseCase =
218+
conversationScope.observeEligibleMembersForConversationAdminRole
219+
212220
@ViewModelScoped
213221
@Provides
214222
fun provideUpdateConversationMutedStatusUseCase(conversationScope: ConversationScope): UpdateConversationMutedStatusUseCase =

app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsMenuViewModel.kt

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import com.wire.android.ui.home.HomeSnackBarMessage
3535
import com.wire.android.ui.home.conversationslist.model.DeleteGroupDialogState
3636
import com.wire.android.ui.home.conversationslist.model.DialogState
3737
import com.wire.android.ui.home.conversationslist.model.LeaveGroupDialogState
38+
import com.wire.android.ui.home.conversationslist.model.LeaveGroupOptionsDialogState
3839
import com.wire.android.util.dispatchers.DispatcherProvider
3940
import com.wire.android.workmanager.worker.enqueueConversationDeletionLocally
4041
import com.wire.kalium.logic.data.conversation.ConversationFolder
@@ -46,9 +47,9 @@ import com.wire.kalium.logic.feature.connection.BlockUserUseCase
4647
import com.wire.kalium.logic.feature.connection.UnblockUserResult
4748
import com.wire.kalium.logic.feature.connection.UnblockUserUseCase
4849
import com.wire.kalium.logic.feature.conversation.ArchiveStatusUpdateResult
50+
import com.wire.kalium.logic.feature.conversation.CheckConversationLeaveConditionsUseCase
4951
import com.wire.kalium.logic.feature.conversation.ClearConversationContentUseCase
5052
import com.wire.kalium.logic.feature.conversation.ConversationUpdateStatusResult
51-
import com.wire.kalium.logic.feature.conversation.CheckConversationLeaveConditionsUseCase
5253
import com.wire.kalium.logic.feature.conversation.LeaveConversationUseCase
5354
import com.wire.kalium.logic.feature.conversation.ObserveConversationDetailsUseCase
5455
import com.wire.kalium.logic.feature.conversation.RemoveMemberFromConversationUseCase
@@ -70,6 +71,8 @@ import kotlinx.coroutines.flow.SharingStarted
7071
import kotlinx.coroutines.flow.StateFlow
7172
import kotlinx.coroutines.flow.combine
7273
import kotlinx.coroutines.flow.distinctUntilChanged
74+
import kotlinx.coroutines.flow.filterIsInstance
75+
import kotlinx.coroutines.flow.firstOrNull
7376
import kotlinx.coroutines.flow.flatMapConcat
7477
import kotlinx.coroutines.flow.flowOf
7578
import kotlinx.coroutines.flow.onCompletion
@@ -82,6 +85,7 @@ import javax.inject.Inject
8285
@ViewModelScopedPreview
8386
interface ConversationOptionsMenuViewModel : ActionsManager<ConversationOptionsMenuViewAction> {
8487
val leaveGroupDialogState: VisibilityState<LeaveGroupDialogState> get() = VisibilityState()
88+
val leaveGroupOptionsDialogState: VisibilityState<LeaveGroupOptionsDialogState> get() = VisibilityState()
8589
val deleteGroupDialogState: VisibilityState<DeleteGroupDialogState> get() = VisibilityState()
8690
val deleteGroupLocallyDialogState: VisibilityState<DeleteGroupDialogState> get() = VisibilityState()
8791
val blockUserDialogState: VisibilityState<BlockUserDialogState> get() = VisibilityState()
@@ -129,6 +133,7 @@ class ConversationOptionsMenuViewModelImpl @Inject constructor(
129133
private val nonCancellableIOContext = NonCancellable + dispatchers.io()
130134
private val conversationStateFlow: ConcurrentHashMap<ConversationId, StateFlow<ConversationOptionsMenuState>> = ConcurrentHashMap()
131135
override val leaveGroupDialogState: VisibilityState<LeaveGroupDialogState> by mutableStateOf(VisibilityState())
136+
override val leaveGroupOptionsDialogState: VisibilityState<LeaveGroupOptionsDialogState> by mutableStateOf(VisibilityState())
132137
override val deleteGroupDialogState: VisibilityState<DeleteGroupDialogState> by mutableStateOf(VisibilityState())
133138
override val deleteGroupLocallyDialogState: VisibilityState<DeleteGroupDialogState> by mutableStateOf(VisibilityState())
134139
override val blockUserDialogState: VisibilityState<BlockUserDialogState> by mutableStateOf(VisibilityState())
@@ -253,7 +258,14 @@ class ConversationOptionsMenuViewModelImpl @Inject constructor(
253258
when (val result = checkConversationLeaveConditions(leaveGroupState.conversationId)) {
254259
CheckConversationLeaveConditionsUseCase.Result.Allow -> leaveGroupDialogState.show(leaveGroupState)
255260
is CheckConversationLeaveConditionsUseCase.Result.DoNotAllow -> {
256-
appLogger.i("TODO: Show new leave options dialog: $result")
261+
leaveGroupOptionsDialogState.show(
262+
LeaveGroupOptionsDialogState(
263+
conversationId = leaveGroupState.conversationId,
264+
conversationName = leaveGroupState.conversationName,
265+
showPromoteOption = result.eligibleUsersAvailable,
266+
canDeleteGroup = canDeleteGroup(leaveGroupState.conversationId),
267+
)
268+
)
257269
}
258270
is CheckConversationLeaveConditionsUseCase.Result.Error -> {
259271
onMessage(HomeSnackBarMessage.LeaveConversationError)
@@ -265,6 +277,12 @@ class ConversationOptionsMenuViewModelImpl @Inject constructor(
265277
}
266278
}
267279

280+
private suspend fun canDeleteGroup(conversationId: ConversationId) = observeConversationStateFlow(conversationId)
281+
.filterIsInstance<ConversationOptionsMenuState.Conversation>()
282+
.firstOrNull()
283+
?.conversation
284+
?.canDeleteGroup() ?: false
285+
268286
override fun leaveGroup(conversationId: ConversationId, conversationName: String, shouldDelete: Boolean) {
269287
viewModelScope.launch {
270288
leaveGroupDialogState.update { it.copy(loading = true) }

app/src/main/kotlin/com/wire/android/ui/common/bottomsheet/conversation/ConversationOptionsModalSheetLayout.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ import com.wire.android.ui.common.snackbar.LocalSnackbarHostState
3939
import com.wire.android.ui.home.conversations.details.dialog.ClearConversationContentDialog
4040
import com.wire.android.ui.home.conversations.details.menu.DeleteConversationGroupDialog
4141
import com.wire.android.ui.home.conversations.details.menu.DeleteConversationGroupLocallyDialog
42+
import com.wire.android.ui.home.conversations.details.menu.LeaveConversationAdminOptionsDialog
4243
import com.wire.android.ui.home.conversations.details.menu.LeaveConversationGroupDialog
4344
import com.wire.android.ui.home.conversations.folder.ConversationFoldersNavArgs
45+
import com.wire.android.ui.home.conversationslist.model.DeleteGroupDialogState
4446
import com.wire.android.ui.theme.WireTheme
4547
import com.wire.android.util.ui.PreviewMultipleThemes
4648
import com.wire.kalium.logic.data.id.ConversationId
@@ -53,6 +55,7 @@ fun ConversationOptionsModalSheetLayout(
5355
onLeftConversation: () -> Unit = {},
5456
onDeletedConversation: () -> Unit = {},
5557
onDeletedConversationLocally: () -> Unit = {},
58+
onPromoteAdmin: (ConversationId) -> Unit = {},
5659
openConversationDebugMenu: (ConversationId) -> Unit = {},
5760
viewModel: ConversationOptionsMenuViewModel =
5861
hiltViewModelScoped<ConversationOptionsMenuViewModelImpl, ConversationOptionsMenuViewModel>()
@@ -63,6 +66,17 @@ fun ConversationOptionsModalSheetLayout(
6366
dialogState = viewModel.leaveGroupDialogState,
6467
onLeaveGroup = { viewModel.leaveGroup(it.conversationId, it.conversationName, it.shouldDelete) }
6568
)
69+
LeaveConversationAdminOptionsDialog(
70+
dialogState = viewModel.leaveGroupOptionsDialogState,
71+
onPromoteAdmin = { state ->
72+
viewModel.leaveGroupOptionsDialogState.dismiss()
73+
onPromoteAdmin(state.conversationId)
74+
},
75+
onDeleteGroup = { state ->
76+
viewModel.leaveGroupOptionsDialogState.dismiss()
77+
viewModel.deleteGroupDialogState.show(DeleteGroupDialogState(state.conversationId, state.conversationName))
78+
},
79+
)
6680
DeleteConversationGroupDialog(
6781
dialogState = viewModel.deleteGroupDialogState,
6882
onDeleteGroup = { viewModel.deleteGroup(it.conversationId, it.conversationName) }

app/src/main/kotlin/com/wire/android/ui/home/conversations/details/GroupConversationDetailsScreen.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ import com.ramcosta.composedestinations.generated.app.destinations.SearchConvers
104104
import com.ramcosta.composedestinations.generated.app.destinations.SelfUserProfileScreenDestination
105105
import com.ramcosta.composedestinations.generated.app.destinations.ServiceDetailsScreenDestination
106106
import com.ramcosta.composedestinations.generated.app.destinations.UpdateAppsAccessScreenDestination
107+
import com.ramcosta.composedestinations.generated.app.destinations.PromoteAdminScreenDestination
107108
import com.wire.android.ui.home.conversations.details.editguestaccess.EditGuestAccessParams
109+
import com.wire.android.ui.home.conversations.promoteadmin.PromoteAdminNavArgs
108110
import com.wire.android.ui.home.conversations.details.options.GroupConversationOptions
109111
import com.wire.android.ui.home.conversations.details.options.GroupConversationOptionsState
110112
import com.wire.android.ui.home.conversations.details.options.LoadingGroupConversation
@@ -299,6 +301,9 @@ fun GroupConversationDetailsScreen(
299301
)
300302
)
301303
},
304+
onPromoteAdmin = { conversationId ->
305+
navigator.navigate(NavigationCommand(PromoteAdminScreenDestination(PromoteAdminNavArgs(conversationId))))
306+
},
302307
openConversationDebugMenu = {
303308
navigator.navigate(
304309
NavigationCommand(
@@ -374,6 +379,7 @@ private fun GroupConversationDetailsContent(
374379
onMoveToFolder: (ConversationFoldersNavArgs) -> Unit = {},
375380
onLeftConversation: () -> Unit = {},
376381
onDeletedConversation: () -> Unit = {},
382+
onPromoteAdmin: (ConversationId) -> Unit = {},
377383
openConversationDebugMenu: (ConversationId) -> Unit = {},
378384
initialPageIndex: GroupConversationDetailsTabItem = GroupConversationDetailsTabItem.OPTIONS,
379385
isScreenLoading: StateFlow<Boolean> = MutableStateFlow(false),
@@ -559,6 +565,7 @@ private fun GroupConversationDetailsContent(
559565
openConversationFolders = onMoveToFolder,
560566
onLeftConversation = onLeftConversation,
561567
onDeletedConversation = onDeletedConversation,
568+
onPromoteAdmin = onPromoteAdmin,
562569
openConversationDebugMenu = openConversationDebugMenu,
563570
)
564571

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
19+
package com.wire.android.ui.home.conversations.details.menu
20+
21+
import androidx.compose.runtime.Composable
22+
import androidx.compose.ui.res.stringResource
23+
import com.wire.android.R
24+
import com.wire.android.ui.common.VisibilityState
25+
import com.wire.android.ui.common.WireDialog
26+
import com.wire.android.ui.common.WireDialogButtonProperties
27+
import com.wire.android.ui.common.WireDialogButtonType
28+
import com.wire.android.ui.common.visbility.VisibilityState
29+
import com.wire.android.ui.home.conversationslist.model.LeaveGroupOptionsDialogState
30+
31+
@Composable
32+
internal fun LeaveConversationAdminOptionsDialog(
33+
dialogState: VisibilityState<LeaveGroupOptionsDialogState>,
34+
onPromoteAdmin: (LeaveGroupOptionsDialogState) -> Unit,
35+
onDeleteGroup: (LeaveGroupOptionsDialogState) -> Unit,
36+
) {
37+
VisibilityState(dialogState) { state ->
38+
val isInformationalOnly = !state.showPromoteOption && !state.canDeleteGroup
39+
WireDialog(
40+
title = stringResource(id = titleRes(state), state.conversationName),
41+
text = stringResource(id = descriptionRes(state)),
42+
buttonsHorizontalAlignment = false,
43+
onDismiss = dialogState::dismiss,
44+
optionButton1Properties = when {
45+
isInformationalOnly -> WireDialogButtonProperties(
46+
onClick = dialogState::dismiss,
47+
text = stringResource(id = R.string.label_ok),
48+
type = WireDialogButtonType.Primary,
49+
)
50+
state.showPromoteOption -> WireDialogButtonProperties(
51+
onClick = { onPromoteAdmin(state) },
52+
text = stringResource(id = R.string.leave_conversation_admin_options_dialog_promote_button),
53+
type = WireDialogButtonType.Primary,
54+
)
55+
else -> null
56+
},
57+
optionButton2Properties = when {
58+
!isInformationalOnly && state.canDeleteGroup -> WireDialogButtonProperties(
59+
onClick = { onDeleteGroup(state) },
60+
text = stringResource(id = R.string.leave_conversation_admin_options_dialog_delete_button),
61+
type = WireDialogButtonType.Primary,
62+
)
63+
else -> null
64+
},
65+
dismissButtonProperties = if (isInformationalOnly) {
66+
null
67+
} else {
68+
WireDialogButtonProperties(
69+
onClick = dialogState::dismiss,
70+
text = stringResource(id = R.string.label_cancel),
71+
)
72+
},
73+
)
74+
}
75+
}
76+
77+
@Composable
78+
private fun descriptionRes(state: LeaveGroupOptionsDialogState): Int = when {
79+
state.showPromoteOption && state.canDeleteGroup ->
80+
R.string.leave_conversation_admin_options_dialog_description_with_promote
81+
82+
state.showPromoteOption && !state.canDeleteGroup ->
83+
R.string.leave_conversation_admin_options_dialog_description_with_promote_no_delete
84+
85+
!state.showPromoteOption && state.canDeleteGroup ->
86+
R.string.leave_conversation_admin_options_dialog_description_no_promote
87+
88+
else ->
89+
R.string.leave_conversation_admin_options_dialog_description_no_promote_no_delete
90+
}
91+
92+
@Composable
93+
private fun titleRes(state: LeaveGroupOptionsDialogState): Int = when {
94+
state.canDeleteGroup || state.showPromoteOption -> R.string.leave_conversation_dialog_title
95+
else -> R.string.cannot_leave_conversation_dialog_title
96+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* Wire
3+
* Copyright (C) 2025 Wire Swiss GmbH
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see http://www.gnu.org/licenses/.
17+
*/
18+
package com.wire.android.ui.home.conversations.promoteadmin
19+
20+
import android.os.Parcelable
21+
import com.wire.android.ui.home.conversations.QualifiedIdParceler
22+
import com.wire.kalium.logic.data.id.ConversationId
23+
import kotlinx.parcelize.Parcelize
24+
import kotlinx.parcelize.TypeParceler
25+
26+
@Parcelize
27+
@TypeParceler<ConversationId, QualifiedIdParceler>()
28+
data class PromoteAdminNavArgs(val conversationId: ConversationId) : Parcelable

0 commit comments

Comments
 (0)