diff --git a/.idea/dictionaries/bmarty.xml b/.idea/dictionaries/bmarty.xml new file mode 100644 index 0000000000..dd650c15e1 --- /dev/null +++ b/.idea/dictionaries/bmarty.xml @@ -0,0 +1,7 @@ + + + + homeserver + + + \ No newline at end of file diff --git a/.maestro/README.md b/.maestro/README.md index 76268e144e..3926dcdf56 100644 --- a/.maestro/README.md +++ b/.maestro/README.md @@ -25,7 +25,7 @@ maestro test \ -e APP_ID=io.element.android.x.debug \ -e USERNAME=user \ -e PASSWORD=123 \ - -e ROOM_NAME="my room" \ + -e ROOM_NAME="MyRoom" \ .maestro/allTests.yaml ``` diff --git a/.maestro/tests/account/changeServer.yaml b/.maestro/tests/account/changeServer.yaml index 505a12d2e1..df4b12f253 100644 --- a/.maestro/tests/account/changeServer.yaml +++ b/.maestro/tests/account/changeServer.yaml @@ -3,4 +3,15 @@ appId: ${APP_ID} - tapOn: id: "login-change_server" - takeScreenshot: build/maestro/200-ChangeServer -- tapOn: "Continue" +- tapOn: "matrix.org" +- tapOn: + id: "login-change_server" +- tapOn: "Other" +- tapOn: + id: "change_server-server" +- inputText: "element" +- hideKeyboard +- tapOn: "element.io" +- tapOn: "Cancel" +- back +- back diff --git a/.maestro/tests/account/login.yaml b/.maestro/tests/account/login.yaml index 845746a76b..728ff98b31 100644 --- a/.maestro/tests/account/login.yaml +++ b/.maestro/tests/account/login.yaml @@ -5,6 +5,8 @@ appId: ${APP_ID} - takeScreenshot: build/maestro/100-SignIn - runFlow: changeServer.yaml - runFlow: ../assertions/assertLoginDisplayed.yaml +- tapOn: + id: "login-continue" - tapOn: id: "login-email_username" - inputText: ${USERNAME} diff --git a/.maestro/tests/assertions/assertLoginDisplayed.yaml b/.maestro/tests/assertions/assertLoginDisplayed.yaml index 41f1ff3306..3abd86ceef 100644 --- a/.maestro/tests/assertions/assertLoginDisplayed.yaml +++ b/.maestro/tests/assertions/assertLoginDisplayed.yaml @@ -1,5 +1,5 @@ appId: ${APP_ID} --- - extendedWaitUntil: - visible: "Welcome back!" + visible: "Change account provider" timeout: 10_000 diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt index f1d87330e1..c07a2d72e9 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInEventProcessor.kt @@ -23,7 +23,6 @@ import io.element.android.libraries.matrix.api.verification.SessionVerificationS import io.element.android.libraries.matrix.api.verification.VerificationFlowState import io.element.android.libraries.ui.strings.R import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn @@ -31,7 +30,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import javax.inject.Inject -import kotlin.coroutines.coroutineContext class LoggedInEventProcessor @Inject constructor( private val snackbarDispatcher: SnackbarDispatcher, diff --git a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt index 3d99f03fa0..4b89b442d7 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/NotLoggedInFlowNode.kt @@ -63,7 +63,9 @@ class NotLoggedInFlowNode @AssistedInject constructor( object OnBoarding : NavTarget @Parcelize - object LoginFlow : NavTarget + data class LoginFlow( + val isAccountCreation: Boolean, + ) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -71,11 +73,11 @@ class NotLoggedInFlowNode @AssistedInject constructor( NavTarget.OnBoarding -> { val callback = object : OnBoardingEntryPoint.Callback { override fun onSignUp() { - //NOOP + backstack.push(NavTarget.LoginFlow(isAccountCreation = true)) } override fun onSignIn() { - backstack.push(NavTarget.LoginFlow) + backstack.push(NavTarget.LoginFlow(isAccountCreation = false)) } } onBoardingEntryPoint @@ -83,8 +85,10 @@ class NotLoggedInFlowNode @AssistedInject constructor( .callback(callback) .build() } - NavTarget.LoginFlow -> { - loginEntryPoint.createNode(this, buildContext) + is NavTarget.LoginFlow -> { + loginEntryPoint.nodeBuilder(this, buildContext) + .params(LoginEntryPoint.Params(isAccountCreation = navTarget.isAccountCreation)) + .build() } } } diff --git a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt index 54ab3d1dc5..03eeb3adc6 100644 --- a/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt +++ b/features/leaveroom/impl/src/test/kotlin/io/element/android/features/leaveroom/impl/LeaveRoomPresenterImplTest.kt @@ -213,5 +213,5 @@ private fun TestScope.createPresenter( ): LeaveRoomPresenter = LeaveRoomPresenterImpl( client = client, roomMembershipObserver = roomMembershipObserver, - dispatchers = testCoroutineDispatchers(testScheduler, false), + dispatchers = testCoroutineDispatchers(false), ) diff --git a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt index 0eac558ba5..07a546192d 100644 --- a/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt +++ b/features/login/api/src/main/kotlin/io/element/android/features/login/api/LoginEntryPoint.kt @@ -16,6 +16,19 @@ package io.element.android.features.login.api -import io.element.android.libraries.architecture.SimpleFeatureEntryPoint +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import io.element.android.libraries.architecture.FeatureEntryPoint -interface LoginEntryPoint : SimpleFeatureEntryPoint +interface LoginEntryPoint : FeatureEntryPoint { + data class Params( + val isAccountCreation: Boolean, + ) + + fun nodeBuilder(parentNode: Node, buildContext: BuildContext): NodeBuilder + + interface NodeBuilder { + fun params(params: Params): NodeBuilder + fun build(): Node + } +} diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index c43a5c2cd3..aacf4c0aeb 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -19,6 +19,7 @@ plugins { alias(libs.plugins.anvil) alias(libs.plugins.ksp) id("kotlin-parcelize") + kotlin("plugin.serialization") version "1.8.21" } android { @@ -41,11 +42,15 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.matrix.api) + implementation(projects.libraries.network) implementation(projects.libraries.designsystem) implementation(projects.libraries.elementresources) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) implementation(libs.androidx.browser) + implementation(libs.network.retrofit) + implementation(libs.serialization.json) api(projects.features.login.api) ksp(libs.showkase.processor) @@ -55,6 +60,7 @@ dependencies { testImplementation(libs.test.truth) testImplementation(libs.test.turbine) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) androidTestImplementation(libs.test.junitext) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt index 64c30b7727..a4290825fb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/DefaultLoginEntryPoint.kt @@ -18,6 +18,7 @@ package io.element.android.features.login.impl import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin import com.squareup.anvil.annotations.ContributesBinding import io.element.android.features.login.api.LoginEntryPoint import io.element.android.libraries.architecture.createNode @@ -26,7 +27,19 @@ import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultLoginEntryPoint @Inject constructor() : LoginEntryPoint { - override fun createNode(parentNode: Node, buildContext: BuildContext): Node { - return parentNode.createNode(buildContext) + override fun nodeBuilder(parentNode: Node, buildContext: BuildContext): LoginEntryPoint.NodeBuilder { + val plugins = ArrayList() + + return object : LoginEntryPoint.NodeBuilder { + + override fun params(params: LoginEntryPoint.Params): LoginEntryPoint.NodeBuilder { + plugins += LoginFlowNode.Inputs(isAccountCreation = params.isAccountCreation) + return this + } + + override fun build(): Node { + return parentNode.createNode(buildContext, plugins) + } + } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt index 36153a33ba..33e3a66abe 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/LoginFlowNode.kt @@ -20,7 +20,6 @@ import android.app.Activity import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.composable.Children @@ -29,17 +28,23 @@ import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push +import com.bumble.appyx.navmodel.backstack.operation.singleTop import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.login.impl.changeserver.ChangeServerNode +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler import io.element.android.features.login.impl.oidc.webview.OidcNode -import io.element.android.features.login.impl.root.LoginRootNode +import io.element.android.features.login.impl.screens.changeaccountprovider.ChangeAccountProviderNode +import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderNode +import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordNode +import io.element.android.features.login.impl.screens.searchaccountprovider.SearchAccountProviderNode import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.architecture.inputs import io.element.android.libraries.designsystem.theme.ElementTheme import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails @@ -51,9 +56,10 @@ class LoginFlowNode @AssistedInject constructor( @Assisted plugins: List, private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, private val customTabHandler: CustomTabHandler, + private val accountProviderDataSource: AccountProviderDataSource, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Root, + initialElement = NavTarget.ConfirmAccountProvider, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -62,12 +68,24 @@ class LoginFlowNode @AssistedInject constructor( private var activity: Activity? = null private var darkTheme: Boolean = false + data class Inputs( + val isAccountCreation: Boolean, + ) : NodeInputs + + private val inputs: Inputs = inputs() + sealed interface NavTarget : Parcelable { @Parcelize - object Root : NavTarget + object ConfirmAccountProvider : NavTarget @Parcelize - object ChangeServer : NavTarget + object ChangeAccountProvider : NavTarget + + @Parcelize + object SearchAccountProvider : NavTarget + + @Parcelize + object LoginPassword : NavTarget @Parcelize data class OidcView(val oidcDetails: OidcDetails) : NavTarget @@ -75,12 +93,11 @@ class LoginFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - NavTarget.Root -> { - val callback = object : LoginRootNode.Callback { - override fun onChangeHomeServer() { - backstack.push(NavTarget.ChangeServer) - } - + NavTarget.ConfirmAccountProvider -> { + val inputs = ConfirmAccountProviderNode.Inputs( + isAccountCreation = inputs.isAccountCreation + ) + val callback = object : ConfirmAccountProviderNode.Callback { override fun onOidcDetails(oidcDetails: OidcDetails) { if (customTabAvailabilityChecker.supportCustomTab()) { // In this case open a Chrome Custom tab @@ -90,11 +107,44 @@ class LoginFlowNode @AssistedInject constructor( backstack.push(NavTarget.OidcView(oidcDetails)) } } - } - createNode(buildContext, plugins = listOf(callback)) - } - NavTarget.ChangeServer -> createNode(buildContext) + override fun onLoginPasswordNeeded() { + backstack.push(NavTarget.LoginPassword) + } + + override fun onChangeAccountProvider() { + backstack.push(NavTarget.ChangeAccountProvider) + } + } + createNode(buildContext, plugins = listOf(inputs, callback)) + } + NavTarget.ChangeAccountProvider -> { + val callback = object : ChangeAccountProviderNode.Callback { + override fun onDone() { + // Go back to the Account Provider screen + backstack.singleTop(NavTarget.ConfirmAccountProvider) + } + + override fun onOtherClicked() { + backstack.push(NavTarget.SearchAccountProvider) + } + } + + createNode(buildContext, plugins = listOf(callback)) + } + NavTarget.SearchAccountProvider -> { + val callback = object : SearchAccountProviderNode.Callback { + override fun onDone() { + // Go back to the Account Provider screen + backstack.singleTop(NavTarget.ConfirmAccountProvider) + } + } + + createNode(buildContext, plugins = listOf(callback)) + } + NavTarget.LoginPassword -> { + createNode(buildContext, plugins = listOf()) + } is NavTarget.OidcView -> { val input = OidcNode.Inputs(navTarget.oidcDetails) createNode(buildContext, plugins = listOf(input)) @@ -109,6 +159,7 @@ class LoginFlowNode @AssistedInject constructor( DisposableEffect(Unit) { onDispose { activity = null + accountProviderDataSource.reset() } } Children( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt new file mode 100644 index 0000000000..b6aea81951 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt @@ -0,0 +1,26 @@ +/* + * 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.features.login.impl.accountprovider + +data class AccountProvider constructor( + val title: String, + val subtitle: String? = null, + val isPublic: Boolean = false, + val isMatrixOrg: Boolean = false, + val isValid: Boolean = false, + val supportSlidingSync: Boolean = false, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt new file mode 100644 index 0000000000..ea541285df --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt @@ -0,0 +1,45 @@ +/* + * 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.features.login.impl.accountprovider + +import io.element.android.features.login.impl.util.defaultAccountProvider +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@SingleIn(AppScope::class) +class AccountProviderDataSource @Inject constructor( +) { + private val accountProvider: MutableStateFlow = MutableStateFlow( + defaultAccountProvider + ) + + fun flow(): StateFlow { + return accountProvider.asStateFlow() + } + + fun reset() { + accountProvider.tryEmit(defaultAccountProvider) + } + + fun userSelection(data: AccountProvider) { + accountProvider.tryEmit(data) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt new file mode 100644 index 0000000000..71e1abd591 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt @@ -0,0 +1,40 @@ +/* + * 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.features.login.impl.accountprovider + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class AccountProviderProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + anAccountProvider(), + anAccountProvider().copy(subtitle = null), + anAccountProvider().copy(subtitle = null, title = "no.sliding.sync", supportSlidingSync = false), + anAccountProvider().copy(subtitle = null, title = "invalid", isValid = false, supportSlidingSync = false), + anAccountProvider().copy(subtitle = null, title = "Other", isPublic = false, isMatrixOrg = false), + // Add other state here + ) +} + +fun anAccountProvider() = AccountProvider( + title = "matrix.org", + subtitle = "Matrix.org is an open network for secure, decentralized communication.", + isPublic = true, + isMatrixOrg = true, + isValid = true, + supportSlidingSync = true, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt new file mode 100644 index 0000000000..0ceb65dea9 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt @@ -0,0 +1,132 @@ +/* + * 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.features.login.impl.accountprovider + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.login.impl.R +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom +import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Divider +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Text + +/** + * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 + */ +@Composable +fun AccountProviderView( + item: AccountProvider, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Column(modifier = modifier + .fillMaxWidth() + .clickable { onClick() }) { + Divider() + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 44.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (item.isMatrixOrg) { + RoundedIconAtom( + size = RoundedIconAtomSize.Medium, + resourceId = R.drawable.ic_matrix, + tint = Color.Unspecified, + ) + } else { + RoundedIconAtom( + size = RoundedIconAtomSize.Medium, + imageVector = Icons.Filled.Search, + tint = MaterialTheme.colorScheme.primary, + ) + } + Text( + modifier = Modifier + .padding(start = 16.dp) + .weight(1f), + text = item.title, + style = ElementTextStyles.Regular.headline.copy(textAlign = TextAlign.Start), + color = MaterialTheme.colorScheme.primary, + ) + if (item.isPublic) { + Icon( + modifier = Modifier + .padding(start = 10.dp) + .size(16.dp), + resourceId = R.drawable.ic_public, + contentDescription = null, + tint = Color.Unspecified, + ) + } + } + if (item.subtitle != null) { + Text( + modifier = Modifier + .padding(start = 46.dp, bottom = 12.dp, end = 26.dp), + text = item.subtitle, + style = ElementTextStyles.Regular.subheadline.copy(textAlign = TextAlign.Start), + color = MaterialTheme.colorScheme.secondary, + ) + } + } + } +} + +@Preview +@Composable +fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) = + ElementPreviewLight { ContentToPreview(item) } + +@Preview +@Composable +fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) = + ElementPreviewDark { ContentToPreview(item) } + +@Composable +private fun ContentToPreview(item: AccountProvider) { + AccountProviderView( + item = item, + onClick = { } + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt index dd1e83c3f8..cd1cb7b4ce 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt @@ -16,8 +16,9 @@ package io.element.android.features.login.impl.changeserver +import io.element.android.features.login.impl.accountprovider.AccountProvider + sealed interface ChangeServerEvents { - data class SetServer(val server: String) : ChangeServerEvents - object Submit : ChangeServerEvents + data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents object ClearError : ChangeServerEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index f2e688812c..2e3f9548d6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt @@ -21,8 +21,9 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.features.login.impl.util.LoginConstants +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.execute @@ -33,44 +34,43 @@ import kotlinx.coroutines.launch import java.net.URL import javax.inject.Inject -class ChangeServerPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : Presenter { +class ChangeServerPresenter @Inject constructor( + private val authenticationService: MatrixAuthenticationService, + private val accountProviderDataSource: AccountProviderDataSource, +) : Presenter { @Composable override fun present(): ChangeServerState { val localCoroutineScope = rememberCoroutineScope() - val homeserver = rememberSaveable { - mutableStateOf(authenticationService.getHomeserverDetails().value?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL) - } val changeServerAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } fun handleEvents(event: ChangeServerEvents) { when (event) { - is ChangeServerEvents.SetServer -> { - homeserver.value = event.server - handleEvents(ChangeServerEvents.ClearError) - } - ChangeServerEvents.Submit -> { - localCoroutineScope.submit(homeserver, changeServerAction) - } + is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction) ChangeServerEvents.ClearError -> changeServerAction.value = Async.Uninitialized } } return ChangeServerState( - homeserver = homeserver.value, changeServerAction = changeServerAction.value, eventSink = ::handleEvents ) } - private fun CoroutineScope.submit(homeserverUrl: MutableState, changeServerAction: MutableState>) = launch { + private fun CoroutineScope.changeServer( + data: AccountProvider, + changeServerAction: MutableState>, + ) = launch { suspend { - val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value - authenticationService.setHomeserver(domain).getOrThrow() - homeserverUrl.value = domain + val domain = tryOrNull { URL(data.title) }?.host ?: data.title + authenticationService.setHomeserver(domain).map { + authenticationService.getHomeserverDetails().value!! + // Valid, remember user choice + accountProviderDataSource.userSelection(data) + }.getOrThrow() }.execute(changeServerAction, errorMapping = ChangeServerError::from) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt index 5a3ea3b856..e49fa1f2fe 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt @@ -19,9 +19,6 @@ package io.element.android.features.login.impl.changeserver import io.element.android.libraries.architecture.Async data class ChangeServerState( - val homeserver: String, val changeServerAction: Async, - val eventSink: (ChangeServerEvents) -> Unit, -) { - val submitEnabled: Boolean get() = homeserver.isNotEmpty() && (changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading) -} + val eventSink: (ChangeServerEvents) -> Unit +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt index 3e6eeef05a..90c2bff455 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt @@ -17,26 +17,16 @@ package io.element.android.features.login.impl.changeserver import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.R import io.element.android.libraries.architecture.Async open class ChangeServerStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aChangeServerState(), - aChangeServerState().copy(homeserver = "matrix.org"), - aChangeServerState().copy(homeserver = "matrix.org", changeServerAction = Async.Loading()), - aChangeServerState().copy( - homeserver = "invalid.org", - changeServerAction = Async.Failure(ChangeServerError.InlineErrorMessage(R.string.screen_change_server_error_invalid_homeserver)) - ), - aChangeServerState().copy(homeserver = "invalid.org", changeServerAction = Async.Failure(ChangeServerError.SlidingSyncAlert)), - aChangeServerState().copy(homeserver = "matrix.org", changeServerAction = Async.Success(Unit)), ) } fun aChangeServerState() = ChangeServerState( - homeserver = "", changeServerAction = Async.Uninitialized, eventSink = {} ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt index cbd2bc9308..f9e9624503 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -16,276 +16,74 @@ package io.element.android.features.login.impl.changeserver -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.consumeWindowInsets -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.ExperimentalTextApi -import androidx.compose.ui.text.ParagraphStyle -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.UrlAnnotation -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.util.LoginConstants +import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog +import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.Async -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.form.textFieldState +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog 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.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton -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.TextField -import io.element.android.libraries.designsystem.theme.components.TopAppBar -import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext -import io.element.android.libraries.testtags.TestTags -import io.element.android.libraries.testtags.testTag -import io.element.android.libraries.ui.strings.R as StringR -@OptIn(ExperimentalMaterial3Api::class, ExperimentalTextApi::class, ExperimentalLayoutApi::class) @Composable fun ChangeServerView( state: ChangeServerState, onLearnMoreClicked: () -> Unit, - onBackPressed: () -> Unit, + onDone: () -> Unit, modifier: Modifier = Modifier, - onChangeServerSuccess: () -> Unit = {}, ) { val eventSink = state.eventSink - val scrollState = rememberScrollState() - val isLoading by remember(state.changeServerAction) { - derivedStateOf { - state.changeServerAction is Async.Loading - } - } - val invalidHomeserverError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage - val slidingSyncNotSupportedError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert - val focusManager = LocalFocusManager.current - - fun submit() { - // Clear focus to prevent keyboard issues with textfields - focusManager.clearFocus(force = true) - - eventSink(ChangeServerEvents.Submit) - } - - Scaffold( - topBar = { - TopAppBar( - title = {}, - navigationIcon = { BackButton(onClick = onBackPressed) } - ) - } - ) { padding -> - Box( - modifier = modifier - .fillMaxSize() - .imePadding() - .padding(padding) - .consumeWindowInsets(padding) - ) { - Column( - modifier = Modifier - .verticalScroll( - state = scrollState, - ) - .padding(horizontal = 16.dp) - ) { - Spacer(Modifier.height(42.dp)) - Box( - modifier = Modifier - .size(width = 70.dp, height = 70.dp) - .align(Alignment.CenterHorizontally) - .background( - color = LocalColors.current.quinary, - shape = RoundedCornerShape(14.dp) - ) - ) { - Icon( - modifier = Modifier - .align(Alignment.Center) - .size(width = 32.dp, height = 32.dp), - tint = MaterialTheme.colorScheme.secondary, - resourceId = R.drawable.ic_homeserver, - contentDescription = "", + when (state.changeServerAction) { + is Async.Failure -> { + when (val error = state.changeServerAction.error) { + is ChangeServerError.Error -> { + ErrorDialog( + modifier = modifier, + content = error.message(), + onDismiss = { + eventSink.invoke(ChangeServerEvents.ClearError) + } ) } - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.screen_change_server_title), - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally), - textAlign = TextAlign.Center, - style = ElementTextStyles.Bold.title2, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(Modifier.height(8.dp)) - Text( - text = stringResource(id = R.string.screen_change_server_subtitle), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - textAlign = TextAlign.Center, - style = ElementTextStyles.Regular.subheadline, - color = MaterialTheme.colorScheme.secondary, - ) - Spacer(Modifier.height(24.dp)) - Text( - stringResource(R.string.screen_change_server_form_header), - style = ElementTextStyles.Regular.formHeader, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) - var homeserverFieldState by textFieldState(stateValue = state.homeserver) - TextField( - value = homeserverFieldState, - readOnly = isLoading, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.changeServerServer) - .onTabOrEnterKeyFocusNext(focusManager), - onValueChange = { - homeserverFieldState = it - eventSink(ChangeServerEvents.SetServer(it)) - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions( - onDone = { submit() } - ), - singleLine = true, - maxLines = 1, - trailingIcon = if (homeserverFieldState.isNotEmpty()) { - { - IconButton(onClick = { - eventSink(ChangeServerEvents.SetServer("")) - }, enabled = !isLoading) { - Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear)) - } - } - } else null, - isError = invalidHomeserverError != null, - supportingText = { - if (invalidHomeserverError != null) { - Text(invalidHomeserverError.message(), color = MaterialTheme.colorScheme.error) - } else { - val footerMessage = stringResource(R.string.screen_change_server_form_notice, "") - val footerAction = stringResource(StringR.string.action_learn_more) - val footerText = buildAnnotatedString { - val defaultColor = MaterialTheme.colorScheme.tertiary - withStyle(ParagraphStyle(textAlign = TextAlign.Start)) { - withStyle(SpanStyle(color = defaultColor)) { - append(footerMessage) - } - val start = length - withStyle(SpanStyle(color = LinkColor)) { - append(footerAction) - } - addUrlAnnotation(UrlAnnotation(LoginConstants.SLIDING_SYNC_READ_MORE_URL), start, length) - } - } - ClickableLinkText( - text = footerText, - interactionSource = MutableInteractionSource(), - style = ElementTextStyles.Regular.caption1, - ) - } - } - - ) - if (slidingSyncNotSupportedError != null) { - SlidingSyncNotSupportedDialog(onLearnMoreClicked = { - onLearnMoreClicked() - eventSink(ChangeServerEvents.ClearError) - }, onDismiss = { - eventSink(ChangeServerEvents.ClearError) + is ChangeServerError.SlidingSyncAlert -> { + SlidingSyncNotSupportedDialog( + modifier = modifier, + onLearnMoreClicked = { + onLearnMoreClicked() + eventSink.invoke(ChangeServerEvents.ClearError) + }, onDismiss = { + eventSink.invoke(ChangeServerEvents.ClearError) }) } - Spacer(Modifier.height(32.dp)) - ButtonWithProgress( - text = stringResource(id = R.string.screen_change_server_submit), - showProgress = isLoading, - onClick = ::submit, - enabled = state.submitEnabled, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.changeServerContinue) - ) - if (state.changeServerAction is Async.Success) { - onChangeServerSuccess() - } } } + is Async.Loading -> ProgressDialog() + is Async.Success -> LaunchedEffect(state.changeServerAction) { + onDone() + } + Async.Uninitialized -> Unit } } -@Composable -internal fun SlidingSyncNotSupportedDialog(onLearnMoreClicked: () -> Unit, onDismiss: () -> Unit) { - ConfirmationDialog( - onDismiss = onDismiss, - submitText = stringResource(StringR.string.action_learn_more), - onSubmitClicked = onLearnMoreClicked, - onCancelClicked = onDismiss, - emphasizeSubmitButton = true, - title = stringResource(StringR.string.dialog_title_error), - content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message), - ) -} - @Preview @Composable -internal fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = +fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = +fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = ElementPreviewDark { ContentToPreview(state) } @Composable private fun ContentToPreview(state: ChangeServerState) { - ChangeServerView(state = state, onBackPressed = {}, onLearnMoreClicked = {}) + ChangeServerView( + state = state, + onLearnMoreClicked = {}, + onDone = {}, + ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt new file mode 100644 index 0000000000..32eb1b0824 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt @@ -0,0 +1,42 @@ +/* + * 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.features.login.impl.dialogs + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.features.login.impl.R +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.R as StringR + +@Composable +internal fun SlidingSyncNotSupportedDialog( + onLearnMoreClicked: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + ConfirmationDialog( + modifier = modifier, + onDismiss = onDismiss, + submitText = stringResource(StringR.string.action_learn_more), + onSubmitClicked = onLearnMoreClicked, + onCancelClicked = onDismiss, + emphasizeSubmitButton = true, + title = stringResource(StringR.string.dialog_title_error), + content = stringResource(R.string.screen_change_server_error_no_sliding_sync_message), + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerError.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt similarity index 83% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerError.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt index 954f4f10dc..444ea3d3f2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerError.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver +package io.element.android.features.login.impl.error import androidx.annotation.StringRes import androidx.compose.runtime.Composable @@ -23,7 +23,7 @@ import io.element.android.features.login.impl.R import io.element.android.libraries.matrix.api.auth.AuthenticationException sealed class ChangeServerError : Throwable() { - data class InlineErrorMessage(@StringRes val messageId: Int) : ChangeServerError() { + data class Error(@StringRes val messageId: Int) : ChangeServerError() { @Composable fun message(): String = stringResource(messageId) } @@ -32,7 +32,7 @@ sealed class ChangeServerError : Throwable() { companion object { fun from(error: Throwable): ChangeServerError = when (error) { is AuthenticationException.SlidingSyncNotAvailable -> SlidingSyncAlert - else -> InlineErrorMessage(R.string.screen_change_server_error_invalid_homeserver) + else -> Error(R.string.screen_change_server_error_invalid_homeserver) } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ErrorFormatter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ErrorFormatter.kt index 68b0a70db4..ec2dc95386 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ErrorFormatter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ErrorFormatter.kt @@ -16,19 +16,21 @@ package io.element.android.features.login.impl.error +import androidx.annotation.StringRes import io.element.android.features.login.impl.R import io.element.android.libraries.matrix.api.auth.AuthErrorCode import io.element.android.libraries.matrix.api.auth.AuthenticationException import io.element.android.libraries.matrix.api.auth.errorCode -import io.element.android.libraries.ui.strings.R.string as StringR +import io.element.android.libraries.ui.strings.R as StringR +@StringRes fun loginError( throwable: Throwable ): Int { - val authException = throwable as? AuthenticationException ?: return StringR.error_unknown + val authException = throwable as? AuthenticationException ?: return StringR.string.error_unknown return when (authException.errorCode) { AuthErrorCode.FORBIDDEN -> R.string.screen_login_error_invalid_credentials AuthErrorCode.USER_DEACTIVATED -> R.string.screen_login_error_deactivated_account - AuthErrorCode.UNKNOWN -> StringR.error_unknown + AuthErrorCode.UNKNOWN -> StringR.string.error_unknown } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt new file mode 100644 index 0000000000..c1f0158605 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt @@ -0,0 +1,26 @@ +/* + * 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.features.login.impl.resolver + +data class HomeserverData constructor( + // The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url + val homeserverUrl: String, + // True if a wellknown file has been found and is valid. If false, it means that the [homeserverUrl] is valid + val isWellknownValid: Boolean, + // True if a wellknown file has been found and is valid and is claiming a sliding sync Url + val supportSlidingSync: Boolean, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt new file mode 100644 index 0000000000..7c692f5208 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt @@ -0,0 +1,104 @@ +/* + * 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.features.login.impl.resolver + +import io.element.android.features.login.impl.resolver.network.WellknownRequest +import io.element.android.libraries.core.bool.orFalse +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.core.uri.ensureProtocol +import io.element.android.libraries.core.uri.isValidUrl +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeout +import java.util.Collections +import javax.inject.Inject + +/** + * Resolve homeserver base on search terms. + */ +class HomeserverResolver @Inject constructor( + private val dispatchers: CoroutineDispatchers, + private val wellknownRequest: WellknownRequest, +) { + + suspend fun resolve(userInput: String): Flow> = flow { + val flowContext = currentCoroutineContext() + val trimmedUserInput = userInput.trim() + if (trimmedUserInput.length < 4) return@flow + val candidateBase = trimmedUserInput.ensureProtocol().removeSuffix("/") + val list = getUrlCandidates(candidateBase) + val currentList = Collections.synchronizedList(mutableListOf()) + // Run all the requests in parallel + withContext(dispatchers.io) { + list.map { url -> + async { + val wellKnown = tryOrNull { + withTimeout(5000) { + wellknownRequest.execute(url) + } + } + val isValid = wellKnown?.isValid().orFalse() + if (isValid) { + val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse() + // Emit the list as soon as possible + currentList.add( + HomeserverData( + homeserverUrl = url, + isWellknownValid = true, + supportSlidingSync = supportSlidingSync + ) + ) + withContext(flowContext) { + emit(currentList.toList()) + } + } + } + }.awaitAll() + } + // If list is empty, and the user has entered an URL, do not block the user. + if (currentList.isEmpty() && trimmedUserInput.isValidUrl()) { + emit( + listOf( + HomeserverData( + homeserverUrl = trimmedUserInput, + isWellknownValid = false, + supportSlidingSync = false, + ) + ) + ) + } + } + + private fun getUrlCandidates(data: String): List { + return buildList { + if (data.contains(".")) { + // TLD detected? + } else { + add("${data}.org") + add("${data}.com") + add("${data}.io") + } + // Always try what the user has entered + add(data) + } + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt new file mode 100644 index 0000000000..7de6d26f10 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt @@ -0,0 +1,36 @@ +/* + * 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.features.login.impl.resolver.network + +import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.network.RetrofitFactory +import javax.inject.Inject + +@ContributesBinding(AppScope::class) +class DefaultWellknownRequest @Inject constructor( + private val retrofitFactory: RetrofitFactory, +) : WellknownRequest { + /** + * Return the WellKnown data, if found. + * @param baseUrl for instance https://matrix.org + */ + override suspend fun execute(baseUrl: String): WellKnown { + val wellknownApi = retrofitFactory.create(baseUrl) + .create(WellknownAPI::class.java) + return wellknownApi.getWellKnown() + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt new file mode 100644 index 0000000000..63b6e7d189 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt @@ -0,0 +1,58 @@ +/* + * 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.features.login.impl.resolver.network + +import io.element.android.libraries.core.bool.orFalse +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "m.homeserver": {
+ *         "base_url": "https://matrix.org"
+ *     },
+ *     "m.identity_server": {
+ *         "base_url": "https://vector.im"
+ *     },
+ *     "org.matrix.msc3575.proxy": {
+ *         "url": "https://slidingsync.lab.matrix.org"
+ *     }
+ * }
+ * 
+ * . + */ +@Serializable +data class WellKnown( + @SerialName("m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + @SerialName("m.identity_server") + val identityServer: WellKnownBaseConfig? = null, + + @SerialName("org.matrix.msc3575.proxy") + val slidingSyncProxy: WellKnownSlidingSyncConfig? = null, +) { + fun isValid(): Boolean { + return homeServer?.baseURL?.isNotBlank().orFalse() + } + + fun supportSlidingSync(): Boolean { + return slidingSyncProxy?.url?.isNotBlank().orFalse() + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt new file mode 100644 index 0000000000..87b86736fa --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt @@ -0,0 +1,35 @@ +/* + * 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.features.login.impl.resolver.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery + *
+ * {
+ *     "base_url": "https://element.io"
+ * }
+ * 
+ * . + */ +@Serializable +data class WellKnownBaseConfig( + @SerialName("base_url") + val baseURL: String? = null +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt new file mode 100644 index 0000000000..98c712d9ac --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt @@ -0,0 +1,26 @@ +/* + * 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.features.login.impl.resolver.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class WellKnownSlidingSyncConfig( + @SerialName("url") + val url: String? = null, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt similarity index 63% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt index 5aa5071876..04a0dfb803 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt @@ -14,12 +14,11 @@ * limitations under the License. */ -package io.element.android.features.login.impl.root +package io.element.android.features.login.impl.resolver.network -sealed interface LoginRootEvents { - object RetryFetchServerInfo : LoginRootEvents - data class SetLogin(val login: String) : LoginRootEvents - data class SetPassword(val password: String) : LoginRootEvents - object Submit : LoginRootEvents - object ClearError : LoginRootEvents +import retrofit2.http.GET + +internal interface WellknownAPI { + @GET(".well-known/matrix/client") + suspend fun getWellKnown(): WellKnown } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt new file mode 100644 index 0000000000..570b621b83 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt @@ -0,0 +1,24 @@ +/* + * 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.features.login.impl.resolver.network + +interface WellknownRequest { + /** + * Return the WellKnown data, or throw an error if not found. + * @param baseUrl for instance https://matrix.org + */ + suspend fun execute(baseUrl: String): WellKnown +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt deleted file mode 100644 index f55c2030e7..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt +++ /dev/null @@ -1,171 +0,0 @@ -/* - * 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.features.login.impl.root - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import io.element.android.features.login.api.oidc.OidcAction -import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow -import io.element.android.features.login.impl.util.LoginConstants -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.architecture.Presenter -import io.element.android.libraries.architecture.execute -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import javax.inject.Inject - -class LoginRootPresenter @Inject constructor( - private val authenticationService: MatrixAuthenticationService, - private val defaultOidcActionFlow: DefaultOidcActionFlow, -) : Presenter { - - @Composable - override fun present(): LoginRootState { - val localCoroutineScope = rememberCoroutineScope() - val currentHomeServerDetails = authenticationService.getHomeserverDetails().collectAsState().value - val homeserver = currentHomeServerDetails?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL - val getHomeServerDetailsAction: MutableState> = remember { - if (currentHomeServerDetails != null) { - mutableStateOf(Async.Success(currentHomeServerDetails)) - } else { - mutableStateOf(Async.Uninitialized) - } - } - - LaunchedEffect(Unit) { - if (currentHomeServerDetails == null) { - getHomeServerDetails(homeserver, getHomeServerDetailsAction) - } - } - - val loggedInState: MutableState = remember { - mutableStateOf(LoggedInState.NotLoggedIn) - } - val formState = rememberSaveable { - mutableStateOf(LoginFormState.Default) - } - - LaunchedEffect(Unit) { - launch { - defaultOidcActionFlow.collect { - onOidcAction(it, loggedInState) - } - } - } - - fun handleEvents(event: LoginRootEvents) { - when (event) { - LoginRootEvents.RetryFetchServerInfo -> localCoroutineScope.getHomeServerDetails(homeserver, getHomeServerDetailsAction) - is LoginRootEvents.SetLogin -> updateFormState(formState) { - copy(login = event.login) - } - is LoginRootEvents.SetPassword -> updateFormState(formState) { - copy(password = event.password) - } - LoginRootEvents.Submit -> { - val homeServerDetails = getHomeServerDetailsAction.value.dataOrNull() ?: return - when { - homeServerDetails.supportsOidcLogin -> localCoroutineScope.submitOidc(loggedInState) - homeServerDetails.supportsPasswordLogin -> localCoroutineScope.submit(formState.value, loggedInState) - } - } - LoginRootEvents.ClearError -> loggedInState.value = LoggedInState.NotLoggedIn - } - } - - return LoginRootState( - homeserverUrl = homeserver, - homeserverDetails = getHomeServerDetailsAction.value, - loggedInState = loggedInState.value, - formState = formState.value, - eventSink = ::handleEvents - ) - } - - private fun CoroutineScope.getHomeServerDetails( - homeserver: String, - state: MutableState>, - ) = launch { - suspend { - authenticationService.setHomeserver(homeserver) - .map { - authenticationService.getHomeserverDetails().value!! - } - .getOrThrow() - }.execute(state) - } - - private fun CoroutineScope.submitOidc(loggedInState: MutableState) = launch { - loggedInState.value = LoggedInState.LoggingIn - authenticationService.getOidcUrl() - .onSuccess { - loggedInState.value = LoggedInState.OidcStarted(it) - } - .onFailure { failure -> - loggedInState.value = LoggedInState.ErrorLoggingIn(failure) - } - } - - private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState) = launch { - loggedInState.value = LoggedInState.LoggingIn - authenticationService.login(formState.login.trim(), formState.password) - .onSuccess { sessionId -> - loggedInState.value = LoggedInState.LoggedIn(sessionId) - } - .onFailure { failure -> - loggedInState.value = LoggedInState.ErrorLoggingIn(failure) - } - } - - private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) { - formState.value = updateLambda(formState.value) - } - - private suspend fun onOidcAction(oidcAction: OidcAction?, loggedInState: MutableState) { - oidcAction ?: return - loggedInState.value = LoggedInState.LoggingIn - when (oidcAction) { - OidcAction.GoBack -> { - authenticationService.cancelOidcLogin() - .onSuccess { - loggedInState.value = LoggedInState.NotLoggedIn - } - .onFailure { failure -> - loggedInState.value = LoggedInState.ErrorLoggingIn(failure) - } - } - is OidcAction.Success -> { - authenticationService.loginWithOidc(oidcAction.url) - .onSuccess { sessionId -> - loggedInState.value = LoggedInState.LoggedIn(sessionId) - } - .onFailure { failure -> - loggedInState.value = LoggedInState.ErrorLoggingIn(failure) - } - } - } - defaultOidcActionFlow.reset() - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt deleted file mode 100644 index 5f6d7c1f3a..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt +++ /dev/null @@ -1,76 +0,0 @@ -/* - * 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.features.login.impl.root - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails -import io.element.android.libraries.matrix.api.core.SessionId - -open class LoginRootStateProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - aLoginRootState(), - aLoginRootState().copy( - homeserverDetails = Async.Success( - MatrixHomeServerDetails( - "some-custom-server.com", - supportsPasswordLogin = true, - supportsOidcLogin = false - ) - ) - ), - aLoginRootState().copy(formState = LoginFormState("user", "pass")), - aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggingIn), - aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.ErrorLoggingIn(Throwable())), - aLoginRootState().copy(formState = LoginFormState("user", "pass"), loggedInState = LoggedInState.LoggedIn(SessionId("@user:domain"))), - // Oidc - aLoginRootState().copy( - homeserverUrl = "server-with-oidc.org", - homeserverDetails = Async.Success( - MatrixHomeServerDetails( - "server-with-oidc.org", - supportsPasswordLogin = false, - supportsOidcLogin = true - ) - ) - ), - // No password, no oidc support - aLoginRootState().copy( - homeserverUrl = "wrong.org", - homeserverDetails = Async.Success( - MatrixHomeServerDetails( - "wrong.org", - supportsPasswordLogin = false, - supportsOidcLogin = false - ) - ) - ), - // Loading - aLoginRootState().copy(homeserverDetails = Async.Loading()), - //Error - aLoginRootState().copy(homeserverDetails = Async.Failure(Exception("An error occurred"))), - ) -} - -fun aLoginRootState() = LoginRootState( - homeserverUrl = "matrix.org", - homeserverDetails = Async.Success(MatrixHomeServerDetails("matrix.org", supportsPasswordLogin = true, supportsOidcLogin = false)), - loggedInState = LoggedInState.NotLoggedIn, - formState = LoginFormState.Default, - eventSink = {} -) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt similarity index 66% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt index 11952ce5a8..45bf4489b5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt @@ -14,49 +14,52 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver +package io.element.android.features.login.impl.screens.changeaccountprovider -import android.content.Context -import android.content.Intent -import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.features.login.impl.util.LoginConstants -import io.element.android.libraries.core.data.tryOrNull +import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class ChangeServerNode @AssistedInject constructor( +class ChangeAccountProviderNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: ChangeServerPresenter, + private val presenter: ChangeAccountProviderPresenter, ) : Node(buildContext, plugins = plugins) { - private fun onSuccess() { - navigateUp() + + interface Callback : Plugin { + fun onDone() + fun onOtherClicked() } - private fun openLearnMorePage(context: Context) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL)) - tryOrNull { context.startActivity(intent) } + private fun onDone() { + plugins().forEach { it.onDone() } + } + + private fun onOtherClicked() { + plugins().forEach { it.onOtherClicked() } } @Composable override fun View(modifier: Modifier) { val state = presenter.present() val context = LocalContext.current - ChangeServerView( + ChangeAccountProviderView( state = state, modifier = modifier, - onChangeServerSuccess = this::onSuccess, - onBackPressed = { navigateUp() }, + onBackPressed = ::navigateUp, onLearnMoreClicked = { openLearnMorePage(context) }, + onDone = ::onDone, + onOtherProviderClicked = ::onOtherClicked, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt new file mode 100644 index 0000000000..dfdb7dcf99 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -0,0 +1,47 @@ +/* + * 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.features.login.impl.screens.changeaccountprovider + +import androidx.compose.runtime.Composable +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.libraries.architecture.Presenter +import javax.inject.Inject + +class ChangeAccountProviderPresenter @Inject constructor( + private val changeServerPresenter: ChangeServerPresenter, +) : Presenter { + + @Composable + override fun present(): ChangeAccountProviderState { + val changeServerState = changeServerPresenter.present() + return ChangeAccountProviderState( + // Just matrix.org by default for now + accountProviders = listOf( + AccountProvider( + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = true, + supportSlidingSync = true, + ) + ), + changeServerState = changeServerState, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt new file mode 100644 index 0000000000..806ce5bc64 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt @@ -0,0 +1,26 @@ +/* + * 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.features.login.impl.screens.changeaccountprovider + +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.changeserver.ChangeServerState + +// Do not use default value, so no member get forgotten in the presenters. +data class ChangeAccountProviderState constructor( + val accountProviders: List, + val changeServerState: ChangeServerState, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt new file mode 100644 index 0000000000..403746f227 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt @@ -0,0 +1,36 @@ +/* + * 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.features.login.impl.screens.changeaccountprovider + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.changeserver.aChangeServerState + +open class ChangeAccountProviderStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aChangeAccountProviderState(), + // Add other state here + ) +} + +fun aChangeAccountProviderState() = ChangeAccountProviderState( + accountProviders = listOf( + anAccountProvider() + ), + changeServerState = aChangeServerState(), +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt new file mode 100644 index 0000000000..0f444350c9 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt @@ -0,0 +1,147 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) + +package io.element.android.features.login.impl.screens.changeaccountprovider + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProviderView +import io.element.android.features.login.impl.changeserver.ChangeServerEvents +import io.element.android.features.login.impl.changeserver.ChangeServerView +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.TopAppBar + +/** + * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 + */ +@Composable +fun ChangeAccountProviderView( + state: ChangeAccountProviderState, + onBackPressed: () -> Unit, + onLearnMoreClicked: () -> Unit, + onDone: () -> Unit, + onOtherProviderClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = { BackButton(onClick = onBackPressed) } + ) + } + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(padding) + .consumeWindowInsets(padding) + ) { + Column( + modifier = Modifier + .verticalScroll(state = rememberScrollState()) + ) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp), + iconImageVector = Icons.Filled.Home, + iconTint = MaterialTheme.colorScheme.primary, + title = stringResource(id = R.string.screen_change_account_provider_title), + subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle), + ) + + state.accountProviders.forEach { item -> + val alteredItem = if (item.isMatrixOrg) { + // Set the subtitle from the resource + item.copy( + subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle), + ) + } else { + item + } + AccountProviderView( + item = alteredItem, + onClick = { + state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(alteredItem)) + } + ) + } + // Other + AccountProviderView( + item = AccountProvider( + title = stringResource(id = R.string.screen_change_account_provider_other), + ), + onClick = onOtherProviderClicked + ) + Spacer(Modifier.height(32.dp)) + } + ChangeServerView( + state = state.changeServerState, + onLearnMoreClicked = onLearnMoreClicked, + onDone = onDone, + ) + } + } +} + +@Preview +@Composable +fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderStateProvider::class) state: ChangeAccountProviderState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: ChangeAccountProviderState) { + ChangeAccountProviderView( + state = state, + onBackPressed = { }, + onLearnMoreClicked = { }, + onDone = { }, + onOtherProviderClicked = { }, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt new file mode 100644 index 0000000000..1ba3cc3028 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt @@ -0,0 +1,22 @@ +/* + * 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.features.login.impl.screens.confirmaccountprovider + +sealed interface ConfirmAccountProviderEvents { + object Continue : ConfirmAccountProviderEvents + object ClearError : ConfirmAccountProviderEvents +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt new file mode 100644 index 0000000000..7cef986013 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt @@ -0,0 +1,84 @@ +/* + * 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.features.login.impl.screens.confirmaccountprovider + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.login.impl.util.openLearnMorePage +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails + +@ContributesNode(AppScope::class) +class ConfirmAccountProviderNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: ConfirmAccountProviderPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val isAccountCreation: Boolean, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create( + ConfirmAccountProviderPresenter.Params( + isAccountCreation = inputs.isAccountCreation, + ) + ) + + interface Callback : Plugin { + fun onLoginPasswordNeeded() + fun onOidcDetails(oidcDetails: OidcDetails) + fun onChangeAccountProvider() + } + + private fun onOidcDetails(data: OidcDetails) { + plugins().forEach { it.onOidcDetails(data) } + } + + private fun onLoginPasswordNeeded() { + plugins().forEach { it.onLoginPasswordNeeded() } + } + + private fun onChangeAccountProvider() { + plugins().forEach { it.onChangeAccountProvider() } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + val context = LocalContext.current + ConfirmAccountProviderView( + state = state, + modifier = modifier, + onOidcDetails = ::onOidcDetails, + onLoginPasswordNeeded = ::onLoginPasswordNeeded, + onChange = ::onChangeAccountProvider, + onLearnMoreClicked = { openLearnMorePage(context) }, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt new file mode 100644 index 0000000000..2626e56365 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -0,0 +1,100 @@ +/* + * 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.features.login.impl.screens.confirmaccountprovider + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.execute +import io.element.android.libraries.core.data.tryOrNull +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import java.net.URL + +class ConfirmAccountProviderPresenter @AssistedInject constructor( + @Assisted private val params: Params, + private val accountProviderDataSource: AccountProviderDataSource, + private val authenticationService: MatrixAuthenticationService +) : Presenter { + + data class Params( + val isAccountCreation: Boolean, + ) + + @AssistedFactory + interface Factory { + fun create(params: Params): ConfirmAccountProviderPresenter + } + + @Composable + override fun present(): ConfirmAccountProviderState { + val accountProvider by accountProviderDataSource.flow().collectAsState() + val localCoroutineScope = rememberCoroutineScope() + + val loginFlowAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + + fun handleEvents(event: ConfirmAccountProviderEvents) { + when (event) { + ConfirmAccountProviderEvents.Continue -> { + localCoroutineScope.submit(accountProvider.title, loginFlowAction) + } + ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized + } + } + + return ConfirmAccountProviderState( + accountProvider = accountProvider, + isAccountCreation = params.isAccountCreation, + loginFlow = loginFlowAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.submit( + homeserverUrl: String, + loginFlowAction: MutableState>, + ) = launch { + suspend { + val domain = tryOrNull { URL(homeserverUrl) }?.host ?: homeserverUrl + authenticationService.setHomeserver(domain).map { + val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!! + if (matrixHomeServerDetails.supportsOidcLogin) { + // Retrieve the details right now + LoginFlow.OidcFlow(authenticationService.getOidcUrl().getOrThrow()) + } else if (matrixHomeServerDetails.supportsPasswordLogin) { + LoginFlow.PasswordLogin + } else { + throw IllegalStateException("Unsupported login flow") + } + }.getOrThrow() + }.execute(loginFlowAction, errorMapping = ChangeServerError::from) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt new file mode 100644 index 0000000000..a870b88c58 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -0,0 +1,36 @@ +/* + * 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.features.login.impl.screens.confirmaccountprovider + +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.auth.OidcDetails + +// Do not use default value, so no member get forgotten in the presenters. +data class ConfirmAccountProviderState( + val accountProvider: AccountProvider, + val isAccountCreation: Boolean, + val loginFlow: Async, + val eventSink: (ConfirmAccountProviderEvents) -> Unit +) { + val submitEnabled: Boolean get() = accountProvider.title.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading) +} + +sealed interface LoginFlow { + object PasswordLogin : LoginFlow + data class OidcFlow(val oidcDetails: OidcDetails) : LoginFlow +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt new file mode 100644 index 0000000000..d5f98f5716 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt @@ -0,0 +1,36 @@ +/* + * 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.features.login.impl.screens.confirmaccountprovider + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.libraries.architecture.Async + +open class ConfirmAccountProviderStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aConfirmAccountProviderState(), + // Add other state here + ) +} + +fun aConfirmAccountProviderState() = ConfirmAccountProviderState( + accountProvider = anAccountProvider(), + isAccountCreation = false, + loginFlow = Async.Uninitialized, + eventSink = {} +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt new file mode 100644 index 0000000000..c9053d67c9 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -0,0 +1,164 @@ +/* + * 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.features.login.impl.screens.confirmaccountprovider + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountCircle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.dialogs.SlidingSyncNotSupportedDialog +import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.atomic.pages.HeaderFooterPage +import io.element.android.libraries.designsystem.components.button.ButtonWithProgress +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.matrix.api.auth.OidcDetails +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag + +@Composable +fun ConfirmAccountProviderView( + state: ConfirmAccountProviderState, + onOidcDetails: (OidcDetails) -> Unit, + onLoginPasswordNeeded: () -> Unit, + onLearnMoreClicked: () -> Unit, + onChange: () -> Unit, + modifier: Modifier = Modifier, +) { + val isLoading by remember(state.loginFlow) { + derivedStateOf { + state.loginFlow is Async.Loading + } + } + val eventSink = state.eventSink + + HeaderFooterPage( + modifier = modifier, + header = { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 60.dp), + iconImageVector = Icons.Filled.AccountCircle, + title = stringResource( + id = if (state.isAccountCreation) { + R.string.screen_account_provider_signup_title + } else { + R.string.screen_account_provider_signin_title + }, + state.accountProvider.title + ), + subTitle = stringResource( + id = if (state.isAccountCreation) { + R.string.screen_account_provider_signup_subtitle + } else { + // Use same value for now. + R.string.screen_account_provider_signup_subtitle + }, + ) + ) + }, + footer = { + ButtonColumnMolecule { + ButtonWithProgress( + text = stringResource(id = R.string.screen_account_provider_continue), + showProgress = isLoading, + onClick = { eventSink.invoke(ConfirmAccountProviderEvents.Continue) }, + enabled = state.submitEnabled, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginContinue) + ) + TextButton( + onClick = onChange, + enabled = true, + modifier = Modifier + .fillMaxWidth() + .testTag(TestTags.loginChangeServer) + ) { + Text(text = stringResource(id = R.string.screen_account_provider_change)) + } + } + } + ) { + when (state.loginFlow) { + is Async.Failure -> { + when (val error = state.loginFlow.error) { + is ChangeServerError.Error -> { + ErrorDialog( + content = error.message(), + onDismiss = { + eventSink.invoke(ConfirmAccountProviderEvents.ClearError) + } + ) + } + is ChangeServerError.SlidingSyncAlert -> { + SlidingSyncNotSupportedDialog(onLearnMoreClicked = { + onLearnMoreClicked() + eventSink(ConfirmAccountProviderEvents.ClearError) + }, onDismiss = { + eventSink(ConfirmAccountProviderEvents.ClearError) + }) + } + } + } + is Async.Loading -> Unit // The Continue button shows the loading state + is Async.Success -> { + when (val loginFlowState = state.loginFlow.state) { + is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails) + LoginFlow.PasswordLogin -> onLoginPasswordNeeded() + } + } + Async.Uninitialized -> Unit + } + } +} + +@Preview +@Composable +fun ConfirmAccountProviderViewLightPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun ConfirmAccountProviderViewDarkPreview(@PreviewParameter(ConfirmAccountProviderStateProvider::class) state: ConfirmAccountProviderState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: ConfirmAccountProviderState) { + ConfirmAccountProviderView( + state = state, + onOidcDetails = {}, + onLoginPasswordNeeded = {}, + onLearnMoreClicked = {}, + onChange = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt new file mode 100644 index 0000000000..e6f23ca418 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt @@ -0,0 +1,24 @@ +/* + * 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.features.login.impl.screens.loginpassword + +sealed interface LoginPasswordEvents { + data class SetLogin(val login: String) : LoginPasswordEvents + data class SetPassword(val password: String) : LoginPasswordEvents + object Submit : LoginPasswordEvents + object ClearError : LoginPasswordEvents +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt new file mode 100644 index 0000000000..630b08570c --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt @@ -0,0 +1,45 @@ +/* + * 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.features.login.impl.screens.loginpassword + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class LoginPasswordNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: LoginPasswordPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + LoginPasswordView( + state = state, + modifier = modifier, + onBackPressed = ::navigateUp + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt new file mode 100644 index 0000000000..1fc4a10bbb --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt @@ -0,0 +1,90 @@ +/* + * 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.features.login.impl.screens.loginpassword + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +class LoginPasswordPresenter @Inject constructor( + private val authenticationService: MatrixAuthenticationService, + private val accountProviderDataSource: AccountProviderDataSource, +) : Presenter { + + @Composable + override fun present(): LoginPasswordState { + val localCoroutineScope = rememberCoroutineScope() + val loginAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + + val formState = rememberSaveable { + mutableStateOf(LoginFormState.Default) + } + val accountProvider by accountProviderDataSource.flow().collectAsState() + + fun handleEvents(event: LoginPasswordEvents) { + when (event) { + is LoginPasswordEvents.SetLogin -> updateFormState(formState) { + copy(login = event.login) + } + is LoginPasswordEvents.SetPassword -> updateFormState(formState) { + copy(password = event.password) + } + LoginPasswordEvents.Submit -> { + localCoroutineScope.submit(formState.value, loginAction) + } + LoginPasswordEvents.ClearError -> loginAction.value = Async.Uninitialized + } + } + + return LoginPasswordState( + accountProvider = accountProvider, + formState = formState.value, + loginAction = loginAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.submit(formState: LoginFormState, loggedInState: MutableState>) = launch { + loggedInState.value = Async.Loading() + authenticationService.login(formState.login.trim(), formState.password) + .onSuccess { sessionId -> + loggedInState.value = Async.Success(sessionId) + } + .onFailure { failure -> + loggedInState.value = Async.Failure(failure) + } + } + + private fun updateFormState(formState: MutableState, updateLambda: LoginFormState.() -> LoginFormState) { + formState.value = updateLambda(formState.value) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt similarity index 50% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt index 45eafa744c..c8fa2f4ad3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt @@ -14,36 +14,23 @@ * limitations under the License. */ -package io.element.android.features.login.impl.root +package io.element.android.features.login.impl.screens.loginpassword import android.os.Parcelable +import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.libraries.architecture.Async -import io.element.android.libraries.core.bool.orFalse -import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails -import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.parcelize.Parcelize -data class LoginRootState( - val homeserverUrl: String, - val homeserverDetails: Async, - val loggedInState: LoggedInState, +data class LoginPasswordState( + val accountProvider: AccountProvider, val formState: LoginFormState, - val eventSink: (LoginRootEvents) -> Unit + val loginAction: Async, + val eventSink: (LoginPasswordEvents) -> Unit ) { - val supportPasswordLogin = (homeserverDetails as? Async.Success)?.state?.supportsPasswordLogin.orFalse() - val supportOidcLogin = (homeserverDetails as? Async.Success)?.state?.supportsOidcLogin.orFalse() val submitEnabled: Boolean - get() = loggedInState !is LoggedInState.ErrorLoggingIn && - ((formState.login.isNotEmpty() && formState.password.isNotEmpty()) || supportOidcLogin) -} - -sealed interface LoggedInState { - object NotLoggedIn : LoggedInState - object LoggingIn : LoggedInState - data class OidcStarted(val oidcDetail: OidcDetails) : LoggedInState - data class ErrorLoggingIn(val failure: Throwable) : LoggedInState - data class LoggedIn(val sessionId: SessionId) : LoggedInState + get() = loginAction !is Async.Failure && + ((formState.login.isNotEmpty() && formState.password.isNotEmpty())) } @Parcelize diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt new file mode 100644 index 0000000000..b4f5a84691 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt @@ -0,0 +1,39 @@ +/* + * 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.features.login.impl.screens.loginpassword + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.libraries.architecture.Async + +open class LoginPasswordStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aLoginPasswordState(), + // Loading + aLoginPasswordState().copy(loginAction = Async.Loading()), + // Error + aLoginPasswordState().copy(loginAction = Async.Failure(Exception("An error occurred"))), + ) +} + +fun aLoginPasswordState() = LoginPasswordState( + accountProvider = anAccountProvider(), + formState = LoginFormState.Default, + loginAction = Async.Uninitialized, + eventSink = {} +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt similarity index 60% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index 2444418725..9154a0792f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * 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. @@ -14,15 +14,11 @@ * limitations under the License. */ -package io.element.android.features.login.impl.root +package io.element.android.features.login.impl.screens.loginpassword -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize @@ -30,31 +26,25 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ChevronRight +import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.autofill.AutofillType -import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource @@ -62,7 +52,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp @@ -70,8 +59,7 @@ import io.element.android.features.login.impl.R import io.element.android.features.login.impl.error.loginError import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.ElementTextStyles -import io.element.android.libraries.designsystem.components.async.AsyncFailure -import io.element.android.libraries.designsystem.components.async.AsyncLoading +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule 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 @@ -86,23 +74,20 @@ import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.designsystem.theme.components.autofill import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext -import io.element.android.libraries.matrix.api.auth.OidcDetails import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) @Composable -fun LoginRootView( - state: LoginRootState, +fun LoginPasswordView( + state: LoginPasswordState, modifier: Modifier = Modifier, - onChangeServer: () -> Unit = {}, - onOidcDetails: (OidcDetails) -> Unit = {}, onBackPressed: () -> Unit, ) { - val isLoading by remember(state.loggedInState) { + val isLoading by remember(state.loginAction) { derivedStateOf { - state.loggedInState == LoggedInState.LoggingIn + state.loginAction is Async.Loading } } val focusManager = LocalFocusManager.current @@ -111,10 +96,11 @@ fun LoginRootView( // Clear focus to prevent keyboard issues with textfields focusManager.clearFocus(force = true) - state.eventSink(LoginRootEvents.Submit) + state.eventSink(LoginPasswordEvents.Submit) } Scaffold( + modifier = modifier, topBar = { TopAppBar( title = {}, @@ -123,7 +109,7 @@ fun LoginRootView( } ) { padding -> Box( - modifier = modifier + modifier = Modifier .fillMaxSize() .imePadding() .padding(padding) @@ -136,142 +122,48 @@ fun LoginRootView( .verticalScroll(state = scrollState) .padding(horizontal = 16.dp), ) { - Spacer(Modifier.height(16.dp)) // Title - Text( - text = stringResource(id = R.string.screen_login_title), + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 20.dp), + iconImageVector = Icons.Filled.AccountCircle, + title = stringResource( + id = R.string.screen_account_provider_signin_title, + state.accountProvider.title + ), + subTitle = stringResource(id = R.string.screen_login_form_header) + ) + Spacer(Modifier.height(32.dp)) + LoginForm(state = state, + isLoading = isLoading, + onSubmit = ::submit + ) + Spacer(Modifier.height(28.dp)) + // Submit + ButtonWithProgress( + text = stringResource(R.string.screen_login_submit), + showProgress = isLoading, + onClick = ::submit, + enabled = state.submitEnabled, modifier = Modifier - .fillMaxWidth(), - style = ElementTextStyles.Bold.title1, - color = MaterialTheme.colorScheme.primary, + .fillMaxWidth() + .testTag(TestTags.loginContinue) ) - Spacer(Modifier.height(32.dp)) - - ChangeServerSection( - interactionEnabled = !isLoading, - homeserver = state.homeserverUrl, - onChangeServer = onChangeServer - ) - - Spacer(Modifier.height(32.dp)) - - when (state.homeserverDetails) { - Async.Uninitialized, - is Async.Loading -> AsyncLoading() - is Async.Failure -> AsyncFailure( - throwable = state.homeserverDetails.error, - onRetry = { - state.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo) - } - ) - is Async.Success -> ServerDetailForm(state, isLoading, ::submit) - } + Spacer(modifier = Modifier.height(32.dp)) } - when (val loggedInState = state.loggedInState) { - is LoggedInState.OidcStarted -> onOidcDetails(loggedInState.oidcDetail) - else -> Unit + + if (state.loginAction is Async.Failure) { + LoginErrorDialog(error = state.loginAction.error, onDismiss = { + state.eventSink(LoginPasswordEvents.ClearError) + }) } } } - - if (state.loggedInState is LoggedInState.ErrorLoggingIn) { - LoginErrorDialog(error = state.loggedInState.failure, onDismiss = { - state.eventSink(LoginRootEvents.ClearError) - }) - } -} - -@Composable -fun ServerDetailForm( - state: LoginRootState, - isLoading: Boolean, - submit: () -> Unit, - modifier: Modifier = Modifier, -) { - when { - state.supportOidcLogin -> { - // Oidc, in this case, just display a Spacer and the submit button - Spacer(modifier.height(28.dp)) - } - state.supportPasswordLogin -> { - LoginForm(state = state, isLoading = isLoading, onSubmit = submit, modifier = modifier) - } - else -> { - Text(modifier = modifier, text = "No supported login flow") - } - } - - Spacer(Modifier.height(28.dp)) - - if (state.supportOidcLogin || state.supportPasswordLogin) { - // Submit - ButtonWithProgress( - text = stringResource(R.string.screen_login_submit), - showProgress = isLoading, - onClick = submit, - enabled = state.submitEnabled, - modifier = Modifier - .fillMaxWidth() - .testTag(TestTags.loginContinue) - ) - Spacer(modifier = Modifier.height(32.dp)) - } -} - -@Composable -internal fun ChangeServerSection( - interactionEnabled: Boolean, - homeserver: String, - onChangeServer: () -> Unit, - modifier: Modifier = Modifier -) { - Column(modifier) { - Text( - modifier = Modifier.padding(start = 16.dp, bottom = 8.dp), - text = stringResource(id = R.string.screen_login_server_header), - style = ElementTextStyles.Regular.formHeader, - ) - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(14.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant) - .testTag(TestTags.loginChangeServer) - .clickable { - if (interactionEnabled) { - onChangeServer() - } - }, - horizontalArrangement = Arrangement.End, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = homeserver, - style = ElementTextStyles.Bold.body, - textAlign = TextAlign.Start, - modifier = Modifier - .weight(1f) - .padding(horizontal = 16.dp, vertical = 16.dp) - ) - IconButton( - modifier = Modifier.size(24.dp), - onClick = { - if (interactionEnabled) { - onChangeServer() - } - } - ) { - Icon(imageVector = Icons.Default.ChevronRight, contentDescription = null, tint = MaterialTheme.colorScheme.tertiary) - } - Spacer(Modifier.width(8.dp)) - } - } } @OptIn(ExperimentalComposeUiApi::class) @Composable internal fun LoginForm( - state: LoginRootState, + state: LoginPasswordState, isLoading: Boolean, onSubmit: () -> Unit, modifier: Modifier = Modifier @@ -299,14 +191,14 @@ internal fun LoginForm( .testTag(TestTags.loginEmailUsername) .autofill(autofillTypes = listOf(AutofillType.Username), onFill = { loginFieldState = it - eventSink(LoginRootEvents.SetLogin(it)) + eventSink(LoginPasswordEvents.SetLogin(it)) }), label = { Text(text = stringResource(R.string.screen_login_username_hint)) }, onValueChange = { loginFieldState = it - eventSink(LoginRootEvents.SetLogin(it)) + eventSink(LoginPasswordEvents.SetLogin(it)) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Email, @@ -316,7 +208,6 @@ internal fun LoginForm( focusManager.moveFocus(FocusDirection.Down) }), singleLine = true, - maxLines = 1, trailingIcon = if (loginFieldState.isNotEmpty()) { { IconButton(onClick = { @@ -329,7 +220,7 @@ internal fun LoginForm( ) var passwordVisible by remember { mutableStateOf(false) } - if (state.loggedInState is LoggedInState.LoggingIn) { + if (state.loginAction is Async.Loading) { // Ensure password is hidden when user submits the form passwordVisible = false } @@ -343,11 +234,11 @@ internal fun LoginForm( .testTag(TestTags.loginPassword) .autofill(autofillTypes = listOf(AutofillType.Password), onFill = { passwordFieldState = it - eventSink(LoginRootEvents.SetPassword(it)) + eventSink(LoginPasswordEvents.SetPassword(it)) }), onValueChange = { passwordFieldState = it - eventSink(LoginRootEvents.SetPassword(it)) + eventSink(LoginPasswordEvents.SetPassword(it)) }, label = { Text(text = stringResource(R.string.screen_login_password_hint)) @@ -371,7 +262,6 @@ internal fun LoginForm( onDone = { onSubmit() } ), singleLine = true, - maxLines = 1, ) } } @@ -386,17 +276,17 @@ internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { @Preview @Composable -internal fun LoginRootScreenLightPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) = +internal fun LoginPasswordViewLightPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun LoginRootScreenDarkPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) = +internal fun LoginPasswordViewDarkPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = ElementPreviewDark { ContentToPreview(state) } @Composable -private fun ContentToPreview(state: LoginRootState) { - LoginRootView( +private fun ContentToPreview(state: LoginPasswordState) { + LoginPasswordView( state = state, onBackPressed = {} ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt new file mode 100644 index 0000000000..53ee45f644 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt @@ -0,0 +1,25 @@ +/* + * 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.features.login.impl.screens.searchaccountprovider + +sealed interface SearchAccountProviderEvents { + /** + * The user has typed something, expect to get a list of matching account provider results + * in the state. + */ + data class UserInput(val input: String) : SearchAccountProviderEvents +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt similarity index 67% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt index 787f5d0b48..7178a105f6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt @@ -14,10 +14,11 @@ * limitations under the License. */ -package io.element.android.features.login.impl.root +package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin @@ -25,38 +26,34 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.libraries.di.AppScope -import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) -class LoginRootNode @AssistedInject constructor( +class SearchAccountProviderNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: LoginRootPresenter, + private val presenter: SearchAccountProviderPresenter, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun onChangeHomeServer() - fun onOidcDetails(oidcDetails: OidcDetails) + fun onDone() } - private fun onChangeHomeServer() { - plugins().forEach { it.onChangeHomeServer() } - } - - private fun onOidcDetails(oidcDetails: OidcDetails) { - plugins().forEach { it.onOidcDetails(oidcDetails) } + private fun onDone() { + plugins().forEach { it.onDone() } } @Composable override fun View(modifier: Modifier) { val state = presenter.present() - LoginRootView( + val context = LocalContext.current + SearchAccountProviderView( state = state, modifier = modifier, - onChangeServer = ::onChangeHomeServer, - onOidcDetails = ::onOidcDetails, - onBackPressed = ::navigateUp + onBackPressed = ::navigateUp, + onLearnMoreClicked = { openLearnMorePage(context) }, + onDone = ::onDone, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt new file mode 100644 index 0000000000..1d8271e394 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt @@ -0,0 +1,85 @@ +/* + * 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.features.login.impl.screens.searchaccountprovider + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.resolver.HomeserverData +import io.element.android.features.login.impl.resolver.HomeserverResolver +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import javax.inject.Inject + +class SearchAccountProviderPresenter @Inject constructor( + private val homeserverResolver: HomeserverResolver, + private val changeServerPresenter: ChangeServerPresenter, +) : Presenter { + + @Composable + override fun present(): SearchAccountProviderState { + var userInput by rememberSaveable { + mutableStateOf("") + } + val changeServerState = changeServerPresenter.present() + + val data: MutableState>> = remember { + mutableStateOf(Async.Uninitialized) + } + + LaunchedEffect(userInput) { + onUserInput(userInput, data) + } + + fun handleEvents(event: SearchAccountProviderEvents) { + when (event) { + is SearchAccountProviderEvents.UserInput -> { + userInput = event.input + } + } + } + + return SearchAccountProviderState( + userInput = userInput, + userInputResult = data.value, + changeServerState = changeServerState, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.onUserInput(userInput: String, data: MutableState>>) = launch { + data.value = Async.Uninitialized + // Debounce + delay(300) + data.value = Async.Loading() + homeserverResolver.resolve(userInput).collect { + data.value = Async.Success(it) + } + if (data.value !is Async.Success) { + data.value = Async.Uninitialized + } + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt new file mode 100644 index 0000000000..15859afde1 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt @@ -0,0 +1,29 @@ +/* + * 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.features.login.impl.screens.searchaccountprovider + +import io.element.android.features.login.impl.changeserver.ChangeServerState +import io.element.android.features.login.impl.resolver.HomeserverData +import io.element.android.libraries.architecture.Async + +// Do not use default value, so no member get forgotten in the presenters. +data class SearchAccountProviderState( + val userInput: String, + val userInputResult: Async>, + val changeServerState: ChangeServerState, + val eventSink: (SearchAccountProviderEvents) -> Unit +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt new file mode 100644 index 0000000000..b6ffac8bd1 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt @@ -0,0 +1,61 @@ +/* + * 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.features.login.impl.screens.searchaccountprovider + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.changeserver.aChangeServerState +import io.element.android.features.login.impl.resolver.HomeserverData +import io.element.android.libraries.architecture.Async + +open class SearchAccountProviderStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aSearchAccountProviderState(), + aSearchAccountProviderState(userInputResult = Async.Success(aHomeserverDataList())), + // Add other state here + ) +} + +fun aSearchAccountProviderState( + userInput: String = "", + userInputResult: Async> = Async.Uninitialized, +) = SearchAccountProviderState( + userInput = userInput, + userInputResult = userInputResult, + changeServerState = aChangeServerState(), + eventSink = {} +) + +fun aHomeserverDataList(): List { + return listOf( + aHomeserverData(isWellknownValid = true, supportSlidingSync = true), + aHomeserverData(homeserverUrl = "https://no.sliding.sync", isWellknownValid = true, supportSlidingSync = false), + aHomeserverData(homeserverUrl = "https://invalid", isWellknownValid = false, supportSlidingSync = false), + ) +} + +fun aHomeserverData( + homeserverUrl: String = "https://matrix.org", + isWellknownValid: Boolean = true, + supportSlidingSync: Boolean = true, +): HomeserverData { + return HomeserverData( + homeserverUrl = homeserverUrl, + isWellknownValid = isWellknownValid, + supportSlidingSync = supportSlidingSync, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt new file mode 100644 index 0000000000..5c280cba0e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt @@ -0,0 +1,230 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) + +package io.element.android.features.login.impl.screens.searchaccountprovider + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProviderView +import io.element.android.features.login.impl.changeserver.ChangeServerEvents +import io.element.android.features.login.impl.changeserver.ChangeServerView +import io.element.android.features.login.impl.resolver.HomeserverData +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule +import io.element.android.libraries.designsystem.components.button.BackButton +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.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.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.onTabOrEnterKeyFocusNext +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag +import io.element.android.libraries.ui.strings.R as StringR + +/** + * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435 + */ +@Composable +fun SearchAccountProviderView( + state: SearchAccountProviderState, + onBackPressed: () -> Unit, + onLearnMoreClicked: () -> Unit, + onDone: () -> Unit, + modifier: Modifier = Modifier, +) { + val eventSink = state.eventSink + Scaffold( + modifier = modifier, + topBar = { + TopAppBar( + title = {}, + navigationIcon = { BackButton(onClick = onBackPressed) } + ) + } + ) { padding -> + Box( + modifier = Modifier + .fillMaxSize() + .imePadding() + .padding(padding) + .consumeWindowInsets(padding) + ) { + LazyColumn(modifier = Modifier.fillMaxWidth(), state = rememberLazyListState()) { + item { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp), + iconImageVector = Icons.Filled.Search, + title = stringResource(id = R.string.screen_account_provider_form_title), + subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle), + ) + } + item { + // TextInput + var userInputState by textFieldState(stateValue = state.userInput) + val focusManager = LocalFocusManager.current + OutlinedTextField( + value = userInputState, + // readOnly = isLoading, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + .onTabOrEnterKeyFocusNext(focusManager) + .testTag(TestTags.changeServerServer), + onValueChange = { + userInputState = it + eventSink(SearchAccountProviderEvents.UserInput(it)) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions(onDone = { + focusManager.moveFocus(FocusDirection.Down) + }), + singleLine = true, + trailingIcon = if (userInputState.isNotEmpty()) { + { + IconButton(onClick = { + userInputState = "" + eventSink(SearchAccountProviderEvents.UserInput("")) + }) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = stringResource(StringR.string.action_clear) + ) + } + } + } else null, + supportingText = { + Text(text = stringResource(id = R.string.screen_account_provider_form_notice), color = MaterialTheme.colorScheme.secondary) + } + ) + } + + when (state.userInputResult) { + is Async.Failure -> { + // Ignore errors (let the user type more chars) + } + is Async.Loading -> { + item { + Box( + modifier = Modifier + .fillMaxSize() + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } + } + } + is Async.Success -> { + items(state.userInputResult.state) { homeserverData -> + val item = homeserverData.toAccountProvider() + AccountProviderView( + item = item, + onClick = { + state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(item)) + } + ) + } + } + Async.Uninitialized -> Unit + } + item { + Spacer(Modifier.height(32.dp)) + } + } + ChangeServerView( + state = state.changeServerState, + onLearnMoreClicked = onLearnMoreClicked, + onDone = onDone, + ) + } + } +} + +@Composable +private fun HomeserverData.toAccountProvider(): AccountProvider { + val isMatrixOrg = homeserverUrl == "https://matrix.org" + return AccountProvider( + title = homeserverUrl.removePrefix("http://").removePrefix("https://"), + subtitle = if (isMatrixOrg) stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle) else null, + isPublic = isMatrixOrg, // There is no need to know for other servers right now + isMatrixOrg = isMatrixOrg, + isValid = isWellknownValid, + supportSlidingSync = supportSlidingSync, + ) +} + +@Preview +@Composable +fun SearchAccountProviderViewLightPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun SearchAccountProviderViewDarkPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: SearchAccountProviderState) { + SearchAccountProviderView( + state = state, + onBackPressed = {}, + onLearnMoreClicked = {}, + onDone = {}, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt index cb01f8095a..e8bcea990e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/LoginConstants.kt @@ -16,8 +16,18 @@ package io.element.android.features.login.impl.util +import io.element.android.features.login.impl.accountprovider.AccountProvider + object LoginConstants { + const val MATRIX_ORG_URL = "matrix.org" const val DEFAULT_HOMESERVER_URL = "matrix.org" // TODO Oidc "synapse-oidc.lab.element.dev" const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" } + +val defaultAccountProvider = AccountProvider( + title = LoginConstants.DEFAULT_HOMESERVER_URL, + subtitle = null, + isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, + isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt new file mode 100644 index 0000000000..261b02c1b8 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt @@ -0,0 +1,27 @@ +/* + * 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.features.login.impl.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import io.element.android.libraries.core.data.tryOrNull + +fun openLearnMorePage(context: Context) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL)) + tryOrNull { context.startActivity(intent) } +} diff --git a/features/login/impl/src/main/res/drawable/ic_matrix.xml b/features/login/impl/src/main/res/drawable/ic_matrix.xml new file mode 100644 index 0000000000..dbc788a031 --- /dev/null +++ b/features/login/impl/src/main/res/drawable/ic_matrix.xml @@ -0,0 +1,12 @@ + + + + diff --git a/features/login/impl/src/main/res/drawable/ic_public.xml b/features/login/impl/src/main/res/drawable/ic_public.xml new file mode 100644 index 0000000000..fc1eacbc9f --- /dev/null +++ b/features/login/impl/src/main/res/drawable/ic_public.xml @@ -0,0 +1,12 @@ + + + + diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index cf59844e89..bb27ca51e1 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -1,5 +1,18 @@ + "Change account provider" + "Continue" + "Homeserver address" + "Enter a search term or a domain address." + "Search for a company, community, or private server." + "Find an account provider" + "You’re about to sign in to %s" + "This is where you conversations will live — just like you would use an email provider to keep your emails." + "You’re about to create an account on %s" + "Matrix.org is an open network for secure, decentralized communication." + "Other" + "Use a different account provider, such as your own private server or a work account." + "Change account provider" "We couldn\'t reach this homeserver. Please check that you have entered the homeserver URL correctly. If the URL is correct, contact your homeserver administrator for further help." "This server currently doesn’t support sliding sync." "Homeserver URL" @@ -13,9 +26,15 @@ "Where your conversations live" "Welcome back!" "Sign in to %1$s" + "Change account provider" + "A private server for Element employees." + "Matrix is an open network for secure, decentralised communication." + "This is where your conversations will live — just like you would use an email provider to keep your emails." + "You’re about to sign in to %1$s" + "You’re about to create an account on %1$s" "Continue" "Select your server" "Password" "Continue" "Username" - \ No newline at end of file + diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index a30bd9449c..9aefafb382 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -20,147 +20,72 @@ 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.util.LoginConstants +import io.element.android.features.login.impl.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource 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_2 -import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import kotlinx.coroutines.test.runTest import org.junit.Test class ChangeServerPresenterTest { @Test - fun `present - should start with default homeserver`() = runTest { + fun `present - initial state`() = runTest { val presenter = ChangeServerPresenter( FakeAuthenticationService(), + AccountProviderDataSource() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.homeserver).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) - assertThat(initialState.submitEnabled).isTrue() + assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) } } @Test - fun `present - authentication service can provide a homeserver`() = runTest { - val presenter = ChangeServerPresenter( - FakeAuthenticationService().apply { - givenHomeserver(A_HOMESERVER.copy(url = A_HOMESERVER_URL_2)) - }, - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.homeserver).isEqualTo(A_HOMESERVER_URL_2) - assertThat(initialState.submitEnabled).isTrue() - } - } - - @Test - fun `present - disable if empty or not correct`() = runTest { - val presenter = ChangeServerPresenter( - FakeAuthenticationService(), - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(ChangeServerEvents.SetServer("")) - val emptyState = awaitItem() - assertThat(emptyState.homeserver).isEqualTo("") - assertThat(emptyState.submitEnabled).isFalse() - } - } - - @Test - fun `present - submit`() = runTest { - val presenter = ChangeServerPresenter( - FakeAuthenticationService(), - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(ChangeServerEvents.Submit) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) - val successState = awaitItem() - assertThat(successState.submitEnabled).isFalse() - assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java) - } - } - - @Test - fun `present - submit parses URL`() = runTest { - val presenter = ChangeServerPresenter( - FakeAuthenticationService(), - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val longUrl = "https://matrix.org/.well-known/" - val initialState = awaitItem() - initialState.eventSink.invoke(ChangeServerEvents.SetServer(longUrl)) - awaitItem() - initialState.eventSink.invoke(ChangeServerEvents.Submit) - val loadingState = awaitItem() - assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) - awaitItem() // Skip changing the url to the parsed domain - val successState = awaitItem() - assertThat(successState.submitEnabled).isFalse() - assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java) - assertThat(successState.homeserver).isEqualTo("matrix.org") - } - } - - @Test - fun `present - submit fails`() = runTest { - val authServer = FakeAuthenticationService() - val presenter = ChangeServerPresenter(authServer) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - authServer.givenChangeServerError(Throwable()) - initialState.eventSink.invoke(ChangeServerEvents.Submit) - skipItems(1) // Loading - val failureState = awaitItem() - assertThat(failureState.submitEnabled).isFalse() - assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java) - } - } - - @Test - fun `present - clear error`() = runTest { + fun `present - change server ok`() = runTest { val authenticationService = FakeAuthenticationService() val presenter = ChangeServerPresenter( authenticationService, + AccountProviderDataSource() ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() + assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) + authenticationService.givenHomeserver(A_HOMESERVER) + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(A_HOMESERVER_URL))) + val loadingState = awaitItem() + assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.changeServerAction).isEqualTo(Async.Success(Unit)) + } + } - // Submit will return an error - authenticationService.givenChangeServerError(A_THROWABLE) - initialState.eventSink(ChangeServerEvents.Submit) - - skipItems(1) // Loading - - // Check an error was returned - val submittedState = awaitItem() - assertThat(submittedState.changeServerAction).isInstanceOf(Async.Failure::class.java) - - // Assert the error is then cleared - submittedState.eventSink(ChangeServerEvents.ClearError) - val clearedState = awaitItem() - assertThat(clearedState.changeServerAction).isEqualTo(Async.Uninitialized) + @Test + fun `present - change server error`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = ChangeServerPresenter( + authenticationService, + AccountProviderDataSource() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(A_HOMESERVER_URL))) + val loadingState = awaitItem() + assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) + val failureState = awaitItem() + assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java) + // Clear error + failureState.eventSink.invoke(ChangeServerEvents.ClearError) + val finalState = awaitItem() + assertThat(finalState.changeServerAction).isEqualTo(Async.Uninitialized) } } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt new file mode 100644 index 0000000000..58c2bf82a3 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt @@ -0,0 +1,28 @@ +/* + * 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.features.login.impl.resolver.network + +class FakeWellknownRequest : WellknownRequest { + private var resultMap: Map = emptyMap() + fun givenResultMap(map: Map) { + resultMap = map + } + + override suspend fun execute(baseUrl: String): WellKnown { + return resultMap[baseUrl] ?: error("No result provided for $baseUrl") + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt deleted file mode 100644 index 0dee8d47c0..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt +++ /dev/null @@ -1,308 +0,0 @@ -/* - * 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.features.login.impl.root - -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.api.oidc.OidcAction -import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow -import io.element.android.features.login.impl.util.LoginConstants -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails -import io.element.android.libraries.matrix.test.A_HOMESERVER -import io.element.android.libraries.matrix.test.A_HOMESERVER_OIDC -import io.element.android.libraries.matrix.test.A_PASSWORD -import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.A_THROWABLE -import io.element.android.libraries.matrix.test.A_USER_NAME -import io.element.android.libraries.matrix.test.auth.A_OIDC_DATA -import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService -import kotlinx.coroutines.test.runTest -import org.junit.Test - -class LoginRootPresenterTest { - @Test - fun `present - initial state`() = runTest { - val presenter = LoginRootPresenter( - FakeAuthenticationService(), - DefaultOidcActionFlow(), - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) - assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) - assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) - assertThat(initialState.formState).isEqualTo(LoginFormState.Default) - assertThat(initialState.submitEnabled).isFalse() - cancelAndIgnoreRemainingEvents() - } - } - - @Test - fun `present - initial state server load`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) - assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) - assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) - assertThat(initialState.formState).isEqualTo(LoginFormState.Default) - assertThat(initialState.submitEnabled).isFalse() - val loadingState = awaitItem() - assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading()) - authenticationService.givenHomeserver(A_HOMESERVER) - skipItems(1) - val loadedState = awaitItem() - assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER)) - } - } - - @Test - fun `present - initial state server load error and retry`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.homeserverUrl).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) - assertThat(initialState.homeserverDetails).isEqualTo(Async.Uninitialized) - assertThat(initialState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) - assertThat(initialState.formState).isEqualTo(LoginFormState.Default) - assertThat(initialState.submitEnabled).isFalse() - val loadingState = awaitItem() - assertThat(loadingState.homeserverDetails).isEqualTo(Async.Loading()) - val aThrowable = Throwable("Error") - authenticationService.givenChangeServerError(aThrowable) - val errorState = awaitItem() - assertThat(errorState.homeserverDetails).isEqualTo(Async.Failure(aThrowable)) - // Retry - errorState.eventSink.invoke(LoginRootEvents.RetryFetchServerInfo) - val loadingState2 = awaitItem() - assertThat(loadingState2.homeserverDetails).isEqualTo(Async.Loading()) - authenticationService.givenChangeServerError(null) - authenticationService.givenHomeserver(A_HOMESERVER) - skipItems(1) - val loadedState = awaitItem() - assertThat(loadedState.homeserverDetails).isEqualTo(Async.Success(A_HOMESERVER)) - } - } - - @Test - fun `present - enter login and password`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - authenticationService.givenHomeserver(A_HOMESERVER) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) - val loginState = awaitItem() - assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = "")) - assertThat(loginState.submitEnabled).isFalse() - initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) - val loginAndPasswordState = awaitItem() - assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD)) - assertThat(loginAndPasswordState.submitEnabled).isTrue() - } - } - - @Test - fun `present - oidc login`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - authenticationService.givenHomeserver(A_HOMESERVER_OIDC) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.submitEnabled).isTrue() - initialState.eventSink.invoke(LoginRootEvents.Submit) - val oidcState = awaitItem() - assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA)) - } - } - - @Test - fun `present - oidc login error`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - authenticationService.givenHomeserver(A_HOMESERVER_OIDC) - authenticationService.givenOidcError(A_THROWABLE) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.submitEnabled).isTrue() - initialState.eventSink.invoke(LoginRootEvents.Submit) - val oidcState = awaitItem() - assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) - } - } - - @Test - fun `present - oidc custom tab login`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - authenticationService.givenHomeserver(A_HOMESERVER_OIDC) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - assertThat(initialState.submitEnabled).isTrue() - initialState.eventSink.invoke(LoginRootEvents.Submit) - val oidcState = awaitItem() - assertThat(oidcState.loggedInState).isEqualTo(LoggedInState.OidcStarted(A_OIDC_DATA)) - // Oidc cancel, sdk error - authenticationService.givenOidcCancelError(A_THROWABLE) - oidcActionFlow.post(OidcAction.GoBack) - val stateCancelSdkError = awaitItem() - assertThat(stateCancelSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) - // Oidc cancel, sdk OK - authenticationService.givenOidcCancelError(null) - oidcActionFlow.post(OidcAction.GoBack) - val stateCancel = awaitItem() - assertThat(stateCancel.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) - // Oidc success, sdk error - authenticationService.givenLoginError(A_THROWABLE) - oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url)) - val stateSuccessSdkErrorLoading = awaitItem() - assertThat(stateSuccessSdkErrorLoading.loggedInState).isEqualTo(LoggedInState.LoggingIn) - val stateSuccessSdkError = awaitItem() - assertThat(stateSuccessSdkError.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) - // Oidc success - authenticationService.givenLoginError(null) - oidcActionFlow.post(OidcAction.Success(A_OIDC_DATA.url)) - val stateSuccess = awaitItem() - assertThat(stateSuccess.loggedInState).isEqualTo(LoggedInState.LoggingIn) - val stateSuccessLoggedIn = awaitItem() - assertThat(stateSuccessLoggedIn.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID)) - } - } - - @Test - fun `present - submit`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - authenticationService.givenHomeserver(A_HOMESERVER) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) - initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) - skipItems(1) - val loginAndPasswordState = awaitItem() - loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit) - val submitState = awaitItem() - assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn) - val loggedInState = awaitItem() - assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.LoggedIn(A_SESSION_ID)) - } - } - - @Test - fun `present - submit with error`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - authenticationService.givenHomeserver(A_HOMESERVER) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - initialState.eventSink.invoke(LoginRootEvents.SetLogin(A_USER_NAME)) - initialState.eventSink.invoke(LoginRootEvents.SetPassword(A_PASSWORD)) - skipItems(1) - val loginAndPasswordState = awaitItem() - authenticationService.givenLoginError(A_THROWABLE) - loginAndPasswordState.eventSink.invoke(LoginRootEvents.Submit) - val submitState = awaitItem() - assertThat(submitState.loggedInState).isEqualTo(LoggedInState.LoggingIn) - val loggedInState = awaitItem() - assertThat(loggedInState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) - } - } - - @Test - fun `present - clear error`() = runTest { - val authenticationService = FakeAuthenticationService() - val oidcActionFlow = DefaultOidcActionFlow() - val presenter = LoginRootPresenter( - authenticationService, - oidcActionFlow, - ) - authenticationService.givenHomeserver(A_HOMESERVER) - moleculeFlow(RecompositionClock.Immediate) { - presenter.present() - }.test { - val initialState = awaitItem() - // Submit will return an error - authenticationService.givenLoginError(A_THROWABLE) - initialState.eventSink(LoginRootEvents.Submit) - awaitItem() // Skip LoggingIn state - - // Check an error was returned - val submittedState = awaitItem() - assertThat(submittedState.loggedInState).isEqualTo(LoggedInState.ErrorLoggingIn(A_THROWABLE)) - - // Assert the error is then cleared - submittedState.eventSink(LoginRootEvents.ClearError) - val clearedState = awaitItem() - assertThat(clearedState.loggedInState).isEqualTo(LoggedInState.NotLoggedIn) - } - } -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt new file mode 100644 index 0000000000..086428257a --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -0,0 +1,58 @@ +/* + * 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.features.login.impl.screens.changeaccountprovider + +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.accountprovider.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ChangeAccountProviderPresenterTest { + @Test + fun `present - initial state`() = runTest { + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = ChangeAccountProviderPresenter( + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.accountProviders).isEqualTo( + listOf( + AccountProvider( + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + isValid = true, + supportSlidingSync = true, + ) + ) + ) + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt new file mode 100644 index 0000000000..131d0d9298 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -0,0 +1,150 @@ +/* + * 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.features.login.impl.screens.confirmaccountprovider + +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.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.util.defaultAccountProvider +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_OIDC +import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ConfirmAccountProviderPresenterTest { + @Test + fun `present - initial test`() = runTest { + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), + AccountProviderDataSource(), + FakeAuthenticationService(), + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isAccountCreation).isFalse() + assertThat(initialState.submitEnabled).isTrue() + assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) + assertThat(initialState.loginFlow).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - continue password login`() = runTest { + val authServer = FakeAuthenticationService() + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), + AccountProviderDataSource(), + authServer, + ) + authServer.givenHomeserver(A_HOMESERVER) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) + val loadingState = awaitItem() + assertThat(loadingState.submitEnabled).isTrue() + assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.submitEnabled).isFalse() + assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) + assertThat(successState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.PasswordLogin) + } + } + + @Test + fun `present - continue oidc`() = runTest { + val authServer = FakeAuthenticationService() + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), + AccountProviderDataSource(), + authServer, + ) + authServer.givenHomeserver(A_HOMESERVER_OIDC) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) + val loadingState = awaitItem() + assertThat(loadingState.submitEnabled).isTrue() + assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) + val successState = awaitItem() + assertThat(successState.submitEnabled).isFalse() + assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) + assertThat(successState.loginFlow.dataOrNull()).isInstanceOf(LoginFlow.OidcFlow::class.java) + } + } + + @Test + fun `present - submit fails`() = runTest { + val authServer = FakeAuthenticationService() + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), + AccountProviderDataSource(), + authServer, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + authServer.givenChangeServerError(Throwable()) + initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) + skipItems(1) // Loading + val failureState = awaitItem() + assertThat(failureState.submitEnabled).isFalse() + assertThat(failureState.loginFlow).isInstanceOf(Async.Failure::class.java) + } + } + + @Test + fun `present - clear error`() = runTest { + val authenticationService = FakeAuthenticationService() + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), + AccountProviderDataSource(), + authenticationService, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + + // Submit will return an error + authenticationService.givenChangeServerError(A_THROWABLE) + initialState.eventSink(ConfirmAccountProviderEvents.Continue) + + skipItems(1) // Loading + + // Check an error was returned + val submittedState = awaitItem() + assertThat(submittedState.loginFlow).isInstanceOf(Async.Failure::class.java) + + // Assert the error is then cleared + submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) + val clearedState = awaitItem() + assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized) + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt new file mode 100644 index 0000000000..c4c8a97155 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt @@ -0,0 +1,161 @@ +/* + * 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.features.login.impl.screens.loginpassword + +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.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.util.defaultAccountProvider +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.test.A_HOMESERVER +import io.element.android.libraries.matrix.test.A_PASSWORD +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_THROWABLE +import io.element.android.libraries.matrix.test.A_USER_NAME +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class LoginPasswordPresenterTest { + @Test + fun `present - initial state`() = runTest { + val authenticationService = FakeAuthenticationService() + val accountProviderDataSource = AccountProviderDataSource() + val presenter = LoginPasswordPresenter( + authenticationService, + accountProviderDataSource, + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) + assertThat(initialState.formState).isEqualTo(LoginFormState.Default) + assertThat(initialState.loginAction).isEqualTo(Async.Uninitialized) + assertThat(initialState.submitEnabled).isFalse() + } + } + + @Test + fun `present - enter login and password`() = runTest { + val authenticationService = FakeAuthenticationService() + val accountProviderDataSource = AccountProviderDataSource() + val presenter = LoginPasswordPresenter( + authenticationService, + accountProviderDataSource, + ) + authenticationService.givenHomeserver(A_HOMESERVER) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) + val loginState = awaitItem() + assertThat(loginState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = "")) + assertThat(loginState.submitEnabled).isFalse() + initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) + val loginAndPasswordState = awaitItem() + assertThat(loginAndPasswordState.formState).isEqualTo(LoginFormState(login = A_USER_NAME, password = A_PASSWORD)) + assertThat(loginAndPasswordState.submitEnabled).isTrue() + } + } + + @Test + fun `present - submit`() = runTest { + val authenticationService = FakeAuthenticationService() + val accountProviderDataSource = AccountProviderDataSource() + val presenter = LoginPasswordPresenter( + authenticationService, + accountProviderDataSource, + ) + authenticationService.givenHomeserver(A_HOMESERVER) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) + skipItems(1) + val loginAndPasswordState = awaitItem() + loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) + val submitState = awaitItem() + assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) + val loggedInState = awaitItem() + assertThat(loggedInState.loginAction).isEqualTo(Async.Success(A_SESSION_ID)) + } + } + + @Test + fun `present - submit with error`() = runTest { + val authenticationService = FakeAuthenticationService() + val accountProviderDataSource = AccountProviderDataSource() + val presenter = LoginPasswordPresenter( + authenticationService, + accountProviderDataSource, + ) + authenticationService.givenHomeserver(A_HOMESERVER) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) + skipItems(1) + val loginAndPasswordState = awaitItem() + authenticationService.givenLoginError(A_THROWABLE) + loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) + val submitState = awaitItem() + assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) + val loggedInState = awaitItem() + assertThat(loggedInState.loginAction).isEqualTo(Async.Failure(A_THROWABLE)) + } + } + + @Test + fun `present - clear error`() = runTest { + val authenticationService = FakeAuthenticationService() + val accountProviderDataSource = AccountProviderDataSource() + val presenter = LoginPasswordPresenter( + authenticationService, + accountProviderDataSource, + ) + authenticationService.givenHomeserver(A_HOMESERVER) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(LoginPasswordEvents.SetLogin(A_USER_NAME)) + initialState.eventSink.invoke(LoginPasswordEvents.SetPassword(A_PASSWORD)) + skipItems(1) + val loginAndPasswordState = awaitItem() + authenticationService.givenLoginError(A_THROWABLE) + loginAndPasswordState.eventSink.invoke(LoginPasswordEvents.Submit) + val submitState = awaitItem() + assertThat(submitState.loginAction).isInstanceOf(Async.Loading::class.java) + val loggedInState = awaitItem() + // Check an error was returned + assertThat(loggedInState.loginAction).isEqualTo(Async.Failure(A_THROWABLE)) + // Assert the error is then cleared + loggedInState.eventSink(LoginPasswordEvents.ClearError) + val clearedState = awaitItem() + assertThat(clearedState.loginAction).isEqualTo(Async.Uninitialized) + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt new file mode 100644 index 0000000000..9163f247f5 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt @@ -0,0 +1,195 @@ +/* + * 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.features.login.impl.screens.searchaccountprovider + +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.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.resolver.HomeserverResolver +import io.element.android.features.login.impl.resolver.network.FakeWellknownRequest +import io.element.android.features.login.impl.resolver.network.WellKnown +import io.element.android.features.login.impl.resolver.network.WellKnownBaseConfig +import io.element.android.features.login.impl.resolver.network.WellKnownSlidingSyncConfig +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class SearchAccountProviderPresenterTest { + @Test + fun `present - initial state`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.userInput).isEmpty() + assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - enter text no result`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) + val withInputState = awaitItem() + assertThat(withInputState.userInput).isEqualTo("test") + assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) + assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().userInputResult).isEqualTo(Async.Uninitialized) + } + } + + @Test + fun `present - enter valid url no wellknown`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org")) + val withInputState = awaitItem() + assertThat(withInputState.userInput).isEqualTo("https://test.org") + assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) + assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().userInputResult).isEqualTo( + Async.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = false, supportSlidingSync = false) + ) + ) + ) + } + } + + @Test + fun `present - enter text one result no sliding sync`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + fakeWellknownRequest.givenResultMap( + mapOf( + "https://test.org" to aWellKnown().copy(slidingSyncProxy = null), + ) + ) + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) + val withInputState = awaitItem() + assertThat(withInputState.userInput).isEqualTo("test") + assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) + assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().userInputResult).isEqualTo( + Async.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.org", isWellknownValid = true, supportSlidingSync = false) + ) + ) + ) + } + } + + @Test + fun `present - enter text one result with sliding sync`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + fakeWellknownRequest.givenResultMap( + mapOf( + "https://test.io" to aWellKnown(), + ) + ) + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) + val withInputState = awaitItem() + assertThat(withInputState.userInput).isEqualTo("test") + assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) + assertThat(awaitItem().userInputResult).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().userInputResult).isEqualTo( + Async.Success( + listOf( + aHomeserverData(homeserverUrl = "https://test.io") + ) + ) + ) + } + } + + private fun aWellKnown(): WellKnown { + return WellKnown( + homeServer = WellKnownBaseConfig( + baseURL = A_HOMESERVER_URL + ), + identityServer = WellKnownBaseConfig( + baseURL = A_HOMESERVER_URL + ), + slidingSyncProxy = WellKnownSlidingSyncConfig( + url = A_HOMESERVER_URL + ) + ) + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt index a5b1780589..beb37f8e51 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/fixtures/timelineItemsFactory.kt @@ -37,8 +37,9 @@ import io.element.android.libraries.dateformatter.test.FakeDaySeparatorFormatter import io.element.android.libraries.eventformatter.api.TimelineEventFormatter import io.element.android.libraries.matrix.api.timeline.item.event.EventTimelineItem import io.element.android.tests.testutils.testCoroutineDispatchers +import kotlinx.coroutines.test.TestScope -internal fun aTimelineItemsFactory(): TimelineItemsFactory { +internal fun TestScope.aTimelineItemsFactory(): TimelineItemsFactory { val timelineEventFormatter = aTimelineEventFormatter() return TimelineItemsFactory( dispatchers = testCoroutineDispatchers(), diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt index a081c0b7ab..d86623cae2 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingNode.kt @@ -53,6 +53,7 @@ class OnBoardingNode @AssistedInject constructor( state = state, modifier = modifier, onSignIn = ::onSignIn, + onCreateAccount = ::onSignUp, ) } } diff --git a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt index 6f071fd090..643ab2bfd8 100644 --- a/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt +++ b/features/onboarding/impl/src/main/kotlin/io/element/android/features/onboarding/impl/OnBoardingView.kt @@ -120,9 +120,7 @@ private fun OnBoardingButtons( ButtonColumnMolecule(modifier = modifier) { if (state.canLoginWithQrCode) { Button( - onClick = { - onSignInWithQrCode() - }, + onClick = onSignInWithQrCode, enabled = true, modifier = Modifier .fillMaxWidth() @@ -136,9 +134,7 @@ private fun OnBoardingButtons( } } Button( - onClick = { - onSignIn() - }, + onClick = onSignIn, enabled = true, modifier = Modifier .fillMaxWidth() @@ -148,9 +144,7 @@ private fun OnBoardingButtons( } if (state.canCreateAccount) { OutlinedButton( - onClick = { - onCreateAccount() - }, + onClick = onCreateAccount, enabled = true, modifier = Modifier .fillMaxWidth() diff --git a/features/onboarding/impl/src/main/res/values/localazy.xml b/features/onboarding/impl/src/main/res/values/localazy.xml index 54d86ba247..ef91de7541 100644 --- a/features/onboarding/impl/src/main/res/values/localazy.xml +++ b/features/onboarding/impl/src/main/res/values/localazy.xml @@ -4,6 +4,6 @@ "Sign in with QR code" "Create account" "Communicate and collaborate securely" - "Welcome to the %1$s Beta. Supercharged, for speed and simplicity." + "Welcome to %1$s. Supercharged, for speed and simplicity." "Be in your Element" \ No newline at end of file diff --git a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt index 05f3232aac..0e0ebaaab2 100644 --- a/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt +++ b/features/rageshake/impl/src/main/kotlin/io/element/android/features/rageshake/impl/bugreport/BugReportView.kt @@ -81,12 +81,9 @@ fun BugReportView( .systemBarsPadding() .imePadding() ) { - val scrollState = rememberScrollState() Column( modifier = Modifier - .verticalScroll( - state = scrollState, - ) + .verticalScroll(state = rememberScrollState()) .padding(horizontal = 16.dp), ) { val isError = state.sending is Async.Failure diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt index ced5e79bbe..867d2f433d 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersView.kt @@ -147,7 +147,7 @@ private fun RoomInviteMembersSearchBar( selectedUsers: ImmutableList, active: Boolean, modifier: Modifier = Modifier, - placeHolderTitle: String = stringResource(io.element.android.libraries.ui.strings.R.string.common_search_for_someone), + placeHolderTitle: String = stringResource(StringR.string.common_search_for_someone), onActiveChanged: (Boolean) -> Unit = {}, onTextChanged: (String) -> Unit = {}, onUserToggled: (MatrixUser) -> Unit = {}, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index cf1d9a49ac..43f1667730 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -252,7 +252,7 @@ class RoomDetailsPresenterTests { fun aMatrixClient( sessionId: SessionId = A_SESSION_ID, -) = FakeMatrixClient() +) = FakeMatrixClient(sessionId = sessionId) fun aMatrixRoom( roomId: RoomId = A_ROOM_ID, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt index 3baea96990..495e639baf 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/impl/invite/RoomInviteMembersPresenterTest.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.usersearch.api.UserSearchResult import io.element.android.libraries.usersearch.test.FakeUserRepository import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.collections.immutable.ImmutableList +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -368,7 +369,7 @@ internal class RoomInviteMembersPresenterTest { } } - private fun createDataSource( + private fun TestScope.createDataSource( matrixRoom: MatrixRoom = aMatrixRoom().apply { givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) }, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 9625a167f7..b9b52b29f8 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -35,6 +35,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Test @@ -165,7 +166,7 @@ class RoomMemberListPresenterTests { } @ExperimentalCoroutinesApi -private fun createDataSource( +private fun TestScope.createDataSource( matrixRoom: MatrixRoom = aMatrixRoom().apply { givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) }, @@ -173,7 +174,7 @@ private fun createDataSource( ) = RoomMemberListDataSource(matrixRoom, coroutineDispatchers) @ExperimentalCoroutinesApi -private fun createPresenter( +private fun TestScope.createPresenter( matrixRoom: MatrixRoom = FakeMatrixRoom(), roomMemberListDataSource: RoomMemberListDataSource = createDataSource(), coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt new file mode 100644 index 0000000000..752c80175d --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt @@ -0,0 +1,126 @@ +/* + * 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.atomic.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +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.LocalColors +import io.element.android.libraries.designsystem.theme.components.Icon + +/** + * RoundedIconAtom is an atom which displays an icon inside a rounded container. + * + * @param modifier the modifier to apply to this layout + * @param size the size of the icon + * @param resourceId the resource id of the icon to display, exclusive with [imageVector] + * @param imageVector the image vector of the icon to display, exclusive with [resourceId] + * @param tint the tint to apply to the icon + */ +@Composable +fun RoundedIconAtom( + modifier: Modifier = Modifier, + size: RoundedIconAtomSize = RoundedIconAtomSize.Large, + resourceId: Int? = null, + imageVector: ImageVector? = null, + tint: Color = MaterialTheme.colorScheme.secondary +) { + Box( + modifier = modifier + .size(size.toContainerSize()) + .background( + color = LocalColors.current.quinary, + shape = RoundedCornerShape(size.toCornerSize()) + ) + ) { + Icon( + modifier = Modifier + .align(Alignment.Center) + .size(size.toIconSize()), + tint = tint, + resourceId = resourceId, + imageVector = imageVector, + contentDescription = "", + ) + } +} + +private fun RoundedIconAtomSize.toContainerSize(): Dp { + return when (this) { + RoundedIconAtomSize.Medium -> 30.dp + RoundedIconAtomSize.Large -> 70.dp + } +} + +private fun RoundedIconAtomSize.toCornerSize(): Dp { + return when (this) { + RoundedIconAtomSize.Medium -> 8.dp + RoundedIconAtomSize.Large -> 14.dp + } +} + +private fun RoundedIconAtomSize.toIconSize(): Dp { + return when (this) { + RoundedIconAtomSize.Medium -> 16.dp + RoundedIconAtomSize.Large -> 48.dp + } +} + +@Preview +@Composable +internal fun RoundedIconAtomLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun RoundedIconAtomDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + RoundedIconAtom( + size = RoundedIconAtomSize.Medium, + imageVector = Icons.Filled.Home, + ) + RoundedIconAtom( + size = RoundedIconAtomSize.Large, + imageVector = Icons.Filled.Home, + ) + } +} + +enum class RoundedIconAtomSize { + Medium, + Large +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt index bda10b8ba2..24adf90156 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/molecules/IconTitleSubtitleMolecule.kt @@ -16,55 +16,55 @@ package io.element.android.libraries.designsystem.atomic.molecules -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.style.TextAlign 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.R +import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtom +import io.element.android.libraries.designsystem.atomic.atoms.RoundedIconAtomSize 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.Icon import io.element.android.libraries.designsystem.theme.components.Text +/** + * IconTitleSubtitleMolecule is a molecule which displays an icon, a title and a subtitle. + * + * @param title the title to display + * @param subTitle the subtitle to display + * @param modifier the modifier to apply to this layout + * @param iconResourceId the resource id of the icon to display, exclusive with [iconImageVector] + * @param iconImageVector the image vector of the icon to display, exclusive with [iconResourceId] + * @param iconTint the tint to apply to the icon + */ @Composable fun IconTitleSubtitleMolecule( - iconResourceId: Int, title: String, subTitle: String, modifier: Modifier = Modifier, + iconResourceId: Int? = null, + iconImageVector: ImageVector? = null, + iconTint: Color = MaterialTheme.colorScheme.primary, ) { Column(modifier) { - Box( + RoundedIconAtom( modifier = Modifier - .size(width = 70.dp, height = 70.dp) - .align(Alignment.CenterHorizontally) - .background( - color = LocalColors.current.quinary, - shape = RoundedCornerShape(14.dp) - ) - ) { - Icon( - modifier = Modifier - .align(Alignment.Center) - .size(width = 48.dp, height = 48.dp), - tint = MaterialTheme.colorScheme.secondary, - resourceId = iconResourceId, - contentDescription = "", - ) - } + .align(Alignment.CenterHorizontally), + size = RoundedIconAtomSize.Large, + resourceId = iconResourceId, + imageVector = iconImageVector, + tint = iconTint, + ) Spacer(modifier = Modifier.height(16.dp)) Text( text = title, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt index 81a2ca36fc..71a41c2460 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/LabelledTextField.kt @@ -37,8 +37,8 @@ fun LabelledTextField( value: String, modifier: Modifier = Modifier, placeholder: String? = null, - maxLines: Int = Int.MAX_VALUE, singleLine: Boolean = false, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, onValueChange: (String) -> Unit = {}, ) { Column( diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt index 5863f4c80c..e81bc140bf 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/async/AsyncFailure.kt @@ -24,12 +24,14 @@ import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource 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 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 @Composable fun AsyncFailure( @@ -43,11 +45,11 @@ fun AsyncFailure( .padding(vertical = 32.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Text(text = throwable.message ?: "An error occurred") + Text(text = throwable.message ?: stringResource(id = StringR.string.error_unknown)) if (onRetry != null) { Spacer(modifier = Modifier.height(24.dp)) Button(onClick = onRetry) { - Text(text = "Retry") + Text(text = stringResource(id = StringR.string.action_retry)) } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt index c7366dcfb7..0de4dbba78 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/form/TextFieldLocalState.kt @@ -22,5 +22,5 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @Composable -public fun textFieldState(stateValue: String): MutableState = +fun textFieldState(stateValue: String): MutableState = remember(stateValue) { mutableStateOf(stateValue) } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt index 10578ebcca..3826e6ebde 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/preferences/PreferenceScreen.kt @@ -69,14 +69,11 @@ fun PreferenceView( ) }, content = { - val scrollState = rememberScrollState() Column( modifier = Modifier .padding(it) .consumeWindowInsets(it) - .verticalScroll( - state = scrollState, - ) + .verticalScroll(state = rememberScrollState()) ) { content() } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt index dedf2af060..24de433058 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Icon.kt @@ -30,6 +30,54 @@ import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup +/** + * Icon is a wrapper around [androidx.compose.material3.Icon] which allows to use + * [ImageVector], [ImageBitmap] or [DrawableRes] as icon source. + * + * @param contentDescription the content description to be used for accessibility + * @param modifier the modifier to apply to this layout + * @param tint the tint to apply to the icon + * @param imageVector the image vector of the icon to display, exclusive with [bitmap] and [resourceId] + * @param bitmap the bitmap of the icon to display, exclusive with [imageVector] and [resourceId] + * @param resourceId the resource id of the icon to display, exclusive with [imageVector] and [bitmap] + */ +@Composable +fun Icon( + contentDescription: String?, + modifier: Modifier = Modifier, + tint: Color = LocalContentColor.current, + imageVector: ImageVector? = null, + bitmap: ImageBitmap? = null, + @DrawableRes resourceId: Int? = null, +) { + when { + imageVector != null -> { + Icon( + imageVector = imageVector, + contentDescription = contentDescription, + modifier = modifier, + tint = tint + ) + } + bitmap != null -> { + Icon( + bitmap = bitmap, + contentDescription = contentDescription, + modifier = modifier, + tint = tint + ) + } + resourceId != null -> { + Icon( + resourceId = resourceId, + contentDescription = contentDescription, + modifier = modifier, + tint = tint + ) + } + } +} + @Composable fun Icon( imageVector: ImageVector, diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt index 423d1e46d1..358ca2abab 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt @@ -67,7 +67,7 @@ fun OutlinedTextField( keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, - maxLines: Int = Int.MAX_VALUE, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = OutlinedTextFieldDefaults.shape, colors: TextFieldColors = OutlinedTextFieldDefaults.colors() diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt index 8376369fba..2ad41887aa 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/TextField.kt @@ -68,7 +68,7 @@ fun TextField( keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, - maxLines: Int = Int.MAX_VALUE, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, shape: Shape = TextFieldDefaults.shape, colors: TextFieldColors = TextFieldDefaults.colors() diff --git a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt index 169ec55a9d..ccf9c2976a 100644 --- a/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt +++ b/libraries/eventformatter/impl/src/main/kotlin/io/element/android/libraries/eventformatter/impl/StateContentFormatter.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.StateContent import io.element.android.services.toolbox.api.strings.StringProvider import timber.log.Timber import javax.inject.Inject +import io.element.android.libraries.ui.strings.R as StringR class StateContentFormatter @Inject constructor( private val sp: StringProvider, @@ -49,7 +50,7 @@ class StateContentFormatter @Inject constructor( sp.getString(R.string.state_event_room_created, senderDisplayName) } } - is OtherState.RoomEncryption -> sp.getString(io.element.android.libraries.ui.strings.R.string.common_encryption_enabled) + is OtherState.RoomEncryption -> sp.getString(StringR.string.common_encryption_enabled) is OtherState.RoomName -> { val hasRoomName = content.name != null when { diff --git a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt index df12c755e3..d702c797a8 100644 --- a/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt +++ b/libraries/testtags/src/main/kotlin/io/element/android/libraries/testtags/TestTags.kt @@ -37,7 +37,6 @@ object TestTags { * Change server screen. */ val changeServerServer = TestTag("change_server-server") - val changeServerContinue = TestTag("change_server-continue") /** * Room list / Home screen. diff --git a/libraries/ui-strings/src/main/res/values-cs/translations.xml b/libraries/ui-strings/src/main/res/values-cs/translations.xml index daffb59602..0f2affe054 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -150,12 +150,6 @@ "Nahrání média se nezdařilo, zkuste to prosím znovu." "Nahrání média se nezdařilo, zkuste to prosím znovu." "Zaškrtněte, pokud chcete skrýt všechny aktuální a budoucí zprávy od tohoto uživatele" - "Změnit poskytovatele účtu" - "Soukromý server pro zaměstnance Elementu." - "Matrix je otevřená síť pro bezpečnou a decentralizovanou komunikaci." - "Zde budou uloženy vaše konverzace - podobně jako u poskytovatele e-mailových služeb uchováváte své e-maily." - "Chystáte se přihlásit do služby %1$s" - "Chystáte se vytvořit účet na %1$s" "Rageshake" "Práh detekce" "Obecné" diff --git a/libraries/ui-strings/src/main/res/values-de/translations.xml b/libraries/ui-strings/src/main/res/values-de/translations.xml index 90f27bd8dd..ad4c3571ad 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -142,9 +142,6 @@ "Medienauswahl fehlgeschlagen, bitte versuche es erneut." "Medien hochladen fehlgeschlagen. Bitte versuchen Sie es erneut." "Prüfe, ob du alle aktuellen und zukünftigen Nachrichten dieses Benutzers ausblenden möchtest" - "Kontoanbieter wechseln" - "Ein privater Server für Element-Mitarbeiter." - "Matrix ist ein offenes Netzwerk für sichere, dezentrale Kommunikation" "Rageshake" "Erkennungsschwelle" "Allgemein" @@ -156,4 +153,4 @@ "Sie können alle unsere Nutzerbedingungen %1$s lesen." "hier" "Nutzer blockieren" - \ No newline at end of file + diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 43d19f2202..93d2cc289e 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -57,9 +57,11 @@ "View Source" "Yes" "About" + "Acceptable use policy" "Analytics" "Audio" "Bubbles" + "Copyright" "Creating room…" "Left room" "Decryption error" @@ -81,11 +83,13 @@ "Message layout" "Message removed" "Modern" + "Mute" "No results" "Offline" "Password" "People" "Permalink" + "Privacy policy" "Reactions" "Replying to %1$s" "Report a bug" @@ -104,11 +108,13 @@ "Sticker" "Success" "Suggestions" + "Third-party notices" "Topic" "What is this room about?" "Unable to decrypt" "We were unable to successfully send invites to one or more users." "Unable to send invite(s)" + "Unmute" "Unsupported event" "Username" "Verification cancelled" @@ -164,12 +170,6 @@ "Failed processing media to upload, please try again." "Failed uploading media, please try again." "Check if you want to hide all current and future messages from this user" - "Change account provider" - "A private server for Element employees." - "Matrix is an open network for secure, decentralised communication." - "This is where your conversations will live — just like you would use an email provider to keep your emails." - "You’re about to sign in to %1$s" - "You’re about to create an account on %1$s" "Rageshake" "Detection threshold" "General" diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt index 9d9971a842..54bef4652b 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/LoginScreen.kt @@ -17,11 +17,13 @@ package io.element.android.samples.minimal import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import io.element.android.features.login.impl.oidc.customtab.DefaultOidcActionFlow -import io.element.android.features.login.impl.root.LoginRootPresenter -import io.element.android.features.login.impl.root.LoginRootView +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordPresenter +import io.element.android.features.login.impl.screens.loginpassword.LoginPasswordView +import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService class LoginScreen(private val authenticationService: MatrixAuthenticationService) { @@ -29,13 +31,18 @@ class LoginScreen(private val authenticationService: MatrixAuthenticationService @Composable fun Content(modifier: Modifier = Modifier) { val presenter = remember { - LoginRootPresenter( + LoginPasswordPresenter( authenticationService = authenticationService, - DefaultOidcActionFlow() + AccountProviderDataSource() ) } + + LaunchedEffect(Unit) { + authenticationService.setHomeserver(defaultAccountProvider.title) + } + val state = presenter.present() - LoginRootView( + LoginPasswordView( state = state, modifier = modifier, onBackPressed = {}, diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt index 4628f4910c..786d8f8cd2 100644 --- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/TestCoroutineDispatchers.kt @@ -21,19 +21,16 @@ package io.element.android.tests.testutils import io.element.android.libraries.core.coroutine.CoroutineDispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher /** * Create a [CoroutineDispatchers] instance for testing. * - * @param testScheduler The [TestCoroutineScheduler] to use. If using [runTest] use the one provided by its [TestScope]. - * If null the [TestDispatcher] logic will select one or create a new one. * @param useUnconfinedTestDispatcher If true, use [UnconfinedTestDispatcher] for all dispatchers. * If false, use [StandardTestDispatcher] for all dispatchers. */ -fun testCoroutineDispatchers( - testScheduler: TestCoroutineScheduler? = null, +fun TestScope.testCoroutineDispatchers( useUnconfinedTestDispatcher: Boolean = true, ): CoroutineDispatchers = when (useUnconfinedTestDispatcher) { false -> CoroutineDispatchers( diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a0ea87cbc0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c5ecc3788746bc425efe843018b13a8fad0a10665fcc0b9b528df6df54791cef +size 20811 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2b5edb0440 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2036b2b9b3815c69dca77fbde216be6d333607d6dc3ed254923ba35244d20061 +size 9441 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..22503a7c95 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59d342f4e163fd52199a2e05a2d403698a87bb51ce2494ee52c01968edbd71d0 +size 10482 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1f570432a2 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e38d29272030a66ce7d581f82f82553a3a19f4694117e1b5dd5d507ecce20c4 +size 8532 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..218db727b5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:07414bddb30f4535ba56c38cd295cc272a8531641485a5339f898b4186d3a466 +size 7464 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d44b573a9a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ce4df1f9faf5704b2407357f06a8879ad57fcc593a0fa59d642f580f1bd7bab +size 20098 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..37e177fe42 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:64ed033fbcccc2a5d40664ebbe2a32782579f77eaa07200a329d49165f2606a2 +size 9122 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bc67803f7f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:09ce6b3272ea2450b30c7b3b5fe6ee0aef44a03e62f433f42ab17b255e2fa680 +size 10172 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..534b7f86b3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d9ac9e05b076079caaaf9cd097b75d6cb928982a7c24a6f2eb9c73573521d959 +size 8276 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e0f3550333 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.accountprovider_null_DefaultGroup_AccountProviderViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:37c6d5fd1dbbb10fe4d18bb70c27409da86d832e3394d05841f194729c5b14cd +size 7148 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 55945f00cb..665c8811ac 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac39dda453fbdf0f5cc3c6df2d9436a171c141c8be2ca2e2d2a2c7ee5d16b36f -size 39650 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 9f65a20aaa..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:49ef94a7bd404fe5982f7319aa1c4f6b5d29d11675f53e192af85b4efb8c3517 -size 43267 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 75c156af00..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:47942f38dd2afb38d0e1174bd155bb708f645b231b250525920f5abaa7074308 -size 43217 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index bfba913061..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:b006f7768bbc48cf68e5d897920a14894496d0b2b84a1839a7158b45edea9a75 -size 48757 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index a63cf4d7e8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:12702b2d71193e376f15df6f1a029531868b9651b70c40d2d738646f15980704 -size 42362 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index 7b72ffcde5..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a942ed49efc175776994cf58d511af3be8f878b30ed5b34ab3ae0d600f2e0db1 -size 42316 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 8c716007cc..665c8811ac 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d692cd9030e6193691e676de62f68a9241cebf220a1f6c7986e095370e25c547 -size 37839 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index c979df7432..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:716e20f80dd634feb58ee26247defdd8e390d268e9ec5d9767ad7b574d7ca2bc -size 41581 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index bc159e7bcb..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:9658c925b80565109772111972a495d437bda9ae97a9d9327d92a44ff0a1b9d1 -size 41521 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index d0764f07b3..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0511bf277f5194a6d38cf13416c2b2a1c834bf216c511243351d2f9bfe1b7e3 -size 47857 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index be3729d669..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e248fa1785749f3a590576ed6f3c6899ad0042f71a80a4afdc7336981c7419a3 -size 40589 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index 7a621153ce..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeserver_null_DefaultGroup_ChangeServerViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:903df8ff5d3816a85e16a3ec46b2a98e742ba0fc0a317df829b0fca57f33d9aa -size 40553 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 22b1ce6d04..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:558f8f3b34e3a94a62460cca37396f94b0caa8197b7fbd49ce894382392f1899 -size 31512 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index 22b1ce6d04..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:558f8f3b34e3a94a62460cca37396f94b0caa8197b7fbd49ce894382392f1899 -size 31512 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index f48af1a990..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4469faa61e68d951ed13b9cf324d11a99a1b041fb096bade2cd5d11c8185acc -size 33232 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index d65333610e..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:81125c72229ddb56f3c3e8b826de42f947f8739c3f372cad99917ee7d4db3b5c -size 33318 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index 4efbb4fca6..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:cff519b372c22b623f72407e7c4113cfa24dae31fc517aea5de75bea9b4d61bf -size 29616 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index f48af1a990..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:c4469faa61e68d951ed13b9cf324d11a99a1b041fb096bade2cd5d11c8185acc -size 33232 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png deleted file mode 100644 index 1a305e0c79..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:78b278d94f46995b7d11719677b72c0804219b1c36072fd8e2ad6dfdb9de8aba -size 25313 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png deleted file mode 100644 index 2adab63ade..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:25b6b15bd1646316a6b1dde8d68396e6b8f1e677b0528f07cab8f8d17a828902 -size 24564 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png deleted file mode 100644 index b31cc0a315..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:a59f9b56497f976a0664855e3da6245c423e8608c24b3f08b0c0cef0809ba0ee -size 20273 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png deleted file mode 100644 index 873614913b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1f8841cada0e8826a43cbca6ed8af3aa703633eccdcb664be3377ab6b38ed960 -size 26073 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index a9d3b597b1..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ef34f0de8599a2c950690d1ef1d4c8f68419b137594177ed09cfe3715deb3767 -size 30490 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index a9d3b597b1..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ef34f0de8599a2c950690d1ef1d4c8f68419b137594177ed09cfe3715deb3767 -size 30490 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 718f68a9f8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:99de01c644442e4d309b1d6bc75d0920eb205df31ef0c0f2411cbb756e0a838e -size 32106 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png deleted file mode 100644 index a11d8fe9d2..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:acdde4f931b9e8aa18280f8f6f6e003ba406801ce1603e87ab087afe44465167 -size 32124 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_4,NEXUS_5,1.0,en].png deleted file mode 100644 index 5b2872a1e2..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6f3cbb18c3c2b766b7b10b204edc300cce73193c458a2c9b7dd88bf6fa7bcd6b -size 29007 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_5,NEXUS_5,1.0,en].png deleted file mode 100644 index 718f68a9f8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:99de01c644442e4d309b1d6bc75d0920eb205df31ef0c0f2411cbb756e0a838e -size 32106 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png deleted file mode 100644 index b7201b0739..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:3a1473e95cf2436f5e42fb1ca2d7bbd40915be830f012b96ad47801ee1fd70a2 -size 24580 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png deleted file mode 100644 index b20d358a12..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e338c8c2d3f89f3b7fdeafc5afffb36399855155e2b85231349e13e381e4cf1f -size 23617 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png deleted file mode 100644 index 4313439757..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:dea12dfe1c1799b1d2d765214b1bd911154a19a7794bf37c6663d179cec76c81 -size 19556 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png deleted file mode 100644 index ed81ad1335..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.root_null_DefaultGroup_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1dc7d39a9b4e0f3eee8157b0290146b6bae5e6d0ea280296ddfc7051f31b95df -size 24786 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d7f8ebf657 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0cf16abe33c78fbe4db6c447fb54a4c6f4495e02ab13774ddd25bc74c24e785b +size 43758 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cd94f38e06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3712ed6d166f726d4c6f56cdf9208787e7b79aec49900e3e59702e23da07c076 +size 43460 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..406ee49b58 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51897c460fad147f358268ad6b15d6865b6378edd8533d744fe108bc96f5d718 +size 40107 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3346d30e90 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.confirmaccountprovider_null_DefaultGroup_ConfirmAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57ace1181dd3d95bdbc0ec11270951b6b2fce6301a68af88c6cddc428acba022 +size 39533 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4fc7c6de68 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84a19a75922a661d173d1213797112bba2e8faf5093561f336925c2f06ecd17a +size 31799 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9dcfbbebaa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20a03449955f95720f3ef1fcf36d9fd82fb34c5e2efea7515e1dd25b11a5257b +size 31834 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4fc7c6de68 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:84a19a75922a661d173d1213797112bba2e8faf5093561f336925c2f06ecd17a +size 31799 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c9c94638f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:367400d1ff6e9e39a5a27bcd45227c75a13fc3d2a81ca532eda75a07481c025a +size 31472 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0ea591d06f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d33b86a10b237ee11b7fcf757462c9685f2cc10fb30ee1c76f2808306b7bc14c +size 31456 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c9c94638f6 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:367400d1ff6e9e39a5a27bcd45227c75a13fc3d2a81ca532eda75a07481c025a +size 31472 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..74b6c3556e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0456d54912f117e8d07c3023c4a0c10fc4933234937f20a87c79cdc8364c3d1 +size 26891 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..02296060db --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ac1a1bec7cc71e7d1ab8513c98cb315f720fe0a7341e3485fabbaff4134352aa +size 48795 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0facef8667 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62d743d3bfa2744891c6cd2bb4052cccb623b9ff6e99c06ce07948c71212cc54 +size 26598 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..95741de0ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.screens.searchaccountprovider_null_DefaultGroup_SearchAccountProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4935ca32e03ea0687d766f6675af3fb78ee7cfff2c7e32e3557d1065fc91ad40 +size 48404 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index f67e775a44..9e0556cea5 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eed20ed1221b53520bcc92c76a7b45f19134d5896220c109f89914d0d2de8c68 -size 29536 +oid sha256:64e2e959463e616c85dbf84f7e89bf4921ad8bda8b56c3ad5923c97d41ebb17a +size 29109 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 2980892672..8e27acb63e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bff3aaf96ea36cd876e0bb262155f7da0a5e110ab5308872bf43d80fa50da414 -size 26909 +oid sha256:b29de0962ec154558d45759678ae01b4cd79b5b0ca77e44023fa8d4c461375a7 +size 26436 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index de5e56864b..484bf3737d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1d4d57ed7cd0ab3c274d0cb46d70f6a2821fcf7fb2b4350258e675ecad8c964 -size 61153 +oid sha256:c1f43f5652bc1312b0a10b074ef7cf1b630774c9633551da36cf563218cd3812 +size 60953 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 35dbc4a40e..a26fee1b27 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5db9cb1ebafb13c52b0e5f68c7dee3d6d27ce8b9bad0a1f5dd5ce76f4bda4cb -size 61738 +oid sha256:cc770343fe1da082c3f65d6f51f4b69e62751807a5b9217a0c785d720789dad2 +size 61539 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png index abbfcf77d5..d034c99ff3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:102bc04b2e12eb68919c45346f4aab946470e8c9e9848ed59a03ad07b178e1bb -size 32639 +oid sha256:e2c28114a14e7251b70254516bb454d3cf05a183f3b5dcec9a8a8c2913d4d80e +size 32441 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png index c5f846756d..04a4cd408d 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:052f915a61028b2cd9605c07fd13000b09f003744397740cfee7054dbcabdcbd -size 27218 +oid sha256:bbf6e55866620ad7422340380a4a02620840217f6b18cc184cfa4b3c2d15a63d +size 27065 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index b6a5f9ef16..f204672d04 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a9d85fcaba465c6747e933974cf9712953222fe934eb2ef3acd513050c9ab19e -size 29087 +oid sha256:4c3b9167b8b97eed74727902987a91fc1808d91709cef3a935b0fc58dbb4650a +size 29049 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 7053e141af..0c9a371a45 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff5f22bda0c7deaaba6e8006abdd7e0c9ba1562016ae62ad41b6a70e67737fdc -size 26179 +oid sha256:1f1c11cdf296cef8514547cee32fffc53eeb2ba16ab85475e98c66eaaa315f0f +size 26294 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index d1e830a2ce..262a0ccbf9 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6faa634a10bf61fa3b84d396c8572586334a5554187fb9935c472d1a56e02884 -size 59497 +oid sha256:bb425ad24d840d6f85fcbc7f8629f58e2ba9b4f8173e1e728056d3185c2bdd49 +size 59691 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_3,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index 1da6df2b42..9ef4777a07 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_3,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:649cd1ab79165a14c67000ce378fdba8abf3e30b10db306641f6f8d158194f0c -size 59923 +oid sha256:0f5f76169e604b2f2a5ab5dc302b91bdc142fc2a0a17001f78d6d8f63774c3f0 +size 60130 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_4,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_4,NEXUS_5,1.0,en].png index 526c311ff2..a5f8157309 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_4,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a6a7c9e187e4825524e292243dcb2e3c57bab49c750dac25016e279d639c56c -size 32266 +oid sha256:8325684caab3d97fdc240748b85592a7fab64233055fbdb65e18e56786968e40 +size 32388 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_5,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_5,NEXUS_5,1.0,en].png index 78effe0524..e8535fd92c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_5,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.verifysession.impl_null_DefaultGroup_VerifySelfSessionViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f9d94a1d66c85094c425f4054cc3115cab28e3109272657a96be939bbc169cf3 -size 26759 +oid sha256:78e9a0f4d9799f1a526e217398dd885d91bee6f69d4544d2f5799a170fefd02a +size 26971 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..bb4c8a386d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fddc4b662ae2284c9179ccea787dbe4fe7cf4923bad81cdbaf891e8149b7131e +size 8083 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e4993027cc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.atoms_null_DefaultGroup_RoundedIconAtomLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e46889dfdfcd8a9999992afcc75d73ea5dd8107a4f5fc19335f9022335c6fc7d +size 6834 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png index 9f2b08ab8d..97f45a7e34 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:73d4db2c6cdd12c05672affc584c75161aa481fd0065c1b084f32ed4c3481c66 -size 10605 +oid sha256:d71452355496c1fe8214f061f72baf3c7690953138777952cdb65befdc133e57 +size 10407 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png index 010dd2b941..70d1d4e355 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.atomic.molecules_null_DefaultGroup_IconTitleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7aadaccb2874346a08bbdcf2cd8d51fd1490319e10c22065611c70543ce8d28d -size 9695 +oid sha256:b508532be8dd29f60e30c00923834ac21ff0664cf3521fb86cdcdd493e9bfff7 +size 9930 diff --git a/tools/check/forbidden_strings_in_code.txt b/tools/check/forbidden_strings_in_code.txt index ae346c170d..41a6c16466 100755 --- a/tools/check/forbidden_strings_in_code.txt +++ b/tools/check/forbidden_strings_in_code.txt @@ -129,3 +129,6 @@ System\.currentTimeMillis\(\)===1 ### Suspicious String template. Please check that the string template will behave as expected, i.e. the class field and not the whole object will be used. For instance `Timber.d("$event.type")` is not correct, you should write `Timber.d("${event.type}")`. In the former the whole event content will be logged, since it's a data class. If this is expected (i.e. to fix false positive), please add explicit curly braces (`{` and `}`) around the variable, for instance `"elementLogs.${i}.txt"` \$[a-zA-Z_]\w*\??\.[a-zA-Z_] + +### Use `import io.element.android.libraries.ui.strings.R as StringsR` then `StringR.string.` instead +io\.element\.android\.libraries\.ui\.strings\.R\. diff --git a/tools/localazy/config.json b/tools/localazy/config.json index a23d652cd8..a7317af0db 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -73,7 +73,10 @@ "name": ":features:login:impl", "includeRegex": [ "screen_login_.*", - "screen_change_server_.*" + "screen_server_confirmation_.*", + "screen_change_server_.*", + "screen_change_account_provider_.*", + "screen_account_provider_.*" ] }, {