Account provider form screen.
This commit is contained in:
parent
7ac8843bf8
commit
d8db9edafc
25 changed files with 810 additions and 128 deletions
|
|
@ -19,7 +19,7 @@ plugins {
|
||||||
alias(libs.plugins.anvil)
|
alias(libs.plugins.anvil)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
id("kotlin-parcelize")
|
id("kotlin-parcelize")
|
||||||
}
|
kotlin("plugin.serialization") version "1.8.21"}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "io.element.android.features.login.impl"
|
namespace = "io.element.android.features.login.impl"
|
||||||
|
|
@ -41,11 +41,15 @@ dependencies {
|
||||||
implementation(projects.libraries.core)
|
implementation(projects.libraries.core)
|
||||||
implementation(projects.libraries.architecture)
|
implementation(projects.libraries.architecture)
|
||||||
implementation(projects.libraries.matrix.api)
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.matrix.api)
|
||||||
|
implementation(projects.libraries.network)
|
||||||
implementation(projects.libraries.designsystem)
|
implementation(projects.libraries.designsystem)
|
||||||
implementation(projects.libraries.elementresources)
|
implementation(projects.libraries.elementresources)
|
||||||
implementation(projects.libraries.testtags)
|
implementation(projects.libraries.testtags)
|
||||||
implementation(projects.libraries.uiStrings)
|
implementation(projects.libraries.uiStrings)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
|
implementation(libs.network.retrofit)
|
||||||
|
implementation(libs.serialization.json)
|
||||||
api(projects.features.login.api)
|
api(projects.features.login.api)
|
||||||
ksp(libs.showkase.processor)
|
ksp(libs.showkase.processor)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,9 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
import io.element.android.features.login.impl.accountprovider.AccountProviderNode
|
import io.element.android.features.login.impl.accountprovider.AccountProviderNode
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.ChangeAccountProviderNode
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.form.ChangeAccountProviderFormNode
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerNode
|
import io.element.android.features.login.impl.changeserver.ChangeServerNode
|
||||||
import io.element.android.features.login.impl.oidc.CustomTabAvailabilityChecker
|
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.customtab.CustomTabHandler
|
||||||
|
|
@ -82,6 +85,9 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object ChangeAccountProvider : NavTarget
|
object ChangeAccountProvider : NavTarget
|
||||||
|
|
||||||
|
@Parcelize
|
||||||
|
object ChangeAccountProviderForm : NavTarget
|
||||||
|
|
||||||
// Not used anymore
|
// Not used anymore
|
||||||
@Parcelize
|
@Parcelize
|
||||||
object ChangeServer : NavTarget
|
object ChangeServer : NavTarget
|
||||||
|
|
@ -134,7 +140,26 @@ class LoginFlowNode @AssistedInject constructor(
|
||||||
createNode<AccountProviderNode>(buildContext, plugins = listOf(inputs, callback))
|
createNode<AccountProviderNode>(buildContext, plugins = listOf(inputs, callback))
|
||||||
}
|
}
|
||||||
NavTarget.ChangeAccountProvider -> {
|
NavTarget.ChangeAccountProvider -> {
|
||||||
TODO()
|
val callback = object : ChangeAccountProviderNode.Callback {
|
||||||
|
override fun onAccountProviderItemClicked(data: AccountProviderItem) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOtherClicked() {
|
||||||
|
backstack.push(NavTarget.ChangeAccountProviderForm)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createNode<ChangeAccountProviderNode>(buildContext, plugins = listOf(callback))
|
||||||
|
}
|
||||||
|
NavTarget.ChangeAccountProviderForm -> {
|
||||||
|
val callback = object : ChangeAccountProviderFormNode.Callback {
|
||||||
|
override fun onAccountProviderItemClicked(data: AccountProviderItem) {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createNode<ChangeAccountProviderFormNode>(buildContext, plugins = listOf(callback))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,8 @@ fun AccountProviderView(
|
||||||
R.string.screen_account_provider_signup_title
|
R.string.screen_account_provider_signup_title
|
||||||
} else {
|
} else {
|
||||||
R.string.screen_account_provider_signin_title
|
R.string.screen_account_provider_signin_title
|
||||||
}
|
},
|
||||||
|
state.homeserver
|
||||||
),
|
),
|
||||||
subTitle = stringResource(
|
subTitle = stringResource(
|
||||||
id = if (state.isAccountCreation) {
|
id = if (state.isAccountCreation) {
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,4 @@
|
||||||
package io.element.android.features.login.impl.changeaccountprovider
|
package io.element.android.features.login.impl.changeaccountprovider
|
||||||
|
|
||||||
sealed interface ChangeAccountProviderEvents {
|
sealed interface ChangeAccountProviderEvents {
|
||||||
data class SetServer(val server: String) : ChangeAccountProviderEvents
|
|
||||||
object Submit : ChangeAccountProviderEvents
|
|
||||||
object ClearError : ChangeAccountProviderEvents
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ import androidx.compose.ui.Modifier
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
import com.bumble.appyx.core.plugin.Plugin
|
import com.bumble.appyx.core.plugin.Plugin
|
||||||
|
import com.bumble.appyx.core.plugin.plugins
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.anvilannotations.ContributesNode
|
import io.element.android.anvilannotations.ContributesNode
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
|
||||||
import io.element.android.libraries.di.AppScope
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
|
|
@ -33,14 +35,28 @@ class ChangeAccountProviderNode @AssistedInject constructor(
|
||||||
private val presenter: ChangeAccountProviderPresenter,
|
private val presenter: ChangeAccountProviderPresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onAccountProviderItemClicked(data: AccountProviderItem)
|
||||||
|
fun onOtherClicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAccountProviderItemClicked(data: AccountProviderItem) {
|
||||||
|
plugins<Callback>().forEach { it.onAccountProviderItemClicked(data) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onOtherClicked() {
|
||||||
|
plugins<Callback>().forEach { it.onOtherClicked() }
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state = presenter.present()
|
val state = presenter.present()
|
||||||
ChangeAccountProviderView(
|
ChangeAccountProviderView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
// TODO
|
onBackPressed = ::navigateUp,
|
||||||
onBackPressed = {}
|
onAccountProviderItemClicked = ::onAccountProviderItemClicked,
|
||||||
|
onOtherProviderClicked = ::onOtherClicked,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,63 +17,25 @@
|
||||||
package io.element.android.features.login.impl.changeaccountprovider
|
package io.element.android.features.login.impl.changeaccountprovider
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.MutableState
|
import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
|
||||||
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.changeserver.ChangeServerError
|
|
||||||
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.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
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ChangeAccountProviderPresenter @Inject constructor(
|
class ChangeAccountProviderPresenter @Inject constructor(
|
||||||
private val authenticationService: MatrixAuthenticationService
|
|
||||||
) : Presenter<ChangeAccountProviderState> {
|
) : Presenter<ChangeAccountProviderState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(): ChangeAccountProviderState {
|
override fun present(): ChangeAccountProviderState {
|
||||||
val localCoroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val homeserver = rememberSaveable {
|
|
||||||
mutableStateOf(authenticationService.getHomeserverDetails().value?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL)
|
|
||||||
}
|
|
||||||
val changeServerAction: MutableState<Async<Unit>> = remember {
|
|
||||||
mutableStateOf(Async.Uninitialized)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun handleEvents(event: ChangeAccountProviderEvents) {
|
|
||||||
when (event) {
|
|
||||||
is ChangeAccountProviderEvents.SetServer -> {
|
|
||||||
homeserver.value = event.server
|
|
||||||
handleEvents(ChangeAccountProviderEvents.ClearError)
|
|
||||||
}
|
|
||||||
ChangeAccountProviderEvents.Submit -> {
|
|
||||||
localCoroutineScope.submit(homeserver, changeServerAction)
|
|
||||||
}
|
|
||||||
ChangeAccountProviderEvents.ClearError -> changeServerAction.value = Async.Uninitialized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ChangeAccountProviderState(
|
return ChangeAccountProviderState(
|
||||||
homeserver = homeserver.value,
|
// Just matrix.org by default for now
|
||||||
changeServerAction = changeServerAction.value,
|
accountProviderItems = listOf(
|
||||||
eventSink = ::handleEvents
|
AccountProviderItem(
|
||||||
|
title = "matrix.org",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = true,
|
||||||
|
isMatrixOrg = true,
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.submit(homeserverUrl: MutableState<String>, changeServerAction: MutableState<Async<Unit>>) = launch {
|
|
||||||
suspend {
|
|
||||||
val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value
|
|
||||||
authenticationService.setHomeserver(domain).getOrThrow()
|
|
||||||
homeserverUrl.value = domain
|
|
||||||
}.execute(changeServerAction, errorMapping = ChangeServerError::from)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.changeaccountprovider
|
package io.element.android.features.login.impl.changeaccountprovider
|
||||||
|
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
|
||||||
|
|
||||||
// Do not use default value, so no member get forgotten in the presenters.
|
// Do not use default value, so no member get forgotten in the presenters.
|
||||||
data class ChangeAccountProviderState(
|
data class ChangeAccountProviderState constructor(
|
||||||
val homeserver: String,
|
val accountProviderItems: List<AccountProviderItem>,
|
||||||
val changeServerAction: Async<Unit>,
|
)
|
||||||
val eventSink: (ChangeAccountProviderEvents) -> Unit
|
|
||||||
) {
|
|
||||||
// TODO Remove
|
|
||||||
val submitEnabled: Boolean get() = changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
package io.element.android.features.login.impl.changeaccountprovider
|
package io.element.android.features.login.impl.changeaccountprovider
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
import io.element.android.libraries.architecture.Async
|
import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
|
||||||
|
|
||||||
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeAccountProviderState> {
|
||||||
override val values: Sequence<ChangeAccountProviderState>
|
override val values: Sequence<ChangeAccountProviderState>
|
||||||
|
|
@ -28,7 +28,12 @@ open class ChangeAccountProviderStateProvider : PreviewParameterProvider<ChangeA
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aChangeAccountProviderState() = ChangeAccountProviderState(
|
fun aChangeAccountProviderState() = ChangeAccountProviderState(
|
||||||
homeserver = "",
|
accountProviderItems = listOf(
|
||||||
changeServerAction = Async.Uninitialized,
|
AccountProviderItem(
|
||||||
eventSink = {}
|
title = "matrix.org",
|
||||||
|
subtitle = null,
|
||||||
|
isPublic = true,
|
||||||
|
isMatrixOrg = true,
|
||||||
|
)
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,21 +34,14 @@ import androidx.compose.material.icons.filled.Home
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
import androidx.compose.ui.platform.LocalFocusManager
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.element.android.features.login.impl.R
|
import io.element.android.features.login.impl.R
|
||||||
import io.element.android.features.login.impl.changeaccountprovider.item.ChangeAccountProviderItem
|
import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem
|
||||||
import io.element.android.features.login.impl.changeaccountprovider.item.ChangeAccountProviderItemView
|
import io.element.android.features.login.impl.changeaccountprovider.item.ChangeAccountProviderItemView
|
||||||
import io.element.android.features.login.impl.changeserver.ChangeServerError
|
|
||||||
import io.element.android.features.login.impl.changeserver.SlidingSyncNotSupportedDialog
|
|
||||||
import io.element.android.libraries.architecture.Async
|
|
||||||
import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule
|
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.BackButton
|
||||||
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
|
||||||
|
|
@ -64,26 +57,10 @@ fun ChangeAccountProviderView(
|
||||||
state: ChangeAccountProviderState,
|
state: ChangeAccountProviderState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit,
|
onBackPressed: () -> Unit,
|
||||||
|
onAccountProviderItemClicked: (AccountProviderItem) -> Unit = {},
|
||||||
onOtherProviderClicked: () -> Unit = {},
|
onOtherProviderClicked: () -> Unit = {},
|
||||||
onChangeServerSuccess: () -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
val eventSink = state.eventSink
|
|
||||||
val scrollState = rememberScrollState()
|
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(ChangeAccountProviderEvents.Submit)
|
|
||||||
}
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -115,34 +92,30 @@ fun ChangeAccountProviderView(
|
||||||
subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle),
|
subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (slidingSyncNotSupportedError != null) {
|
state.accountProviderItems.forEach { item ->
|
||||||
SlidingSyncNotSupportedDialog(onLearnMoreClicked = {
|
val alteredItem = if (item.isMatrixOrg) {
|
||||||
eventSink(ChangeAccountProviderEvents.ClearError)
|
// Set the subtitle from the resource
|
||||||
}, onDismiss = {
|
item.copy(
|
||||||
eventSink(ChangeAccountProviderEvents.ClearError)
|
subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle),
|
||||||
})
|
)
|
||||||
}
|
} else {
|
||||||
ChangeAccountProviderItemView(
|
item
|
||||||
item = ChangeAccountProviderItem(
|
|
||||||
title = "matrix.org",
|
|
||||||
subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle),
|
|
||||||
isPublic = true,
|
|
||||||
isMatrix = true,
|
|
||||||
),
|
|
||||||
onClick = {
|
|
||||||
TODO()
|
|
||||||
}
|
}
|
||||||
)
|
ChangeAccountProviderItemView(
|
||||||
|
item = alteredItem,
|
||||||
|
onClick = {
|
||||||
|
onAccountProviderItemClicked(alteredItem)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Other
|
||||||
ChangeAccountProviderItemView(
|
ChangeAccountProviderItemView(
|
||||||
item = ChangeAccountProviderItem(
|
item = AccountProviderItem(
|
||||||
title = stringResource(id = R.string.screen_change_account_provider_other),
|
title = stringResource(id = R.string.screen_change_account_provider_other),
|
||||||
),
|
),
|
||||||
onClick = onOtherProviderClicked
|
onClick = onOtherProviderClicked
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
if (state.changeServerAction is Async.Success) {
|
|
||||||
onChangeServerSuccess()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.changeaccountprovider.form
|
||||||
|
|
||||||
|
sealed interface ChangeAccountProviderFormEvents {
|
||||||
|
/**
|
||||||
|
* The user has typed something, expect to get a list of result in the state
|
||||||
|
*/
|
||||||
|
data class UserInput(val input: String) : ChangeAccountProviderFormEvents
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
* 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.changeaccountprovider.form
|
||||||
|
|
||||||
|
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 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.changeaccountprovider.item.AccountProviderItem
|
||||||
|
import io.element.android.libraries.di.AppScope
|
||||||
|
|
||||||
|
@ContributesNode(AppScope::class)
|
||||||
|
class ChangeAccountProviderFormNode @AssistedInject constructor(
|
||||||
|
@Assisted buildContext: BuildContext,
|
||||||
|
@Assisted plugins: List<Plugin>,
|
||||||
|
private val presenter: ChangeAccountProviderFormPresenter,
|
||||||
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
|
interface Callback : Plugin {
|
||||||
|
fun onAccountProviderItemClicked(data: AccountProviderItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onAccountProviderItemClicked(data: AccountProviderItem) {
|
||||||
|
plugins<Callback>().forEach { it.onAccountProviderItemClicked(data) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun View(modifier: Modifier) {
|
||||||
|
val state = presenter.present()
|
||||||
|
ChangeAccountProviderFormView(
|
||||||
|
state = state,
|
||||||
|
modifier = modifier,
|
||||||
|
onBackPressed = ::navigateUp,
|
||||||
|
onProviderClicked = ::onAccountProviderItemClicked
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
/*
|
||||||
|
* 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.changeaccountprovider.form
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.libraries.architecture.Async
|
||||||
|
import io.element.android.libraries.architecture.Presenter
|
||||||
|
import io.element.android.libraries.architecture.execute
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ChangeAccountProviderFormPresenter @Inject constructor(
|
||||||
|
private val homeserverResolver: HomeserverResolver,
|
||||||
|
) : Presenter<ChangeAccountProviderFormState> {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun present(): ChangeAccountProviderFormState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var currentJob: Job? = remember { null }
|
||||||
|
|
||||||
|
val userInput = rememberSaveable {
|
||||||
|
mutableStateOf("")
|
||||||
|
}
|
||||||
|
val userInputResult: MutableState<Async<List<HomeserverData>>> = remember {
|
||||||
|
mutableStateOf(Async.Uninitialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: ChangeAccountProviderFormEvents) {
|
||||||
|
when (event) {
|
||||||
|
is ChangeAccountProviderFormEvents.UserInput -> {
|
||||||
|
currentJob?.cancel()
|
||||||
|
currentJob = localCoroutineScope.userInput(event.input, userInputResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ChangeAccountProviderFormState(
|
||||||
|
userInput = userInput.value,
|
||||||
|
userInputResult = userInputResult.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun CoroutineScope.userInput(userInput: String, userInputResult: MutableState<Async<List<HomeserverData>>>) = launch {
|
||||||
|
suspend {
|
||||||
|
homeserverResolver.resolve(userInput)
|
||||||
|
}.execute(userInputResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,202 @@
|
||||||
|
/*
|
||||||
|
* 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, ExperimentalMaterial3Api::class)
|
||||||
|
|
||||||
|
package io.element.android.features.login.impl.changeaccountprovider.form
|
||||||
|
|
||||||
|
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.rememberScrollState
|
||||||
|
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.material.icons.filled.Home
|
||||||
|
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.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.changeaccountprovider.item.AccountProviderItem
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.item.ChangeAccountProviderItemView
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ChangeAccountProviderFormView(
|
||||||
|
state: ChangeAccountProviderFormState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackPressed: () -> Unit,
|
||||||
|
onProviderClicked: (AccountProviderItem) -> Unit = {},
|
||||||
|
) {
|
||||||
|
val eventSink = state.eventSink
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
|
||||||
|
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 = scrollState,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
IconTitleSubtitleMolecule(
|
||||||
|
modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp),
|
||||||
|
iconImageVector = Icons.Filled.Home,
|
||||||
|
iconTint = MaterialTheme.colorScheme.primary,
|
||||||
|
title = stringResource(id = R.string.screen_account_provider_form_title),
|
||||||
|
subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle),
|
||||||
|
)
|
||||||
|
|
||||||
|
// TextInput
|
||||||
|
var userInputState by textFieldState(stateValue = state.userInput)
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = userInputState,
|
||||||
|
// readOnly = isLoading,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 16.dp, bottom = 30.dp),
|
||||||
|
onValueChange = {
|
||||||
|
userInputState = it
|
||||||
|
eventSink(ChangeAccountProviderFormEvents.UserInput(it))
|
||||||
|
},
|
||||||
|
keyboardOptions = KeyboardOptions(
|
||||||
|
keyboardType = KeyboardType.Uri,
|
||||||
|
imeAction = ImeAction.Done,
|
||||||
|
),
|
||||||
|
singleLine = true,
|
||||||
|
maxLines = 1,
|
||||||
|
trailingIcon = if (userInputState.isNotEmpty()) {
|
||||||
|
{
|
||||||
|
IconButton(onClick = {
|
||||||
|
userInputState = ""
|
||||||
|
eventSink(ChangeAccountProviderFormEvents.UserInput(""))
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Filled.Close,
|
||||||
|
contentDescription = stringResource(io.element.android.libraries.ui.strings.R.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 -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Async.Success -> {
|
||||||
|
state.userInputResult.state.forEach { homeserverData ->
|
||||||
|
val isMatrixOrg = homeserverData.homeserverUrl == "https://matrix.org"
|
||||||
|
val item = AccountProviderItem(
|
||||||
|
title = homeserverData.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,
|
||||||
|
)
|
||||||
|
ChangeAccountProviderItemView(
|
||||||
|
item = item,
|
||||||
|
onClick = {
|
||||||
|
onProviderClicked(item)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Async.Uninitialized -> Unit
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(32.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ChangeAccountProviderFormViewLightPreview(@PreviewParameter(ChangeAccountProviderStateFormProvider::class) state: ChangeAccountProviderFormState) =
|
||||||
|
ElementPreviewLight { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ChangeAccountProviderFormViewDarkPreview(@PreviewParameter(ChangeAccountProviderStateFormProvider::class) state: ChangeAccountProviderFormState) =
|
||||||
|
ElementPreviewDark { ContentToPreview(state) }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ContentToPreview(state: ChangeAccountProviderFormState) {
|
||||||
|
ChangeAccountProviderFormView(
|
||||||
|
state = state,
|
||||||
|
onBackPressed = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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.changeaccountprovider.form
|
||||||
|
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
|
// Do not use default value, so no member get forgotten in the presenters.
|
||||||
|
data class ChangeAccountProviderFormState(
|
||||||
|
val userInput: String,
|
||||||
|
val userInputResult: Async<List<HomeserverData>>,
|
||||||
|
val eventSink: (ChangeAccountProviderFormEvents) -> Unit
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
/*
|
||||||
|
* 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.changeaccountprovider.form
|
||||||
|
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
import io.element.android.libraries.architecture.Async
|
||||||
|
|
||||||
|
open class ChangeAccountProviderStateFormProvider : PreviewParameterProvider<ChangeAccountProviderFormState> {
|
||||||
|
override val values: Sequence<ChangeAccountProviderFormState>
|
||||||
|
get() = sequenceOf(
|
||||||
|
aChangeAccountProviderFormState(),
|
||||||
|
aChangeAccountProviderFormState(userInputResult = Async.Success(aHomeserverDataList())),
|
||||||
|
// Add other state here
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun aChangeAccountProviderFormState(
|
||||||
|
userInput: String = "",
|
||||||
|
userInputResult: Async<List<HomeserverData>> = Async.Uninitialized,
|
||||||
|
) = ChangeAccountProviderFormState(
|
||||||
|
userInput = userInput,
|
||||||
|
userInputResult = userInputResult,
|
||||||
|
eventSink = {}
|
||||||
|
)
|
||||||
|
|
||||||
|
fun aHomeserverDataList(): List<HomeserverData> {
|
||||||
|
return listOf(
|
||||||
|
HomeserverData(
|
||||||
|
userInput = "matrix",
|
||||||
|
homeserverUrl = "https://matrix.org",
|
||||||
|
isWellknownValid = true,
|
||||||
|
),
|
||||||
|
HomeserverData(
|
||||||
|
userInput = "matrix",
|
||||||
|
homeserverUrl = "https://matrix.io",
|
||||||
|
isWellknownValid = false,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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.changeaccountprovider.form
|
||||||
|
|
||||||
|
data class HomeserverData(
|
||||||
|
// What the user has entered
|
||||||
|
val userInput: String,
|
||||||
|
// 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,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
/*
|
||||||
|
* 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.changeaccountprovider.form
|
||||||
|
|
||||||
|
import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest
|
||||||
|
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
|
||||||
|
import io.element.android.libraries.core.uri.ensureProtocol
|
||||||
|
import io.element.android.libraries.core.uri.isValidUrl
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.joinAll
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
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): List<HomeserverData> {
|
||||||
|
return withContext(dispatchers.io) {
|
||||||
|
val cleanedUpUserInput = userInput.trim()
|
||||||
|
if (cleanedUpUserInput.length < 4) {
|
||||||
|
// Wait for more chars
|
||||||
|
emptyList()
|
||||||
|
} else {
|
||||||
|
val list = getUrlCandidate(cleanedUpUserInput)
|
||||||
|
val resolvedList = resolveList(userInput, list)
|
||||||
|
|
||||||
|
// If list is empty, and the user as entered an URL, do not block the user.
|
||||||
|
if (resolvedList.isEmpty() && userInput.isValidUrl()) {
|
||||||
|
listOf(
|
||||||
|
HomeserverData(
|
||||||
|
userInput = userInput,
|
||||||
|
homeserverUrl = userInput,
|
||||||
|
isWellknownValid = false
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
resolvedList
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveList(userInput: String, list: List<String>): List<HomeserverData> {
|
||||||
|
return coroutineScope {
|
||||||
|
buildList {
|
||||||
|
list.map {
|
||||||
|
async {
|
||||||
|
val isValid = wellknownRequest.execute(it)
|
||||||
|
if (isValid) {
|
||||||
|
add(HomeserverData(userInput, it, true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.joinAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUrlCandidate(data: String): List<String> {
|
||||||
|
return buildList {
|
||||||
|
val s = data.ensureProtocol()
|
||||||
|
.removeSuffix("/")
|
||||||
|
|
||||||
|
// Always try what the user has entered
|
||||||
|
add(s)
|
||||||
|
|
||||||
|
if (s.contains(".")) {
|
||||||
|
// TLD detected?
|
||||||
|
} else {
|
||||||
|
add("$s.org")
|
||||||
|
add("$s.com")
|
||||||
|
add("$s.io")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.changeaccountprovider.form.network
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||||
|
* <pre>
|
||||||
|
* {
|
||||||
|
* "m.homeserver": {
|
||||||
|
* "base_url": "https://matrix.org"
|
||||||
|
* },
|
||||||
|
* "m.identity_server": {
|
||||||
|
* "base_url": "https://vector.im"
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* .
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class WellKnown(
|
||||||
|
@SerialName("m.homeserver")
|
||||||
|
val homeServer: WellKnownBaseConfig? = null,
|
||||||
|
|
||||||
|
@SerialName("m.identity_server")
|
||||||
|
val identityServer: WellKnownBaseConfig? = null,
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||||
|
*
|
||||||
|
* 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.changeaccountprovider.form.network
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
|
||||||
|
* <pre>
|
||||||
|
* {
|
||||||
|
* "base_url": "https://element.io"
|
||||||
|
* }
|
||||||
|
* </pre>
|
||||||
|
* .
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class WellKnownBaseConfig(
|
||||||
|
@SerialName("base_url")
|
||||||
|
val baseURL: String? = null
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
/*
|
||||||
|
* 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.changeaccountprovider.form.network
|
||||||
|
|
||||||
|
import retrofit2.http.GET
|
||||||
|
|
||||||
|
internal interface WellknownAPI {
|
||||||
|
@GET(".well-known/matrix/client")
|
||||||
|
suspend fun getWellKnown(): WellKnown
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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.changeaccountprovider.form.network
|
||||||
|
|
||||||
|
import io.element.android.libraries.core.bool.orFalse
|
||||||
|
import io.element.android.libraries.network.RetrofitFactory
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class WellknownRequest @Inject constructor(
|
||||||
|
private val retrofitFactory: RetrofitFactory,
|
||||||
|
) {
|
||||||
|
/**
|
||||||
|
* Return true if the wellknown can be retrieved and is valid
|
||||||
|
* @param baseUrl for instance https://matrix.org
|
||||||
|
*/
|
||||||
|
suspend fun execute(baseUrl: String): Boolean {
|
||||||
|
val wellknownApi = retrofitFactory.create(baseUrl)
|
||||||
|
.create(WellknownAPI::class.java)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val response = wellknownApi.getWellKnown()
|
||||||
|
response.isValid()
|
||||||
|
} catch (throwable: Throwable) {
|
||||||
|
Timber.e(throwable)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WellKnown.isValid(): Boolean {
|
||||||
|
return homeServer?.baseURL?.isNotBlank().orFalse()
|
||||||
|
}
|
||||||
|
|
@ -16,9 +16,9 @@
|
||||||
|
|
||||||
package io.element.android.features.login.impl.changeaccountprovider.item
|
package io.element.android.features.login.impl.changeaccountprovider.item
|
||||||
|
|
||||||
data class ChangeAccountProviderItem(
|
data class AccountProviderItem constructor(
|
||||||
val title: String,
|
val title: String,
|
||||||
val subtitle: String? = null,
|
val subtitle: String? = null,
|
||||||
val isPublic: Boolean = false,
|
val isPublic: Boolean = false,
|
||||||
val isMatrix: Boolean = false,
|
val isMatrixOrg: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
@ -18,19 +18,19 @@ package io.element.android.features.login.impl.changeaccountprovider.item
|
||||||
|
|
||||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||||
|
|
||||||
open class ChangeAccountProviderItemProvider : PreviewParameterProvider<ChangeAccountProviderItem> {
|
open class ChangeAccountProviderItemProvider : PreviewParameterProvider<AccountProviderItem> {
|
||||||
override val values: Sequence<ChangeAccountProviderItem>
|
override val values: Sequence<AccountProviderItem>
|
||||||
get() = sequenceOf(
|
get() = sequenceOf(
|
||||||
aChangeAccountProviderItem(),
|
aChangeAccountProviderItem(),
|
||||||
aChangeAccountProviderItem().copy(subtitle = null),
|
aChangeAccountProviderItem().copy(subtitle = null),
|
||||||
aChangeAccountProviderItem().copy(title = "Other", subtitle = null, isPublic = false, isMatrix = false),
|
aChangeAccountProviderItem().copy(title = "Other", subtitle = null, isPublic = false, isMatrixOrg = false),
|
||||||
// Add other state here
|
// Add other state here
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun aChangeAccountProviderItem() = ChangeAccountProviderItem(
|
fun aChangeAccountProviderItem() = AccountProviderItem(
|
||||||
title = "matrix.org",
|
title = "matrix.org",
|
||||||
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
subtitle = "Matrix.org is an open network for secure, decentralized communication.",
|
||||||
isPublic = true,
|
isPublic = true,
|
||||||
isMatrix = true,
|
isMatrixOrg = true,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangeAccountProviderItemView(
|
fun ChangeAccountProviderItemView(
|
||||||
item: ChangeAccountProviderItem,
|
item: AccountProviderItem,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -68,7 +68,7 @@ fun ChangeAccountProviderItemView(
|
||||||
.heightIn(min = 44.dp),
|
.heightIn(min = 44.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
if (item.isMatrix) {
|
if (item.isMatrixOrg) {
|
||||||
RoundedIconAtom(
|
RoundedIconAtom(
|
||||||
size = RoundedIconAtomSize.Medium,
|
size = RoundedIconAtomSize.Medium,
|
||||||
resourceId = R.drawable.ic_matrix,
|
resourceId = R.drawable.ic_matrix,
|
||||||
|
|
@ -115,16 +115,16 @@ fun ChangeAccountProviderItemView(
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: ChangeAccountProviderItem) =
|
fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: AccountProviderItem) =
|
||||||
ElementPreviewLight { ContentToPreview(item) }
|
ElementPreviewLight { ContentToPreview(item) }
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: ChangeAccountProviderItem) =
|
fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: AccountProviderItem) =
|
||||||
ElementPreviewDark { ContentToPreview(item) }
|
ElementPreviewDark { ContentToPreview(item) }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ContentToPreview(item: ChangeAccountProviderItem) {
|
private fun ContentToPreview(item: AccountProviderItem) {
|
||||||
ChangeAccountProviderItemView(
|
ChangeAccountProviderItemView(
|
||||||
item = item,
|
item = item,
|
||||||
onClick = { }
|
onClick = { }
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||||
<string name="screen_account_provider_change">"Change account provider"</string>
|
<string name="screen_account_provider_change">"Change account provider"</string>
|
||||||
<string name="screen_account_provider_continue">"Continue"</string>
|
<string name="screen_account_provider_continue">"Continue"</string>
|
||||||
|
<string name="screen_account_provider_form_hint">"Homeserver address"</string>
|
||||||
|
<string name="screen_account_provider_form_notice">"Enter a search term or a domain address."</string>
|
||||||
|
<string name="screen_account_provider_form_subtitle">"Search for a company, community, or private server."</string>
|
||||||
|
<string name="screen_account_provider_form_title">"Find an account provider"</string>
|
||||||
<string name="screen_account_provider_signin_title">"You’re about to sign in to %s"</string>
|
<string name="screen_account_provider_signin_title">"You’re about to sign in to %s"</string>
|
||||||
<string name="screen_account_provider_signup_subtitle">"This is where you conversations will live — just like you would use an email provider to keep your emails."</string>
|
<string name="screen_account_provider_signup_subtitle">"This is where you conversations will live — just like you would use an email provider to keep your emails."</string>
|
||||||
<string name="screen_account_provider_signup_title">"You’re about to create an account on %s"</string>
|
<string name="screen_account_provider_signup_title">"You’re about to create an account on %s"</string>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue