Refine sign in flow to match designs and iOS flow (#100)

* Fix dark theme

* First attempt at replicating iOS' UI & flows.

* Try to fix Maestro tests

* Add error dialogs and tests

* Remove unused modifiers

* Try to fix detekt issues

* Tidy up maestro login flow a bit

* Add `CompoundColorPalette` with some needed colors

* Fixes to designs

* Fix detekt issues

* More design fixes

* Some other minor design fixes

* Add changelog

* Minor tweaks.

* Remove legacy dark material theme as it's no longer needed.

* Move sliding sync 'learn more' url to constants object

* Remove unused focusManager

* Change how the displayed homeserver works

* Keep user input as homeserver if it's valid

* Remove `CompoundColorPalette`, try to fix issue when toggling dark mode.

* Add `@Stable` to the theme, adjust how it toggles in dark mode

* Remove unused strings

* Update screenshots

* Re-organize components in LoginRootScreen

* Bump min coverage to 55, max to 60

* Always replace the snapshots contents when running `recordPaparazzi`

* Fix dark theme preview of components using content colors

* Add `BackButton` component

* Handle errors with dialogs in a generic way

* Align our Dialog components with the designs, use them were needed

* Use a `MatrixHomeserverDetails` data class instead of just an URL.

* `AuthenticationService.getHomeserverDetails()` now returns a `StateFlow`.

Also, try to fix coverage issues in tests.
This commit is contained in:
Jorge Martin Espinosa 2023-03-06 09:30:16 +01:00 committed by GitHub
parent 7997a65182
commit c87c0ea28c
271 changed files with 1431 additions and 871 deletions

View file

@ -41,6 +41,22 @@ val SystemGrey5Dark = Color(0xFF2C2C2E)
val SystemGrey6Light = Color(0xFFF2F2F7)
val SystemGrey6Dark = Color(0xFF1C1C1E)
// For light themes
val Gray_25 = Color(0xFFF4F6FA)
val Gray_50 = Color(0xFFE3E8F0)
val Gray_100 = Color(0xFFC1C6CD)
val Gray_150 = Color(0xFF8D97A5)
val Gray_200 = Color(0xFF737D8C)
val Black_900 = Color(0xFF17191C)
// For dark themes
val Gray_250 = Color(0xFFA9B2BC)
val Gray_300 = Color(0xFF8E99A4)
val Gray_400 = Color(0xFF6F7882)
val Gray_450 = Color(0xFF394049)
val Black_800 = Color(0xFF15191E)
val Black_950 = Color(0xFF21262C)
val Azure = Color(0xFF368BD6)
val Kiwi = Color(0xFF74D12C)
val Grape = Color(0xFFAC3BA8)

View file

@ -25,6 +25,14 @@ import androidx.compose.ui.unit.sp
// TODO Remove
object ElementTextStyles {
val Button = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.Bold,
lineHeight = 22.sp,
fontStyle = FontStyle.Normal,
textAlign = TextAlign.Center,
)
object Bold {
val largeTitle = TextStyle(
fontSize = 34.sp,
@ -180,6 +188,14 @@ object ElementTextStyles {
textAlign = TextAlign.Center
)
val formHeader = TextStyle(
fontSize = 14.sp,
fontWeight = FontWeight.Normal,
fontStyle = FontStyle.Normal,
lineHeight = 20.sp,
textAlign = TextAlign.Start
)
val footnote = TextStyle(
fontSize = 13.sp,
fontWeight = FontWeight.Normal,

View file

@ -17,13 +17,12 @@
package io.element.android.libraries.designsystem.components.dialogs
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -31,10 +30,8 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
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
import io.element.android.libraries.designsystem.theme.components.Text
import io.element.android.libraries.ui.strings.R as StringR
@ -67,43 +64,24 @@ fun ConfirmationDialog(
Text(content)
},
dismissButton = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Column {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
onCancelClicked()
}) {
Text(cancelText)
}
if (thirdButtonText != null) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
onThirdButtonClicked()
}) {
Text(thirdButtonText)
}
if (thirdButtonText != null) {
// If there is a 3rd item it should be at the end of the dialog
// Having this 3rd action is discouraged, see https://m3.material.io/components/dialogs/guidelines#e13b68f5-e367-4275-ad6f-c552ee8e358f
TextButton(onClick = onThirdButtonClicked) {
Text(thirdButtonText)
}
}
TextButton(onClick = onCancelClicked) {
Text(cancelText)
}
},
confirmButton = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
onSubmitClicked()
}
) {
Text(submitText)
TextButton(
onClick = {
onSubmitClicked()
}
) {
Text(submitText)
}
},
shape = shape,

View file

@ -23,6 +23,7 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@ -55,24 +56,14 @@ fun ErrorDialog(
modifier = modifier,
onDismissRequest = onDismiss,
title = {
Text(text = title)
Text(title)
},
text = {
Text(content)
},
confirmButton = {
Row(
modifier = Modifier.padding(all = 8.dp),
horizontalArrangement = Arrangement.Center
) {
Button(
modifier = Modifier.fillMaxWidth(),
onClick = {
onDismiss()
}
) {
Text(submitText)
}
TextButton(onClick = onDismiss) {
Text(submitText)
}
},
shape = shape,

View file

@ -16,12 +16,9 @@
package io.element.android.libraries.designsystem.preview
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import io.element.android.libraries.designsystem.theme.ElementTheme
import io.element.android.libraries.designsystem.theme.components.Surface
@Composable
fun ElementPreviewLight(
@ -55,9 +52,8 @@ private fun ElementPreview(
) {
ElementTheme(darkTheme = darkTheme) {
if (showBackground) {
Box(modifier = Modifier.background(MaterialTheme.colorScheme.background)) {
content()
}
// If we have a proper contentColor applied we need a Surface instead of a Box
Surface { content() }
} else {
content()
}

View file

@ -21,7 +21,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.Azure
import io.element.android.libraries.designsystem.Black_800
import io.element.android.libraries.designsystem.Black_950
import io.element.android.libraries.designsystem.DarkGrey
import io.element.android.libraries.designsystem.Gray_400
import io.element.android.libraries.designsystem.Gray_450
import io.element.android.libraries.designsystem.SystemGrey5Dark
import io.element.android.libraries.designsystem.SystemGrey6Dark
import io.element.android.libraries.designsystem.theme.previews.ColorsSchemePreview
@ -30,6 +34,8 @@ fun elementColorsDark() = ElementColors(
messageFromMeBackground = SystemGrey5Dark,
messageFromOtherBackground = SystemGrey6Dark,
messageHighlightedBackground = Azure,
quaternary = Gray_400,
quinary = Gray_450,
isLight = false,
)
@ -48,12 +54,12 @@ val materialColorSchemeDark = darkColorScheme(
// TODO onTertiary = ColorDarkTokens.OnTertiary,
// TODO tertiaryContainer = ColorDarkTokens.TertiaryContainer,
// TODO onTertiaryContainer = ColorDarkTokens.OnTertiaryContainer,
background = Color.Black,
background = Black_800,
onBackground = Color.White,
surface = Color.Black,
surface = Black_800,
onSurface = Color.White,
surfaceVariant = SystemGrey5Dark,
// TODO onSurfaceVariant = ColorDarkTokens.OnSurfaceVariant,
surfaceVariant = Black_950,
onSurfaceVariant = Color.White,
// TODO surfaceTint = primary,
// TODO inverseSurface = ColorDarkTokens.InverseSurface,
// TODO inverseOnSurface = ColorDarkTokens.InverseOnSurface,

View file

@ -21,7 +21,12 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.Azure
import io.element.android.libraries.designsystem.LightGrey
import io.element.android.libraries.designsystem.Black_900
import io.element.android.libraries.designsystem.Gray_100
import io.element.android.libraries.designsystem.Gray_150
import io.element.android.libraries.designsystem.Gray_200
import io.element.android.libraries.designsystem.Gray_25
import io.element.android.libraries.designsystem.Gray_50
import io.element.android.libraries.designsystem.SystemGrey5Light
import io.element.android.libraries.designsystem.SystemGrey6Light
import io.element.android.libraries.designsystem.theme.previews.ColorsSchemePreview
@ -30,21 +35,23 @@ fun elementColorsLight() = ElementColors(
messageFromMeBackground = SystemGrey5Light,
messageFromOtherBackground = SystemGrey6Light,
messageHighlightedBackground = Azure,
quaternary = Gray_100,
quinary = Gray_50,
isLight = true,
)
// TODO Lots of colors are missing
val materialColorSchemeLight = lightColorScheme(
primary = Color.Black,
primary = Black_900,
onPrimary = Color.White,
// TODO primaryContainer = ColorLightTokens.PrimaryContainer,
// TODO onPrimaryContainer = ColorLightTokens.OnPrimaryContainer,
// TODO inversePrimary = ColorLightTokens.InversePrimary,
secondary = LightGrey,
secondary = Gray_200,
// TODO onSecondary = ColorLightTokens.OnSecondary,
// TODO secondaryContainer = ColorLightTokens.SecondaryContainer,
// TODO onSecondaryContainer = ColorLightTokens.OnSecondaryContainer,
tertiary = Color.Black,
tertiary = Gray_150,
// TODO onTertiary = ColorLightTokens.OnTertiary,
// TODO tertiaryContainer = ColorLightTokens.TertiaryContainer,
// TODO onTertiaryContainer = ColorLightTokens.OnTertiaryContainer,
@ -52,8 +59,8 @@ val materialColorSchemeLight = lightColorScheme(
onBackground = Color.Black,
surface = Color.White,
onSurface = Color.Black,
surfaceVariant = SystemGrey5Light,
onSurfaceVariant = Color.Black,
surfaceVariant = Gray_25,
onSurfaceVariant = Gray_150,
// TODO surfaceTint = primary,
// TODO inverseSurface = ColorLightTokens.InverseSurface,
// TODO inverseOnSurface = ColorLightTokens.InverseOnSurface,

View file

@ -16,15 +16,19 @@
package io.element.android.libraries.designsystem.theme
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.graphics.Color
@Stable
class ElementColors(
messageFromMeBackground: Color,
messageFromOtherBackground: Color,
messageHighlightedBackground: Color,
quaternary: Color,
quinary: Color,
isLight: Boolean,
) {
var messageFromMeBackground by mutableStateOf(messageFromMeBackground)
@ -34,6 +38,12 @@ class ElementColors(
var messageHighlightedBackground by mutableStateOf(messageHighlightedBackground)
private set
var quaternary by mutableStateOf(quaternary)
private set
var quinary by mutableStateOf(quinary)
private set
var isLight by mutableStateOf(isLight)
private set
@ -41,11 +51,15 @@ class ElementColors(
messageFromMeBackground: Color = this.messageFromMeBackground,
messageFromOtherBackground: Color = this.messageFromOtherBackground,
messageHighlightedBackground: Color = this.messageHighlightedBackground,
quaternary: Color = this.quaternary,
quinary: Color = this.quinary,
isLight: Boolean = this.isLight,
) = ElementColors(
messageFromMeBackground = messageFromMeBackground,
messageFromOtherBackground = messageFromOtherBackground,
messageHighlightedBackground = messageHighlightedBackground,
quaternary = quaternary,
quinary = quinary,
isLight = isLight,
)
@ -53,6 +67,8 @@ class ElementColors(
messageFromMeBackground = other.messageFromMeBackground
messageFromOtherBackground = other.messageFromOtherBackground
messageHighlightedBackground = other.messageHighlightedBackground
quaternary = other.quaternary
quinary = other.quinary
isLight = other.isLight
}
}

View file

@ -49,15 +49,16 @@ val LocalColors = staticCompositionLocalOf { elementColorsLight() }
fun ElementTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = false, /* true to enable MaterialYou */
lightColors: ElementColors = elementColorsLight(),
darkColors: ElementColors = elementColorsDark(),
colors: ElementColors = if (darkTheme) elementColorsDark() else elementColorsLight(),
materialLightColors: ColorScheme = materialColorSchemeLight,
materialDarkColors: ColorScheme = materialColorSchemeDark,
content: @Composable () -> Unit,
) {
val systemUiController = rememberSystemUiController()
val useDarkIcons = !darkTheme
val currentColor = remember { if (darkTheme) darkColors else lightColors }
val currentColor = remember(darkTheme) {
colors.copy()
}.apply { updateColorsFrom(colors) }
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
@ -75,9 +76,8 @@ fun ElementTheme(
darkIcons = useDarkIcons
)
}
val rememberedColors = remember { currentColor.copy() }.apply { updateColorsFrom(currentColor) }
CompositionLocalProvider(
LocalColors provides rememberedColors,
LocalColors provides currentColor,
) {
MaterialTheme(
colorScheme = colorScheme,

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.theme.components
import androidx.compose.foundation.layout.Column
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.ui.strings.R.string as StringR
@Composable
fun BackButton(
action: () -> Unit,
modifier: Modifier = Modifier,
icon: ImageVector = Icons.Default.ArrowBack,
contentDescription: String = stringResource(StringR.action_back),
enabled: Boolean = true
) {
IconButton(
modifier = modifier,
onClick = action,
enabled = enabled,
) {
Icon(imageVector = icon, contentDescription = contentDescription)
}
}
@Preview
@Composable
internal fun BackButtonPreviewLight() = ElementPreviewLight { ContentToPreview() }
@Preview
@Composable
internal fun BackButtonPreviewDark() = ElementPreviewDark { ContentToPreview() }
@Composable
private fun ContentToPreview() {
Column {
BackButton(action = { }, enabled = true, contentDescription = "Back")
BackButton(action = { }, enabled = false, contentDescription = "Back")
}
}

View file

@ -29,6 +29,7 @@ import androidx.compose.runtime.remember
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.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
@ -37,11 +38,11 @@ fun Button(
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
shape: Shape = ButtonDefaults.shape,
colors: ButtonColors = ButtonDefaults.buttonColors(),
elevation: ButtonElevation? = ButtonDefaults.buttonElevation(),
shape: Shape = ElementButtonDefaults.shape,
colors: ButtonColors = ElementButtonDefaults.buttonColors(),
elevation: ButtonElevation? = ElementButtonDefaults.buttonElevation(),
border: BorderStroke? = null,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
contentPadding: PaddingValues = ElementButtonDefaults.ContentPadding,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: @Composable RowScope.() -> Unit
) {
@ -59,6 +60,17 @@ fun Button(
)
}
object ElementButtonDefaults {
val ContentPadding = PaddingValues(horizontal = 24.dp, vertical = 14.dp)
val shape: Shape @Composable get() = ButtonDefaults.shape
@Composable
fun buttonElevation(): ButtonElevation = ButtonDefaults.buttonElevation()
@Composable
fun buttonColors(): ButtonColors = ButtonDefaults.buttonColors()
}
@Preview
@Composable
internal fun ButtonsLightPreview() = ElementPreviewLight { ContentToPreview() }

View file

@ -29,8 +29,17 @@ import androidx.compose.material3.TextFieldColors
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.KeyEventType
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.key.type
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
@ -40,7 +49,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.utils.allBooleans
import io.element.android.libraries.designsystem.utils.asInt
@OptIn(ExperimentalMaterial3Api::class)
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
@Composable
fun OutlinedTextField(
value: String,
@ -88,6 +97,16 @@ fun OutlinedTextField(
)
}
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.onTabOrEnterKeyFocusNext(focusManager: FocusManager): Modifier = onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && (event.key == Key.Tab || event.key == Key.Enter)) {
focusManager.moveFocus(FocusDirection.Down)
true
} else {
false
}
}
@Preview
@Composable
internal fun OutlinedTextFieldsLightPreview() = ElementPreviewLight { ContentToPreview() }