Skip to content

Commit 120c30e

Browse files
richvdhElementBotjmartinesp
authored
Show progress dialog while we are sending invites in a room (#5342)
* Add `InvitePeopleState.sendInvitesAction` Keep track of the progress on sending invites with a new state property. * Keep `RoomInviteMembersView` open until invites are sent * Sync strings from localazy * extend `ProgressDialog` to support custom content For my current design, a simple text element is insufficient. I extend `ProgressDialog` to give more flexibility over the content of the dialog. * Show progress dialog while invites are being sent * Add new ProgressDialog previews to the naming exceptions list * Update screenshots --------- Co-authored-by: ElementBot <[email protected]> Co-authored-by: Jorge Martín <[email protected]>
1 parent 3af4405 commit 120c30e

File tree

22 files changed

+190
-20
lines changed

22 files changed

+190
-20
lines changed

features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleState.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,11 @@
77

88
package io.element.android.features.invitepeople.api
99

10+
import io.element.android.libraries.architecture.AsyncAction
11+
1012
interface InvitePeopleState {
1113
val canInvite: Boolean
1214
val isSearchActive: Boolean
15+
val sendInvitesAction: AsyncAction<Unit>
1316
val eventSink: (InvitePeopleEvents) -> Unit
1417
}

features/invitepeople/api/src/main/kotlin/io/element/android/features/invitepeople/api/InvitePeopleStateProvider.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,33 @@
88
package io.element.android.features.invitepeople.api
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
import io.element.android.libraries.architecture.AsyncAction
1112

1213
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
1314
override val values: Sequence<InvitePeopleState>
1415
get() = sequenceOf(
1516
aPreviewInvitePeopleState(),
1617
aPreviewInvitePeopleState(canInvite = true),
17-
aPreviewInvitePeopleState(isSearchActive = true)
18+
aPreviewInvitePeopleState(isSearchActive = true),
19+
aPreviewInvitePeopleState(sendInvitesAction = AsyncAction.Loading),
1820
)
1921
}
2022

2123
private data class PreviewInvitePeopleState(
2224
override val canInvite: Boolean,
2325
override val isSearchActive: Boolean,
26+
override val sendInvitesAction: AsyncAction<Unit>,
2427
override val eventSink: (InvitePeopleEvents) -> Unit,
2528
) : InvitePeopleState
2629

2730
private fun aPreviewInvitePeopleState(
2831
canInvite: Boolean = false,
2932
isSearchActive: Boolean = false,
33+
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
3034
eventSink: (InvitePeopleEvents) -> Unit = {},
3135
) = PreviewInvitePeopleState(
3236
canInvite = canInvite,
3337
isSearchActive = isSearchActive,
38+
sendInvitesAction = sendInvitesAction,
3439
eventSink = eventSink
3540
)

features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenter.kt

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ import dev.zacsweers.metro.Inject
2323
import io.element.android.features.invitepeople.api.InvitePeopleEvents
2424
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
2525
import io.element.android.features.invitepeople.api.InvitePeopleState
26+
import io.element.android.libraries.architecture.AsyncAction
2627
import io.element.android.libraries.architecture.AsyncData
2728
import io.element.android.libraries.architecture.map
2829
import io.element.android.libraries.architecture.runCatchingUpdatingState
30+
import io.element.android.libraries.architecture.runUpdatingState
2931
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
3032
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
3133
import io.element.android.libraries.di.SessionScope
@@ -73,6 +75,8 @@ class DefaultInvitePeoplePresenter(
7375
var searchQuery by rememberSaveable { mutableStateOf("") }
7476
var searchActive by rememberSaveable { mutableStateOf(false) }
7577
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
78+
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
79+
7680
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
7781
if (joinedRoom == null) {
7882
val result = matrixClient.getJoinedRoom(roomId)
@@ -116,7 +120,7 @@ class DefaultInvitePeoplePresenter(
116120
}
117121
is InvitePeopleEvents.SendInvites -> {
118122
room.dataOrNull()?.let {
119-
sessionCoroutineScope.sendInvites(it, selectedUsers.value)
123+
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
120124
}
121125
}
122126
is InvitePeopleEvents.CloseSearch -> {
@@ -128,29 +132,35 @@ class DefaultInvitePeoplePresenter(
128132

129133
return DefaultInvitePeopleState(
130134
room = room.map { },
131-
canInvite = selectedUsers.value.isNotEmpty(),
135+
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
132136
selectedUsers = selectedUsers.value,
133137
searchQuery = searchQuery,
134138
isSearchActive = searchActive,
135139
searchResults = searchResults.value,
136140
showSearchLoader = showSearchLoader.value,
141+
sendInvitesAction = sendInvitesAction.value,
137142
eventSink = ::handleEvents,
138143
)
139144
}
140145

141146
private fun CoroutineScope.sendInvites(
142147
room: JoinedRoom,
143148
selectedUsers: List<MatrixUser>,
149+
sendInvitesAction: MutableState<AsyncAction<Unit>>,
144150
) = launch {
145-
val anyInviteFailed = selectedUsers
146-
.map { room.inviteUserById(it.userId) }
147-
.any { it.isFailure }
148-
149-
if (anyInviteFailed) {
150-
appErrorStateService.showError(
151-
titleRes = CommonStrings.common_unable_to_invite_title,
152-
bodyRes = CommonStrings.common_unable_to_invite_message,
153-
)
151+
sendInvitesAction.runUpdatingState {
152+
val anyInviteFailed = selectedUsers
153+
.map { room.inviteUserById(it.userId) }
154+
.any { it.isFailure }
155+
156+
if (anyInviteFailed) {
157+
appErrorStateService.showError(
158+
titleRes = CommonStrings.common_unable_to_invite_title,
159+
bodyRes = CommonStrings.common_unable_to_invite_message,
160+
)
161+
}
162+
163+
Result.success(Unit)
154164
}
155165
}
156166

features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleState.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ package io.element.android.features.invitepeople.impl
99

1010
import io.element.android.features.invitepeople.api.InvitePeopleEvents
1111
import io.element.android.features.invitepeople.api.InvitePeopleState
12+
import io.element.android.libraries.architecture.AsyncAction
1213
import io.element.android.libraries.architecture.AsyncData
1314
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
1415
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -22,5 +23,6 @@ data class DefaultInvitePeopleState(
2223
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
2324
val selectedUsers: ImmutableList<MatrixUser>,
2425
override val isSearchActive: Boolean,
26+
override val sendInvitesAction: AsyncAction<Unit>,
2527
override val eventSink: (InvitePeopleEvents) -> Unit
2628
) : InvitePeopleState

features/invitepeople/impl/src/main/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeopleStateProvider.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package io.element.android.features.invitepeople.impl
99

1010
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
11+
import io.element.android.libraries.architecture.AsyncAction
1112
import io.element.android.libraries.architecture.AsyncData
1213
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
1314
import io.element.android.libraries.matrix.api.user.MatrixUser
@@ -68,6 +69,11 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<Defau
6869
showSearchLoader = true,
6970
),
7071
aDefaultInvitePeopleState(room = AsyncData.Failure(Exception("Room not found"))),
72+
aDefaultInvitePeopleState(
73+
canInvite = false,
74+
selectedUsers = aMatrixUserList().toImmutableList(),
75+
sendInvitesAction = AsyncAction.Loading,
76+
),
7177
)
7278
}
7379

@@ -93,6 +99,7 @@ private fun aDefaultInvitePeopleState(
9399
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
94100
isSearchActive: Boolean = false,
95101
showSearchLoader: Boolean = false,
102+
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
96103
): DefaultInvitePeopleState {
97104
return DefaultInvitePeopleState(
98105
room = room,
@@ -102,6 +109,7 @@ private fun aDefaultInvitePeopleState(
102109
selectedUsers = selectedUsers,
103110
isSearchActive = isSearchActive,
104111
showSearchLoader = showSearchLoader,
112+
sendInvitesAction = sendInvitesAction,
105113
eventSink = {},
106114
)
107115
}

features/invitepeople/impl/src/test/kotlin/io/element/android/features/invitepeople/impl/DefaultInvitePeoplePresenterTest.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,10 +409,23 @@ internal class DefaultInvitePeoplePresenterTest {
409409
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
410410
// Send invites
411411
initialState.eventSink(InvitePeopleEvents.SendInvites)
412+
413+
// Can't invite in the loading state
414+
awaitItem().run {
415+
assertThat(sendInvitesAction.isLoading()).isTrue()
416+
assertThat(canInvite).isFalse()
417+
}
418+
412419
delay(1_000)
413420
inviteUserResult.assertions().isCalledOnce().with(
414421
value(selectedUser.userId)
415422
)
423+
424+
// Can invite again once the action is finished
425+
awaitItem().run {
426+
assertThat(sendInvitesAction.isReady()).isTrue()
427+
assertThat(canInvite).isTrue()
428+
}
416429
}
417430
}
418431

@@ -445,6 +458,13 @@ internal class DefaultInvitePeoplePresenterTest {
445458
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
446459
// Send invites
447460
initialState.eventSink(InvitePeopleEvents.SendInvites)
461+
462+
// Can't invite in the loading state
463+
awaitItem().run {
464+
assertThat(sendInvitesAction.isLoading()).isTrue()
465+
assertThat(canInvite).isFalse()
466+
}
467+
448468
delay(1_000)
449469
inviteUserResult.assertions().isCalledOnce().with(
450470
value(selectedUser.userId)
@@ -455,6 +475,12 @@ internal class DefaultInvitePeoplePresenterTest {
455475
value(CommonStrings.common_unable_to_invite_title),
456476
value(CommonStrings.common_unable_to_invite_message)
457477
)
478+
479+
// Can invite again once the action is finished
480+
awaitItem().run {
481+
assertThat(sendInvitesAction.isReady()).isTrue()
482+
assertThat(canInvite).isTrue()
483+
}
458484
}
459485
}
460486

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersNode.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
package io.element.android.features.roomdetails.impl.invite
99

1010
import androidx.compose.runtime.Composable
11+
import androidx.compose.runtime.LaunchedEffect
1112
import androidx.compose.ui.Modifier
1213
import com.bumble.appyx.core.lifecycle.subscribe
1314
import com.bumble.appyx.core.modality.BuildContext
@@ -49,11 +50,18 @@ class RoomInviteMembersNode(
4950
@Composable
5051
override fun View(modifier: Modifier) {
5152
val state = invitePeoplePresenter.present()
53+
54+
// Once invites have been sent successfully, close the Invite view.
55+
LaunchedEffect(state.sendInvitesAction) {
56+
if (state.sendInvitesAction.isReady()) {
57+
navigateUp()
58+
}
59+
}
60+
5261
RoomInviteMembersView(
5362
state = state,
5463
modifier = modifier,
55-
onBackClick = { navigateUp() },
56-
onDone = { navigateUp() }
64+
onBackClick = { navigateUp() }
5765
) {
5866
invitePeopleRenderer.Render(state, Modifier)
5967
}

features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,29 @@
88
package io.element.android.features.roomdetails.impl.invite
99

1010
import androidx.compose.foundation.layout.Box
11+
import androidx.compose.foundation.layout.Spacer
1112
import androidx.compose.foundation.layout.consumeWindowInsets
1213
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.height
1315
import androidx.compose.foundation.layout.padding
1416
import androidx.compose.material3.ExperimentalMaterial3Api
17+
import androidx.compose.material3.MaterialTheme
1518
import androidx.compose.runtime.Composable
1619
import androidx.compose.ui.Modifier
1720
import androidx.compose.ui.res.stringResource
1821
import androidx.compose.ui.tooling.preview.PreviewParameter
22+
import androidx.compose.ui.unit.dp
23+
import io.element.android.compound.theme.ElementTheme
1924
import io.element.android.features.invitepeople.api.InvitePeopleEvents
2025
import io.element.android.features.invitepeople.api.InvitePeopleState
2126
import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
2227
import io.element.android.features.roomdetails.impl.R
28+
import io.element.android.libraries.designsystem.components.ProgressDialog
2329
import io.element.android.libraries.designsystem.components.button.BackButton
2430
import io.element.android.libraries.designsystem.preview.ElementPreview
2531
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
2632
import io.element.android.libraries.designsystem.theme.components.Scaffold
33+
import io.element.android.libraries.designsystem.theme.components.Text
2734
import io.element.android.libraries.designsystem.theme.components.TextButton
2835
import io.element.android.libraries.designsystem.theme.components.TopAppBar
2936
import io.element.android.libraries.ui.strings.CommonStrings
@@ -32,7 +39,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
3239
fun RoomInviteMembersView(
3340
state: InvitePeopleState,
3441
onBackClick: () -> Unit,
35-
onDone: () -> Unit,
3642
modifier: Modifier = Modifier,
3743
invitePeopleView: @Composable () -> Unit,
3844
) {
@@ -49,7 +55,6 @@ fun RoomInviteMembersView(
4955
},
5056
onSubmitClick = {
5157
state.eventSink(InvitePeopleEvents.SendInvites)
52-
onDone()
5358
},
5459
canSend = state.canInvite,
5560
)
@@ -64,6 +69,10 @@ fun RoomInviteMembersView(
6469
invitePeopleView()
6570
}
6671
}
72+
73+
if (state.sendInvitesAction.isLoading()) {
74+
InviteProgressDialog()
75+
}
6776
}
6877

6978
@OptIn(ExperimentalMaterial3Api::class)
@@ -86,13 +95,30 @@ private fun RoomInviteMembersTopBar(
8695
)
8796
}
8897

98+
@Composable
99+
private fun InviteProgressDialog() {
100+
ProgressDialog {
101+
Spacer(modifier = Modifier.height(8.dp))
102+
Text(
103+
text = stringResource(R.string.screen_room_details_invite_people_preparing),
104+
color = ElementTheme.colors.textPrimary,
105+
style = ElementTheme.typography.fontHeadingSmMedium,
106+
)
107+
Spacer(modifier = Modifier.height(16.dp))
108+
Text(
109+
text = stringResource(R.string.screen_room_details_invite_people_dont_close),
110+
color = ElementTheme.colors.textSecondary,
111+
style = MaterialTheme.typography.bodyMedium,
112+
)
113+
}
114+
}
115+
89116
@PreviewsDayNight
90117
@Composable
91118
internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
92119
RoomInviteMembersView(
93120
state = state,
94121
invitePeopleView = {},
95122
onBackClick = {},
96-
onDone = {},
97123
)
98124
}

features/roomdetails/impl/src/main/res/values/localazy.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@
5050
<string name="screen_room_details_error_loading_notification_settings">"An error occurred when loading notification settings."</string>
5151
<string name="screen_room_details_error_muting">"Failed muting this room, please try again."</string>
5252
<string name="screen_room_details_error_unmuting">"Failed unmuting this room, please try again."</string>
53+
<string name="screen_room_details_invite_people_dont_close">"Don\'t close the app until finished."</string>
54+
<string name="screen_room_details_invite_people_preparing">"Preparing invitations…"</string>
5355
<string name="screen_room_details_invite_people_title">"Invite people"</string>
5456
<string name="screen_room_details_leave_conversation_title">"Leave conversation"</string>
5557
<string name="screen_room_details_leave_room_title">"Leave room"</string>

0 commit comments

Comments
 (0)