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 <android@element.io> Co-authored-by: Jorge Martín <jorgem@element.io>
This commit is contained in:
parent
06e7bc1604
commit
5f920eb100
22 changed files with 189 additions and 19 deletions
|
|
@ -7,8 +7,11 @@
|
||||||
|
|
||||||
package io.element.android.features.invitepeople.api
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
interface InvitePeopleState {
|
interface InvitePeopleState {
|
||||||
val canInvite: Boolean
|
val canInvite: Boolean
|
||||||
val isSearchActive: Boolean
|
val isSearchActive: Boolean
|
||||||
|
val sendInvitesAction: AsyncAction<Unit>
|
||||||
val eventSink: (InvitePeopleEvents) -> Unit
|
val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,28 +8,33 @@
|
||||||
package io.element.android.features.invitepeople.api
|
package io.element.android.features.invitepeople.api
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
|
|
||||||
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
class InvitePeopleStateProvider : PreviewParameterProvider<InvitePeopleState> {
|
||||||
override val values: Sequence<InvitePeopleState>
|
override val values: Sequence<InvitePeopleState>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aPreviewInvitePeopleState(),
|
aPreviewInvitePeopleState(),
|
||||||
aPreviewInvitePeopleState(canInvite = true),
|
aPreviewInvitePeopleState(canInvite = true),
|
||||||
aPreviewInvitePeopleState(isSearchActive = true)
|
aPreviewInvitePeopleState(isSearchActive = true),
|
||||||
|
aPreviewInvitePeopleState(sendInvitesAction = AsyncAction.Loading),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class PreviewInvitePeopleState(
|
private data class PreviewInvitePeopleState(
|
||||||
override val canInvite: Boolean,
|
override val canInvite: Boolean,
|
||||||
override val isSearchActive: Boolean,
|
override val isSearchActive: Boolean,
|
||||||
|
override val sendInvitesAction: AsyncAction<Unit>,
|
||||||
override val eventSink: (InvitePeopleEvents) -> Unit,
|
override val eventSink: (InvitePeopleEvents) -> Unit,
|
||||||
) : InvitePeopleState
|
) : InvitePeopleState
|
||||||
|
|
||||||
private fun aPreviewInvitePeopleState(
|
private fun aPreviewInvitePeopleState(
|
||||||
canInvite: Boolean = false,
|
canInvite: Boolean = false,
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
|
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
eventSink: (InvitePeopleEvents) -> Unit = {},
|
eventSink: (InvitePeopleEvents) -> Unit = {},
|
||||||
) = PreviewInvitePeopleState(
|
) = PreviewInvitePeopleState(
|
||||||
canInvite = canInvite,
|
canInvite = canInvite,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
|
sendInvitesAction = sendInvitesAction,
|
||||||
eventSink = eventSink
|
eventSink = eventSink
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,11 @@ import dev.zacsweers.metro.Inject
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||||
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
import io.element.android.features.invitepeople.api.InvitePeoplePresenter
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.architecture.map
|
import io.element.android.libraries.architecture.map
|
||||||
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
import io.element.android.libraries.architecture.runCatchingUpdatingState
|
||||||
|
import io.element.android.libraries.architecture.runUpdatingState
|
||||||
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.di.SessionScope
|
import io.element.android.libraries.di.SessionScope
|
||||||
|
|
@ -73,6 +75,8 @@ class DefaultInvitePeoplePresenter(
|
||||||
var searchQuery by rememberSaveable { mutableStateOf("") }
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||||
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
val showSearchLoader = rememberSaveable { mutableStateOf(false) }
|
||||||
|
val sendInvitesAction = remember { mutableStateOf<AsyncAction<Unit>>(AsyncAction.Uninitialized) }
|
||||||
|
|
||||||
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
|
val room by produceState(if (joinedRoom != null) AsyncData.Success(joinedRoom) else AsyncData.Loading()) {
|
||||||
if (joinedRoom == null) {
|
if (joinedRoom == null) {
|
||||||
val result = matrixClient.getJoinedRoom(roomId)
|
val result = matrixClient.getJoinedRoom(roomId)
|
||||||
|
|
@ -116,7 +120,7 @@ class DefaultInvitePeoplePresenter(
|
||||||
}
|
}
|
||||||
is InvitePeopleEvents.SendInvites -> {
|
is InvitePeopleEvents.SendInvites -> {
|
||||||
room.dataOrNull()?.let {
|
room.dataOrNull()?.let {
|
||||||
sessionCoroutineScope.sendInvites(it, selectedUsers.value)
|
sessionCoroutineScope.sendInvites(it, selectedUsers.value, sendInvitesAction)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is InvitePeopleEvents.CloseSearch -> {
|
is InvitePeopleEvents.CloseSearch -> {
|
||||||
|
|
@ -128,12 +132,13 @@ class DefaultInvitePeoplePresenter(
|
||||||
|
|
||||||
return DefaultInvitePeopleState(
|
return DefaultInvitePeopleState(
|
||||||
room = room.map { },
|
room = room.map { },
|
||||||
canInvite = selectedUsers.value.isNotEmpty(),
|
canInvite = selectedUsers.value.isNotEmpty() && !sendInvitesAction.value.isLoading(),
|
||||||
selectedUsers = selectedUsers.value,
|
selectedUsers = selectedUsers.value,
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
isSearchActive = searchActive,
|
isSearchActive = searchActive,
|
||||||
searchResults = searchResults.value,
|
searchResults = searchResults.value,
|
||||||
showSearchLoader = showSearchLoader.value,
|
showSearchLoader = showSearchLoader.value,
|
||||||
|
sendInvitesAction = sendInvitesAction.value,
|
||||||
eventSink = ::handleEvents,
|
eventSink = ::handleEvents,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -141,16 +146,21 @@ class DefaultInvitePeoplePresenter(
|
||||||
private fun CoroutineScope.sendInvites(
|
private fun CoroutineScope.sendInvites(
|
||||||
room: JoinedRoom,
|
room: JoinedRoom,
|
||||||
selectedUsers: List<MatrixUser>,
|
selectedUsers: List<MatrixUser>,
|
||||||
|
sendInvitesAction: MutableState<AsyncAction<Unit>>,
|
||||||
) = launch {
|
) = launch {
|
||||||
val anyInviteFailed = selectedUsers
|
sendInvitesAction.runUpdatingState {
|
||||||
.map { room.inviteUserById(it.userId) }
|
val anyInviteFailed = selectedUsers
|
||||||
.any { it.isFailure }
|
.map { room.inviteUserById(it.userId) }
|
||||||
|
.any { it.isFailure }
|
||||||
|
|
||||||
if (anyInviteFailed) {
|
if (anyInviteFailed) {
|
||||||
appErrorStateService.showError(
|
appErrorStateService.showError(
|
||||||
titleRes = CommonStrings.common_unable_to_invite_title,
|
titleRes = CommonStrings.common_unable_to_invite_title,
|
||||||
bodyRes = CommonStrings.common_unable_to_invite_message,
|
bodyRes = CommonStrings.common_unable_to_invite_message,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.success(Unit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -22,5 +23,6 @@ data class DefaultInvitePeopleState(
|
||||||
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
val searchResults: SearchBarResultState<ImmutableList<InvitableUser>>,
|
||||||
val selectedUsers: ImmutableList<MatrixUser>,
|
val selectedUsers: ImmutableList<MatrixUser>,
|
||||||
override val isSearchActive: Boolean,
|
override val isSearchActive: Boolean,
|
||||||
|
override val sendInvitesAction: AsyncAction<Unit>,
|
||||||
override val eventSink: (InvitePeopleEvents) -> Unit
|
override val eventSink: (InvitePeopleEvents) -> Unit
|
||||||
) : InvitePeopleState
|
) : InvitePeopleState
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.invitepeople.impl
|
package io.element.android.features.invitepeople.impl
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.AsyncAction
|
||||||
import io.element.android.libraries.architecture.AsyncData
|
import io.element.android.libraries.architecture.AsyncData
|
||||||
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
import io.element.android.libraries.designsystem.theme.components.SearchBarResultState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
|
@ -68,6 +69,11 @@ internal class DefaultInvitePeopleStateProvider : PreviewParameterProvider<Defau
|
||||||
showSearchLoader = true,
|
showSearchLoader = true,
|
||||||
),
|
),
|
||||||
aDefaultInvitePeopleState(room = AsyncData.Failure(Exception("Room not found"))),
|
aDefaultInvitePeopleState(room = AsyncData.Failure(Exception("Room not found"))),
|
||||||
|
aDefaultInvitePeopleState(
|
||||||
|
canInvite = false,
|
||||||
|
selectedUsers = aMatrixUserList().toImmutableList(),
|
||||||
|
sendInvitesAction = AsyncAction.Loading,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,6 +99,7 @@ private fun aDefaultInvitePeopleState(
|
||||||
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
selectedUsers: ImmutableList<MatrixUser> = persistentListOf(),
|
||||||
isSearchActive: Boolean = false,
|
isSearchActive: Boolean = false,
|
||||||
showSearchLoader: Boolean = false,
|
showSearchLoader: Boolean = false,
|
||||||
|
sendInvitesAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
|
||||||
): DefaultInvitePeopleState {
|
): DefaultInvitePeopleState {
|
||||||
return DefaultInvitePeopleState(
|
return DefaultInvitePeopleState(
|
||||||
room = room,
|
room = room,
|
||||||
|
|
@ -102,6 +109,7 @@ private fun aDefaultInvitePeopleState(
|
||||||
selectedUsers = selectedUsers,
|
selectedUsers = selectedUsers,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
showSearchLoader = showSearchLoader,
|
showSearchLoader = showSearchLoader,
|
||||||
|
sendInvitesAction = sendInvitesAction,
|
||||||
eventSink = {},
|
eventSink = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -409,10 +409,23 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
// Send invites
|
// Send invites
|
||||||
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||||
|
|
||||||
|
// Can't invite in the loading state
|
||||||
|
awaitItem().run {
|
||||||
|
assertThat(sendInvitesAction.isLoading()).isTrue()
|
||||||
|
assertThat(canInvite).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
delay(1_000)
|
delay(1_000)
|
||||||
inviteUserResult.assertions().isCalledOnce().with(
|
inviteUserResult.assertions().isCalledOnce().with(
|
||||||
value(selectedUser.userId)
|
value(selectedUser.userId)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Can invite again once the action is finished
|
||||||
|
awaitItem().run {
|
||||||
|
assertThat(sendInvitesAction.isReady()).isTrue()
|
||||||
|
assertThat(canInvite).isTrue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -445,6 +458,13 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
assertThat(resultState.searchResults).isInstanceOf(SearchBarResultState.Results::class.java)
|
||||||
// Send invites
|
// Send invites
|
||||||
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
initialState.eventSink(InvitePeopleEvents.SendInvites)
|
||||||
|
|
||||||
|
// Can't invite in the loading state
|
||||||
|
awaitItem().run {
|
||||||
|
assertThat(sendInvitesAction.isLoading()).isTrue()
|
||||||
|
assertThat(canInvite).isFalse()
|
||||||
|
}
|
||||||
|
|
||||||
delay(1_000)
|
delay(1_000)
|
||||||
inviteUserResult.assertions().isCalledOnce().with(
|
inviteUserResult.assertions().isCalledOnce().with(
|
||||||
value(selectedUser.userId)
|
value(selectedUser.userId)
|
||||||
|
|
@ -455,6 +475,12 @@ internal class DefaultInvitePeoplePresenterTest {
|
||||||
value(CommonStrings.common_unable_to_invite_title),
|
value(CommonStrings.common_unable_to_invite_title),
|
||||||
value(CommonStrings.common_unable_to_invite_message)
|
value(CommonStrings.common_unable_to_invite_message)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Can invite again once the action is finished
|
||||||
|
awaitItem().run {
|
||||||
|
assertThat(sendInvitesAction.isReady()).isTrue()
|
||||||
|
assertThat(canInvite).isTrue()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
package io.element.android.features.roomdetails.impl.invite
|
package io.element.android.features.roomdetails.impl.invite
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
|
|
@ -49,11 +50,18 @@ class RoomInviteMembersNode(
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = invitePeoplePresenter.present()
|
val state = invitePeoplePresenter.present()
|
||||||
|
|
||||||
|
// Once invites have been sent successfully, close the Invite view.
|
||||||
|
LaunchedEffect(state.sendInvitesAction) {
|
||||||
|
if (state.sendInvitesAction.isReady()) {
|
||||||
|
navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
RoomInviteMembersView(
|
RoomInviteMembersView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onBackClick = { navigateUp() },
|
onBackClick = { navigateUp() }
|
||||||
onDone = { navigateUp() }
|
|
||||||
) {
|
) {
|
||||||
invitePeopleRenderer.Render(state, Modifier)
|
invitePeopleRenderer.Render(state, Modifier)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,22 +8,29 @@
|
||||||
package io.element.android.features.roomdetails.impl.invite
|
package io.element.android.features.roomdetails.impl.invite
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.element.android.compound.theme.ElementTheme
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
import io.element.android.features.invitepeople.api.InvitePeopleEvents
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleState
|
import io.element.android.features.invitepeople.api.InvitePeopleState
|
||||||
import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
|
import io.element.android.features.invitepeople.api.InvitePeopleStateProvider
|
||||||
import io.element.android.features.roomdetails.impl.R
|
import io.element.android.features.roomdetails.impl.R
|
||||||
|
import io.element.android.libraries.designsystem.components.ProgressDialog
|
||||||
import io.element.android.libraries.designsystem.components.button.BackButton
|
import io.element.android.libraries.designsystem.components.button.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreview
|
import io.element.android.libraries.designsystem.preview.ElementPreview
|
||||||
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
|
||||||
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
import io.element.android.libraries.designsystem.theme.components.Scaffold
|
||||||
|
import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
import io.element.android.libraries.designsystem.theme.components.TextButton
|
import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
|
@ -32,7 +39,6 @@ import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
fun RoomInviteMembersView(
|
fun RoomInviteMembersView(
|
||||||
state: InvitePeopleState,
|
state: InvitePeopleState,
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onDone: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
invitePeopleView: @Composable () -> Unit,
|
invitePeopleView: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -49,7 +55,6 @@ fun RoomInviteMembersView(
|
||||||
},
|
},
|
||||||
onSubmitClick = {
|
onSubmitClick = {
|
||||||
state.eventSink(InvitePeopleEvents.SendInvites)
|
state.eventSink(InvitePeopleEvents.SendInvites)
|
||||||
onDone()
|
|
||||||
},
|
},
|
||||||
canSend = state.canInvite,
|
canSend = state.canInvite,
|
||||||
)
|
)
|
||||||
|
|
@ -64,6 +69,10 @@ fun RoomInviteMembersView(
|
||||||
invitePeopleView()
|
invitePeopleView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.sendInvitesAction.isLoading()) {
|
||||||
|
InviteProgressDialog()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
|
@ -86,6 +95,24 @@ private fun RoomInviteMembersTopBar(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun InviteProgressDialog() {
|
||||||
|
ProgressDialog {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_room_details_invite_people_preparing),
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.screen_room_details_invite_people_dont_close),
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@PreviewsDayNight
|
@PreviewsDayNight
|
||||||
@Composable
|
@Composable
|
||||||
internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
|
internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStateProvider::class) state: InvitePeopleState) = ElementPreview {
|
||||||
|
|
@ -93,6 +120,5 @@ internal fun RoomInviteMembersViewPreview(@PreviewParameter(InvitePeopleStatePro
|
||||||
state = state,
|
state = state,
|
||||||
invitePeopleView = {},
|
invitePeopleView = {},
|
||||||
onBackClick = {},
|
onBackClick = {},
|
||||||
onDone = {},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,8 @@
|
||||||
<string name="screen_room_details_error_loading_notification_settings">"An error occurred when loading notification settings."</string>
|
<string name="screen_room_details_error_loading_notification_settings">"An error occurred when loading notification settings."</string>
|
||||||
<string name="screen_room_details_error_muting">"Failed muting this room, please try again."</string>
|
<string name="screen_room_details_error_muting">"Failed muting this room, please try again."</string>
|
||||||
<string name="screen_room_details_error_unmuting">"Failed unmuting this room, please try again."</string>
|
<string name="screen_room_details_error_unmuting">"Failed unmuting this room, please try again."</string>
|
||||||
|
<string name="screen_room_details_invite_people_dont_close">"Don\'t close the app until finished."</string>
|
||||||
|
<string name="screen_room_details_invite_people_preparing">"Preparing invitations…"</string>
|
||||||
<string name="screen_room_details_invite_people_title">"Invite people"</string>
|
<string name="screen_room_details_invite_people_title">"Invite people"</string>
|
||||||
<string name="screen_room_details_leave_conversation_title">"Leave conversation"</string>
|
<string name="screen_room_details_leave_conversation_title">"Leave conversation"</string>
|
||||||
<string name="screen_room_details_leave_room_title">"Leave room"</string>
|
<string name="screen_room_details_leave_room_title">"Leave room"</string>
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,18 @@ import io.element.android.libraries.designsystem.theme.components.TextButton
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A progress dialog, with a spinner, and optional text content.
|
||||||
|
*
|
||||||
|
* @param modifier
|
||||||
|
* @param text Optional text to show under the spinner.
|
||||||
|
* @param type
|
||||||
|
* @param properties
|
||||||
|
* @param showCancelButton
|
||||||
|
* @param onDismissRequest
|
||||||
|
* @param content Optional additional content to show under the spinner, and above the cancel button (if shown). If both `text` and `content` are supplied,
|
||||||
|
* `text` is shown above `content`.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ProgressDialog(
|
fun ProgressDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -46,6 +58,7 @@ fun ProgressDialog(
|
||||||
properties: DialogProperties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
properties: DialogProperties = DialogProperties(dismissOnBackPress = false, dismissOnClickOutside = false),
|
||||||
showCancelButton: Boolean = false,
|
showCancelButton: Boolean = false,
|
||||||
onDismissRequest: () -> Unit = {},
|
onDismissRequest: () -> Unit = {},
|
||||||
|
content: @Composable () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
onDispose {
|
onDispose {
|
||||||
|
|
@ -75,7 +88,8 @@ fun ProgressDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,7 +110,8 @@ private fun ProgressDialogContent(
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
color = ElementTheme.colors.iconPrimary
|
color = ElementTheme.colors.iconPrimary
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
|
@ -118,6 +133,7 @@ private fun ProgressDialogContent(
|
||||||
color = ElementTheme.colors.textPrimary,
|
color = ElementTheme.colors.textPrimary,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
content()
|
||||||
if (showCancelButton) {
|
if (showCancelButton) {
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -138,7 +154,7 @@ private fun ProgressDialogContent(
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
||||||
DialogPreview {
|
DialogPreview {
|
||||||
ProgressDialogContent(text = "test dialog content", showCancelButton = true)
|
ProgressDialogContent(text = "test dialog content", showCancelButton = true, content = {})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -147,3 +163,34 @@ internal fun ProgressDialogContentPreview() = ElementThemedPreview {
|
||||||
internal fun ProgressDialogPreview() = ElementPreview {
|
internal fun ProgressDialogPreview() = ElementPreview {
|
||||||
ProgressDialog(text = "test dialog content", showCancelButton = true)
|
ProgressDialog(text = "test dialog content", showCancelButton = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ProgressDialogWithContentPreview() = ElementPreview {
|
||||||
|
ProgressDialog(showCancelButton = true) {
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = "Heading",
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = "Subtext",
|
||||||
|
color = ElementTheme.colors.textSecondary,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreviewsDayNight
|
||||||
|
@Composable
|
||||||
|
internal fun ProgressDialogWithTextAndContentPreview() = ElementPreview {
|
||||||
|
ProgressDialog(text = "Text Content") {
|
||||||
|
Text(
|
||||||
|
text = "blah blah",
|
||||||
|
color = ElementTheme.colors.textPrimary,
|
||||||
|
style = ElementTheme.typography.fontHeadingSmMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -304,6 +304,7 @@ Reason: %1$s."</string>
|
||||||
<string name="common_sent">"Sent"</string>
|
<string name="common_sent">"Sent"</string>
|
||||||
<string name="common_sentence_delimiter">". "</string>
|
<string name="common_sentence_delimiter">". "</string>
|
||||||
<string name="common_server_not_supported">"Server not supported"</string>
|
<string name="common_server_not_supported">"Server not supported"</string>
|
||||||
|
<string name="common_server_unreachable">"Server unreachable"</string>
|
||||||
<string name="common_server_url">"Server URL"</string>
|
<string name="common_server_url">"Server URL"</string>
|
||||||
<string name="common_settings">"Settings"</string>
|
<string name="common_settings">"Settings"</string>
|
||||||
<string name="common_shared_location">"Shared location"</string>
|
<string name="common_shared_location">"Shared location"</string>
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,8 @@ class KonsistPreviewTest {
|
||||||
"PollContentViewDisclosedPreview",
|
"PollContentViewDisclosedPreview",
|
||||||
"PollContentViewEndedPreview",
|
"PollContentViewEndedPreview",
|
||||||
"PollContentViewUndisclosedPreview",
|
"PollContentViewUndisclosedPreview",
|
||||||
|
"ProgressDialogWithContentPreview",
|
||||||
|
"ProgressDialogWithTextAndContentPreview",
|
||||||
"ReadReceiptBottomSheetPreview",
|
"ReadReceiptBottomSheetPreview",
|
||||||
"RoomMemberListViewBannedPreview",
|
"RoomMemberListViewBannedPreview",
|
||||||
"SasEmojisPreview",
|
"SasEmojisPreview",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7415b3286707130c0218a8d1e82a9d9c61c8eec8df346653dd67626d0dd7709c
|
||||||
|
size 10038
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:39c67413ca997982b0fcd32831ccedee1d7a0763619784b8758b194df6c7ff81
|
||||||
|
size 9639
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:94341fe8aa52b4551ef8a73cf298aaa1c4352ae0d4c83990c04c4c6e491b64e8
|
||||||
|
size 21444
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:00a961c76598dc98a0d9c7a45781865984c51bb440bb1605c381e9496ca1789f
|
||||||
|
size 21971
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:a809a8792d519f93df9a15c57c06d3fd4f3cb7c84245e80cf61ff07ab1c5d620
|
||||||
|
size 20363
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:433e3467cfa0c0f4e737ec15a1c41d6bbc36d4d2c2dd9f2f7f22bb457208f280
|
||||||
|
size 19180
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:7b637e70da73ea0fca81adb861abf69fb2092d35493417fb29103f5acebf6fa0
|
||||||
|
size 11706
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:440b6f0e84c9fccfbbc997a40e1e8459df1962218fa4fdceb7ea8ecc8c96090b
|
||||||
|
size 11442
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:b3212b09963d5fa9b89b1ebfd8f904b3f436399299e67f560f39620d063eb997
|
||||||
|
size 11408
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
version https://git-lfs.github.com/spec/v1
|
||||||
|
oid sha256:ae69faa0c95f4ae4b7b410da438f0e3992571c21837e644e63621fa1f73e297a
|
||||||
|
size 11220
|
||||||
Loading…
Add table
Add a link
Reference in a new issue