Sign in with QR code (#2793)

* Add QR code login.
* Add FF to disable it in release mode.
* Force portrait orientation on the login flow.
* Create `NumberedList` UI components.
* Improve camera permission dialog.
* Make nodes in qrcode feature use `QrCodeLoginScope` instead of `AppScope`
* Bump SDK version.
* Fix maestro tests

---------

Co-authored-by: Benoit Marty <benoit@matrix.org>
Co-authored-by: ElementBot <benoitm+elementbot@element.io>
This commit is contained in:
Jorge Martin Espinosa 2024-05-31 14:38:27 +02:00 committed by GitHub
parent e0c55ff4c8
commit 35702c04e9
253 changed files with 4421 additions and 326 deletions

View file

@ -0,0 +1,67 @@
/*
* Copyright (c) 2024 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.atomic.molecules
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.libraries.designsystem.modifiers.squareSize
import io.element.android.libraries.designsystem.theme.components.Text
@Composable
fun NumberedListMolecule(
index: Int,
text: AnnotatedString,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
ItemNumber(index = index)
Text(text = text, style = ElementTheme.typography.fontBodyMdRegular, color = ElementTheme.colors.textPrimary)
}
}
@Composable
private fun ItemNumber(
index: Int,
) {
val color = ElementTheme.colors.textPlaceholder
Box(
modifier = Modifier
.border(1.dp, color, CircleShape)
.squareSize()
) {
Text(
modifier = Modifier.padding(1.5.dp),
text = index.toString(),
style = ElementTheme.typography.fontBodySmRegular,
color = color,
textAlign = TextAlign.Center,
)
}
}

View file

@ -0,0 +1,42 @@
/*
* Copyright (c) 2024 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.atomic.organisms
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import io.element.android.libraries.designsystem.atomic.molecules.NumberedListMolecule
import kotlinx.collections.immutable.ImmutableList
@Composable
fun NumberedListOrganism(
items: ImmutableList<AnnotatedString>,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
verticalArrangement = Arrangement.spacedBy(24.dp),
) {
itemsIndexed(items) { index, item ->
NumberedListMolecule(index = index + 1, text = item)
}
}
}

View file

@ -25,12 +25,12 @@ import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.tokens.generated.CompoundIcons
import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
import io.element.android.libraries.designsystem.components.BigIcon
import io.element.android.libraries.designsystem.components.PageTitle
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.preview.ElementPreview
import io.element.android.libraries.designsystem.preview.PreviewsDayNight
@ -49,7 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun FlowStepPage(
iconVector: ImageVector?,
iconStyle: BigIcon.Style,
title: String,
modifier: Modifier = Modifier,
onBackClick: (() -> Unit)? = null,
@ -73,10 +73,10 @@ fun FlowStepPage(
)
},
header = {
IconTitleSubtitleMolecule(
iconImageVector = iconVector,
PageTitle(
title = title,
subTitle = subTitle,
subtitle = subTitle,
iconStyle = iconStyle,
)
},
content = content,
@ -97,7 +97,7 @@ internal fun FlowStepPagePreview() = ElementPreview {
onBackClick = {},
title = "Title",
subTitle = "Subtitle",
iconVector = CompoundIcons.Computer(),
iconStyle = BigIcon.Style.Default(CompoundIcons.Computer()),
buttons = {
TextButton(text = "A button", onClick = { })
Button(text = "Continue", onClick = { })

View file

@ -44,6 +44,7 @@ fun ConfirmationDialog(
thirdButtonText: String? = null,
onCancelClick: () -> Unit = onDismiss,
onThirdButtonClick: () -> Unit = {},
icon: @Composable (() -> Unit)? = null,
) {
BasicAlertDialog(modifier = modifier, onDismissRequest = onDismiss) {
ConfirmationDialogContent(
@ -56,6 +57,7 @@ fun ConfirmationDialog(
onSubmitClick = onSubmitClick,
onCancelClick = onCancelClick,
onThirdButtonClick = onThirdButtonClick,
icon = icon,
)
}
}

View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2024 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.modifiers
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.unit.Dp
import io.element.android.libraries.designsystem.text.toPx
/**
* Draw a border on corners around the content.
*/
@Suppress("ModifierComposed")
fun Modifier.cornerBorder(
strokeWidth: Dp,
color: Color,
cornerSizeDp: Dp,
) = composed(
factory = {
val strokeWidthPx = strokeWidth.toPx()
val cornerSize = cornerSizeDp.toPx()
drawWithContent {
drawContent()
val width = size.width
val height = size.height
drawPath(
path = Path().apply {
// Top left corner
moveTo(0f, cornerSize)
lineTo(0f, 0f)
lineTo(cornerSize, 0f)
// Top right corner
moveTo(width - cornerSize, 0f)
lineTo(width, 0f)
lineTo(width, cornerSize)
// Bottom right corner
moveTo(width, height - cornerSize)
lineTo(width, height)
lineTo(width - cornerSize, height)
// Bottom left corner
moveTo(cornerSize, height)
lineTo(0f, height)
lineTo(0f, height - cornerSize)
},
color = color,
style = Stroke(
width = strokeWidthPx,
pathEffect = PathEffect.cornerPathEffect(strokeWidthPx / 2),
cap = StrokeCap.Round,
),
)
}
}
)

View file

@ -37,6 +37,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@ -150,6 +151,7 @@ internal fun SimpleAlertDialogContent(
Text(
text = titleText,
style = ElementTheme.typography.fontHeadingSmMedium,
textAlign = TextAlign.Center,
)
}
},

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2024 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.utils
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
@Composable
fun annotatedTextWithBold(text: String, boldText: String): AnnotatedString {
return buildAnnotatedString {
append(text)
val start = text.indexOf(boldText)
val end = start + boldText.length
val textRange = 0..text.length
if (start in textRange && end in textRange) {
addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end)
}
}
}

View file

@ -0,0 +1,41 @@
/*
* Copyright (c) 2024 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.utils
import android.content.pm.ActivityInfo
import androidx.activity.ComponentActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.platform.LocalContext
@Composable
fun ForceOrientation(orientation: ScreenOrientation) {
val activity = LocalContext.current as? ComponentActivity ?: return
val orientationFlags = when (orientation) {
ScreenOrientation.PORTRAIT -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
ScreenOrientation.LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
}
DisposableEffect(orientation) {
activity.requestedOrientation = orientationFlags
onDispose { activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED }
}
}
enum class ScreenOrientation {
PORTRAIT,
LANDSCAPE
}

View file

@ -0,0 +1,34 @@
/*
* Copyright (c) 2024 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.utils
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.runtime.Composable
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
@Composable
fun ForceOrientationInMobileDevices(orientation: ScreenOrientation) {
val windowAdaptiveInfo = currentWindowAdaptiveInfo()
if (windowAdaptiveInfo.windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
windowAdaptiveInfo.windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact
) {
ForceOrientation(orientation = orientation)
}
}