Improve ButtonWithProgress component, replace login and change server buttons (#235)

* Improve `ButtonWithProgress` component.

* Replace `CircularProgresIndicator` with `ButtonWithProgress` in login and server selection screens.
This commit is contained in:
Jorge Martin Espinosa 2023-03-28 22:56:59 +02:00 committed by GitHub
parent 012ee00c85
commit 4a121fbd0f
17 changed files with 96 additions and 104 deletions

1
changelog.d/235.misc Normal file
View file

@ -0,0 +1 @@
Improve ButtonWithProgress component, replace login and change server buttons

View file

@ -23,5 +23,5 @@ data class ChangeServerState(
val changeServerAction: Async<Unit>,
val eventSink: (ChangeServerEvents) -> Unit,
) {
val submitEnabled = homeserver.isNotEmpty() && changeServerAction is Async.Uninitialized
val submitEnabled: Boolean get() = homeserver.isNotEmpty() && (changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading)
}

View file

@ -64,14 +64,12 @@ import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.LinkColor
import io.element.android.libraries.designsystem.components.ClickableLinkText
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -94,9 +92,9 @@ fun ChangeServerView(
) {
val eventSink = state.eventSink
val scrollState = rememberScrollState()
val interactionEnabled by remember(state.changeServerAction) {
val isLoading by remember(state.changeServerAction) {
derivedStateOf {
state.changeServerAction !is Async.Loading
state.changeServerAction is Async.Loading
}
}
val invalidHomeserverError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage
@ -114,7 +112,7 @@ fun ChangeServerView(
topBar = {
TopAppBar(
title = {},
navigationIcon = { BackButton(onClick = onBackPressed, enabled = interactionEnabled) }
navigationIcon = { BackButton(onClick = onBackPressed) }
)
}
) { padding ->
@ -179,7 +177,7 @@ fun ChangeServerView(
var homeserverFieldState by textFieldState(stateValue = state.homeserver)
TextField(
value = homeserverFieldState,
readOnly = !interactionEnabled,
readOnly = isLoading,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.changeServerServer)
@ -201,7 +199,7 @@ fun ChangeServerView(
{
IconButton(onClick = {
eventSink(ChangeServerEvents.SetServer(""))
}, enabled = interactionEnabled) {
}, enabled = !isLoading) {
Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear))
}
}
@ -244,37 +242,23 @@ fun ChangeServerView(
})
}
Spacer(Modifier.height(32.dp))
Button(
ButtonWithProgress(
text = stringResource(id = R.string.screen_change_server_submit),
showProgress = isLoading,
onClick = ::submit,
enabled = interactionEnabled && state.submitEnabled,
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.changeServerContinue)
) {
Text(text = stringResource(id = R.string.screen_change_server_submit), style = ElementTextStyles.Button)
}
)
if (state.changeServerAction is Async.Success) {
onChangeServerSuccess()
}
}
if (state.changeServerAction is Async.Loading) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Composable
internal fun ChangeServerErrorDialog(title: String, message: String, onDismiss: () -> Unit) {
ErrorDialog(
title = title,
content = message,
onDismiss = onDismiss
)
}
@Composable
internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) {
ConfirmationDialog(

View file

@ -27,8 +27,8 @@ data class LoginRootState(
val formState: LoginFormState,
val eventSink: (LoginRootEvents) -> Unit
) {
val submitEnabled =
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn
val submitEnabled: Boolean get() =
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState !is LoggedInState.ErrorLoggingIn
}
sealed interface LoggedInState {

View file

@ -65,13 +65,12 @@ import androidx.compose.ui.unit.dp
import io.element.android.features.login.impl.R
import io.element.android.features.login.impl.error.loginError
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.button.ButtonWithProgress
import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog
import io.element.android.libraries.designsystem.components.form.textFieldState
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.IconButton
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -93,16 +92,16 @@ fun LoginRootView(
onLoginWithSuccess: (SessionId) -> Unit = {},
onBackPressed: () -> Unit,
) {
val interactionEnabled by remember(state.loggedInState) {
val isLoading by remember(state.loggedInState) {
derivedStateOf {
state.loggedInState != LoggedInState.LoggingIn
state.loggedInState == LoggedInState.LoggingIn
}
}
Scaffold(
topBar = {
TopAppBar(
title = {},
navigationIcon = { BackButton(onClick = onBackPressed, enabled = interactionEnabled) },
navigationIcon = { BackButton(onClick = onBackPressed) },
)
}
) { padding ->
@ -131,14 +130,14 @@ fun LoginRootView(
Spacer(Modifier.height(32.dp))
ChangeServerSection(
interactionEnabled = interactionEnabled,
interactionEnabled = !isLoading,
homeserver = state.homeserverDetails.url,
onChangeServer = onChangeServer
)
Spacer(Modifier.height(32.dp))
LoginForm(state = state, interactionEnabled = interactionEnabled)
LoginForm(state = state, isLoading = isLoading)
Spacer(modifier = Modifier.height(32.dp))
@ -147,12 +146,6 @@ fun LoginRootView(
is LoggedInState.LoggedIn -> onLoginWithSuccess(loggedInState.sessionId)
else -> Unit
}
if (state.loggedInState is LoggedInState.LoggingIn) {
CircularProgressIndicator(
modifier = Modifier.align(Alignment.Center)
)
}
}
}
@ -216,7 +209,7 @@ internal fun ChangeServerSection(
@Composable
internal fun LoginForm(
state: LoginRootState,
interactionEnabled: Boolean,
isLoading: Boolean,
modifier: Modifier = Modifier
) {
var loginFieldState by textFieldState(stateValue = state.formState.login)
@ -242,7 +235,7 @@ internal fun LoginForm(
Spacer(modifier = Modifier.height(8.dp))
TextField(
value = loginFieldState,
readOnly = !interactionEnabled,
readOnly = isLoading,
modifier = Modifier
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
@ -282,7 +275,7 @@ internal fun LoginForm(
Spacer(Modifier.height(20.dp))
TextField(
value = passwordFieldState,
readOnly = !interactionEnabled,
readOnly = isLoading,
modifier = Modifier
.fillMaxWidth()
.onTabOrEnterKeyFocusNext(focusManager)
@ -318,15 +311,15 @@ internal fun LoginForm(
Spacer(Modifier.height(28.dp))
// Submit
Button(
ButtonWithProgress(
text = stringResource(R.string.screen_login_submit),
showProgress = isLoading,
onClick = ::submit,
enabled = interactionEnabled && state.submitEnabled,
enabled = state.submitEnabled,
modifier = Modifier
.fillMaxWidth()
.testTag(TestTags.loginContinue)
) {
Text(text = stringResource(R.string.screen_login_submit), style = ElementTextStyles.Button)
}
)
}
}

View file

@ -22,8 +22,6 @@ import app.cash.molecule.RecompositionClock
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.features.login.impl.changeserver.ChangeServerEvents
import io.element.android.features.login.impl.changeserver.ChangeServerPresenter
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.matrix.test.A_HOMESERVER
import io.element.android.libraries.matrix.test.A_HOMESERVER_URL
@ -34,6 +32,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class)
class ChangeServerPresenterTest {
@Test
fun `present - should start with default homeserver`() = runTest {
@ -92,7 +91,7 @@ class ChangeServerPresenterTest {
val initialState = awaitItem()
initialState.eventSink.invoke(ChangeServerEvents.Submit)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isFalse()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
val successState = awaitItem()
assertThat(successState.submitEnabled).isFalse()
@ -114,7 +113,7 @@ class ChangeServerPresenterTest {
awaitItem()
initialState.eventSink.invoke(ChangeServerEvents.Submit)
val loadingState = awaitItem()
assertThat(loadingState.submitEnabled).isFalse()
assertThat(loadingState.submitEnabled).isTrue()
assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java)
awaitItem() // Skip changing the url to the parsed domain
val successState = awaitItem()

View file

@ -56,7 +56,6 @@ import io.element.android.libraries.designsystem.components.button.ButtonWithPro
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.LocalColors
import io.element.android.libraries.designsystem.theme.components.Button
import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator
import io.element.android.libraries.designsystem.theme.components.Icon
import io.element.android.libraries.designsystem.theme.components.Surface
@ -257,21 +256,12 @@ internal fun BottomMenu(screenState: VerifySelfSessionState, goBack: () -> Unit)
.padding(horizontal = 20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isVerifying) {
ButtonWithProgress(
modifier = Modifier.fillMaxWidth(),
onClick = { positiveButtonEvent?.let { eventSink(it) } }
) {
positiveButtonTitle?.let { Text(stringResource(it), style = ElementTextStyles.Button) }
}
} else {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { positiveButtonEvent?.let { eventSink(it) } }
) {
positiveButtonTitle?.let { Text(stringResource(it), style = ElementTextStyles.Button) }
}
}
ButtonWithProgress(
text = positiveButtonTitle?.let { stringResource(it) },
showProgress = isVerifying,
modifier = Modifier.fillMaxWidth(),
onClick = { positiveButtonEvent?.let { eventSink(it) } }
)
if (negativeButtonTitle != null) {
Spacer(modifier = Modifier.height(16.dp))
TextButton(

View file

@ -19,7 +19,6 @@ package io.element.android.libraries.designsystem.components.button
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -33,6 +32,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.ElementTextStyles
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Button
@ -40,10 +40,20 @@ import io.element.android.libraries.designsystem.theme.components.CircularProgre
import io.element.android.libraries.designsystem.theme.components.ElementButtonDefaults
import io.element.android.libraries.designsystem.theme.components.Text
/**
* A component that will display a button with an indeterminate circular progressbar.
* When [showProgress] is true:
* - A circular progressbar is displayed.
* - [text] is replaced by [progressText], if defined.
* - [onClick] gets disabled.
*/
@Composable
fun ButtonWithProgress(
text: String?,
onClick: () -> Unit,
modifier: Modifier = Modifier,
showProgress: Boolean = false,
progressText: String? = text,
enabled: Boolean = true,
shape: Shape = ElementButtonDefaults.shape,
colors: ButtonColors = ElementButtonDefaults.buttonColors(),
@ -51,10 +61,11 @@ fun ButtonWithProgress(
border: BorderStroke? = null,
contentPadding: PaddingValues = ElementButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable RowScope.() -> Unit
) {
Button(
onClick = onClick,
onClick = {
if (!showProgress) { onClick() }
},
modifier = modifier,
enabled = enabled,
shape = shape,
@ -64,15 +75,21 @@ fun ButtonWithProgress(
contentPadding = contentPadding,
interactionSource = interactionSource,
) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(18.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp,
)
Spacer(Modifier.width(10.dp))
content()
if (showProgress) {
CircularProgressIndicator(
modifier = Modifier
.progressSemantics()
.size(18.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp,
)
if (progressText != null) {
Spacer(Modifier.width(10.dp))
Text(progressText, style = ElementTextStyles.Button)
}
} else if (text != null) {
Text(text, style = ElementTextStyles.Button)
}
}
}
@ -86,7 +103,9 @@ internal fun ButtonWithProgressDarkPreview() = ElementPreviewDark { ContentToPre
@Composable
private fun ContentToPreview() {
ButtonWithProgress(onClick = {}) {
Text(text = "Button with progress")
}
ButtonWithProgress(
text = "Button with progress",
onClick = {},
showProgress = true,
)
}

View file

@ -122,10 +122,16 @@
<string name="rageshake_dialog_content">"You seem to be shaking the phone in frustration. Would you like to open the bug report screen?"</string>
<string name="report_content_explanation">"Reporting this message will send its unique event ID to the administrator of your homeserver. If messages in this room are encrypted, your homeserver administrator will not be able to read the message text or view any files or images."</string>
<string name="report_content_hint">"Reason for reporting this content"</string>
<string name="room_details_encryption_enabled_description">"Messages in this room are end-to-end encrypted. Learn more &amp; verify users in their profile."</string>
<string name="room_timeline_beginning_of_room">"This is the beginning of %1$s."</string>
<string name="room_timeline_beginning_of_room_no_name">"This is the beginning of this conversation."</string>
<string name="room_timeline_read_marker_title">"New"</string>
<string name="screen_room_details_encryption_enabled_subtitle">"Messages are secured with locks. Only you and the recipients have the unique keys to unlock them."</string>
<string name="screen_room_details_encryption_enabled_title">"Message encryption enabled"</string>
<string name="screen_room_details_invite_people_title">"Invite people"</string>
<string name="screen_room_details_leave_room_title">"Leave room"</string>
<string name="screen_room_details_people_title">"People"</string>
<string name="screen_room_details_security_title">"Security"</string>
<string name="screen_room_details_topic_title">"Topic"</string>
<string name="settings_rageshake">"Rageshake"</string>
<string name="settings_rageshake_detection_threshold">"Detection threshold"</string>
<string name="settings_title_general">"General"</string>

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:cb351458d7b5d70f2201f94e4466d5c7dbebb60bca164a986967d2d8006405da
size 43814
oid sha256:5450116320f6d75befbc784a31a8ad51c868936794e75e65bf24e6e1b69f1e23
size 44421

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f51052d3ece86fe7e07cc686a539b81d23f795c64bf66bd093b9d0e8e9fc7192
size 40335
oid sha256:af77d0276f38c78524201c49484548e29d1c99168e68e11874c49f0db0803df2
size 41392

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:00fbd0fb3779ed0d1a0c9bf32b6628e4c03f8cf686111d6ca438b5f513152f6f
size 32704
oid sha256:762a1d43dc3bc9edfa47f081b8385614f7075b5cecdd3eb3cb6b4959baf6811c
size 33445

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:2a2ef5bf60554a079c2a71d5f3e30e42b8e19203ca3784c77fe3aff49d4b9650
size 30254
oid sha256:7559f64cb74c33ef441dd15d557607061b72ffcdd3dce7ec7f8a663d928fd681
size 29585

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:4afa56c98494f6d6366c8e7e0032481c813a2798fbada236aa2f5f431be04de0
size 31485
oid sha256:2fafe08ac635bf3f0e62753c832ead92de297f0826ec735efe1a1f90481cb421
size 32417

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6a257ed45da52a2ef3f90376353ed2fa08b02c6e5919ebfae36f7b37a4166ea7
size 30073
oid sha256:7954d3f7064666b287ce6ab05fb6814f0e4570db2db6a44437291f0f90d8e87f
size 29062

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:01f7d29ebcba189d7b662c4427faeed01edcc60818e73437274520c2993e225b
size 9827
oid sha256:6aa8d1bbb81616f3561e23b5fae5a37d7d826f67c562ff7fb978681296fae8ec
size 10275

View file

@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:6371d2ea1465a5410e9b54f7bba662e3b8b334aacddcdf7b78d304f0ed580ddb
size 10315
oid sha256:c456be740c9f8480c4eb06f5f67eef868b6d27e206a7515b4d3356d1d0e2cde4
size 10672