From 937e616319012554e6b5f922c1ff09c502e4de36 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 5 Jun 2023 15:03:48 +0200 Subject: [PATCH 001/130] Improve composable for icons and create RoundedIconAtom. --- .../atomic/atoms/RoundedIconAtom.kt | 115 ++++++++++++++++++ .../molecules/IconTitleSubtitleMolecule.kt | 38 +++--- .../designsystem/theme/components/Icon.kt | 37 ++++++ 3 files changed, 166 insertions(+), 24 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt 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..e73af11254 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/RoundedIconAtom.kt @@ -0,0 +1,115 @@ +/* + * 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.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 + +enum class RoundedIconAtomSize { + Medium, + Large +} + +@Composable +fun RoundedIconAtom( + modifier: Modifier = Modifier, + size: RoundedIconAtomSize = RoundedIconAtomSize.Large, + resourceId: Int? = null, + imageVector: ImageVector? = null, +) { + 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 = MaterialTheme.colorScheme.secondary, + 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, + ) + } +} 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..171e3655a0 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,45 @@ 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.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 +/** + * Provide either an `iconResourceId` or an `iconImageVector` + */ @Composable fun IconTitleSubtitleMolecule( - iconResourceId: Int, title: String, subTitle: String, modifier: Modifier = Modifier, + iconResourceId: Int? = null, + iconImageVector: ImageVector? = null, ) { 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, + ) Spacer(modifier = Modifier.height(16.dp)) Text( text = title, 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..1a9bfe3a0a 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,43 @@ import androidx.compose.ui.tooling.preview.Preview import io.element.android.libraries.designsystem.preview.ElementThemedPreview import io.element.android.libraries.designsystem.preview.PreviewGroup +@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, From 96f9eb839774a10a1dea5e03ad29fd4bf5e36f0f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 5 Jun 2023 17:52:16 +0200 Subject: [PATCH 002/130] Change account provider screen. --- .../ChangeAccountProviderEvents.kt | 23 +++ .../ChangeAccountProviderNode.kt | 46 +++++ .../ChangeAccountProviderPresenter.kt | 79 ++++++++ .../ChangeAccountProviderState.kt | 29 +++ .../ChangeAccountProviderStateProvider.kt | 34 ++++ .../ChangeAccountProviderView.kt | 178 ++++++++++++++++++ .../item/ChangeAccountProviderItem.kt | 24 +++ .../item/ChangeAccountProviderItemProvider.kt | 36 ++++ .../item/ChangeAccountProviderItemView.kt | 131 +++++++++++++ .../impl/src/main/res/drawable/ic_matrix.xml | 12 ++ .../impl/src/main/res/drawable/ic_public.xml | 12 ++ .../impl/src/main/res/values/localazy.xml | 4 + .../atomic/atoms/RoundedIconAtom.kt | 4 +- .../atomic/atoms/SeparatorAtom.kt | 62 ++++++ tools/localazy/config.json | 3 +- 15 files changed, 675 insertions(+), 2 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItem.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt create mode 100644 features/login/impl/src/main/res/drawable/ic_matrix.xml create mode 100644 features/login/impl/src/main/res/drawable/ic_public.xml create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SeparatorAtom.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt new file mode 100644 index 0000000000..73b9929dca --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt @@ -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 + +sealed interface ChangeAccountProviderEvents { + data class SetServer(val server: String) : ChangeAccountProviderEvents + object Submit : ChangeAccountProviderEvents + object ClearError : ChangeAccountProviderEvents +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt new file mode 100644 index 0000000000..00b9d312c0 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt @@ -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 + +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 ChangeAccountProviderNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenter: ChangeAccountProviderPresenter, +) : Node(buildContext, plugins = plugins) { + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ChangeAccountProviderView( + state = state, + modifier = modifier, + // TODO + onBackPressed = {} + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt new file mode 100644 index 0000000000..b4fbdac771 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -0,0 +1,79 @@ +/* + * 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 + +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.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.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 + +class ChangeAccountProviderPresenter @Inject constructor( + private val authenticationService: MatrixAuthenticationService +) : Presenter { + + @Composable + override fun present(): ChangeAccountProviderState { + 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: 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( + homeserver = homeserver.value, + changeServerAction = changeServerAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.submit(homeserverUrl: MutableState, changeServerAction: MutableState>) = launch { + suspend { + val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value + authenticationService.setHomeserver(domain).getOrThrow() + homeserverUrl.value = domain + }.execute(changeServerAction, errorMapping = ChangeServerError::from) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt new file mode 100644 index 0000000000..98f95ca357 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.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.changeaccountprovider + +import io.element.android.libraries.architecture.Async + +// Do not use default value, so no member get forgotten in the presenters. +data class ChangeAccountProviderState( + val homeserver: String, + val changeServerAction: Async, + val eventSink: (ChangeAccountProviderEvents) -> Unit +) { + // TODO Remove + val submitEnabled: Boolean get() = changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt new file mode 100644 index 0000000000..a176de572e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt @@ -0,0 +1,34 @@ +/* + * 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 + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async + +open class ChangeAccountProviderStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aChangeAccountProviderState(), + // Add other state here + ) +} + +fun aChangeAccountProviderState() = ChangeAccountProviderState( + homeserver = "", + changeServerAction = Async.Uninitialized, + eventSink = {} +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt new file mode 100644 index 0000000000..d778bb3c35 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt @@ -0,0 +1,178 @@ +/* + * 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.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.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.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material3.ExperimentalMaterial3Api +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.platform.LocalFocusManager +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.changeaccountprovider.item.ChangeAccountProviderItem +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.components.button.BackButton +import io.element.android.libraries.designsystem.components.button.ButtonWithProgress +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 +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag + +/** + * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 + */ +@Composable +fun ChangeAccountProviderView( + state: ChangeAccountProviderState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit, + onOtherProviderClicked: () -> Unit = {}, + 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(ChangeAccountProviderEvents.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, + ) + ) { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp), + iconImageVector = Icons.Filled.Home, + title = stringResource(id = R.string.screen_change_account_provider_title), + subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle) + ) + + if (slidingSyncNotSupportedError != null) { + SlidingSyncNotSupportedDialog(onLearnMoreClicked = { + eventSink(ChangeAccountProviderEvents.ClearError) + }, onDismiss = { + eventSink(ChangeAccountProviderEvents.ClearError) + }) + } + ChangeAccountProviderItemView( + 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 = ChangeAccountProviderItem( + title = stringResource(id = R.string.screen_change_account_provider_other), + ), + onClick = onOtherProviderClicked + ) + + 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() + } + } + } + } +} + +@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 = { } + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItem.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItem.kt new file mode 100644 index 0000000000..c913923205 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItem.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.changeaccountprovider.item + +data class ChangeAccountProviderItem( + val title: String, + val subtitle: String? = null, + val isPublic: Boolean = false, + val isMatrix: Boolean = false, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt new file mode 100644 index 0000000000..bf7a92cf08 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.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.changeaccountprovider.item + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class ChangeAccountProviderItemProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aChangeAccountProviderItem(), + aChangeAccountProviderItem().copy(subtitle = null), + aChangeAccountProviderItem().copy(title = "Other", subtitle = null, isPublic = false, isMatrix = false), + // Add other state here + ) +} + +fun aChangeAccountProviderItem() = ChangeAccountProviderItem( + title = "matrix.org", + subtitle = "Matrix.org is an open network for secure, decentralized communication.", + isPublic = true, + isMatrix = true, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt new file mode 100644 index 0000000000..96d7a26e7c --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt @@ -0,0 +1,131 @@ +/* + * 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.item + +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.atomic.atoms.SeparatorAtom +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +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 ChangeAccountProviderItemView( + item: ChangeAccountProviderItem, + modifier: Modifier = Modifier, + onClick: () -> Unit, +) { + Column(modifier = modifier + .fillMaxWidth() + .clickable { onClick() }) { + SeparatorAtom() + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp, horizontal = 16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 44.dp), + verticalAlignment = Alignment.CenterVertically + ) { + if (item.isMatrix) { + RoundedIconAtom( + size = RoundedIconAtomSize.Medium, + resourceId = R.drawable.ic_matrix, + tint = Color.Unspecified, + ) + } else { + RoundedIconAtom( + size = RoundedIconAtomSize.Medium, + imageVector = Icons.Filled.Search, + ) + } + 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 ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: ChangeAccountProviderItem) = + ElementPreviewLight { ContentToPreview(item) } + +@Preview +@Composable +fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: ChangeAccountProviderItem) = + ElementPreviewDark { ContentToPreview(item) } + +@Composable +private fun ContentToPreview(item: ChangeAccountProviderItem) { + ChangeAccountProviderItemView( + item = item, + onClick = { } + ) +} 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..39c35ac92f 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -1,5 +1,9 @@ + "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" 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 index e73af11254..01fa2355d3 100644 --- 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 @@ -28,6 +28,7 @@ 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 @@ -48,6 +49,7 @@ fun RoundedIconAtom( size: RoundedIconAtomSize = RoundedIconAtomSize.Large, resourceId: Int? = null, imageVector: ImageVector? = null, + tint: Color = MaterialTheme.colorScheme.secondary ) { Box( modifier = modifier @@ -61,7 +63,7 @@ fun RoundedIconAtom( modifier = Modifier .align(Alignment.Center) .size(size.toIconSize()), - tint = MaterialTheme.colorScheme.secondary, + tint = tint, resourceId = resourceId, imageVector = imageVector, contentDescription = "", diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SeparatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SeparatorAtom.kt new file mode 100644 index 0000000000..fe15ae5480 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SeparatorAtom.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.atomic.atoms + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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 + +@Composable +fun SeparatorAtom( + modifier: Modifier = Modifier, + horizontalPadding: Dp = 0.dp, +) { + Spacer( + modifier = modifier + .height(0.5f.dp) + .fillMaxWidth() + .padding(horizontal = horizontalPadding) + .background( + color = LocalColors.current.quinary, + ) + ) +} + +@Preview +@Composable +internal fun SeparatorAtomLightPreview() = + ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun SeparatorAtomDarkPreview() = + ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + SeparatorAtom(horizontalPadding = 20.dp) +} diff --git a/tools/localazy/config.json b/tools/localazy/config.json index a23d652cd8..23908ecad8 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -73,7 +73,8 @@ "name": ":features:login:impl", "includeRegex": [ "screen_login_.*", - "screen_change_server_.*" + "screen_change_server_.*", + "screen_change_account_provider_.*" ] }, { From 9809969b64b12dcf107925ddba117e6f9323b676 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Jun 2023 10:07:29 +0200 Subject: [PATCH 003/130] Account provider screen. - Crashing --- .../android/appnav/NotLoggedInFlowNode.kt | 14 ++- .../features/login/api/LoginEntryPoint.kt | 17 ++- .../login/impl/DefaultLoginEntryPoint.kt | 17 ++- .../features/login/impl/LoginFlowNode.kt | 41 ++++++- .../accountprovider/AccountProviderEvents.kt | 22 ++++ .../accountprovider/AccountProviderNode.kt | 77 ++++++++++++ .../AccountProviderPresenter.kt | 56 +++++++++ .../accountprovider/AccountProviderState.kt | 25 ++++ .../AccountProviderStateProvider.kt | 34 ++++++ .../accountprovider/AccountProviderView.kt | 113 ++++++++++++++++++ .../ChangeAccountProviderView.kt | 21 +--- .../item/ChangeAccountProviderItemView.kt | 1 + .../impl/src/main/res/values/localazy.xml | 5 + .../onboarding/impl/OnBoardingNode.kt | 1 + .../molecules/IconTitleSubtitleMolecule.kt | 3 + tools/localazy/config.json | 3 +- 16 files changed, 422 insertions(+), 28 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt 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/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/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..ad81d7c441 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 @@ -32,14 +31,18 @@ import com.bumble.appyx.navmodel.backstack.operation.push import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.login.impl.accountprovider.AccountProviderNode 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.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.util.LoginConstants 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 @@ -53,7 +56,7 @@ class LoginFlowNode @AssistedInject constructor( private val customTabHandler: CustomTabHandler, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.Root, + initialElement = NavTarget.AccountProvider, // NavTarget.Root, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -62,10 +65,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 { + // Not used anymore @Parcelize object Root : NavTarget + @Parcelize + object AccountProvider : NavTarget + + @Parcelize + object ChangeAccountProvider : NavTarget + + // Not used anymore @Parcelize object ChangeServer : NavTarget @@ -99,6 +116,26 @@ class LoginFlowNode @AssistedInject constructor( val input = OidcNode.Inputs(navTarget.oidcDetails) createNode(buildContext, plugins = listOf(input)) } + NavTarget.AccountProvider -> { + val inputs = AccountProviderNode.Inputs( + homeserver = LoginConstants.DEFAULT_HOMESERVER_URL, + isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == "matrix.org", + isAccountCreation = inputs.isAccountCreation + ) + val callback = object : AccountProviderNode.Callback { + override fun onContinue() { + TODO("Not yet implemented") + } + + override fun onChangeAccountProvider() { + backstack.push(NavTarget.ChangeAccountProvider) + } + } + createNode(buildContext, plugins = listOf(inputs, callback)) + } + NavTarget.ChangeAccountProvider -> { + TODO() + } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt new file mode 100644 index 0000000000..7a4cbf0366 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.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.accountprovider + +// TODO Add your events or remove the file completely if no events +sealed interface AccountProviderEvents { + object MyEvent : AccountProviderEvents +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt new file mode 100644 index 0000000000..bd1dbf24c2 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt @@ -0,0 +1,77 @@ +/* + * 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.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.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.AppScope + +@ContributesNode(AppScope::class) +class AccountProviderNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: AccountProviderPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val homeserver: String, + val isMatrixOrg: Boolean, + val isAccountCreation: Boolean, + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter = presenterFactory.create( + AccountProviderPresenterParams( + homeserver = inputs.homeserver, + isMatrixOrg = inputs.isMatrixOrg, + isAccountCreation = inputs.isAccountCreation, + ) + ) + + interface Callback : Plugin { + fun onContinue() + fun onChangeAccountProvider() + } + + private fun onContinue() { + plugins().forEach { it.onContinue() } + } + + private fun onChangeAccountProvider() { + plugins().forEach { it.onChangeAccountProvider() } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + AccountProviderView( + state = state, + modifier = modifier, + onContinue = ::onContinue, + onChange = ::onChangeAccountProvider, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt new file mode 100644 index 0000000000..577d22ec7c --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt @@ -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.accountprovider + +import androidx.compose.runtime.Composable +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter + +data class AccountProviderPresenterParams( + val homeserver: String, + val isMatrixOrg: Boolean, + val isAccountCreation: Boolean, +) + +class AccountProviderPresenter @AssistedInject constructor( + @Assisted private val params: AccountProviderPresenterParams, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(params: AccountProviderPresenterParams): AccountProviderPresenter + } + + @Composable + override fun present(): AccountProviderState { + + fun handleEvents(event: AccountProviderEvents) { + when (event) { + AccountProviderEvents.MyEvent -> Unit + } + } + + return AccountProviderState( + homeserver = params.homeserver, + isMatrix = params.isMatrixOrg, + isAccountCreation = params.isAccountCreation, + eventSink = ::handleEvents + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt new file mode 100644 index 0000000000..1bee64e88d --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.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.accountprovider + +// Do not use default value, so no member get forgotten in the presenters. +data class AccountProviderState( + val homeserver: String, + val isMatrix: Boolean, + val isAccountCreation: Boolean, + val eventSink: (AccountProviderEvents) -> Unit +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt new file mode 100644 index 0000000000..35452cd256 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt @@ -0,0 +1,34 @@ +/* + * 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 AccountProviderStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aAccountProviderState(), + // Add other state here + ) +} + +fun aAccountProviderState() = AccountProviderState( + homeserver = "matrix.org", + isMatrix = true, + isAccountCreation = false, + eventSink = {} +) 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..60f21d0878 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderView.kt @@ -0,0 +1,113 @@ +/* + * 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.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.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.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.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.designsystem.theme.components.TextButton + +@Composable +fun AccountProviderView( + state: AccountProviderState, + modifier: Modifier = Modifier, + onContinue: () -> Unit = {}, + onChange: () -> Unit = {}, +) { + 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 + } + ), + 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 { + Button( + onClick = { + onContinue() + }, + enabled = true, + modifier = Modifier + .fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.screen_account_provider_continue)) + } + TextButton( + onClick = { + onChange() + }, + enabled = true, + modifier = Modifier + .fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.screen_account_provider_change)) + } + } + } + ) { + // No content + } +} + +@Preview +@Composable +fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderStateProvider::class) state: AccountProviderState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderStateProvider::class) state: AccountProviderState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: AccountProviderState) { + AccountProviderView( + state = state, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt index d778bb3c35..c31593b766 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt @@ -24,7 +24,6 @@ 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 @@ -33,6 +32,7 @@ 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.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -51,13 +51,10 @@ import io.element.android.features.login.impl.changeserver.SlidingSyncNotSupport 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.button.ButtonWithProgress 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 -import io.element.android.libraries.testtags.TestTags -import io.element.android.libraries.testtags.testTag /** * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 @@ -89,6 +86,7 @@ fun ChangeAccountProviderView( } Scaffold( + modifier = modifier, topBar = { TopAppBar( title = {}, @@ -97,7 +95,7 @@ fun ChangeAccountProviderView( } ) { padding -> Box( - modifier = modifier + modifier = Modifier .fillMaxSize() .imePadding() .padding(padding) @@ -112,8 +110,9 @@ fun ChangeAccountProviderView( 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) + subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle), ) if (slidingSyncNotSupportedError != null) { @@ -140,17 +139,7 @@ fun ChangeAccountProviderView( ), onClick = onOtherProviderClicked ) - 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() } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt index 96d7a26e7c..2c0d66c23b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt @@ -78,6 +78,7 @@ fun ChangeAccountProviderItemView( RoundedIconAtom( size = RoundedIconAtomSize.Medium, imageVector = Icons.Filled.Search, + tint = MaterialTheme.colorScheme.primary, ) } Text( diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 39c35ac92f..0238d788f5 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -1,5 +1,10 @@ + "Change account provider" + "Continue" + "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." 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/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 171e3655a0..c1365d4954 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 @@ -24,6 +24,7 @@ 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 @@ -46,6 +47,7 @@ fun IconTitleSubtitleMolecule( modifier: Modifier = Modifier, iconResourceId: Int? = null, iconImageVector: ImageVector? = null, + iconTint: Color = Color.Unspecified, ) { Column(modifier) { RoundedIconAtom( @@ -54,6 +56,7 @@ fun IconTitleSubtitleMolecule( size = RoundedIconAtomSize.Large, resourceId = iconResourceId, imageVector = iconImageVector, + tint = iconTint, ) Spacer(modifier = Modifier.height(16.dp)) Text( diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 23908ecad8..8fa70179e3 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -74,7 +74,8 @@ "includeRegex": [ "screen_login_.*", "screen_change_server_.*", - "screen_change_account_provider_.*" + "screen_change_account_provider_.*", + "screen_account_provider_.*" ] }, { From 7ac8843bf8ed14e8885d7ac4384907763931751b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Jun 2023 10:07:51 +0200 Subject: [PATCH 004/130] Add personal dict. --- .idea/dictionaries/bmarty.xml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .idea/dictionaries/bmarty.xml 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 From d8db9edafc18c571a3ea28d39cd4a76f3da30cbf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Jun 2023 14:40:16 +0200 Subject: [PATCH 005/130] Account provider form screen. --- features/login/impl/build.gradle.kts | 6 +- .../features/login/impl/LoginFlowNode.kt | 27 ++- .../accountprovider/AccountProviderView.kt | 3 +- .../ChangeAccountProviderEvents.kt | 3 - .../ChangeAccountProviderNode.kt | 20 +- .../ChangeAccountProviderPresenter.kt | 58 +---- .../ChangeAccountProviderState.kt | 13 +- .../ChangeAccountProviderStateProvider.kt | 13 +- .../ChangeAccountProviderView.kt | 65 ++---- .../form/ChangeAccountProviderFormEvents.kt | 24 +++ .../form/ChangeAccountProviderFormNode.kt | 56 +++++ .../ChangeAccountProviderFormPresenter.kt | 71 ++++++ .../form/ChangeAccountProviderFormView.kt | 202 ++++++++++++++++++ .../form/ChangeAccountProviderState.kt | 26 +++ .../ChangeAccountProviderStateProvider.kt | 53 +++++ .../form/HomeserverData.kt | 26 +++ .../form/HomeserverResolver.kt | 95 ++++++++ .../form/network/WellKnown.kt | 43 ++++ .../form/network/WellKnownBaseConfig.kt | 35 +++ .../form/network/WellknownAPI.kt | 23 ++ .../form/network/WellknownRequest.kt | 46 ++++ ...ProviderItem.kt => AccountProviderItem.kt} | 4 +- .../item/ChangeAccountProviderItemProvider.kt | 10 +- .../item/ChangeAccountProviderItemView.kt | 10 +- .../impl/src/main/res/values/localazy.xml | 6 +- 25 files changed, 810 insertions(+), 128 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/{ChangeAccountProviderItem.kt => AccountProviderItem.kt} (90%) diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index c43a5c2cd3..5f4563c3a9 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -19,7 +19,7 @@ plugins { alias(libs.plugins.anvil) alias(libs.plugins.ksp) id("kotlin-parcelize") -} + kotlin("plugin.serialization") version "1.8.21"} android { namespace = "io.element.android.features.login.impl" @@ -41,11 +41,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) 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 ad81d7c441..2d85ec7677 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 @@ -32,6 +32,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode 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.oidc.CustomTabAvailabilityChecker import io.element.android.features.login.impl.oidc.customtab.CustomTabHandler @@ -82,6 +85,9 @@ class LoginFlowNode @AssistedInject constructor( @Parcelize object ChangeAccountProvider : NavTarget + @Parcelize + object ChangeAccountProviderForm : NavTarget + // Not used anymore @Parcelize object ChangeServer : NavTarget @@ -134,7 +140,26 @@ class LoginFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(inputs, callback)) } 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(buildContext, plugins = listOf(callback)) + } + NavTarget.ChangeAccountProviderForm -> { + val callback = object : ChangeAccountProviderFormNode.Callback { + override fun onAccountProviderItemClicked(data: AccountProviderItem) { + TODO("Not yet implemented") + } + } + + createNode(buildContext, plugins = listOf(callback)) } } } 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 index 60f21d0878..73f03a75e4 100644 --- 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 @@ -54,7 +54,8 @@ fun AccountProviderView( R.string.screen_account_provider_signup_title } else { R.string.screen_account_provider_signin_title - } + }, + state.homeserver ), subTitle = stringResource( id = if (state.isAccountCreation) { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt index 73b9929dca..0579006dfc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt @@ -17,7 +17,4 @@ package io.element.android.features.login.impl.changeaccountprovider sealed interface ChangeAccountProviderEvents { - data class SetServer(val server: String) : ChangeAccountProviderEvents - object Submit : ChangeAccountProviderEvents - object ClearError : ChangeAccountProviderEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt index 00b9d312c0..e54d4347d2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt @@ -21,9 +21,11 @@ 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) @@ -33,14 +35,28 @@ class ChangeAccountProviderNode @AssistedInject constructor( private val presenter: ChangeAccountProviderPresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onAccountProviderItemClicked(data: AccountProviderItem) + fun onOtherClicked() + } + + private fun onAccountProviderItemClicked(data: AccountProviderItem) { + plugins().forEach { it.onAccountProviderItemClicked(data) } + } + + private fun onOtherClicked() { + plugins().forEach { it.onOtherClicked() } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() ChangeAccountProviderView( state = state, modifier = modifier, - // TODO - onBackPressed = {} + onBackPressed = ::navigateUp, + onAccountProviderItemClicked = ::onAccountProviderItemClicked, + onOtherProviderClicked = ::onOtherClicked, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt index b4fbdac771..8749fbec06 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -17,63 +17,25 @@ package io.element.android.features.login.impl.changeaccountprovider 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.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.features.login.impl.changeaccountprovider.item.AccountProviderItem 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 class ChangeAccountProviderPresenter @Inject constructor( - private val authenticationService: MatrixAuthenticationService ) : Presenter { @Composable override fun present(): ChangeAccountProviderState { - 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: 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( - homeserver = homeserver.value, - changeServerAction = changeServerAction.value, - eventSink = ::handleEvents + // Just matrix.org by default for now + accountProviderItems = listOf( + AccountProviderItem( + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + ) + ), ) } - - private fun CoroutineScope.submit(homeserverUrl: MutableState, changeServerAction: MutableState>) = launch { - suspend { - val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value - authenticationService.setHomeserver(domain).getOrThrow() - homeserverUrl.value = domain - }.execute(changeServerAction, errorMapping = ChangeServerError::from) - } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt index 98f95ca357..62c756ad07 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt @@ -16,14 +16,9 @@ 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. -data class ChangeAccountProviderState( - val homeserver: String, - val changeServerAction: Async, - val eventSink: (ChangeAccountProviderEvents) -> Unit -) { - // TODO Remove - val submitEnabled: Boolean get() = changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading -} +data class ChangeAccountProviderState constructor( + val accountProviderItems: List, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt index a176de572e..7707c66945 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.login.impl.changeaccountprovider 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 { override val values: Sequence @@ -28,7 +28,12 @@ open class ChangeAccountProviderStateProvider : PreviewParameterProvider Unit, + onAccountProviderItemClicked: (AccountProviderItem) -> Unit = {}, onOtherProviderClicked: () -> Unit = {}, - 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(ChangeAccountProviderEvents.Submit) - } Scaffold( modifier = modifier, @@ -115,34 +92,30 @@ fun ChangeAccountProviderView( subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle), ) - if (slidingSyncNotSupportedError != null) { - SlidingSyncNotSupportedDialog(onLearnMoreClicked = { - eventSink(ChangeAccountProviderEvents.ClearError) - }, onDismiss = { - eventSink(ChangeAccountProviderEvents.ClearError) - }) - } - ChangeAccountProviderItemView( - item = ChangeAccountProviderItem( - title = "matrix.org", - subtitle = stringResource(id = R.string.screen_change_account_provider_matrix_org_subtitle), - isPublic = true, - isMatrix = true, - ), - onClick = { - TODO() + state.accountProviderItems.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 } - ) + ChangeAccountProviderItemView( + item = alteredItem, + onClick = { + onAccountProviderItemClicked(alteredItem) + } + ) + } + // Other ChangeAccountProviderItemView( - item = ChangeAccountProviderItem( + item = AccountProviderItem( title = stringResource(id = R.string.screen_change_account_provider_other), ), onClick = onOtherProviderClicked ) Spacer(Modifier.height(32.dp)) - if (state.changeServerAction is Async.Success) { - onChangeServerSuccess() - } } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt new file mode 100644 index 0000000000..2e1180a211 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.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.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 +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt new file mode 100644 index 0000000000..28e48f7263 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt @@ -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, + private val presenter: ChangeAccountProviderFormPresenter, +) : Node(buildContext, plugins = plugins) { + + interface Callback : Plugin { + fun onAccountProviderItemClicked(data: AccountProviderItem) + } + + private fun onAccountProviderItemClicked(data: AccountProviderItem) { + plugins().forEach { it.onAccountProviderItemClicked(data) } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ChangeAccountProviderFormView( + state = state, + modifier = modifier, + onBackPressed = ::navigateUp, + onProviderClicked = ::onAccountProviderItemClicked + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt new file mode 100644 index 0000000000..24da7892ee --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt @@ -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 { + + @Composable + override fun present(): ChangeAccountProviderFormState { + val localCoroutineScope = rememberCoroutineScope() + + var currentJob: Job? = remember { null } + + val userInput = rememberSaveable { + mutableStateOf("") + } + val userInputResult: MutableState>> = 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>>) = launch { + suspend { + homeserverResolver.resolve(userInput) + }.execute(userInputResult) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt new file mode 100644 index 0000000000..54ddd5f10e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt @@ -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 = { } + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderState.kt new file mode 100644 index 0000000000..e87180b954 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/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.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>, + val eventSink: (ChangeAccountProviderFormEvents) -> Unit +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt new file mode 100644 index 0000000000..c61e03cca3 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt @@ -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 { + override val values: Sequence + get() = sequenceOf( + aChangeAccountProviderFormState(), + aChangeAccountProviderFormState(userInputResult = Async.Success(aHomeserverDataList())), + // Add other state here + ) +} + +fun aChangeAccountProviderFormState( + userInput: String = "", + userInputResult: Async> = Async.Uninitialized, +) = ChangeAccountProviderFormState( + userInput = userInput, + userInputResult = userInputResult, + eventSink = {} +) + +fun aHomeserverDataList(): List { + return listOf( + HomeserverData( + userInput = "matrix", + homeserverUrl = "https://matrix.org", + isWellknownValid = true, + ), + HomeserverData( + userInput = "matrix", + homeserverUrl = "https://matrix.io", + isWellknownValid = false, + ) + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt new file mode 100644 index 0000000000..6d254e3738 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/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.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, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt new file mode 100644 index 0000000000..fceddaf42f --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt @@ -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 { + 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): List { + 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 { + 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") + } + } + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt new file mode 100644 index 0000000000..a78f41a266 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt @@ -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 + *
+ * {
+ *     "m.homeserver": {
+ *         "base_url": "https://matrix.org"
+ *     },
+ *     "m.identity_server": {
+ *         "base_url": "https://vector.im"
+ *     }
+ * }
+ * 
+ * . + */ +@Serializable +data class WellKnown( + @SerialName("m.homeserver") + val homeServer: WellKnownBaseConfig? = null, + + @SerialName("m.identity_server") + val identityServer: WellKnownBaseConfig? = null, +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt new file mode 100644 index 0000000000..b0d33e1854 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt @@ -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 + *
+ * {
+ *     "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/changeaccountprovider/form/network/WellknownAPI.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt new file mode 100644 index 0000000000..13153cfabb --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt @@ -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 +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt new file mode 100644 index 0000000000..a8d911ee86 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt @@ -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() +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItem.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/AccountProviderItem.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItem.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/AccountProviderItem.kt index c913923205..22c8079c48 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItem.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/AccountProviderItem.kt @@ -16,9 +16,9 @@ package io.element.android.features.login.impl.changeaccountprovider.item -data class ChangeAccountProviderItem( +data class AccountProviderItem constructor( val title: String, val subtitle: String? = null, val isPublic: Boolean = false, - val isMatrix: Boolean = false, + val isMatrixOrg: Boolean = false, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt index bf7a92cf08..523b4143d3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt @@ -18,19 +18,19 @@ package io.element.android.features.login.impl.changeaccountprovider.item import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class ChangeAccountProviderItemProvider : PreviewParameterProvider { - override val values: Sequence +open class ChangeAccountProviderItemProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( aChangeAccountProviderItem(), 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 ) } -fun aChangeAccountProviderItem() = ChangeAccountProviderItem( +fun aChangeAccountProviderItem() = AccountProviderItem( title = "matrix.org", subtitle = "Matrix.org is an open network for secure, decentralized communication.", isPublic = true, - isMatrix = true, + isMatrixOrg = true, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt index 2c0d66c23b..867666e59c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt @@ -49,7 +49,7 @@ import io.element.android.libraries.designsystem.theme.components.Text */ @Composable fun ChangeAccountProviderItemView( - item: ChangeAccountProviderItem, + item: AccountProviderItem, modifier: Modifier = Modifier, onClick: () -> Unit, ) { @@ -68,7 +68,7 @@ fun ChangeAccountProviderItemView( .heightIn(min = 44.dp), verticalAlignment = Alignment.CenterVertically ) { - if (item.isMatrix) { + if (item.isMatrixOrg) { RoundedIconAtom( size = RoundedIconAtomSize.Medium, resourceId = R.drawable.ic_matrix, @@ -115,16 +115,16 @@ fun ChangeAccountProviderItemView( @Preview @Composable -fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: ChangeAccountProviderItem) = +fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: AccountProviderItem) = ElementPreviewLight { ContentToPreview(item) } @Preview @Composable -fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: ChangeAccountProviderItem) = +fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: AccountProviderItem) = ElementPreviewDark { ContentToPreview(item) } @Composable -private fun ContentToPreview(item: ChangeAccountProviderItem) { +private fun ContentToPreview(item: AccountProviderItem) { ChangeAccountProviderItemView( item = item, onClick = { } diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 0238d788f5..96d05df794 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -2,6 +2,10 @@ "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" @@ -27,4 +31,4 @@ "Password" "Continue" "Username" - \ No newline at end of file +
From 22ed3b7bfdd7c660a3539d3ba05d3031ae298b7c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Jun 2023 15:44:06 +0200 Subject: [PATCH 006/130] Navigation --- .../features/login/impl/LoginFlowNode.kt | 17 ++++-- .../accountprovider/AccountProviderEvents.kt | 4 +- .../accountprovider/AccountProviderNode.kt | 25 +++++--- .../AccountProviderPresenter.kt | 48 +++++++++++++-- .../accountprovider/AccountProviderState.kt | 8 ++- .../AccountProviderStateProvider.kt | 2 + .../accountprovider/AccountProviderView.kt | 60 +++++++++++++++---- .../datasource/AccountProviderDataSource.kt | 42 +++++++++++++ .../login/impl/util/LoginConstants.kt | 10 ++++ 9 files changed, 183 insertions(+), 33 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt 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 2d85ec7677..75681099b1 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 @@ -28,6 +28,7 @@ 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 @@ -36,11 +37,11 @@ import io.element.android.features.login.impl.changeaccountprovider.ChangeAccoun 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.datasource.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.util.LoginConstants import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -57,6 +58,7 @@ class LoginFlowNode @AssistedInject constructor( @Assisted plugins: List, private val customTabAvailabilityChecker: CustomTabAvailabilityChecker, private val customTabHandler: CustomTabHandler, + private val accountProviderDataSource: AccountProviderDataSource, ) : BackstackNode( backstack = BackStack( initialElement = NavTarget.AccountProvider, // NavTarget.Root, @@ -124,12 +126,11 @@ class LoginFlowNode @AssistedInject constructor( } NavTarget.AccountProvider -> { val inputs = AccountProviderNode.Inputs( - homeserver = LoginConstants.DEFAULT_HOMESERVER_URL, - isMatrixOrg = LoginConstants.DEFAULT_HOMESERVER_URL == "matrix.org", isAccountCreation = inputs.isAccountCreation ) val callback = object : AccountProviderNode.Callback { - override fun onContinue() { + override fun onServerValidated() { + // TODO TODO("Not yet implemented") } @@ -142,7 +143,9 @@ class LoginFlowNode @AssistedInject constructor( NavTarget.ChangeAccountProvider -> { val callback = object : ChangeAccountProviderNode.Callback { override fun onAccountProviderItemClicked(data: AccountProviderItem) { - TODO("Not yet implemented") + accountProviderDataSource.userSelection(data) + // Go back to the Account Provider screen + backstack.singleTop(NavTarget.AccountProvider) } override fun onOtherClicked() { @@ -155,7 +158,9 @@ class LoginFlowNode @AssistedInject constructor( NavTarget.ChangeAccountProviderForm -> { val callback = object : ChangeAccountProviderFormNode.Callback { override fun onAccountProviderItemClicked(data: AccountProviderItem) { - TODO("Not yet implemented") + accountProviderDataSource.userSelection(data) + // Go back to the Account Provider screen + backstack.singleTop(NavTarget.AccountProvider) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt index 7a4cbf0366..ec3a879c89 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt @@ -16,7 +16,7 @@ package io.element.android.features.login.impl.accountprovider -// TODO Add your events or remove the file completely if no events sealed interface AccountProviderEvents { - object MyEvent : AccountProviderEvents + object Continue : AccountProviderEvents + object ClearError : AccountProviderEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt index bd1dbf24c2..3331080f23 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt @@ -16,8 +16,12 @@ package io.element.android.features.login.impl.accountprovider +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 @@ -25,8 +29,10 @@ 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.architecture.NodeInputs import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) @@ -37,41 +43,44 @@ class AccountProviderNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { data class Inputs( - val homeserver: String, - val isMatrixOrg: Boolean, val isAccountCreation: Boolean, ) : NodeInputs private val inputs: Inputs = inputs() private val presenter = presenterFactory.create( AccountProviderPresenterParams( - homeserver = inputs.homeserver, - isMatrixOrg = inputs.isMatrixOrg, isAccountCreation = inputs.isAccountCreation, ) ) interface Callback : Plugin { - fun onContinue() + fun onServerValidated() fun onChangeAccountProvider() } - private fun onContinue() { - plugins().forEach { it.onContinue() } + private fun onServerValidated() { + plugins().forEach { it.onServerValidated() } } private fun onChangeAccountProvider() { plugins().forEach { it.onChangeAccountProvider() } } + private fun openLearnMorePage(context: Context) { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL)) + tryOrNull { context.startActivity(intent) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() + val context = LocalContext.current AccountProviderView( state = state, modifier = modifier, - onContinue = ::onContinue, + onChangeServerSuccess = ::onServerValidated, onChange = ::onChangeAccountProvider, + onLearnMoreClicked = { openLearnMorePage(context) }, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt index 577d22ec7c..3561e2f19a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt @@ -17,19 +17,35 @@ package io.element.android.features.login.impl.accountprovider 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 dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.features.login.impl.changeserver.ChangeServerError +import io.element.android.features.login.impl.datasource.AccountProviderDataSource +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 data class AccountProviderPresenterParams( - val homeserver: String, - val isMatrixOrg: Boolean, val isAccountCreation: Boolean, ) class AccountProviderPresenter @AssistedInject constructor( @Assisted private val params: AccountProviderPresenterParams, + private val accountProviderDataSource: AccountProviderDataSource, + private val authenticationService: MatrixAuthenticationService ) : Presenter { @AssistedFactory @@ -39,18 +55,40 @@ class AccountProviderPresenter @AssistedInject constructor( @Composable override fun present(): AccountProviderState { + val accountProvider by accountProviderDataSource.flow().collectAsState() + + val localCoroutineScope = rememberCoroutineScope() + + val homeserver = rememberSaveable { + mutableStateOf(accountProvider.title /* TODO There is a mix of data and UI here, which is not nice */) + } + val changeServerAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } fun handleEvents(event: AccountProviderEvents) { when (event) { - AccountProviderEvents.MyEvent -> Unit + AccountProviderEvents.Continue -> { + localCoroutineScope.submit(homeserver, changeServerAction) + } + AccountProviderEvents.ClearError -> changeServerAction.value = Async.Uninitialized } } return AccountProviderState( - homeserver = params.homeserver, - isMatrix = params.isMatrixOrg, + homeserver = accountProvider.title, + isMatrix = accountProvider.isMatrixOrg, isAccountCreation = params.isAccountCreation, + changeServerAction = changeServerAction.value, eventSink = ::handleEvents ) } + + private fun CoroutineScope.submit(homeserverUrl: MutableState, changeServerAction: MutableState>) = launch { + suspend { + val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value + authenticationService.setHomeserver(domain).getOrThrow() + homeserverUrl.value = domain + }.execute(changeServerAction, errorMapping = ChangeServerError::from) + } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt index 1bee64e88d..d7462895d4 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt @@ -16,10 +16,16 @@ package io.element.android.features.login.impl.accountprovider +import io.element.android.libraries.architecture.Async + // Do not use default value, so no member get forgotten in the presenters. data class AccountProviderState( val homeserver: String, val isMatrix: Boolean, val isAccountCreation: Boolean, + // TODO Rename + val changeServerAction: Async, val eventSink: (AccountProviderEvents) -> Unit -) +) { + val submitEnabled: Boolean get() = homeserver.isNotEmpty() && (changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt index 35452cd256..908888f2a1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.login.impl.accountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async open class AccountProviderStateProvider : PreviewParameterProvider { override val values: Sequence @@ -30,5 +31,6 @@ fun aAccountProviderState() = AccountProviderState( homeserver = "matrix.org", isMatrix = true, isAccountCreation = false, + changeServerAction = Async.Uninitialized, eventSink = {} ) 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 index 73f03a75e4..5989dfa8bd 100644 --- 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 @@ -21,28 +21,49 @@ 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.changeserver.ChangeServerError +import io.element.android.features.login.impl.changeserver.ChangeServerEvents +import io.element.android.features.login.impl.changeserver.SlidingSyncNotSupportedDialog +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.Button import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.testtags.TestTags +import io.element.android.libraries.testtags.testTag @Composable fun AccountProviderView( state: AccountProviderState, modifier: Modifier = Modifier, - onContinue: () -> Unit = {}, + // TODO Rename + onChangeServerSuccess: () -> Unit = {}, + onLearnMoreClicked: () -> Unit = {}, onChange: () -> Unit = {}, ) { + val isLoading by remember(state.changeServerAction) { + derivedStateOf { + state.changeServerAction is Async.Loading + } + } + val eventSink = state.eventSink + val invalidHomeserverError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage + val slidingSyncNotSupportedError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert + HeaderFooterPage( modifier = modifier, header = { @@ -69,16 +90,15 @@ fun AccountProviderView( }, footer = { ButtonColumnMolecule { - Button( - onClick = { - onContinue() - }, - enabled = true, + ButtonWithProgress( + text = stringResource(id = R.string.screen_account_provider_continue), + showProgress = isLoading, + onClick = { eventSink.invoke(AccountProviderEvents.Continue) }, + enabled = state.submitEnabled, modifier = Modifier .fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.screen_account_provider_continue)) - } + .testTag(TestTags.changeServerContinue) + ) TextButton( onClick = { onChange() @@ -92,7 +112,25 @@ fun AccountProviderView( } } ) { - // No content + if (slidingSyncNotSupportedError != null) { + SlidingSyncNotSupportedDialog(onLearnMoreClicked = { + onLearnMoreClicked() + eventSink(AccountProviderEvents.ClearError) + }, onDismiss = { + eventSink(AccountProviderEvents.ClearError) + }) + } + if (invalidHomeserverError != null) { + ErrorDialog( + content = invalidHomeserverError.message(), + onDismiss = { + eventSink.invoke(AccountProviderEvents.ClearError) + } + ) + } + } + if (state.changeServerAction is Async.Success) { + onChangeServerSuccess() } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt new file mode 100644 index 0000000000..9a548f7aab --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.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.datasource + +import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem +import io.element.android.features.login.impl.util.defaultAccountProviderItem +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( + defaultAccountProviderItem + ) + + fun flow(): StateFlow { + return accountProvider.asStateFlow() + } + + fun userSelection(data: AccountProviderItem) { + accountProvider.tryEmit(data) + } +} 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..06ffcb94f4 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.changeaccountprovider.item.AccountProviderItem + 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 defaultAccountProviderItem = AccountProviderItem( + 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, +) From 32f953aa83abe043cbd4acf7180774c496633ec3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Jun 2023 18:16:35 +0200 Subject: [PATCH 007/130] LoginPasswordNode --- .../features/login/impl/LoginFlowNode.kt | 22 +- .../accountprovider/AccountProviderNode.kt | 15 +- .../AccountProviderPresenter.kt | 41 ++- .../accountprovider/AccountProviderState.kt | 11 +- .../AccountProviderStateProvider.kt | 2 +- .../accountprovider/AccountProviderView.kt | 37 ++- .../impl/loginpassword/LoginPasswordEvents.kt | 24 ++ .../impl/loginpassword/LoginPasswordNode.kt | 45 +++ .../loginpassword/LoginPasswordPresenter.kt | 84 +++++ .../impl/loginpassword/LoginPasswordState.kt | 43 +++ .../LoginPasswordStateProvider.kt | 37 +++ .../impl/loginpassword/LoginPasswordView.kt | 305 ++++++++++++++++++ 12 files changed, 636 insertions(+), 30 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordEvents.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordNode.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt 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 75681099b1..8f0564cf33 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 @@ -38,6 +38,7 @@ import io.element.android.features.login.impl.changeaccountprovider.form.ChangeA 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.datasource.AccountProviderDataSource +import io.element.android.features.login.impl.loginpassword.LoginPasswordNode 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 @@ -90,6 +91,9 @@ class LoginFlowNode @AssistedInject constructor( @Parcelize object ChangeAccountProviderForm : NavTarget + @Parcelize + object LoginPasswordForm : NavTarget + // Not used anymore @Parcelize object ChangeServer : NavTarget @@ -129,9 +133,18 @@ class LoginFlowNode @AssistedInject constructor( isAccountCreation = inputs.isAccountCreation ) val callback = object : AccountProviderNode.Callback { - override fun onServerValidated() { - // TODO - TODO("Not yet implemented") + override fun onOidcDetails(oidcDetails: OidcDetails) { + if (customTabAvailabilityChecker.supportCustomTab()) { + // In this case open a Chrome Custom tab + activity?.let { customTabHandler.open(it, darkTheme, oidcDetails.url) } + } else { + // Fallback to WebView mode + backstack.push(NavTarget.OidcView(oidcDetails)) + } + } + + override fun onLoginPasswordNeeded() { + backstack.push(NavTarget.LoginPasswordForm) } override fun onChangeAccountProvider() { @@ -166,6 +179,9 @@ class LoginFlowNode @AssistedInject constructor( createNode(buildContext, plugins = listOf(callback)) } + NavTarget.LoginPasswordForm -> { + createNode(buildContext, plugins = listOf()) + } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt index 3331080f23..aac5068294 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.AppScope +import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) class AccountProviderNode @AssistedInject constructor( @@ -54,12 +55,17 @@ class AccountProviderNode @AssistedInject constructor( ) interface Callback : Plugin { - fun onServerValidated() + fun onLoginPasswordNeeded() + fun onOidcDetails(oidcDetails: OidcDetails) fun onChangeAccountProvider() } - private fun onServerValidated() { - plugins().forEach { it.onServerValidated() } + private fun onOidcDetails(data: OidcDetails) { + plugins().forEach { it.onOidcDetails(data) } + } + + private fun onLoginPasswordNeeded() { + plugins().forEach { it.onLoginPasswordNeeded() } } private fun onChangeAccountProvider() { @@ -78,7 +84,8 @@ class AccountProviderNode @AssistedInject constructor( AccountProviderView( state = state, modifier = modifier, - onChangeServerSuccess = ::onServerValidated, + onOidcDetails = ::onOidcDetails, + onLoginPasswordNeeded = ::onLoginPasswordNeeded, onChange = ::onChangeAccountProvider, onLearnMoreClicked = { openLearnMorePage(context) }, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt index 3561e2f19a..b20a69fcae 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt @@ -29,11 +29,13 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.login.impl.changeserver.ChangeServerError import io.element.android.features.login.impl.datasource.AccountProviderDataSource +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.core.data.tryOrNull 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 java.net.URL @@ -56,22 +58,29 @@ class AccountProviderPresenter @AssistedInject constructor( @Composable override fun present(): AccountProviderState { val accountProvider by accountProviderDataSource.flow().collectAsState() - + val currentHomeServerDetails = authenticationService.getHomeserverDetails().collectAsState().value + val getHomeServerDetailsAction: MutableState> = remember { + if (currentHomeServerDetails != null) { + mutableStateOf(Async.Success(currentHomeServerDetails)) + } else { + mutableStateOf(Async.Uninitialized) + } + } val localCoroutineScope = rememberCoroutineScope() val homeserver = rememberSaveable { - mutableStateOf(accountProvider.title /* TODO There is a mix of data and UI here, which is not nice */) + mutableStateOf(currentHomeServerDetails?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL) } - val changeServerAction: MutableState> = remember { + val loginFlowAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } fun handleEvents(event: AccountProviderEvents) { when (event) { AccountProviderEvents.Continue -> { - localCoroutineScope.submit(homeserver, changeServerAction) + localCoroutineScope.submit(homeserver, loginFlowAction) } - AccountProviderEvents.ClearError -> changeServerAction.value = Async.Uninitialized + AccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized } } @@ -79,16 +88,30 @@ class AccountProviderPresenter @AssistedInject constructor( homeserver = accountProvider.title, isMatrix = accountProvider.isMatrixOrg, isAccountCreation = params.isAccountCreation, - changeServerAction = changeServerAction.value, + loginFlow = loginFlowAction.value, eventSink = ::handleEvents ) } - private fun CoroutineScope.submit(homeserverUrl: MutableState, changeServerAction: MutableState>) = launch { + private fun CoroutineScope.submit( + homeserverUrl: MutableState, + loginFlowAction: MutableState>, + ) = launch { suspend { val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value - authenticationService.setHomeserver(domain).getOrThrow() homeserverUrl.value = domain - }.execute(changeServerAction, errorMapping = ChangeServerError::from) + authenticationService.setHomeserver(domain).map { + authenticationService.getHomeserverDetails().value!! + }.map { + if (it.supportsOidcLogin) { + // Retrieve the details right now + LoginFlow.OidcFlow(authenticationService.getOidcUrl().getOrThrow()) + } else if (it.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/accountprovider/AccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt index d7462895d4..fd6a7914fb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt @@ -17,15 +17,20 @@ package io.element.android.features.login.impl.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 AccountProviderState( val homeserver: String, val isMatrix: Boolean, val isAccountCreation: Boolean, - // TODO Rename - val changeServerAction: Async, + val loginFlow: Async, val eventSink: (AccountProviderEvents) -> Unit ) { - val submitEnabled: Boolean get() = homeserver.isNotEmpty() && (changeServerAction is Async.Uninitialized || changeServerAction is Async.Loading) + val submitEnabled: Boolean get() = homeserver.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/accountprovider/AccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt index 908888f2a1..9d68942dae 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt @@ -31,6 +31,6 @@ fun aAccountProviderState() = AccountProviderState( homeserver = "matrix.org", isMatrix = true, isAccountCreation = false, - changeServerAction = Async.Uninitialized, + loginFlow = Async.Uninitialized, eventSink = {} ) 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 index 5989dfa8bd..3548504802 100644 --- 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 @@ -31,18 +31,20 @@ 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.changeserver.ChangeServerError -import io.element.android.features.login.impl.changeserver.ChangeServerEvents import io.element.android.features.login.impl.changeserver.SlidingSyncNotSupportedDialog 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.async.AsyncFailure +import io.element.android.libraries.designsystem.components.async.AsyncLoading 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 @@ -50,19 +52,19 @@ import io.element.android.libraries.testtags.testTag fun AccountProviderView( state: AccountProviderState, modifier: Modifier = Modifier, - // TODO Rename - onChangeServerSuccess: () -> Unit = {}, + onOidcDetails: (OidcDetails) -> Unit = {}, + onLoginPasswordNeeded: () -> Unit = {}, onLearnMoreClicked: () -> Unit = {}, onChange: () -> Unit = {}, ) { - val isLoading by remember(state.changeServerAction) { + val isLoading by remember(state.loginFlow) { derivedStateOf { - state.changeServerAction is Async.Loading + state.loginFlow is Async.Loading } } val eventSink = state.eventSink - val invalidHomeserverError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage - val slidingSyncNotSupportedError = (state.changeServerAction as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert + val invalidHomeserverError = (state.loginFlow as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage + val slidingSyncNotSupportedError = (state.loginFlow as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert HeaderFooterPage( modifier = modifier, @@ -112,6 +114,24 @@ fun AccountProviderView( } } ) { + when (state.loginFlow) { + is Async.Failure -> { + AsyncFailure( + throwable = state.loginFlow.error, + onRetry = { + state.eventSink.invoke(AccountProviderEvents.Continue) + } + ) + } + is Async.Loading -> AsyncLoading() + is Async.Success -> { + when (val loginFlowState = state.loginFlow.state) { + is LoginFlow.OidcFlow -> onOidcDetails(loginFlowState.oidcDetails) + LoginFlow.PasswordLogin -> onLoginPasswordNeeded() + } + } + Async.Uninitialized -> Unit + } if (slidingSyncNotSupportedError != null) { SlidingSyncNotSupportedDialog(onLearnMoreClicked = { onLearnMoreClicked() @@ -129,9 +149,6 @@ fun AccountProviderView( ) } } - if (state.changeServerAction is Async.Success) { - onChangeServerSuccess() - } } @Preview diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordEvents.kt new file mode 100644 index 0000000000..44b442dd08 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/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.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/loginpassword/LoginPasswordNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordNode.kt new file mode 100644 index 0000000000..ea731f379b --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/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.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/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt new file mode 100644 index 0000000000..af495eb106 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.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.loginpassword + +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.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, +) : Presenter { + + @Composable + override fun present(): LoginPasswordState { + val localCoroutineScope = rememberCoroutineScope() + val loginAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + + val formState = rememberSaveable { + mutableStateOf(LoginFormState.Default) + } + + 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( + 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/loginpassword/LoginPasswordState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt new file mode 100644 index 0000000000..eac406a1c4 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt @@ -0,0 +1,43 @@ +/* + * 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.loginpassword + +import android.os.Parcelable +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.api.core.SessionId +import kotlinx.parcelize.Parcelize + +data class LoginPasswordState( + val formState: LoginFormState, + val loginAction: Async, + val eventSink: (LoginPasswordEvents) -> Unit +) { + val submitEnabled: Boolean + get() = loginAction !is Async.Failure && + ((formState.login.isNotEmpty() && formState.password.isNotEmpty())) +} + +@Parcelize +data class LoginFormState( + val login: String, + val password: String +) : Parcelable { + + companion object { + val Default = LoginFormState("", "") + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt new file mode 100644 index 0000000000..deb7e86acc --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt @@ -0,0 +1,37 @@ +/* + * 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.loginpassword + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +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( + formState = LoginFormState.Default, + loginAction = Async.Uninitialized, + eventSink = {} +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt new file mode 100644 index 0000000000..f8b0345c01 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt @@ -0,0 +1,305 @@ +/* + * Copyright (c) 2022 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.loginpassword + +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.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.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.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.autofill.AutofillType +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.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +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.error.loginError +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.button.ButtonWithProgress +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.components.form.textFieldState +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.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.autofill +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, ExperimentalLayoutApi::class) +@Composable +fun LoginPasswordView( + state: LoginPasswordState, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit, +) { + val isLoading by remember(state.loginAction) { + derivedStateOf { + state.loginAction is Async.Loading + } + } + val focusManager = LocalFocusManager.current + + fun submit() { + // Clear focus to prevent keyboard issues with textfields + focusManager.clearFocus(force = true) + + state.eventSink(LoginPasswordEvents.Submit) + } + + Scaffold( + topBar = { + TopAppBar( + title = {}, + navigationIcon = { BackButton(onClick = onBackPressed) }, + ) + } + ) { padding -> + Box( + modifier = modifier + .fillMaxSize() + .imePadding() + .padding(padding) + .consumeWindowInsets(padding) + ) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .verticalScroll(state = scrollState) + .padding(horizontal = 16.dp), + ) { + Spacer(Modifier.height(16.dp)) + // Title + Text( + text = stringResource(id = R.string.screen_login_title), + modifier = Modifier + .fillMaxWidth(), + style = ElementTextStyles.Bold.title1, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.height(32.dp)) + ServerDetailForm( + state = state, + isLoading = isLoading, + submit = ::submit + ) + } + } + } + + if (state.loginAction is Async.Failure) { + LoginErrorDialog(error = state.loginAction.error, onDismiss = { + state.eventSink(LoginPasswordEvents.ClearError) + }) + } +} + +@Composable +fun ServerDetailForm( + state: LoginPasswordState, + isLoading: Boolean, + submit: () -> Unit, + modifier: Modifier = Modifier, +) { + LoginForm(state = state, isLoading = isLoading, onSubmit = submit, modifier = modifier) + + Spacer(Modifier.height(28.dp)) + + // 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)) +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +internal fun LoginForm( + state: LoginPasswordState, + isLoading: Boolean, + onSubmit: () -> Unit, + modifier: Modifier = Modifier +) { + var loginFieldState by textFieldState(stateValue = state.formState.login) + var passwordFieldState by textFieldState(stateValue = state.formState.password) + + val focusManager = LocalFocusManager.current + val eventSink = state.eventSink + + Column(modifier) { + Text( + text = stringResource(R.string.screen_login_form_header), + modifier = Modifier.padding(start = 16.dp), + style = ElementTextStyles.Regular.formHeader + ) + + Spacer(modifier = Modifier.height(8.dp)) + TextField( + value = loginFieldState, + readOnly = isLoading, + modifier = Modifier + .fillMaxWidth() + .onTabOrEnterKeyFocusNext(focusManager) + .testTag(TestTags.loginEmailUsername) + .autofill(autofillTypes = listOf(AutofillType.Username), onFill = { + loginFieldState = it + eventSink(LoginPasswordEvents.SetLogin(it)) + }), + label = { + Text(text = stringResource(R.string.screen_login_username_hint)) + }, + onValueChange = { + loginFieldState = it + eventSink(LoginPasswordEvents.SetLogin(it)) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Email, + imeAction = ImeAction.Next + ), + keyboardActions = KeyboardActions(onNext = { + focusManager.moveFocus(FocusDirection.Down) + }), + singleLine = true, + maxLines = 1, + trailingIcon = if (loginFieldState.isNotEmpty()) { + { + IconButton(onClick = { + loginFieldState = "" + }) { + Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear)) + } + } + } else null, + ) + + var passwordVisible by remember { mutableStateOf(false) } + if (state.loginAction is Async.Loading) { + // Ensure password is hidden when user submits the form + passwordVisible = false + } + Spacer(Modifier.height(20.dp)) + TextField( + value = passwordFieldState, + readOnly = isLoading, + modifier = Modifier + .fillMaxWidth() + .onTabOrEnterKeyFocusNext(focusManager) + .testTag(TestTags.loginPassword) + .autofill(autofillTypes = listOf(AutofillType.Password), onFill = { + passwordFieldState = it + eventSink(LoginPasswordEvents.SetPassword(it)) + }), + onValueChange = { + passwordFieldState = it + eventSink(LoginPasswordEvents.SetPassword(it)) + }, + label = { + Text(text = stringResource(R.string.screen_login_password_hint)) + }, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + val image = + if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff + val description = + if (passwordVisible) stringResource(StringR.string.a11y_hide_password) else stringResource(StringR.string.a11y_show_password) + + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon(imageVector = image, description) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + keyboardActions = KeyboardActions( + onDone = { onSubmit() } + ), + singleLine = true, + maxLines = 1, + ) + } +} + +@Composable +internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { + ErrorDialog( + content = stringResource(loginError(error)), + onDismiss = onDismiss + ) +} + +@Preview +@Composable +internal fun LoginRootScreenLightPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +internal fun LoginRootScreenDarkPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: LoginPasswordState) { + LoginPasswordView( + state = state, + onBackPressed = {} + ) +} From 6eeac4dc414ad8352fb3b82ea1c4ce31e83b57ac Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 6 Jun 2023 18:46:31 +0200 Subject: [PATCH 008/130] Improve screen --- .../impl/loginpassword/LoginPasswordView.kt | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt index f8b0345c01..8509e718f5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt @@ -31,11 +31,11 @@ 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.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 @@ -59,6 +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.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 @@ -122,12 +123,14 @@ fun LoginPasswordView( ) { Spacer(Modifier.height(16.dp)) // Title - Text( - text = stringResource(id = R.string.screen_login_title), - modifier = Modifier - .fillMaxWidth(), - style = ElementTextStyles.Bold.title1, - color = MaterialTheme.colorScheme.primary, + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 60.dp), + iconImageVector = Icons.Filled.AccountCircle, + title = stringResource( + id = R.string.screen_account_provider_signin_title, + "state.homeserver" // TODO + ), + subTitle = stringResource(id = R.string.screen_login_form_header) ) Spacer(Modifier.height(32.dp)) ServerDetailForm( From 7b6a5d3af43a079af2dca21d3cc64f4b9e197453 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 10:01:41 +0200 Subject: [PATCH 009/130] Cleanup --- .../android/features/login/impl/LoginFlowNode.kt | 6 +++--- .../accountprovider/AccountProviderPresenter.kt | 13 ++----------- .../impl/accountprovider/AccountProviderState.kt | 6 +++--- .../AccountProviderStateProvider.kt | 8 ++++---- .../impl/accountprovider/AccountProviderView.kt | 7 +++---- .../item/AccountProvider.kt} | 4 ++-- .../item/AccountProviderProvider.kt} | 14 +++++++------- .../item/AccountProviderView.kt} | 14 +++++++------- .../ChangeAccountProviderNode.kt | 10 +++++----- .../ChangeAccountProviderPresenter.kt | 6 +++--- .../ChangeAccountProviderState.kt | 4 ++-- .../ChangeAccountProviderStateProvider.kt | 11 +++-------- .../ChangeAccountProviderView.kt | 16 ++++++++-------- .../form/ChangeAccountProviderFormNode.kt | 10 +++++----- .../form/ChangeAccountProviderFormView.kt | 12 ++++++------ .../login/impl/changeserver/ChangeServerNode.kt | 3 +++ .../impl/changeserver/ChangeServerPresenter.kt | 1 + .../changeserver/ChangeServerStateProvider.kt | 4 +++- .../login/impl/changeserver/ChangeServerView.kt | 1 + .../impl/datasource/AccountProviderDataSource.kt | 12 ++++++------ .../{changeserver => error}/ChangeServerError.kt | 2 +- .../features/login/impl/error/ErrorFormatter.kt | 2 ++ .../impl/loginpassword/LoginPasswordPresenter.kt | 6 ++++++ .../impl/loginpassword/LoginPasswordState.kt | 2 ++ .../loginpassword/LoginPasswordStateProvider.kt | 2 ++ .../impl/loginpassword/LoginPasswordView.kt | 8 ++++---- .../features/login/impl/root/LoginRootNode.kt | 3 +++ .../features/login/impl/util/LoginConstants.kt | 4 ++-- 28 files changed, 99 insertions(+), 92 deletions(-) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/item/AccountProviderItem.kt => accountprovider/item/AccountProvider.kt} (86%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/item/ChangeAccountProviderItemProvider.kt => accountprovider/item/AccountProviderProvider.kt} (64%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/item/ChangeAccountProviderItemView.kt => accountprovider/item/AccountProviderView.kt} (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => error}/ChangeServerError.kt (95%) 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 8f0564cf33..67a28d1569 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 @@ -33,9 +33,9 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.login.impl.accountprovider.AccountProviderNode +import io.element.android.features.login.impl.accountprovider.item.AccountProvider 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.datasource.AccountProviderDataSource import io.element.android.features.login.impl.loginpassword.LoginPasswordNode @@ -155,7 +155,7 @@ class LoginFlowNode @AssistedInject constructor( } NavTarget.ChangeAccountProvider -> { val callback = object : ChangeAccountProviderNode.Callback { - override fun onAccountProviderItemClicked(data: AccountProviderItem) { + override fun onAccountProviderClicked(data: AccountProvider) { accountProviderDataSource.userSelection(data) // Go back to the Account Provider screen backstack.singleTop(NavTarget.AccountProvider) @@ -170,7 +170,7 @@ class LoginFlowNode @AssistedInject constructor( } NavTarget.ChangeAccountProviderForm -> { val callback = object : ChangeAccountProviderFormNode.Callback { - override fun onAccountProviderItemClicked(data: AccountProviderItem) { + override fun onAccountProviderClicked(data: AccountProvider) { accountProviderDataSource.userSelection(data) // Go back to the Account Provider screen backstack.singleTop(NavTarget.AccountProvider) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt index b20a69fcae..9302ed7a48 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt @@ -27,7 +27,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.login.impl.changeserver.ChangeServerError +import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.datasource.AccountProviderDataSource import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async @@ -35,7 +35,6 @@ 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 io.element.android.libraries.matrix.api.auth.MatrixHomeServerDetails import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.net.URL @@ -59,13 +58,6 @@ class AccountProviderPresenter @AssistedInject constructor( override fun present(): AccountProviderState { val accountProvider by accountProviderDataSource.flow().collectAsState() val currentHomeServerDetails = authenticationService.getHomeserverDetails().collectAsState().value - val getHomeServerDetailsAction: MutableState> = remember { - if (currentHomeServerDetails != null) { - mutableStateOf(Async.Success(currentHomeServerDetails)) - } else { - mutableStateOf(Async.Uninitialized) - } - } val localCoroutineScope = rememberCoroutineScope() val homeserver = rememberSaveable { @@ -85,8 +77,7 @@ class AccountProviderPresenter @AssistedInject constructor( } return AccountProviderState( - homeserver = accountProvider.title, - isMatrix = accountProvider.isMatrixOrg, + accountProvider = accountProvider, isAccountCreation = params.isAccountCreation, loginFlow = loginFlowAction.value, eventSink = ::handleEvents diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt index fd6a7914fb..625ef0070d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt @@ -16,18 +16,18 @@ package io.element.android.features.login.impl.accountprovider +import io.element.android.features.login.impl.accountprovider.item.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 AccountProviderState( - val homeserver: String, - val isMatrix: Boolean, + val accountProvider: AccountProvider, val isAccountCreation: Boolean, val loginFlow: Async, val eventSink: (AccountProviderEvents) -> Unit ) { - val submitEnabled: Boolean get() = homeserver.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading) + val submitEnabled: Boolean get() = accountProvider.title.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading) } sealed interface LoginFlow { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt index 9d68942dae..56584c1935 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt @@ -17,19 +17,19 @@ package io.element.android.features.login.impl.accountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.item.anAccountProvider import io.element.android.libraries.architecture.Async open class AccountProviderStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( - aAccountProviderState(), + anAccountProviderState(), // Add other state here ) } -fun aAccountProviderState() = AccountProviderState( - homeserver = "matrix.org", - isMatrix = true, +fun anAccountProviderState() = AccountProviderState( + accountProvider = anAccountProvider(), isAccountCreation = false, loginFlow = Async.Uninitialized, eventSink = {} 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 index 3548504802..6c8b1df60e 100644 --- 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 @@ -30,14 +30,13 @@ 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.changeserver.ChangeServerError +import io.element.android.features.login.impl.error.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.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.async.AsyncFailure -import io.element.android.libraries.designsystem.components.async.AsyncLoading 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 @@ -78,7 +77,7 @@ fun AccountProviderView( } else { R.string.screen_account_provider_signin_title }, - state.homeserver + state.accountProvider.title ), subTitle = stringResource( id = if (state.isAccountCreation) { @@ -123,7 +122,7 @@ fun AccountProviderView( } ) } - is Async.Loading -> AsyncLoading() + 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) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/AccountProviderItem.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt similarity index 86% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/AccountProviderItem.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt index 22c8079c48..6f8adff403 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/AccountProviderItem.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.item +package io.element.android.features.login.impl.accountprovider.item -data class AccountProviderItem constructor( +data class AccountProvider constructor( val title: String, val subtitle: String? = null, val isPublic: Boolean = false, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt similarity index 64% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt index 523b4143d3..c931c3e801 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt @@ -14,21 +14,21 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.item +package io.element.android.features.login.impl.accountprovider.item import androidx.compose.ui.tooling.preview.PreviewParameterProvider -open class ChangeAccountProviderItemProvider : PreviewParameterProvider { - override val values: Sequence +open class AccountProviderProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aChangeAccountProviderItem(), - aChangeAccountProviderItem().copy(subtitle = null), - aChangeAccountProviderItem().copy(title = "Other", subtitle = null, isPublic = false, isMatrixOrg = false), + anAccountProvider(), + anAccountProvider().copy(subtitle = null), + anAccountProvider().copy(title = "Other", subtitle = null, isPublic = false, isMatrixOrg = false), // Add other state here ) } -fun aChangeAccountProviderItem() = AccountProviderItem( +fun anAccountProvider() = AccountProvider( title = "matrix.org", subtitle = "Matrix.org is an open network for secure, decentralized communication.", isPublic = true, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt index 867666e59c..eb2f67947e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/item/ChangeAccountProviderItemView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.item +package io.element.android.features.login.impl.accountprovider.item import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -48,8 +48,8 @@ import io.element.android.libraries.designsystem.theme.components.Text * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 */ @Composable -fun ChangeAccountProviderItemView( - item: AccountProviderItem, +fun AccountProviderView( + item: AccountProvider, modifier: Modifier = Modifier, onClick: () -> Unit, ) { @@ -115,17 +115,17 @@ fun ChangeAccountProviderItemView( @Preview @Composable -fun ChangeAccountProviderViewLightPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: AccountProviderItem) = +fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) = ElementPreviewLight { ContentToPreview(item) } @Preview @Composable -fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProviderItemProvider::class) item: AccountProviderItem) = +fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) = ElementPreviewDark { ContentToPreview(item) } @Composable -private fun ContentToPreview(item: AccountProviderItem) { - ChangeAccountProviderItemView( +private fun ContentToPreview(item: AccountProvider) { + AccountProviderView( item = item, onClick = { } ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt index e54d4347d2..aafaee818d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt @@ -25,7 +25,7 @@ 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.features.login.impl.accountprovider.item.AccountProvider import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) @@ -36,12 +36,12 @@ class ChangeAccountProviderNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun onAccountProviderItemClicked(data: AccountProviderItem) + fun onAccountProviderClicked(data: AccountProvider) fun onOtherClicked() } - private fun onAccountProviderItemClicked(data: AccountProviderItem) { - plugins().forEach { it.onAccountProviderItemClicked(data) } + private fun onAccountProviderClicked(data: AccountProvider) { + plugins().forEach { it.onAccountProviderClicked(data) } } private fun onOtherClicked() { @@ -55,7 +55,7 @@ class ChangeAccountProviderNode @AssistedInject constructor( state = state, modifier = modifier, onBackPressed = ::navigateUp, - onAccountProviderItemClicked = ::onAccountProviderItemClicked, + onAccountProviderClicked = ::onAccountProviderClicked, onOtherProviderClicked = ::onOtherClicked, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt index 8749fbec06..055541ca09 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -17,7 +17,7 @@ package io.element.android.features.login.impl.changeaccountprovider import androidx.compose.runtime.Composable -import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem +import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.libraries.architecture.Presenter import javax.inject.Inject @@ -28,8 +28,8 @@ class ChangeAccountProviderPresenter @Inject constructor( override fun present(): ChangeAccountProviderState { return ChangeAccountProviderState( // Just matrix.org by default for now - accountProviderItems = listOf( - AccountProviderItem( + accountProviders = listOf( + AccountProvider( title = "matrix.org", subtitle = null, isPublic = true, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt index 62c756ad07..89a286518e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt @@ -16,9 +16,9 @@ package io.element.android.features.login.impl.changeaccountprovider -import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem +import io.element.android.features.login.impl.accountprovider.item.AccountProvider // Do not use default value, so no member get forgotten in the presenters. data class ChangeAccountProviderState constructor( - val accountProviderItems: List, + val accountProviders: List, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt index 7707c66945..627f11c742 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt @@ -17,7 +17,7 @@ package io.element.android.features.login.impl.changeaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem +import io.element.android.features.login.impl.accountprovider.item.anAccountProvider open class ChangeAccountProviderStateProvider : PreviewParameterProvider { override val values: Sequence @@ -28,12 +28,7 @@ open class ChangeAccountProviderStateProvider : PreviewParameterProvider Unit, - onAccountProviderItemClicked: (AccountProviderItem) -> Unit = {}, + onAccountProviderClicked: (AccountProvider) -> Unit = {}, onOtherProviderClicked: () -> Unit = {}, ) { val scrollState = rememberScrollState() @@ -92,7 +92,7 @@ fun ChangeAccountProviderView( subTitle = stringResource(id = R.string.screen_change_account_provider_subtitle), ) - state.accountProviderItems.forEach { item -> + state.accountProviders.forEach { item -> val alteredItem = if (item.isMatrixOrg) { // Set the subtitle from the resource item.copy( @@ -101,16 +101,16 @@ fun ChangeAccountProviderView( } else { item } - ChangeAccountProviderItemView( + AccountProviderView( item = alteredItem, onClick = { - onAccountProviderItemClicked(alteredItem) + onAccountProviderClicked(alteredItem) } ) } // Other - ChangeAccountProviderItemView( - item = AccountProviderItem( + AccountProviderView( + item = AccountProvider( title = stringResource(id = R.string.screen_change_account_provider_other), ), onClick = onOtherProviderClicked diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt index 28e48f7263..8786497e60 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt @@ -25,7 +25,7 @@ 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.features.login.impl.accountprovider.item.AccountProvider import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) @@ -36,11 +36,11 @@ class ChangeAccountProviderFormNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun onAccountProviderItemClicked(data: AccountProviderItem) + fun onAccountProviderClicked(data: AccountProvider) } - private fun onAccountProviderItemClicked(data: AccountProviderItem) { - plugins().forEach { it.onAccountProviderItemClicked(data) } + private fun onAccountProviderClicked(data: AccountProvider) { + plugins().forEach { it.onAccountProviderClicked(data) } } @Composable @@ -50,7 +50,7 @@ class ChangeAccountProviderFormNode @AssistedInject constructor( state = state, modifier = modifier, onBackPressed = ::navigateUp, - onProviderClicked = ::onAccountProviderItemClicked + onAccountProviderClicked = ::onAccountProviderClicked ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt index 54ddd5f10e..9e00d9c47d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt @@ -50,8 +50,8 @@ 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.features.login.impl.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.accountprovider.item.AccountProviderView 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 @@ -72,7 +72,7 @@ fun ChangeAccountProviderFormView( state: ChangeAccountProviderFormState, modifier: Modifier = Modifier, onBackPressed: () -> Unit, - onProviderClicked: (AccountProviderItem) -> Unit = {}, + onAccountProviderClicked: (AccountProvider) -> Unit = {}, ) { val eventSink = state.eventSink val scrollState = rememberScrollState() @@ -161,16 +161,16 @@ fun ChangeAccountProviderFormView( is Async.Success -> { state.userInputResult.state.forEach { homeserverData -> val isMatrixOrg = homeserverData.homeserverUrl == "https://matrix.org" - val item = AccountProviderItem( + val item = AccountProvider( 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( + AccountProviderView( item = item, onClick = { - onProviderClicked(item) + onAccountProviderClicked(item) } ) } 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/changeserver/ChangeServerNode.kt index 11952ce5a8..5ac13ed409 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/changeserver/ChangeServerNode.kt @@ -32,6 +32,9 @@ import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.AppScope +/** + * Not used anymore. + */ @ContributesNode(AppScope::class) class ChangeServerNode @AssistedInject constructor( @Assisted buildContext: BuildContext, 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..81a19e5021 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 @@ -22,6 +22,7 @@ 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.error.ChangeServerError import io.element.android.features.login.impl.util.LoginConstants import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter 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..9e938ce15b 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 @@ -18,6 +18,7 @@ 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.features.login.impl.error.ChangeServerError import io.element.android.libraries.architecture.Async open class ChangeServerStateProvider : PreviewParameterProvider { @@ -30,7 +31,8 @@ open class ChangeServerStateProvider : PreviewParameterProvider = MutableStateFlow( - defaultAccountProviderItem + private val accountProvider: MutableStateFlow = MutableStateFlow( + defaultAccountProvider ) - fun flow(): StateFlow { + fun flow(): StateFlow { return accountProvider.asStateFlow() } - fun userSelection(data: AccountProviderItem) { + fun userSelection(data: AccountProvider) { accountProvider.tryEmit(data) } } 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 95% 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..9976089f52 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 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..86820645ae 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,12 +16,14 @@ 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 +@StringRes fun loginError( throwable: Throwable ): Int { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt index af495eb106..96f749a89c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt @@ -18,10 +18,13 @@ package io.element.android.features.login.impl.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.datasource.AccountProviderDataSource import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -32,6 +35,7 @@ import javax.inject.Inject class LoginPasswordPresenter @Inject constructor( private val authenticationService: MatrixAuthenticationService, + private val accountProviderDataSource: AccountProviderDataSource, ) : Presenter { @Composable @@ -44,6 +48,7 @@ class LoginPasswordPresenter @Inject constructor( val formState = rememberSaveable { mutableStateOf(LoginFormState.Default) } + val accountProvider by accountProviderDataSource.flow().collectAsState() fun handleEvents(event: LoginPasswordEvents) { when (event) { @@ -61,6 +66,7 @@ class LoginPasswordPresenter @Inject constructor( } return LoginPasswordState( + accountProvider = accountProvider, formState = formState.value, loginAction = loginAction.value, eventSink = ::handleEvents diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt index eac406a1c4..07bb7d1c51 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt @@ -17,11 +17,13 @@ package io.element.android.features.login.impl.loginpassword import android.os.Parcelable +import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.parcelize.Parcelize data class LoginPasswordState( + val accountProvider: AccountProvider, val formState: LoginFormState, val loginAction: Async, val eventSink: (LoginPasswordEvents) -> Unit diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt index deb7e86acc..f53cfb168a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.login.impl.loginpassword import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.accountprovider.item.anAccountProvider import io.element.android.libraries.architecture.Async open class LoginPasswordStateProvider : PreviewParameterProvider { @@ -31,6 +32,7 @@ open class LoginPasswordStateProvider : PreviewParameterProvider Box( - modifier = modifier + modifier = Modifier .fillMaxSize() .imePadding() .padding(padding) @@ -121,14 +122,13 @@ fun LoginPasswordView( .verticalScroll(state = scrollState) .padding(horizontal = 16.dp), ) { - Spacer(Modifier.height(16.dp)) // Title IconTitleSubtitleMolecule( - modifier = Modifier.padding(top = 60.dp), + modifier = Modifier.padding(top = 20.dp), iconImageVector = Icons.Filled.AccountCircle, title = stringResource( id = R.string.screen_account_provider_signin_title, - "state.homeserver" // TODO + state.accountProvider.title ), subTitle = stringResource(id = R.string.screen_login_form_header) ) 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/root/LoginRootNode.kt index 787f5d0b48..1de7477555 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/root/LoginRootNode.kt @@ -28,6 +28,9 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails +/** + * Not used anymore. + */ @ContributesNode(AppScope::class) class LoginRootNode @AssistedInject constructor( @Assisted buildContext: BuildContext, 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 06ffcb94f4..10f69c3d1a 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,7 +16,7 @@ package io.element.android.features.login.impl.util -import io.element.android.features.login.impl.changeaccountprovider.item.AccountProviderItem +import io.element.android.features.login.impl.accountprovider.item.AccountProvider object LoginConstants { const val MATRIX_ORG_URL = "matrix.org" @@ -25,7 +25,7 @@ object LoginConstants { const val SLIDING_SYNC_READ_MORE_URL = "https://github.com/matrix-org/sliding-sync/blob/main/docs/Landing.md" } -val defaultAccountProviderItem = AccountProviderItem( +val defaultAccountProvider = AccountProvider( title = LoginConstants.DEFAULT_HOMESERVER_URL, subtitle = null, isPublic = LoginConstants.DEFAULT_HOMESERVER_URL == LoginConstants.MATRIX_ORG_URL, From 9836b3fc8a917a63ec93a228a5aa3f894dc3b3e2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 10:26:13 +0200 Subject: [PATCH 010/130] More fixes --- .../features/login/impl/LoginFlowNode.kt | 1 + .../AccountProviderPresenter.kt | 15 ++++----------- .../accountprovider/AccountProviderView.kt | 9 +-------- .../form/ChangeAccountProviderFormPresenter.kt | 18 +++++++++++++----- .../datasource/AccountProviderDataSource.kt | 4 ++++ 5 files changed, 23 insertions(+), 24 deletions(-) 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 67a28d1569..3220d8b897 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 @@ -192,6 +192,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/AccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt index 9302ed7a48..5246f9c528 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt @@ -23,13 +23,11 @@ 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 dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.datasource.AccountProviderDataSource -import io.element.android.features.login.impl.util.LoginConstants +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 @@ -57,12 +55,8 @@ class AccountProviderPresenter @AssistedInject constructor( @Composable override fun present(): AccountProviderState { val accountProvider by accountProviderDataSource.flow().collectAsState() - val currentHomeServerDetails = authenticationService.getHomeserverDetails().collectAsState().value val localCoroutineScope = rememberCoroutineScope() - val homeserver = rememberSaveable { - mutableStateOf(currentHomeServerDetails?.url ?: LoginConstants.DEFAULT_HOMESERVER_URL) - } val loginFlowAction: MutableState> = remember { mutableStateOf(Async.Uninitialized) } @@ -70,7 +64,7 @@ class AccountProviderPresenter @AssistedInject constructor( fun handleEvents(event: AccountProviderEvents) { when (event) { AccountProviderEvents.Continue -> { - localCoroutineScope.submit(homeserver, loginFlowAction) + localCoroutineScope.submit(accountProvider.title, loginFlowAction) } AccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized } @@ -85,12 +79,11 @@ class AccountProviderPresenter @AssistedInject constructor( } private fun CoroutineScope.submit( - homeserverUrl: MutableState, + homeserverUrl: String, loginFlowAction: MutableState>, ) = launch { suspend { - val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value - homeserverUrl.value = domain + val domain = tryOrNull { URL(homeserverUrl) }?.host ?: homeserverUrl authenticationService.setHomeserver(domain).map { authenticationService.getHomeserverDetails().value!! }.map { 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 index 6c8b1df60e..36352416f9 100644 --- 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 @@ -114,14 +114,7 @@ fun AccountProviderView( } ) { when (state.loginFlow) { - is Async.Failure -> { - AsyncFailure( - throwable = state.loginFlow.error, - onRetry = { - state.eventSink.invoke(AccountProviderEvents.Continue) - } - ) - } + is Async.Failure -> Unit // Error dialog will be displayed is Async.Loading -> Unit // The Continue button shows the loading state is Async.Success -> { when (val loginFlowState = state.loginFlow.state) { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt index 24da7892ee..157dc8a120 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt @@ -24,9 +24,9 @@ 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.delay import kotlinx.coroutines.launch import javax.inject.Inject @@ -63,9 +63,17 @@ class ChangeAccountProviderFormPresenter @Inject constructor( ) } - private fun CoroutineScope.userInput(userInput: String, userInputResult: MutableState>>) = launch { - suspend { - homeserverResolver.resolve(userInput) - }.execute(userInputResult) + // Could be reworked using LaunchedEffect + private fun CoroutineScope.userInput(userInput: String, state: MutableState>>) = launch { + state.value = Async.Uninitialized + // Debounce + delay(300) + state.value = Async.Loading() + try { + val result = homeserverResolver.resolve(userInput) + state.value = Async.Success(result) + } catch (error: Throwable) { + state.value = Async.Failure(error) + } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt index 1240124c5a..ebd1c15ae1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt @@ -36,6 +36,10 @@ class AccountProviderDataSource @Inject constructor( return accountProvider.asStateFlow() } + fun reset() { + accountProvider.tryEmit(defaultAccountProvider) + } + fun userSelection(data: AccountProvider) { accountProvider.tryEmit(data) } From 27313ada3747038ec85a10155bedf70282646ede Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 12:20:11 +0200 Subject: [PATCH 011/130] Get faster results --- .../ChangeAccountProviderFormPresenter.kt | 31 ++----- .../form/HomeserverResolver.kt | 86 ++++++++++++------- 2 files changed, 61 insertions(+), 56 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt index 157dc8a120..8262c35a27 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt @@ -17,16 +17,13 @@ package io.element.android.features.login.impl.changeaccountprovider.form 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.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject @@ -38,42 +35,28 @@ class ChangeAccountProviderFormPresenter @Inject constructor( override fun present(): ChangeAccountProviderFormState { val localCoroutineScope = rememberCoroutineScope() - var currentJob: Job? = remember { null } - val userInput = rememberSaveable { mutableStateOf("") } - val userInputResult: MutableState>> = remember { - mutableStateOf(Async.Uninitialized) - } + val data by homeserverResolver.flow().collectAsState() fun handleEvents(event: ChangeAccountProviderFormEvents) { when (event) { is ChangeAccountProviderFormEvents.UserInput -> { - currentJob?.cancel() - currentJob = localCoroutineScope.userInput(event.input, userInputResult) + localCoroutineScope.userInput(event.input) } } } return ChangeAccountProviderFormState( userInput = userInput.value, - userInputResult = userInputResult.value, + userInputResult = data, eventSink = ::handleEvents ) } // Could be reworked using LaunchedEffect - private fun CoroutineScope.userInput(userInput: String, state: MutableState>>) = launch { - state.value = Async.Uninitialized - // Debounce - delay(300) - state.value = Async.Loading() - try { - val result = homeserverResolver.resolve(userInput) - state.value = Async.Success(result) - } catch (error: Throwable) { - state.value = Async.Failure(error) - } + private fun CoroutineScope.userInput(userInput: String) = launch { + homeserverResolver.accept(userInput) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt index fceddaf42f..d80b20f1fd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt @@ -17,12 +17,20 @@ 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.architecture.Async +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.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import javax.inject.Inject @@ -33,45 +41,59 @@ class HomeserverResolver @Inject constructor( private val dispatchers: CoroutineDispatchers, private val wellknownRequest: WellknownRequest, ) { + private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) - suspend fun resolve(userInput: String): List { - return withContext(dispatchers.io) { - val cleanedUpUserInput = userInput.trim() - if (cleanedUpUserInput.length < 4) { - // Wait for more chars - emptyList() - } else { + fun flow(): StateFlow>> = mutableFlow + + private var currentJob: Job? = null + + suspend fun accept(userInput: String) { + currentJob?.cancel() + val cleanedUpUserInput = userInput.trim() + mutableFlow.tryEmit(Async.Uninitialized) + if (cleanedUpUserInput.length > 3) { + delay(300) + mutableFlow.tryEmit(Async.Loading()) + withContext(dispatchers.io) { 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 - } + currentJob = resolveList(userInput, list) } } } - private suspend fun resolveList(userInput: String, list: List): List { - return coroutineScope { - buildList { - list.map { - async { - val isValid = wellknownRequest.execute(it) - if (isValid) { - add(HomeserverData(userInput, it, true)) + private fun CoroutineScope.resolveList(userInput: String, list: List): Job { + val currentList = mutableListOf() + return launch { + list.map { + async { + val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse() + if (isValid) { + // Emit the list as soon as possible + currentList.add(HomeserverData(userInput, it, true)) + mutableFlow.tryEmit(Async.Success(currentList)) + } + } + }.joinAll() + .also { + // If list is empty, and the user as entered an URL, do not block the user. + if (currentList.isEmpty()) { + if (userInput.isValidUrl()) { + mutableFlow.tryEmit( + Async.Success( + listOf( + HomeserverData( + userInput = userInput, + homeserverUrl = userInput, + isWellknownValid = false + ) + ) + ) + ) + } else { + mutableFlow.tryEmit(Async.Uninitialized) } } - }.joinAll() - } + } } } From b453ae246d43fa8765ca0fef8cb585958402306f Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 12:21:54 +0200 Subject: [PATCH 012/130] Quality --- .../designsystem/atomic/atoms/RoundedIconAtom.kt | 10 +++++----- .../atomic/molecules/IconTitleSubtitleMolecule.kt | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) 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 index 01fa2355d3..b5c98cf483 100644 --- 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 @@ -38,11 +38,6 @@ 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 -enum class RoundedIconAtomSize { - Medium, - Large -} - @Composable fun RoundedIconAtom( modifier: Modifier = Modifier, @@ -115,3 +110,8 @@ private fun ContentToPreview() { ) } } + +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 c1365d4954..06c111d069 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 @@ -38,7 +38,7 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Text /** - * Provide either an `iconResourceId` or an `iconImageVector` + * Provide either an `iconResourceId` or an `iconImageVector`. */ @Composable fun IconTitleSubtitleMolecule( From c5c5d90c2404776ab3c5b736ff49a47a66e96096 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 14:18:46 +0200 Subject: [PATCH 013/130] Remove old screens. --- .../features/login/impl/LoginFlowNode.kt | 30 -- .../accountprovider/AccountProviderView.kt | 2 - .../SlidingSyncNotSupportedDialog.kt | 36 ++ .../impl/changeserver/ChangeServerEvents.kt | 23 - .../impl/changeserver/ChangeServerNode.kt | 65 --- .../changeserver/ChangeServerPresenter.kt | 77 ---- .../impl/changeserver/ChangeServerState.kt | 27 -- .../changeserver/ChangeServerStateProvider.kt | 44 -- .../impl/changeserver/ChangeServerView.kt | 292 ------------- .../login/impl/root/LoginRootEvents.kt | 25 -- .../features/login/impl/root/LoginRootNode.kt | 65 --- .../login/impl/root/LoginRootPresenter.kt | 171 -------- .../login/impl/root/LoginRootState.kt | 58 --- .../login/impl/root/LoginRootStateProvider.kt | 76 ---- .../features/login/impl/root/LoginRootView.kt | 403 ------------------ 15 files changed, 36 insertions(+), 1358 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootPresenter.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootStateProvider.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt 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 3220d8b897..1c0ab78ee4 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 @@ -36,13 +36,11 @@ import io.element.android.features.login.impl.accountprovider.AccountProviderNod import io.element.android.features.login.impl.accountprovider.item.AccountProvider 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.changeserver.ChangeServerNode import io.element.android.features.login.impl.datasource.AccountProviderDataSource import io.element.android.features.login.impl.loginpassword.LoginPasswordNode 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.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler @@ -78,10 +76,6 @@ class LoginFlowNode @AssistedInject constructor( private val inputs: Inputs = inputs() sealed interface NavTarget : Parcelable { - // Not used anymore - @Parcelize - object Root : NavTarget - @Parcelize object AccountProvider : NavTarget @@ -94,36 +88,12 @@ class LoginFlowNode @AssistedInject constructor( @Parcelize object LoginPasswordForm : NavTarget - // Not used anymore - @Parcelize - object ChangeServer : NavTarget - @Parcelize data class OidcView(val oidcDetails: OidcDetails) : NavTarget } 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) - } - - override fun onOidcDetails(oidcDetails: OidcDetails) { - if (customTabAvailabilityChecker.supportCustomTab()) { - // In this case open a Chrome Custom tab - activity?.let { customTabHandler.open(it, darkTheme, oidcDetails.url) } - } else { - // Fallback to WebView mode - backstack.push(NavTarget.OidcView(oidcDetails)) - } - } - } - createNode(buildContext, plugins = listOf(callback)) - } - - NavTarget.ChangeServer -> createNode(buildContext) is NavTarget.OidcView -> { val input = OidcNode.Inputs(navTarget.oidcDetails) createNode(buildContext, plugins = listOf(input)) 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 index 36352416f9..dcc219f93e 100644 --- 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 @@ -31,12 +31,10 @@ 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.error.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.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.async.AsyncFailure 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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt new file mode 100644 index 0000000000..03cf3a754e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.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.accountprovider + +import androidx.compose.runtime.Composable +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) { + 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), + ) +} 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 deleted file mode 100644 index dd1e83c3f8..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt +++ /dev/null @@ -1,23 +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.changeserver - -sealed interface ChangeServerEvents { - data class SetServer(val server: String) : ChangeServerEvents - object Submit : ChangeServerEvents - object ClearError : ChangeServerEvents -} 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/changeserver/ChangeServerNode.kt deleted file mode 100644 index 5ac13ed409..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerNode.kt +++ /dev/null @@ -1,65 +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.changeserver - -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 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.libraries.di.AppScope - -/** - * Not used anymore. - */ -@ContributesNode(AppScope::class) -class ChangeServerNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: ChangeServerPresenter, -) : Node(buildContext, plugins = plugins) { - private fun onSuccess() { - navigateUp() - } - - private fun openLearnMorePage(context: Context) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL)) - tryOrNull { context.startActivity(intent) } - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - val context = LocalContext.current - ChangeServerView( - state = state, - modifier = modifier, - onChangeServerSuccess = this::onSuccess, - onBackPressed = { navigateUp() }, - onLearnMoreClicked = { openLearnMorePage(context) }, - ) - } -} 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 deleted file mode 100644 index 81a19e5021..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt +++ /dev/null @@ -1,77 +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.changeserver - -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.features.login.impl.error.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.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 - -class ChangeServerPresenter @Inject constructor(private val authenticationService: MatrixAuthenticationService) : 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) - } - 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 { - suspend { - val domain = tryOrNull { URL(homeserverUrl.value) }?.host ?: homeserverUrl.value - authenticationService.setHomeserver(domain).getOrThrow() - homeserverUrl.value = domain - }.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 deleted file mode 100644 index 5a3ea3b856..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt +++ /dev/null @@ -1,27 +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.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) -} 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 deleted file mode 100644 index 9e938ce15b..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt +++ /dev/null @@ -1,44 +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.changeserver - -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.error.ChangeServerError -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 deleted file mode 100644 index 3d9a8d6545..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (c) 2022 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.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.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.error.ChangeServerError -import io.element.android.features.login.impl.util.LoginConstants -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.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, - 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 = "", - ) - } - 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) - }) - } - 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() - } - } - } - } -} - -@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) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview -@Composable -internal fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = - ElementPreviewDark { ContentToPreview(state) } - -@Composable -private fun ContentToPreview(state: ChangeServerState) { - ChangeServerView(state = state, onBackPressed = {}, onLearnMoreClicked = {}) -} 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/root/LoginRootEvents.kt deleted file mode 100644 index 5aa5071876..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootEvents.kt +++ /dev/null @@ -1,25 +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 - -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 -} 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/root/LoginRootNode.kt deleted file mode 100644 index 1de7477555..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootNode.kt +++ /dev/null @@ -1,65 +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.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.libraries.di.AppScope -import io.element.android.libraries.matrix.api.auth.OidcDetails - -/** - * Not used anymore. - */ -@ContributesNode(AppScope::class) -class LoginRootNode @AssistedInject constructor( - @Assisted buildContext: BuildContext, - @Assisted plugins: List, - private val presenter: LoginRootPresenter, -) : Node(buildContext, plugins = plugins) { - - interface Callback : Plugin { - fun onChangeHomeServer() - fun onOidcDetails(oidcDetails: OidcDetails) - } - - private fun onChangeHomeServer() { - plugins().forEach { it.onChangeHomeServer() } - } - - private fun onOidcDetails(oidcDetails: OidcDetails) { - plugins().forEach { it.onOidcDetails(oidcDetails) } - } - - @Composable - override fun View(modifier: Modifier) { - val state = presenter.present() - LoginRootView( - state = state, - modifier = modifier, - onChangeServer = ::onChangeHomeServer, - onOidcDetails = ::onOidcDetails, - onBackPressed = ::navigateUp - ) - } -} 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/LoginRootState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt deleted file mode 100644 index 45eafa744c..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootState.kt +++ /dev/null @@ -1,58 +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 android.os.Parcelable -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, - val formState: LoginFormState, - val eventSink: (LoginRootEvents) -> 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 -} - -@Parcelize -data class LoginFormState( - val login: String, - val password: String -) : Parcelable { - - companion object { - val Default = LoginFormState("", "") - } -} 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/root/LoginRootView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt deleted file mode 100644 index 2444418725..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/root/LoginRootView.kt +++ /dev/null @@ -1,403 +0,0 @@ -/* - * Copyright (c) 2022 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.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 -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.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 -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 -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.components.button.BackButton -import io.element.android.libraries.designsystem.components.button.ButtonWithProgress -import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog -import io.element.android.libraries.designsystem.components.form.textFieldState -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.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.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, - modifier: Modifier = Modifier, - onChangeServer: () -> Unit = {}, - onOidcDetails: (OidcDetails) -> Unit = {}, - onBackPressed: () -> Unit, -) { - val isLoading by remember(state.loggedInState) { - derivedStateOf { - state.loggedInState == LoggedInState.LoggingIn - } - } - val focusManager = LocalFocusManager.current - - fun submit() { - // Clear focus to prevent keyboard issues with textfields - focusManager.clearFocus(force = true) - - state.eventSink(LoginRootEvents.Submit) - } - - Scaffold( - topBar = { - TopAppBar( - title = {}, - navigationIcon = { BackButton(onClick = onBackPressed) }, - ) - } - ) { padding -> - Box( - modifier = modifier - .fillMaxSize() - .imePadding() - .padding(padding) - .consumeWindowInsets(padding) - ) { - val scrollState = rememberScrollState() - - Column( - modifier = Modifier - .verticalScroll(state = scrollState) - .padding(horizontal = 16.dp), - ) { - Spacer(Modifier.height(16.dp)) - // Title - Text( - text = stringResource(id = R.string.screen_login_title), - modifier = Modifier - .fillMaxWidth(), - style = ElementTextStyles.Bold.title1, - color = MaterialTheme.colorScheme.primary, - ) - 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) - } - } - when (val loggedInState = state.loggedInState) { - is LoggedInState.OidcStarted -> onOidcDetails(loggedInState.oidcDetail) - else -> Unit - } - } - } - - 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, - isLoading: Boolean, - onSubmit: () -> Unit, - modifier: Modifier = Modifier -) { - var loginFieldState by textFieldState(stateValue = state.formState.login) - var passwordFieldState by textFieldState(stateValue = state.formState.password) - - val focusManager = LocalFocusManager.current - val eventSink = state.eventSink - - Column(modifier) { - Text( - text = stringResource(R.string.screen_login_form_header), - modifier = Modifier.padding(start = 16.dp), - style = ElementTextStyles.Regular.formHeader - ) - - Spacer(modifier = Modifier.height(8.dp)) - TextField( - value = loginFieldState, - readOnly = isLoading, - modifier = Modifier - .fillMaxWidth() - .onTabOrEnterKeyFocusNext(focusManager) - .testTag(TestTags.loginEmailUsername) - .autofill(autofillTypes = listOf(AutofillType.Username), onFill = { - loginFieldState = it - eventSink(LoginRootEvents.SetLogin(it)) - }), - label = { - Text(text = stringResource(R.string.screen_login_username_hint)) - }, - onValueChange = { - loginFieldState = it - eventSink(LoginRootEvents.SetLogin(it)) - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Email, - imeAction = ImeAction.Next - ), - keyboardActions = KeyboardActions(onNext = { - focusManager.moveFocus(FocusDirection.Down) - }), - singleLine = true, - maxLines = 1, - trailingIcon = if (loginFieldState.isNotEmpty()) { - { - IconButton(onClick = { - loginFieldState = "" - }) { - Icon(imageVector = Icons.Filled.Close, contentDescription = stringResource(StringR.string.action_clear)) - } - } - } else null, - ) - - var passwordVisible by remember { mutableStateOf(false) } - if (state.loggedInState is LoggedInState.LoggingIn) { - // Ensure password is hidden when user submits the form - passwordVisible = false - } - Spacer(Modifier.height(20.dp)) - TextField( - value = passwordFieldState, - readOnly = isLoading, - modifier = Modifier - .fillMaxWidth() - .onTabOrEnterKeyFocusNext(focusManager) - .testTag(TestTags.loginPassword) - .autofill(autofillTypes = listOf(AutofillType.Password), onFill = { - passwordFieldState = it - eventSink(LoginRootEvents.SetPassword(it)) - }), - onValueChange = { - passwordFieldState = it - eventSink(LoginRootEvents.SetPassword(it)) - }, - label = { - Text(text = stringResource(R.string.screen_login_password_hint)) - }, - visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), - trailingIcon = { - val image = - if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff - val description = - if (passwordVisible) stringResource(StringR.string.a11y_hide_password) else stringResource(StringR.string.a11y_show_password) - - IconButton(onClick = { passwordVisible = !passwordVisible }) { - Icon(imageVector = image, description) - } - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done, - ), - keyboardActions = KeyboardActions( - onDone = { onSubmit() } - ), - singleLine = true, - maxLines = 1, - ) - } -} - -@Composable -internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { - ErrorDialog( - content = stringResource(loginError(error)), - onDismiss = onDismiss - ) -} - -@Preview -@Composable -internal fun LoginRootScreenLightPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview -@Composable -internal fun LoginRootScreenDarkPreview(@PreviewParameter(LoginRootStateProvider::class) state: LoginRootState) = - ElementPreviewDark { ContentToPreview(state) } - -@Composable -private fun ContentToPreview(state: LoginRootState) { - LoginRootView( - state = state, - onBackPressed = {} - ) -} From 3d0156e5e38743681c4c8d645731d62c98f2d603 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 14:40:02 +0200 Subject: [PATCH 014/130] Fix compilation of minimal app. --- .../android/samples/minimal/LoginScreen.kt | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) 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..976a68f81c 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.datasource.AccountProviderDataSource +import io.element.android.features.login.impl.loginpassword.LoginPasswordPresenter +import io.element.android.features.login.impl.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 = {}, From c94b29bf83790a34ceb822c6cb015ae679d7a4fd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 14:47:42 +0200 Subject: [PATCH 015/130] Add test for AccountProviderPresenter --- .../AccountProviderPresenterTest.kt} | 112 ++++++++---------- 1 file changed, 48 insertions(+), 64 deletions(-) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{changeserver/ChangeServerPresenterTest.kt => accountprovider/AccountProviderPresenterTest.kt} (50%) 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/accountprovider/AccountProviderPresenterTest.kt similarity index 50% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenterTest.kt index a30bd9449c..0e34c623ae 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/accountprovider/AccountProviderPresenterTest.kt @@ -14,132 +14,116 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver +package io.element.android.features.login.impl.accountprovider 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.datasource.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_URL_2 +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 ChangeServerPresenterTest { +class AccountProviderPresenterTest { @Test - fun `present - should start with default homeserver`() = runTest { - val presenter = ChangeServerPresenter( + fun `present - initial test`() = runTest { + val presenter = AccountProviderPresenter( + AccountProviderPresenterParams(isAccountCreation = false), + AccountProviderDataSource(), FakeAuthenticationService(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.homeserver).isEqualTo(LoginConstants.DEFAULT_HOMESERVER_URL) + assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.submitEnabled).isTrue() + assertThat(initialState.accountProvider).isEqualTo(defaultAccountProvider) + assertThat(initialState.loginFlow).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)) - }, + fun `present - continue password login`() = runTest { + val authServer = FakeAuthenticationService() + val presenter = AccountProviderPresenter( + AccountProviderPresenterParams(isAccountCreation = false), + AccountProviderDataSource(), + authServer, ) + authServer.givenHomeserver(A_HOMESERVER) 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) + initialState.eventSink.invoke(AccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) + assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java) + assertThat(successState.loginFlow).isInstanceOf(Async.Success::class.java) + assertThat(successState.loginFlow.dataOrNull()).isEqualTo(LoginFlow.PasswordLogin) } } @Test - fun `present - submit parses URL`() = runTest { - val presenter = ChangeServerPresenter( - FakeAuthenticationService(), + fun `present - continue oidc`() = runTest { + val authServer = FakeAuthenticationService() + val presenter = AccountProviderPresenter( + AccountProviderPresenterParams(isAccountCreation = false), + AccountProviderDataSource(), + authServer, ) + authServer.givenHomeserver(A_HOMESERVER_OIDC) 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) + initialState.eventSink.invoke(AccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.changeServerAction).isInstanceOf(Async.Loading::class.java) - awaitItem() // Skip changing the url to the parsed domain + assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.changeServerAction).isInstanceOf(Async.Success::class.java) - assertThat(successState.homeserver).isEqualTo("matrix.org") + 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 = ChangeServerPresenter(authServer) + val presenter = AccountProviderPresenter( + AccountProviderPresenterParams(isAccountCreation = false), + AccountProviderDataSource(), + authServer, + ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() authServer.givenChangeServerError(Throwable()) - initialState.eventSink.invoke(ChangeServerEvents.Submit) + initialState.eventSink.invoke(AccountProviderEvents.Continue) skipItems(1) // Loading val failureState = awaitItem() assertThat(failureState.submitEnabled).isFalse() - assertThat(failureState.changeServerAction).isInstanceOf(Async.Failure::class.java) + assertThat(failureState.loginFlow).isInstanceOf(Async.Failure::class.java) } } @Test fun `present - clear error`() = runTest { val authenticationService = FakeAuthenticationService() - val presenter = ChangeServerPresenter( + val presenter = AccountProviderPresenter( + AccountProviderPresenterParams(isAccountCreation = false), + AccountProviderDataSource(), authenticationService, ) moleculeFlow(RecompositionClock.Immediate) { @@ -149,18 +133,18 @@ class ChangeServerPresenterTest { // Submit will return an error authenticationService.givenChangeServerError(A_THROWABLE) - initialState.eventSink(ChangeServerEvents.Submit) + initialState.eventSink(AccountProviderEvents.Continue) skipItems(1) // Loading // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.changeServerAction).isInstanceOf(Async.Failure::class.java) + assertThat(submittedState.loginFlow).isInstanceOf(Async.Failure::class.java) // Assert the error is then cleared - submittedState.eventSink(ChangeServerEvents.ClearError) + submittedState.eventSink(AccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.changeServerAction).isEqualTo(Async.Uninitialized) + assertThat(clearedState.loginFlow).isEqualTo(Async.Uninitialized) } } } From 97090b9007ee20335399ad564598b1143734916c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 14:50:31 +0200 Subject: [PATCH 016/130] Add test for ChangeAccountProviderPresenter --- .../ChangeAccountProviderPresenterTest.kt | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt new file mode 100644 index 0000000000..add636a4c4 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -0,0 +1,48 @@ +/* + * 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 + +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.item.AccountProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ChangeAccountProviderPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = ChangeAccountProviderPresenter( + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.accountProviders).isEqualTo( + listOf( + AccountProvider( + title = "matrix.org", + subtitle = null, + isPublic = true, + isMatrixOrg = true, + ) + ) + ) + } + } +} From ae38ffa91433c142d61e75c6410531a1071a23c4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 15:18:23 +0200 Subject: [PATCH 017/130] Add test for ChangeAccountProviderPresenter and other presenters. --- .../ChangeAccountProviderFormPresenter.kt | 1 + .../ChangeAccountProviderStateProvider.kt | 8 + .../form/DefaultHomeserverResolver.kt | 120 +++++++ .../form/HomeserverData.kt | 2 +- .../form/HomeserverResolver.kt | 97 +----- .../ChangeAccountProviderFormPresenterTest.kt | 87 +++++ .../form/FakeHomeServerResolver.kt | 49 +++ .../LoginPasswordPresenterTest.kt | 161 +++++++++ .../login/impl/root/LoginRootPresenterTest.kt | 308 ------------------ .../components/form/TextFieldLocalState.kt | 2 +- 10 files changed, 432 insertions(+), 403 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenterTest.kt delete mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/root/LoginRootPresenterTest.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt index 8262c35a27..ed8c98bb33 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt @@ -43,6 +43,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor( fun handleEvents(event: ChangeAccountProviderFormEvents) { when (event) { is ChangeAccountProviderFormEvents.UserInput -> { + userInput.value = event.input localCoroutineScope.userInput(event.input) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt index c61e03cca3..6cc00dd0ee 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt @@ -51,3 +51,11 @@ fun aHomeserverDataList(): List { ) ) } + +fun aHomeserverData(): HomeserverData { + return HomeserverData( + userInput = "matrix", + homeserverUrl = "https://matrix.org", + isWellknownValid = true, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt new file mode 100644 index 0000000000..812f98f323 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt @@ -0,0 +1,120 @@ +/* + * 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 com.squareup.anvil.annotations.ContributesBinding +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest +import io.element.android.libraries.architecture.Async +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 io.element.android.libraries.di.AppScope +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import javax.inject.Inject + +/** + * Resolve homeserver base on search terms + */ +@ContributesBinding(AppScope::class) +class DefaultHomeserverResolver @Inject constructor( + private val dispatchers: CoroutineDispatchers, + private val wellknownRequest: WellknownRequest, +): HomeserverResolver { + private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) + + override fun flow(): StateFlow>> = mutableFlow + + private var currentJob: Job? = null + + override suspend fun accept(userInput: String) { + currentJob?.cancel() + val cleanedUpUserInput = userInput.trim() + mutableFlow.tryEmit(Async.Uninitialized) + if (cleanedUpUserInput.length > 3) { + delay(300) + mutableFlow.tryEmit(Async.Loading()) + withContext(dispatchers.io) { + val list = getUrlCandidate(cleanedUpUserInput) + currentJob = resolveList(userInput, list) + } + } + } + + private fun CoroutineScope.resolveList(userInput: String, list: List): Job { + val currentList = mutableListOf() + return launch { + list.map { + async { + val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse() + if (isValid) { + // Emit the list as soon as possible + currentList.add(HomeserverData(userInput, it, true)) + mutableFlow.tryEmit(Async.Success(currentList)) + } + } + }.joinAll() + .also { + // If list is empty, and the user as entered an URL, do not block the user. + if (currentList.isEmpty()) { + if (userInput.isValidUrl()) { + mutableFlow.tryEmit( + Async.Success( + listOf( + HomeserverData( + userInput = userInput, + homeserverUrl = userInput, + isWellknownValid = false + ) + ) + ) + ) + } else { + mutableFlow.tryEmit(Async.Uninitialized) + } + } + } + } + } + + private fun getUrlCandidate(data: String): List { + 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") + } + } + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt index 6d254e3738..44dd583f00 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt @@ -16,7 +16,7 @@ package io.element.android.features.login.impl.changeaccountprovider.form -data class HomeserverData( +data class HomeserverData constructor( // 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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt index d80b20f1fd..a7abb444f6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt @@ -16,102 +16,13 @@ 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.architecture.Async -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.CoroutineScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import javax.inject.Inject /** - * Resolve homeserver base on search terms + * Resolve homeserver base on search terms. */ -class HomeserverResolver @Inject constructor( - private val dispatchers: CoroutineDispatchers, - private val wellknownRequest: WellknownRequest, -) { - private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) - - fun flow(): StateFlow>> = mutableFlow - - private var currentJob: Job? = null - - suspend fun accept(userInput: String) { - currentJob?.cancel() - val cleanedUpUserInput = userInput.trim() - mutableFlow.tryEmit(Async.Uninitialized) - if (cleanedUpUserInput.length > 3) { - delay(300) - mutableFlow.tryEmit(Async.Loading()) - withContext(dispatchers.io) { - val list = getUrlCandidate(cleanedUpUserInput) - currentJob = resolveList(userInput, list) - } - } - } - - private fun CoroutineScope.resolveList(userInput: String, list: List): Job { - val currentList = mutableListOf() - return launch { - list.map { - async { - val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse() - if (isValid) { - // Emit the list as soon as possible - currentList.add(HomeserverData(userInput, it, true)) - mutableFlow.tryEmit(Async.Success(currentList)) - } - } - }.joinAll() - .also { - // If list is empty, and the user as entered an URL, do not block the user. - if (currentList.isEmpty()) { - if (userInput.isValidUrl()) { - mutableFlow.tryEmit( - Async.Success( - listOf( - HomeserverData( - userInput = userInput, - homeserverUrl = userInput, - isWellknownValid = false - ) - ) - ) - ) - } else { - mutableFlow.tryEmit(Async.Uninitialized) - } - } - } - } - } - - private fun getUrlCandidate(data: String): List { - 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") - } - } - } +interface HomeserverResolver { + fun flow(): StateFlow>> + suspend fun accept(userInput: String) } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt new file mode 100644 index 0000000000..e2b2a8f546 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt @@ -0,0 +1,87 @@ +/* + * 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 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.libraries.architecture.Async +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ChangeAccountProviderFormPresenterTest { + @Test + fun `present - initial state`() = runTest { + val homeServerResolver = FakeHomeServerResolver() + val presenter = ChangeAccountProviderFormPresenter( + homeServerResolver + ) + 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 homeServerResolver = FakeHomeServerResolver() + val presenter = ChangeAccountProviderFormPresenter( + homeServerResolver + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeAccountProviderFormEvents.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 text one then two results`() = runTest { + val homeServerResolver = FakeHomeServerResolver() + homeServerResolver.givenResult( + listOf( + listOf(aHomeserverData()), + listOf(aHomeserverData(), aHomeserverData()), + ) + ) + val presenter = ChangeAccountProviderFormPresenter( + homeServerResolver + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeAccountProviderFormEvents.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()))) + assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData(), aHomeserverData()))) + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt new file mode 100644 index 0000000000..db86e09903 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt @@ -0,0 +1,49 @@ +/* + * 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 +import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class FakeHomeServerResolver : HomeserverResolver { + private var pendingResult: List> = emptyList() + fun givenResult(result: List>) { + pendingResult = result + } + + private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) + + override fun flow(): StateFlow>> = mutableFlow + + override suspend fun accept(userInput: String) { + mutableFlow.tryEmit(Async.Uninitialized) + delay(FAKE_DELAY_IN_MS) + mutableFlow.tryEmit(Async.Loading()) + // Sending the pending result + if (pendingResult.isEmpty()) { + mutableFlow.tryEmit(Async.Uninitialized) + } else { + pendingResult.forEach { + delay(FAKE_DELAY_IN_MS) + mutableFlow.tryEmit(Async.Success(it)) + } + } + } +} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenterTest.kt new file mode 100644 index 0000000000..996f0b88bf --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/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.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.datasource.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/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/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) } From c97751cce538e43f082e2977ecd59bb31a597274 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 16:33:30 +0200 Subject: [PATCH 018/130] Cleanup --- .../kotlin/io/element/android/appnav/LoggedInEventProcessor.kt | 2 -- 1 file changed, 2 deletions(-) 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, From 3eeeee2c9833feebcb017ba2f65ac51c4d21c40b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 16:43:56 +0200 Subject: [PATCH 019/130] Code quality. --- .../changeaccountprovider/form/DefaultHomeserverResolver.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt index 812f98f323..356dbf4f42 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt @@ -111,9 +111,9 @@ class DefaultHomeserverResolver @Inject constructor( if (s.contains(".")) { // TLD detected? } else { - add("$s.org") - add("$s.com") - add("$s.io") + add("${s}.org") + add("${s}.com") + add("${s}.io") } } } From b4723bb1827eb2c58b6fbc2b0dc2195f0f8b158c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 16:55:37 +0200 Subject: [PATCH 020/130] Fix some errors --- .../item/AccountProviderView.kt | 8 +-- .../ChangeAccountProviderEvents.kt | 20 ------ .../form/ChangeAccountProviderFormView.kt | 5 +- .../form/network/WellKnown.kt | 2 +- .../form/network/WellKnownBaseConfig.kt | 2 +- .../form/network/WellknownAPI.kt | 1 + .../form/network/WellknownRequest.kt | 2 +- .../impl/loginpassword/LoginPasswordView.kt | 12 ++-- .../atomic/atoms/SeparatorAtom.kt | 62 ------------------- 9 files changed, 16 insertions(+), 98 deletions(-) delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt delete mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SeparatorAtom.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt index eb2f67947e..ae3aea8878 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt @@ -38,9 +38,9 @@ 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.atomic.atoms.SeparatorAtom 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 @@ -56,7 +56,7 @@ fun AccountProviderView( Column(modifier = modifier .fillMaxWidth() .clickable { onClick() }) { - SeparatorAtom() + Divider() Column( modifier = Modifier .fillMaxWidth() @@ -82,7 +82,7 @@ fun AccountProviderView( ) } Text( - modifier = modifier + modifier = Modifier .padding(start = 16.dp) .weight(1f), text = item.title, @@ -102,7 +102,7 @@ fun AccountProviderView( } if (item.subtitle != null) { Text( - modifier = modifier + modifier = Modifier .padding(start = 46.dp, bottom = 12.dp, end = 26.dp), text = item.subtitle, style = ElementTextStyles.Regular.subheadline.copy(textAlign = TextAlign.Start), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt deleted file mode 100644 index 0579006dfc..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderEvents.kt +++ /dev/null @@ -1,20 +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.changeaccountprovider - -sealed interface ChangeAccountProviderEvents { -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt index 9e00d9c47d..311eb21684 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) package io.element.android.features.login.impl.changeaccountprovider.form @@ -71,7 +71,7 @@ import io.element.android.libraries.designsystem.theme.components.Text fun ChangeAccountProviderFormView( state: ChangeAccountProviderFormState, modifier: Modifier = Modifier, - onBackPressed: () -> Unit, + onBackPressed: () -> Unit = {}, onAccountProviderClicked: (AccountProvider) -> Unit = {}, ) { val eventSink = state.eventSink @@ -197,6 +197,5 @@ fun ChangeAccountProviderFormViewDarkPreview(@PreviewParameter(ChangeAccountProv private fun ContentToPreview(state: ChangeAccountProviderFormState) { ChangeAccountProviderFormView( state = state, - onBackPressed = { } ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt index a78f41a266..60006e7530 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * 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. diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt index b0d33e1854..619b656d22 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Matrix.org Foundation C.I.C. + * 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. diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt index 13153cfabb..0362a67c60 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt @@ -13,6 +13,7 @@ * 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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt index a8d911ee86..709eea0255 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt @@ -24,7 +24,7 @@ class WellknownRequest @Inject constructor( private val retrofitFactory: RetrofitFactory, ) { /** - * Return true if the wellknown can be retrieved and is valid + * Return true if the wellknown can be retrieved and is valid. * @param baseUrl for instance https://matrix.org */ suspend fun execute(baseUrl: String): Boolean { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt index c7decb8cb7..958ab94ae3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt @@ -139,13 +139,13 @@ fun LoginPasswordView( submit = ::submit ) } - } - } - if (state.loginAction is Async.Failure) { - LoginErrorDialog(error = state.loginAction.error, onDismiss = { - state.eventSink(LoginPasswordEvents.ClearError) - }) + if (state.loginAction is Async.Failure) { + LoginErrorDialog(error = state.loginAction.error, onDismiss = { + state.eventSink(LoginPasswordEvents.ClearError) + }) + } + } } } diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SeparatorAtom.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SeparatorAtom.kt deleted file mode 100644 index fe15ae5480..0000000000 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/atomic/atoms/SeparatorAtom.kt +++ /dev/null @@ -1,62 +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.libraries.designsystem.atomic.atoms - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -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 - -@Composable -fun SeparatorAtom( - modifier: Modifier = Modifier, - horizontalPadding: Dp = 0.dp, -) { - Spacer( - modifier = modifier - .height(0.5f.dp) - .fillMaxWidth() - .padding(horizontal = horizontalPadding) - .background( - color = LocalColors.current.quinary, - ) - ) -} - -@Preview -@Composable -internal fun SeparatorAtomLightPreview() = - ElementPreviewLight { ContentToPreview() } - -@Preview -@Composable -internal fun SeparatorAtomDarkPreview() = - ElementPreviewDark { ContentToPreview() } - -@Composable -private fun ContentToPreview() { - SeparatorAtom(horizontalPadding = 20.dp) -} From de31591eba160f4d2218c5f72614d37d80a0f5c0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 7 Jun 2023 16:58:31 +0200 Subject: [PATCH 021/130] Moar fixes. --- .../form/ChangeAccountProviderFormEvents.kt | 2 +- ...ountProviderState.kt => ChangeAccountProviderFormState.kt} | 0 ...eProvider.kt => ChangeAccountProviderFormStateProvider.kt} | 2 +- .../form/ChangeAccountProviderFormView.kt | 4 ++-- .../changeaccountprovider/form/DefaultHomeserverResolver.kt | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/{ChangeAccountProviderState.kt => ChangeAccountProviderFormState.kt} (100%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/{ChangeAccountProviderStateProvider.kt => ChangeAccountProviderFormStateProvider.kt} (96%) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt index 2e1180a211..79bbc5cc95 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt @@ -18,7 +18,7 @@ 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 + * The user has typed something, expect to get a list of result in the state. */ data class UserInput(val input: String) : ChangeAccountProviderFormEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt similarity index 100% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderState.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt index 6cc00dd0ee..f5232f8239 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt @@ -19,7 +19,7 @@ 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 { +open class ChangeAccountProviderFormStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aChangeAccountProviderFormState(), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt index 311eb21684..e301cfd546 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt @@ -185,12 +185,12 @@ fun ChangeAccountProviderFormView( @Preview @Composable -fun ChangeAccountProviderFormViewLightPreview(@PreviewParameter(ChangeAccountProviderStateFormProvider::class) state: ChangeAccountProviderFormState) = +fun ChangeAccountProviderFormViewLightPreview(@PreviewParameter(ChangeAccountProviderFormStateProvider::class) state: ChangeAccountProviderFormState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -fun ChangeAccountProviderFormViewDarkPreview(@PreviewParameter(ChangeAccountProviderStateFormProvider::class) state: ChangeAccountProviderFormState) = +fun ChangeAccountProviderFormViewDarkPreview(@PreviewParameter(ChangeAccountProviderFormStateProvider::class) state: ChangeAccountProviderFormState) = ElementPreviewDark { ContentToPreview(state) } @Composable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt index 356dbf4f42..73db831ac5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.withContext import javax.inject.Inject /** - * Resolve homeserver base on search terms + * Resolve homeserver base on search terms. */ @ContributesBinding(AppScope::class) class DefaultHomeserverResolver @Inject constructor( From 248d0bad83a444de4d9aaa2315b91c9db89bb0aa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jun 2023 10:32:24 +0200 Subject: [PATCH 022/130] Moar fixes --- .../ChangeAccountProviderView.kt | 2 +- .../impl/loginpassword/LoginPasswordView.kt | 41 +++++++------------ 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt index f0adfd101b..4c7dab1961 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt @@ -56,7 +56,7 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar fun ChangeAccountProviderView( state: ChangeAccountProviderState, modifier: Modifier = Modifier, - onBackPressed: () -> Unit, + onBackPressed: () -> Unit = {}, onAccountProviderClicked: (AccountProvider) -> Unit = {}, onOtherProviderClicked: () -> Unit = {}, ) { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt index 958ab94ae3..d52843ea5e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt @@ -133,11 +133,22 @@ fun LoginPasswordView( subTitle = stringResource(id = R.string.screen_login_form_header) ) Spacer(Modifier.height(32.dp)) - ServerDetailForm( - state = state, + LoginForm(state = state, isLoading = isLoading, - submit = ::submit + 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() + .testTag(TestTags.loginContinue) + ) + Spacer(modifier = Modifier.height(32.dp)) } if (state.loginAction is Async.Failure) { @@ -149,30 +160,6 @@ fun LoginPasswordView( } } -@Composable -fun ServerDetailForm( - state: LoginPasswordState, - isLoading: Boolean, - submit: () -> Unit, - modifier: Modifier = Modifier, -) { - LoginForm(state = state, isLoading = isLoading, onSubmit = submit, modifier = modifier) - - Spacer(Modifier.height(28.dp)) - - // 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)) -} - @OptIn(ExperimentalComposeUiApi::class) @Composable internal fun LoginForm( From 48a95c6e0110294715c80b857b508e8bec17b361 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jun 2023 14:32:58 +0200 Subject: [PATCH 023/130] Get more info from WellKnown request. --- .../accountprovider/item/AccountProvider.kt | 2 ++ .../item/AccountProviderProvider.kt | 6 +++- .../ChangeAccountProviderPresenter.kt | 2 ++ .../ChangeAccountProviderFormStateProvider.kt | 25 ++++++------- .../form/ChangeAccountProviderFormView.kt | 21 +++++++---- .../form/DefaultHomeserverResolver.kt | 35 +++++++++++-------- .../form/HomeserverData.kt | 4 +-- .../form/network/WellKnown.kt | 17 ++++++++- .../network/WellKnownSlidyincSyncConfig.kt | 26 ++++++++++++++ .../form/network/WellknownRequest.kt | 19 ++-------- .../ChangeAccountProviderPresenterTest.kt | 2 ++ 11 files changed, 103 insertions(+), 56 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidyincSyncConfig.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt index 6f8adff403..c09d6fab9b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt @@ -21,4 +21,6 @@ data class AccountProvider constructor( 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/item/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt index c931c3e801..211aa4f5bc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt @@ -23,7 +23,9 @@ open class AccountProviderProvider : PreviewParameterProvider { get() = sequenceOf( anAccountProvider(), anAccountProvider().copy(subtitle = null), - anAccountProvider().copy(title = "Other", subtitle = null, isPublic = false, isMatrixOrg = false), + 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 ) } @@ -33,4 +35,6 @@ fun anAccountProvider() = AccountProvider( 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/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt index 055541ca09..f80610d935 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -34,6 +34,8 @@ class ChangeAccountProviderPresenter @Inject constructor( subtitle = null, isPublic = true, isMatrixOrg = true, + isValid = true, + supportSlidingSync = true, ) ), ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt index f5232f8239..c73cbc5acb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt @@ -39,23 +39,20 @@ fun aChangeAccountProviderFormState( fun aHomeserverDataList(): List { return listOf( - HomeserverData( - userInput = "matrix", - homeserverUrl = "https://matrix.org", - isWellknownValid = true, - ), - HomeserverData( - userInput = "matrix", - homeserverUrl = "https://matrix.io", - isWellknownValid = false, - ) + 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(): HomeserverData { +fun aHomeserverData( + homeserverUrl: String = "https://matrix.org", + isWellknownValid: Boolean = true, + supportSlidingSync: Boolean = true, +): HomeserverData { return HomeserverData( - userInput = "matrix", - homeserverUrl = "https://matrix.org", - isWellknownValid = true, + homeserverUrl = homeserverUrl, + isWellknownValid = isWellknownValid, + supportSlidingSync = supportSlidingSync, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt index e301cfd546..16fdd10c5c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt @@ -160,13 +160,7 @@ fun ChangeAccountProviderFormView( } is Async.Success -> { state.userInputResult.state.forEach { homeserverData -> - val isMatrixOrg = homeserverData.homeserverUrl == "https://matrix.org" - val item = AccountProvider( - 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, - ) + val item = homeserverData.toAccountProvider() AccountProviderView( item = item, onClick = { @@ -183,6 +177,19 @@ fun ChangeAccountProviderFormView( } } +@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 ChangeAccountProviderFormViewLightPreview(@PreviewParameter(ChangeAccountProviderFormStateProvider::class) state: ChangeAccountProviderFormState) = diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt index 73db831ac5..6f7274f35f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt @@ -43,7 +43,7 @@ import javax.inject.Inject class DefaultHomeserverResolver @Inject constructor( private val dispatchers: CoroutineDispatchers, private val wellknownRequest: WellknownRequest, -): HomeserverResolver { +) : HomeserverResolver { private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) override fun flow(): StateFlow>> = mutableFlow @@ -52,14 +52,14 @@ class DefaultHomeserverResolver @Inject constructor( override suspend fun accept(userInput: String) { currentJob?.cancel() - val cleanedUpUserInput = userInput.trim() + val cleanedUpUserInput = userInput.trim().ensureProtocol().removeSuffix("/") mutableFlow.tryEmit(Async.Uninitialized) if (cleanedUpUserInput.length > 3) { delay(300) mutableFlow.tryEmit(Async.Loading()) withContext(dispatchers.io) { val list = getUrlCandidate(cleanedUpUserInput) - currentJob = resolveList(userInput, list) + currentJob = resolveList(cleanedUpUserInput, list) } } } @@ -69,10 +69,18 @@ class DefaultHomeserverResolver @Inject constructor( return launch { list.map { async { - val isValid = tryOrNull { wellknownRequest.execute(it) }.orFalse() + val wellKnown = tryOrNull { wellknownRequest.execute(it) } + val isValid = wellKnown?.isValid().orFalse() + val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse() if (isValid) { // Emit the list as soon as possible - currentList.add(HomeserverData(userInput, it, true)) + currentList.add( + HomeserverData( + homeserverUrl = it, + isWellknownValid = true, + supportSlidingSync = supportSlidingSync + ) + ) mutableFlow.tryEmit(Async.Success(currentList)) } } @@ -85,9 +93,9 @@ class DefaultHomeserverResolver @Inject constructor( Async.Success( listOf( HomeserverData( - userInput = userInput, homeserverUrl = userInput, - isWellknownValid = false + isWellknownValid = false, + supportSlidingSync = false, ) ) ) @@ -102,18 +110,15 @@ class DefaultHomeserverResolver @Inject constructor( private fun getUrlCandidate(data: String): List { return buildList { - val s = data.ensureProtocol() - .removeSuffix("/") - // Always try what the user has entered - add(s) + add(data) - if (s.contains(".")) { + if (data.contains(".")) { // TLD detected? } else { - add("${s}.org") - add("${s}.com") - add("${s}.io") + add("${data}.org") + add("${data}.com") + add("${data}.io") } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt index 44dd583f00..e0d7dbc0d5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt @@ -17,10 +17,10 @@ package io.element.android.features.login.impl.changeaccountprovider.form data class HomeserverData constructor( - // 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, + // 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/changeaccountprovider/form/network/WellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt index 60006e7530..283e8eb496 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt @@ -16,6 +16,7 @@ package io.element.android.features.login.impl.changeaccountprovider.form.network +import io.element.android.libraries.core.bool.orFalse import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -28,6 +29,9 @@ import kotlinx.serialization.Serializable * }, * "m.identity_server": { * "base_url": "https://vector.im" + * }, + * "org.matrix.msc3575.proxy": { + * "url": "https://slidingsync.lab.matrix.org" * } * } * @@ -40,4 +44,15 @@ data class WellKnown( @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/changeaccountprovider/form/network/WellKnownSlidyincSyncConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidyincSyncConfig.kt new file mode 100644 index 0000000000..4ff8633d0d --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidyincSyncConfig.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.changeaccountprovider.form.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/changeaccountprovider/form/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt index 709eea0255..1f18edc695 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt @@ -15,32 +15,19 @@ */ 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. + * Return the WellKnown data, if found. * @param baseUrl for instance https://matrix.org */ - suspend fun execute(baseUrl: String): Boolean { + suspend fun execute(baseUrl: String): WellKnown { val wellknownApi = retrofitFactory.create(baseUrl) .create(WellknownAPI::class.java) - - return try { - val response = wellknownApi.getWellKnown() - response.isValid() - } catch (throwable: Throwable) { - Timber.e(throwable) - false - } + return wellknownApi.getWellKnown() } } - -private fun WellKnown.isValid(): Boolean { - return homeServer?.baseURL?.isNotBlank().orFalse() -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index add636a4c4..911ecaab78 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -40,6 +40,8 @@ class ChangeAccountProviderPresenterTest { subtitle = null, isPublic = true, isMatrixOrg = true, + isValid = true, + supportSlidingSync = true, ) ) ) From 9a07b72a0ee0c128dc61bbff56546847737072b4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jun 2023 15:52:45 +0200 Subject: [PATCH 024/130] Validate server on user click. --- .../features/login/impl/LoginFlowNode.kt | 7 +- .../accountprovider/AccountProviderNode.kt | 11 +-- .../accountprovider/AccountProviderView.kt | 39 ++++----- .../SlidingSyncNotSupportedDialog.kt | 8 +- .../ChangeAccountProviderNode.kt | 13 +-- .../ChangeAccountProviderPresenter.kt | 4 + .../ChangeAccountProviderState.kt | 2 + .../ChangeAccountProviderStateProvider.kt | 2 + .../ChangeAccountProviderView.kt | 12 ++- .../common/ChangeServerEvents.kt | 24 ++++++ .../common/ChangeServerPresenter.kt | 76 +++++++++++++++++ .../common/ChangeServerState.kt | 24 ++++++ .../common/ChangeServerStateProvider.kt | 32 +++++++ .../common/ChangeServerView.kt | 84 +++++++++++++++++++ .../form/ChangeAccountProviderFormNode.kt | 13 +-- .../ChangeAccountProviderFormPresenter.kt | 4 + .../form/ChangeAccountProviderFormState.kt | 2 + .../ChangeAccountProviderFormStateProvider.kt | 2 + .../form/ChangeAccountProviderFormView.kt | 12 ++- .../android/features/login/impl/util/Util.kt | 27 ++++++ .../components/async/AsyncFailure.kt | 6 +- 21 files changed, 353 insertions(+), 51 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerEvents.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenter.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerState.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerStateProvider.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/util/Util.kt 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 1c0ab78ee4..36b6d904e7 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 @@ -33,7 +33,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.login.impl.accountprovider.AccountProviderNode -import io.element.android.features.login.impl.accountprovider.item.AccountProvider 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.datasource.AccountProviderDataSource @@ -125,8 +124,7 @@ class LoginFlowNode @AssistedInject constructor( } NavTarget.ChangeAccountProvider -> { val callback = object : ChangeAccountProviderNode.Callback { - override fun onAccountProviderClicked(data: AccountProvider) { - accountProviderDataSource.userSelection(data) + override fun onDone() { // Go back to the Account Provider screen backstack.singleTop(NavTarget.AccountProvider) } @@ -140,8 +138,7 @@ class LoginFlowNode @AssistedInject constructor( } NavTarget.ChangeAccountProviderForm -> { val callback = object : ChangeAccountProviderFormNode.Callback { - override fun onAccountProviderClicked(data: AccountProvider) { - accountProviderDataSource.userSelection(data) + override fun onDone() { // Go back to the Account Provider screen backstack.singleTop(NavTarget.AccountProvider) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt index aac5068294..dfcf6fd034 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt @@ -16,9 +16,6 @@ package io.element.android.features.login.impl.accountprovider -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 @@ -29,10 +26,9 @@ 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.features.login.impl.util.openLearnMorePage import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails @@ -72,11 +68,6 @@ class AccountProviderNode @AssistedInject constructor( plugins().forEach { it.onChangeAccountProvider() } } - private fun openLearnMorePage(context: Context) { - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(LoginConstants.SLIDING_SYNC_READ_MORE_URL)) - tryOrNull { context.startActivity(intent) } - } - @Composable override fun View(modifier: Modifier) { val state = presenter.present() 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 index dcc219f93e..56ea41a3d9 100644 --- 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 @@ -60,8 +60,6 @@ fun AccountProviderView( } } val eventSink = state.eventSink - val invalidHomeserverError = (state.loginFlow as? Async.Failure)?.error as? ChangeServerError.InlineErrorMessage - val slidingSyncNotSupportedError = (state.loginFlow as? Async.Failure)?.error as? ChangeServerError.SlidingSyncAlert HeaderFooterPage( modifier = modifier, @@ -112,7 +110,26 @@ fun AccountProviderView( } ) { when (state.loginFlow) { - is Async.Failure -> Unit // Error dialog will be displayed + is Async.Failure -> { + when (val error = state.loginFlow.error) { + is ChangeServerError.InlineErrorMessage -> { + ErrorDialog( + content = error.message(), + onDismiss = { + eventSink.invoke(AccountProviderEvents.ClearError) + } + ) + } + is ChangeServerError.SlidingSyncAlert -> { + SlidingSyncNotSupportedDialog(onLearnMoreClicked = { + onLearnMoreClicked() + eventSink(AccountProviderEvents.ClearError) + }, onDismiss = { + eventSink(AccountProviderEvents.ClearError) + }) + } + } + } is Async.Loading -> Unit // The Continue button shows the loading state is Async.Success -> { when (val loginFlowState = state.loginFlow.state) { @@ -122,22 +139,6 @@ fun AccountProviderView( } Async.Uninitialized -> Unit } - if (slidingSyncNotSupportedError != null) { - SlidingSyncNotSupportedDialog(onLearnMoreClicked = { - onLearnMoreClicked() - eventSink(AccountProviderEvents.ClearError) - }, onDismiss = { - eventSink(AccountProviderEvents.ClearError) - }) - } - if (invalidHomeserverError != null) { - ErrorDialog( - content = invalidHomeserverError.message(), - onDismiss = { - eventSink.invoke(AccountProviderEvents.ClearError) - } - ) - } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt index 03cf3a754e..364b4d955a 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt @@ -17,14 +17,20 @@ package io.element.android.features.login.impl.accountprovider 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) { +internal fun SlidingSyncNotSupportedDialog( + onLearnMoreClicked: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { ConfirmationDialog( + modifier = modifier, onDismiss = onDismiss, submitText = stringResource(StringR.string.action_learn_more), onSubmitClicked = onLearnMoreClicked, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt index aafaee818d..9787efc9cc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt @@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider 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,7 +26,7 @@ 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.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) @@ -36,12 +37,12 @@ class ChangeAccountProviderNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun onAccountProviderClicked(data: AccountProvider) + fun onDone() fun onOtherClicked() } - private fun onAccountProviderClicked(data: AccountProvider) { - plugins().forEach { it.onAccountProviderClicked(data) } + private fun onDone() { + plugins().forEach { it.onDone() } } private fun onOtherClicked() { @@ -51,11 +52,13 @@ class ChangeAccountProviderNode @AssistedInject constructor( @Composable override fun View(modifier: Modifier) { val state = presenter.present() + val context = LocalContext.current ChangeAccountProviderView( state = state, modifier = modifier, onBackPressed = ::navigateUp, - onAccountProviderClicked = ::onAccountProviderClicked, + onLearnMoreClicked = { openLearnMorePage(context) }, + onDone = ::onDone, onOtherProviderClicked = ::onOtherClicked, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt index f80610d935..be1da695f8 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -18,14 +18,17 @@ package io.element.android.features.login.impl.changeaccountprovider import androidx.compose.runtime.Composable import io.element.android.features.login.impl.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.changeaccountprovider.common.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( @@ -38,6 +41,7 @@ class ChangeAccountProviderPresenter @Inject constructor( supportSlidingSync = true, ) ), + changeServerState = changeServerState, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt index 89a286518e..2fd472e5c5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt @@ -17,8 +17,10 @@ package io.element.android.features.login.impl.changeaccountprovider import io.element.android.features.login.impl.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.changeaccountprovider.common.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/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt index 627f11c742..01ffb65500 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt @@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.accountprovider.item.anAccountProvider +import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState open class ChangeAccountProviderStateProvider : PreviewParameterProvider { override val values: Sequence @@ -31,4 +32,5 @@ fun aChangeAccountProviderState() = ChangeAccountProviderState( accountProviders = listOf( anAccountProvider() ), + changeServerState = aChangeServerState(), ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt index 4c7dab1961..d413f807e0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt @@ -42,6 +42,8 @@ import androidx.compose.ui.unit.dp import io.element.android.features.login.impl.R import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.accountprovider.item.AccountProviderView +import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents +import io.element.android.features.login.impl.changeaccountprovider.common.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 @@ -57,7 +59,8 @@ fun ChangeAccountProviderView( state: ChangeAccountProviderState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onAccountProviderClicked: (AccountProvider) -> Unit = {}, + onLearnMoreClicked: () -> Unit = {}, + onDone: () -> Unit = {}, onOtherProviderClicked: () -> Unit = {}, ) { val scrollState = rememberScrollState() @@ -104,7 +107,7 @@ fun ChangeAccountProviderView( AccountProviderView( item = alteredItem, onClick = { - onAccountProviderClicked(alteredItem) + state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(alteredItem)) } ) } @@ -117,6 +120,11 @@ fun ChangeAccountProviderView( ) Spacer(Modifier.height(32.dp)) } + ChangeServerView( + state = state.changeServerState, + onLearnMoreClicked = onLearnMoreClicked, + onDone = onDone, + ) } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerEvents.kt new file mode 100644 index 0000000000..8cded25388 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerEvents.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.changeaccountprovider.common + +import io.element.android.features.login.impl.accountprovider.item.AccountProvider + +sealed interface 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/changeaccountprovider/common/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenter.kt new file mode 100644 index 0000000000..fcc4dc7885 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenter.kt @@ -0,0 +1,76 @@ +/* + * 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.common + +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 io.element.android.features.login.impl.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.datasource.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 +import javax.inject.Inject + +class ChangeServerPresenter @Inject constructor( + private val authenticationService: MatrixAuthenticationService, + private val accountProviderDataSource: AccountProviderDataSource, +) : Presenter { + + @Composable + override fun present(): ChangeServerState { + val localCoroutineScope = rememberCoroutineScope() + + val changeServerAction: MutableState> = remember { + mutableStateOf(Async.Uninitialized) + } + + fun handleEvents(event: ChangeServerEvents) { + when (event) { + is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction) + ChangeServerEvents.ClearError -> changeServerAction.value = Async.Uninitialized + } + } + + return ChangeServerState( + changeServerAction = changeServerAction.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.changeServer( + data: AccountProvider, + changeServerAction: MutableState>, + ) = launch { + suspend { + 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/changeaccountprovider/common/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerState.kt new file mode 100644 index 0000000000..fbe4c37117 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerState.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.changeaccountprovider.common + +import io.element.android.libraries.architecture.Async + +data class ChangeServerState( + val changeServerAction: Async, + val eventSink: (ChangeServerEvents) -> Unit +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerStateProvider.kt new file mode 100644 index 0000000000..b089ef9ff9 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerStateProvider.kt @@ -0,0 +1,32 @@ +/* + * 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.common + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async + +open class ChangeServerStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aChangeServerState(), + ) +} + +fun aChangeServerState() = ChangeServerState( + changeServerAction = Async.Uninitialized, + eventSink = {} +) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerView.kt new file mode 100644 index 0000000000..dd46fb5b2e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerView.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.changeaccountprovider.common + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.features.login.impl.accountprovider.SlidingSyncNotSupportedDialog +import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.libraries.architecture.Async +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 + +@Composable +fun ChangeServerView( + state: ChangeServerState, + modifier: Modifier = Modifier, + onLearnMoreClicked: () -> Unit = {}, + onDone: () -> Unit = {}, +) { + val eventSink = state.eventSink + when (state.changeServerAction) { + is Async.Failure -> { + when (val error = state.changeServerAction.error) { + is ChangeServerError.InlineErrorMessage -> { + ErrorDialog( + modifier = modifier, + content = error.message(), + onDismiss = { + eventSink.invoke(ChangeServerEvents.ClearError) + } + ) + } + is ChangeServerError.SlidingSyncAlert -> { + SlidingSyncNotSupportedDialog( + modifier = modifier, + onLearnMoreClicked = { + onLearnMoreClicked() + eventSink.invoke(ChangeServerEvents.ClearError) + }, onDismiss = { + eventSink.invoke(ChangeServerEvents.ClearError) + }) + } + } + } + is Async.Loading -> ProgressDialog() + is Async.Success -> onDone() + Async.Uninitialized -> Unit + } +} + +@Preview +@Composable +fun ChangeServerViewLightPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::class) state: ChangeServerState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: ChangeServerState) { + ChangeServerView( + state = state, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt index 8786497e60..dac0b01f01 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt @@ -18,6 +18,7 @@ package io.element.android.features.login.impl.changeaccountprovider.form 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,7 +26,7 @@ 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.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) @@ -36,21 +37,23 @@ class ChangeAccountProviderFormNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun onAccountProviderClicked(data: AccountProvider) + fun onDone() } - private fun onAccountProviderClicked(data: AccountProvider) { - plugins().forEach { it.onAccountProviderClicked(data) } + private fun onDone() { + plugins().forEach { it.onDone() } } @Composable override fun View(modifier: Modifier) { val state = presenter.present() + val context = LocalContext.current ChangeAccountProviderFormView( state = state, modifier = modifier, onBackPressed = ::navigateUp, - onAccountProviderClicked = ::onAccountProviderClicked + onLearnMoreClicked = { openLearnMorePage(context) }, + onDone = ::onDone, ) } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt index ed8c98bb33..5ba3146895 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter import io.element.android.libraries.architecture.Presenter import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -29,6 +30,7 @@ import javax.inject.Inject class ChangeAccountProviderFormPresenter @Inject constructor( private val homeserverResolver: HomeserverResolver, + private val changeServerPresenter: ChangeServerPresenter, ) : Presenter { @Composable @@ -38,6 +40,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor( val userInput = rememberSaveable { mutableStateOf("") } + val changeServerState = changeServerPresenter.present() val data by homeserverResolver.flow().collectAsState() fun handleEvents(event: ChangeAccountProviderFormEvents) { @@ -52,6 +55,7 @@ class ChangeAccountProviderFormPresenter @Inject constructor( return ChangeAccountProviderFormState( userInput = userInput.value, userInputResult = data, + changeServerState = changeServerState, eventSink = ::handleEvents ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt index e87180b954..f7b841bed3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt @@ -16,11 +16,13 @@ package io.element.android.features.login.impl.changeaccountprovider.form +import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState 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>, + val changeServerState: ChangeServerState, val eventSink: (ChangeAccountProviderFormEvents) -> Unit ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt index c73cbc5acb..5c1b4a092b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt @@ -17,6 +17,7 @@ package io.element.android.features.login.impl.changeaccountprovider.form import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState import io.element.android.libraries.architecture.Async open class ChangeAccountProviderFormStateProvider : PreviewParameterProvider { @@ -34,6 +35,7 @@ fun aChangeAccountProviderFormState( ) = ChangeAccountProviderFormState( userInput = userInput, userInputResult = userInputResult, + changeServerState = aChangeServerState(), eventSink = {} ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt index 16fdd10c5c..72868873eb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt @@ -52,6 +52,8 @@ import androidx.compose.ui.unit.dp import io.element.android.features.login.impl.R import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.accountprovider.item.AccountProviderView +import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents +import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView 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 @@ -72,7 +74,8 @@ fun ChangeAccountProviderFormView( state: ChangeAccountProviderFormState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onAccountProviderClicked: (AccountProvider) -> Unit = {}, + onLearnMoreClicked: () -> Unit = {}, + onDone: () -> Unit = {}, ) { val eventSink = state.eventSink val scrollState = rememberScrollState() @@ -164,7 +167,7 @@ fun ChangeAccountProviderFormView( AccountProviderView( item = item, onClick = { - onAccountProviderClicked(item) + state.changeServerState.eventSink.invoke(ChangeServerEvents.ChangeServer(item)) } ) } @@ -173,6 +176,11 @@ fun ChangeAccountProviderFormView( } Spacer(Modifier.height(32.dp)) } + ChangeServerView( + state = state.changeServerState, + onLearnMoreClicked = onLearnMoreClicked, + onDone = onDone, + ) } } } 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/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)) } } } From aa61c95e8c47e0b0c127036cbbd35e0077c22a10 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jun 2023 16:30:08 +0200 Subject: [PATCH 025/130] Test ChangeServerPresenter --- .../ChangeAccountProviderPresenterTest.kt | 8 ++ .../common/ChangeServerPresenterTest.kt | 91 +++++++++++++++++++ .../ChangeAccountProviderFormPresenterTest.kt | 24 ++++- 3 files changed, 120 insertions(+), 3 deletions(-) create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenterTest.kt diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index 911ecaab78..e6f4a84471 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -21,13 +21,21 @@ 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.item.AccountProvider +import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter +import io.element.android.features.login.impl.datasource.AccountProviderDataSource +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() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenterTest.kt new file mode 100644 index 0000000000..cc668f8217 --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenterTest.kt @@ -0,0 +1,91 @@ +/* + * 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.common + +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.item.AccountProvider +import io.element.android.features.login.impl.datasource.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 +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ChangeServerPresenterTest { + @Test + fun `present - initial state`() = runTest { + val presenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.changeServerAction).isEqualTo(Async.Uninitialized) + } + } + + @Test + 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)) + } + } + + @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/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt index e2b2a8f546..1a425993a8 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt @@ -20,7 +20,10 @@ 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.changeaccountprovider.common.ChangeServerPresenter +import io.element.android.features.login.impl.datasource.AccountProviderDataSource import io.element.android.libraries.architecture.Async +import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import kotlinx.coroutines.test.runTest import org.junit.Test @@ -28,8 +31,13 @@ class ChangeAccountProviderFormPresenterTest { @Test fun `present - initial state`() = runTest { val homeServerResolver = FakeHomeServerResolver() + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver + homeServerResolver, + changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -43,8 +51,13 @@ class ChangeAccountProviderFormPresenterTest { @Test fun `present - enter text no result`() = runTest { val homeServerResolver = FakeHomeServerResolver() + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver + homeServerResolver, + changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -68,8 +81,13 @@ class ChangeAccountProviderFormPresenterTest { listOf(aHomeserverData(), aHomeserverData()), ) ) + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver + homeServerResolver, + changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() From 5a82834459b3b2e1480769796f4933860793779f Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 8 Jun 2023 14:43:37 +0000 Subject: [PATCH 026/130] Update screenshots --- ...ccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...p_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ntProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ntProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...tProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...tProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...p_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...p_ChangeServerViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...p_ChangeServerViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...p_ChangeServerViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...p_ChangeServerViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 --- ...p_ChangeServerViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 --- ..._ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ..._ChangeServerViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ..._ChangeServerViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ..._ChangeServerViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ..._ChangeServerViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 --- ..._ChangeServerViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...up_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...up_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...p_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...p_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...p_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...up_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_8,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_9,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_6,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_7,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_8,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_9,NEXUS_5,1.0,en].png | 3 --- ...ifySelfSessionViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ifySelfSessionViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ifySelfSessionViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ifySelfSessionViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ifySelfSessionViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ifySelfSessionViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...roup_RoundedIconAtomDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...oup_RoundedIconAtomLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...itleSubtitleMoleculeDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...tleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 64 files changed, 82 insertions(+), 124 deletions(-) create mode 100644 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 create mode 100644 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 create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 create mode 100644 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 create mode 100644 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 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..e96d33b1db --- /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:c5b66e68165054b6c4e09c00b7ebcff13229373bdc0eb10520f111b9d15576cb +size 39804 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..3f672cd84a --- /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:0292719b35c9e9b75a783628300fab10f75bb857c040e6829ee9ec51e5abc8dd +size 39605 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_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.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +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.changeaccountprovider.common_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.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..20c687f808 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32ae0372d47d6e68e853fbcdeb00e7829d78944b2cb3c284a845a0d463247c2b +size 25670 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..1c8e3c110e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:905cbb22344e797506dcabcbad15cdb2ab5c2e0ec2deaf544fdc89fdeca2615d +size 47638 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e1a1286e15 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaf00b3d9944a98db7d15890f93bf82834fd7376d323347f03bba3830aa2caa6 +size 25431 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c08530f117 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99ed6c29df9a96d7d6e0dcd34770d9837def14f73fb6509a8ccaf85ef964a934 +size 47046 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.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.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.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.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.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.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.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 deleted file mode 100644 index 55945f00cb..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_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ac39dda453fbdf0f5cc3c6df2d9436a171c141c8be2ca2e2d2a2c7ee5d16b36f -size 39650 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 deleted file mode 100644 index 8c716007cc..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_0,NEXUS_5,1.0,en].png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:d692cd9030e6193691e676de62f68a9241cebf220a1f6c7986e095370e25c547 -size 37839 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.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..44a334df69 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:871df52acf33bad33443a9e8140b58d17e5a068e008c5414be06d45f3b3e89ce +size 31619 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a5aa6e658b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:871baa23b7ce95b95d55956f47cd1659408901c2a4221cb32231d4187a6861f8 +size 31653 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..44a334df69 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:871df52acf33bad33443a9e8140b58d17e5a068e008c5414be06d45f3b3e89ce +size 31619 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..71d1ec0eda --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82ee4c067a7396dc3f1247169f72792f027029a14f04ab512d4db1c0f1920996 +size 31509 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c4fa17263b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5b05b56e0b8b54e2d13df513d180f7fed3d20df558793dab4358edeed4bac751 +size 31493 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..71d1ec0eda --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:82ee4c067a7396dc3f1247169f72792f027029a14f04ab512d4db1c0f1920996 +size 31509 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.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..ccf74db6e2 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:31b41c8f96870545a06ef473206b6c1d3b24f0fa8626c18adbdc31bc592e363f +size 28957 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..5391ae55ba 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:296a54854a782fd3c358960b42cdfba0c6822656742daa7034d2c36b53d040fa +size 25959 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..486a20f3dc 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:4a399eaa0f0e8a33bf7eb2e524dd162265918ddb90fd3200d9bb48a9db75a2c3 +size 58921 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..89246cbcaa 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:17bec083f13ac0d24787cd171f232b673d9c3f9cbffba9161bf70acf48b30c85 +size 59343 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..9ac1730d01 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:2e5afbd696458ba2ec094da4bbee3b74faa32f0abbc215a1082eb45b628dfcaf +size 31835 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..8c57f95a28 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:a857ce94e305acd45c97af4ae2532bfa9310a1e1bf0e549c780f654a89065e5e +size 26160 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..a874edaa42 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:be000cc43901f8b61a12c93201de8026fd57654e6a66cf2d5507f0b29af76e6a +size 9227 From 52545bc6205bd6416063931e8d3729265180dd79 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jun 2023 16:56:20 +0200 Subject: [PATCH 027/130] Fix icon tint --- .../form/ChangeAccountProviderFormView.kt | 1 - .../features/login/impl/loginpassword/LoginPasswordView.kt | 4 ++-- .../atomic/molecules/IconTitleSubtitleMolecule.kt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt index 72868873eb..cb35774147 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt @@ -105,7 +105,6 @@ fun ChangeAccountProviderFormView( 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), ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt index d52843ea5e..04db2ea73b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt @@ -278,12 +278,12 @@ internal fun LoginErrorDialog(error: Throwable, onDismiss: () -> Unit) { @Preview @Composable -internal fun LoginRootScreenLightPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = +internal fun LoginPasswordViewLightPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun LoginRootScreenDarkPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = +internal fun LoginPasswordViewDarkPreview(@PreviewParameter(LoginPasswordStateProvider::class) state: LoginPasswordState) = ElementPreviewDark { ContentToPreview(state) } @Composable 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 06c111d069..9d348b706f 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 @@ -47,7 +47,7 @@ fun IconTitleSubtitleMolecule( modifier: Modifier = Modifier, iconResourceId: Int? = null, iconImageVector: ImageVector? = null, - iconTint: Color = Color.Unspecified, + iconTint: Color = MaterialTheme.colorScheme.primary, ) { Column(modifier) { RoundedIconAtom( From 763159651a648ab10372ff5317bd428b8cb70f3d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jun 2023 18:20:26 +0200 Subject: [PATCH 028/130] Rename file --- ...llKnownSlidyincSyncConfig.kt => WellKnownSlidingSyncConfig.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/{WellKnownSlidyincSyncConfig.kt => WellKnownSlidingSyncConfig.kt} (100%) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidyincSyncConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidingSyncConfig.kt similarity index 100% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidyincSyncConfig.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidingSyncConfig.kt From e1e984cfb0523549e5f2ce795bee9bd6f1ad5374 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jun 2023 22:04:12 +0200 Subject: [PATCH 029/130] Rework HomeserverResolver --- .../ChangeAccountProviderFormPresenter.kt | 33 +++---- .../form/DefaultHomeserverResolver.kt | 94 ++++++++----------- .../form/HomeserverResolver.kt | 5 +- .../form/FakeHomeServerResolver.kt | 18 ++-- 4 files changed, 67 insertions(+), 83 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt index 5ba3146895..6c4b07b3ed 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt @@ -17,15 +17,15 @@ package io.element.android.features.login.impl.changeaccountprovider.form import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter +import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch import javax.inject.Inject class ChangeAccountProviderFormPresenter @Inject constructor( @@ -35,33 +35,34 @@ class ChangeAccountProviderFormPresenter @Inject constructor( @Composable override fun present(): ChangeAccountProviderFormState { - val localCoroutineScope = rememberCoroutineScope() - - val userInput = rememberSaveable { + var userInput by rememberSaveable { mutableStateOf("") } val changeServerState = changeServerPresenter.present() - val data by homeserverResolver.flow().collectAsState() + + var data: Async> by remember { + mutableStateOf(Async.Uninitialized) + } + + LaunchedEffect(userInput) { + homeserverResolver.resolve(userInput).collect { + data = it + } + } fun handleEvents(event: ChangeAccountProviderFormEvents) { when (event) { is ChangeAccountProviderFormEvents.UserInput -> { - userInput.value = event.input - localCoroutineScope.userInput(event.input) + userInput = event.input } } } return ChangeAccountProviderFormState( - userInput = userInput.value, + userInput = userInput, userInputResult = data, changeServerState = changeServerState, eventSink = ::handleEvents ) } - - // Could be reworked using LaunchedEffect - private fun CoroutineScope.userInput(userInput: String) = launch { - homeserverResolver.accept(userInput) - } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt index 6f7274f35f..706b954b86 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt @@ -25,15 +25,14 @@ 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 io.element.android.libraries.di.AppScope -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Job import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.joinAll -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext +import java.util.Collections import javax.inject.Inject /** @@ -44,35 +43,25 @@ class DefaultHomeserverResolver @Inject constructor( private val dispatchers: CoroutineDispatchers, private val wellknownRequest: WellknownRequest, ) : HomeserverResolver { - private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) - override fun flow(): StateFlow>> = mutableFlow - - private var currentJob: Job? = null - - override suspend fun accept(userInput: String) { - currentJob?.cancel() - val cleanedUpUserInput = userInput.trim().ensureProtocol().removeSuffix("/") - mutableFlow.tryEmit(Async.Uninitialized) - if (cleanedUpUserInput.length > 3) { - delay(300) - mutableFlow.tryEmit(Async.Loading()) - withContext(dispatchers.io) { - val list = getUrlCandidate(cleanedUpUserInput) - currentJob = resolveList(cleanedUpUserInput, list) - } - } - } - - private fun CoroutineScope.resolveList(userInput: String, list: List): Job { - val currentList = mutableListOf() - return launch { + override suspend fun resolve(userInput: String): Flow>> = flow { + val flowContext = currentCoroutineContext() + emit(Async.Uninitialized) + // Debounce + delay(300) + val clean = userInput.trim() + if (clean.length < 4) return@flow + emit(Async.Loading()) + val list = getUrlCandidate(clean.ensureProtocol().removeSuffix("/")) + val currentList = Collections.synchronizedList(mutableListOf()) + // Run all the requests in parallel + withContext(dispatchers.io) { list.map { async { val wellKnown = tryOrNull { wellknownRequest.execute(it) } val isValid = wellKnown?.isValid().orFalse() - val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse() if (isValid) { + val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse() // Emit the list as soon as possible currentList.add( HomeserverData( @@ -81,38 +70,35 @@ class DefaultHomeserverResolver @Inject constructor( supportSlidingSync = supportSlidingSync ) ) - mutableFlow.tryEmit(Async.Success(currentList)) - } - } - }.joinAll() - .also { - // If list is empty, and the user as entered an URL, do not block the user. - if (currentList.isEmpty()) { - if (userInput.isValidUrl()) { - mutableFlow.tryEmit( - Async.Success( - listOf( - HomeserverData( - homeserverUrl = userInput, - isWellknownValid = false, - supportSlidingSync = false, - ) - ) - ) - ) - } else { - mutableFlow.tryEmit(Async.Uninitialized) + withContext(flowContext) { + emit(Async.Success(currentList)) } } } + }.awaitAll() + } + // If list is empty, and the user as entered an URL, do not block the user. + if (currentList.isEmpty()) { + if (userInput.isValidUrl()) { + emit( + Async.Success( + listOf( + HomeserverData( + homeserverUrl = userInput, + isWellknownValid = false, + supportSlidingSync = false, + ) + ) + ) + ) + } else { + emit(Async.Uninitialized) + } } } private fun getUrlCandidate(data: String): List { return buildList { - // Always try what the user has entered - add(data) - if (data.contains(".")) { // TLD detected? } else { @@ -120,6 +106,8 @@ class DefaultHomeserverResolver @Inject constructor( 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/changeaccountprovider/form/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt index a7abb444f6..0c8bcfc712 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt @@ -17,12 +17,11 @@ package io.element.android.features.login.impl.changeaccountprovider.form import io.element.android.libraries.architecture.Async -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow /** * Resolve homeserver base on search terms. */ interface HomeserverResolver { - fun flow(): StateFlow>> - suspend fun accept(userInput: String) + suspend fun resolve(userInput: String): Flow>> } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt index db86e09903..8a712aa4a0 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt @@ -19,8 +19,8 @@ package io.element.android.features.login.impl.changeaccountprovider.form import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow class FakeHomeServerResolver : HomeserverResolver { private var pendingResult: List> = emptyList() @@ -28,21 +28,17 @@ class FakeHomeServerResolver : HomeserverResolver { pendingResult = result } - private val mutableFlow: MutableStateFlow>> = MutableStateFlow(Async.Uninitialized) - - override fun flow(): StateFlow>> = mutableFlow - - override suspend fun accept(userInput: String) { - mutableFlow.tryEmit(Async.Uninitialized) + override suspend fun resolve(userInput: String): Flow>> = flow { + emit(Async.Uninitialized) delay(FAKE_DELAY_IN_MS) - mutableFlow.tryEmit(Async.Loading()) + emit(Async.Loading()) // Sending the pending result if (pendingResult.isEmpty()) { - mutableFlow.tryEmit(Async.Uninitialized) + emit(Async.Uninitialized) } else { pendingResult.forEach { delay(FAKE_DELAY_IN_MS) - mutableFlow.tryEmit(Async.Success(it)) + emit(Async.Success(it)) } } } From dbe77967049c551ab0cbc23a8eb2ba1c3f1c34a5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 8 Jun 2023 22:56:36 +0200 Subject: [PATCH 030/130] Add tests. --- features/login/impl/build.gradle.kts | 1 + .../form/DefaultHomeserverResolver.kt | 15 +-- .../form/network/DefaultWellknownRequest.kt | 36 ++++++ .../form/network/WellknownRequest.kt | 15 +-- .../ChangeAccountProviderFormPresenterTest.kt | 114 ++++++++++++++++-- .../form/FakeHomeServerResolver.kt | 45 ------- .../form/FakeWellknownRequest.kt | 31 +++++ 7 files changed, 180 insertions(+), 77 deletions(-) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt delete mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt create mode 100644 features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 5f4563c3a9..279ac27767 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -59,6 +59,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/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt index 706b954b86..1079e6d64b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt @@ -49,10 +49,11 @@ class DefaultHomeserverResolver @Inject constructor( emit(Async.Uninitialized) // Debounce delay(300) - val clean = userInput.trim() - if (clean.length < 4) return@flow + val trimmedUserInput = userInput.trim() + if (trimmedUserInput.length < 4) return@flow emit(Async.Loading()) - val list = getUrlCandidate(clean.ensureProtocol().removeSuffix("/")) + val candidateBase = trimmedUserInput.ensureProtocol().removeSuffix("/") + val list = getUrlCandidates(candidateBase) val currentList = Collections.synchronizedList(mutableListOf()) // Run all the requests in parallel withContext(dispatchers.io) { @@ -77,14 +78,14 @@ class DefaultHomeserverResolver @Inject constructor( } }.awaitAll() } - // If list is empty, and the user as entered an URL, do not block the user. + // If list is empty, and the user has entered an URL, do not block the user. if (currentList.isEmpty()) { - if (userInput.isValidUrl()) { + if (trimmedUserInput.isValidUrl()) { emit( Async.Success( listOf( HomeserverData( - homeserverUrl = userInput, + homeserverUrl = trimmedUserInput, isWellknownValid = false, supportSlidingSync = false, ) @@ -97,7 +98,7 @@ class DefaultHomeserverResolver @Inject constructor( } } - private fun getUrlCandidate(data: String): List { + private fun getUrlCandidates(data: String): List { return buildList { if (data.contains(".")) { // TLD detected? diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt new file mode 100644 index 0000000000..36aa47bbd0 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/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.changeaccountprovider.form.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/changeaccountprovider/form/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt index 1f18edc695..85d471edbd 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt @@ -15,19 +15,10 @@ */ package io.element.android.features.login.impl.changeaccountprovider.form.network -import io.element.android.libraries.network.RetrofitFactory -import javax.inject.Inject - -class WellknownRequest @Inject constructor( - private val retrofitFactory: RetrofitFactory, -) { +interface WellknownRequest { /** - * Return the WellKnown data, if found. + * Return the WellKnown data, or throw an error if not found. * @param baseUrl for instance https://matrix.org */ - suspend fun execute(baseUrl: String): WellKnown { - val wellknownApi = retrofitFactory.create(baseUrl) - .create(WellknownAPI::class.java) - return wellknownApi.getWellKnown() - } + suspend fun execute(baseUrl: String): WellKnown } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt index 1a425993a8..dff15a63c6 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt @@ -21,22 +21,27 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownBaseConfig +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownSlidingSyncConfig import io.element.android.features.login.impl.datasource.AccountProviderDataSource 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 ChangeAccountProviderFormPresenterTest { @Test fun `present - initial state`() = runTest { - val homeServerResolver = FakeHomeServerResolver() + val fakeWellknownRequest = FakeWellknownRequest() val changeServerPresenter = ChangeServerPresenter( FakeAuthenticationService(), AccountProviderDataSource() ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver, + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -50,13 +55,13 @@ class ChangeAccountProviderFormPresenterTest { @Test fun `present - enter text no result`() = runTest { - val homeServerResolver = FakeHomeServerResolver() + val fakeWellknownRequest = FakeWellknownRequest() val changeServerPresenter = ChangeServerPresenter( FakeAuthenticationService(), AccountProviderDataSource() ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver, + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -73,12 +78,41 @@ class ChangeAccountProviderFormPresenterTest { } @Test - fun `present - enter text one then two results`() = runTest { - val homeServerResolver = FakeHomeServerResolver() - homeServerResolver.givenResult( - listOf( - listOf(aHomeserverData()), - listOf(aHomeserverData(), aHomeserverData()), + fun `present - enter valid url no wellknown`() = runTest { + val fakeWellknownRequest = FakeWellknownRequest() + val changeServerPresenter = ChangeServerPresenter( + FakeAuthenticationService(), + AccountProviderDataSource() + ) + val presenter = ChangeAccountProviderFormPresenter( + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeAccountProviderFormEvents.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( @@ -86,7 +120,7 @@ class ChangeAccountProviderFormPresenterTest { AccountProviderDataSource() ) val presenter = ChangeAccountProviderFormPresenter( - homeServerResolver, + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -98,8 +132,62 @@ class ChangeAccountProviderFormPresenterTest { 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()))) - assertThat(awaitItem().userInputResult).isEqualTo(Async.Success(listOf(aHomeserverData(), aHomeserverData()))) + 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 = ChangeAccountProviderFormPresenter( + DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + changeServerPresenter + ) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeAccountProviderFormEvents.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/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt deleted file mode 100644 index 8a712aa4a0..0000000000 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeHomeServerResolver.kt +++ /dev/null @@ -1,45 +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.changeaccountprovider.form - -import io.element.android.libraries.architecture.Async -import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow - -class FakeHomeServerResolver : HomeserverResolver { - private var pendingResult: List> = emptyList() - fun givenResult(result: List>) { - pendingResult = result - } - - override suspend fun resolve(userInput: String): Flow>> = flow { - emit(Async.Uninitialized) - delay(FAKE_DELAY_IN_MS) - emit(Async.Loading()) - // Sending the pending result - if (pendingResult.isEmpty()) { - emit(Async.Uninitialized) - } else { - pendingResult.forEach { - delay(FAKE_DELAY_IN_MS) - emit(Async.Success(it)) - } - } - } -} diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt new file mode 100644 index 0000000000..0cc4a8435d --- /dev/null +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt @@ -0,0 +1,31 @@ +/* + * 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.WellKnown +import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest + +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") + } +} From e53c415075c811d08f43d6d5ce69b952d3aa3d53 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 8 Jun 2023 21:05:14 +0000 Subject: [PATCH 031/130] Update screenshots --- ...ccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ..._LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ..._LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...up_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...up_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...p_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 --- ...fySelfSessionViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...fySelfSessionViewLightPreview_0_null_5,NEXUS_5,1.0,en].png | 4 ++-- ...tleSubtitleMoleculeLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 21 files changed, 36 insertions(+), 36 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png 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 index e96d33b1db..406ee49b58 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5b66e68165054b6c4e09c00b7ebcff13229373bdc0eb10520f111b9d15576cb -size 39804 +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.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 index 3f672cd84a..3346d30e90 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0292719b35c9e9b75a783628300fab10f75bb857c040e6829ee9ec51e5abc8dd -size 39605 +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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.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.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 44a334df69..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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:871df52acf33bad33443a9e8140b58d17e5a068e008c5414be06d45f3b3e89ce -size 31619 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index a5aa6e658b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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:871baa23b7ce95b95d55956f47cd1659408901c2a4221cb32231d4187a6861f8 -size 31653 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenDarkPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 44a334df69..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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:871df52acf33bad33443a9e8140b58d17e5a068e008c5414be06d45f3b3e89ce -size 31619 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index 71d1ec0eda..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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:82ee4c067a7396dc3f1247169f72792f027029a14f04ab512d4db1c0f1920996 -size 31509 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index c4fa17263b..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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:5b05b56e0b8b54e2d13df513d180f7fed3d20df558793dab4358edeed4bac751 -size 31493 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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.loginpassword_null_DefaultGroup_LoginRootScreenLightPreview_0_null_2,NEXUS_5,1.0,en].png deleted file mode 100644 index 71d1ec0eda..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_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:82ee4c067a7396dc3f1247169f72792f027029a14f04ab512d4db1c0f1920996 -size 31509 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 ccf74db6e2..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:31b41c8f96870545a06ef473206b6c1d3b24f0fa8626c18adbdc31bc592e363f -size 28957 +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 5391ae55ba..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:296a54854a782fd3c358960b42cdfba0c6822656742daa7034d2c36b53d040fa -size 25959 +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 486a20f3dc..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:4a399eaa0f0e8a33bf7eb2e524dd162265918ddb90fd3200d9bb48a9db75a2c3 -size 58921 +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 89246cbcaa..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:17bec083f13ac0d24787cd171f232b673d9c3f9cbffba9161bf70acf48b30c85 -size 59343 +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 9ac1730d01..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:2e5afbd696458ba2ec094da4bbee3b74faa32f0abbc215a1082eb45b628dfcaf -size 31835 +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 8c57f95a28..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:a857ce94e305acd45c97af4ae2532bfa9310a1e1bf0e549c780f654a89065e5e -size 26160 +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.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 a874edaa42..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:be000cc43901f8b61a12c93201de8026fd57654e6a66cf2d5507f0b29af76e6a -size 9227 +oid sha256:b508532be8dd29f60e30c00923834ac21ff0664cf3521fb86cdcdd493e9bfff7 +size 9930 From b72172e548bf3f865267cc376e29a8d9d085f5f0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 09:28:50 +0200 Subject: [PATCH 032/130] Format --- features/login/impl/build.gradle.kts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index 279ac27767..aacf4c0aeb 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -19,7 +19,8 @@ plugins { alias(libs.plugins.anvil) alias(libs.plugins.ksp) id("kotlin-parcelize") - kotlin("plugin.serialization") version "1.8.21"} + kotlin("plugin.serialization") version "1.8.21" +} android { namespace = "io.element.android.features.login.impl" From 4c214db5c49675c810b3e59fc90bedcd07b8184d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 10:17:30 +0200 Subject: [PATCH 033/130] Update maestro test regarding the new login flow. --- .maestro/README.md | 2 +- .maestro/tests/account/changeServer.yaml | 13 ++++++++++++- .maestro/tests/account/login.yaml | 2 ++ .maestro/tests/assertions/assertLoginDisplayed.yaml | 2 +- .../impl/accountprovider/AccountProviderView.kt | 3 ++- .../form/ChangeAccountProviderFormView.kt | 5 ++++- .../element/android/libraries/testtags/TestTags.kt | 1 - 7 files changed, 22 insertions(+), 6 deletions(-) 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/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 index 56ea41a3d9..5c0ba23ab5 100644 --- 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 @@ -94,7 +94,7 @@ fun AccountProviderView( enabled = state.submitEnabled, modifier = Modifier .fillMaxWidth() - .testTag(TestTags.changeServerContinue) + .testTag(TestTags.loginContinue) ) TextButton( onClick = { @@ -103,6 +103,7 @@ fun AccountProviderView( enabled = true, modifier = Modifier .fillMaxWidth() + .testTag(TestTags.loginChangeServer) ) { Text(text = stringResource(id = R.string.screen_account_provider_change)) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt index cb35774147..8ab0659d7b 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt @@ -65,6 +65,8 @@ 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.testtags.TestTags +import io.element.android.libraries.testtags.testTag /** * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435 @@ -117,7 +119,8 @@ fun ChangeAccountProviderFormView( // readOnly = isLoading, modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 30.dp), + .padding(start = 16.dp, end = 16.dp, bottom = 30.dp) + .testTag(TestTags.changeServerServer), onValueChange = { userInputState = it eventSink(ChangeAccountProviderFormEvents.UserInput(it)) 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. From 0bb3d12e9d94bed6fa229f849c83b4c32c4ec07e Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 9 Jun 2023 09:01:02 +0000 Subject: [PATCH 034/130] Update screenshots --- ...ntProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ntProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...tProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...tProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 20c687f808..f90601bd76 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32ae0372d47d6e68e853fbcdeb00e7829d78944b2cb3c284a845a0d463247c2b -size 25670 +oid sha256:abc7f0b614bb7532341dd016fab0f5ba72a08f9718bb496b420c94edfcedad2f +size 25927 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 1c8e3c110e..ac844925a8 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:905cbb22344e797506dcabcbad15cdb2ab5c2e0ec2deaf544fdc89fdeca2615d -size 47638 +oid sha256:33dfbd2203f5d78760730e65c689182b54169988f10906ba987af9fe43d0d03d +size 47910 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index e1a1286e15..fe94473bb3 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aaf00b3d9944a98db7d15890f93bf82834fd7376d323347f03bba3830aa2caa6 -size 25431 +oid sha256:9d11e04a2440ecd57a4178a0e2c1aa2755c62d147e28c065c7b10a957fef35da +size 25669 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index c08530f117..a11e2c6787 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:99ed6c29df9a96d7d6e0dcd34770d9837def14f73fb6509a8ccaf85ef964a934 -size 47046 +oid sha256:3ebf802513c7372eaba77b4ebaabbc8e313c1b8f8b00ef8e496194586c9431dc +size 47340 From 67cca41e0dec36f9fd4172e0eeeda40e0e0eded2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 12:48:44 +0200 Subject: [PATCH 035/130] trigger CI From 06297bb792416f6bf568f8e2c1614e2a637d000c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 16:34:22 +0200 Subject: [PATCH 036/130] Update strings. --- .../login/impl/src/main/res/values/localazy.xml | 6 ++++++ .../impl/src/main/res/values/localazy.xml | 2 +- .../push/impl/src/main/res/values/localazy.xml | 1 + .../ui-strings/src/main/res/values/localazy.xml | 13 +++++++------ tools/localazy/config.json | 1 + 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 96d05df794..bb27ca51e1 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -26,6 +26,12 @@ "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" 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/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index d38bf7d8dd..922d35b3e1 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -7,6 +7,7 @@ "** Failed to send - please open room" "Join" "Reject" + "invited you" "New Messages" "Mark as read" "Me" diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index f3cada2464..4f3ca67296 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -34,6 +34,7 @@ "No" "Not now" "OK" + "Open with" "Quick reply" "Quote" "Remove" @@ -56,9 +57,11 @@ "View Source" "Yes" "About" + "Acceptable use policy" "Analytics" "Audio" "Bubbles" + "Copyright" "Creating room…" "Left room" "Decryption error" @@ -80,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" @@ -103,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" @@ -150,12 +157,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/tools/localazy/config.json b/tools/localazy/config.json index 8fa70179e3..a7317af0db 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -73,6 +73,7 @@ "name": ":features:login:impl", "includeRegex": [ "screen_login_.*", + "screen_server_confirmation_.*", "screen_change_server_.*", "screen_change_account_provider_.*", "screen_account_provider_.*" From dc064069babfe5eeccbf62deb9149ca2df29099b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 17:34:14 +0200 Subject: [PATCH 037/130] Rename nodes and organize by package into `screens` subpackage for clarity --- .../features/login/impl/LoginFlowNode.kt | 50 ++--- .../{item => }/AccountProvider.kt | 2 +- .../AccountProviderDataSource.kt | 3 +- .../{item => }/AccountProviderProvider.kt | 2 +- .../accountprovider/AccountProviderView.kt | 187 ++++++++---------- .../item/AccountProviderView.kt | 132 ------------- .../form/HomeserverResolver.kt | 27 --- .../ChangeServerEvents.kt | 4 +- .../ChangeServerPresenter.kt | 6 +- .../ChangeServerState.kt | 2 +- .../ChangeServerStateProvider.kt | 2 +- .../ChangeServerView.kt | 4 +- .../resolver}/HomeserverData.kt | 2 +- .../resolver/HomeserverResolver.kt} | 13 +- .../network/DefaultWellknownRequest.kt | 2 +- .../resolver}/network/WellKnown.kt | 2 +- .../resolver}/network/WellKnownBaseConfig.kt | 2 +- .../network/WellKnownSlidingSyncConfig.kt | 2 +- .../resolver}/network/WellknownAPI.kt | 2 +- .../resolver}/network/WellknownRequest.kt | 2 +- .../SlidingSyncNotSupportedDialog.kt | 2 +- .../ChangeAccountProviderNode.kt | 2 +- .../ChangeAccountProviderPresenter.kt | 6 +- .../ChangeAccountProviderState.kt | 6 +- .../ChangeAccountProviderStateProvider.kt | 6 +- .../ChangeAccountProviderView.kt | 10 +- .../ConfirmAccountProviderEvents.kt} | 8 +- .../ConfirmAccountProviderNode.kt} | 10 +- .../ConfirmAccountProviderPresenter.kt} | 30 +-- .../ConfirmAccountProviderState.kt} | 8 +- .../ConfirmAccountProviderStateProvider.kt} | 12 +- .../ConfirmAccountProviderView.kt | 162 +++++++++++++++ .../loginpassword/LoginPasswordEvents.kt | 2 +- .../loginpassword/LoginPasswordNode.kt | 2 +- .../loginpassword/LoginPasswordPresenter.kt | 4 +- .../loginpassword/LoginPasswordState.kt | 4 +- .../LoginPasswordStateProvider.kt | 4 +- .../loginpassword/LoginPasswordView.kt | 4 +- .../SearchAccountProviderEvents.kt} | 6 +- .../SearchAccountProviderNode.kt} | 8 +- .../SearchAccountProviderPresenter.kt} | 18 +- .../SearchAccountProviderState.kt} | 9 +- .../SearchAccountProviderStateProvider.kt} | 17 +- .../SearchAccountProviderView.kt} | 27 +-- .../login/impl/util/LoginConstants.kt | 2 +- .../ChangeServerPresenterTest.kt | 6 +- .../resolver/network}/FakeWellknownRequest.kt | 5 +- .../ChangeAccountProviderPresenterTest.kt | 8 +- .../ConfirmAccountProviderPresenterTest.kt} | 36 ++-- .../LoginPasswordPresenterTest.kt | 4 +- .../SearchAccountProviderPresenterTest.kt} | 44 +++-- .../android/samples/minimal/LoginScreen.kt | 6 +- 52 files changed, 450 insertions(+), 476 deletions(-) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/{item => }/AccountProvider.kt (92%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{datasource => accountprovider}/AccountProviderDataSource.kt (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/{item => }/AccountProviderProvider.kt (95%) delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/common => changeserver}/ChangeServerEvents.kt (82%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/common => changeserver}/ChangeServerPresenter.kt (92%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/common => changeserver}/ChangeServerState.kt (91%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/common => changeserver}/ChangeServerStateProvider.kt (93%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/common => changeserver}/ChangeServerView.kt (94%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form => changeserver/resolver}/HomeserverData.kt (93%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form/DefaultHomeserverResolver.kt => changeserver/resolver/HomeserverResolver.kt} (89%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form => changeserver/resolver}/network/DefaultWellknownRequest.kt (94%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form => changeserver/resolver}/network/WellKnown.kt (95%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form => changeserver/resolver}/network/WellKnownBaseConfig.kt (92%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form => changeserver/resolver}/network/WellKnownSlidingSyncConfig.kt (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form => changeserver/resolver}/network/WellknownAPI.kt (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form => changeserver/resolver}/network/WellknownRequest.kt (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{accountprovider => dialogs}/SlidingSyncNotSupportedDialog.kt (96%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/changeaccountprovider/ChangeAccountProviderNode.kt (96%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/changeaccountprovider/ChangeAccountProviderPresenter.kt (86%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/changeaccountprovider/ChangeAccountProviderState.kt (77%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/changeaccountprovider/ChangeAccountProviderStateProvider.kt (82%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/changeaccountprovider/ChangeAccountProviderView.kt (93%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{accountprovider/AccountProviderEvents.kt => screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt} (72%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{accountprovider/AccountProviderNode.kt => screens/confirmaccountprovider/ConfirmAccountProviderNode.kt} (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{accountprovider/AccountProviderPresenter.kt => screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt} (80%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{accountprovider/AccountProviderState.kt => screens/confirmaccountprovider/ConfirmAccountProviderState.kt} (82%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{accountprovider/AccountProviderStateProvider.kt => screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt} (68%) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/loginpassword/LoginPasswordEvents.kt (92%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/loginpassword/LoginPasswordNode.kt (95%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/loginpassword/LoginPasswordPresenter.kt (95%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/loginpassword/LoginPasswordState.kt (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/loginpassword/LoginPasswordStateProvider.kt (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{ => screens}/loginpassword/LoginPasswordView.kt (99%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form/ChangeAccountProviderFormEvents.kt => screens/searchaccountprovider/SearchAccountProviderEvents.kt} (77%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form/ChangeAccountProviderFormNode.kt => screens/searchaccountprovider/SearchAccountProviderNode.kt} (88%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt => screens/searchaccountprovider/SearchAccountProviderPresenter.kt} (74%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form/ChangeAccountProviderFormState.kt => screens/searchaccountprovider/SearchAccountProviderState.kt} (72%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt => screens/searchaccountprovider/SearchAccountProviderStateProvider.kt} (74%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form/ChangeAccountProviderFormView.kt => screens/searchaccountprovider/SearchAccountProviderView.kt} (88%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{changeaccountprovider/common => changeserver}/ChangeServerPresenterTest.kt (93%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form => changeserver/resolver/network}/FakeWellknownRequest.kt (78%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{ => screens}/changeaccountprovider/ChangeAccountProviderPresenterTest.kt (84%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{accountprovider/AccountProviderPresenterTest.kt => screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt} (80%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{ => screens}/loginpassword/LoginPasswordPresenterTest.kt (97%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt => screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt} (78%) 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 36b6d904e7..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 @@ -32,14 +32,14 @@ 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.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.datasource.AccountProviderDataSource -import io.element.android.features.login.impl.loginpassword.LoginPasswordNode +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.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 @@ -59,7 +59,7 @@ class LoginFlowNode @AssistedInject constructor( private val accountProviderDataSource: AccountProviderDataSource, ) : BackstackNode( backstack = BackStack( - initialElement = NavTarget.AccountProvider, // NavTarget.Root, + initialElement = NavTarget.ConfirmAccountProvider, savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, @@ -76,16 +76,16 @@ class LoginFlowNode @AssistedInject constructor( sealed interface NavTarget : Parcelable { @Parcelize - object AccountProvider : NavTarget + object ConfirmAccountProvider : NavTarget @Parcelize object ChangeAccountProvider : NavTarget @Parcelize - object ChangeAccountProviderForm : NavTarget + object SearchAccountProvider : NavTarget @Parcelize - object LoginPasswordForm : NavTarget + object LoginPassword : NavTarget @Parcelize data class OidcView(val oidcDetails: OidcDetails) : NavTarget @@ -93,15 +93,11 @@ class LoginFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { - is NavTarget.OidcView -> { - val input = OidcNode.Inputs(navTarget.oidcDetails) - createNode(buildContext, plugins = listOf(input)) - } - NavTarget.AccountProvider -> { - val inputs = AccountProviderNode.Inputs( + NavTarget.ConfirmAccountProvider -> { + val inputs = ConfirmAccountProviderNode.Inputs( isAccountCreation = inputs.isAccountCreation ) - val callback = object : AccountProviderNode.Callback { + val callback = object : ConfirmAccountProviderNode.Callback { override fun onOidcDetails(oidcDetails: OidcDetails) { if (customTabAvailabilityChecker.supportCustomTab()) { // In this case open a Chrome Custom tab @@ -113,42 +109,46 @@ class LoginFlowNode @AssistedInject constructor( } override fun onLoginPasswordNeeded() { - backstack.push(NavTarget.LoginPasswordForm) + backstack.push(NavTarget.LoginPassword) } override fun onChangeAccountProvider() { backstack.push(NavTarget.ChangeAccountProvider) } } - createNode(buildContext, plugins = listOf(inputs, callback)) + 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.AccountProvider) + backstack.singleTop(NavTarget.ConfirmAccountProvider) } override fun onOtherClicked() { - backstack.push(NavTarget.ChangeAccountProviderForm) + backstack.push(NavTarget.SearchAccountProvider) } } createNode(buildContext, plugins = listOf(callback)) } - NavTarget.ChangeAccountProviderForm -> { - val callback = object : ChangeAccountProviderFormNode.Callback { + NavTarget.SearchAccountProvider -> { + val callback = object : SearchAccountProviderNode.Callback { override fun onDone() { // Go back to the Account Provider screen - backstack.singleTop(NavTarget.AccountProvider) + backstack.singleTop(NavTarget.ConfirmAccountProvider) } } - createNode(buildContext, plugins = listOf(callback)) + createNode(buildContext, plugins = listOf(callback)) } - NavTarget.LoginPasswordForm -> { + NavTarget.LoginPassword -> { createNode(buildContext, plugins = listOf()) } + is NavTarget.OidcView -> { + val input = OidcNode.Inputs(navTarget.oidcDetails) + createNode(buildContext, plugins = listOf(input)) + } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt similarity index 92% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt index c09d6fab9b..b6aea81951 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider.item +package io.element.android.features.login.impl.accountprovider data class AccountProvider constructor( val title: String, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt index ebd1c15ae1..ea541285df 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/datasource/AccountProviderDataSource.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderDataSource.kt @@ -14,9 +14,8 @@ * limitations under the License. */ -package io.element.android.features.login.impl.datasource +package io.element.android.features.login.impl.accountprovider -import io.element.android.features.login.impl.accountprovider.item.AccountProvider import io.element.android.features.login.impl.util.defaultAccountProvider import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.SingleIn diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt index 211aa4f5bc..71e1abd591 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider.item +package io.element.android.features.login.impl.accountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider 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 index 5c0ba23ab5..0ceb65dea9 100644 --- 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 @@ -16,146 +16,117 @@ 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.AccountCircle +import androidx.compose.material.icons.filled.Search +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.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource +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.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.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 -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 +/** + * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=604-60817 + */ @Composable fun AccountProviderView( - state: AccountProviderState, + item: AccountProvider, modifier: Modifier = Modifier, - onOidcDetails: (OidcDetails) -> Unit = {}, - onLoginPasswordNeeded: () -> Unit = {}, - onLearnMoreClicked: () -> Unit = {}, - onChange: () -> Unit = {}, + onClick: () -> Unit, ) { - 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(AccountProviderEvents.Continue) }, - enabled = state.submitEnabled, + 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 - .fillMaxWidth() - .testTag(TestTags.loginContinue) + .padding(start = 16.dp) + .weight(1f), + text = item.title, + style = ElementTextStyles.Regular.headline.copy(textAlign = TextAlign.Start), + color = MaterialTheme.colorScheme.primary, ) - TextButton( - onClick = { - onChange() - }, - enabled = true, + 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 - .fillMaxWidth() - .testTag(TestTags.loginChangeServer) - ) { - Text(text = stringResource(id = R.string.screen_account_provider_change)) - } + .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, + ) } } - ) { - when (state.loginFlow) { - is Async.Failure -> { - when (val error = state.loginFlow.error) { - is ChangeServerError.InlineErrorMessage -> { - ErrorDialog( - content = error.message(), - onDismiss = { - eventSink.invoke(AccountProviderEvents.ClearError) - } - ) - } - is ChangeServerError.SlidingSyncAlert -> { - SlidingSyncNotSupportedDialog(onLearnMoreClicked = { - onLearnMoreClicked() - eventSink(AccountProviderEvents.ClearError) - }, onDismiss = { - eventSink(AccountProviderEvents.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 AccountProviderViewLightPreview(@PreviewParameter(AccountProviderStateProvider::class) state: AccountProviderState) = - ElementPreviewLight { ContentToPreview(state) } +fun AccountProviderViewLightPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) = + ElementPreviewLight { ContentToPreview(item) } @Preview @Composable -fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderStateProvider::class) state: AccountProviderState) = - ElementPreviewDark { ContentToPreview(state) } +fun AccountProviderViewDarkPreview(@PreviewParameter(AccountProviderProvider::class) item: AccountProvider) = + ElementPreviewDark { ContentToPreview(item) } @Composable -private fun ContentToPreview(state: AccountProviderState) { +private fun ContentToPreview(item: AccountProvider) { AccountProviderView( - state = state, + item = item, + onClick = { } ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt deleted file mode 100644 index ae3aea8878..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/item/AccountProviderView.kt +++ /dev/null @@ -1,132 +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.accountprovider.item - -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/changeaccountprovider/form/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt deleted file mode 100644 index 0c8bcfc712..0000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverResolver.kt +++ /dev/null @@ -1,27 +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.changeaccountprovider.form - -import io.element.android.libraries.architecture.Async -import kotlinx.coroutines.flow.Flow - -/** - * Resolve homeserver base on search terms. - */ -interface HomeserverResolver { - suspend fun resolve(userInput: String): Flow>> -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt similarity index 82% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt index 8cded25388..cd1cb7b4ce 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.common +package io.element.android.features.login.impl.changeserver -import io.element.android.features.login.impl.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider sealed interface ChangeServerEvents { data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt similarity index 92% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenter.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index fcc4dc7885..2e3f9548d6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.common +package io.element.android.features.login.impl.changeserver 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 io.element.android.features.login.impl.accountprovider.item.AccountProvider -import io.element.android.features.login.impl.datasource.AccountProviderDataSource +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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt similarity index 91% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerState.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt index fbe4c37117..e49fa1f2fe 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.common +package io.element.android.features.login.impl.changeserver import io.element.android.libraries.architecture.Async diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt index b089ef9ff9..90c2bff455 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.common +package io.element.android.features.login.impl.changeserver import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.Async diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt similarity index 94% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerView.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt index dd46fb5b2e..37d90f581c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.common +package io.element.android.features.login.impl.changeserver import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter -import io.element.android.features.login.impl.accountprovider.SlidingSyncNotSupportedDialog +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.components.ProgressDialog diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverData.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverData.kt index e0d7dbc0d5..04bcd0099d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/HomeserverData.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverData.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form +package io.element.android.features.login.impl.changeserver.resolver data class HomeserverData constructor( // The computed homeserver url, for which a wellknown file has been retrieved, or just a valid Url diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverResolver.kt similarity index 89% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverResolver.kt index 1079e6d64b..9409f285e5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/DefaultHomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverResolver.kt @@ -14,17 +14,15 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form +package io.element.android.features.login.impl.changeserver.resolver -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest +import io.element.android.features.login.impl.changeserver.resolver.network.WellknownRequest import io.element.android.libraries.architecture.Async 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 io.element.android.libraries.di.AppScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.currentCoroutineContext @@ -38,13 +36,12 @@ import javax.inject.Inject /** * Resolve homeserver base on search terms. */ -@ContributesBinding(AppScope::class) -class DefaultHomeserverResolver @Inject constructor( +class HomeserverResolver @Inject constructor( private val dispatchers: CoroutineDispatchers, private val wellknownRequest: WellknownRequest, -) : HomeserverResolver { +) { - override suspend fun resolve(userInput: String): Flow>> = flow { + suspend fun resolve(userInput: String): Flow>> = flow { val flowContext = currentCoroutineContext() emit(Async.Uninitialized) // Debounce diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/DefaultWellknownRequest.kt similarity index 94% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/DefaultWellknownRequest.kt index 36aa47bbd0..95960971c3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/DefaultWellknownRequest.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/DefaultWellknownRequest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form.network +package io.element.android.features.login.impl.changeserver.resolver.network import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnown.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnown.kt index 283e8eb496..af5a4f17f3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnown.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnown.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form.network +package io.element.android.features.login.impl.changeserver.resolver.network import io.element.android.libraries.core.bool.orFalse import kotlinx.serialization.SerialName diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownBaseConfig.kt similarity index 92% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownBaseConfig.kt index 619b656d22..3bac743d64 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownBaseConfig.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownBaseConfig.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form.network +package io.element.android.features.login.impl.changeserver.resolver.network import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidingSyncConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownSlidingSyncConfig.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidingSyncConfig.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownSlidingSyncConfig.kt index 4ff8633d0d..863b524f84 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellKnownSlidingSyncConfig.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownSlidingSyncConfig.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form.network +package io.element.android.features.login.impl.changeserver.resolver.network import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownAPI.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownAPI.kt index 0362a67c60..f4d4ba7c67 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownAPI.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownAPI.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form.network +package io.element.android.features.login.impl.changeserver.resolver.network import retrofit2.http.GET diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownRequest.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownRequest.kt index 85d471edbd..54c393e522 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/network/WellknownRequest.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownRequest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form.network +package io.element.android.features.login.impl.changeserver.resolver.network interface WellknownRequest { /** diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt index 364b4d955a..32eb1b0824 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/SlidingSyncNotSupportedDialog.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/dialogs/SlidingSyncNotSupportedDialog.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider +package io.element.android.features.login.impl.dialogs import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt index 9787efc9cc..45bf4489b5 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider +package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt similarity index 86% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt index be1da695f8..dfdb7dcf99 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenter.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider +package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.runtime.Composable -import io.element.android.features.login.impl.accountprovider.item.AccountProvider -import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter +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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt similarity index 77% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt index 2fd472e5c5..806ce5bc64 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderState.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider +package io.element.android.features.login.impl.screens.changeaccountprovider -import io.element.android.features.login.impl.accountprovider.item.AccountProvider -import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState +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( diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt similarity index 82% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt index 01ffb65500..403746f227 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderStateProvider.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider +package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.accountprovider.item.anAccountProvider -import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState +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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt index d413f807e0..d7ffbe3d8d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderView.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) -package io.element.android.features.login.impl.changeaccountprovider +package io.element.android.features.login.impl.screens.changeaccountprovider import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -40,10 +40,10 @@ 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.item.AccountProvider -import io.element.android.features.login.impl.accountprovider.item.AccountProviderView -import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents -import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView +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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt similarity index 72% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt index ec3a879c89..1ba3cc3028 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderEvents.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider +package io.element.android.features.login.impl.screens.confirmaccountprovider -sealed interface AccountProviderEvents { - object Continue : AccountProviderEvents - object ClearError : AccountProviderEvents +sealed interface ConfirmAccountProviderEvents { + object Continue : ConfirmAccountProviderEvents + object ClearError : ConfirmAccountProviderEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt index dfcf6fd034..7cef986013 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider +package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -33,10 +33,10 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.OidcDetails @ContributesNode(AppScope::class) -class AccountProviderNode @AssistedInject constructor( +class ConfirmAccountProviderNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - presenterFactory: AccountProviderPresenter.Factory, + presenterFactory: ConfirmAccountProviderPresenter.Factory, ) : Node(buildContext, plugins = plugins) { data class Inputs( @@ -45,7 +45,7 @@ class AccountProviderNode @AssistedInject constructor( private val inputs: Inputs = inputs() private val presenter = presenterFactory.create( - AccountProviderPresenterParams( + ConfirmAccountProviderPresenter.Params( isAccountCreation = inputs.isAccountCreation, ) ) @@ -72,7 +72,7 @@ class AccountProviderNode @AssistedInject constructor( override fun View(modifier: Modifier) { val state = presenter.present() val context = LocalContext.current - AccountProviderView( + ConfirmAccountProviderView( state = state, modifier = modifier, onOidcDetails = ::onOidcDetails, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt similarity index 80% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt index 5246f9c528..446d6611d0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider +package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -26,7 +26,7 @@ import androidx.compose.runtime.rememberCoroutineScope import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.login.impl.datasource.AccountProviderDataSource +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 @@ -37,23 +37,23 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.net.URL -data class AccountProviderPresenterParams( - val isAccountCreation: Boolean, -) - -class AccountProviderPresenter @AssistedInject constructor( - @Assisted private val params: AccountProviderPresenterParams, +class ConfirmAccountProviderPresenter @AssistedInject constructor( + @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, private val authenticationService: MatrixAuthenticationService -) : Presenter { +) : Presenter { + + data class Params( + val isAccountCreation: Boolean, + ) @AssistedFactory interface Factory { - fun create(params: AccountProviderPresenterParams): AccountProviderPresenter + fun create(params: Params): ConfirmAccountProviderPresenter } @Composable - override fun present(): AccountProviderState { + override fun present(): ConfirmAccountProviderState { val accountProvider by accountProviderDataSource.flow().collectAsState() val localCoroutineScope = rememberCoroutineScope() @@ -61,16 +61,16 @@ class AccountProviderPresenter @AssistedInject constructor( mutableStateOf(Async.Uninitialized) } - fun handleEvents(event: AccountProviderEvents) { + fun handleEvents(event: ConfirmAccountProviderEvents) { when (event) { - AccountProviderEvents.Continue -> { + ConfirmAccountProviderEvents.Continue -> { localCoroutineScope.submit(accountProvider.title, loginFlowAction) } - AccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized + ConfirmAccountProviderEvents.ClearError -> loginFlowAction.value = Async.Uninitialized } } - return AccountProviderState( + return ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = params.isAccountCreation, loginFlow = loginFlowAction.value, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt similarity index 82% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index 625ef0070d..a870b88c58 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -14,18 +14,18 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider +package io.element.android.features.login.impl.screens.confirmaccountprovider -import io.element.android.features.login.impl.accountprovider.item.AccountProvider +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 AccountProviderState( +data class ConfirmAccountProviderState( val accountProvider: AccountProvider, val isAccountCreation: Boolean, val loginFlow: Async, - val eventSink: (AccountProviderEvents) -> Unit + val eventSink: (ConfirmAccountProviderEvents) -> Unit ) { val submitEnabled: Boolean get() = accountProvider.title.isNotEmpty() && (loginFlow is Async.Uninitialized || loginFlow is Async.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt similarity index 68% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt index 56584c1935..d5f98f5716 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt @@ -14,21 +14,21 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider +package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.accountprovider.item.anAccountProvider +import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.libraries.architecture.Async -open class AccountProviderStateProvider : PreviewParameterProvider { - override val values: Sequence +open class ConfirmAccountProviderStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - anAccountProviderState(), + aConfirmAccountProviderState(), // Add other state here ) } -fun anAccountProviderState() = AccountProviderState( +fun aConfirmAccountProviderState() = ConfirmAccountProviderState( accountProvider = anAccountProvider(), isAccountCreation = false, loginFlow = Async.Uninitialized, 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..b16214b34a --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt @@ -0,0 +1,162 @@ +/* + * 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, + modifier: Modifier = Modifier, + onOidcDetails: (OidcDetails) -> Unit = {}, + onLoginPasswordNeeded: () -> Unit = {}, + onLearnMoreClicked: () -> Unit = {}, + onChange: () -> Unit = {}, +) { + 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.InlineErrorMessage -> { + 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, + ) +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt similarity index 92% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt index 44b442dd08..e6f23ca418 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordEvents.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.loginpassword +package io.element.android.features.login.impl.screens.loginpassword sealed interface LoginPasswordEvents { data class SetLogin(val login: String) : LoginPasswordEvents diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordNode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt index ea731f379b..630b08570c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.loginpassword +package io.element.android.features.login.impl.screens.loginpassword import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt index 96f749a89c..1fc4a10bbb 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.loginpassword +package io.element.android.features.login.impl.screens.loginpassword import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState @@ -24,7 +24,7 @@ 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.datasource.AccountProviderDataSource +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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt index 07bb7d1c51..c8fa2f4ad3 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordState.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.login.impl.loginpassword +package io.element.android.features.login.impl.screens.loginpassword import android.os.Parcelable -import io.element.android.features.login.impl.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.parcelize.Parcelize diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt index f53cfb168a..b4f5a84691 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordStateProvider.kt @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.features.login.impl.loginpassword +package io.element.android.features.login.impl.screens.loginpassword import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.accountprovider.item.anAccountProvider +import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.libraries.architecture.Async open class LoginPasswordStateProvider : PreviewParameterProvider { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt similarity index 99% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index 04db2ea73b..5016a61054 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordView.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,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.loginpassword +package io.element.android.features.login.impl.screens.loginpassword import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt similarity index 77% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt index 79bbc5cc95..9a6a175cab 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderEvents.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form +package io.element.android.features.login.impl.screens.searchaccountprovider -sealed interface ChangeAccountProviderFormEvents { +sealed interface SearchAccountProviderEvents { /** * The user has typed something, expect to get a list of result in the state. */ - data class UserInput(val input: String) : ChangeAccountProviderFormEvents + data class UserInput(val input: String) : SearchAccountProviderEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt similarity index 88% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt index dac0b01f01..7178a105f6 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormNode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderNode.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form +package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -30,10 +30,10 @@ import io.element.android.features.login.impl.util.openLearnMorePage import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) -class ChangeAccountProviderFormNode @AssistedInject constructor( +class SearchAccountProviderNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: ChangeAccountProviderFormPresenter, + private val presenter: SearchAccountProviderPresenter, ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { @@ -48,7 +48,7 @@ class ChangeAccountProviderFormNode @AssistedInject constructor( override fun View(modifier: Modifier) { val state = presenter.present() val context = LocalContext.current - ChangeAccountProviderFormView( + SearchAccountProviderView( state = state, modifier = modifier, onBackPressed = ::navigateUp, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt similarity index 74% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt index 6c4b07b3ed..1ff510ea94 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form +package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -23,18 +23,20 @@ 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.changeaccountprovider.common.ChangeServerPresenter +import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.changeserver.resolver.HomeserverData +import io.element.android.features.login.impl.changeserver.resolver.HomeserverResolver import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.Presenter import javax.inject.Inject -class ChangeAccountProviderFormPresenter @Inject constructor( +class SearchAccountProviderPresenter @Inject constructor( private val homeserverResolver: HomeserverResolver, private val changeServerPresenter: ChangeServerPresenter, -) : Presenter { +) : Presenter { @Composable - override fun present(): ChangeAccountProviderFormState { + override fun present(): SearchAccountProviderState { var userInput by rememberSaveable { mutableStateOf("") } @@ -50,15 +52,15 @@ class ChangeAccountProviderFormPresenter @Inject constructor( } } - fun handleEvents(event: ChangeAccountProviderFormEvents) { + fun handleEvents(event: SearchAccountProviderEvents) { when (event) { - is ChangeAccountProviderFormEvents.UserInput -> { + is SearchAccountProviderEvents.UserInput -> { userInput = event.input } } } - return ChangeAccountProviderFormState( + return SearchAccountProviderState( userInput = userInput, userInputResult = data, changeServerState = changeServerState, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt similarity index 72% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt index f7b841bed3..c89dc53aff 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderState.kt @@ -14,15 +14,16 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form +package io.element.android.features.login.impl.screens.searchaccountprovider -import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerState +import io.element.android.features.login.impl.changeserver.ChangeServerState +import io.element.android.features.login.impl.changeserver.resolver.HomeserverData import io.element.android.libraries.architecture.Async // Do not use default value, so no member get forgotten in the presenters. -data class ChangeAccountProviderFormState( +data class SearchAccountProviderState( val userInput: String, val userInputResult: Async>, val changeServerState: ChangeServerState, - val eventSink: (ChangeAccountProviderFormEvents) -> Unit + val eventSink: (SearchAccountProviderEvents) -> Unit ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt similarity index 74% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt index 5c1b4a092b..9c15a63802 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderStateProvider.kt @@ -14,25 +14,26 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form +package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.changeaccountprovider.common.aChangeServerState +import io.element.android.features.login.impl.changeserver.aChangeServerState +import io.element.android.features.login.impl.changeserver.resolver.HomeserverData import io.element.android.libraries.architecture.Async -open class ChangeAccountProviderFormStateProvider : PreviewParameterProvider { - override val values: Sequence +open class SearchAccountProviderStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aChangeAccountProviderFormState(), - aChangeAccountProviderFormState(userInputResult = Async.Success(aHomeserverDataList())), + aSearchAccountProviderState(), + aSearchAccountProviderState(userInputResult = Async.Success(aHomeserverDataList())), // Add other state here ) } -fun aChangeAccountProviderFormState( +fun aSearchAccountProviderState( userInput: String = "", userInputResult: Async> = Async.Uninitialized, -) = ChangeAccountProviderFormState( +) = SearchAccountProviderState( userInput = userInput, userInputResult = userInputResult, changeServerState = aChangeServerState(), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt similarity index 88% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index 8ab0659d7b..cc4d326d64 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) -package io.element.android.features.login.impl.changeaccountprovider.form +package io.element.android.features.login.impl.screens.searchaccountprovider import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -50,10 +50,11 @@ 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.item.AccountProvider -import io.element.android.features.login.impl.accountprovider.item.AccountProviderView -import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerEvents -import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerView +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.changeserver.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 @@ -72,8 +73,8 @@ import io.element.android.libraries.testtags.testTag * https://www.figma.com/file/o9p34zmiuEpZRyvZXJZAYL/FTUE?type=design&node-id=611-61435 */ @Composable -fun ChangeAccountProviderFormView( - state: ChangeAccountProviderFormState, +fun SearchAccountProviderView( + state: SearchAccountProviderState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, onLearnMoreClicked: () -> Unit = {}, @@ -123,7 +124,7 @@ fun ChangeAccountProviderFormView( .testTag(TestTags.changeServerServer), onValueChange = { userInputState = it - eventSink(ChangeAccountProviderFormEvents.UserInput(it)) + eventSink(SearchAccountProviderEvents.UserInput(it)) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Uri, @@ -135,7 +136,7 @@ fun ChangeAccountProviderFormView( { IconButton(onClick = { userInputState = "" - eventSink(ChangeAccountProviderFormEvents.UserInput("")) + eventSink(SearchAccountProviderEvents.UserInput("")) }) { Icon( imageVector = Icons.Filled.Close, @@ -202,17 +203,17 @@ private fun HomeserverData.toAccountProvider(): AccountProvider { @Preview @Composable -fun ChangeAccountProviderFormViewLightPreview(@PreviewParameter(ChangeAccountProviderFormStateProvider::class) state: ChangeAccountProviderFormState) = +fun SearchAccountProviderViewLightPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -fun ChangeAccountProviderFormViewDarkPreview(@PreviewParameter(ChangeAccountProviderFormStateProvider::class) state: ChangeAccountProviderFormState) = +fun SearchAccountProviderViewDarkPreview(@PreviewParameter(SearchAccountProviderStateProvider::class) state: SearchAccountProviderState) = ElementPreviewDark { ContentToPreview(state) } @Composable -private fun ContentToPreview(state: ChangeAccountProviderFormState) { - ChangeAccountProviderFormView( +private fun ContentToPreview(state: SearchAccountProviderState) { + SearchAccountProviderView( state = state, ) } 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 10f69c3d1a..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,7 +16,7 @@ package io.element.android.features.login.impl.util -import io.element.android.features.login.impl.accountprovider.item.AccountProvider +import io.element.android.features.login.impl.accountprovider.AccountProvider object LoginConstants { const val MATRIX_ORG_URL = "matrix.org" diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt similarity index 93% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenterTest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index cc668f8217..9aefafb382 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/common/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -14,14 +14,14 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.common +package io.element.android.features.login.impl.changeserver 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.item.AccountProvider -import io.element.android.features.login.impl.datasource.AccountProviderDataSource +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 diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/FakeWellknownRequest.kt similarity index 78% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/FakeWellknownRequest.kt index 0cc4a8435d..4dea24b1d7 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/FakeWellknownRequest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/FakeWellknownRequest.kt @@ -14,10 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form - -import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown -import io.element.android.features.login.impl.changeaccountprovider.form.network.WellknownRequest +package io.element.android.features.login.impl.changeserver.resolver.network class FakeWellknownRequest : WellknownRequest { private var resultMap: Map = emptyMap() diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt similarity index 84% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt index e6f4a84471..086428257a 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/ChangeAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/changeaccountprovider/ChangeAccountProviderPresenterTest.kt @@ -14,15 +14,15 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider +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.item.AccountProvider -import io.element.android.features.login.impl.changeaccountprovider.common.ChangeServerPresenter -import io.element.android.features.login.impl.datasource.AccountProviderDataSource +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 diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt similarity index 80% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenterTest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index 0e34c623ae..131d0d9298 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/accountprovider/AccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.login.impl.accountprovider +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.datasource.AccountProviderDataSource +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 @@ -30,11 +30,11 @@ import io.element.android.libraries.matrix.test.auth.FakeAuthenticationService import kotlinx.coroutines.test.runTest import org.junit.Test -class AccountProviderPresenterTest { +class ConfirmAccountProviderPresenterTest { @Test fun `present - initial test`() = runTest { - val presenter = AccountProviderPresenter( - AccountProviderPresenterParams(isAccountCreation = false), + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), AccountProviderDataSource(), FakeAuthenticationService(), ) @@ -52,8 +52,8 @@ class AccountProviderPresenterTest { @Test fun `present - continue password login`() = runTest { val authServer = FakeAuthenticationService() - val presenter = AccountProviderPresenter( - AccountProviderPresenterParams(isAccountCreation = false), + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), AccountProviderDataSource(), authServer, ) @@ -62,7 +62,7 @@ class AccountProviderPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(AccountProviderEvents.Continue) + initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) @@ -76,8 +76,8 @@ class AccountProviderPresenterTest { @Test fun `present - continue oidc`() = runTest { val authServer = FakeAuthenticationService() - val presenter = AccountProviderPresenter( - AccountProviderPresenterParams(isAccountCreation = false), + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), AccountProviderDataSource(), authServer, ) @@ -86,7 +86,7 @@ class AccountProviderPresenterTest { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(AccountProviderEvents.Continue) + initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() assertThat(loadingState.loginFlow).isInstanceOf(Async.Loading::class.java) @@ -100,8 +100,8 @@ class AccountProviderPresenterTest { @Test fun `present - submit fails`() = runTest { val authServer = FakeAuthenticationService() - val presenter = AccountProviderPresenter( - AccountProviderPresenterParams(isAccountCreation = false), + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), AccountProviderDataSource(), authServer, ) @@ -110,7 +110,7 @@ class AccountProviderPresenterTest { }.test { val initialState = awaitItem() authServer.givenChangeServerError(Throwable()) - initialState.eventSink.invoke(AccountProviderEvents.Continue) + initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val failureState = awaitItem() assertThat(failureState.submitEnabled).isFalse() @@ -121,8 +121,8 @@ class AccountProviderPresenterTest { @Test fun `present - clear error`() = runTest { val authenticationService = FakeAuthenticationService() - val presenter = AccountProviderPresenter( - AccountProviderPresenterParams(isAccountCreation = false), + val presenter = ConfirmAccountProviderPresenter( + ConfirmAccountProviderPresenter.Params(isAccountCreation = false), AccountProviderDataSource(), authenticationService, ) @@ -133,7 +133,7 @@ class AccountProviderPresenterTest { // Submit will return an error authenticationService.givenChangeServerError(A_THROWABLE) - initialState.eventSink(AccountProviderEvents.Continue) + initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading @@ -142,7 +142,7 @@ class AccountProviderPresenterTest { assertThat(submittedState.loginFlow).isInstanceOf(Async.Failure::class.java) // Assert the error is then cleared - submittedState.eventSink(AccountProviderEvents.ClearError) + 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/loginpassword/LoginPasswordPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt similarity index 97% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenterTest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt index 996f0b88bf..c4c8a97155 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/loginpassword/LoginPasswordPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordPresenterTest.kt @@ -14,13 +14,13 @@ * limitations under the License. */ -package io.element.android.features.login.impl.loginpassword +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.datasource.AccountProviderDataSource +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 diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt similarity index 78% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt index dff15a63c6..1337e0f228 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeaccountprovider/form/ChangeAccountProviderFormPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderPresenterTest.kt @@ -14,17 +14,19 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeaccountprovider.form +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.changeaccountprovider.common.ChangeServerPresenter -import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnown -import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownBaseConfig -import io.element.android.features.login.impl.changeaccountprovider.form.network.WellKnownSlidingSyncConfig -import io.element.android.features.login.impl.datasource.AccountProviderDataSource +import io.element.android.features.login.impl.changeserver.ChangeServerPresenter +import io.element.android.features.login.impl.changeserver.resolver.network.WellKnown +import io.element.android.features.login.impl.changeserver.resolver.network.WellKnownBaseConfig +import io.element.android.features.login.impl.changeserver.resolver.network.WellKnownSlidingSyncConfig +import io.element.android.features.login.impl.changeserver.resolver.HomeserverResolver +import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource +import io.element.android.features.login.impl.changeserver.resolver.network.FakeWellknownRequest 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 @@ -32,7 +34,7 @@ import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.test.runTest import org.junit.Test -class ChangeAccountProviderFormPresenterTest { +class SearchAccountProviderPresenterTest { @Test fun `present - initial state`() = runTest { val fakeWellknownRequest = FakeWellknownRequest() @@ -40,8 +42,8 @@ class ChangeAccountProviderFormPresenterTest { FakeAuthenticationService(), AccountProviderDataSource() ) - val presenter = ChangeAccountProviderFormPresenter( - DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -60,15 +62,15 @@ class ChangeAccountProviderFormPresenterTest { FakeAuthenticationService(), AccountProviderDataSource() ) - val presenter = ChangeAccountProviderFormPresenter( - DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test")) + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) val withInputState = awaitItem() assertThat(withInputState.userInput).isEqualTo("test") assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) @@ -84,15 +86,15 @@ class ChangeAccountProviderFormPresenterTest { FakeAuthenticationService(), AccountProviderDataSource() ) - val presenter = ChangeAccountProviderFormPresenter( - DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("https://test.org")) + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("https://test.org")) val withInputState = awaitItem() assertThat(withInputState.userInput).isEqualTo("https://test.org") assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) @@ -119,15 +121,15 @@ class ChangeAccountProviderFormPresenterTest { FakeAuthenticationService(), AccountProviderDataSource() ) - val presenter = ChangeAccountProviderFormPresenter( - DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test")) + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) val withInputState = awaitItem() assertThat(withInputState.userInput).isEqualTo("test") assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) @@ -154,15 +156,15 @@ class ChangeAccountProviderFormPresenterTest { FakeAuthenticationService(), AccountProviderDataSource() ) - val presenter = ChangeAccountProviderFormPresenter( - DefaultHomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + val presenter = SearchAccountProviderPresenter( + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { val initialState = awaitItem() - initialState.eventSink.invoke(ChangeAccountProviderFormEvents.UserInput("test")) + initialState.eventSink.invoke(SearchAccountProviderEvents.UserInput("test")) val withInputState = awaitItem() assertThat(withInputState.userInput).isEqualTo("test") assertThat(initialState.userInputResult).isEqualTo(Async.Uninitialized) 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 976a68f81c..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 @@ -20,9 +20,9 @@ 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.datasource.AccountProviderDataSource -import io.element.android.features.login.impl.loginpassword.LoginPasswordPresenter -import io.element.android.features.login.impl.loginpassword.LoginPasswordView +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 From 9fbc918f10ee1ae503ba46d2c2bf24fd1ab471f3 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 17:43:34 +0200 Subject: [PATCH 038/130] Enforce lambda parameters to be passed in. --- .../login/impl/changeserver/ChangeServerView.kt | 6 ++++-- .../ChangeAccountProviderView.kt | 13 ++++++++----- .../ConfirmAccountProviderView.kt | 12 ++++++++---- .../SearchAccountProviderView.kt | 9 ++++++--- 4 files changed, 26 insertions(+), 14 deletions(-) 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 37d90f581c..136a4a7d5b 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 @@ -31,9 +31,9 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight @Composable fun ChangeServerView( state: ChangeServerState, + onLearnMoreClicked: () -> Unit, + onDone: () -> Unit, modifier: Modifier = Modifier, - onLearnMoreClicked: () -> Unit = {}, - onDone: () -> Unit = {}, ) { val eventSink = state.eventSink when (state.changeServerAction) { @@ -80,5 +80,7 @@ fun ChangeServerViewDarkPreview(@PreviewParameter(ChangeServerStateProvider::cla private fun ContentToPreview(state: ChangeServerState) { ChangeServerView( state = state, + onLearnMoreClicked = {}, + onDone = {}, ) } 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 index d7ffbe3d8d..eb8ee11a1c 100644 --- 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 @@ -57,11 +57,11 @@ import io.element.android.libraries.designsystem.theme.components.TopAppBar @Composable fun ChangeAccountProviderView( state: ChangeAccountProviderState, + onBackPressed: () -> Unit, + onLearnMoreClicked: () -> Unit, + onDone: () -> Unit, + onOtherProviderClicked: () -> Unit, modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, - onLearnMoreClicked: () -> Unit = {}, - onDone: () -> Unit = {}, - onOtherProviderClicked: () -> Unit = {}, ) { val scrollState = rememberScrollState() @@ -143,6 +143,9 @@ fun ChangeAccountProviderViewDarkPreview(@PreviewParameter(ChangeAccountProvider private fun ContentToPreview(state: ChangeAccountProviderState) { ChangeAccountProviderView( state = state, - onBackPressed = { } + onBackPressed = { }, + onLearnMoreClicked = { }, + onDone = { }, + onOtherProviderClicked = { }, ) } 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 index b16214b34a..91b79bca77 100644 --- 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 @@ -49,11 +49,11 @@ import io.element.android.libraries.testtags.testTag @Composable fun ConfirmAccountProviderView( state: ConfirmAccountProviderState, + onOidcDetails: (OidcDetails) -> Unit, + onLoginPasswordNeeded: () -> Unit, + onLearnMoreClicked: () -> Unit, + onChange: () -> Unit, modifier: Modifier = Modifier, - onOidcDetails: (OidcDetails) -> Unit = {}, - onLoginPasswordNeeded: () -> Unit = {}, - onLearnMoreClicked: () -> Unit = {}, - onChange: () -> Unit = {}, ) { val isLoading by remember(state.loginFlow) { derivedStateOf { @@ -158,5 +158,9 @@ fun ConfirmAccountProviderViewDarkPreview(@PreviewParameter(ConfirmAccountProvid 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/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index cc4d326d64..f5b178ce27 100644 --- 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 @@ -75,10 +75,10 @@ import io.element.android.libraries.testtags.testTag @Composable fun SearchAccountProviderView( state: SearchAccountProviderState, + onBackPressed: () -> Unit, + onLearnMoreClicked: () -> Unit, + onDone: () -> Unit, modifier: Modifier = Modifier, - onBackPressed: () -> Unit = {}, - onLearnMoreClicked: () -> Unit = {}, - onDone: () -> Unit = {}, ) { val eventSink = state.eventSink val scrollState = rememberScrollState() @@ -215,5 +215,8 @@ fun SearchAccountProviderViewDarkPreview(@PreviewParameter(SearchAccountProvider private fun ContentToPreview(state: SearchAccountProviderState) { SearchAccountProviderView( state = state, + onBackPressed = {}, + onLearnMoreClicked = {}, + onDone = {}, ) } From 0dd4109d21b5042c2062cdcd06fe4b23d6ed1725 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 17:45:51 +0200 Subject: [PATCH 039/130] Use function ref. --- .../ConfirmAccountProviderView.kt | 4 +--- .../features/onboarding/impl/OnBoardingView.kt | 12 +++--------- 2 files changed, 4 insertions(+), 12 deletions(-) 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 index 91b79bca77..9305bccd12 100644 --- 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 @@ -98,9 +98,7 @@ fun ConfirmAccountProviderView( .testTag(TestTags.loginContinue) ) TextButton( - onClick = { - onChange() - }, + onClick = onChange, enabled = true, modifier = Modifier .fillMaxWidth() 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() From 8b177645a90a17be33894edbecab0bd2b5946f5e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 17:51:42 +0200 Subject: [PATCH 040/130] Rename `InlineErrorMessage` to simple `Error` to not consider how the error will be used (actually used in a dialog now) --- .../features/login/impl/changeserver/ChangeServerView.kt | 2 +- .../android/features/login/impl/error/ChangeServerError.kt | 4 ++-- .../confirmaccountprovider/ConfirmAccountProviderView.kt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) 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 136a4a7d5b..d2e1a03cf5 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 @@ -39,7 +39,7 @@ fun ChangeServerView( when (state.changeServerAction) { is Async.Failure -> { when (val error = state.changeServerAction.error) { - is ChangeServerError.InlineErrorMessage -> { + is ChangeServerError.Error -> { ErrorDialog( modifier = modifier, content = error.message(), diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt index 9976089f52..444ea3d3f2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/error/ChangeServerError.kt @@ -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/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index 9305bccd12..c9053d67c9 100644 --- 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 @@ -112,7 +112,7 @@ fun ConfirmAccountProviderView( when (state.loginFlow) { is Async.Failure -> { when (val error = state.loginFlow.error) { - is ChangeServerError.InlineErrorMessage -> { + is ChangeServerError.Error -> { ErrorDialog( content = error.message(), onDismiss = { From b98010d74de0470dc08e5ceb9fc13b858b07f220 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 17:55:09 +0200 Subject: [PATCH 041/130] Merge the 2 map. --- .../ConfirmAccountProviderPresenter.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 index 446d6611d0..2626e56365 100644 --- 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 @@ -85,12 +85,11 @@ class ConfirmAccountProviderPresenter @AssistedInject constructor( suspend { val domain = tryOrNull { URL(homeserverUrl) }?.host ?: homeserverUrl authenticationService.setHomeserver(domain).map { - authenticationService.getHomeserverDetails().value!! - }.map { - if (it.supportsOidcLogin) { + val matrixHomeServerDetails = authenticationService.getHomeserverDetails().value!! + if (matrixHomeServerDetails.supportsOidcLogin) { // Retrieve the details right now LoginFlow.OidcFlow(authenticationService.getOidcUrl().getOrThrow()) - } else if (it.supportsPasswordLogin) { + } else if (matrixHomeServerDetails.supportsPasswordLogin) { LoginFlow.PasswordLogin } else { throw IllegalStateException("Unsupported login flow") From 1e3727bb3983546286adf06f8579d1289a1e22b7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 17:58:36 +0200 Subject: [PATCH 042/130] Use LaunchedEffect to avoid multiple calls. --- .../features/login/impl/changeserver/ChangeServerView.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 d2e1a03cf5..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 @@ -17,6 +17,7 @@ package io.element.android.features.login.impl.changeserver import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -61,7 +62,9 @@ fun ChangeServerView( } } is Async.Loading -> ProgressDialog() - is Async.Success -> onDone() + is Async.Success -> LaunchedEffect(state.changeServerAction) { + onDone() + } Async.Uninitialized -> Unit } } From 0c199a012b5526c47f348750226301b7e09bc984 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:05:08 +0200 Subject: [PATCH 043/130] Move the resolver to the parent package. --- .../{changeserver => }/resolver/HomeserverData.kt | 2 +- .../resolver/HomeserverResolver.kt | 4 ++-- .../resolver/network/DefaultWellknownRequest.kt | 2 +- .../{changeserver => }/resolver/network/WellKnown.kt | 2 +- .../resolver/network/WellKnownBaseConfig.kt | 2 +- .../resolver/network/WellKnownSlidingSyncConfig.kt | 2 +- .../resolver/network/WellknownAPI.kt | 2 +- .../resolver/network/WellknownRequest.kt | 2 +- .../SearchAccountProviderPresenter.kt | 4 ++-- .../SearchAccountProviderState.kt | 2 +- .../SearchAccountProviderStateProvider.kt | 2 +- .../SearchAccountProviderView.kt | 2 +- .../resolver/network/FakeWellknownRequest.kt | 2 +- .../SearchAccountProviderPresenterTest.kt | 12 ++++++------ 14 files changed, 21 insertions(+), 21 deletions(-) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/HomeserverData.kt (93%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/HomeserverResolver.kt (96%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/network/DefaultWellknownRequest.kt (94%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/network/WellKnown.kt (95%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/network/WellKnownBaseConfig.kt (92%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/network/WellKnownSlidingSyncConfig.kt (91%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/network/WellknownAPI.kt (90%) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/network/WellknownRequest.kt (91%) rename features/login/impl/src/test/kotlin/io/element/android/features/login/impl/{changeserver => }/resolver/network/FakeWellknownRequest.kt (92%) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverData.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt similarity index 93% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverData.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt index 04bcd0099d..c1f0158605 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverData.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverData.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver +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 diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverResolver.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt similarity index 96% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverResolver.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt index 9409f285e5..8a1a72670c 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/HomeserverResolver.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/HomeserverResolver.kt @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver +package io.element.android.features.login.impl.resolver -import io.element.android.features.login.impl.changeserver.resolver.network.WellknownRequest +import io.element.android.features.login.impl.resolver.network.WellknownRequest import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/DefaultWellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt similarity index 94% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/DefaultWellknownRequest.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt index 95960971c3..7de6d26f10 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/DefaultWellknownRequest.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/DefaultWellknownRequest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver.network +package io.element.android.features.login.impl.resolver.network import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnown.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt similarity index 95% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnown.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt index af5a4f17f3..63b6e7d189 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnown.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnown.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver.network +package io.element.android.features.login.impl.resolver.network import io.element.android.libraries.core.bool.orFalse import kotlinx.serialization.SerialName diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownBaseConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt similarity index 92% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownBaseConfig.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt index 3bac743d64..87b86736fa 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownBaseConfig.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownBaseConfig.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver.network +package io.element.android.features.login.impl.resolver.network import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownSlidingSyncConfig.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt similarity index 91% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownSlidingSyncConfig.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt index 863b524f84..98c712d9ac 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellKnownSlidingSyncConfig.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellKnownSlidingSyncConfig.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver.network +package io.element.android.features.login.impl.resolver.network import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownAPI.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt similarity index 90% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownAPI.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt index f4d4ba7c67..04a0dfb803 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownAPI.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownAPI.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver.network +package io.element.android.features.login.impl.resolver.network import retrofit2.http.GET diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownRequest.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt similarity index 91% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownRequest.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt index 54c393e522..570b621b83 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/WellknownRequest.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/resolver/network/WellknownRequest.kt @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver.network +package io.element.android.features.login.impl.resolver.network interface WellknownRequest { /** 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 index 1ff510ea94..f95c027620 100644 --- 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 @@ -24,8 +24,8 @@ 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.changeserver.resolver.HomeserverData -import io.element.android.features.login.impl.changeserver.resolver.HomeserverResolver +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 javax.inject.Inject 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 index c89dc53aff..15859afde1 100644 --- 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 @@ -17,7 +17,7 @@ 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.changeserver.resolver.HomeserverData +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. 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 index 9c15a63802..b6ffac8bd1 100644 --- 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 @@ -18,7 +18,7 @@ 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.changeserver.resolver.HomeserverData +import io.element.android.features.login.impl.resolver.HomeserverData import io.element.android.libraries.architecture.Async open class SearchAccountProviderStateProvider : PreviewParameterProvider { 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 index f5b178ce27..605992d930 100644 --- 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 @@ -54,7 +54,7 @@ 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.changeserver.resolver.HomeserverData +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 diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/FakeWellknownRequest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt similarity index 92% rename from features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/FakeWellknownRequest.kt rename to features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt index 4dea24b1d7..58c2bf82a3 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/resolver/network/FakeWellknownRequest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/resolver/network/FakeWellknownRequest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.changeserver.resolver.network +package io.element.android.features.login.impl.resolver.network class FakeWellknownRequest : WellknownRequest { private var resultMap: Map = emptyMap() 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 index 1337e0f228..9163f247f5 100644 --- 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 @@ -20,13 +20,13 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.login.impl.changeserver.ChangeServerPresenter -import io.element.android.features.login.impl.changeserver.resolver.network.WellKnown -import io.element.android.features.login.impl.changeserver.resolver.network.WellKnownBaseConfig -import io.element.android.features.login.impl.changeserver.resolver.network.WellKnownSlidingSyncConfig -import io.element.android.features.login.impl.changeserver.resolver.HomeserverResolver import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.changeserver.resolver.network.FakeWellknownRequest +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 From 4fa30f384d7b045a0edd29ec3a1485ca2eaf612a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:06:29 +0200 Subject: [PATCH 044/130] Better comment --- .../searchaccountprovider/SearchAccountProviderEvents.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 9a6a175cab..53ee45f644 100644 --- 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 @@ -18,7 +18,8 @@ package io.element.android.features.login.impl.screens.searchaccountprovider sealed interface SearchAccountProviderEvents { /** - * The user has typed something, expect to get a list of result in the state. + * 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 } From f8a16f2ea3f7a2cf791ec52cb9964e906f99c85a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:09:09 +0200 Subject: [PATCH 045/130] Inline the scroll state. --- .../changeaccountprovider/ChangeAccountProviderView.kt | 6 +----- .../searchaccountprovider/SearchAccountProviderView.kt | 6 +----- .../features/rageshake/impl/bugreport/BugReportView.kt | 5 +---- .../designsystem/components/preferences/PreferenceScreen.kt | 5 +---- 4 files changed, 4 insertions(+), 18 deletions(-) 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 index eb8ee11a1c..0f444350c9 100644 --- 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 @@ -63,8 +63,6 @@ fun ChangeAccountProviderView( onOtherProviderClicked: () -> Unit, modifier: Modifier = Modifier, ) { - val scrollState = rememberScrollState() - Scaffold( modifier = modifier, topBar = { @@ -83,9 +81,7 @@ fun ChangeAccountProviderView( ) { Column( modifier = Modifier - .verticalScroll( - state = scrollState, - ) + .verticalScroll(state = rememberScrollState()) ) { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 16.dp, bottom = 32.dp, start = 16.dp, end = 16.dp), 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 index 605992d930..3ee9f115d7 100644 --- 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 @@ -81,8 +81,6 @@ fun SearchAccountProviderView( modifier: Modifier = Modifier, ) { val eventSink = state.eventSink - val scrollState = rememberScrollState() - Scaffold( modifier = modifier, topBar = { @@ -101,9 +99,7 @@ fun SearchAccountProviderView( ) { Column( modifier = Modifier - .verticalScroll( - state = scrollState, - ) + .verticalScroll(state = rememberScrollState()) ) { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp), 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/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 eacdf82fb7..65cd428729 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 @@ -68,14 +68,11 @@ fun PreferenceView( ) }, content = { - val scrollState = rememberScrollState() Column( modifier = Modifier .padding(it) .consumeWindowInsets(it) - .verticalScroll( - state = scrollState, - ) + .verticalScroll(state = rememberScrollState()) ) { content() } From 03273985b2984f42d9e94de2ed96a5ef3e67e08e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:10:59 +0200 Subject: [PATCH 046/130] Use the search icon on this screen. --- .../searchaccountprovider/SearchAccountProviderView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index 3ee9f115d7..c9d769c1e5 100644 --- 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 @@ -33,7 +33,7 @@ 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.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -103,7 +103,7 @@ fun SearchAccountProviderView( ) { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 16.dp, bottom = 40.dp, start = 16.dp, end = 16.dp), - iconImageVector = Icons.Filled.Home, + iconImageVector = Icons.Filled.Search, title = stringResource(id = R.string.screen_account_provider_form_title), subTitle = stringResource(id = R.string.screen_account_provider_form_subtitle), ) From 8d50acaff49460f3448a8c4fd6b0c8fd0fa1e3f6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:16:23 +0200 Subject: [PATCH 047/130] No need to pass `maxLines = 1` if `singleline = true` anymore. --- .../login/impl/screens/loginpassword/LoginPasswordView.kt | 2 -- .../screens/searchaccountprovider/SearchAccountProviderView.kt | 1 - .../libraries/designsystem/components/LabelledTextField.kt | 2 +- .../designsystem/theme/components/OutlinedTextField.kt | 2 +- .../libraries/designsystem/theme/components/TextField.kt | 2 +- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt index 5016a61054..9154a0792f 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/loginpassword/LoginPasswordView.kt @@ -208,7 +208,6 @@ internal fun LoginForm( focusManager.moveFocus(FocusDirection.Down) }), singleLine = true, - maxLines = 1, trailingIcon = if (loginFieldState.isNotEmpty()) { { IconButton(onClick = { @@ -263,7 +262,6 @@ internal fun LoginForm( onDone = { onSubmit() } ), singleLine = true, - maxLines = 1, ) } } 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 index c9d769c1e5..f61bc1caf8 100644 --- 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 @@ -127,7 +127,6 @@ fun SearchAccountProviderView( imeAction = ImeAction.Done, ), singleLine = true, - maxLines = 1, trailingIcon = if (userInputState.isNotEmpty()) { { IconButton(onClick = { 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 0f0ada5497..f7b4d9ab5c 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 @@ -36,8 +36,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/theme/components/OutlinedTextField.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/OutlinedTextField.kt index e0edeed050..8b8aec7030 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 @@ -66,7 +66,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 54fe50b8bf..e244639e7a 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 @@ -67,7 +67,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() From 19e090f6dc04c3bce95a559e18403ad2085b128a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:19:00 +0200 Subject: [PATCH 048/130] Always use `import io.element.android.libraries.ui.strings.R as StringR` --- .../android/features/login/impl/error/ErrorFormatter.kt | 6 +++--- .../searchaccountprovider/SearchAccountProviderView.kt | 3 ++- .../roomdetails/impl/invite/RoomInviteMembersView.kt | 2 +- .../libraries/eventformatter/impl/StateContentFormatter.kt | 3 ++- 4 files changed, 8 insertions(+), 6 deletions(-) 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 86820645ae..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 @@ -21,16 +21,16 @@ 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/screens/searchaccountprovider/SearchAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/searchaccountprovider/SearchAccountProviderView.kt index f61bc1caf8..26928446d3 100644 --- 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 @@ -68,6 +68,7 @@ import io.element.android.libraries.designsystem.theme.components.OutlinedTextFi import io.element.android.libraries.designsystem.theme.components.Text 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 @@ -135,7 +136,7 @@ fun SearchAccountProviderView( }) { Icon( imageVector = Icons.Filled.Close, - contentDescription = stringResource(io.element.android.libraries.ui.strings.R.string.action_clear) + contentDescription = stringResource(StringR.string.action_clear) ) } } 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/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 { From 18f32e51b463f050216f9a4d5a0a7e2ea27f4127 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:24:51 +0200 Subject: [PATCH 049/130] Prevent direct usage of `io.element.android.libraries.ui.strings.R` package. --- tools/check/forbidden_strings_in_code.txt | 3 +++ 1 file changed, 3 insertions(+) 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\. From 6b1809ed3b16035c4421970346eb6b311af1269a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:30:44 +0200 Subject: [PATCH 050/130] Use a LazyColumn, in case we get more results in the future. --- .../SearchAccountProviderView.kt | 116 +++++++++--------- 1 file changed, 60 insertions(+), 56 deletions(-) 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 index 26928446d3..53061c4b92 100644 --- 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 @@ -19,7 +19,6 @@ package io.element.android.features.login.impl.screens.searchaccountprovider 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 @@ -28,9 +27,10 @@ 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.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState 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.Search @@ -98,70 +98,72 @@ fun SearchAccountProviderView( .padding(padding) .consumeWindowInsets(padding) ) { - Column( - modifier = Modifier - .verticalScroll(state = rememberScrollState()) - ) { - 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), - ) + 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) - // 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) - .testTag(TestTags.changeServerServer), - onValueChange = { - userInputState = it - eventSink(SearchAccountProviderEvents.UserInput(it)) - }, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Done, - ), - singleLine = true, - trailingIcon = if (userInputState.isNotEmpty()) { - { - IconButton(onClick = { - userInputState = "" - eventSink(SearchAccountProviderEvents.UserInput("")) - }) { - Icon( - imageVector = Icons.Filled.Close, - contentDescription = stringResource(StringR.string.action_clear) - ) + OutlinedTextField( + value = userInputState, + // readOnly = isLoading, + modifier = Modifier + .fillMaxWidth() + .padding(start = 16.dp, end = 16.dp, bottom = 30.dp) + .testTag(TestTags.changeServerServer), + onValueChange = { + userInputState = it + eventSink(SearchAccountProviderEvents.UserInput(it)) + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Uri, + imeAction = ImeAction.Done, + ), + 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) } - } 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) - ) + item { + Box( + modifier = Modifier + .fillMaxSize() + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } } } is Async.Success -> { - state.userInputResult.state.forEach { homeserverData -> + items(state.userInputResult.state) { homeserverData -> val item = homeserverData.toAccountProvider() AccountProviderView( item = item, @@ -173,7 +175,9 @@ fun SearchAccountProviderView( } Async.Uninitialized -> Unit } - Spacer(Modifier.height(32.dp)) + item { + Spacer(Modifier.height(32.dp)) + } } ChangeServerView( state = state.changeServerState, From cdf89e86d695a55c85d8ff5a96fb5b51f1dc03c6 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:34:00 +0200 Subject: [PATCH 051/130] Add a 5s timeout to avoid infinite loading (actually waiting for network timeout which can be long and is harder to configure). --- .../features/login/impl/resolver/HomeserverResolver.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 index 8a1a72670c..a11b680f63 100644 --- 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 @@ -30,6 +30,7 @@ import kotlinx.coroutines.delay 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 @@ -56,7 +57,11 @@ class HomeserverResolver @Inject constructor( withContext(dispatchers.io) { list.map { async { - val wellKnown = tryOrNull { wellknownRequest.execute(it) } + val wellKnown = tryOrNull { + withTimeout(5000) { + wellknownRequest.execute(it) + } + } val isValid = wellKnown?.isValid().orFalse() if (isValid) { val supportSlidingSync = wellKnown?.supportSlidingSync().orFalse() From ddafe50eb51f3be93122f1f76ee69b995850d830 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:36:10 +0200 Subject: [PATCH 052/130] Reduce space above the result to give more visibility especially when the keyboard is opened. --- .../screens/searchaccountprovider/SearchAccountProviderView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 53061c4b92..9c6d91dfaa 100644 --- 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 @@ -116,7 +116,7 @@ fun SearchAccountProviderView( // readOnly = isLoading, modifier = Modifier .fillMaxWidth() - .padding(start = 16.dp, end = 16.dp, bottom = 30.dp) + .padding(start = 16.dp, end = 16.dp, bottom = 16.dp) .testTag(TestTags.changeServerServer), onValueChange = { userInputState = it From 937f4b81eeb0ddb2cf11c0ac897b55e28582fcff Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:40:31 +0200 Subject: [PATCH 053/130] Add some generated KDoc. --- .../designsystem/atomic/atoms/RoundedIconAtom.kt | 9 +++++++++ .../atomic/molecules/IconTitleSubtitleMolecule.kt | 9 ++++++++- .../libraries/designsystem/theme/components/Icon.kt | 11 +++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) 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 index b5c98cf483..752c80175d 100644 --- 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 @@ -38,6 +38,15 @@ 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, 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 9d348b706f..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 @@ -38,7 +38,14 @@ import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Text /** - * Provide either an `iconResourceId` or an `iconImageVector`. + * 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( 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 1a9bfe3a0a..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,17 @@ 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?, From de3eb23a24903832cde2c47bbcf02245325c3b9a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 9 Jun 2023 18:45:32 +0200 Subject: [PATCH 054/130] Improve accessibility with keyboard. --- .../searchaccountprovider/SearchAccountProviderView.kt | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) 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 index 9c6d91dfaa..5c280cba0e 100644 --- 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 @@ -30,6 +30,7 @@ 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 @@ -43,6 +44,8 @@ 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 @@ -66,6 +69,7 @@ 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 @@ -110,13 +114,14 @@ fun SearchAccountProviderView( 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 @@ -126,6 +131,9 @@ fun SearchAccountProviderView( keyboardType = KeyboardType.Uri, imeAction = ImeAction.Done, ), + keyboardActions = KeyboardActions(onDone = { + focusManager.moveFocus(FocusDirection.Down) + }), singleLine = true, trailingIcon = if (userInputState.isNotEmpty()) { { From a75d6c9231f9fef7308d045998af3d33b5e3620a Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 9 Jun 2023 16:55:57 +0000 Subject: [PATCH 055/130] Update screenshots --- ...ccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...ccountProviderViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...ccountProviderViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 +++ ...ccountProviderViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 +++ ...ntProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...ntProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ...tProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 --- ...tProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 --- ..._ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...countProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...ountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...ccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png} | 0 ...LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png} | 0 ...oginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png} | 0 ...oginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png} | 0 ...oginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png} | 0 ...ccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...ccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...countProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ 30 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png delete mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png => 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} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png => 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} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png => 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} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png => 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} (100%) create mode 100644 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 create mode 100644 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 rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png => 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} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png => 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} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png => 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} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png => 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} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png => 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} (100%) rename tests/uitests/src/test/snapshots/images/{io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png => 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} (100%) create mode 100644 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 create mode 100644 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 create mode 100644 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 create mode 100644 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 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 index 406ee49b58..a0ea87cbc0 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51897c460fad147f358268ad6b15d6865b6378edd8533d744fe108bc96f5d718 -size 40107 +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 index 3346d30e90..d44b573a9a 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:57ace1181dd3d95bdbc0ec11270951b6b2fce6301a68af88c6cddc428acba022 -size 39533 +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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index f90601bd76..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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:abc7f0b614bb7532341dd016fab0f5ba72a08f9718bb496b420c94edfcedad2f -size 25927 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index ac844925a8..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewDarkPreview_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:33dfbd2203f5d78760730e65c689182b54169988f10906ba987af9fe43d0d03d -size 47910 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_0,NEXUS_5,1.0,en].png deleted file mode 100644 index fe94473bb3..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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:9d11e04a2440ecd57a4178a0e2c1aa2755c62d147e28c065c7b10a957fef35da -size 25669 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_0_null_1,NEXUS_5,1.0,en].png deleted file mode 100644 index a11e2c6787..0000000000 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.form_null_DefaultGroup_ChangeAccountProviderFormViewLightPreview_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:3ebf802513c7372eaba77b4ebaabbc8e313c1b8f8b00ef8e496194586c9431dc -size 47340 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider.common_null_DefaultGroup_ChangeServerViewLightPreview_0_null_0,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.changeaccountprovider_null_DefaultGroup_ChangeAccountProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png rename to 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 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.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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_0,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_1,NEXUS_5,1.0,en].png rename to 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 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.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 similarity index 100% rename from tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.login.impl.loginpassword_null_DefaultGroup_LoginPasswordViewLightPreview_0_null_2,NEXUS_5,1.0,en].png rename to 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 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 From e3d939726c52d871fe1e30b60d7aa9832f9dd9e2 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 9 Jun 2023 22:43:59 +0200 Subject: [PATCH 056/130] Fix crash on MainActivity when restored --- .../io/element/android/x/MainActivity.kt | 57 ++++++++++++------- 1 file changed, 35 insertions(+), 22 deletions(-) diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index a969c8e0ba..e8a51cca22 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat @@ -39,7 +40,7 @@ private val loggerTag = LoggerTag("MainActivity") class MainActivity : NodeComponentActivity() { - lateinit var mainNode: MainNode + private lateinit var mainNode: MainNode override fun onCreate(savedInstanceState: Bundle?) { Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}") @@ -49,26 +50,32 @@ class MainActivity : NodeComponentActivity() { appBindings.matrixClientsHolder().restore(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { - ElementTheme { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), - ) { - NodeHost(integrationPoint = appyxIntegrationPoint) { - MainNode( - it, - appBindings.mainDaggerComponentOwner(), - plugins = listOf( - object : NodeReadyObserver { - override fun init(node: MainNode) { - mainNode = node - mainNode.handleIntent(intent) - } + MainContent(appBindings) + } + } + + @Composable + private fun MainContent(appBindings: AppBindings) { + ElementTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { + NodeHost(integrationPoint = appyxIntegrationPoint) { + MainNode( + it, + appBindings.mainDaggerComponentOwner(), + plugins = listOf( + object : NodeReadyObserver { + override fun init(node: MainNode) { + Timber.tag(loggerTag.value).w("onMainNodeInit") + mainNode = node + mainNode.handleIntent(intent) } - ) + } ) - } + ) } } } @@ -80,11 +87,17 @@ class MainActivity : NodeComponentActivity() { * - a notification is clicked. * - the app is going to background (<- this is strange) */ - override fun onNewIntent(intent: Intent?) { + override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) Timber.tag(loggerTag.value).w("onNewIntent") - intent ?: return - mainNode.handleIntent(intent) + // If the mainNode is not init yet, keep the intent for later. + // It can happen when the activity is killed by the system. The methods are called in this order : + // onCreate(savedInstanceState=true) -> onNewIntent -> onResume -> onMainNodeInit + if (::mainNode.isInitialized) { + mainNode.handleIntent(intent) + } else { + setIntent(intent) + } } override fun onPause() { From f57a2a694d60943cb5f2e2fec6063b101021f79e Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 9 Jun 2023 23:04:33 +0200 Subject: [PATCH 057/130] Dagger: makes sure to not remove an active component when navigating --- app/src/main/kotlin/io/element/android/x/MainNode.kt | 8 ++++---- .../io/element/android/x/di/MainDaggerComponentsOwner.kt | 4 ++++ .../kotlin/io/element/android/appnav/LoggedInFlowNode.kt | 9 ++++----- .../kotlin/io/element/android/appnav/RoomFlowNode.kt | 8 ++++---- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt index fb551f326d..d0b6baec04 100644 --- a/app/src/main/kotlin/io/element/android/x/MainNode.kt +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -68,13 +68,13 @@ class MainNode( } private val roomFlowNodeCallback = object : RoomFlowNode.LifecycleCallback { - override fun onFlowCreated(room: MatrixRoom) { + override fun onFlowCreated(owner: String, room: MatrixRoom) { val component = bindings().roomComponentBuilder().room(room).build() - mainDaggerComponentOwner.addComponent(room.roomId.value, component) + mainDaggerComponentOwner.addComponent(owner, component) } - override fun onFlowReleased(room: MatrixRoom) { - mainDaggerComponentOwner.removeComponent(room.roomId.value) + override fun onFlowReleased(owner: String, room: MatrixRoom) { + mainDaggerComponentOwner.removeComponent(owner) } } diff --git a/app/src/main/kotlin/io/element/android/x/di/MainDaggerComponentsOwner.kt b/app/src/main/kotlin/io/element/android/x/di/MainDaggerComponentsOwner.kt index 5489b07830..de800bb587 100644 --- a/app/src/main/kotlin/io/element/android/x/di/MainDaggerComponentsOwner.kt +++ b/app/src/main/kotlin/io/element/android/x/di/MainDaggerComponentsOwner.kt @@ -38,6 +38,10 @@ class MainDaggerComponentsOwner @Inject constructor(@ApplicationContext context: daggerComponents.remove(identifier) } + /** + * We expose the dagger components in the opposite order they arrived. + * So we pick the most recent component when searching with the [io.element.android.libraries.architecture.bindings] methods. + */ override val daggerComponent: Any get() = daggerComponents.values.reversed() } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index d23cbaabce..ba002b78ac 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -64,7 +64,6 @@ import io.element.android.services.analytics.api.AnalyticsService import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.parcelize.Parcelize @@ -118,9 +117,9 @@ class LoggedInFlowNode @AssistedInject constructor( } interface LifecycleCallback : NodeLifecycleCallback { - fun onFlowCreated(client: MatrixClient) = Unit + fun onFlowCreated(identifier: String, client: MatrixClient) = Unit - fun onFlowReleased(client: MatrixClient) = Unit + fun onFlowReleased(identifier: String, client: MatrixClient) = Unit } data class Inputs( @@ -139,7 +138,7 @@ class LoggedInFlowNode @AssistedInject constructor( observeAnalyticsState() lifecycle.subscribe( onCreate = { - plugins().forEach { it.onFlowCreated(inputs.matrixClient) } + plugins().forEach { it.onFlowCreated(id, inputs.matrixClient) } val imageLoaderFactory = bindings().loggedInImageLoaderFactory() Coil.setImageLoader(imageLoaderFactory) inputs.matrixClient.startSync() @@ -151,7 +150,7 @@ class LoggedInFlowNode @AssistedInject constructor( onDestroy = { val imageLoaderFactory = bindings().notLoggedInImageLoaderFactory() Coil.setImageLoader(imageLoaderFactory) - plugins().forEach { it.onFlowReleased(inputs.matrixClient) } + plugins().forEach { it.onFlowReleased(id, inputs.matrixClient) } appNavigationStateService.onLeavingSpace(id) appNavigationStateService.onLeavingSession(id) loggedInFlowProcessor.stopObserving() diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index fb3a8d566e..68f25d9b54 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -68,8 +68,8 @@ class RoomFlowNode @AssistedInject constructor( ) { interface LifecycleCallback : NodeLifecycleCallback { - fun onFlowCreated(room: MatrixRoom) = Unit - fun onFlowReleased(room: MatrixRoom) = Unit + fun onFlowCreated(owner: String, room: MatrixRoom) = Unit + fun onFlowReleased(owner: String, room: MatrixRoom) = Unit } data class Inputs( @@ -83,14 +83,14 @@ class RoomFlowNode @AssistedInject constructor( lifecycle.subscribe( onCreate = { Timber.v("OnCreate") - plugins().forEach { it.onFlowCreated(inputs.room) } + plugins().forEach { it.onFlowCreated(id, inputs.room) } appNavigationStateService.onNavigateToRoom(id, inputs.room.roomId) fetchRoomMembers() }, onDestroy = { Timber.v("OnDestroy") inputs.room.close() - plugins().forEach { it.onFlowReleased(inputs.room) } + plugins().forEach { it.onFlowReleased(id, inputs.room) } appNavigationStateService.onLeavingRoom(id) } ) From 78461639c0f49c1d3e61e1b19f44468dcfca3573 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Jun 2023 10:40:34 +0200 Subject: [PATCH 058/130] trigger CI From 01f76816db21a570a96f21cafcbba93810d52d83 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Jun 2023 11:04:53 +0200 Subject: [PATCH 059/130] Fix lint errors manually (a string sync will be needed on develop). --- .../ui-strings/src/main/res/values-cs/translations.xml | 6 ------ .../ui-strings/src/main/res/values-de/translations.xml | 5 +---- 2 files changed, 1 insertion(+), 10 deletions(-) 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 + From 476ddd6d8eb812cc1931952d242fd0ef89b63c50 Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 12 Jun 2023 11:39:28 +0200 Subject: [PATCH 060/130] Node: fix compilation after changes --- .../main/kotlin/io/element/android/x/MainNode.kt | 16 ++++++++-------- .../io/element/android/appnav/RoomFlowNode.kt | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt index d0b6baec04..7f56ef4d63 100644 --- a/app/src/main/kotlin/io/element/android/x/MainNode.kt +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -57,24 +57,24 @@ class MainNode( DaggerComponentOwner by mainDaggerComponentOwner { private val loggedInFlowNodeCallback = object : LoggedInFlowNode.LifecycleCallback { - override fun onFlowCreated(client: MatrixClient) { + override fun onFlowCreated(identifier: String, client: MatrixClient) { val component = bindings().sessionComponentBuilder().client(client).build() - mainDaggerComponentOwner.addComponent(client.sessionId.value, component) + mainDaggerComponentOwner.addComponent(identifier, component) } - override fun onFlowReleased(client: MatrixClient) { - mainDaggerComponentOwner.removeComponent(client.sessionId.value) + override fun onFlowReleased(identifier: String, client: MatrixClient) { + mainDaggerComponentOwner.removeComponent(identifier) } } private val roomFlowNodeCallback = object : RoomFlowNode.LifecycleCallback { - override fun onFlowCreated(owner: String, room: MatrixRoom) { + override fun onFlowCreated(identifier: String, room: MatrixRoom) { val component = bindings().roomComponentBuilder().room(room).build() - mainDaggerComponentOwner.addComponent(owner, component) + mainDaggerComponentOwner.addComponent(identifier, component) } - override fun onFlowReleased(owner: String, room: MatrixRoom) { - mainDaggerComponentOwner.removeComponent(owner) + override fun onFlowReleased(identifier: String, room: MatrixRoom) { + mainDaggerComponentOwner.removeComponent(identifier) } } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 68f25d9b54..0bcf9000e7 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -68,8 +68,8 @@ class RoomFlowNode @AssistedInject constructor( ) { interface LifecycleCallback : NodeLifecycleCallback { - fun onFlowCreated(owner: String, room: MatrixRoom) = Unit - fun onFlowReleased(owner: String, room: MatrixRoom) = Unit + fun onFlowCreated(identifier: String, room: MatrixRoom) = Unit + fun onFlowReleased(identifier: String, room: MatrixRoom) = Unit } data class Inputs( From b51591c794a0ed5443a189f47ed57da3f0579307 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 11:20:37 +0000 Subject: [PATCH 061/130] Update plugin sonarqube to v4.2.1.3168 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 11d6e102ff..a27edecca7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -187,6 +187,6 @@ dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version. dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" } paparazzi = "app.cash.paparazzi:1.2.0" -sonarqube = "org.sonarqube:4.2.0.3129" +sonarqube = "org.sonarqube:4.2.1.3168" kover = "org.jetbrains.kotlinx.kover:0.6.1" sqldelight = { id = "com.squareup.sqldelight", version.ref = "sqldelight" } From 9a54368d9cd988fe86adff9d093d09ee88da9e7b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 13:57:55 +0200 Subject: [PATCH 062/130] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.17 (#576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency org.matrix.rustcomponents:sdk-android to v0.1.17 * Fix breaking changes. --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín --- gradle/libs.versions.toml | 2 +- .../libraries/matrix/impl/timeline/RustMatrixTimeline.kt | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 11d6e102ff..e9fdd9d872 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -139,7 +139,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.16" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.17" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index af27aedc13..79b92e3c29 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -138,7 +138,8 @@ class RustMatrixTimeline( } val paginationOptions = PaginationOptions.UntilNumItems( eventLimit = requestSize.toUShort(), - items = untilNumberOfItems.toUShort() + items = untilNumberOfItems.toUShort(), + waitForToken = true, ) innerRoom.paginateBackwards(paginationOptions) }.onFailure { From 7cc9b6312491c9cae8ce50b557e1865e5913fe4a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Jun 2023 17:42:01 +0200 Subject: [PATCH 063/130] Fix warning --- .../android/features/roomdetails/RoomDetailsPresenterTests.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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, From a0d0d14973c1489c0a9ff177071ef95663c7dec7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 16:46:53 +0000 Subject: [PATCH 064/130] Update dependency com.google.auto.service:auto-service to v1.1.1 --- anvilcodegen/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts index e592d98b2e..5dd6ae1844 100644 --- a/anvilcodegen/build.gradle.kts +++ b/anvilcodegen/build.gradle.kts @@ -26,5 +26,5 @@ dependencies { implementation("com.squareup:kotlinpoet:1.14.2") implementation(libs.dagger) compileOnly("com.google.auto.service:auto-service-annotations:1.1.0") - kapt("com.google.auto.service:auto-service:1.1.0") + kapt("com.google.auto.service:auto-service:1.1.1") } From 8d7219bf3855f65e50854286c425bc261b81fc0c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jun 2023 16:46:59 +0000 Subject: [PATCH 065/130] Update dependency com.google.auto.service:auto-service-annotations to v1.1.1 --- anvilcodegen/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts index e592d98b2e..404297b20d 100644 --- a/anvilcodegen/build.gradle.kts +++ b/anvilcodegen/build.gradle.kts @@ -25,6 +25,6 @@ dependencies { implementation(libs.anvil.compiler.utils) implementation("com.squareup:kotlinpoet:1.14.2") implementation(libs.dagger) - compileOnly("com.google.auto.service:auto-service-annotations:1.1.0") + compileOnly("com.google.auto.service:auto-service-annotations:1.1.1") kapt("com.google.auto.service:auto-service:1.1.0") } From 7705c93eade1f23e8831273f4c6e235693a1c1fc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Jun 2023 18:28:57 +0200 Subject: [PATCH 066/130] Let HomeserverResolver emit the List and not the Async. --- .../login/impl/resolver/HomeserverResolver.kt | 38 +++++++------------ .../SearchAccountProviderPresenter.kt | 25 +++++++++--- 2 files changed, 33 insertions(+), 30 deletions(-) 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 index a11b680f63..7c692f5208 100644 --- 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 @@ -17,7 +17,6 @@ package io.element.android.features.login.impl.resolver import io.element.android.features.login.impl.resolver.network.WellknownRequest -import io.element.android.libraries.architecture.Async import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull @@ -26,7 +25,6 @@ import io.element.android.libraries.core.uri.isValidUrl import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.currentCoroutineContext -import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.withContext @@ -42,24 +40,20 @@ class HomeserverResolver @Inject constructor( private val wellknownRequest: WellknownRequest, ) { - suspend fun resolve(userInput: String): Flow>> = flow { + suspend fun resolve(userInput: String): Flow> = flow { val flowContext = currentCoroutineContext() - emit(Async.Uninitialized) - // Debounce - delay(300) val trimmedUserInput = userInput.trim() if (trimmedUserInput.length < 4) return@flow - emit(Async.Loading()) 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 { + list.map { url -> async { val wellKnown = tryOrNull { withTimeout(5000) { - wellknownRequest.execute(it) + wellknownRequest.execute(url) } } val isValid = wellKnown?.isValid().orFalse() @@ -68,35 +62,29 @@ class HomeserverResolver @Inject constructor( // Emit the list as soon as possible currentList.add( HomeserverData( - homeserverUrl = it, + homeserverUrl = url, isWellknownValid = true, supportSlidingSync = supportSlidingSync ) ) withContext(flowContext) { - emit(Async.Success(currentList)) + emit(currentList.toList()) } } } }.awaitAll() } // If list is empty, and the user has entered an URL, do not block the user. - if (currentList.isEmpty()) { - if (trimmedUserInput.isValidUrl()) { - emit( - Async.Success( - listOf( - HomeserverData( - homeserverUrl = trimmedUserInput, - isWellknownValid = false, - supportSlidingSync = false, - ) - ) + if (currentList.isEmpty() && trimmedUserInput.isValidUrl()) { + emit( + listOf( + HomeserverData( + homeserverUrl = trimmedUserInput, + isWellknownValid = false, + supportSlidingSync = false, ) ) - } else { - emit(Async.Uninitialized) - } + ) } } 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 index f95c027620..1d8271e394 100644 --- 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 @@ -18,6 +18,7 @@ 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 @@ -28,6 +29,9 @@ 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( @@ -42,14 +46,12 @@ class SearchAccountProviderPresenter @Inject constructor( } val changeServerState = changeServerPresenter.present() - var data: Async> by remember { + val data: MutableState>> = remember { mutableStateOf(Async.Uninitialized) } LaunchedEffect(userInput) { - homeserverResolver.resolve(userInput).collect { - data = it - } + onUserInput(userInput, data) } fun handleEvents(event: SearchAccountProviderEvents) { @@ -62,9 +64,22 @@ class SearchAccountProviderPresenter @Inject constructor( return SearchAccountProviderState( userInput = userInput, - userInputResult = data, + 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 + } + } } From 0d55c47d4ffdb270a89099f0cd9b72707eb4051b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Jun 2023 18:51:17 +0200 Subject: [PATCH 067/130] Fix infinite tests. --- .../SearchAccountProviderPresenterTest.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 index 9163f247f5..c1d36c2e2f 100644 --- 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 @@ -63,7 +63,7 @@ class SearchAccountProviderPresenterTest { AccountProviderDataSource() ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + HomeserverResolver(testCoroutineDispatchers(testScheduler), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -87,7 +87,7 @@ class SearchAccountProviderPresenterTest { AccountProviderDataSource() ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + HomeserverResolver(testCoroutineDispatchers(testScheduler), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -122,7 +122,7 @@ class SearchAccountProviderPresenterTest { AccountProviderDataSource() ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + HomeserverResolver(testCoroutineDispatchers(testScheduler), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -157,7 +157,7 @@ class SearchAccountProviderPresenterTest { AccountProviderDataSource() ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), + HomeserverResolver(testCoroutineDispatchers(testScheduler), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { From 91d20e1b7867553a82523a5abff4ffb49ea5fac7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 12 Jun 2023 19:49:56 +0200 Subject: [PATCH 068/130] Enforce usage of `TestScope.testScheduler` --- .../features/leaveroom/impl/LeaveRoomPresenterImplTest.kt | 2 +- .../SearchAccountProviderPresenterTest.kt | 8 ++++---- .../features/messages/fixtures/timelineItemsFactory.kt | 3 ++- .../impl/invite/RoomInviteMembersPresenterTest.kt | 3 ++- .../roomdetails/members/RoomMemberListPresenterTests.kt | 5 +++-- .../android/tests/testutils/TestCoroutineDispatchers.kt | 7 ++----- 6 files changed, 14 insertions(+), 14 deletions(-) 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/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 index c1d36c2e2f..9163f247f5 100644 --- 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 @@ -63,7 +63,7 @@ class SearchAccountProviderPresenterTest { AccountProviderDataSource() ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(testScheduler), fakeWellknownRequest), + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -87,7 +87,7 @@ class SearchAccountProviderPresenterTest { AccountProviderDataSource() ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(testScheduler), fakeWellknownRequest), + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -122,7 +122,7 @@ class SearchAccountProviderPresenterTest { AccountProviderDataSource() ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(testScheduler), fakeWellknownRequest), + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { @@ -157,7 +157,7 @@ class SearchAccountProviderPresenterTest { AccountProviderDataSource() ) val presenter = SearchAccountProviderPresenter( - HomeserverResolver(testCoroutineDispatchers(testScheduler), fakeWellknownRequest), + HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRequest), changeServerPresenter ) moleculeFlow(RecompositionClock.Immediate) { 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 c6ea04f407..ba1e7a8869 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 @@ -36,8 +36,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/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/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( From 92adef538481c5b7c47732e30e844394fcff76dd Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 12 Jun 2023 20:23:58 +0200 Subject: [PATCH 069/130] Extract fetchDetailsForEvent and use coroutineDispatchers.io --- .../timeline/factories/TimelineItemsFactory.kt | 5 +++-- .../matrix/api/timeline/MatrixTimeline.kt | 2 ++ .../matrix/impl/room/RustMatrixRoom.kt | 11 +++++------ .../impl/timeline/MatrixTimelineItemMapper.kt | 17 +++++------------ .../matrix/impl/timeline/RustMatrixTimeline.kt | 8 +++++++- .../matrix/test/timeline/FakeMatrixTimeline.kt | 4 ++++ 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt index f4e771a32d..c08c8d65a3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/TimelineItemsFactory.kt @@ -26,6 +26,7 @@ import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.timeline.MatrixTimelineItem import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toPersistentList import kotlinx.coroutines.flow.MutableStateFlow @@ -45,7 +46,7 @@ class TimelineItemsFactory @Inject constructor( private val timelineItemGrouper: TimelineItemGrouper, ) { - private val timelineItems = MutableStateFlow(emptyList().toImmutableList()) + private val timelineItems = MutableStateFlow(persistentListOf()) private val timelineItemsCache = arrayListOf() // Items from rust sdk, used for diffing @@ -95,7 +96,7 @@ class TimelineItemsFactory @Inject constructor( Timber.v("Time to apply diff on new list of ${newTimelineItems.size} items: $timeToDiff ms") } - private suspend fun buildAndCacheItem( + private fun buildAndCacheItem( timelineItems: List, index: Int ): TimelineItem? { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt index 091691b0ec..4eaf04d775 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimeline.kt @@ -42,4 +42,6 @@ interface MatrixTimeline { suspend fun editMessage(originalEventId: EventId, message: String): Result suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result + + suspend fun fetchDetailsForEvent(eventId: EventId): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 63d54892f7..177076ed28 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -34,7 +34,6 @@ import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -259,7 +258,7 @@ class RustMatrixRoom( } } - override suspend fun sendReaction(emoji: String, eventId: EventId): Result = withContext(Dispatchers.IO) { + override suspend fun sendReaction(emoji: String, eventId: EventId): Result = withContext(coroutineDispatchers.io) { runCatching { innerRoom.sendReaction(key = emoji, eventId = eventId.value) } @@ -267,28 +266,28 @@ class RustMatrixRoom( @OptIn(ExperimentalUnsignedTypes::class) override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { runCatching { innerRoom.uploadAvatar(mimeType, data.toUByteArray().toList()) } } override suspend fun removeAvatar(): Result = - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { runCatching { innerRoom.removeAvatar() } } override suspend fun setName(name: String): Result = - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { runCatching { innerRoom.setName(name) } } override suspend fun setTopic(topic: String): Result = - withContext(Dispatchers.IO) { + withContext(coroutineDispatchers.io) { runCatching { innerRoom.setTopic(topic) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineItemMapper.kt index c90e672f28..f7cf728691 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/MatrixTimelineItemMapper.kt @@ -22,15 +22,13 @@ import io.element.android.libraries.matrix.impl.timeline.item.event.EventTimelin import io.element.android.libraries.matrix.impl.timeline.item.virtual.VirtualTimelineItemMapper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import org.matrix.rustcomponents.sdk.Room import org.matrix.rustcomponents.sdk.TimelineItem -import timber.log.Timber class MatrixTimelineItemMapper( - private val room: Room, + private val fetchDetailsForEvent: suspend (EventId) -> Result, private val coroutineScope: CoroutineScope, private val virtualTimelineItemMapper: VirtualTimelineItemMapper = VirtualTimelineItemMapper(), - private val eventTimelineItemMapper: EventTimelineItemMapper= EventTimelineItemMapper(), + private val eventTimelineItemMapper: EventTimelineItemMapper = EventTimelineItemMapper(), ) { fun map(timelineItem: TimelineItem): MatrixTimelineItem = timelineItem.use { @@ -40,7 +38,7 @@ class MatrixTimelineItemMapper( if (eventTimelineItem.hasNotLoadedInReplyTo() && eventTimelineItem.eventId != null) { - fetchDetailsForEvent(eventTimelineItem.eventId!!) + fetchEventDetails(eventTimelineItem.eventId!!) } return MatrixTimelineItem.Event(eventTimelineItem) @@ -53,12 +51,7 @@ class MatrixTimelineItemMapper( return MatrixTimelineItem.Other } - private fun fetchDetailsForEvent(eventId: EventId) = coroutineScope.launch { - runCatching { - room.fetchDetailsForEvent(eventId.value) - }.onFailure { - Timber.e(it) - } + private fun fetchEventDetails(eventId: EventId) = coroutineScope.launch { + fetchDetailsForEvent(eventId) } - } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt index 79b92e3c29..4b07d59970 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/RustMatrixTimeline.kt @@ -63,7 +63,7 @@ class RustMatrixTimeline( ) private val timelineItemFactory = MatrixTimelineItemMapper( - room = innerRoom, + fetchDetailsForEvent = this::fetchDetailsForEvent, coroutineScope = coroutineScope, virtualTimelineItemMapper = VirtualTimelineItemMapper(), eventTimelineItemMapper = EventTimelineItemMapper( @@ -130,6 +130,12 @@ class RustMatrixTimeline( return matrixRoom.replyMessage(inReplyToEventId, message) } + override suspend fun fetchDetailsForEvent(eventId: EventId): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.fetchDetailsForEvent(eventId.value) + } + } + override suspend fun paginateBackwards(requestSize: Int, untilNumberOfItems: Int): Result = withContext(coroutineDispatchers.io) { runCatching { Timber.v("Start back paginating for room ${matrixRoom.roomId} ") diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt index bb5c0157b6..696a778df2 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/timeline/FakeMatrixTimeline.kt @@ -82,4 +82,8 @@ class FakeMatrixTimeline( override suspend fun replyMessage(inReplyToEventId: EventId, message: String): Result { return Result.success(Unit) } + + override suspend fun fetchDetailsForEvent(eventId: EventId): Result { + return Result.success(Unit) + } } From 764ca521c09f0ab35d6e375061ead091f34a0a9d Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 13 Jun 2023 10:02:08 +0200 Subject: [PATCH 070/130] Upgrade Rust SDK to 0.1.18 (#582) --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41e01194e7..1844d3bdfd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -139,7 +139,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.17" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.18" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } From 3c250b2a0dabadc1063a710f963cad0082c609cd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 13 Jun 2023 11:10:29 +0200 Subject: [PATCH 071/130] Unblock develop --- .../messages/MessagesPresenterTest.kt | 2 +- .../media/viewer/MediaViewerPresenterTest.kt | 13 ++++++------ .../libraries/matrix/test/FakeMatrixClient.kt | 5 +---- .../matrix/test/media/FakeMediaLoader.kt | 20 +++++++++---------- 4 files changed, 18 insertions(+), 22 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index d517b261e2..1ced503920 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -330,7 +330,7 @@ class MessagesPresenterTest { networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), messageSummaryFormatter = FakeMessageSummaryFormatter(), - dispatchers = testCoroutineDispatchers(testScheduler), + dispatchers = testCoroutineDispatchers(), ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt index ea40cfe791..31cddffb95 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -23,7 +23,6 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.local.aFileInfo import io.element.android.features.messages.impl.media.viewer.MediaViewerEvents import io.element.android.features.messages.impl.media.viewer.MediaViewerNode @@ -49,8 +48,8 @@ class MediaViewerPresenterTest { @Test fun `present - download media success scenario`() = runTest { - val coroutineDispatchers = testCoroutineDispatchers(testScheduler, useUnconfinedTestDispatcher = false) - val mediaLoader = FakeMediaLoader(coroutineDispatchers) + val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = false) + val mediaLoader = FakeMediaLoader() val mediaActions = FakeLocalMediaActions(coroutineDispatchers) val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) moleculeFlow(RecompositionClock.Immediate) { @@ -70,8 +69,8 @@ class MediaViewerPresenterTest { @Test fun `present - check all actions `() = runTest { - val coroutineDispatchers = testCoroutineDispatchers(testScheduler, useUnconfinedTestDispatcher = false) - val mediaLoader = FakeMediaLoader(coroutineDispatchers) + val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = false) + val mediaLoader = FakeMediaLoader() val mediaActions = FakeLocalMediaActions(coroutineDispatchers) val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) moleculeFlow(RecompositionClock.Immediate) { @@ -119,8 +118,8 @@ class MediaViewerPresenterTest { @Test fun `present - download media failure then retry with success scenario`() = runTest { - val coroutineDispatchers = testCoroutineDispatchers(testScheduler, useUnconfinedTestDispatcher = false) - val mediaLoader = FakeMediaLoader(coroutineDispatchers) + val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = false) + val mediaLoader = FakeMediaLoader() val mediaActions = FakeLocalMediaActions(coroutineDispatchers) val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) moleculeFlow(RecompositionClock.Immediate) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 79a8186e1a..aaaac28ba7 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -37,18 +37,15 @@ import io.element.android.libraries.matrix.test.pushers.FakePushersService import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService -import io.element.android.tests.testutils.testCoroutineDispatchers import kotlinx.coroutines.delay -import kotlinx.coroutines.test.StandardTestDispatcher class FakeMatrixClient( override val sessionId: SessionId = A_SESSION_ID, - private val coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), private val userDisplayName: Result = Result.success(A_USER_NAME), private val userAvatarURLString: Result = Result.success(AN_AVATAR_URL), override val roomSummaryDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(), override val invitesDataSource: RoomSummaryDataSource = FakeRoomSummaryDataSource(), - override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(coroutineDispatchers), + override val mediaLoader: MatrixMediaLoader = FakeMediaLoader(), private val sessionVerificationService: FakeSessionVerificationService = FakeSessionVerificationService(), private val pushersService: FakePushersService = FakePushersService(), private val notificationService: FakeNotificationService = FakeNotificationService(), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt index 4282860c99..f351ace464 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -16,37 +16,37 @@ package io.element.android.libraries.matrix.test.media -import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS import kotlinx.coroutines.delay -import kotlinx.coroutines.withContext -import kotlin.coroutines.coroutineContext -class FakeMediaLoader(private val coroutineDispatchers: CoroutineDispatchers) : MatrixMediaLoader { +class FakeMediaLoader : MatrixMediaLoader { var shouldFail = false - override suspend fun loadMediaContent(source: MediaSource): Result = withContext(coroutineDispatchers.io){ - if (shouldFail) { + override suspend fun loadMediaContent(source: MediaSource): Result { + delay(FAKE_DELAY_IN_MS) + return if (shouldFail) { Result.failure(RuntimeException()) } else { Result.success(ByteArray(0)) } } - override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result = withContext(coroutineDispatchers.io){ - if (shouldFail) { + override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result { + delay(FAKE_DELAY_IN_MS) + return if (shouldFail) { Result.failure(RuntimeException()) } else { Result.success(ByteArray(0)) } } - override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result = withContext(coroutineDispatchers.io){ - if (shouldFail) { + override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result { + delay(FAKE_DELAY_IN_MS) + return if (shouldFail) { Result.failure(RuntimeException()) } else { Result.success(FakeMediaFile("")) From da486d388bba3f1bfb9be21c7f0df97713ea3282 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 14 Jun 2023 07:38:55 +0000 Subject: [PATCH 072/130] Update screenshots --- ...ccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ccountProviderViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ccountProviderViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...ccountProviderViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...ccountProviderViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ccountProviderViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...countProviderViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- 14 files changed, 28 insertions(+), 28 deletions(-) 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 index a0ea87cbc0..9b57449f4c 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c5ecc3788746bc425efe843018b13a8fad0a10665fcc0b9b528df6df54791cef -size 20811 +oid sha256:bbb3612de857b16799f9133da4f4cd90665f4f81738127b63c57b0265e097483 +size 20870 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 index 2b5edb0440..d827cba4e0 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2036b2b9b3815c69dca77fbde216be6d333607d6dc3ed254923ba35244d20061 -size 9441 +oid sha256:df5c0566208a88e9fcc606d12c34a4ccbb3773e30ea9d27b8527f4bbe1bf31b7 +size 9402 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 index 22503a7c95..3c9a443627 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:59d342f4e163fd52199a2e05a2d403698a87bb51ce2494ee52c01968edbd71d0 -size 10482 +oid sha256:2268f7d747e2926cdaae791bacd2741c659018112854fc5b525321206d441a58 +size 10390 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 index 1f570432a2..4b13301d6f 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e38d29272030a66ce7d581f82f82553a3a19f4694117e1b5dd5d507ecce20c4 -size 8532 +oid sha256:6e533e3d36ee815d8a9f61ba7359cb4031e9948e1d7d396caf12f73f2dbf03b2 +size 8476 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 index 218db727b5..5f165a39f2 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:07414bddb30f4535ba56c38cd295cc272a8531641485a5339f898b4186d3a466 -size 7464 +oid sha256:742bfd7e633fb4d743ff8036901e1d6afa43f3f041faaa075ec020301fa7ef1f +size 7373 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 index d44b573a9a..13291776bd 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6ce4df1f9faf5704b2407357f06a8879ad57fcc593a0fa59d642f580f1bd7bab -size 20098 +oid sha256:bebd1d3386ed304b3501cee88a3b192585e65ad23322cf8a6905971ed5fc8251 +size 20244 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 index 37e177fe42..6038fa4f29 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:64ed033fbcccc2a5d40664ebbe2a32782579f77eaa07200a329d49165f2606a2 -size 9122 +oid sha256:c1d0c114d197386b28ae086089a87d5b85d09c08805671b66d9fb4ff9889d2ed +size 9117 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 index bc67803f7f..c14b4e259f 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:09ce6b3272ea2450b30c7b3b5fe6ee0aef44a03e62f433f42ab17b255e2fa680 -size 10172 +oid sha256:43fcfc09cbe37e93bab1787505085d1f2913f56af04dd2ce08c6eaec24ee61ba +size 10107 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 index 534b7f86b3..5eb18cacf0 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9ac9e05b076079caaaf9cd097b75d6cb928982a7c24a6f2eb9c73573521d959 -size 8276 +oid sha256:742e857cdf1691ee9402fdd3babd209509b18d82be6337073b7d493466cfefa7 +size 8217 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 index e0f3550333..7f6de35768 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:37c6d5fd1dbbb10fe4d18bb70c27409da86d832e3394d05841f194729c5b14cd -size 7148 +oid sha256:f855ee063e8bc310038db2f6dd6fd5042272096303b8ce953b77ebefc707628e +size 7088 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 index d7f8ebf657..1108e511aa 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0cf16abe33c78fbe4db6c447fb54a4c6f4495e02ab13774ddd25bc74c24e785b -size 43758 +oid sha256:83875da521afb2a14f76d9a51c30cca720589352c0cf1e4aded4c89173b44800 +size 43615 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 index cd94f38e06..622c4a378c 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3712ed6d166f726d4c6f56cdf9208787e7b79aec49900e3e59702e23da07c076 -size 43460 +oid sha256:9b7298092a46e91cb5aa96d6fef49eada76389c821962ea6192d9595fdcff7cb +size 43183 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 index 02296060db..da0ea09508 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ac1a1bec7cc71e7d1ab8513c98cb315f720fe0a7341e3485fabbaff4134352aa -size 48795 +oid sha256:c316e712fe97ffbc22d182a44cebcd0b27c498af6d33028006907f17d12f9b8b +size 48688 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 index 95741de0ba..f17a59d55f 100644 --- 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 @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4935ca32e03ea0687d766f6675af3fb78ee7cfff2c7e32e3557d1065fc91ad40 -size 48404 +oid sha256:f65024164d91bbec1058a104dcac0b835f98bfb7a46ce0a0d21a3ddb11fa7e32 +size 48332 From d2a089a5a6dce76c14f40e9fc9c02c115397a978 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 14 Jun 2023 10:33:26 +0200 Subject: [PATCH 073/130] Extract dependencies from group `com.google.auto.service` into libs.versions.toml to avoid multiple PR to upgrade the 2 deps. --- anvilcodegen/build.gradle.kts | 4 ++-- gradle/libs.versions.toml | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/anvilcodegen/build.gradle.kts b/anvilcodegen/build.gradle.kts index 340cf43073..57758f8909 100644 --- a/anvilcodegen/build.gradle.kts +++ b/anvilcodegen/build.gradle.kts @@ -25,6 +25,6 @@ dependencies { implementation(libs.anvil.compiler.utils) implementation("com.squareup:kotlinpoet:1.14.2") implementation(libs.dagger) - compileOnly("com.google.auto.service:auto-service-annotations:1.1.1") - kapt("com.google.auto.service:auto-service:1.1.1") + compileOnly(libs.google.autoservice.annotations) + kapt(libs.google.autoservice) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1844d3bdfd..8d2631c889 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -50,6 +50,9 @@ telephoto = "0.4.0" dagger = "2.46.1" anvil = "2.4.6" +# Auto service +autoservice = "1.1.1" + # quality detekt = "1.23.0" dependencygraph = "0.12" @@ -165,11 +168,16 @@ dagger_compiler = { module = "com.google.dagger:dagger-compiler", version.ref = anvil_compiler_api = { module = "com.squareup.anvil:compiler-api", version.ref = "anvil" } anvil_compiler_utils = { module = "com.squareup.anvil:compiler-utils", version.ref = "anvil" } +# Auto services +google_autoservice = { module = "com.google.auto.service:auto-service", version.ref = "autoservice" } +google_autoservice_annotations = { module = "com.google.auto.service:auto-service-annotations", version.ref = "autoservice" } + + # Miscellaneous # Add unused dependency to androidx.compose.compiler:compiler to let Renovate create PR to change the # value of `composecompiler` (which is used to set composeOptions.kotlinCompilerExtensionVersion. # See https://github.com/renovatebot/renovate/issues/18354 -android_composeCompiler = {module="androidx.compose.compiler:compiler", version.ref ="composecompiler"} +android_composeCompiler = { module = "androidx.compose.compiler:compiler", version.ref = "composecompiler" } [bundles] From 8bfca0232acfe09feabd7a24acb107b1065d90d9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Jun 2023 11:15:31 +0200 Subject: [PATCH 074/130] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.19 (#592) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1844d3bdfd..32c82a7c14 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -139,7 +139,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.18" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.19" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } From 123872dc3abb196aa5b99df0feb5d24bb21c96f4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Jun 2023 10:52:12 +0200 Subject: [PATCH 075/130] Timeline: fix ReplyToContent clip on click --- .../messages/impl/timeline/components/TimelineItemEventRow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index f84a899c6b..4eec5cbda8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -251,6 +251,7 @@ private fun MessageEventBubbleContent( attachmentThumbnailInfo = attachmentThumbnailInfo, modifier = Modifier .padding(top = 8.dp, start = 8.dp, end = 8.dp) + .clip(RoundedCornerShape(6.dp)) .clickable(enabled = true, onClick = inReplyToClick), ) } @@ -295,7 +296,6 @@ private fun ReplyToContent( } Row( modifier - .clip(RoundedCornerShape(6.dp)) .background(MaterialTheme.colorScheme.surface) .padding(paddings) ) { From 2f3a930ca32d84c35096affbc376f8fdcc1717a8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 13 Jun 2023 11:20:24 +0200 Subject: [PATCH 076/130] Timeline: fix wrong username for reply --- .../messages/impl/timeline/components/TimelineItemEventRow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index 4eec5cbda8..270f76b010 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -243,7 +243,7 @@ private fun MessageEventBubbleContent( ) { EqualWidthColumn(modifier = modifier, spacing = 8.dp) { if (inReplyToDetails != null) { - val senderName = event.senderDisplayName ?: event.senderId.value + val senderName = inReplyToDetails.senderDisplayName ?: inReplyToDetails.senderId.value val attachmentThumbnailInfo = attachmentThumbnailInfoForInReplyTo(inReplyToDetails) ReplyToContent( senderName = senderName, From 9321a9f7184002e737cb73cf16983307fdf2bc57 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 15 Jun 2023 10:57:05 +0200 Subject: [PATCH 077/130] Introduce `simulateLongTask` to ensure that the Presenter State `Loading` is visible. Also do some cleanup on the tests. --- .../ConfigureRoomPresenterTests.kt | 4 +- .../impl/root/CreateRoomRootPresenterTests.kt | 7 ++- .../impl/InviteListPresenterTests.kt | 21 +------- .../impl/LogoutPreferencePresenterTest.kt | 7 ++- .../messages/MessagesPresenterTest.kt | 11 ++-- .../AttachmentsPreviewPresenterTest.kt | 4 -- .../messages/media/FakeLocalMediaActions.kt | 11 ++-- .../media/viewer/MediaViewerPresenterTest.kt | 10 ++-- .../roomdetails/RoomDetailsPresenterTests.kt | 7 +-- .../edit/RoomDetailsEditPresenterTest.kt | 25 ++++++---- .../invite/RoomInviteMembersPresenterTest.kt | 50 +++++++++++-------- .../members/RoomMemberListPresenterTests.kt | 7 ++- .../RoomMemberDetailsPresenterTests.kt | 18 +++---- .../impl/DefaultInviteStateDataSourceTest.kt | 4 +- .../roomlist/impl/RoomListPresenterTests.kt | 20 +++----- .../libraries/matrix/test/FakeMatrixClient.kt | 1 - .../android/libraries/matrix/test/TestData.kt | 2 - .../test/auth/FakeAuthenticationService.kt | 22 ++++---- .../matrix/test/media/FakeMediaLoader.kt | 18 +++---- .../matrix/test/room/FakeMatrixRoom.kt | 49 ++++++++---------- libraries/mediaupload/test/build.gradle.kts | 1 + .../mediaupload/test/FakeMediaPreProcessor.kt | 5 +- .../android/samples/minimal/RoomListScreen.kt | 2 +- .../android/tests/testutils/LongTask.kt | 28 +++++++++++ .../testutils/TestCoroutineDispatchers.kt | 15 +++--- 25 files changed, 169 insertions(+), 180 deletions(-) create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/LongTask.kt diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 7f36b9e2b9..736fca9cb4 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -21,7 +21,6 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.createroom.impl.userlist.UserListDataStore @@ -33,6 +32,7 @@ import io.element.android.libraries.matrix.test.A_ROOM_NAME import io.element.android.libraries.matrix.test.A_THROWABLE import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.media.AvatarAction import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaUploadInfo import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor @@ -226,6 +226,7 @@ class ConfigureRoomPresenterTests { val initialState = awaitItem() initialState.eventSink(ConfigureRoomEvents.CreateRoom(initialState.config)) + assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java) val stateAfterCreateRoom = awaitItem() assertThat(stateAfterCreateRoom.createRoomAction).isInstanceOf(Async.Failure::class.java) @@ -234,7 +235,6 @@ class ConfigureRoomPresenterTests { assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Uninitialized::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().createRoomAction).isInstanceOf(Async.Success::class.java) - } } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index 8d9819eeae..0d0fdcce44 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -49,7 +49,12 @@ class CreateRoomRootPresenterTests { fakeUserListPresenter = FakeUserListPresenter() fakeMatrixClient = FakeMatrixClient() userRepository = FakeUserRepository() - presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userRepository, UserListDataStore(), fakeMatrixClient) + presenter = CreateRoomRootPresenter( + presenterFactory = FakeUserListPresenterFactory(fakeUserListPresenter), + userRepository = userRepository, + userListDataStore = UserListDataStore(), + matrixClient = fakeMatrixClient + ) } @Test diff --git a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt index 462faa405f..cf4f1058e5 100644 --- a/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt +++ b/features/invitelist/impl/src/test/kotlin/io/element/android/features/invitelist/impl/InviteListPresenterTests.kt @@ -32,12 +32,11 @@ import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_ID_2 import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient -import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import kotlinx.coroutines.test.runTest import org.junit.Test @@ -48,7 +47,6 @@ class InviteListPresenterTests { val invitesDataSource = FakeRoomSummaryDataSource() val presenter = InviteListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ), FakeSeenInvitesStore(), @@ -73,7 +71,6 @@ class InviteListPresenterTests { val invitesDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation() val presenter = InviteListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ), FakeSeenInvitesStore(), @@ -100,10 +97,8 @@ class InviteListPresenterTests { @Test fun `present - includes sender details for room invites`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() - val presenter = InviteListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ), FakeSeenInvitesStore(), @@ -128,10 +123,8 @@ class InviteListPresenterTests { @Test fun `present - shows confirm dialog for declining direct chat invites`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withDirectChatInvitation() - val presenter = InviteListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ), FakeSeenInvitesStore(), @@ -154,10 +147,8 @@ class InviteListPresenterTests { @Test fun `present - shows confirm dialog for declining room invites`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() - val presenter = InviteListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ), FakeSeenInvitesStore(), @@ -180,10 +171,8 @@ class InviteListPresenterTests { @Test fun `present - hides confirm dialog when cancelling`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() - val presenter = InviteListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ), FakeSeenInvitesStore(), @@ -207,7 +196,6 @@ class InviteListPresenterTests { fun `present - declines invite after confirming`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val client = FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ) val room = FakeMatrixRoom() @@ -234,7 +222,6 @@ class InviteListPresenterTests { fun `present - declines invite after confirming and sets state on error`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val client = FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ) val room = FakeMatrixRoom() @@ -266,7 +253,6 @@ class InviteListPresenterTests { fun `present - dismisses declining error state`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val client = FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ) val room = FakeMatrixRoom() @@ -299,7 +285,6 @@ class InviteListPresenterTests { fun `present - accepts invites and sets state on success`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val client = FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ) val room = FakeMatrixRoom() @@ -323,7 +308,6 @@ class InviteListPresenterTests { fun `present - accepts invites and sets state on error`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val client = FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ) val room = FakeMatrixRoom() @@ -349,7 +333,6 @@ class InviteListPresenterTests { fun `present - dismisses accepting error state`() = runTest { val invitesDataSource = FakeRoomSummaryDataSource().withRoomInvitation() val client = FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ) val room = FakeMatrixRoom() @@ -379,7 +362,6 @@ class InviteListPresenterTests { val store = FakeSeenInvitesStore() val presenter = InviteListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ), store, @@ -416,7 +398,6 @@ class InviteListPresenterTests { store.publishRoomIds(setOf(A_ROOM_ID)) val presenter = InviteListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, invitesDataSource = invitesDataSource, ), store, diff --git a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt index 7a3556389f..bed33006d6 100644 --- a/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt +++ b/features/logout/impl/src/test/kotlin/io/element/android/features/logout/impl/LogoutPreferencePresenterTest.kt @@ -23,7 +23,6 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.logout.api.LogoutPreferenceEvents import io.element.android.features.logout.api.LogoutPreferenceState import io.element.android.libraries.architecture.Async -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.FakeMatrixClient import kotlinx.coroutines.test.runTest @@ -33,7 +32,7 @@ class LogoutPreferencePresenterTest { @Test fun `present - initial state`() = runTest { val presenter = DefaultLogoutPreferencePresenter( - FakeMatrixClient(A_SESSION_ID), + FakeMatrixClient(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -46,7 +45,7 @@ class LogoutPreferencePresenterTest { @Test fun `present - logout`() = runTest { val presenter = DefaultLogoutPreferencePresenter( - FakeMatrixClient(A_SESSION_ID), + FakeMatrixClient(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -62,7 +61,7 @@ class LogoutPreferencePresenterTest { @Test fun `present - logout with error`() = runTest { - val matrixClient = FakeMatrixClient(A_SESSION_ID) + val matrixClient = FakeMatrixClient() val presenter = DefaultLogoutPreferencePresenter( matrixClient, ) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 1ced503920..a31e4e10a5 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -35,6 +35,7 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.media.FakeLocalMediaFactory import io.element.android.features.messages.utils.messagesummary.FakeMessageSummaryFormatter import io.element.android.features.networkmonitor.test.FakeNetworkMonitor +import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.core.mimetype.MimeTypes @@ -53,7 +54,6 @@ import io.element.android.libraries.textcomposer.MessageComposerMode import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.mockk import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.Test @@ -75,8 +75,9 @@ class MessagesPresenterTest { @Test fun `present - handle sending a reaction`() = runTest { + val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val room = FakeMatrixRoom() - val presenter = createMessagePresenter(matrixRoom = room) + val presenter = createMessagePresenter(matrixRoom = room, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -254,8 +255,9 @@ class MessagesPresenterTest { @Test fun `present - handle action redact`() = runTest { + val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val matrixRoom = FakeMatrixRoom() - val presenter = createMessagePresenter(matrixRoom) + val presenter = createMessagePresenter(matrixRoom = matrixRoom, coroutineDispatchers = coroutineDispatchers) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -293,6 +295,7 @@ class MessagesPresenterTest { } private fun TestScope.createMessagePresenter( + coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixRoom: MatrixRoom = FakeMatrixRoom() ): MessagesPresenter { val messageComposerPresenter = MessageComposerPresenter( @@ -330,7 +333,7 @@ class MessagesPresenterTest { networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), messageSummaryFormatter = FakeMessageSummaryFormatter(), - dispatchers = testCoroutineDispatchers(), + dispatchers = coroutineDispatchers, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt index 3789c36146..18f5ae8da3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/attachments/AttachmentsPreviewPresenterTest.kt @@ -19,7 +19,6 @@ package io.element.android.features.messages.attachments import android.net.Uri -import androidx.media3.common.MimeTypes import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test @@ -31,7 +30,6 @@ import io.element.android.features.messages.impl.attachments.preview.Attachments import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS import io.element.android.libraries.matrix.test.room.FakeMatrixRoom import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender @@ -58,7 +56,6 @@ class AttachmentsPreviewPresenterTest { initialState.eventSink(AttachmentsPreviewEvents.SendAttachment) val loadingState = awaitItem() assertThat(loadingState.sendActionState).isEqualTo(Async.Loading()) - testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS) val successState = awaitItem() assertThat(successState.sendActionState).isEqualTo(Async.Success(Unit)) assertThat(room.sendMediaCount).isEqualTo(1) @@ -79,7 +76,6 @@ class AttachmentsPreviewPresenterTest { initialState.eventSink(AttachmentsPreviewEvents.SendAttachment) val loadingState = awaitItem() assertThat(loadingState.sendActionState).isEqualTo(Async.Loading()) - testScheduler.advanceTimeBy(FAKE_DELAY_IN_MS) val failureState = awaitItem() assertThat(failureState.sendActionState).isEqualTo(Async.Failure(failure)) assertThat(room.sendMediaCount).isEqualTo(0) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaActions.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaActions.kt index 25a62e439a..5bdef5f9b1 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaActions.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/FakeLocalMediaActions.kt @@ -19,10 +19,9 @@ package io.element.android.features.messages.media import androidx.compose.runtime.Composable import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.LocalMediaActions -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import kotlinx.coroutines.withContext +import io.element.android.tests.testutils.simulateLongTask -class FakeLocalMediaActions(private val coroutineDispatchers: CoroutineDispatchers) : LocalMediaActions { +class FakeLocalMediaActions : LocalMediaActions { var shouldFail = false @@ -31,7 +30,7 @@ class FakeLocalMediaActions(private val coroutineDispatchers: CoroutineDispatche //NOOP } - override suspend fun saveOnDisk(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { + override suspend fun saveOnDisk(localMedia: LocalMedia): Result = simulateLongTask { if (shouldFail) { Result.failure(RuntimeException()) } else { @@ -39,7 +38,7 @@ class FakeLocalMediaActions(private val coroutineDispatchers: CoroutineDispatche } } - override suspend fun share(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { + override suspend fun share(localMedia: LocalMedia): Result = simulateLongTask { if (shouldFail) { Result.failure(RuntimeException()) } else { @@ -47,7 +46,7 @@ class FakeLocalMediaActions(private val coroutineDispatchers: CoroutineDispatche } } - override suspend fun open(localMedia: LocalMedia): Result = withContext(coroutineDispatchers.io) { + override suspend fun open(localMedia: LocalMedia): Result = simulateLongTask { if (shouldFail) { Result.failure(RuntimeException()) } else { diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt index 31cddffb95..8a6025d3bd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -33,7 +33,6 @@ import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.matrix.test.media.FakeMediaLoader import io.element.android.libraries.matrix.test.media.aMediaSource -import io.element.android.tests.testutils.testCoroutineDispatchers import io.mockk.mockk import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -48,9 +47,8 @@ class MediaViewerPresenterTest { @Test fun `present - download media success scenario`() = runTest { - val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = false) val mediaLoader = FakeMediaLoader() - val mediaActions = FakeLocalMediaActions(coroutineDispatchers) + val mediaActions = FakeLocalMediaActions() val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -69,9 +67,8 @@ class MediaViewerPresenterTest { @Test fun `present - check all actions `() = runTest { - val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = false) val mediaLoader = FakeMediaLoader() - val mediaActions = FakeLocalMediaActions(coroutineDispatchers) + val mediaActions = FakeLocalMediaActions() val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -118,9 +115,8 @@ class MediaViewerPresenterTest { @Test fun `present - download media failure then retry with success scenario`() = runTest { - val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = false) val mediaLoader = FakeMediaLoader() - val mediaActions = FakeLocalMediaActions(coroutineDispatchers) + val mediaActions = FakeLocalMediaActions() val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) moleculeFlow(RecompositionClock.Immediate) { presenter.present() 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 43f1667730..f85942c3ee 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 @@ -27,7 +27,6 @@ import io.element.android.features.roomdetails.impl.RoomTopicState import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState @@ -48,7 +47,7 @@ class RoomDetailsPresenterTests { private fun aRoomDetailsPresenter(room: MatrixRoom): RoomDetailsPresenter { val roomMemberDetailsPresenterFactory = object : RoomMemberDetailsPresenter.Factory { override fun create(roomMemberId: UserId): RoomMemberDetailsPresenter { - return RoomMemberDetailsPresenter(aMatrixClient(), room, roomMemberId) + return RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMemberId) } } return RoomDetailsPresenter(room, roomMemberDetailsPresenterFactory, LeaveRoomPresenterFake()) @@ -250,10 +249,6 @@ class RoomDetailsPresenterTests { } } -fun aMatrixClient( - sessionId: SessionId = A_SESSION_ID, -) = FakeMatrixClient(sessionId = sessionId) - fun aMatrixRoom( roomId: RoomId = A_ROOM_ID, name: String? = A_ROOM_NAME, diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt index c1d098dab5..df80f40e9b 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/edit/RoomDetailsEditPresenterTest.kt @@ -105,7 +105,8 @@ class RoomDetailsEditPresenterTest { val room = aMatrixRoom(avatarUrl = AN_AVATAR_URL).apply { givenCanSendStateResult(StateEventType.ROOM_NAME, Result.success(true)) givenCanSendStateResult(StateEventType.ROOM_AVATAR, Result.success(false)) - givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.failure(Throwable("Oops"))) } + givenCanSendStateResult(StateEventType.ROOM_TOPIC, Result.failure(Throwable("Oops"))) + } val presenter = aRoomDetailsEditPresenter(room) moleculeFlow(RecompositionClock.Immediate) { @@ -381,7 +382,7 @@ class RoomDetailsEditPresenterTest { initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("New topic")) initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.Remove)) initialState.eventSink(RoomDetailsEditEvents.Save) - + skipItems(5) assertThat(room.newName).isEqualTo("New name") assertThat(room.newTopic).isEqualTo("New topic") assertThat(room.newAvatarData).isNull() @@ -476,7 +477,7 @@ class RoomDetailsEditPresenterTest { initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(RoomDetailsEditEvents.Save) - skipItems(2) + skipItems(3) assertThat(room.newName).isNull() assertThat(room.newTopic).isNull() @@ -501,7 +502,7 @@ class RoomDetailsEditPresenterTest { initialState.eventSink(RoomDetailsEditEvents.HandleAvatarAction(AvatarAction.ChoosePhoto)) initialState.eventSink(RoomDetailsEditEvents.Save) - skipItems(1) + skipItems(2) assertThat(room.newName).isNull() assertThat(room.newTopic).isNull() @@ -567,7 +568,7 @@ class RoomDetailsEditPresenterTest { initialState.eventSink(RoomDetailsEditEvents.UpdateRoomTopic("foo")) initialState.eventSink(RoomDetailsEditEvents.Save) - skipItems(1) + skipItems(2) assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) @@ -588,6 +589,7 @@ class RoomDetailsEditPresenterTest { initialState.eventSink(RoomDetailsEditEvents.Save) skipItems(1) + assertThat(awaitItem().saveAction).isInstanceOf(Async.Loading::class.java) assertThat(awaitItem().saveAction).isInstanceOf(Async.Failure::class.java) } } @@ -599,14 +601,17 @@ class RoomDetailsEditPresenterTest { } fakePickerProvider.givenResult(anotherAvatarUri) - fakeMediaPreProcessor.givenResult(Result.success(MediaUploadInfo.AnyFile( - file = processedFile, - info = mockk(), - ))) + fakeMediaPreProcessor.givenResult( + Result.success( + MediaUploadInfo.AnyFile( + file = processedFile, + info = mockk(), + ) + ) + ) } companion object { private const val ANOTHER_AVATAR_URL = "example://camera/foo.jpg" } - } 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 495e639baf..8600cefeac 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 @@ -93,9 +93,8 @@ internal class RoomInviteMembersPresenterTest { val presenter = RoomInviteMembersPresenter( userRepository = repository, roomMemberListDataSource = createDataSource(FakeMatrixRoom()), - coroutineDispatchers = testCoroutineDispatchers() + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) - moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -120,9 +119,8 @@ internal class RoomInviteMembersPresenterTest { val presenter = RoomInviteMembersPresenter( userRepository = repository, roomMemberListDataSource = createDataSource(FakeMatrixRoom()), - coroutineDispatchers = testCoroutineDispatchers() + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) - moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -157,17 +155,24 @@ internal class RoomInviteMembersPresenterTest { val invitedUser = userList[1] val repository = FakeUserRepository() + val coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) val presenter = RoomInviteMembersPresenter( userRepository = repository, - roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(listOf( - aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), - aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), - ))) - }), - coroutineDispatchers = testCoroutineDispatchers() + roomMemberListDataSource = createDataSource( + matrixRoom = FakeMatrixRoom().apply { + givenRoomMembersState( + MatrixRoomMembersState.Ready( + listOf( + aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), + aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), + ) + ) + ) + }, + coroutineDispatchers = coroutineDispatchers, + ), + coroutineDispatchers = coroutineDispatchers ) - moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -215,12 +220,16 @@ internal class RoomInviteMembersPresenterTest { val presenter = RoomInviteMembersPresenter( userRepository = repository, roomMemberListDataSource = createDataSource(FakeMatrixRoom().apply { - givenRoomMembersState(MatrixRoomMembersState.Ready(listOf( - aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), - aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), - ))) + givenRoomMembersState( + MatrixRoomMembersState.Ready( + listOf( + aRoomMember(userId = joinedUser.userId, membership = RoomMembershipState.JOIN), + aRoomMember(userId = invitedUser.userId, membership = RoomMembershipState.INVITE), + ) + ) + ) }), - coroutineDispatchers = testCoroutineDispatchers() + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) moleculeFlow(RecompositionClock.Immediate) { @@ -287,9 +296,8 @@ internal class RoomInviteMembersPresenterTest { val presenter = RoomInviteMembersPresenter( userRepository = repository, roomMemberListDataSource = createDataSource(FakeMatrixRoom()), - coroutineDispatchers = testCoroutineDispatchers() + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) - moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -323,16 +331,14 @@ internal class RoomInviteMembersPresenterTest { } } - @Test fun `present - toggling a user updates existing search results`() = runTest { val repository = FakeUserRepository() val presenter = RoomInviteMembersPresenter( userRepository = repository, roomMemberListDataSource = createDataSource(FakeMatrixRoom()), - coroutineDispatchers = testCoroutineDispatchers() + coroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true) ) - moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { 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 b9b52b29f8..01f60847f8 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 @@ -20,7 +20,6 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth -import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.impl.members.RoomMemberListDataSource import io.element.android.features.roomdetails.impl.members.RoomMemberListEvents import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter @@ -167,7 +166,7 @@ class RoomMemberListPresenterTests { @ExperimentalCoroutinesApi private fun TestScope.createDataSource( - matrixRoom: MatrixRoom = aMatrixRoom().apply { + matrixRoom: MatrixRoom = FakeMatrixRoom().apply { givenRoomMembersState(MatrixRoomMembersState.Ready(aRoomMemberList())) }, coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() @@ -175,7 +174,7 @@ private fun TestScope.createDataSource( @ExperimentalCoroutinesApi private fun TestScope.createPresenter( + coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(useUnconfinedTestDispatcher = true), matrixRoom: MatrixRoom = FakeMatrixRoom(), - roomMemberListDataSource: RoomMemberListDataSource = createDataSource(), - coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers() + roomMemberListDataSource: RoomMemberListDataSource = createDataSource(coroutineDispatchers = coroutineDispatchers), ) = RoomMemberListPresenter(matrixRoom, roomMemberListDataSource, coroutineDispatchers) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt index 294b689ea9..912f354c89 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -20,13 +20,13 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth -import io.element.android.features.roomdetails.aMatrixClient import io.element.android.features.roomdetails.aMatrixRoom import io.element.android.features.roomdetails.impl.members.aRoomMember import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsEvents import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsState import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.test.FakeMatrixClient import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -34,8 +34,6 @@ import org.junit.Test @ExperimentalCoroutinesApi class RoomMemberDetailsPresenterTests { - private val matrixClient = aMatrixClient() - @Test fun `present - returns the room member's data, then updates it if needed`() = runTest { val roomMember = aRoomMember(displayName = "Alice") @@ -44,7 +42,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.success("A custom avatar")) givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) } - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) + val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -53,7 +51,7 @@ class RoomMemberDetailsPresenterTests { Truth.assertThat(initialState.userName).isEqualTo(roomMember.displayName) Truth.assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) Truth.assertThat(initialState.isBlocked).isEqualTo(roomMember.isIgnored) - + skipItems(1) val loadedState = awaitItem() Truth.assertThat(loadedState.userName).isEqualTo("A custom name") Truth.assertThat(loadedState.avatarUrl).isEqualTo("A custom avatar") @@ -68,7 +66,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.failure(Throwable())) givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) } - val presenter = RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) + val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -88,7 +86,7 @@ class RoomMemberDetailsPresenterTests { givenUserAvatarUrlResult(Result.success(null)) givenRoomMembersState(MatrixRoomMembersState.Ready(listOf(roomMember))) } - val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) + val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -104,7 +102,7 @@ class RoomMemberDetailsPresenterTests { fun `present - BlockUser needing confirmation displays confirmation dialog`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) + val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -125,7 +123,7 @@ class RoomMemberDetailsPresenterTests { fun `present - BlockUser and UnblockUser without confirmation change the 'blocked' state`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) + val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -142,7 +140,7 @@ class RoomMemberDetailsPresenterTests { fun `present - UnblockUser needing confirmation displays confirmation dialog`() = runTest { val room = aMatrixRoom() val roomMember = aRoomMember() - val presenter =RoomMemberDetailsPresenter(matrixClient, room, roomMember.userId) + val presenter = RoomMemberDetailsPresenter(FakeMatrixClient(), room, roomMember.userId) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultInviteStateDataSourceTest.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultInviteStateDataSourceTest.kt index 040a5b5b50..389aee1e92 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultInviteStateDataSourceTest.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/DefaultInviteStateDataSourceTest.kt @@ -69,7 +69,7 @@ internal class DefaultInviteStateDataSourceTest { val client = FakeMatrixClient(invitesDataSource = matrixDataSource) val seenStore = FakeSeenInvitesStore() seenStore.publishRoomIds(setOf(A_ROOM_ID)) - val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers()) + val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true)) moleculeFlow(RecompositionClock.Immediate) { dataSource.inviteState() @@ -86,7 +86,7 @@ internal class DefaultInviteStateDataSourceTest { val client = FakeMatrixClient(invitesDataSource = matrixDataSource) val seenStore = FakeSeenInvitesStore() seenStore.publishRoomIds(setOf(A_ROOM_ID)) - val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers()) + val dataSource = DefaultInviteStateDataSource(client, seenStore, testCoroutineDispatchers(useUnconfinedTestDispatcher = true)) moleculeFlow(RecompositionClock.Immediate) { dataSource.inviteState() diff --git a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt index fe76d9837b..37623b4f28 100644 --- a/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt +++ b/features/roomlist/impl/src/test/kotlin/io/element/android/features/roomlist/impl/RoomListPresenterTests.kt @@ -35,7 +35,6 @@ import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_ROOM_NAME -import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.libraries.matrix.test.A_USER_NAME import io.element.android.libraries.matrix.test.FakeMatrixClient @@ -51,7 +50,7 @@ class RoomListPresenterTests { @Test fun `present - should start with no user and then load user with success`() = runTest { val presenter = RoomListPresenter( - FakeMatrixClient(A_SESSION_ID), + FakeMatrixClient(), createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), @@ -77,7 +76,6 @@ class RoomListPresenterTests { fun `present - should start with no user and then load user with error`() = runTest { val presenter = RoomListPresenter( FakeMatrixClient( - A_SESSION_ID, userDisplayName = Result.failure(AN_EXCEPTION), userAvatarURLString = Result.failure(AN_EXCEPTION), ), @@ -102,7 +100,7 @@ class RoomListPresenterTests { @Test fun `present - should filter room with success`() = runTest { val presenter = RoomListPresenter( - FakeMatrixClient(A_SESSION_ID), + FakeMatrixClient(), createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), @@ -130,7 +128,6 @@ class RoomListPresenterTests { val roomSummaryDataSource = FakeRoomSummaryDataSource() val presenter = RoomListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource ), createDateFormatter(), @@ -163,7 +160,6 @@ class RoomListPresenterTests { val roomSummaryDataSource = FakeRoomSummaryDataSource() val presenter = RoomListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource ), createDateFormatter(), @@ -202,7 +198,6 @@ class RoomListPresenterTests { val roomSummaryDataSource = FakeRoomSummaryDataSource() val presenter = RoomListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource ), createDateFormatter(), @@ -251,7 +246,6 @@ class RoomListPresenterTests { val roomSummaryDataSource = FakeRoomSummaryDataSource() val presenter = RoomListPresenter( FakeMatrixClient( - sessionId = A_SESSION_ID, roomSummaryDataSource = roomSummaryDataSource ), createDateFormatter(), @@ -280,9 +274,7 @@ class RoomListPresenterTests { fun `present - sets invite state`() = runTest { val inviteStateFlow = MutableStateFlow(InvitesState.NoInvites) val presenter = RoomListPresenter( - FakeMatrixClient( - sessionId = A_SESSION_ID, - ), + FakeMatrixClient(), createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), @@ -312,7 +304,7 @@ class RoomListPresenterTests { @Test fun `present - show context menu`() = runTest { val presenter = RoomListPresenter( - FakeMatrixClient(A_SESSION_ID), + FakeMatrixClient(), createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), @@ -339,7 +331,7 @@ class RoomListPresenterTests { @Test fun `present - hide context menu`() = runTest { val presenter = RoomListPresenter( - FakeMatrixClient(A_SESSION_ID), + FakeMatrixClient(), createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), @@ -371,7 +363,7 @@ class RoomListPresenterTests { fun `present - leave room calls into leave room presenter`() = runTest { val leaveRoomPresenter = LeaveRoomPresenterFake() val presenter = RoomListPresenter( - FakeMatrixClient(A_SESSION_ID), + FakeMatrixClient(), createDateFormatter(), FakeRoomLastMessageFormatter(), FakeSessionVerificationService(), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index aaaac28ba7..85c3555844 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.matrix.test -import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 7e54d8e851..5b5ecc4510 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -54,8 +54,6 @@ const val AN_AVATAR_URL = "mxc://data" const val A_FAILURE_REASON = "There has been a failure" -const val FAKE_DELAY_IN_MS = 100L - val A_THROWABLE = Throwable(A_FAILURE_REASON) val AN_EXCEPTION = Exception(A_FAILURE_REASON) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt index 2b34a158a4..816bfc572a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/auth/FakeAuthenticationService.kt @@ -22,8 +22,7 @@ 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 io.element.android.libraries.matrix.test.A_USER_ID -import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS -import kotlinx.coroutines.delay +import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -58,27 +57,24 @@ class FakeAuthenticationService : MatrixAuthenticationService { this.homeserver.value = homeserver } - override suspend fun setHomeserver(homeserver: String): Result { - delay(FAKE_DELAY_IN_MS) - return changeServerError?.let { Result.failure(it) } ?: Result.success(Unit) + override suspend fun setHomeserver(homeserver: String): Result = simulateLongTask { + changeServerError?.let { Result.failure(it) } ?: Result.success(Unit) } - override suspend fun login(username: String, password: String): Result { - delay(FAKE_DELAY_IN_MS) - return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID) + override suspend fun login(username: String, password: String): Result = simulateLongTask { + loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID) } - override suspend fun getOidcUrl(): Result { - return oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA) + override suspend fun getOidcUrl(): Result = simulateLongTask { + oidcError?.let { Result.failure(it) } ?: Result.success(A_OIDC_DATA) } override suspend fun cancelOidcLogin(): Result { return oidcCancelError?.let { Result.failure(it) } ?: Result.success(Unit) } - override suspend fun loginWithOidc(callbackUrl: String): Result { - delay(FAKE_DELAY_IN_MS) - return loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID) + override suspend fun loginWithOidc(callbackUrl: String): Result = simulateLongTask { + loginError?.let { Result.failure(it) } ?: Result.success(A_USER_ID) } fun givenOidcError(throwable: Throwable?) { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt index f351ace464..9ef0413a3a 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/media/FakeMediaLoader.kt @@ -19,34 +19,30 @@ package io.element.android.libraries.matrix.test.media import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.media.MediaFile import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS -import kotlinx.coroutines.delay +import io.element.android.tests.testutils.simulateLongTask class FakeMediaLoader : MatrixMediaLoader { var shouldFail = false - override suspend fun loadMediaContent(source: MediaSource): Result { - delay(FAKE_DELAY_IN_MS) - return if (shouldFail) { + override suspend fun loadMediaContent(source: MediaSource): Result = simulateLongTask { + if (shouldFail) { Result.failure(RuntimeException()) } else { Result.success(ByteArray(0)) } } - override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result { - delay(FAKE_DELAY_IN_MS) - return if (shouldFail) { + override suspend fun loadMediaThumbnail(source: MediaSource, width: Long, height: Long): Result = simulateLongTask { + if (shouldFail) { Result.failure(RuntimeException()) } else { Result.success(ByteArray(0)) } } - override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result { - delay(FAKE_DELAY_IN_MS) - return if (shouldFail) { + override suspend fun downloadMediaFile(source: MediaSource, mimeType: String?, body: String?): Result = simulateLongTask { + if (shouldFail) { Result.failure(RuntimeException()) } else { Result.success(FakeMediaFile("")) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 1199d94ac4..fd9f9e3a4c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -30,9 +30,8 @@ import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.matrix.test.A_SESSION_ID -import io.element.android.libraries.matrix.test.FAKE_DELAY_IN_MS import io.element.android.libraries.matrix.test.timeline.FakeMatrixTimeline -import kotlinx.coroutines.delay +import io.element.android.tests.testutils.simulateLongTask import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow @@ -103,8 +102,8 @@ class FakeMatrixRoom( override val membersStateFlow: MutableStateFlow = MutableStateFlow(MatrixRoomMembersState.Unknown) - override suspend fun updateMembers(): Result { - return updateMembersResult + override suspend fun updateMembers(): Result = simulateLongTask { + updateMembersResult } override fun syncUpdateFlow(): Flow { @@ -115,17 +114,16 @@ class FakeMatrixRoom( return matrixTimeline } - override suspend fun userDisplayName(userId: UserId): Result { - return userDisplayNameResult + override suspend fun userDisplayName(userId: UserId): Result = simulateLongTask { + userDisplayNameResult } - override suspend fun userAvatarUrl(userId: UserId): Result { - return userAvatarUrlResult + override suspend fun userAvatarUrl(userId: UserId): Result = simulateLongTask { + userAvatarUrlResult } - override suspend fun sendMessage(message: String): Result { - delay(FAKE_DELAY_IN_MS) - return Result.success(Unit) + override suspend fun sendMessage(message: String): Result = simulateLongTask { + Result.success(Unit) } override suspend fun sendReaction(emoji: String, eventId: EventId): Result { @@ -138,7 +136,6 @@ class FakeMatrixRoom( override suspend fun editMessage(originalEventId: EventId, message: String): Result { editMessageParameter = message - delay(FAKE_DELAY_IN_MS) return Result.success(Unit) } @@ -147,7 +144,6 @@ class FakeMatrixRoom( override suspend fun replyMessage(eventId: EventId, message: String): Result { replyMessageParameter = message - delay(FAKE_DELAY_IN_MS) return Result.success(Unit) } @@ -156,11 +152,11 @@ class FakeMatrixRoom( override suspend fun redactEvent(eventId: EventId, reason: String?): Result { redactEventEventIdParam = eventId - delay(FAKE_DELAY_IN_MS) return Result.success(Unit) } override suspend fun leave(): Result = leaveRoomError?.let { Result.failure(it) } ?: Result.success(Unit) + override suspend fun acceptInvitation(): Result { isInviteAccepted = true return acceptInviteResult @@ -171,9 +167,9 @@ class FakeMatrixRoom( return rejectInviteResult } - override suspend fun inviteUserById(id: UserId): Result { + override suspend fun inviteUserById(id: UserId): Result = simulateLongTask { invitedUserId = id - return inviteUserResult + inviteUserResult } override suspend fun canInvite(): Result { @@ -192,31 +188,30 @@ class FakeMatrixRoom( override suspend fun sendFile(file: File, fileInfo: FileInfo): Result = fakeSendMedia() - private suspend fun fakeSendMedia(): Result { - delay(FAKE_DELAY_IN_MS) - return sendMediaResult.onSuccess { + private suspend fun fakeSendMedia(): Result = simulateLongTask { + sendMediaResult.onSuccess { sendMediaCount++ } } - override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result { + override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = simulateLongTask { newAvatarData = data - return updateAvatarResult + updateAvatarResult } - override suspend fun removeAvatar(): Result { + override suspend fun removeAvatar(): Result = simulateLongTask { removedAvatar = true - return removeAvatarResult + removeAvatarResult } - override suspend fun setName(name: String): Result { + override suspend fun setName(name: String): Result = simulateLongTask { newName = name - return setNameResult + setNameResult } - override suspend fun setTopic(topic: String): Result { + override suspend fun setTopic(topic: String): Result = simulateLongTask { newTopic = topic - return setTopicResult + setTopicResult } override fun close() = Unit diff --git a/libraries/mediaupload/test/build.gradle.kts b/libraries/mediaupload/test/build.gradle.kts index 535ba51ac4..956afffbe0 100644 --- a/libraries/mediaupload/test/build.gradle.kts +++ b/libraries/mediaupload/test/build.gradle.kts @@ -24,4 +24,5 @@ android { dependencies { api(projects.libraries.mediaupload.api) + implementation(projects.tests.testutils) } diff --git a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt index c4ab8d57b1..0cc7803578 100644 --- a/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt +++ b/libraries/mediaupload/test/src/main/kotlin/io/element/android/libraries/mediaupload/test/FakeMediaPreProcessor.kt @@ -20,6 +20,7 @@ import android.net.Uri import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaUploadInfo +import io.element.android.tests.testutils.simulateLongTask import java.io.File class FakeMediaPreProcessor : MediaPreProcessor { @@ -41,7 +42,9 @@ class FakeMediaPreProcessor : MediaPreProcessor { mimeType: String, deleteOriginal: Boolean, compressIfPossible: Boolean - ): Result = result + ): Result = simulateLongTask { + result + } fun givenResult(value: Result) { this.result = value diff --git a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt index d6cc2e2e31..e4cb389d94 100644 --- a/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt +++ b/samples/minimal/src/main/kotlin/io/element/android/samples/minimal/RoomListScreen.kt @@ -71,7 +71,7 @@ class RoomListScreen( networkMonitor = NetworkMonitorImpl(context), snackbarDispatcher = SnackbarDispatcher(), inviteStateDataSource = DefaultInviteStateDataSource(matrixClient, DefaultSeenInvitesStore(context), coroutineDispatchers), - leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver() ,coroutineDispatchers) + leaveRoomPresenter = LeaveRoomPresenterImpl(matrixClient, RoomMembershipObserver(), coroutineDispatchers) ) @Composable diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/LongTask.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/LongTask.kt new file mode 100644 index 0000000000..154877efe9 --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/LongTask.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.tests.testutils + +import kotlinx.coroutines.delay + +/** + * Workaround for https://github.com/cashapp/molecule/issues/249. + * This functions should be removed/deprecated right after we find a proper fix. + */ +suspend inline fun simulateLongTask(lambda: () -> T): T { + delay(1) + return lambda() +} 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 786d8f8cd2..b8376ddf6f 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 @@ -31,19 +31,18 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher * If false, use [StandardTestDispatcher] for all dispatchers. */ fun TestScope.testCoroutineDispatchers( - useUnconfinedTestDispatcher: Boolean = true, + useUnconfinedTestDispatcher: Boolean = false, ): CoroutineDispatchers = when (useUnconfinedTestDispatcher) { - false -> CoroutineDispatchers( - io = StandardTestDispatcher(testScheduler), - computation = StandardTestDispatcher(testScheduler), - main = StandardTestDispatcher(testScheduler), - diffUpdateDispatcher = StandardTestDispatcher(testScheduler), - ) - true -> CoroutineDispatchers( io = UnconfinedTestDispatcher(testScheduler), computation = UnconfinedTestDispatcher(testScheduler), main = UnconfinedTestDispatcher(testScheduler), diffUpdateDispatcher = UnconfinedTestDispatcher(testScheduler), ) + false -> CoroutineDispatchers( + io = StandardTestDispatcher(testScheduler), + computation = StandardTestDispatcher(testScheduler), + main = StandardTestDispatcher(testScheduler), + diffUpdateDispatcher = StandardTestDispatcher(testScheduler), + ) } From 7ddf93ed09b75e2114e1f9f80118cc1da93e6b87 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 15 Jun 2023 11:27:37 +0200 Subject: [PATCH 078/130] [Message Actions] Retry sending failed messages (#596) * Add `RetrySendMessageMenu` to retry sending failed messages or removing its local echo. * Fix initial event being retrieved, not the updated one --------- Co-authored-by: ElementBot --- changelog.d/487.feature | 1 + .../messages/impl/MessagesPresenter.kt | 4 + .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 5 + .../features/messages/impl/MessagesView.kt | 25 ++- .../impl/timeline/TimelineStateProvider.kt | 2 + .../messages/impl/timeline/TimelineView.kt | 17 +- .../components/TimelineEventTimestampView.kt | 13 +- .../components/TimelineItemEventRow.kt | 13 +- .../retrysendmenu/RetrySendMenuEvents.kt | 26 +++ .../retrysendmenu/RetrySendMenuPresenter.kt | 72 ++++++++ .../retrysendmenu/RetrySendMenuState.kt | 26 +++ .../RetrySendMenuStateProvider.kt | 31 ++++ .../retrysendmenu/RetrySendMessageMenu.kt | 168 ++++++++++++++++++ .../event/TimelineItemEventFactory.kt | 1 + .../impl/timeline/model/TimelineItem.kt | 1 + .../impl/src/main/res/values/localazy.xml | 3 + .../messages/MessagesPresenterTest.kt | 4 + .../RetrySendMenuPresenterTests.kt | 161 +++++++++++++++++ .../libraries/matrix/api/room/MatrixRoom.kt | 4 + .../matrix/api/timeline/MatrixTimelineItem.kt | 1 + .../timeline/item/event/EventTimelineItem.kt | 1 + .../matrix/impl/room/RustMatrixRoom.kt | 15 ++ .../item/event/EventTimelineItemMapper.kt | 1 + .../android/libraries/matrix/test/TestData.kt | 1 + .../matrix/test/room/FakeMatrixRoom.kt | 26 +++ .../matrix/test/room/RoomSummaryFixture.kt | 2 + ...nuPreviewDark_0_null_0,NEXUS_5,1.0,en].png | 3 + ...nuPreviewDark_0_null_1,NEXUS_5,1.0,en].png | 3 + ...uPreviewLight_0_null_0,NEXUS_5,1.0,en].png | 3 + ...uPreviewLight_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 +- ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 +- ...wDarkPreview_0_null_10,NEXUS_5,1.0,en].png | 4 +- ...LightPreview_0_null_10,NEXUS_5,1.0,en].png | 4 +- 41 files changed, 641 insertions(+), 37 deletions(-) create mode 100644 changelog.d/487.feature create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_1,NEXUS_5,1.0,en].png diff --git a/changelog.d/487.feature b/changelog.d/487.feature new file mode 100644 index 0000000000..ee1106eb58 --- /dev/null +++ b/changelog.d/487.feature @@ -0,0 +1 @@ +Add menu to retry sending failed messages or delete their local echoes. diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index de2916a6b4..96384a295c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -32,6 +32,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent @@ -65,6 +66,7 @@ class MessagesPresenter @Inject constructor( private val composerPresenter: MessageComposerPresenter, private val timelinePresenter: TimelinePresenter, private val actionListPresenter: ActionListPresenter, + private val retrySendMenuPresenter: RetrySendMenuPresenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, private val messageSummaryFormatter: MessageSummaryFormatter, @@ -77,6 +79,7 @@ class MessagesPresenter @Inject constructor( val composerState = composerPresenter.present() val timelineState = timelinePresenter.present() val actionListState = actionListPresenter.present() + val retryState = retrySendMenuPresenter.present() val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L) val roomName: MutableState = rememberSaveable { @@ -116,6 +119,7 @@ class MessagesPresenter @Inject constructor( composerState = composerState, timelineState = timelineState, actionListState = actionListState, + retrySendMenuState = retryState, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, snackbarMessage = snackbarMessage, eventSink = ::handleEvents diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 8c876ea49c..7b53421b39 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.timeline.TimelineState +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.SnackbarMessage import io.element.android.libraries.matrix.api.core.RoomId @@ -32,6 +33,7 @@ data class MessagesState( val composerState: MessageComposerState, val timelineState: TimelineState, val actionListState: ActionListState, + val retrySendMenuState: RetrySendMenuState, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, val eventSink: (MessagesEvents) -> Unit diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index e37fd11540..c3cd05d823 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.core.data.StableCharSequence import io.element.android.libraries.designsystem.components.avatar.AvatarData @@ -48,6 +49,10 @@ fun aMessagesState() = MessagesState( timelineState = aTimelineState().copy( timelineItems = aTimelineItemList(aTimelineItemTextContent()), ), + retrySendMenuState = RetrySendMenuState( + selectedEvent = null, + eventSink = {}, + ), actionListState = anActionListState(), hasNetworkConnection = true, snackbarMessage = null, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 542f48e1ae..fb618fbba2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -34,14 +34,10 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetState -import androidx.compose.material3.SheetValue import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -59,7 +55,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.messages.impl.actionlist.ActionListEvents -import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.ActionListView import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.attachments.Attachment @@ -67,6 +62,8 @@ import io.element.android.features.messages.impl.messagecomposer.AttachmentsStat import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.timeline.TimelineView import io.element.android.features.messages.impl.timeline.components.CustomReactionBottomSheet +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.networkmonitor.api.ui.ConnectivityIndicatorView import io.element.android.libraries.androidutils.ui.hideKeyboard @@ -85,6 +82,7 @@ import io.element.android.libraries.designsystem.utils.rememberSnackbarHostState import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo +import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import kotlinx.collections.immutable.ImmutableList import kotlinx.coroutines.launch import timber.log.Timber @@ -176,6 +174,11 @@ fun MessagesView( onMessageClicked = ::onMessageClicked, onMessageLongClicked = ::onMessageLongClicked, onUserDataClicked = onUserDataClicked, + onTimestampClicked = { event -> + if (event.sendState is EventSendState.SendingFailed) { + state.retrySendMenuState.eventSink(RetrySendMenuEvents.EventSelected(event)) + } + } ) }, snackbarHost = { @@ -225,6 +228,10 @@ fun MessagesView( } } ) + + RetrySendMessageMenu( + state = state.retrySendMenuState + ) } @Composable @@ -244,10 +251,11 @@ private fun AttachmentStateView( @Composable fun MessagesViewContent( state: MessagesState, + onMessageClicked: (TimelineItem.Event) -> Unit, + onUserDataClicked: (UserId) -> Unit, + onMessageLongClicked: (TimelineItem.Event) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, - onMessageClicked: (TimelineItem.Event) -> Unit = {}, - onUserDataClicked: (UserId) -> Unit = {}, - onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, ) { Column( modifier = modifier @@ -263,6 +271,7 @@ fun MessagesViewContent( onMessageClicked = onMessageClicked, onMessageLongClicked = onMessageLongClicked, onUserDataClicked = onUserDataClicked, + onTimestampClicked = onTimestampClicked, ) } MessageComposerView( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt index 47e09fc577..c170886672 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineStateProvider.kt @@ -94,6 +94,7 @@ internal fun aTimelineItemList(content: TimelineItemEventContent): ImmutableList internal fun aTimelineItemEvent( eventId: EventId = EventId("\$" + Random.nextInt().toString()), + transactionId: String? = null, isMine: Boolean = false, content: TimelineItemEventContent = aTimelineItemTextContent(), groupPosition: TimelineItemGroupPosition = TimelineItemGroupPosition.None, @@ -104,6 +105,7 @@ internal fun aTimelineItemEvent( return TimelineItem.Event( id = eventId.value, eventId = eventId, + transactionId = transactionId, senderId = UserId("@senderId:domain"), senderAvatar = AvatarData("@senderId:domain", "sender"), content = content, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt index 4c6416ec67..c73cce50f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/TimelineView.kt @@ -68,10 +68,11 @@ import kotlinx.coroutines.launch @Composable fun TimelineView( state: TimelineState, + onUserDataClicked: (UserId) -> Unit, + onMessageClicked: (TimelineItem.Event) -> Unit, + onMessageLongClicked: (TimelineItem.Event) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier, - onUserDataClicked: (UserId) -> Unit = {}, - onMessageClicked: (TimelineItem.Event) -> Unit = {}, - onMessageLongClicked: (TimelineItem.Event) -> Unit = {}, ) { fun onReachedLoadMore() { state.eventSink(TimelineEvents.LoadMore) @@ -102,6 +103,7 @@ fun TimelineView( onLongClick = onMessageLongClicked, onUserDataClick = onUserDataClicked, inReplyToClick = ::inReplyToClicked, + onTimestampClicked = onTimestampClicked, ) if (index == state.timelineItems.lastIndex) { onReachedLoadMore() @@ -125,6 +127,7 @@ fun TimelineItemRow( onClick: (TimelineItem.Event) -> Unit, onLongClick: (TimelineItem.Event) -> Unit, inReplyToClick: (EventId) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier ) { when (timelineItem) { @@ -159,6 +162,7 @@ fun TimelineItemRow( onLongClick = ::onLongClick, onUserDataClick = onUserDataClick, inReplyToClick = inReplyToClick, + onTimestampClicked = onTimestampClicked, modifier = modifier, ) } @@ -191,6 +195,7 @@ fun TimelineItemRow( onLongClick = onLongClick, inReplyToClick = inReplyToClick, onUserDataClick = onUserDataClick, + onTimestampClicked = onTimestampClicked, ) } } @@ -276,6 +281,10 @@ fun TimelineViewDarkPreview( private fun ContentToPreview(content: TimelineItemEventContent) { val timelineItems = aTimelineItemList(content) TimelineView( - state = aTimelineState(timelineItems) + state = aTimelineState(timelineItems), + onMessageClicked = {}, + onTimestampClicked = {}, + onUserDataClicked = {}, + onMessageLongClicked = {}, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt index ed4febf982..530f62a188 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineEventTimestampView.kt @@ -17,12 +17,15 @@ package io.element.android.features.messages.impl.timeline.components import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Error +import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -54,7 +57,15 @@ fun TimelineEventTimestampView( val isMessageEdited = (event.content as? TimelineItemTextBasedContent)?.isEdited.orFalse() val tint = if (hasMessageSendingFailed) ElementTheme.colors.textActionCritical else null Row( - modifier = modifier.clickable(onClick = onClick), + modifier = Modifier + .clickable( + onClick = onClick, + enabled = true, + indication = rememberRipple(bounded = false), + interactionSource = MutableInteractionSource() + ) + .padding(start = 16.dp) // Add extra padding for touch target size + .then(modifier), verticalAlignment = Alignment.CenterVertically, ) { if (isMessageEdited) { diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt index f84a899c6b..4f07f66f5c 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemEventRow.kt @@ -75,6 +75,7 @@ fun TimelineItemEventRow( onLongClick: () -> Unit, onUserDataClick: (UserId) -> Unit, inReplyToClick: (EventId) -> Unit, + onTimestampClicked: (TimelineItem.Event) -> Unit, modifier: Modifier = Modifier ) { val interactionSource = remember { MutableInteractionSource() } @@ -83,7 +84,7 @@ fun TimelineItemEventRow( onUserDataClick(event.senderId) } - fun inReplayToClicked() { + fun inReplyToClicked() { val inReplyToEventId = (event.inReplyTo as? InReplyTo.Ready)?.eventId ?: return inReplyToClick(inReplyToEventId) } @@ -131,7 +132,10 @@ fun TimelineItemEventRow( interactionSource = interactionSource, onMessageClick = onClick, onMessageLongClick = onLongClick, - inReplyToClick = ::inReplayToClicked, + inReplyToClick = ::inReplyToClicked, + onTimestampClicked = { + onTimestampClicked(event) + } ) } TimelineItemReactionsView( @@ -177,6 +181,7 @@ private fun MessageEventBubbleContent( onMessageClick: () -> Unit, onMessageLongClick: () -> Unit, inReplyToClick: () -> Unit, + onTimestampClicked: () -> Unit, modifier: Modifier = Modifier ) { val isMediaItem = event.content is TimelineItemImageContent || event.content is TimelineItemVideoContent @@ -207,7 +212,7 @@ private fun MessageEventBubbleContent( ContentView(modifier = contentModifier) TimelineEventTimestampView( event = event, - onClick = onMessageClick, + onClick = onTimestampClicked, modifier = timestampModifier .padding(horizontal = 4.dp, vertical = 4.dp) // Outer padding .background(LocalColors.current.gray300, RoundedCornerShape(10.0.dp)) @@ -220,7 +225,7 @@ private fun MessageEventBubbleContent( ContentView(modifier = contentModifier.padding(start = 12.dp, end = 12.dp, top = 8.dp)) TimelineEventTimestampView( event = event, - onClick = onMessageClick, + onClick = onTimestampClicked, modifier = timestampModifier .align(Alignment.End) .padding(horizontal = 8.dp, vertical = 2.dp) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.kt new file mode 100644 index 0000000000..ab6e32f078 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuEvents.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.messages.impl.timeline.components.retrysendmenu + +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +sealed interface RetrySendMenuEvents { + data class EventSelected(val event: TimelineItem.Event) : RetrySendMenuEvents + object RetrySend : RetrySendMenuEvents + object RemoveFailed : RetrySendMenuEvents + object Dismiss: RetrySendMenuEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt new file mode 100644 index 0000000000..237dc5683d --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuPresenter.kt @@ -0,0 +1,72 @@ +/* + * 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.messages.impl.timeline.components.retrysendmenu + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import io.element.android.features.messages.impl.timeline.model.TimelineItem +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.coroutines.launch +import javax.inject.Inject + +class RetrySendMenuPresenter @Inject constructor( + private val room: MatrixRoom, +) : Presenter { + + @Composable + override fun present(): RetrySendMenuState { + val coroutineScope = rememberCoroutineScope() + var selectedEvent: TimelineItem.Event? by remember { mutableStateOf(null) } + + fun handleEvent(event: RetrySendMenuEvents) { + when (event) { + is RetrySendMenuEvents.EventSelected -> { + selectedEvent = event.event + } + RetrySendMenuEvents.RetrySend -> { + coroutineScope.launch { + selectedEvent?.transactionId?.let { transactionId -> + room.retrySendMessage(transactionId) + } + selectedEvent = null + } + } + RetrySendMenuEvents.RemoveFailed -> { + coroutineScope.launch { + selectedEvent?.transactionId?.let { transactionId -> + room.cancelSend(transactionId) + } + selectedEvent = null + } + } + RetrySendMenuEvents.Dismiss -> { + selectedEvent = null + } + } + } + + return RetrySendMenuState( + selectedEvent = selectedEvent, + eventSink = ::handleEvent, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.kt new file mode 100644 index 0000000000..e10e9c752c --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuState.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.messages.impl.timeline.components.retrysendmenu + +import androidx.compose.runtime.Immutable +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +@Immutable +data class RetrySendMenuState( + val selectedEvent: TimelineItem.Event?, + val eventSink: (RetrySendMenuEvents) -> Unit, +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt new file mode 100644 index 0000000000..ccb5c26982 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMenuStateProvider.kt @@ -0,0 +1,31 @@ +/* + * 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.messages.impl.timeline.components.retrysendmenu + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.features.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.model.TimelineItem + +class RetrySendMenuStateProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf( + aRetrySendMenuState(event = null), + aRetrySendMenuState(event = aTimelineItemEvent()), + ) +} + +fun aRetrySendMenuState(event: TimelineItem.Event? = aTimelineItemEvent()) = + RetrySendMenuState(selectedEvent = event, eventSink = {}) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt new file mode 100644 index 0000000000..9d8c8283ca --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/retrysendmenu/RetrySendMessageMenu.kt @@ -0,0 +1,168 @@ +/* + * 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.messages.impl.timeline.components.retrysendmenu + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +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.Text +import io.element.android.features.messages.impl.R +import kotlinx.coroutines.launch + +@Composable +internal fun RetrySendMessageMenu( + state: RetrySendMenuState, + modifier: Modifier = Modifier, +) { + val isVisible = state.selectedEvent != null + + fun onDismiss() { + state.eventSink(RetrySendMenuEvents.Dismiss) + } + + fun onRetry() { + state.eventSink(RetrySendMenuEvents.RetrySend) + } + + fun onRemoveFailed() { + state.eventSink(RetrySendMenuEvents.RemoveFailed) + } + + RetrySendMessageMenuBottomSheet( + modifier = modifier, + isVisible = isVisible, + onRetry = ::onRetry, + onRemoveFailed = ::onRemoveFailed, + onDismiss = ::onDismiss + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun RetrySendMessageMenuBottomSheet( + isVisible: Boolean, + onRetry: () -> Unit, + onRemoveFailed: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val sheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + if (isVisible) { + ModalBottomSheet( + modifier = modifier, +// modifier = modifier.navigationBarsPadding() - FIXME after https://issuetracker.google.com/issues/275849044 +// .imePadding() + sheetState = sheetState, + onDismissRequest = { + coroutineScope.launch { + sheetState.hide() + onDismiss() + } + } + ) { + RetrySendMenuContents(onRetry = onRetry, onRemoveFailed = onRemoveFailed) + // FIXME remove after https://issuetracker.google.com/issues/275849044 + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ColumnScope.RetrySendMenuContents( + onRetry: () -> Unit, + onRemoveFailed: () -> Unit, + sheetState: SheetState = rememberModalBottomSheetState(), +) { + val coroutineScope = rememberCoroutineScope() + + ListItem(headlineContent = { + Text(stringResource(R.string.screen_room_retry_send_menu_title), fontWeight = FontWeight.Medium) + }) + ListItem( + headlineContent = { + Text(stringResource(R.string.screen_room_retry_send_menu_send_again_action)) + }, + modifier = Modifier.clickable { + coroutineScope.launch { + sheetState.hide() + onRetry() + } + } + ) + ListItem( + headlineContent = { + Text(stringResource(R.string.screen_room_retry_send_menu_remove_action)) + }, + colors = ListItemDefaults.colors(headlineColor = LocalColors.current.textActionCritical), + modifier = Modifier.clickable { + coroutineScope.launch { + sheetState.hide() + onRemoveFailed() + } + } + ) +} + +@Preview +@Composable +internal fun RetrySendMessageMenuPreviewLight(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) { + ElementPreviewLight { + ContentToPreview(state) + } +} + +@Preview +@Composable +internal fun RetrySendMessageMenuPreviewDark(@PreviewParameter(RetrySendMenuStateProvider::class) state: RetrySendMenuState) { + ElementPreviewDark { + ContentToPreview(state) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ContentToPreview(state: RetrySendMenuState) { + // TODO restore RetrySendMessageMenuBottomSheet once the issue with bottom sheet not being previewable is fixed + Column { + RetrySendMenuContents( + onRetry = {}, + onRemoveFailed = {}, + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt index ce9a558b97..62aca35d6e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemEventFactory.kt @@ -72,6 +72,7 @@ class TimelineItemEventFactory @Inject constructor( return TimelineItem.Event( id = currentTimelineItem.uniqueId, eventId = currentTimelineItem.eventId, + transactionId = currentTimelineItem.transactionId, senderId = currentSender, senderDisplayName = senderDisplayName, senderAvatar = senderAvatarData, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt index 0328bf6603..08f9df0535 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/TimelineItem.kt @@ -52,6 +52,7 @@ sealed interface TimelineItem { data class Event( val id: String, val eventId: EventId? = null, + val transactionId: String? = null, val senderId: UserId, val senderDisplayName: String?, val senderAvatar: AvatarData, diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index d94f32a88f..16672f15ec 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -10,5 +10,8 @@ "Attachment" "Photo & Video Library" "Could not retrieve user details" + "Send again" + "Your message failed to send" "Failed processing media to upload, please try again." + "Remove" \ No newline at end of file diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 1ced503920..4238a56423 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -29,6 +29,7 @@ import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent @@ -322,15 +323,18 @@ class MessagesPresenterTest { flavorShortDescription = "", ) val actionListPresenter = ActionListPresenter(buildMeta = buildMeta) + val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, timelinePresenter = timelinePresenter, actionListPresenter = actionListPresenter, + retrySendMenuPresenter = retrySendMenuPresenter, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), messageSummaryFormatter = FakeMessageSummaryFormatter(), dispatchers = testCoroutineDispatchers(), ) + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.kt new file mode 100644 index 0000000000..1e467b82af --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/retrysendmenu/RetrySendMenuPresenterTests.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.messages.timeline.components.retrysendmenu + +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.messages.impl.timeline.aTimelineItemEvent +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents +import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter +import io.element.android.libraries.matrix.test.A_TRANSACTION_ID +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class RetrySendMenuPresenterTests { + + private val room = FakeMatrixRoom() + private val presenter = RetrySendMenuPresenter(room) + + @Test + fun `present - handle event selected`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent() + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + + assertThat(awaitItem().selectedEvent).isSameInstanceAs(selectedEvent) + } + } + + @Test + fun `present - handle dismiss`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent() + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.Dismiss) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle resend with transactionId`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RetrySend) + assertThat(room.retrySendMessageCount).isEqualTo(1) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle resend without transactionId`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = null) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RetrySend) + assertThat(room.retrySendMessageCount).isEqualTo(0) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle resend with error`() = runTest { + room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error"))) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RetrySend) + assertThat(room.retrySendMessageCount).isEqualTo(1) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle remove failed message with transactionId`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + assertThat(room.cancelSendCount).isEqualTo(1) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle remove failed message without transactionId`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = null) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + assertThat(room.cancelSendCount).isEqualTo(0) + assertThat(awaitItem().selectedEvent).isNull() + } + } + + @Test + fun `present - handle remove failed message with error`() = runTest { + room.givenRetrySendMessageResult(Result.failure(IllegalStateException("An error"))) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val selectedEvent = aTimelineItemEvent(transactionId = A_TRANSACTION_ID) + initialState.eventSink(RetrySendMenuEvents.EventSelected(selectedEvent)) + skipItems(1) + + initialState.eventSink(RetrySendMenuEvents.RemoveFailed) + assertThat(room.cancelSendCount).isEqualTo(1) + assertThat(awaitItem().selectedEvent).isNull() + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 92374b5b00..0ddd75966b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -83,6 +83,10 @@ interface MatrixRoom : Closeable { suspend fun sendReaction(emoji: String, eventId: EventId): Result + suspend fun retrySendMessage(transactionId: String): Result + + suspend fun cancelSend(transactionId: String): Result + suspend fun leave(): Result suspend fun acceptInvitation(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt index 547e593a42..f84f1875e4 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/MatrixTimelineItem.kt @@ -24,6 +24,7 @@ sealed interface MatrixTimelineItem { data class Event(val event: EventTimelineItem) : MatrixTimelineItem { val uniqueId: String = event.uniqueIdentifier val eventId: EventId? = event.eventId + val transactionId: String? = event.transactionId } data class Virtual(val virtual: VirtualTimelineItem) : MatrixTimelineItem diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt index 05f440c413..2a5c068519 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/timeline/item/event/EventTimelineItem.kt @@ -23,6 +23,7 @@ import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugIn data class EventTimelineItem( val uniqueIdentifier: String, val eventId: EventId?, + val transactionId: String?, val isEditable: Boolean, val isLocal: Boolean, val isOwn: Boolean, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 177076ed28..8452ef193b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -264,6 +265,20 @@ class RustMatrixRoom( } } + override suspend fun retrySendMessage(transactionId: String): Result = + withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.retrySend(transactionId) + } + } + + override suspend fun cancelSend(transactionId: String): Result = + withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.cancelSend(transactionId) + } + } + @OptIn(ExperimentalUnsignedTypes::class) override suspend fun updateAvatar(mimeType: String, data: ByteArray): Result = withContext(coroutineDispatchers.io) { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt index a77ddbd80f..bbb9c8fe2a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventTimelineItemMapper.kt @@ -35,6 +35,7 @@ class EventTimelineItemMapper(private val contentMapper: TimelineEventContentMap EventTimelineItem( uniqueIdentifier = it.uniqueIdentifier(), eventId = it.eventId()?.let(::EventId), + transactionId = it.transactionId(), isEditable = it.isEditable(), isLocal = it.isLocal(), isOwn = it.isOwn(), diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index 7e54d8e851..38234d2466 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -37,6 +37,7 @@ val A_ROOM_ID_2 = RoomId("!aRoomId2:domain") val A_THREAD_ID = ThreadId("\$aThreadId") val AN_EVENT_ID = EventId("\$anEventId") val AN_EVENT_ID_2 = EventId("\$anEventId2") +const val A_TRANSACTION_ID = "aTransactionId" const val A_UNIQUE_ID = "aUniqueId" const val A_ROOM_NAME = "A room name" diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 1199d94ac4..9aa244f503 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -71,6 +71,8 @@ class FakeMatrixRoom( private var updateAvatarResult = Result.success(Unit) private var removeAvatarResult = Result.success(Unit) private var sendReactionResult = Result.success(Unit) + private var retrySendMessageResult = Result.success(Unit) + private var cancelSendResult = Result.success(Unit) var sendMediaCount = 0 private set @@ -78,6 +80,12 @@ class FakeMatrixRoom( var sendReactionCount = 0 private set + var retrySendMessageCount: Int = 0 + private set + + var cancelSendCount: Int = 0 + private set + var isInviteAccepted: Boolean = false private set @@ -133,6 +141,16 @@ class FakeMatrixRoom( return sendReactionResult } + override suspend fun retrySendMessage(transactionId: String): Result { + retrySendMessageCount++ + return retrySendMessageResult + } + + override suspend fun cancelSend(transactionId: String): Result { + cancelSendCount++ + return cancelSendResult + } + var editMessageParameter: String? = null private set @@ -292,4 +310,12 @@ class FakeMatrixRoom( fun givenSendReactionResult(result: Result) { sendReactionResult = result } + + fun givenRetrySendMessageResult(result: Result) { + retrySendMessageResult = result + } + + fun givenCancelSendResult(result: Result) { + cancelSendResult = result + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt index e6ac93a3ab..e8e3ff38b9 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomSummaryFixture.kt @@ -89,6 +89,7 @@ fun aRoomMessage( fun anEventTimelineItem( uniqueIdentifier: String = A_UNIQUE_ID, eventId: EventId = AN_EVENT_ID, + transactionId: String? = null, isEditable: Boolean = false, isLocal: Boolean = false, isOwn: Boolean = false, @@ -103,6 +104,7 @@ fun anEventTimelineItem( ) = EventTimelineItem( uniqueIdentifier = uniqueIdentifier, eventId = eventId, + transactionId = transactionId, isEditable = isEditable, isLocal = isLocal, isOwn = isOwn, diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_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.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..309f151dcc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:307bca06295570124970ed808f4a068f50086212623fa0e69630f52cf9e8752a +size 15440 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_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.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..309f151dcc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewDark_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:307bca06295570124970ed808f4a068f50086212623fa0e69630f52cf9e8752a +size 15440 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_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.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af6c929a30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e47e880d5bf8ffedd0662ec0c791483a2cb76e921d12958afffa959b1370c270 +size 14073 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_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.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..af6c929a30 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.retrysendmenu_null_DefaultGroup_RetrySendMessageMenuPreviewLight_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e47e880d5bf8ffedd0662ec0c791483a2cb76e921d12958afffa959b1370c270 +size 14073 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index b244bdb73f..6434e7e2f4 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bdf7fe891aa40f4d626733deb130481d64b3315531712128d1b4073e5ccedf19 -size 5394 +oid sha256:0f56dcf7dfb9ca58618e891aae28099a82457b889da3cd8ec613aeb3757e9750 +size 5403 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index 25c4fffbab..573a379b18 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0baf33f0ec99cd8e4b201d72b38208a02b0511da544276a38d17ff5653e2b754 -size 5906 +oid sha256:b0c0973dd677105248cd17351ac32e9cf79dc6082eb3a9e26a7ff37e2475ca2f +size 5860 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 5aa7211ab3..f4a4fb3c17 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8d9dc01ec07f9e43032e3ec7610eda5588145f3003cc3d329ddf4325e19f239 -size 6674 +oid sha256:2fd42d7ae900d5ece9499b1877766d82d8d174d4b8b04633c93e1de04d3aa069 +size 6715 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 2ed9e07432..718fa7e4ad 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e0f86449d651c50966fe594a46e3a59ae9a5505013e3e36f2458be35ba1844e -size 7174 +oid sha256:e324ac2b0578de021d27cddbba851d5835086f80066cb53c1c1829241ac63fec +size 7172 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index f640d9af42..8af55ef3ae 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:621facc4799c0069fe83b4be9a5085ecafb042de1cdfbbf4ccf0c15548373aa9 -size 5332 +oid sha256:fc5e3c5e68dcab10bd67878cd28d1cf769b19cb4daa3dfc02a310b9e44e38d59 +size 5362 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 8ed48c31f6..c7059cab27 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:986f40750d69639e771c9bf1aac30c49dbf122aa60dc0280164a8868671b638a -size 5910 +oid sha256:8ec5e0cbd36f80526f2a55637d35c41eec7cd8dd07562bc9ed115b2886963b39 +size 5888 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 9735300d61..159f0b046a 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6d4fb8d68603309963d434f3166821055d818836b1ebfb4fc4c637aba992277d -size 6617 +oid sha256:961c557c4d7f77a6d1deb9bbb908cdd855f5e3ebee91e211c91afe857ba3785d +size 6593 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index d265165330..e0496389bb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_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.messages.impl.timeline.components_null_DefaultGroup_TimelineEventTimestampViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1fa641ca8d5c796c858b54ad997052aabf817bc86e254d38d4b33038bc21eedf -size 7285 +oid sha256:833ccceceff1f0a74db95058f3714da514525d05cd3e040619fb221e32ba8d33 +size 7287 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,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.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png index df3b482afd..9c924da5c9 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,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.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:052a95ccf790c0ef93bbfd24ff063fd5489b2c250a9c53ab6d58438653fbbea5 -size 46098 +oid sha256:259022501a602e5e6e22a800f866d221500f34f7ce475df9996b897f93e7fc49 +size 45929 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,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.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png index 9535f67822..09f6f440f9 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,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.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_10,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:744f2192c85b1d6a56cc2eb7f6c873eb45827d2ad2d36fd1678c4ace4118af2c -size 45945 +oid sha256:4fe9d2349725bb5c6dbfcfd29dcf8c7c10079deefdc8a0cb367dfe23cb453b85 +size 45873 From 022e4645b42ee6430c709b83c5fb6956d45d79fd Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jun 2023 12:00:40 +0200 Subject: [PATCH 079/130] Media: introduce a Kind.File so we don't use In-memory bytearray in timeline --- .../impl/media/viewer/MediaViewerView.kt | 6 +- .../components/event/TimelineItemImageView.kt | 2 +- .../components/event/TimelineItemVideoView.kt | 2 +- .../matrix/ui/media/CoilMediaFetcher.kt | 97 ++++++++++++++----- .../matrix/ui/media/MediaRequestData.kt | 15 ++- 5 files changed, 94 insertions(+), 28 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt index afbb9bb331..9cc1ed69fd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/media/viewer/MediaViewerView.kt @@ -54,9 +54,11 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import io.element.android.features.messages.impl.media.local.LocalMedia import io.element.android.features.messages.impl.media.local.LocalMediaView +import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.local.rememberLocalMediaViewState import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.isLoading +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.components.dialogs.RetryDialog import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -140,6 +142,7 @@ fun MediaViewerView( mediaInfo = state.mediaInfo, ) ThumbnailView( + mediaInfo = state.mediaInfo, thumbnailSource = state.thumbnailSource, showThumbnail = showThumbnail, ) @@ -211,6 +214,7 @@ private fun MediaViewerTopBar( private fun ThumbnailView( thumbnailSource: MediaSource?, showThumbnail: Boolean, + mediaInfo: MediaInfo, ) { AnimatedVisibility( visible = showThumbnail, @@ -223,7 +227,7 @@ private fun ThumbnailView( ) { val mediaRequestData = MediaRequestData( source = thumbnailSource, - kind = MediaRequestData.Kind.Content + kind = MediaRequestData.Kind.File(mediaInfo.name, mediaInfo.mimeType) ) AsyncImage( modifier = Modifier.fillMaxSize(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index 566e899a36..6b176604f4 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -43,7 +43,7 @@ fun TimelineItemImageView( modifier = modifier ) { BlurHashAsyncImage( - model = MediaRequestData(content.mediaSource, MediaRequestData.Kind.Content), + model = MediaRequestData(content.mediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)), blurHash = content.blurhash, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index aa024e1033..f3bd4129d1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -49,7 +49,7 @@ fun TimelineItemVideoView( contentAlignment = Alignment.Center, ) { BlurHashAsyncImage( - model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.Content), + model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.File(content.body, content.mimeType)), blurHash = content.blurHash, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt index d638db2902..8b421c6c28 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/CoilMediaFetcher.kt @@ -17,42 +17,93 @@ package io.element.android.libraries.matrix.ui.media import coil.ImageLoader +import coil.decode.DataSource +import coil.decode.ImageSource import coil.fetch.FetchResult import coil.fetch.Fetcher +import coil.fetch.SourceResult import coil.request.Options import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.media.MatrixMediaLoader +import io.element.android.libraries.matrix.api.media.MediaSource +import io.element.android.libraries.matrix.api.media.toFile +import okio.Buffer +import okio.Path.Companion.toOkioPath +import timber.log.Timber import java.nio.ByteBuffer internal class CoilMediaFetcher( private val mediaLoader: MatrixMediaLoader, private val mediaData: MediaRequestData?, - private val options: Options, - private val imageLoader: ImageLoader + private val options: Options ) : Fetcher { override suspend fun fetch(): FetchResult? { - return loadMedia() - .map { data -> - val byteBuffer = ByteBuffer.wrap(data) - imageLoader.components.newFetcher(byteBuffer, options, imageLoader)?.first?.fetch() - }.getOrThrow() - } - - private suspend fun loadMedia(): Result { - if (mediaData?.source == null) return Result.failure(IllegalStateException("No media data to fetch.")) + if (mediaData?.source == null) return null return when (mediaData.kind) { - is MediaRequestData.Kind.Content -> mediaLoader.loadMediaContent(source = mediaData.source) - is MediaRequestData.Kind.Thumbnail -> mediaLoader.loadMediaThumbnail( - source = mediaData.source, - width = mediaData.kind.width, - height = mediaData.kind.height - ) + is MediaRequestData.Kind.Content -> fetchContent(mediaData.source, options) + is MediaRequestData.Kind.Thumbnail -> fetchThumbnail(mediaData.source, mediaData.kind, options) + is MediaRequestData.Kind.File -> fetchFile(mediaData.source, mediaData.kind) } } - class MediaRequestDataFactory(private val client: MatrixClient) : + /** + * This method is here to avoid using [MatrixMediaLoader.loadMediaContent] as too many ByteArray allocations will flood the memory and cause lots of GC. + * The MediaFile will be closed (and so destroyed from disk) when the image source is closed. + * + */ + private suspend fun fetchFile(mediaSource: MediaSource, kind: MediaRequestData.Kind.File): FetchResult? { + return mediaLoader.downloadMediaFile(mediaSource, kind.mimeType, kind.body) + .map { mediaFile -> + val file = mediaFile.toFile() + SourceResult( + source = ImageSource(file = file.toOkioPath(), closeable = mediaFile), + mimeType = null, + dataSource = DataSource.DISK + ) + } + .onFailure { + Timber.e(it) + } + .getOrNull() + } + + private suspend fun fetchContent(mediaSource: MediaSource, options: Options): FetchResult? { + return mediaLoader.loadMediaContent( + source = mediaSource, + ).map { byteArray -> + byteArray.asSourceResult(options) + }.getOrNull() + } + + private suspend fun fetchThumbnail(mediaSource: MediaSource, kind: MediaRequestData.Kind.Thumbnail, options: Options): FetchResult? { + return mediaLoader.loadMediaThumbnail( + source = mediaSource, + width = kind.width, + height = kind.height + ).map { byteArray -> + byteArray.asSourceResult(options) + }.getOrNull() + } + + private fun ByteArray.asSourceResult(options: Options): SourceResult { + val byteBuffer = ByteBuffer.wrap(this) + val bufferedSource = try { + Buffer().apply { write(byteBuffer) } + } finally { + byteBuffer.position(0) + } + return SourceResult( + source = ImageSource(bufferedSource, options.context), + mimeType = null, + dataSource = DataSource.MEMORY + ) + } + + class MediaRequestDataFactory( + private val client: MatrixClient + ) : Fetcher.Factory { override fun create( data: MediaRequestData, @@ -62,13 +113,14 @@ internal class CoilMediaFetcher( return CoilMediaFetcher( mediaLoader = client.mediaLoader, mediaData = data, - options = options, - imageLoader = imageLoader + options = options ) } } - class AvatarFactory(private val client: MatrixClient) : + class AvatarFactory( + private val client: MatrixClient + ) : Fetcher.Factory { override fun create( @@ -79,8 +131,7 @@ internal class CoilMediaFetcher( return CoilMediaFetcher( mediaLoader = client.mediaLoader, mediaData = data.toMediaRequestData(), - options = options, - imageLoader = imageLoader + options = options ) } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt index 02a7ed4e8c..f2593766bc 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/media/MediaRequestData.kt @@ -18,17 +18,28 @@ package io.element.android.libraries.matrix.ui.media import io.element.android.libraries.matrix.api.media.MediaSource +/** + * Can be use with [coil.compose.AsyncImage] to load a [MediaSource]. + * This will go internally through our [CoilMediaFetcher]. + * + * Example of usage: + * AsyncImage( + * model = MediaRequestData(mediaSource, MediaRequestData.Kind.Content), + * contentScale = ContentScale.Fit, + * ) + * + */ data class MediaRequestData( val source: MediaSource?, val kind: Kind ) { sealed interface Kind { + object Content : Kind + data class File(val body: String?, val mimeType: String) : Kind data class Thumbnail(val width: Long, val height: Long) : Kind { constructor(size: Long) : this(size, size) } - - object Content : Kind } } From 8207098091bdbd664ddace57e2ffca9eafcdaebd Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jun 2023 12:11:37 +0200 Subject: [PATCH 080/130] Timeline: use thumbnailSource for image if available (except for gif) --- .../android/features/messages/impl/MessagesFlowNode.kt | 2 +- .../timeline/components/event/TimelineItemImageView.kt | 2 +- .../factories/event/TimelineItemContentMessageFactory.kt | 1 + .../impl/timeline/model/event/TimelineItemImageContent.kt | 8 ++++++++ .../model/event/TimelineItemImageContentProvider.kt | 1 + .../android/features/messages/MessagesPresenterTest.kt | 1 + 6 files changed, 13 insertions(+), 2 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index ec4d2f31d3..ab8a053716 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -138,7 +138,7 @@ class MessagesFlowNode @AssistedInject constructor( fileExtension = event.content.fileExtension ), mediaSource = event.content.mediaSource, - thumbnailSource = event.content.mediaSource, + thumbnailSource = event.content.thumbnailSource, ) backstack.push(navTarget) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index 6b176604f4..f85f9d58e0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -43,7 +43,7 @@ fun TimelineItemImageView( modifier = modifier ) { BlurHashAsyncImage( - model = MediaRequestData(content.mediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)), + model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)), blurHash = content.blurhash, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index f1a29ebcf9..0e407003e8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -54,6 +54,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemImageContent( body = messageType.body, mediaSource = messageType.source, + thumbnailSource = messageType.info?.thumbnailSource, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, blurhash = messageType.info?.blurhash, width = messageType.info?.width?.toInt(), diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index a5ef890c82..286d3412f1 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -16,11 +16,13 @@ package io.element.android.features.messages.impl.timeline.model.event +import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.matrix.api.media.MediaSource data class TimelineItemImageContent( val body: String, val mediaSource: MediaSource, + val thumbnailSource: MediaSource?, val formattedFileSize: String, val fileExtension: String, val mimeType: String, @@ -30,4 +32,10 @@ data class TimelineItemImageContent( val aspectRatio: Float ) : TimelineItemEventContent { override val type: String = "TimelineItemImageContent" + + val preferredMediaSource = if (mimeType == MimeTypes.Gif) { + mediaSource + } else { + thumbnailSource ?: mediaSource + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt index 519f1e58a4..004bac390a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContentProvider.kt @@ -32,6 +32,7 @@ open class TimelineItemImageContentProvider : PreviewParameterProvider Date: Thu, 15 Jun 2023 12:53:11 +0200 Subject: [PATCH 081/130] Inline Async extension functions (#598) Just a tiny perf improvement. --- .../io/element/android/libraries/architecture/Async.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt index 3be961598d..301dcfd936 100644 --- a/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt +++ b/libraries/architecture/src/main/kotlin/io/element/android/libraries/architecture/Async.kt @@ -36,17 +36,20 @@ sealed interface Async { } } -suspend fun (suspend () -> T).execute(state: MutableState>, errorMapping: ((Throwable) -> Throwable)? = null) { +suspend inline fun (suspend () -> T).execute( + state: MutableState>, + errorMapping: ((Throwable) -> Throwable) = { it }, +) { try { state.value = Async.Loading() val result = this() state.value = Async.Success(result) } catch (error: Throwable) { - state.value = Async.Failure(errorMapping?.invoke(error) ?: error) + state.value = Async.Failure(errorMapping.invoke(error)) } } -suspend fun (suspend () -> Result).executeResult(state: MutableState>) { +suspend inline fun (suspend () -> Result).executeResult(state: MutableState>) { if (state.value !is Async.Success) { state.value = Async.Loading() } From f945b1390093e9f99d2223ed22e081053ec5e355 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jun 2023 12:36:51 +0000 Subject: [PATCH 082/130] Update dependency io.sentry:sentry-android to v6.23.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 434d39e743..dffce1488d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -158,7 +158,7 @@ statemachine = "com.freeletics.flowredux:compose:1.1.0" # Analytics posthog = "com.posthog.android:posthog:2.0.3" -sentry_android = "io.sentry:sentry-android:6.22.0" +sentry_android = "io.sentry:sentry-android:6.23.0" matrix_analytics_events = "com.github.matrix-org:matrix-analytics-events:main-SNAPSHOT" # Di From 9bdf53f43d6a1ecaaeeacd8356c38ce008aa231a Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 15 Jun 2023 14:58:18 +0200 Subject: [PATCH 083/130] Move logic of different BottomSheets in `MessagesView` to presenters (#600) * Move bottom sheet logic in `MessagesView` to presenters. * Make the block inside `SheetState.hide` suspend. --- .../features/messages/impl/MessagesEvents.kt | 1 + .../messages/impl/MessagesPresenter.kt | 14 +++- .../features/messages/impl/MessagesState.kt | 2 + .../messages/impl/MessagesStateProvider.kt | 5 ++ .../features/messages/impl/MessagesView.kt | 57 ++-------------- .../impl/actionlist/ActionListView.kt | 32 +++++++-- .../impl/timeline/components/EmojiPicker.kt | 19 ------ .../CustomReactionBottomSheet.kt | 66 +++++++++++++++++++ .../customreaction/CustomReactionEvents.kt | 23 +++++++ .../customreaction/CustomReactionPresenter.kt | 42 ++++++++++++ .../customreaction/CustomReactionState.kt | 24 +++++++ .../messages/MessagesPresenterTest.kt | 35 ++++++++-- .../CustomReactionPresenterTests.kt | 48 ++++++++++++++ .../theme/components/ModalBottomSheet.kt | 10 +++ 14 files changed, 294 insertions(+), 84 deletions(-) create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt index 4dbf6d42d9..2d8af99fcd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt @@ -23,4 +23,5 @@ import io.element.android.libraries.matrix.api.core.EventId sealed interface MessagesEvents { data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents data class SendReaction(val emoji: String, val eventId: EventId) : MessagesEvents + object Dismiss : MessagesEvents } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 96384a295c..089f0e79cb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -25,6 +25,7 @@ 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.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents @@ -32,6 +33,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.timeline.TimelineEvents import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemEncryptedContent @@ -66,6 +68,7 @@ class MessagesPresenter @Inject constructor( private val composerPresenter: MessageComposerPresenter, private val timelinePresenter: TimelinePresenter, private val actionListPresenter: ActionListPresenter, + private val customReactionPresenter: CustomReactionPresenter, private val retrySendMenuPresenter: RetrySendMenuPresenter, private val networkMonitor: NetworkMonitor, private val snackbarDispatcher: SnackbarDispatcher, @@ -79,6 +82,7 @@ class MessagesPresenter @Inject constructor( val composerState = composerPresenter.present() val timelineState = timelinePresenter.present() val actionListState = actionListPresenter.present() + val customReactionState = customReactionPresenter.present() val retryState = retrySendMenuPresenter.present() val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L) @@ -108,8 +112,13 @@ class MessagesPresenter @Inject constructor( } fun handleEvents(event: MessagesEvents) { when (event) { - is MessagesEvents.HandleAction -> localCoroutineScope.handleTimelineAction(event.action, event.event, composerState) - is MessagesEvents.SendReaction -> localCoroutineScope.sendReaction(event.emoji, event.eventId) + is MessagesEvents.HandleAction -> { + localCoroutineScope.handleTimelineAction(event.action, event.event, composerState) + } + is MessagesEvents.SendReaction -> { + localCoroutineScope.sendReaction(event.emoji, event.eventId) + } + is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear) } } return MessagesState( @@ -119,6 +128,7 @@ class MessagesPresenter @Inject constructor( composerState = composerState, timelineState = timelineState, actionListState = actionListState, + customReactionState = customReactionState, retrySendMenuState = retryState, hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online, snackbarMessage = snackbarMessage, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 7b53421b39..6e29eefe08 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -20,6 +20,7 @@ import androidx.compose.runtime.Immutable import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.messagecomposer.MessageComposerState import io.element.android.features.messages.impl.timeline.TimelineState +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.designsystem.utils.SnackbarMessage @@ -33,6 +34,7 @@ data class MessagesState( val composerState: MessageComposerState, val timelineState: TimelineState, val actionListState: ActionListState, + val customReactionState: CustomReactionState, val retrySendMenuState: RetrySendMenuState, val hasNetworkConnection: Boolean, val snackbarMessage: SnackbarMessage?, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index c3cd05d823..4ed43315cb 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -21,6 +21,7 @@ import io.element.android.features.messages.impl.actionlist.anActionListState import io.element.android.features.messages.impl.messagecomposer.aMessageComposerState import io.element.android.features.messages.impl.timeline.aTimelineItemList import io.element.android.features.messages.impl.timeline.aTimelineState +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent import io.element.android.libraries.core.data.StableCharSequence @@ -54,6 +55,10 @@ fun aMessagesState() = MessagesState( eventSink = {}, ), actionListState = anActionListState(), + customReactionState = CustomReactionState( + selectedEventId = null, + eventSink = {}, + ), hasNetworkConnection = true, snackbarMessage = null, eventSink = {} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index fb618fbba2..0687d46787 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -35,15 +35,8 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.SnackbarHost -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -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 androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView @@ -61,7 +54,8 @@ import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.messagecomposer.AttachmentsState import io.element.android.features.messages.impl.messagecomposer.MessageComposerView import io.element.android.features.messages.impl.timeline.TimelineView -import io.element.android.features.messages.impl.timeline.components.CustomReactionBottomSheet +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionBottomSheet +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionEvents import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuEvents import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMessageMenu import io.element.android.features.messages.impl.timeline.model.TimelineItem @@ -84,7 +78,6 @@ import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo import io.element.android.libraries.matrix.api.timeline.item.event.EventSendState import kotlinx.collections.immutable.ImmutableList -import kotlinx.coroutines.launch import timber.log.Timber import io.element.android.libraries.ui.strings.R as StringsR @@ -100,13 +93,7 @@ fun MessagesView( onItemDebugInfoClicked: (EventId, TimelineItemDebugInfo) -> Unit, modifier: Modifier = Modifier, ) { - val coroutineScope = rememberCoroutineScope() - val actionListViewBottomSheetState = rememberModalBottomSheetState() - val customReactionBottomSheetState = rememberModalBottomSheetState() - LogCompositions(tag = "MessagesScreen", msg = "Root") - var isMessageActionsBottomSheetVisible by rememberSaveable { mutableStateOf(false) } - var isCustomReactionBottomSheetVisible by rememberSaveable { mutableStateOf(false) } AttachmentStateView(state.composerState.attachmentsState, onPreviewAttachments) @@ -126,19 +113,11 @@ fun MessagesView( Timber.v("OnMessageLongClicked= ${event.id}") localView.hideKeyboard() state.actionListState.eventSink(ActionListEvents.ComputeForMessage(event)) - isMessageActionsBottomSheetVisible = true - } - - suspend fun onDismissActionListBottomSheet() { - state.actionListState.eventSink(ActionListEvents.Clear) - actionListViewBottomSheetState.hide() - isMessageActionsBottomSheetVisible = false } fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) { - coroutineScope.launch { onDismissActionListBottomSheet() } when (action) { - is TimelineItemAction.Developer -> if (event.eventId != null && event.debugInfo != null) { + is TimelineItemAction.Developer -> if (event.eventId != null) { onItemDebugInfoClicked(event.eventId, event.debugInfo) } else -> state.eventSink(MessagesEvents.HandleAction(action, event)) @@ -147,7 +126,6 @@ fun MessagesView( fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) { if (event.eventId == null) return - coroutineScope.launch { onDismissActionListBottomSheet() } state.eventSink(MessagesEvents.SendReaction(emoji, event.eventId)) } @@ -189,42 +167,21 @@ fun MessagesView( }, ) - var reactingToEventId: EventId? by remember { mutableStateOf(null) } ActionListView( state = state.actionListState, - sheetState = actionListViewBottomSheetState, - isVisible = isMessageActionsBottomSheetVisible, - onDismiss = { coroutineScope.launch { onDismissActionListBottomSheet() } }, onActionSelected = ::onActionSelected, onCustomReactionClicked = { event -> - reactingToEventId = event.eventId - coroutineScope.launch { - onDismissActionListBottomSheet() - isCustomReactionBottomSheetVisible = true - } + state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(event.eventId)) }, onEmojiReactionClicked = ::onEmojiReactionClicked, ) CustomReactionBottomSheet( - isVisible = isCustomReactionBottomSheetVisible, - sheetState = customReactionBottomSheetState, - onDismiss = { - reactingToEventId = null - coroutineScope.launch { - customReactionBottomSheetState.hide() - isCustomReactionBottomSheetVisible = false - } - }, + state = state.customReactionState, onEmojiSelected = { emoji -> - val eventId = reactingToEventId - if (eventId != null) { + state.customReactionState.selectedEventId?.let { eventId -> state.eventSink(MessagesEvents.SendReaction(emoji.unicode, eventId)) - reactingToEventId = null - coroutineScope.launch { - customReactionBottomSheetState.hide() - isCustomReactionBottomSheetVisible = false - } + state.customReactionState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) } } ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt index 27ec05fb51..4a88a23fa2 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/actionlist/ActionListView.kt @@ -42,7 +42,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SheetState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -74,6 +77,7 @@ 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.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.hide import io.element.android.libraries.matrix.ui.components.AttachmentThumbnail import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType @@ -82,37 +86,51 @@ import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType @Composable fun ActionListView( state: ActionListState, - isVisible: Boolean, onActionSelected: (action: TimelineItemAction, TimelineItem.Event) -> Unit, onEmojiReactionClicked: (String, TimelineItem.Event) -> Unit, onCustomReactionClicked: (TimelineItem.Event) -> Unit, - onDismiss: () -> Unit, modifier: Modifier = Modifier, sheetState: SheetState = rememberModalBottomSheetState() ) { + val coroutineScope = rememberCoroutineScope() val targetItem = (state.target as? ActionListState.Target.Success)?.event fun onItemActionClicked( itemAction: TimelineItemAction ) { if (targetItem == null) return - onActionSelected(itemAction, targetItem) + sheetState.hide(coroutineScope) { + state.eventSink(ActionListEvents.Clear) + onActionSelected(itemAction, targetItem) + } } fun onEmojiReactionClicked(emoji: String) { if (targetItem == null) return - onEmojiReactionClicked(emoji, targetItem) + sheetState.hide(coroutineScope) { + state.eventSink(ActionListEvents.Clear) + onEmojiReactionClicked(emoji, targetItem) + } } fun onCustomReactionClicked() { if (targetItem == null) return - onCustomReactionClicked(targetItem) + sheetState.hide(coroutineScope) { + state.eventSink(ActionListEvents.Clear) + onCustomReactionClicked(targetItem) + } } - if (isVisible) { + fun onDismiss() { + sheetState.hide(coroutineScope) { + state.eventSink(ActionListEvents.Clear) + } + } + + if (targetItem != null) { ModalBottomSheet( sheetState = sheetState, - onDismissRequest = onDismiss + onDismissRequest = ::onDismiss, ) { SheetContent( state = state, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt index ca96df7e7d..8c1d5e2f31 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/EmojiPicker.kt @@ -32,8 +32,6 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.ripple.rememberRipple -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.SheetState import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.runtime.Composable @@ -49,26 +47,9 @@ import com.vanniktech.emoji.google.GoogleEmojiProvider import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet import io.element.android.libraries.designsystem.theme.components.Text import kotlinx.coroutines.launch -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun CustomReactionBottomSheet( - isVisible: Boolean, - sheetState: SheetState, - onDismiss: () -> Unit, - onEmojiSelected: (Emoji) -> Unit, - modifier: Modifier = Modifier, -) { - if (isVisible) { - ModalBottomSheet(onDismissRequest = onDismiss, sheetState = sheetState, modifier = modifier) { - EmojiPicker(onEmojiSelected = onEmojiSelected, modifier = Modifier.fillMaxSize()) - } - } -} - @OptIn(ExperimentalFoundationApi::class) @Composable fun EmojiPicker( diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt new file mode 100644 index 0000000000..6ea290cdb8 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionBottomSheet.kt @@ -0,0 +1,66 @@ +/* + * 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.messages.impl.timeline.components.customreaction + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import com.vanniktech.emoji.Emoji +import io.element.android.features.messages.impl.timeline.components.EmojiPicker +import io.element.android.libraries.designsystem.theme.components.ModalBottomSheet +import io.element.android.libraries.designsystem.theme.components.hide + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun CustomReactionBottomSheet( + state: CustomReactionState, + onEmojiSelected: (Emoji) -> Unit, + modifier: Modifier = Modifier, +) { + val sheetState = rememberModalBottomSheetState() + val coroutineScope = rememberCoroutineScope() + + fun onDismiss() { + sheetState.hide(coroutineScope) { + state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) + } + } + + fun onEmojiSelectedDismiss(emoji: Emoji) { + sheetState.hide(coroutineScope) { + state.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) + onEmojiSelected(emoji) + } + } + + val isVisible = state.selectedEventId != null + if (isVisible) { + ModalBottomSheet( + onDismissRequest = ::onDismiss, + sheetState = sheetState, + modifier = modifier + ) { + EmojiPicker( + onEmojiSelected = ::onEmojiSelectedDismiss, + modifier = Modifier.fillMaxSize() + ) + } + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt new file mode 100644 index 0000000000..b7c210553e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionEvents.kt @@ -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.messages.impl.timeline.components.customreaction + +import io.element.android.libraries.matrix.api.core.EventId + +sealed interface CustomReactionEvents { + data class UpdateSelectedEvent(val eventId: EventId?) : CustomReactionEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.kt new file mode 100644 index 0000000000..0a23d42085 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionPresenter.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.messages.impl.timeline.components.customreaction + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.core.EventId +import javax.inject.Inject + +class CustomReactionPresenter @Inject constructor() : Presenter { + + @Composable + override fun present(): CustomReactionState { + var selectedEventId by remember { mutableStateOf(null) } + + fun handleEvents(event: CustomReactionEvents) { + when (event) { + is CustomReactionEvents.UpdateSelectedEvent -> selectedEventId = event.eventId + } + } + + return CustomReactionState(selectedEventId = selectedEventId, eventSink = ::handleEvents) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.kt new file mode 100644 index 0000000000..6c0c7f3599 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/customreaction/CustomReactionState.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.messages.impl.timeline.components.customreaction + +import io.element.android.libraries.matrix.api.core.EventId + +data class CustomReactionState( + val selectedEventId: EventId?, + val eventSink: (CustomReactionEvents) -> Unit, +) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 7d9c8a29de..ec25f2ca62 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -26,9 +26,11 @@ import io.element.android.features.messages.fixtures.aTimelineItemsFactory import io.element.android.features.messages.impl.MessagesEvents import io.element.android.features.messages.impl.MessagesPresenter import io.element.android.features.messages.impl.actionlist.ActionListPresenter +import io.element.android.features.messages.impl.actionlist.ActionListState import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter import io.element.android.features.messages.impl.timeline.TimelinePresenter +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuPresenter import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent @@ -90,6 +92,7 @@ class MessagesPresenterTest { room.givenSendReactionResult(Result.failure(IllegalStateException("Failed to send reaction"))) initialState.eventSink.invoke(MessagesEvents.SendReaction("👍", AN_EVENT_ID)) assertThat(room.sendReactionCount).isEqualTo(2) + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -102,7 +105,7 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) - // Still a TODO in the code + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -115,7 +118,7 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Copy, aMessageEvent())) - // Still a TODO in the code + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -131,6 +134,7 @@ class MessagesPresenterTest { skipItems(1) val finalState = awaitItem() assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -143,7 +147,7 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Reply, aMessageEvent(eventId = null))) - skipItems(1) + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) // Otherwise we would have some extra items here ensureAllEventsConsumed() } @@ -177,6 +181,7 @@ class MessagesPresenterTest { assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) val replyMode = finalState.composerState.mode as MessageComposerMode.Reply assertThat(replyMode.attachmentThumbnailInfo).isNotNull() + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -209,6 +214,7 @@ class MessagesPresenterTest { assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) val replyMode = finalState.composerState.mode as MessageComposerMode.Reply assertThat(replyMode.attachmentThumbnailInfo).isNotNull() + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -236,6 +242,7 @@ class MessagesPresenterTest { assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Reply::class.java) val replyMode = finalState.composerState.mode as MessageComposerMode.Reply assertThat(replyMode.attachmentThumbnailInfo).isNotNull() + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -251,6 +258,7 @@ class MessagesPresenterTest { skipItems(1) val finalState = awaitItem() assertThat(finalState.composerState.mode).isInstanceOf(MessageComposerMode.Edit::class.java) + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -265,6 +273,7 @@ class MessagesPresenterTest { val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Redact, aMessageEvent())) assertThat(matrixRoom.redactEventEventIdParam).isEqualTo(AN_EVENT_ID) + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -277,7 +286,20 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) - // Still a TODO in the code + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + } + } + + @Test + fun `present - handle dismiss action`() = runTest { + val presenter = createMessagePresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + val initialState = awaitItem() + initialState.eventSink.invoke(MessagesEvents.Dismiss) + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -290,7 +312,7 @@ class MessagesPresenterTest { skipItems(1) val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent())) - // Still a TODO in the code + assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) } } @@ -324,18 +346,19 @@ class MessagesPresenterTest { flavorShortDescription = "", ) val actionListPresenter = ActionListPresenter(buildMeta = buildMeta) + val customReactionPresenter = CustomReactionPresenter() val retrySendMenuPresenter = RetrySendMenuPresenter(room = matrixRoom) return MessagesPresenter( room = matrixRoom, composerPresenter = messageComposerPresenter, timelinePresenter = timelinePresenter, actionListPresenter = actionListPresenter, + customReactionPresenter = customReactionPresenter, retrySendMenuPresenter = retrySendMenuPresenter, networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), messageSummaryFormatter = FakeMessageSummaryFormatter(), dispatchers = testCoroutineDispatchers(), ) - } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt new file mode 100644 index 0000000000..237cb81d38 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/timeline/components/customreaction/CustomReactionPresenterTests.kt @@ -0,0 +1,48 @@ +/* + * 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.messages.timeline.components.customreaction + +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.messages.impl.timeline.components.customreaction.CustomReactionEvents +import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class CustomReactionPresenterTests { + + private val presenter = CustomReactionPresenter() + + @Test + fun `present - handle selecting and de-selecting an event`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.selectedEventId).isNull() + + initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(AN_EVENT_ID)) + assertThat(awaitItem().selectedEventId).isEqualTo(AN_EVENT_ID) + + initialState.eventSink(CustomReactionEvents.UpdateSelectedEvent(null)) + assertThat(awaitItem().selectedEventId).isNull() + } + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt index 0c98caaa7d..f7ca7ba1ce 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt @@ -37,6 +37,8 @@ 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.preview.PreviewGroup +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -66,6 +68,14 @@ fun ModalBottomSheet( ) } +@OptIn(ExperimentalMaterial3Api::class) +fun SheetState.hide(coroutineScope: CoroutineScope, then: suspend () -> Unit) { + coroutineScope.launch { + hide() + then() + } +} + // This preview and its screenshots are blank, see: https://issuetracker.google.com/issues/283843380 @Preview(group = PreviewGroup.BottomSheets) @Composable From 170c74113001ecb9a05413762b1e0b5f84b0aced Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jun 2023 15:26:53 +0200 Subject: [PATCH 084/130] Matrix rust sdk: update to 0.1.20 (and fix compilation issues) --- .../event/TimelineItemContentMessageFactory.kt | 4 ++-- gradle/libs.versions.toml | 2 +- .../android/libraries/matrix/api/media/AudioInfo.kt | 4 +++- .../android/libraries/matrix/api/media/VideoInfo.kt | 4 +++- .../android/libraries/matrix/impl/media/AudioInfo.kt | 4 ++-- .../android/libraries/matrix/impl/media/VideoInfo.kt | 4 ++-- .../impl/timeline/item/event/EventMessageMapper.kt | 4 ++-- .../libraries/mediaupload/AndroidMediaPreProcessor.kt | 11 ++++++++--- 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 0e407003e8..d20560d53a 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -54,7 +54,7 @@ class TimelineItemContentMessageFactory @Inject constructor( TimelineItemImageContent( body = messageType.body, mediaSource = messageType.source, - thumbnailSource = messageType.info?.thumbnailSource, + thumbnailSource = messageType.info?.thumbnailSource, mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, blurhash = messageType.info?.blurhash, width = messageType.info?.width?.toInt(), @@ -73,7 +73,7 @@ class TimelineItemContentMessageFactory @Inject constructor( mimeType = messageType.info?.mimetype ?: MimeTypes.OctetStream, width = messageType.info?.width?.toInt(), height = messageType.info?.height?.toInt(), - duration = messageType.info?.duration ?: 0L, + duration = messageType.info?.duration?.toMillis() ?: 0L, blurHash = messageType.info?.blurhash, aspectRatio = aspectRatio, formattedFileSize = fileSizeFormatter.format(messageType.info?.size ?: 0), diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 434d39e743..a225832eda 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -142,7 +142,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.19" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.20" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt index 6ed6e474b6..e9708a6926 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/AudioInfo.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.matrix.api.media +import java.time.Duration + data class AudioInfo( - val duration: Long?, + val duration: Duration?, val size: Long?, val mimeType: String?, ) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt index aa291bd653..b7af54c6b2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/media/VideoInfo.kt @@ -16,8 +16,10 @@ package io.element.android.libraries.matrix.api.media +import java.time.Duration + data class VideoInfo( - val duration: Long?, + val duration: Duration?, val height: Long?, val width: Long?, val mimetype: String?, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt index 7c35c14fb7..2f0d6879a4 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/AudioInfo.kt @@ -20,13 +20,13 @@ import io.element.android.libraries.matrix.api.media.AudioInfo import org.matrix.rustcomponents.sdk.AudioInfo as RustAudioInfo fun RustAudioInfo.map(): AudioInfo = AudioInfo( - duration = duration?.toLong(), + duration = duration, size = size?.toLong(), mimeType = mimetype ) fun AudioInfo.map(): RustAudioInfo = RustAudioInfo( - duration = duration?.toULong(), + duration = duration, size = size?.toULong(), mimetype = mimeType, ) diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt index b474c2ab2e..661d1b9b33 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/media/VideoInfo.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.matrix.api.media.VideoInfo import org.matrix.rustcomponents.sdk.VideoInfo as RustVideoInfo fun RustVideoInfo.map(): VideoInfo = VideoInfo( - duration = duration?.toLong(), + duration = duration, height = height?.toLong(), width = width?.toLong(), mimetype = mimetype, @@ -31,7 +31,7 @@ fun RustVideoInfo.map(): VideoInfo = VideoInfo( ) fun VideoInfo.map(): RustVideoInfo = RustVideoInfo( - duration = duration?.toULong(), + duration = duration, height = height?.toULong(), width = width?.toULong(), mimetype = mimetype, diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 8a052d1a3a..7fdbe06e8a 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -103,7 +103,7 @@ private fun RustFormattedBody.map(): FormattedBody = FormattedBody( private fun RustMessageFormat.map(): MessageFormat { return when (this) { - RustMessageFormat.HTML -> MessageFormat.HTML - RustMessageFormat.UNKNOWN -> MessageFormat.UNKNOWN + RustMessageFormat.Html -> MessageFormat.HTML + is RustMessageFormat.Unknown -> MessageFormat.UNKNOWN } } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 4882000307..0f8ebab21a 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -51,6 +51,7 @@ import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream +import java.time.Duration import javax.inject.Inject import kotlin.time.Duration.Companion.seconds @@ -175,9 +176,8 @@ class AndroidMediaPreProcessor @Inject constructor( val file = copyToTmpFile(uri) return MediaMetadataRetriever().runAndRelease { setDataSource(context, Uri.fromFile(file)) - val info = AudioInfo( - duration = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L, + duration = extractDuration(), size = file.length(), mimeType = mimeType, ) @@ -219,7 +219,7 @@ class AndroidMediaPreProcessor @Inject constructor( MediaMetadataRetriever().runAndRelease { setDataSource(context, Uri.fromFile(file)) VideoInfo( - duration = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L, + duration = extractDuration(), width = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toLong() ?: 0L, height = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toLong() ?: 0L, mimetype = mimeType, @@ -252,6 +252,11 @@ class AndroidMediaPreProcessor @Inject constructor( } } +private fun MediaMetadataRetriever.extractDuration(): Duration { + val durationInMs = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L + return Duration.ofMillis(durationInMs) +} + fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailUrl: String?, thumbnailInfo: ThumbnailInfo?) = ImageInfo( width = width.toLong(), height = height.toLong(), From dcb9bcd11e73e457af9580e77a7eb51302737e6b Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 15 Jun 2023 22:50:01 +0200 Subject: [PATCH 085/130] Timeline: fix unknown aspectRatio --- .../components/event/TimelineItemAspectRatioBox.kt | 11 ++++++----- .../components/event/TimelineItemImageView.kt | 7 +------ .../event/TimelineItemContentMessageFactory.kt | 4 ++-- .../timeline/model/event/TimelineItemImageContent.kt | 2 +- .../timeline/model/event/TimelineItemVideoContent.kt | 2 +- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt index 043019cc7f..b1240f9236 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt @@ -24,22 +24,23 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import kotlin.math.min @Composable fun TimelineItemAspectRatioBox( height: Int?, - aspectRatio: Float, + aspectRatio: Float?, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, content: @Composable BoxScope.() -> Unit, ) { - // TODO should probably be moved to an ElementTheme.dimensions - val maxHeight = min(300, height ?: 0) + val maxHeight = minOf(300, maxOf(100, height ?: Int.MAX_VALUE)) + val aspectRatioModifier = aspectRatio?.let { + Modifier.aspectRatio(it) + } ?: Modifier Box( modifier = modifier .heightIn(max = maxHeight.dp) - .aspectRatio(aspectRatio, matchHeightConstraintsFirst = true), + .then(aspectRatioModifier), contentAlignment = contentAlignment, content = content ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index f85f9d58e0..f6c1c069b9 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -16,7 +16,6 @@ package io.element.android.features.messages.impl.timeline.components.event -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -28,24 +27,20 @@ import io.element.android.libraries.designsystem.components.BlurHashAsyncImage import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.matrix.ui.media.MediaRequestData -import kotlin.math.max @Composable fun TimelineItemImageView( content: TimelineItemImageContent, modifier: Modifier = Modifier, ) { - // TODO place this value somewhere else? - val minHeight = max(100, content.height ?: 0) TimelineItemAspectRatioBox( - height = minHeight, + height = content.height, aspectRatio = content.aspectRatio, modifier = modifier ) { BlurHashAsyncImage( model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)), blurHash = content.blurhash, - modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Fit, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt index 0e407003e8..18250a05a7 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/factories/event/TimelineItemContentMessageFactory.kt @@ -102,11 +102,11 @@ class TimelineItemContentMessageFactory @Inject constructor( } } - private fun aspectRatioOf(width: Long?, height: Long?): Float { + private fun aspectRatioOf(width: Long?, height: Long?): Float? { return if (height != null && width != null) { width.toFloat() / height.toFloat() } else { - 0.7f + null } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt index 286d3412f1..342e0a336b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemImageContent.kt @@ -29,7 +29,7 @@ data class TimelineItemImageContent( val blurhash: String?, val width: Int?, val height: Int?, - val aspectRatio: Float + val aspectRatio: Float? ) : TimelineItemEventContent { override val type: String = "TimelineItemImageContent" diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt index a76be8c13c..14ec0a972d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/model/event/TimelineItemVideoContent.kt @@ -23,7 +23,7 @@ data class TimelineItemVideoContent( val duration: Long, val videoSource: MediaSource, val thumbnailSource: MediaSource?, - val aspectRatio: Float, + val aspectRatio: Float?, val blurHash: String?, val height: Int?, val width: Int?, From b71b4615b88f9aa232ebee71977eac286662270b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jun 2023 21:57:50 +0000 Subject: [PATCH 086/130] Update dependency com.google.firebase:firebase-bom to v32.1.1 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a225832eda..b7573db2e3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -64,7 +64,7 @@ android_gradle_plugin = { module = "com.android.tools.build:gradle", version.ref android_desugar = "com.android.tools:desugar_jdk_libs:2.0.3" kotlin_gradle_plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } # https://firebase.google.com/docs/android/setup#available-libraries -google_firebase_bom = "com.google.firebase:firebase-bom:32.1.0" +google_firebase_bom = "com.google.firebase:firebase-bom:32.1.1" # AndroidX androidx_material = { module = "com.google.android.material:material", version.ref = "material" } From 1e73dfbaf8285c303c0fb627cd26fdf660a96bcf Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 09:28:21 +0200 Subject: [PATCH 087/130] Iterate on analytics OptIn screen --- .../analytics/impl/AnalyticsOptInView.kt | 227 ++++++++++-------- .../main/res/drawable/element_logo_stars.xml | 57 ----- .../impl/src/main/res/values/localazy.xml | 8 +- 3 files changed, 125 insertions(+), 167 deletions(-) delete mode 100644 features/analytics/impl/src/main/res/drawable/element_logo_stars.xml diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index 899c217e00..1856cd4d3c 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -22,25 +22,27 @@ import android.text.style.ForegroundColorSpan import android.text.style.StyleSpan import android.text.style.UnderlineSpan import androidx.annotation.StringRes -import androidx.compose.foundation.Image +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.Row -import androidx.compose.foundation.layout.Spacer 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.layout.systemBarsPadding +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.CheckCircle +import androidx.compose.material.icons.filled.Poll +import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle @@ -54,9 +56,14 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.analytics.api.AnalyticsOptInEvents +import io.element.android.libraries.designsystem.ElementTextStyles import io.element.android.libraries.designsystem.LinkColor +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.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Icon import io.element.android.libraries.designsystem.theme.components.Text @@ -71,114 +78,122 @@ fun AnalyticsOptInView( ) { LogCompositions(tag = "Analytics", msg = "Root") val eventSink = state.eventSink - Box( + HeaderFooterPage( modifier = modifier .fillMaxSize() .systemBarsPadding() - .imePadding() + .imePadding(), + header = { AnalyticsOptInHeader(state) }, + content = { AnalyticsOptInContent() }, + footer = { AnalyticsOptInFooter(eventSink) }) +} + +@Composable +fun AnalyticsOptInHeader(state: AnalyticsOptInState) { + Column { + IconTitleSubtitleMolecule( + modifier = Modifier.padding(top = 60.dp), + title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName), + subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), + iconImageVector = Icons.Filled.Poll + ) + Text( + text = buildAnnotatedStringWithColoredPart( + R.string.screen_analytics_prompt_read_terms, + R.string.screen_analytics_prompt_read_terms_content_link + ), + modifier = Modifier + .fillMaxWidth() + .padding(top = 20.dp), + style = ElementTextStyles.Regular.subheadline, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.secondary, + ) + } +} + +@Composable +fun AnalyticsOptInContent() { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = BiasAlignment( + horizontalBias = 0f, + verticalBias = -0.4f + ) ) { Column( - modifier = Modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - Column(modifier = Modifier.weight(1f)) { - Image( - painterResource(id = R.drawable.element_logo_stars), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(16.dp) - ) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp), - textAlign = TextAlign.Center, - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - color = MaterialTheme.colorScheme.primary, - ) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_help_us_improve, state.applicationName), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp), - textAlign = TextAlign.Center, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.secondary, - ) + AnalyticsOptInContentRow( + text = stringResource(id = R.string.screen_analytics_prompt_data_usage), + idx = 0 + ) + AnalyticsOptInContentRow( + text = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing), + idx = 1 + ) + AnalyticsOptInContentRow( + text = stringResource(id = R.string.screen_analytics_prompt_settings), + idx = 2 + ) + } + } +} - Text( - text = buildAnnotatedStringWithColoredPart( - R.string.screen_analytics_prompt_read_terms, - R.string.screen_analytics_prompt_read_terms_content_link - ), - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 16.dp), - textAlign = TextAlign.Center, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.secondary, - ) +@Composable +fun AnalyticsOptInContentRow( + text: String, + idx: Int, +) { + val radius = 14.dp + val bgShape = when (idx) { + 0 -> RoundedCornerShape(topStart = radius, topEnd = radius) + 2 -> RoundedCornerShape(bottomStart = radius, bottomEnd = radius) + else -> RoundedCornerShape(0.dp) + } + Row( + modifier = Modifier + .fillMaxWidth() + .background( + color = LocalColors.current.quinary, + shape = bgShape, + ) + .padding(vertical = 12.dp, horizontal = 20.dp), + ) { + Icon( + modifier = Modifier + .size(20.dp) + .background(color = MaterialTheme.colorScheme.background, shape = CircleShape) + .padding(2.dp), + imageVector = Icons.Rounded.Check, + contentDescription = null, + // TODO Compound, this color is not yet in the theme + tint = Color(0xFF007A61) + ) + Text( + modifier = Modifier.padding(start = 16.dp), + text = text, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.primary, + ) + } +} - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(imageVector = Icons.Outlined.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_data_usage).toAnnotatedString(), - color = MaterialTheme.colorScheme.secondary, - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(imageVector = Icons.Outlined.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_third_party_sharing).toAnnotatedString(), - color = MaterialTheme.colorScheme.secondary, - ) - } - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon(imageVector = Icons.Outlined.CheckCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary) - Text( - text = stringResource(id = R.string.screen_analytics_prompt_settings), - color = MaterialTheme.colorScheme.secondary, - ) - } - } - - Button( - onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = StringR.string.action_enable)) - } - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text(text = stringResource(id = StringR.string.action_not_now)) - } - Spacer(Modifier.height(40.dp)) +@Composable +fun AnalyticsOptInFooter(eventSink: (AnalyticsOptInEvents) -> Unit) { + ButtonColumnMolecule { + Button( + onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = StringR.string.action_ok)) + } + TextButton( + onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(false)) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text(text = stringResource(id = StringR.string.action_not_now)) } } } diff --git a/features/analytics/impl/src/main/res/drawable/element_logo_stars.xml b/features/analytics/impl/src/main/res/drawable/element_logo_stars.xml deleted file mode 100644 index d982fbedc4..0000000000 --- a/features/analytics/impl/src/main/res/drawable/element_logo_stars.xml +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/features/analytics/impl/src/main/res/values/localazy.xml b/features/analytics/impl/src/main/res/values/localazy.xml index e6b1c6419d..d6083c860d 100644 --- a/features/analytics/impl/src/main/res/values/localazy.xml +++ b/features/analytics/impl/src/main/res/values/localazy.xml @@ -1,10 +1,10 @@ - "We ""don\'t"" record or profile any account data" - "Help us identify issues and improve %1$s by sharing anonymous usage data." + "We won\'t record or profile any personal data" + "Share anonymous usage data to help us identify issues." "You can read all our terms %1$s." "here" - "You can turn this off anytime in settings" - "We ""don\'t"" share information with third parties" + "You can turn this off anytime" + "We won\'t share your data with third parties" "Help improve %1$s" \ No newline at end of file From 9d6946aa206d1e2cf876db60ae905d64d65fb241 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 09:40:00 +0200 Subject: [PATCH 088/130] Extract method about text to the design system module. --- .../analytics/impl/AnalyticsOptInView.kt | 57 ++----------- .../designsystem/text/AnnotatedStrings.kt | 84 +++++++++++++++++++ 2 files changed, 90 insertions(+), 51 deletions(-) create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index 1856cd4d3c..aac025ed68 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -16,12 +16,6 @@ package io.element.android.features.analytics.impl -import android.graphics.Typeface -import android.text.SpannableString -import android.text.style.ForegroundColorSpan -import android.text.style.StyleSpan -import android.text.style.UnderlineSpan -import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -44,25 +38,20 @@ import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.analytics.api.AnalyticsOptInEvents import io.element.android.libraries.designsystem.ElementTextStyles -import io.element.android.libraries.designsystem.LinkColor 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.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.text.buildAnnotatedStringWithStyledPart import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Icon @@ -98,9 +87,12 @@ fun AnalyticsOptInHeader(state: AnalyticsOptInState) { iconImageVector = Icons.Filled.Poll ) Text( - text = buildAnnotatedStringWithColoredPart( + text = buildAnnotatedStringWithStyledPart( R.string.screen_analytics_prompt_read_terms, - R.string.screen_analytics_prompt_read_terms_content_link + R.string.screen_analytics_prompt_read_terms_content_link, + color = Color.Unspecified, + underline = false, + bold = true, ), modifier = Modifier .fillMaxWidth() @@ -198,43 +190,6 @@ fun AnalyticsOptInFooter(eventSink: (AnalyticsOptInEvents) -> Unit) { } } -fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString { - append(this@toAnnotatedString) - val spannable = SpannableString(this@toAnnotatedString) - spannable.getSpans(0, spannable.length, Any::class.java).forEach { span -> - val start = spannable.getSpanStart(span) - val end = spannable.getSpanEnd(span) - when (span) { - is StyleSpan -> when (span.style) { - Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) - Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end) - Typeface.BOLD_ITALIC -> addStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), start, end) - } - is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end) - is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end) - } - } -} - -@Composable -fun buildAnnotatedStringWithColoredPart( - @StringRes fullTextRes: Int, - @StringRes coloredTextRes: Int, - color: Color = LinkColor, - underline: Boolean = true, -) = buildAnnotatedString { - val coloredPart = stringResource(coloredTextRes) - val fullText = stringResource(fullTextRes, coloredPart) - val startIndex = fullText.indexOf(coloredPart) - append(fullText) - addStyle( - style = SpanStyle( - color = color, - textDecoration = if (underline) TextDecoration.Underline else null - ), start = startIndex, end = startIndex + coloredPart.length - ) -} - @Preview @Composable fun AnalyticsOptInViewLightPreview(@PreviewParameter(AnalyticsOptInStateProvider::class) state: AnalyticsOptInState) = ElementPreviewLight { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.kt new file mode 100644 index 0000000000..7d4de84301 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/text/AnnotatedStrings.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.libraries.designsystem.text + +import android.graphics.Typeface +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.text.style.StyleSpan +import android.text.style.UnderlineSpan +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import io.element.android.libraries.designsystem.LinkColor + +fun String.toAnnotatedString(): AnnotatedString = buildAnnotatedString { + append(this@toAnnotatedString) + val spannable = SpannableString(this@toAnnotatedString) + spannable.getSpans(0, spannable.length, Any::class.java).forEach { span -> + val start = spannable.getSpanStart(span) + val end = spannable.getSpanEnd(span) + when (span) { + is StyleSpan -> when (span.style) { + Typeface.BOLD -> addStyle(SpanStyle(fontWeight = FontWeight.Bold), start, end) + Typeface.ITALIC -> addStyle(SpanStyle(fontStyle = FontStyle.Italic), start, end) + Typeface.BOLD_ITALIC -> addStyle(SpanStyle(fontWeight = FontWeight.Bold, fontStyle = FontStyle.Italic), start, end) + } + is UnderlineSpan -> addStyle(SpanStyle(textDecoration = TextDecoration.Underline), start, end) + is ForegroundColorSpan -> addStyle(SpanStyle(color = Color(span.foregroundColor)), start, end) + } + } +} + +/** + * Convert a string to an [AnnotatedString] with styles applied. + * + * @param fullTextRes the string resource to use as the full text. Must contain a single %s + * @param coloredTextRes the string resource to use as the colored part of the string + * @param color the color to apply to the string + * @param underline whether to underline the string + * @param bold whether to bold the string + */ +@Composable +fun buildAnnotatedStringWithStyledPart( + @StringRes fullTextRes: Int, + @StringRes coloredTextRes: Int, + color: Color = LinkColor, + underline: Boolean = true, + bold: Boolean = false, +) = buildAnnotatedString { + val coloredPart = stringResource(coloredTextRes) + val fullText = stringResource(fullTextRes, coloredPart) + val startIndex = fullText.indexOf(coloredPart) + append(fullText) + addStyle( + style = SpanStyle( + color = color, + textDecoration = if (underline) TextDecoration.Underline else null, + fontWeight = if (bold) FontWeight.Bold else null, + ), + start = startIndex, + end = startIndex + coloredPart.length, + ) +} From 1fea2fa255aa30a733661b2d5e5064408003aaa7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jun 2023 09:57:47 +0200 Subject: [PATCH 089/130] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.21 (#610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update dependency org.matrix.rustcomponents:sdk-android to v0.1.21 * Fix `SlidingSyncState` being renamed to `SlidingSyncListLoadingState` --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jorge Martín --- gradle/libs.versions.toml | 2 +- .../libraries/matrix/impl/room/RustRoomSummaryDataSource.kt | 6 +++--- .../libraries/matrix/impl/sync/SlidingSyncListFlows.kt | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a225832eda..4cbbe5dcdf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -142,7 +142,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.20" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.21" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt index ab94298418..34c0a9cb48 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt @@ -39,7 +39,7 @@ import org.matrix.rustcomponents.sdk.SlidingSync import org.matrix.rustcomponents.sdk.SlidingSyncList import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsListDiff import org.matrix.rustcomponents.sdk.SlidingSyncSelectiveModeBuilder -import org.matrix.rustcomponents.sdk.SlidingSyncState +import org.matrix.rustcomponents.sdk.SlidingSyncListLoadingState import org.matrix.rustcomponents.sdk.UpdateSummary import timber.log.Timber import java.io.Closeable @@ -56,7 +56,7 @@ internal class RustRoomSummaryDataSource( private val coroutineScope = CoroutineScope(SupervisorJob() + coroutineDispatchers.io) private val roomSummaries = MutableStateFlow>(emptyList()) - private val state = MutableStateFlow(SlidingSyncState.NOT_LOADED) + private val state = MutableStateFlow(SlidingSyncListLoadingState.NOT_LOADED) fun init() { coroutineScope.launch { @@ -107,7 +107,7 @@ internal class RustRoomSummaryDataSource( private suspend fun didReceiveSyncUpdate(summary: UpdateSummary) { Timber.v("UpdateRooms with identifiers: ${summary.rooms}") - if (state.value != SlidingSyncState.FULLY_LOADED) { + if (state.value != SlidingSyncListLoadingState.FULLY_LOADED) { return } updateRoomSummaries { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SlidingSyncListFlows.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SlidingSyncListFlows.kt index 2aa0c59330..eb8019a79d 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SlidingSyncListFlows.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/sync/SlidingSyncListFlows.kt @@ -21,11 +21,11 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch import org.matrix.rustcomponents.sdk.SlidingSyncList +import org.matrix.rustcomponents.sdk.SlidingSyncListLoadingState import org.matrix.rustcomponents.sdk.SlidingSyncListRoomListObserver import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsCountObserver import org.matrix.rustcomponents.sdk.SlidingSyncListRoomsListDiff import org.matrix.rustcomponents.sdk.SlidingSyncListStateObserver -import org.matrix.rustcomponents.sdk.SlidingSyncState fun SlidingSyncList.roomListDiff(scope: CoroutineScope): Flow = mxCallbackFlow { @@ -39,9 +39,9 @@ fun SlidingSyncList.roomListDiff(scope: CoroutineScope): Flow = mxCallbackFlow { +fun SlidingSyncList.state(scope: CoroutineScope): Flow = mxCallbackFlow { val observer = object : SlidingSyncListStateObserver { - override fun didReceiveUpdate(newState: SlidingSyncState) { + override fun didReceiveUpdate(newState: SlidingSyncListLoadingState) { scope.launch { send(newState) } From f70d99938263759d085f789b46945b2adfce0357 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 10:13:26 +0200 Subject: [PATCH 090/130] Move extension `openUrlInChromeCustomTab` to :androidutils module. --- features/login/impl/build.gradle.kts | 1 + .../features/login/impl/oidc/customtab/CustomTabHandler.kt | 1 + libraries/androidutils/build.gradle.kts | 1 + .../android/libraries/androidutils/browser/ChromeCustomTab.kt | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt => libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt (97%) diff --git a/features/login/impl/build.gradle.kts b/features/login/impl/build.gradle.kts index aacf4c0aeb..2236438e2a 100644 --- a/features/login/impl/build.gradle.kts +++ b/features/login/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.anvilannotations) anvil(projects.anvilcodegen) implementation(projects.libraries.core) + implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.matrix.api) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt index 407459c5bf..48c674e0a0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/CustomTabHandler.kt @@ -23,6 +23,7 @@ import android.net.Uri import androidx.browser.customtabs.CustomTabsClient import androidx.browser.customtabs.CustomTabsServiceConnection import androidx.browser.customtabs.CustomTabsSession +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.ApplicationContext import javax.inject.Inject diff --git a/libraries/androidutils/build.gradle.kts b/libraries/androidutils/build.gradle.kts index e9a8feaa05..f98914e08a 100644 --- a/libraries/androidutils/build.gradle.kts +++ b/libraries/androidutils/build.gradle.kts @@ -28,5 +28,6 @@ dependencies { implementation(libs.androidx.activity.activity) implementation(libs.androidx.exifinterface) implementation(libs.androidx.security.crypto) + implementation(libs.androidx.browser) implementation(projects.libraries.core) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt similarity index 97% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt rename to libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt index be98566e7c..ec0d9662c7 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/oidc/customtab/Extensions.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/browser/ChromeCustomTab.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.features.login.impl.oidc.customtab +package io.element.android.libraries.androidutils.browser import android.app.Activity import android.content.ActivityNotFoundException From 11926e80ef3c46ac1e5e6ff1e31ca3921e092fc9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 10:22:01 +0200 Subject: [PATCH 091/130] Make the link to the policy active. --- .../android/features/analytics/api/Config.kt | 22 ++++++++++++++++ features/analytics/impl/build.gradle.kts | 1 + .../analytics/impl/AnalyticsOptInNode.kt | 12 +++++++++ .../analytics/impl/AnalyticsOptInView.kt | 25 +++++++++++++------ 4 files changed, 53 insertions(+), 7 deletions(-) create mode 100644 features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/Config.kt diff --git a/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/Config.kt b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/Config.kt new file mode 100644 index 0000000000..883e0d1dc3 --- /dev/null +++ b/features/analytics/api/src/main/kotlin/io/element/android/features/analytics/api/Config.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.analytics.api + +object Config { + const val POLICY_LINK = "https://element.io/cookie-policy" +} + diff --git a/features/analytics/impl/build.gradle.kts b/features/analytics/impl/build.gradle.kts index b72d8dbbec..60b4887a88 100644 --- a/features/analytics/impl/build.gradle.kts +++ b/features/analytics/impl/build.gradle.kts @@ -41,6 +41,7 @@ dependencies { api(projects.features.analytics.api) api(projects.services.analytics.api) implementation(libs.androidx.datastore.preferences) + implementation(libs.androidx.browser) ksp(libs.showkase.processor) testImplementation(libs.test.junit) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt index aafb3d4490..ab060a51cf 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInNode.kt @@ -16,14 +16,19 @@ package io.element.android.features.analytics.impl +import android.app.Activity +import androidx.compose.material.MaterialTheme 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 dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.analytics.api.Config +import io.element.android.libraries.androidutils.browser.openUrlInChromeCustomTab import io.element.android.libraries.di.AppScope @ContributesNode(AppScope::class) @@ -33,12 +38,19 @@ class AnalyticsOptInNode @AssistedInject constructor( private val presenter: AnalyticsOptInPresenter, ) : Node(buildContext, plugins = plugins) { + private fun onClickTerms(activity: Activity, darkTheme: Boolean) { + activity.openUrlInChromeCustomTab(null, darkTheme, Config.POLICY_LINK) + } + @Composable override fun View(modifier: Modifier) { + val activity = LocalContext.current as Activity + val isDark = MaterialTheme.colors.isLight.not() val state = presenter.present() AnalyticsOptInView( state = state, modifier = modifier, + onClickTerms = { onClickTerms(activity, isDark) }, ) } } diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index aac025ed68..908eb4a329 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -17,6 +17,7 @@ package io.element.android.features.analytics.impl 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 @@ -34,8 +35,10 @@ import androidx.compose.material.icons.filled.Poll import androidx.compose.material.icons.rounded.Check import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight @@ -63,6 +66,7 @@ import io.element.android.libraries.ui.strings.R as StringR @Composable fun AnalyticsOptInView( state: AnalyticsOptInState, + onClickTerms: () -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "Analytics", msg = "Root") @@ -72,16 +76,19 @@ fun AnalyticsOptInView( .fillMaxSize() .systemBarsPadding() .imePadding(), - header = { AnalyticsOptInHeader(state) }, + header = { AnalyticsOptInHeader(state, onClickTerms) }, content = { AnalyticsOptInContent() }, footer = { AnalyticsOptInFooter(eventSink) }) } @Composable -fun AnalyticsOptInHeader(state: AnalyticsOptInState) { - Column { +fun AnalyticsOptInHeader( + state: AnalyticsOptInState, + onClickTerms: () -> Unit, +) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { IconTitleSubtitleMolecule( - modifier = Modifier.padding(top = 60.dp), + modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName), subTitle = stringResource(id = R.string.screen_analytics_prompt_help_us_improve), iconImageVector = Icons.Filled.Poll @@ -95,8 +102,9 @@ fun AnalyticsOptInHeader(state: AnalyticsOptInState) { bold = true, ), modifier = Modifier - .fillMaxWidth() - .padding(top = 20.dp), + .clip(shape = RoundedCornerShape(8.dp)) + .clickable { onClickTerms() } + .padding(8.dp), style = ElementTextStyles.Regular.subheadline, textAlign = TextAlign.Center, color = MaterialTheme.colorScheme.secondary, @@ -204,5 +212,8 @@ fun AnalyticsOptInViewDarkPreview(@PreviewParameter(AnalyticsOptInStateProvider: @Composable private fun ContentToPreview(state: AnalyticsOptInState) { - AnalyticsOptInView(state = state) + AnalyticsOptInView( + state = state, + onClickTerms = {}, + ) } From cc426d5109bdf315b8c5c2c616052e6d269f20a5 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 16 Jun 2023 08:56:52 +0000 Subject: [PATCH 092/130] Update screenshots --- ...AnalyticsOptInViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...nalyticsOptInViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewDarkPreview_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.analytics.impl_null_DefaultGroup_AnalyticsOptInViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index eaaa4b3a91..23bd49cb88 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewDarkPreview_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.analytics.impl_null_DefaultGroup_AnalyticsOptInViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:88d5d2054bae36664b69a20853e11e4567bd6cb1e6a8f7ea0a6747ec534807f6 -size 47160 +oid sha256:bde1e07ccd740f6f19bc39be2dc0feaff956a6fbf079639b3f0949b7f2d2dc96 +size 51661 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewLightPreview_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.analytics.impl_null_DefaultGroup_AnalyticsOptInViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index cf5be1e386..d44e6c6dce 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.impl_null_DefaultGroup_AnalyticsOptInViewLightPreview_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.analytics.impl_null_DefaultGroup_AnalyticsOptInViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9774baeb4de35b8fc4f47d4e41eb38f856a098465ffce0cdea66c32e4cebf57 -size 46475 +oid sha256:b5c99a23745883c9b6f9c5112974e9e80f202d7a5255776f66c638af4ed28abc +size 51339 From b61ad40c65a8aa6ad8ae97b99ade41778ecb7cad Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 12:00:05 +0200 Subject: [PATCH 093/130] Delete obsolete translation (also deleted on Localazy) --- features/analytics/impl/src/main/res/values-de/translations.xml | 1 - features/analytics/impl/src/main/res/values-ro/translations.xml | 1 - 2 files changed, 2 deletions(-) diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml index b606a6d82e..e0b033f68e 100644 --- a/features/analytics/impl/src/main/res/values-de/translations.xml +++ b/features/analytics/impl/src/main/res/values-de/translations.xml @@ -1,7 +1,6 @@ "Wir erfassen und analysieren ""keine"" Account-Daten" - "Helfen Sie uns, Probleme zu identifizieren und %1$s zu verbessern, indem Sie anonyme Nutzungsdaten weitergeben." "Sie können alle unsere Nutzerbedingungen %1$s lesen." "hier" "Sie können die Analyse jederzeit in den Einstellungen deaktivieren" diff --git a/features/analytics/impl/src/main/res/values-ro/translations.xml b/features/analytics/impl/src/main/res/values-ro/translations.xml index 91beaa7c52..c46378e6da 100644 --- a/features/analytics/impl/src/main/res/values-ro/translations.xml +++ b/features/analytics/impl/src/main/res/values-ro/translations.xml @@ -1,7 +1,6 @@ "Nu"" înregistrăm sau profilăm datele contului" - "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime." "Puteți citi toate condițiile noastre %1$s." "aici" "Puteți dezactiva această opțiune oricând din setări" From b6e7228ce049f814acf1a58a09a6dc133f5d9498 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 16 Jun 2023 13:52:07 +0200 Subject: [PATCH 094/130] Fix MediaPreProcessor for images/videos sent as file --- .../MessageComposerPresenterTest.kt | 8 +- .../media/MediaMetaDataRetriever.kt | 6 +- .../libraries/mediaupload/api/MediaSender.kt | 17 +- .../mediaupload/api/MediaUploadInfo.kt | 11 +- .../mediaupload/AndroidMediaPreProcessor.kt | 219 ++++++++---------- .../libraries/mediaupload/ImageCompressor.kt | 15 +- .../libraries/mediaupload/ThumbnailFactory.kt | 122 ++++++++++ 7 files changed, 251 insertions(+), 147 deletions(-) create mode 100644 libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index 8fc9b702bc..f646e1efb8 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -50,7 +50,7 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo -import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo +import io.element.android.libraries.mediaupload.api.ThumbnailResult import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode import io.mockk.mockk @@ -301,7 +301,7 @@ class MessageComposerPresenterTest { thumbnailSource = null, blurhash = null, ), - thumbnailInfo = ThumbnailProcessingInfo( + thumbnailInfo = ThumbnailResult( file = File("/some/path"), info = ThumbnailInfo( width = null, @@ -309,7 +309,6 @@ class MessageComposerPresenterTest { mimetype = null, size = null, ), - blurhash = "", ) ) ) @@ -344,7 +343,7 @@ class MessageComposerPresenterTest { thumbnailSource = null, blurhash = null, ), - thumbnailInfo = ThumbnailProcessingInfo( + thumbnailInfo = ThumbnailResult( file = File("/some/path"), info = ThumbnailInfo( width = null, @@ -352,7 +351,6 @@ class MessageComposerPresenterTest { mimetype = null, size = null, ), - blurhash = "", ) ) ) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/MediaMetaDataRetriever.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/MediaMetaDataRetriever.kt index 3b29787285..8f942957e0 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/MediaMetaDataRetriever.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/media/MediaMetaDataRetriever.kt @@ -20,5 +20,9 @@ import android.media.MediaMetadataRetriever /** [MediaMetadataRetriever] only implements `AutoClosable` since API 29, so we need to execute this to have the same in older APIs. */ inline fun MediaMetadataRetriever.runAndRelease(block: MediaMetadataRetriever.() -> T): T { - return block().also { release() } + return try { + block() + } finally { + release() + } } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 585670d939..5de8239e38 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -44,15 +44,26 @@ class MediaSender @Inject constructor( ): Result { return when (info) { is MediaUploadInfo.Image -> { - sendImage(info.file, info.thumbnailInfo.file, info.info) + sendImage( + file = info.file, + thumbnailFile = info.thumbnailFile, + imageInfo = info.info + ) } is MediaUploadInfo.Video -> { - sendVideo(info.file, info.thumbnailInfo.file, info.info) + sendVideo( + file = info.file, + thumbnailFile = info.thumbnailFile, + videoInfo = info.info + ) } is MediaUploadInfo.AnyFile -> { - sendFile(info.file, info.info) + sendFile( + file = info.file, + fileInfo = info.info + ) } else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info")) } diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt index 47fa26ae79..5da3d36c44 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaUploadInfo.kt @@ -19,7 +19,6 @@ package io.element.android.libraries.mediaupload.api import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.ThumbnailInfo import io.element.android.libraries.matrix.api.media.VideoInfo import java.io.File @@ -27,14 +26,8 @@ sealed interface MediaUploadInfo { val file: File - data class Image(override val file: File, val info: ImageInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo - data class Video(override val file: File, val info: VideoInfo, val thumbnailInfo: ThumbnailProcessingInfo) : MediaUploadInfo + data class Image(override val file: File, val info: ImageInfo, val thumbnailFile: File) : MediaUploadInfo + data class Video(override val file: File, val info: VideoInfo, val thumbnailFile: File) : MediaUploadInfo data class Audio(override val file: File, val info: AudioInfo) : MediaUploadInfo data class AnyFile(override val file: File, val info: FileInfo) : MediaUploadInfo } - -data class ThumbnailProcessingInfo( - val file: File, - val info: ThumbnailInfo, - val blurhash: String, -) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 4882000307..dc56846b87 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -17,7 +17,7 @@ package io.element.android.libraries.mediaupload import android.content.Context -import android.graphics.Bitmap +import android.graphics.BitmapFactory import android.media.MediaMetadataRetriever import android.net.Uri import androidx.exifinterface.media.ExifInterface @@ -37,26 +37,21 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.media.AudioInfo import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.MediaSource -import io.element.android.libraries.matrix.api.media.ThumbnailInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaUploadInfo -import io.element.android.libraries.mediaupload.api.ThumbnailProcessingInfo import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext -import java.io.ByteArrayInputStream -import java.io.ByteArrayOutputStream import java.io.File import java.io.InputStream import javax.inject.Inject -import kotlin.time.Duration.Companion.seconds @ContributesBinding(AppScope::class) class AndroidMediaPreProcessor @Inject constructor( @ApplicationContext private val context: Context, + private val thumbnailFactory: ThumbnailFactory, private val imageCompressor: ImageCompressor, private val videoCompressor: VideoCompressor, private val coroutineDispatchers: CoroutineDispatchers, @@ -69,23 +64,6 @@ class AndroidMediaPreProcessor @Inject constructor( * values may surpass this limit. (i.e.: an image of `480x3000px` would have `inSampleSize=1` and be sent as is). */ private const val IMAGE_SCALE_REF_SIZE = 640 - - /** - * Max width of thumbnail images. - * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/?ref=blog.gitter.im#thumbnails). - */ - private const val THUMB_MAX_WIDTH = 800 - - /** - * Max height of thumbnail images. - * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/?ref=blog.gitter.im#thumbnails). - */ - private const val THUMB_MAX_HEIGHT = 600 - - /** - * Frame of the video to be used for generating a thumbnail. - */ - private val VIDEO_THUMB_FRAME = 5.seconds.inWholeMicroseconds } private val contentResolver = context.contentResolver @@ -95,40 +73,34 @@ class AndroidMediaPreProcessor @Inject constructor( mimeType: String, deleteOriginal: Boolean, compressIfPossible: Boolean, - ): Result = runCatching { - val shouldBeCompressed = compressIfPossible && - (mimeType.isMimeTypeImage() && mimeType != MimeTypes.Gif) || - mimeType.isMimeTypeVideo() - - val result = if (shouldBeCompressed) { - when { - mimeType.isMimeTypeImage() -> processImage(uri) - mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType) + ): Result = withContext(coroutineDispatchers.computation) { + runCatching { + val result = when { + mimeType.isMimeTypeImage() -> processImage(uri, mimeType, compressIfPossible && mimeType != MimeTypes.Gif) + mimeType.isMimeTypeVideo() -> processVideo(uri, mimeType, compressIfPossible) mimeType.isMimeTypeAudio() -> processAudio(uri, mimeType) - else -> error("Cannot compress file of type: $mimeType") + else -> processFile(uri, mimeType) } - } else { - val file = copyToTmpFile(uri) - // Remove image metadata here too - if (mimeType.isMimeTypeImage() && mimeType != MimeTypes.Gif) { - removeSensitiveImageMetadata(file) + if (deleteOriginal) { + tryOrNull { + contentResolver.delete(uri, null, null) + } } - val info = FileInfo( - mimetype = mimeType, - size = file.length(), - thumbnailInfo = null, - thumbnailSource = null, - ) - MediaUploadInfo.AnyFile(file, info) + result.postProcess(uri) } - if (deleteOriginal) { - tryOrNull { - contentResolver.delete(uri, null, null) - } - } - result.postProcess(uri) }.mapFailure { MediaPreProcessor.Failure(it) } + private suspend fun processFile(uri: Uri, mimeType: String): MediaUploadInfo { + val file = copyToTmpFile(uri) + val info = FileInfo( + mimetype = mimeType, + size = file.length(), + thumbnailInfo = null, + thumbnailSource = null, + ) + return MediaUploadInfo.AnyFile(file, info) + } + private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo { val name = context.getFileName(uri) ?: return this val renamedFile = File(context.cacheDir, name).also { @@ -142,40 +114,83 @@ class AndroidMediaPreProcessor @Inject constructor( } } - private suspend fun processImage(uri: Uri): MediaUploadInfo { - val compressedFileResult = contentResolver.openInputStream(uri).use { input -> - imageCompressor.compressToTmpFile( - inputStream = requireNotNull(input), - resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE), - ).getOrThrow() + private suspend fun processImage(uri: Uri, mimeType: String, shouldBeCompressed: Boolean): MediaUploadInfo { + + suspend fun processImageWithCompression(): MediaUploadInfo { + val compressionResult = contentResolver.openInputStream(uri).use { input -> + imageCompressor.compressToTmpFile( + inputStream = requireNotNull(input), + resizeMode = ResizeMode.Approximate(IMAGE_SCALE_REF_SIZE, IMAGE_SCALE_REF_SIZE), + ).getOrThrow() + } + val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(compressionResult.file) + val imageInfo = compressionResult.toImageInfo( + mimeType = mimeType, + thumbnailResult = thumbnailResult + ) + removeSensitiveImageMetadata(compressionResult.file) + return MediaUploadInfo.Image( + file = compressionResult.file, + info = imageInfo, + thumbnailFile = thumbnailResult.file + ) } - removeSensitiveImageMetadata(compressedFileResult.file) + suspend fun processImageWithoutCompression(): MediaUploadInfo { + val file = copyToTmpFile(uri) + val thumbnailResult: ThumbnailResult = thumbnailFactory.createImageThumbnail(file) + val imageInfo = contentResolver.openInputStream(uri).use { input -> + val bitmap = BitmapFactory.decodeStream(input, null, null)!! + ImageInfo( + width = bitmap.width.toLong(), + height = bitmap.height.toLong(), + mimetype = mimeType, + size = file.length(), + thumbnailInfo = thumbnailResult.info, + thumbnailSource = null, + blurhash = thumbnailResult.blurhash, + ) + } + removeSensitiveImageMetadata(file) + return MediaUploadInfo.Image( + file = file, + info = imageInfo, + thumbnailFile = thumbnailResult.file + ) + } - val thumbnailResult = compressedFileResult.file.inputStream().use { generateImageThumbnail(it) } - val processingResult = compressedFileResult.toImageInfo(MimeTypes.Jpeg, thumbnailResult.file.path, thumbnailResult.info) - return MediaUploadInfo.Image(compressedFileResult.file, processingResult, thumbnailResult) + return if (shouldBeCompressed) { + processImageWithCompression() + } else { + processImageWithoutCompression() + } } - private suspend fun processVideo(uri: Uri, mimeType: String?): MediaUploadInfo { - val thumbnailInfo = extractVideoThumbnail(uri) - val resultFile = videoCompressor.compress(uri) - .onEach { - // TODO handle progress - } - .filterIsInstance() - .first() - .file - - val videoProcessingInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo.file.path, thumbnailInfo) - return MediaUploadInfo.Video(resultFile, videoProcessingInfo, thumbnailInfo) + private suspend fun processVideo(uri: Uri, mimeType: String?, shouldBeCompressed: Boolean): MediaUploadInfo { + val resultFile = if (shouldBeCompressed) { + videoCompressor.compress(uri) + .onEach { + // TODO handle progress + } + .filterIsInstance() + .first() + .file + } else { + copyToTmpFile(uri) + } + val thumbnailInfo = thumbnailFactory.createVideoThumbnail(resultFile) + val videoInfo = extractVideoMetadata(resultFile, mimeType, thumbnailInfo) + return MediaUploadInfo.Video( + file = resultFile, + info = videoInfo, + thumbnailFile = thumbnailInfo.file + ) } private suspend fun processAudio(uri: Uri, mimeType: String?): MediaUploadInfo { val file = copyToTmpFile(uri) return MediaMetadataRetriever().runAndRelease { setDataSource(context, Uri.fromFile(file)) - val info = AudioInfo( duration = extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLong() ?: 0L, size = file.length(), @@ -186,15 +201,6 @@ class AndroidMediaPreProcessor @Inject constructor( } } - private suspend fun generateImageThumbnail(inputStream: InputStream): ThumbnailProcessingInfo { - val thumbnailResult = imageCompressor - .compressToTmpFile( - inputStream = inputStream, - resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT), - ).getOrThrow() - return thumbnailResult.toThumbnailProcessingInfo(MimeTypes.Jpeg) - } - private fun removeSensitiveImageMetadata(file: File) { // Remove GPS info, user comments and subject location tags val exifInterface = ExifInterface(file) @@ -215,7 +221,7 @@ class AndroidMediaPreProcessor @Inject constructor( } } - private fun extractVideoMetadata(file: File, mimeType: String?, thumbnailUrl: String?, thumbnailInfo: ThumbnailProcessingInfo?): VideoInfo = + private fun extractVideoMetadata(file: File, mimeType: String?, thumbnailResult: ThumbnailResult): VideoInfo = MediaMetadataRetriever().runAndRelease { setDataSource(context, Uri.fromFile(file)) VideoInfo( @@ -224,51 +230,26 @@ class AndroidMediaPreProcessor @Inject constructor( height = extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toLong() ?: 0L, mimetype = mimeType, size = file.length(), - thumbnailInfo = thumbnailInfo?.info, - thumbnailSource = thumbnailUrl?.let { MediaSource(it) }, - blurhash = thumbnailInfo?.blurhash, + thumbnailInfo = thumbnailResult.info, + // Will be computed by the rust sdk + thumbnailSource = null, + blurhash = thumbnailResult.blurhash, ) } - private suspend fun extractVideoThumbnail(uri: Uri): ThumbnailProcessingInfo = - MediaMetadataRetriever().runAndRelease { - setDataSource(context, uri) - val bitmap = requireNotNull(getFrameAtTime(VIDEO_THUMB_FRAME)) - val inputStream = ByteArrayOutputStream().use { - bitmap.compress(Bitmap.CompressFormat.JPEG, 80, it) - ByteArrayInputStream(it.toByteArray()) - } - - val result = imageCompressor.compressToTmpFile( - inputStream = inputStream, - resizeMode = ResizeMode.Strict(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT), - ) - result.getOrThrow().toThumbnailProcessingInfo(MimeTypes.Jpeg) - } - private suspend fun copyToTmpFile(uri: Uri): File { return contentResolver.openInputStream(uri)?.use { createTmpFileWithInput(it) } ?: error("Could not copy the contents of $uri to a temporary file") } } -fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailUrl: String?, thumbnailInfo: ThumbnailInfo?) = ImageInfo( +fun ImageCompressionResult.toImageInfo(mimeType: String, thumbnailResult: ThumbnailResult) = ImageInfo( width = width.toLong(), height = height.toLong(), mimetype = mimeType, size = size, - thumbnailInfo = thumbnailInfo, - thumbnailSource = thumbnailUrl?.let { MediaSource(it) }, - blurhash = blurhash, -) - -fun ImageCompressionResult.toThumbnailProcessingInfo(mimeType: String) = ThumbnailProcessingInfo( - file = file, - info = ThumbnailInfo( - width = width.toLong(), - height = height.toLong(), - mimetype = mimeType, - size = size, - ), - blurhash = blurhash, + thumbnailInfo = thumbnailResult.info, + // Will be computed by the rust sdk + thumbnailSource = null, + blurhash = thumbnailResult.blurhash, ) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt index 96d5e2ea63..2b8669fe42 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ImageCompressor.kt @@ -42,27 +42,23 @@ class ImageCompressor @Inject constructor( * @return a [Result] containing the resulting [ImageCompressionResult] with the temporary [File] and some metadata. */ suspend fun compressToTmpFile( - inputStream: InputStream, - resizeMode: ResizeMode, - format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, - desiredQuality: Int = 80, + inputStream: InputStream, + resizeMode: ResizeMode, + format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, + desiredQuality: Int = 80, ): Result = withContext(Dispatchers.IO) { runCatching { val compressedBitmap = compressToBitmap(inputStream, resizeMode).getOrThrow() - val blurhash = BlurHash.encode(compressedBitmap, 3, 3) - // Encode bitmap to the destination temporary file val tmpFile = context.createTmpFile(extension = "jpeg") tmpFile.outputStream().use { compressedBitmap.compress(format, desiredQuality, it) } - ImageCompressionResult( file = tmpFile, width = compressedBitmap.width, height = compressedBitmap.height, - size = tmpFile.length(), - blurhash = blurhash + size = tmpFile.length() ) } } @@ -116,7 +112,6 @@ data class ImageCompressionResult( val width: Int, val height: Int, val size: Long, - val blurhash: String, ) sealed interface ResizeMode { diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt new file mode 100644 index 0000000000..f0010cf9a7 --- /dev/null +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt @@ -0,0 +1,122 @@ +/* + * 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.mediaupload + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Bitmap +import android.media.MediaMetadataRetriever +import android.media.ThumbnailUtils +import android.os.Build +import android.os.CancellationSignal +import android.provider.MediaStore +import android.util.Size +import androidx.core.net.toUri +import com.vanniktech.blurhash.BlurHash +import io.element.android.libraries.androidutils.file.createTmpFile +import io.element.android.libraries.androidutils.media.runAndRelease +import io.element.android.libraries.core.mimetype.MimeTypes +import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.media.ThumbnailInfo +import kotlinx.coroutines.suspendCancellableCoroutine +import java.io.File +import javax.inject.Inject +import kotlin.coroutines.resume + +/** + * Max width of thumbnail images. + * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/?ref=blog.gitter.im#thumbnails). + */ +private const val THUMB_MAX_WIDTH = 800 + +/** + * Max height of thumbnail images. + * See [the Matrix spec](https://spec.matrix.org/latest/client-server-api/?ref=blog.gitter.im#thumbnails). + */ +private const val THUMB_MAX_HEIGHT = 600 + +/** + * Frame of the video to be used for generating a thumbnail. + */ +private const val VIDEO_THUMB_FRAME = 0L + +class ThumbnailFactory @Inject constructor( + @ApplicationContext private val context: Context, +) { + + @SuppressLint("NewApi") + suspend fun createImageThumbnail(file: File): ThumbnailResult { + return createThumbnail { cancellationSignal -> + // This API works correctly with GIF + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ThumbnailUtils.createImageThumbnail( + file, + Size(THUMB_MAX_WIDTH, THUMB_MAX_HEIGHT), + cancellationSignal + ) + } else { + ThumbnailUtils.createImageThumbnail( + file.path, + MediaStore.Images.Thumbnails.MINI_KIND, + ) + } + } + } + + suspend fun createVideoThumbnail(file: File): ThumbnailResult { + return createThumbnail { + MediaMetadataRetriever().runAndRelease { + setDataSource(context, file.toUri()) + getFrameAtTime(VIDEO_THUMB_FRAME) + } + } + } + + private suspend fun createThumbnail(bitmapThumbnailFactory: (CancellationSignal) -> Bitmap?): ThumbnailResult = suspendCancellableCoroutine { continuation -> + val cancellationSignal = CancellationSignal() + continuation.invokeOnCancellation { + cancellationSignal.cancel() + } + val bitmapThumbnail: Bitmap? = bitmapThumbnailFactory(cancellationSignal) + val thumbnailFile = context.createTmpFile(extension = "jpeg") + thumbnailFile.outputStream().use { outputStream -> + bitmapThumbnail?.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) + } + val blurhash = bitmapThumbnail?.let { + BlurHash.encode(it, 3, 3) + } + val thumbnailResult = ThumbnailResult( + file = thumbnailFile, + info = ThumbnailInfo( + height = bitmapThumbnail?.height?.toLong(), + width = bitmapThumbnail?.width?.toLong(), + mimetype = MimeTypes.Jpeg, + size = thumbnailFile.length() + ), + blurhash = blurhash + ) + bitmapThumbnail?.recycle() + continuation.resume(thumbnailResult) + + } +} + +data class ThumbnailResult( + val file: File, + val info: ThumbnailInfo, + val blurhash: String?, +) From 551a97ac2e4d6bc7cb18358a13d9e4edab2bea61 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 16 Jun 2023 13:59:59 +0200 Subject: [PATCH 095/130] Media: fix detekt --- .../element/android/libraries/mediaupload/ThumbnailFactory.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt index f0010cf9a7..a9ed6319cb 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/ThumbnailFactory.kt @@ -86,12 +86,12 @@ class ThumbnailFactory @Inject constructor( } } - private suspend fun createThumbnail(bitmapThumbnailFactory: (CancellationSignal) -> Bitmap?): ThumbnailResult = suspendCancellableCoroutine { continuation -> + private suspend fun createThumbnail(bitmapFactory: (CancellationSignal) -> Bitmap?): ThumbnailResult = suspendCancellableCoroutine { continuation -> val cancellationSignal = CancellationSignal() continuation.invokeOnCancellation { cancellationSignal.cancel() } - val bitmapThumbnail: Bitmap? = bitmapThumbnailFactory(cancellationSignal) + val bitmapThumbnail: Bitmap? = bitmapFactory(cancellationSignal) val thumbnailFile = context.createTmpFile(extension = "jpeg") thumbnailFile.outputStream().use { outputStream -> bitmapThumbnail?.compress(Bitmap.CompressFormat.JPEG, 80, outputStream) From 7f8250106c022b2cf321c699f7bd2402defdf187 Mon Sep 17 00:00:00 2001 From: ganfra Date: Fri, 16 Jun 2023 14:23:52 +0200 Subject: [PATCH 096/130] Media : Fix test compilation --- .../MessageComposerPresenterTest.kt | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt index f646e1efb8..7e0cbeeba3 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/textcomposer/MessageComposerPresenterTest.kt @@ -36,7 +36,6 @@ import io.element.android.libraries.featureflag.api.FeatureFlagService import io.element.android.libraries.featureflag.api.FeatureFlags import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.media.ImageInfo -import io.element.android.libraries.matrix.api.media.ThumbnailInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.test.ANOTHER_MESSAGE @@ -50,7 +49,6 @@ import io.element.android.libraries.mediapickers.test.FakePickerProvider import io.element.android.libraries.mediaupload.api.MediaPreProcessor import io.element.android.libraries.mediaupload.api.MediaSender import io.element.android.libraries.mediaupload.api.MediaUploadInfo -import io.element.android.libraries.mediaupload.api.ThumbnailResult import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor import io.element.android.libraries.textcomposer.MessageComposerMode import io.mockk.mockk @@ -301,15 +299,7 @@ class MessageComposerPresenterTest { thumbnailSource = null, blurhash = null, ), - thumbnailInfo = ThumbnailResult( - file = File("/some/path"), - info = ThumbnailInfo( - width = null, - height = null, - mimetype = null, - size = null, - ), - ) + thumbnailFile = File("/some/path") ) ) ) @@ -343,15 +333,7 @@ class MessageComposerPresenterTest { thumbnailSource = null, blurhash = null, ), - thumbnailInfo = ThumbnailResult( - file = File("/some/path"), - info = ThumbnailInfo( - width = null, - height = null, - mimetype = null, - size = null, - ), - ) + thumbnailFile = File("/some/path") ) ) ) From 7dea3c1dd2bc064d0be888ee1afce8669f8747c2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 15:14:00 +0200 Subject: [PATCH 097/130] Ensure files downloaded by localazy end with a new line. --- tools/localazy/downloadStrings.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/localazy/downloadStrings.sh b/tools/localazy/downloadStrings.sh index 0f35f9ec59..bc00488258 100755 --- a/tools/localazy/downloadStrings.sh +++ b/tools/localazy/downloadStrings.sh @@ -40,6 +40,12 @@ fi echo "Importing the strings..." localazy download --config ./tools/localazy/localazy.json +echo "Add new lines to the end of the files..." +find . -name 'localazy.xml' -print0 -exec bash -c "echo \"\" >> \"{}\"" \; >> /dev/null +if [[ $allFiles == 1 ]]; then + find . -name 'translations.xml' -print0 -exec bash -c "echo \"\" >> \"{}\"" \; >> /dev/null +fi + echo "Removing the generated config" rm ./tools/localazy/localazy.json From 7a1416aad4a7d7a5bf50805bdc76827ab1040b3d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 15:27:09 +0200 Subject: [PATCH 098/130] Add Modifier parameter and make internal Composable private. --- .../analytics/impl/AnalyticsOptInView.kt | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt index 908eb4a329..e2af75ceca 100644 --- a/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt +++ b/features/analytics/impl/src/main/kotlin/io/element/android/features/analytics/impl/AnalyticsOptInView.kt @@ -82,11 +82,15 @@ fun AnalyticsOptInView( } @Composable -fun AnalyticsOptInHeader( +private fun AnalyticsOptInHeader( state: AnalyticsOptInState, onClickTerms: () -> Unit, + modifier: Modifier = Modifier, ) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + ) { IconTitleSubtitleMolecule( modifier = Modifier.padding(top = 60.dp, bottom = 12.dp), title = stringResource(id = R.string.screen_analytics_prompt_title, state.applicationName), @@ -113,9 +117,11 @@ fun AnalyticsOptInHeader( } @Composable -fun AnalyticsOptInContent() { +private fun AnalyticsOptInContent( + modifier: Modifier = Modifier, +) { Box( - modifier = Modifier.fillMaxSize(), + modifier = modifier.fillMaxSize(), contentAlignment = BiasAlignment( horizontalBias = 0f, verticalBias = -0.4f @@ -141,9 +147,10 @@ fun AnalyticsOptInContent() { } @Composable -fun AnalyticsOptInContentRow( +private fun AnalyticsOptInContentRow( text: String, idx: Int, + modifier: Modifier = Modifier, ) { val radius = 14.dp val bgShape = when (idx) { @@ -152,7 +159,7 @@ fun AnalyticsOptInContentRow( else -> RoundedCornerShape(0.dp) } Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() .background( color = LocalColors.current.quinary, @@ -181,8 +188,13 @@ fun AnalyticsOptInContentRow( } @Composable -fun AnalyticsOptInFooter(eventSink: (AnalyticsOptInEvents) -> Unit) { - ButtonColumnMolecule { +private fun AnalyticsOptInFooter( + eventSink: (AnalyticsOptInEvents) -> Unit, + modifier: Modifier = Modifier, +) { + ButtonColumnMolecule( + modifier = modifier, + ) { Button( onClick = { eventSink(AnalyticsOptInEvents.EnableAnalytics(true)) }, modifier = Modifier.fillMaxWidth(), From 2a3dd31da96e70dae9627184d7773042393335bd Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 17:07:31 +0200 Subject: [PATCH 099/130] Add missing module, so that they can contribute to the screenshot test and the test coverage. --- tests/uitests/build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index 307cbcdd5d..ed4f5298ce 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -16,6 +16,7 @@ import extension.allFeaturesImpl import extension.allLibrariesImpl +import extension.allServicesImpl plugins { id("io.element.android-compose-library") @@ -37,5 +38,6 @@ dependencies { implementation(libs.showkase) allLibrariesImpl() + allServicesImpl() allFeaturesImpl(rootDir, logger) } From 094376cb8ce87ffcb514196377ecaa34e32e54d4 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 16 Jun 2023 15:19:29 +0000 Subject: [PATCH 100/130] Update screenshots --- ...ultGroup_AppErrorViewDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ltGroup_AppErrorViewLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.services.apperror.impl_null_DefaultGroup_AppErrorViewDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.services.apperror.impl_null_DefaultGroup_AppErrorViewLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.services.apperror.impl_null_DefaultGroup_AppErrorViewDarkPreview_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.services.apperror.impl_null_DefaultGroup_AppErrorViewDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..f97e9386c7 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.services.apperror.impl_null_DefaultGroup_AppErrorViewDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:62224b9bbc7b9c1ee4eb419a1f47783d3386c9a11d1f70ccdacddb3b84793651 +size 21747 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.services.apperror.impl_null_DefaultGroup_AppErrorViewLightPreview_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.services.apperror.impl_null_DefaultGroup_AppErrorViewLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..fe1883275b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.services.apperror.impl_null_DefaultGroup_AppErrorViewLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cdb537e5fbe1f8202e4e0463e907f2a4a9b3c3f5cd4d05f97c3ae6c65d614091 +size 21556 From b602a4cd5d4c0836ac4f106dfd77d8e7f3463b49 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 18:00:40 +0200 Subject: [PATCH 101/130] Restore sonar task on PR, we want to have sonarcloud comment on PR to track code quality (#619) --- .github/workflows/tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index aea05b5a22..3e9dbab5c6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -81,8 +81,12 @@ jobs: **/out/failures/ **/build/reports/tests/*UnitTest/ - - name: 🔊 Publish results to Sonar (disabled) - run: echo "This is now done only once a day, see nightlyReports.yml" + - name: 🔊 Publish results to Sonar + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + ORG_GRADLE_PROJECT_SONAR_LOGIN: ${{ secrets.SONAR_TOKEN }} + if: ${{ always() && env.SONAR_TOKEN != '' && env.ORG_GRADLE_PROJECT_SONAR_LOGIN != '' }} + run: ./gradlew sonar $CI_GRADLE_ARG_PROPERTIES # https://github.com/codecov/codecov-action - name: ☂️ Upload coverage reports to codecov From 9c5f05dba40d5516ce476d3aebb8ed40796e99e9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 16 Jun 2023 18:08:20 +0200 Subject: [PATCH 102/130] Fix the 6 issues reported by Sonar. --- .../features/createroom/impl/CreateRoomDataStore.kt | 3 ++- .../ConfirmAccountProviderView.kt | 3 +-- .../login/impl/src/main/res/values/localazy.xml | 1 + .../android/libraries/androidutils/file/File.kt | 13 +++++++++++++ .../mediaupload/AndroidMediaPreProcessor.kt | 3 ++- .../libraries/mediaupload/VideoCompressor.kt | 5 +++-- .../notifications/NotificationEventPersistence.kt | 3 ++- 7 files changed, 24 insertions(+), 7 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt index 46de8fff91..f79284d082 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt @@ -20,6 +20,7 @@ import android.net.Uri import io.element.android.features.createroom.impl.configureroom.RoomPrivacy import io.element.android.features.createroom.impl.di.CreateRoomScope import io.element.android.features.createroom.impl.userlist.UserListDataStore +import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.di.SingleIn import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow @@ -36,7 +37,7 @@ class CreateRoomDataStore @Inject constructor( private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig()) private var cachedAvatarUri: Uri? = null set(value) { - field?.path?.let { File(it) }?.delete() + field?.path?.let { File(it) }?.safeDelete() field = value } 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 index c9053d67c9..e6e1ce8e83 100644 --- 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 @@ -80,8 +80,7 @@ fun ConfirmAccountProviderView( id = if (state.isAccountCreation) { R.string.screen_account_provider_signup_subtitle } else { - // Use same value for now. - R.string.screen_account_provider_signup_subtitle + R.string.screen_account_provider_signin_subtitle }, ) ) diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index bb27ca51e1..55324613ed 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -6,6 +6,7 @@ "Enter a search term or a domain address." "Search for a company, community, or private server." "Find an account provider" + "This is where you conversations will live — just like you would use an email provider to keep your emails." "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" diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt index 581d45a2b0..269407d3b5 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/file/File.kt @@ -35,6 +35,19 @@ fun File.safeDelete() { ) } +fun File.safeRenameTo(dest: File) { + tryOrNull( + onError = { + Timber.e(it, "Error, unable to rename file $path to ${dest.path}") + }, + operation = { + if (renameTo(dest).not()) { + Timber.w("Warning, unable to rename file $path to ${dest.path}") + } + } + ) +} + fun Context.createTmpFile(baseDir: File = cacheDir, extension: String? = null): File { val suffix = extension?.let { ".$extension" } return File.createTempFile(UUID.randomUUID().toString(), suffix, baseDir).apply { mkdirs() } diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt index 0f8ebab21a..c53efeeafc 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/AndroidMediaPreProcessor.kt @@ -24,6 +24,7 @@ import androidx.exifinterface.media.ExifInterface import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.androidutils.file.createTmpFile import io.element.android.libraries.androidutils.file.getFileName +import io.element.android.libraries.androidutils.file.safeRenameTo import io.element.android.libraries.androidutils.media.runAndRelease import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull @@ -133,7 +134,7 @@ class AndroidMediaPreProcessor @Inject constructor( private fun MediaUploadInfo.postProcess(uri: Uri): MediaUploadInfo { val name = context.getFileName(uri) ?: return this val renamedFile = File(context.cacheDir, name).also { - file.renameTo(it) + file.safeRenameTo(it) } return when (this) { is MediaUploadInfo.AnyFile -> copy(file = renamedFile) diff --git a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/VideoCompressor.kt b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/VideoCompressor.kt index 490e286353..e7e294cd7c 100644 --- a/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/VideoCompressor.kt +++ b/libraries/mediaupload/impl/src/main/kotlin/io/element/android/libraries/mediaupload/VideoCompressor.kt @@ -21,6 +21,7 @@ import android.net.Uri import com.otaliastudios.transcoder.Transcoder import com.otaliastudios.transcoder.TranscoderListener import io.element.android.libraries.androidutils.file.createTmpFile +import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.di.ApplicationContext import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow @@ -46,12 +47,12 @@ class VideoCompressor @Inject constructor( } override fun onTranscodeCanceled() { - tmpFile.delete() + tmpFile.safeDelete() close() } override fun onTranscodeFailed(exception: Throwable) { - tmpFile.delete() + tmpFile.safeDelete() close(exception) } }) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt index d6135f28b0..613a8d2bb7 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventPersistence.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.impl.notifications import android.content.Context import io.element.android.libraries.androidutils.file.EncryptedFileFactory +import io.element.android.libraries.androidutils.file.safeDelete import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.ApplicationContext @@ -70,7 +71,7 @@ class NotificationEventPersistence @Inject constructor( fun persistEvents(queuedEvents: NotificationEventQueue) { Timber.tag(loggerTag.value).d("Serializing ${queuedEvents.rawEvents().size} NotifiableEvent(s)") // Always delete file before writing, or encryptedFile.openFileOutput() will throw - file.delete() + file.safeDelete() if (queuedEvents.isEmpty()) return try { encryptedFile.openFileOutput().use { fos -> From ee9b61a180bb5963b93503b009622717af058b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Romanik?= Date: Sat, 17 Jun 2023 22:18:40 +0200 Subject: [PATCH 103/130] add '.' to MATRIX_ROOM_IDENTIFIER_REGEX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Przemysław Romanik --- .../element/android/libraries/matrix/api/core/MatrixPatterns.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt index 4e6d468c54..bc0f0c04bc 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt @@ -34,7 +34,7 @@ object MatrixPatterns { val PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER = MATRIX_USER_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) // regex pattern to find room ids in a string. - private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9-]+$DOMAIN_REGEX" + private const val MATRIX_ROOM_IDENTIFIER_REGEX = "![A-Z0-9.-]+$DOMAIN_REGEX" private val PATTERN_CONTAIN_MATRIX_ROOM_IDENTIFIER = MATRIX_ROOM_IDENTIFIER_REGEX.toRegex(RegexOption.IGNORE_CASE) // regex pattern to find room aliases in a string. From 21f59442bdb6306a398124ca593ef3959fec6b1b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 08:51:47 +0200 Subject: [PATCH 104/130] Update dependency com.google.truth:truth to v1.1.5 (#629) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0ec97d5514..75649f4314 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -125,7 +125,7 @@ test_barista = "com.adevinta.android:barista:4.3.0" test_hamcrest = "org.hamcrest:hamcrest:2.2" test_orchestrator = "androidx.test:orchestrator:1.4.2" test_turbine = "app.cash.turbine:turbine:0.13.0" -test_truth = "com.google.truth:truth:1.1.4" +test_truth = "com.google.truth:truth:1.1.5" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.12" test_robolectric = "org.robolectric:robolectric:4.10.3" test_appyx_junit = { module = "com.bumble.appyx:testing-junit4", version.ref = "appyx" } From dfcbb33c0a2d49a8577a3f20d780ac9f88039588 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 19 Jun 2023 11:16:55 +0200 Subject: [PATCH 105/130] Display a banner instead of the composer when user cannot post message to a room (#602) --- .../messages/impl/MessagesPresenter.kt | 13 +++++ .../features/messages/impl/MessagesState.kt | 1 + .../messages/impl/MessagesStateProvider.kt | 2 + .../features/messages/impl/MessagesView.kt | 43 ++++++++++++-- .../impl/src/main/res/values/localazy.xml | 5 +- .../messages/MessagesPresenterTest.kt | 29 +++++++++- .../libraries/matrix/api/room/MatrixRoom.kt | 2 + .../matrix/api/room/MessageEventType.kt | 36 ++++++++++++ .../matrix/impl/room/MessageEventType.kt | 58 +++++++++++++++++++ .../matrix/impl/room/RustMatrixRoom.kt | 10 +++- .../matrix/test/room/FakeMatrixRoom.kt | 10 ++++ 11 files changed, 199 insertions(+), 10 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 089f0e79cb..d78025dea0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -19,6 +19,7 @@ package io.element.android.features.messages.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -55,6 +56,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.designsystem.utils.handleSnackbarMessage import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType import io.element.android.libraries.textcomposer.MessageComposerMode @@ -86,6 +88,7 @@ class MessagesPresenter @Inject constructor( val retryState = retrySendMenuPresenter.present() val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L) + val userHasPermissionToSendMessage by getCanSendEvent(MessageEventType.ROOM_MESSAGE) val roomName: MutableState = rememberSaveable { mutableStateOf(null) } @@ -125,6 +128,7 @@ class MessagesPresenter @Inject constructor( roomId = room.roomId, roomName = roomName.value, roomAvatar = roomAvatar.value, + userHasPermissionToSendMessage = userHasPermissionToSendMessage, composerState = composerState, timelineState = timelineState, actionListState = actionListState, @@ -218,4 +222,13 @@ class MessagesPresenter @Inject constructor( MessageComposerEvents.SetMode(composerMode) ) } + + @Composable + private fun getCanSendEvent(type: MessageEventType): State { + val canSendEvent = remember(type) { mutableStateOf(false) } + LaunchedEffect(type) { + canSendEvent.value = room.canSendEvent(type).getOrElse { false } + } + return canSendEvent + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt index 6e29eefe08..38c9101c9e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt @@ -31,6 +31,7 @@ data class MessagesState( val roomId: RoomId, val roomName: String?, val roomAvatar: AvatarData?, + val userHasPermissionToSendMessage: Boolean, val composerState: MessageComposerState, val timelineState: TimelineState, val actionListState: ActionListState, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt index 4ed43315cb..7691788e58 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt @@ -35,6 +35,7 @@ open class MessagesStateProvider : PreviewParameterProvider { aMessagesState(), aMessagesState().copy(hasNetworkConnection = false), aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)), + aMessagesState().copy(userHasPermissionToSendMessage = false), ) } @@ -42,6 +43,7 @@ fun aMessagesState() = MessagesState( roomId = RoomId("!id:domain"), roomName = "Room name", roomAvatar = AvatarData("!id:domain", "Room name"), + userHasPermissionToSendMessage = true, composerState = aMessageComposerState().copy( text = StableCharSequence("Hello"), isFullScreen = false, diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 0687d46787..93b74806d6 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -16,7 +16,9 @@ package io.element.android.features.messages.impl +import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.Row @@ -34,6 +36,7 @@ import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHost import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -41,7 +44,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -231,12 +236,16 @@ fun MessagesViewContent( onTimestampClicked = onTimestampClicked, ) } - MessageComposerView( - state = state.composerState, - modifier = Modifier - .fillMaxWidth() - .wrapContentHeight(Alignment.Bottom) - ) + if (state.userHasPermissionToSendMessage) { + MessageComposerView( + state = state.composerState, + modifier = Modifier + .fillMaxWidth() + .wrapContentHeight(Alignment.Bottom) + ) + } else { + CantSendMessageBanner() + } } } @@ -281,6 +290,28 @@ fun MessagesViewTopBar( ) } +@Composable +fun CantSendMessageBanner( + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.secondary) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(id = R.string.screen_room_no_permission_to_post), + color = MaterialTheme.colorScheme.onSecondary, + style = MaterialTheme.typography.bodyMedium, + textAlign = TextAlign.Center, + fontStyle = FontStyle.Italic, + ) + } +} + @Preview @Composable internal fun MessagesViewLightPreview(@PreviewParameter(MessagesStateProvider::class) state: MessagesState) = diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 16672f15ec..557b6ccd90 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -10,8 +10,11 @@ "Attachment" "Photo & Video Library" "Could not retrieve user details" + "Would you like to invite them back?" + "You are alone in this chat" + "You do not have permission to post to this room" "Send again" "Your message failed to send" "Failed processing media to upload, please try again." "Remove" - \ No newline at end of file + diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 1e03f60c88..4a71ec5546 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -46,6 +46,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -166,7 +167,7 @@ class MessagesPresenterTest { content = TimelineItemImageContent( body = "image.jpg", mediaSource = MediaSource(AN_AVATAR_URL), - thumbnailSource = null, + thumbnailSource = null, mimeType = MimeTypes.Jpeg, blurhash = null, width = 20, @@ -318,6 +319,32 @@ class MessagesPresenterTest { } } + @Test + fun `present - permission to post`() = runTest { + val matrixRoom = FakeMatrixRoom() + matrixRoom.givenCanSendEventResult(MessageEventType.ROOM_MESSAGE, Result.success(true)) + val presenter = createMessagePresenter(matrixRoom = matrixRoom) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + assertThat(awaitItem().userHasPermissionToSendMessage).isTrue() + } + } + + @Test + fun `present - no permission to post`() = runTest { + val matrixRoom = FakeMatrixRoom() + matrixRoom.givenCanSendEventResult(MessageEventType.ROOM_MESSAGE, Result.success(false)) + val presenter = createMessagePresenter(matrixRoom = matrixRoom) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + skipItems(1) + assertThat(awaitItem().userHasPermissionToSendMessage).isFalse() + } + } + private fun TestScope.createMessagePresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), matrixRoom: MatrixRoom = FakeMatrixRoom() diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 0ddd75966b..c31db970e0 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -99,6 +99,8 @@ interface MatrixRoom : Closeable { suspend fun canSendStateEvent(type: StateEventType): Result + suspend fun canSendEvent(type: MessageEventType): Result + suspend fun updateAvatar(mimeType: String, data: ByteArray): Result suspend fun removeAvatar(): Result diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.kt new file mode 100644 index 0000000000..109e50e602 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MessageEventType.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.libraries.matrix.api.room + +enum class MessageEventType { + CALL_ANSWER, + CALL_INVITE, + CALL_HANGUP, + CALL_CANDIDATES, + KEY_VERIFICATION_READY, + KEY_VERIFICATION_START, + KEY_VERIFICATION_CANCEL, + KEY_VERIFICATION_ACCEPT, + KEY_VERIFICATION_KEY, + KEY_VERIFICATION_MAC, + KEY_VERIFICATION_DONE, + REACTION_SENT, + ROOM_ENCRYPTED, + ROOM_MESSAGE, + ROOM_REDACTION, + STICKER +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.kt new file mode 100644 index 0000000000..a117c1d313 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/MessageEventType.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.libraries.matrix.impl.room + +import io.element.android.libraries.matrix.api.room.MessageEventType +import org.matrix.rustcomponents.sdk.MessageLikeEventType + +fun MessageEventType.map(): MessageLikeEventType = when (this) { + MessageEventType.CALL_ANSWER -> MessageLikeEventType.CALL_ANSWER + MessageEventType.CALL_INVITE -> MessageLikeEventType.CALL_INVITE + MessageEventType.CALL_HANGUP -> MessageLikeEventType.CALL_HANGUP + MessageEventType.CALL_CANDIDATES -> MessageLikeEventType.CALL_CANDIDATES + MessageEventType.KEY_VERIFICATION_READY -> MessageLikeEventType.KEY_VERIFICATION_READY + MessageEventType.KEY_VERIFICATION_START -> MessageLikeEventType.KEY_VERIFICATION_START + MessageEventType.KEY_VERIFICATION_CANCEL -> MessageLikeEventType.KEY_VERIFICATION_CANCEL + MessageEventType.KEY_VERIFICATION_ACCEPT -> MessageLikeEventType.KEY_VERIFICATION_ACCEPT + MessageEventType.KEY_VERIFICATION_KEY -> MessageLikeEventType.KEY_VERIFICATION_KEY + MessageEventType.KEY_VERIFICATION_MAC -> MessageLikeEventType.KEY_VERIFICATION_MAC + MessageEventType.KEY_VERIFICATION_DONE -> MessageLikeEventType.KEY_VERIFICATION_DONE + MessageEventType.REACTION_SENT -> MessageLikeEventType.REACTION_SENT + MessageEventType.ROOM_ENCRYPTED -> MessageLikeEventType.ROOM_ENCRYPTED + MessageEventType.ROOM_MESSAGE -> MessageLikeEventType.ROOM_MESSAGE + MessageEventType.ROOM_REDACTION -> MessageLikeEventType.ROOM_REDACTION + MessageEventType.STICKER -> MessageLikeEventType.STICKER +} + +fun MessageLikeEventType.map(): MessageEventType = when (this) { + MessageLikeEventType.CALL_ANSWER -> MessageEventType.CALL_ANSWER + MessageLikeEventType.CALL_INVITE -> MessageEventType.CALL_INVITE + MessageLikeEventType.CALL_HANGUP -> MessageEventType.CALL_HANGUP + MessageLikeEventType.CALL_CANDIDATES -> MessageEventType.CALL_CANDIDATES + MessageLikeEventType.KEY_VERIFICATION_READY -> MessageEventType.KEY_VERIFICATION_READY + MessageLikeEventType.KEY_VERIFICATION_START -> MessageEventType.KEY_VERIFICATION_START + MessageLikeEventType.KEY_VERIFICATION_CANCEL -> MessageEventType.KEY_VERIFICATION_CANCEL + MessageLikeEventType.KEY_VERIFICATION_ACCEPT -> MessageEventType.KEY_VERIFICATION_ACCEPT + MessageLikeEventType.KEY_VERIFICATION_KEY -> MessageEventType.KEY_VERIFICATION_KEY + MessageLikeEventType.KEY_VERIFICATION_MAC -> MessageEventType.KEY_VERIFICATION_MAC + MessageLikeEventType.KEY_VERIFICATION_DONE -> MessageEventType.KEY_VERIFICATION_DONE + MessageLikeEventType.REACTION_SENT -> MessageEventType.REACTION_SENT + MessageLikeEventType.ROOM_ENCRYPTED -> MessageEventType.ROOM_ENCRYPTED + MessageLikeEventType.ROOM_MESSAGE -> MessageEventType.ROOM_MESSAGE + MessageLikeEventType.ROOM_REDACTION -> MessageEventType.ROOM_REDACTION + MessageLikeEventType.STICKER -> MessageEventType.STICKER +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 8452ef193b..a0cfd24c05 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.MatrixTimeline @@ -34,7 +35,6 @@ import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.services.toolbox.api.systemclock.SystemClock import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -235,6 +235,12 @@ class RustMatrixRoom( } } + override suspend fun canSendEvent(type: MessageEventType): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.member(sessionId.value).use { it.canSendMessage(type.map()) } + } + } + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = withContext(coroutineDispatchers.io) { runCatching { innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map()) @@ -272,7 +278,7 @@ class RustMatrixRoom( } } - override suspend fun cancelSend(transactionId: String): Result = + override suspend fun cancelSend(transactionId: String): Result = withContext(coroutineDispatchers.io) { runCatching { innerRoom.cancelSend(transactionId) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index b6d2f102af..912f0c629d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState +import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -64,6 +65,7 @@ class FakeMatrixRoom( private var inviteUserResult = Result.success(Unit) private var canInviteResult = Result.success(true) private val canSendStateResults = mutableMapOf>() + private val canSendEventResults = mutableMapOf>() private var sendMediaResult = Result.success(Unit) private var setNameResult = Result.success(Unit) private var setTopicResult = Result.success(Unit) @@ -198,6 +200,10 @@ class FakeMatrixRoom( return canSendStateResults[type] ?: Result.failure(IllegalStateException("No fake answer")) } + override suspend fun canSendEvent(type: MessageEventType): Result { + return canSendEventResults[type] ?: Result.failure(IllegalStateException("No fake answer")) + } + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = fakeSendMedia() override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = fakeSendMedia() @@ -274,6 +280,10 @@ class FakeMatrixRoom( canSendStateResults[type] = result } + fun givenCanSendEventResult(type: MessageEventType, result: Result) { + canSendEventResults[type] = result + } + fun givenIgnoreResult(result: Result) { ignoreResult = result } From 9dfaa62f428423a940b8c0d69aa4a32807468b4e Mon Sep 17 00:00:00 2001 From: ElementBot Date: Mon, 19 Jun 2023 10:00:45 +0000 Subject: [PATCH 106/130] Update screenshots --- ...tGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 +++ ...Group_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 +++ 2 files changed, 6 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_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.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..77ae334920 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f145f138983fce06084866f28e14e6babe6b242ea40f89a7544a8c2580020249 +size 49121 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_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.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..84d4bfb339 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eb2aa0a032dcd1597fd2e7971f47a9025f41f4fb3bf29fac709dd6d33a0a2cf8 +size 48551 From 3f7738c00175769bb05fb05270eea778ea26149f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Jun 2023 11:18:01 +0000 Subject: [PATCH 107/130] Sync Strings (#630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Sync Strings from Localazy * Update strings, remove broken German translation * Update screenshots --------- Co-authored-by: bmarty Co-authored-by: Jorge Martín Co-authored-by: ElementBot --- .../src/main/res/values-cs/translations.xml | 10 +++++++++ .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 6 ++--- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 1 + .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 22 ++++++++++++++++++- .../src/main/res/values-de/translations.xml | 21 +++++++++++++++++- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 22 ++++++++++++++++++- .../impl/src/main/res/values/localazy.xml | 1 + .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../api/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 8 ++++++- .../src/main/res/values-de/translations.xml | 7 +++++- .../src/main/res/values-es/translations.xml | 3 ++- .../src/main/res/values-fr/translations.xml | 3 ++- .../src/main/res/values-it/translations.xml | 3 ++- .../src/main/res/values-ro/translations.xml | 4 +++- .../impl/src/main/res/values/localazy.xml | 5 ++++- .../src/main/res/values-cs/translations.xml | 4 ++-- .../src/main/res/values-de/translations.xml | 6 +++-- .../src/main/res/values-es/translations.xml | 4 ++-- .../src/main/res/values-fr/translations.xml | 4 ++-- .../src/main/res/values-it/translations.xml | 4 ++-- .../src/main/res/values-ro/translations.xml | 8 +++++-- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../api/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 4 +++- .../src/main/res/values-de/translations.xml | 7 +++++- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 3 ++- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 5 ++++- .../impl/src/main/res/values/localazy.xml | 3 ++- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../impl/src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 4 +++- .../src/main/res/values-de/translations.xml | 3 ++- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 3 ++- .../impl/src/main/res/values/localazy.xml | 5 +++-- .../src/main/res/values-cs/translations.xml | 2 +- .../src/main/res/values-de/translations.xml | 2 +- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 2 +- .../src/main/res/values/localazy.xml | 2 +- .../src/main/res/values-cs/translations.xml | 17 ++++++++++++-- .../src/main/res/values-de/translations.xml | 19 +++++++++++++++- .../src/main/res/values-es/translations.xml | 2 +- .../src/main/res/values-fr/translations.xml | 2 +- .../src/main/res/values-it/translations.xml | 2 +- .../src/main/res/values-ro/translations.xml | 16 ++++++++++++-- .../src/main/res/values/localazy.xml | 22 ++++++------------- ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...arkPreview--1_1_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...arkPreview--1_1_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ghtPreview--0_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ghtPreview--0_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- 115 files changed, 290 insertions(+), 143 deletions(-) create mode 100644 features/analytics/impl/src/main/res/values-cs/translations.xml diff --git a/features/analytics/impl/src/main/res/values-cs/translations.xml b/features/analytics/impl/src/main/res/values-cs/translations.xml new file mode 100644 index 0000000000..b75b359216 --- /dev/null +++ b/features/analytics/impl/src/main/res/values-cs/translations.xml @@ -0,0 +1,10 @@ + + + "Nezaznamenáváme ani neprofilujeme žádné údaje o účtu" + "Sdílejte anonymní údaje o používání, které nám pomohou identifikovat problémy." + "Můžete si přečíst všechny naše podmínky %1$s." + "zde" + "Tuto funkci můžete kdykoli vypnout" + "Nesdílíme informace s třetími stranami" + "Pomozte vylepšit %1$s" + diff --git a/features/analytics/impl/src/main/res/values-de/translations.xml b/features/analytics/impl/src/main/res/values-de/translations.xml index e0b033f68e..fc79b4297a 100644 --- a/features/analytics/impl/src/main/res/values-de/translations.xml +++ b/features/analytics/impl/src/main/res/values-de/translations.xml @@ -6,4 +6,4 @@ "Sie können die Analyse jederzeit in den Einstellungen deaktivieren" "Wir geben ""keine"" Informationen an Dritte weiter" "Helfen Sie %1$s zu verbessern" - \ No newline at end of file + diff --git a/features/analytics/impl/src/main/res/values-ro/translations.xml b/features/analytics/impl/src/main/res/values-ro/translations.xml index c46378e6da..f4db2e7416 100644 --- a/features/analytics/impl/src/main/res/values-ro/translations.xml +++ b/features/analytics/impl/src/main/res/values-ro/translations.xml @@ -6,4 +6,4 @@ "Puteți dezactiva această opțiune oricând din setări" "Nu"" împărtășim informații cu terți" "Ajutați la îmbunătățirea %1$s" - \ No newline at end of file + diff --git a/features/analytics/impl/src/main/res/values/localazy.xml b/features/analytics/impl/src/main/res/values/localazy.xml index d6083c860d..a496bdd0c6 100644 --- a/features/analytics/impl/src/main/res/values/localazy.xml +++ b/features/analytics/impl/src/main/res/values/localazy.xml @@ -7,4 +7,4 @@ "You can turn this off anytime" "We won\'t share your data with third parties" "Help improve %1$s" - \ No newline at end of file + diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml index 9a3030ba71..9a5d672fd4 100644 --- a/features/createroom/impl/src/main/res/values-es/translations.xml +++ b/features/createroom/impl/src/main/res/values-es/translations.xml @@ -5,4 +5,4 @@ "Añadir personas" "Se ha producido un error al intentar iniciar un chat" "Crear una sala" - \ No newline at end of file + diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml index 214cd9406d..ceddb71154 100644 --- a/features/createroom/impl/src/main/res/values-it/translations.xml +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -5,4 +5,4 @@ "Aggiungi persone" "Si è verificato un errore durante il tentativo di avviare una chat" "Crea una stanza" - \ No newline at end of file + diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml index 90a2e4f7f4..9f68a006e5 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -1,8 +1,8 @@ "Cameră nouă" - "Invitați persoane" - "Adaugați persoane" + "Invitați prieteni în Element" + "Invitați persoane" "A apărut o eroare la crearea camerei" "Mesajele din această cameră sunt criptate. Criptarea nu poate fi dezactivată ulterior." "Cameră privată (doar pe bază de invitație)" @@ -12,4 +12,4 @@ "Subiect (opțional)" "A apărut o eroare la încercarea începerii conversației" "Creați o cameră" - \ No newline at end of file + diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml index 0b6d87b8b8..68f318d385 100644 --- a/features/createroom/impl/src/main/res/values/localazy.xml +++ b/features/createroom/impl/src/main/res/values/localazy.xml @@ -12,4 +12,4 @@ "Topic (optional)" "An error occurred when trying to start a chat" "Create a room" - \ No newline at end of file + diff --git a/features/invitelist/impl/src/main/res/values-cs/translations.xml b/features/invitelist/impl/src/main/res/values-cs/translations.xml index b1f64dc0b4..9c0f592297 100644 --- a/features/invitelist/impl/src/main/res/values-cs/translations.xml +++ b/features/invitelist/impl/src/main/res/values-cs/translations.xml @@ -6,4 +6,4 @@ "Odmítnout chat" "Žádné pozvánky" "%1$s (%2$s) vás pozval(a)" - \ No newline at end of file + diff --git a/features/invitelist/impl/src/main/res/values-de/translations.xml b/features/invitelist/impl/src/main/res/values-de/translations.xml index 571e1da4a0..1e2fcc2e86 100644 --- a/features/invitelist/impl/src/main/res/values-de/translations.xml +++ b/features/invitelist/impl/src/main/res/values-de/translations.xml @@ -5,4 +5,5 @@ "Möchten Sie den Chat mit %1$s wirklich ablehnen?" "Chat ablehnen" "Keine Einladungen" + "%1$s (%2$s) hat dich eingeladen" diff --git a/features/invitelist/impl/src/main/res/values/localazy.xml b/features/invitelist/impl/src/main/res/values/localazy.xml index 6d52033110..966b56a625 100644 --- a/features/invitelist/impl/src/main/res/values/localazy.xml +++ b/features/invitelist/impl/src/main/res/values/localazy.xml @@ -6,4 +6,4 @@ "Decline chat" "No Invites" "%1$s (%2$s) invited you" - \ No newline at end of file + diff --git a/features/login/impl/src/main/res/values-cs/translations.xml b/features/login/impl/src/main/res/values-cs/translations.xml index 03ef5b6868..300d851693 100644 --- a/features/login/impl/src/main/res/values-cs/translations.xml +++ b/features/login/impl/src/main/res/values-cs/translations.xml @@ -1,5 +1,19 @@ + "Změna poskytovatele účtu" + "Pokračovat" + "Adresa domovského serveru" + "Zadejte hledaný výraz nebo adresu domény." + "Vyhledejte společnost, komunitu nebo soukromý server." + "Najít poskytovatele účtu" + "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 %s" + "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 vytvořit účet na %s" + "Matrix.org je otevřená síť pro bezpečnou, decentralizovanou komunikaci." + "Jiný" + "Použijte jiného poskytovatele účtu, například vlastní soukromý server nebo pracovní účet." + "Změnit poskytovatele účtu" "Nepodařilo se nám připojit k tomuto domovskému serveru. Zkontrolujte prosím, zda jste správně zadali adresu URL domovského serveru. Pokud je adresa URL správná, obraťte se na správce domovského serveru, který vám poskytne další pomoc." "Tento server v současné době nepodporuje klouzavou synchronizaci." "Adresa URL domovského serveru" @@ -13,9 +27,15 @@ "Kde budou vaše konverzace probíhat" "Vítejte zpět!" "Přihlaste se k %1$s" + "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" "Pokračovat" "Vyberte svůj server" "Heslo" "Pokračovat" "Uživatelské jméno" - \ No newline at end of file + diff --git a/features/login/impl/src/main/res/values-de/translations.xml b/features/login/impl/src/main/res/values-de/translations.xml index df6d1c38ab..d1393dbaf3 100644 --- a/features/login/impl/src/main/res/values-de/translations.xml +++ b/features/login/impl/src/main/res/values-de/translations.xml @@ -1,5 +1,18 @@ + "Kontoanbieter wechseln" + "Weiter" + "Adresse des Homeservers" + "Geben Sie einen Suchbegriff oder eine Domainadresse ein." + "Suche nach einem Unternehmen, einer Community oder einem privaten Server." + "Finde einen Accountanbieter" + "Du bist dabei dich bei %s anzumelden" + "Hier werden deine Konversationen stattfinden — genauso wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren." + "Du bist dabei einen Account auf %s zu erstellen" + "Matrix.org ist ein offenes Netzwerk für sichere, dezentralisierte Kommunikation." + "Andere" + "Verwende einen anderen Kontoanbieter, z. B. deinen eigenen privaten Server oder ein Arbeitskonto." + "Kontoanbieter ändern" "Wir konnten diesen Homeserver nicht erreichen. Bitte überprüfen Sie, ob Sie die Homeserver-URL korrekt eingegeben haben. Wenn die URL korrekt ist, wenden Sie sich an Ihren Homeserver-Administrator, um weitere Hilfe zu erhalten." "Dieser Server unterstützt derzeit keine Sliding Sync." "Homeserver-URL" @@ -13,9 +26,15 @@ "Wo deine Gespräche leben" "Willkommen zurück!" "Bei %1$s anmelden" + "Kontoanbieter wechseln" + "Ein privater Server für Element-Mitarbeiter." + "Matrix ist ein offenes Netzwerk für sichere, dezentrale Kommunikation" + "Hier werden deine Konversationen stattfinden — genau so wie du einen E-Mail-Anbieter verwenden würdest, um deine E-Mails aufzubewahren." + "Du bist dabei dich bei %1$s anzumelden" + "Du bist dabei einen Account auf %1$s zu erstellen" "Weiter" "Wählen deinen Server" "Passwort" "Weiter" "Benutzername" - \ No newline at end of file + diff --git a/features/login/impl/src/main/res/values-es/translations.xml b/features/login/impl/src/main/res/values-es/translations.xml index 284527c2f5..0e55589556 100644 --- a/features/login/impl/src/main/res/values-es/translations.xml +++ b/features/login/impl/src/main/res/values-es/translations.xml @@ -17,4 +17,4 @@ "Contraseña" "Continuar" "Usuario" - \ No newline at end of file + diff --git a/features/login/impl/src/main/res/values-fr/translations.xml b/features/login/impl/src/main/res/values-fr/translations.xml index 9d8f50e979..d56836e360 100644 --- a/features/login/impl/src/main/res/values-fr/translations.xml +++ b/features/login/impl/src/main/res/values-fr/translations.xml @@ -17,4 +17,4 @@ "Mot de passe" "Continuer" "Nom d\'utilisateur" - \ No newline at end of file + diff --git a/features/login/impl/src/main/res/values-it/translations.xml b/features/login/impl/src/main/res/values-it/translations.xml index b11875a18e..feb74db373 100644 --- a/features/login/impl/src/main/res/values-it/translations.xml +++ b/features/login/impl/src/main/res/values-it/translations.xml @@ -17,4 +17,4 @@ "Password" "Continua" "Nome utente" - \ No newline at end of file + diff --git a/features/login/impl/src/main/res/values-ro/translations.xml b/features/login/impl/src/main/res/values-ro/translations.xml index 349e3ddc04..7d0c25b97e 100644 --- a/features/login/impl/src/main/res/values-ro/translations.xml +++ b/features/login/impl/src/main/res/values-ro/translations.xml @@ -1,5 +1,18 @@ + "Schimbați furnizorul contului" + "Continuați" + "Adresa Homeserver-ului" + "Introduceţi un termen de căutare sau o adresă de domeniu." + "Căutați o companie, o comunitate sau un server privat." + "Găsiți un furnizor de cont" + "Sunteți pe cale să vă conectați la %s" + "Aici vor trăi conversațiile - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." + "Sunteți pe cale să creați un cont pe %s" + "Matrix.org este o rețea deschisă pentru o comunicare sigură și descentralizată." + "Altul" + "Utilizați un alt furnizor de cont, cum ar fi propriul server privat sau un cont de serviciu." + "Schimbați furnizorul contului" "Nu am putut accesa acest homeserver. Te rugăm să verifici că ai introdus corect adresa URL a homeserver-ului. Dacă adresa URL este corectă, contactează administratorul homeserver-ului pentru ajutor suplimentar." "Momentan acest server nu oferă suport pentru sliding sync." "Adresa URL a homeserver-ului" @@ -12,9 +25,16 @@ "Introduceți detaliile" "Locul unde trăiesc conversațiile tale" "Bine ați revenit!" + "Conectați-vă la %1$s" + "Schimbați furnizorul contului" + "Un server privat pentru angajații Element." + "Matrix este o rețea deschisă pentru o comunicare sigură și descentralizată." + "Aici vor trăi conversațiile dvs. - la fel cum ați folosi un furnizor de e-mail pentru a vă păstra e-mailurile." + "Sunteți pe cale să vă conectați la %1$s" + "Sunteți pe cale să creați un cont pe %1$s" "Continuați" "Selectați serverul" "Parola" "Continuați" "Utilizator" - \ No newline at end of file + diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index bb27ca51e1..55324613ed 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -6,6 +6,7 @@ "Enter a search term or a domain address." "Search for a company, community, or private server." "Find an account provider" + "This is where you conversations will live — just like you would use an email provider to keep your emails." "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" diff --git a/features/logout/api/src/main/res/values-cs/translations.xml b/features/logout/api/src/main/res/values-cs/translations.xml index 31761ee2d9..20be439d90 100644 --- a/features/logout/api/src/main/res/values-cs/translations.xml +++ b/features/logout/api/src/main/res/values-cs/translations.xml @@ -5,4 +5,4 @@ "Odhlašování…" "Odhlásit se" "Odhlásit se" - \ No newline at end of file + diff --git a/features/logout/api/src/main/res/values-de/translations.xml b/features/logout/api/src/main/res/values-de/translations.xml index 5b9fac1031..0cd8ac389a 100644 --- a/features/logout/api/src/main/res/values-de/translations.xml +++ b/features/logout/api/src/main/res/values-de/translations.xml @@ -5,4 +5,4 @@ "Abmeldung läuft…" "Abmelden" "Abmelden" - \ No newline at end of file + diff --git a/features/logout/api/src/main/res/values-es/translations.xml b/features/logout/api/src/main/res/values-es/translations.xml index 8028039235..5ac0656935 100644 --- a/features/logout/api/src/main/res/values-es/translations.xml +++ b/features/logout/api/src/main/res/values-es/translations.xml @@ -5,4 +5,4 @@ "Cerrando sesión…" "Cerrar sesión" "Cerrar sesión" - \ No newline at end of file + diff --git a/features/logout/api/src/main/res/values-fr/translations.xml b/features/logout/api/src/main/res/values-fr/translations.xml index 9d9ad724df..b6d5137072 100644 --- a/features/logout/api/src/main/res/values-fr/translations.xml +++ b/features/logout/api/src/main/res/values-fr/translations.xml @@ -5,4 +5,4 @@ "Déconnexion en cours…" "Se déconnecter" "Se déconnecter" - \ No newline at end of file + diff --git a/features/logout/api/src/main/res/values-it/translations.xml b/features/logout/api/src/main/res/values-it/translations.xml index 351a5e208b..4e8217a7f2 100644 --- a/features/logout/api/src/main/res/values-it/translations.xml +++ b/features/logout/api/src/main/res/values-it/translations.xml @@ -5,4 +5,4 @@ "Uscita in corso…" "Esci" "Esci" - \ No newline at end of file + diff --git a/features/logout/api/src/main/res/values-ro/translations.xml b/features/logout/api/src/main/res/values-ro/translations.xml index bb1e36b426..4b2c7fbe7b 100644 --- a/features/logout/api/src/main/res/values-ro/translations.xml +++ b/features/logout/api/src/main/res/values-ro/translations.xml @@ -5,4 +5,4 @@ "Deconectare în curs…" "Deconectați-vă" "Deconectați-vă" - \ No newline at end of file + diff --git a/features/logout/api/src/main/res/values/localazy.xml b/features/logout/api/src/main/res/values/localazy.xml index 514c002567..9ea4bb77fd 100644 --- a/features/logout/api/src/main/res/values/localazy.xml +++ b/features/logout/api/src/main/res/values/localazy.xml @@ -5,4 +5,4 @@ "Signing out…" "Sign out" "Sign out" - \ No newline at end of file + diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index c1199ddf39..c684fe079e 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -10,5 +10,11 @@ "Natočit video" "Příloha" "Knihovna fotografií a videí" + "Nepodařilo se načíst údaje o uživateli" + "Chtěli byste je pozvat zpět?" + "V tomto chatu jste sami" + "Odeslat znovu" + "Vaši zprávu se nepodařilo odeslat" "Nahrání média se nezdařilo, zkuste to prosím znovu." - \ No newline at end of file + "Odstranit" + diff --git a/features/messages/impl/src/main/res/values-de/translations.xml b/features/messages/impl/src/main/res/values-de/translations.xml index 78088386ad..b8811cb203 100644 --- a/features/messages/impl/src/main/res/values-de/translations.xml +++ b/features/messages/impl/src/main/res/values-de/translations.xml @@ -9,4 +9,9 @@ "Video aufnehmen" "Anhang" "Foto- & Video-Bibliothek" - \ No newline at end of file + "Benutzerdetails konnten nicht abgerufen werden" + "Erneut senden" + "Ihre Nachricht konnte nicht gesendet werden" + "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuchen Sie es erneut." + "Entfernen" + diff --git a/features/messages/impl/src/main/res/values-es/translations.xml b/features/messages/impl/src/main/res/values-es/translations.xml index 7cd4b6e764..5d41b319bd 100644 --- a/features/messages/impl/src/main/res/values-es/translations.xml +++ b/features/messages/impl/src/main/res/values-es/translations.xml @@ -4,4 +4,5 @@ "%1$d cambio en la sala" "%1$d cambios en la sala" - \ No newline at end of file + "Eliminar" + diff --git a/features/messages/impl/src/main/res/values-fr/translations.xml b/features/messages/impl/src/main/res/values-fr/translations.xml index 2eb6016f2f..8a873e08f9 100644 --- a/features/messages/impl/src/main/res/values-fr/translations.xml +++ b/features/messages/impl/src/main/res/values-fr/translations.xml @@ -5,4 +5,5 @@ "%1$d changements dans la conversation" "Prendre une photo" - \ No newline at end of file + "Supprimer" + diff --git a/features/messages/impl/src/main/res/values-it/translations.xml b/features/messages/impl/src/main/res/values-it/translations.xml index 649a91405b..694de002fe 100644 --- a/features/messages/impl/src/main/res/values-it/translations.xml +++ b/features/messages/impl/src/main/res/values-it/translations.xml @@ -4,4 +4,5 @@ "%1$d modifica alla stanza" "%1$d modifiche alla stanza" - \ No newline at end of file + "Rimuovi" + diff --git a/features/messages/impl/src/main/res/values-ro/translations.xml b/features/messages/impl/src/main/res/values-ro/translations.xml index 0a2aa20456..f8ed777638 100644 --- a/features/messages/impl/src/main/res/values-ro/translations.xml +++ b/features/messages/impl/src/main/res/values-ro/translations.xml @@ -10,5 +10,7 @@ "Înregistrați un videoclip" "Atașament" "Bibliotecă foto și video" + "Nu am putut găsi detaliile utilizatorului" "Procesarea datelor media a eșuat, vă rugăm să încercați din nou." - \ No newline at end of file + "Ștergeți" + diff --git a/features/messages/impl/src/main/res/values/localazy.xml b/features/messages/impl/src/main/res/values/localazy.xml index 16672f15ec..557b6ccd90 100644 --- a/features/messages/impl/src/main/res/values/localazy.xml +++ b/features/messages/impl/src/main/res/values/localazy.xml @@ -10,8 +10,11 @@ "Attachment" "Photo & Video Library" "Could not retrieve user details" + "Would you like to invite them back?" + "You are alone in this chat" + "You do not have permission to post to this room" "Send again" "Your message failed to send" "Failed processing media to upload, please try again." "Remove" - \ No newline at end of file + diff --git a/features/onboarding/impl/src/main/res/values-cs/translations.xml b/features/onboarding/impl/src/main/res/values-cs/translations.xml index 279037e455..176c446673 100644 --- a/features/onboarding/impl/src/main/res/values-cs/translations.xml +++ b/features/onboarding/impl/src/main/res/values-cs/translations.xml @@ -4,6 +4,6 @@ "Přihlásit se pomocí QR kódu" "Vytvořit účet" "Komunikujte a spolupracujte bezpečně" - "Vítejte v %1$s Beta. Vylepšený, pro rychlost a jednoduchost." + "Vítejte v %1$s. Vylepšený, pro rychlost a jednoduchost." "Buďte ve svém živlu" - \ No newline at end of file + diff --git a/features/onboarding/impl/src/main/res/values-de/translations.xml b/features/onboarding/impl/src/main/res/values-de/translations.xml index da5178aaf2..c6d934cbf2 100644 --- a/features/onboarding/impl/src/main/res/values-de/translations.xml +++ b/features/onboarding/impl/src/main/res/values-de/translations.xml @@ -1,7 +1,9 @@ + "Manuell anmelden" "Mit QR-Code anmelden" "Konto erstellen" - "Willkommen zur %1$s Beta. Verbessert, für Geschwindigkeit und Einfachheit." + "Sicher kommunizieren und zusammenarbeiten" + "Willkommen zur %1$s. Verbessert, für Geschwindigkeit und Einfachheit." "Sei in deinem Element" - \ No newline at end of file + diff --git a/features/onboarding/impl/src/main/res/values-es/translations.xml b/features/onboarding/impl/src/main/res/values-es/translations.xml index 235fb4558a..2489344438 100644 --- a/features/onboarding/impl/src/main/res/values-es/translations.xml +++ b/features/onboarding/impl/src/main/res/values-es/translations.xml @@ -1,5 +1,5 @@ - "Bienvenido a la beta de %1$s. Vitaminado, para mayor rapidez y sencillez." + "Bienvenido a %1$s. Vitaminado, para mayor rapidez y sencillez." "Siéntente en tu Elemento" - \ No newline at end of file + diff --git a/features/onboarding/impl/src/main/res/values-fr/translations.xml b/features/onboarding/impl/src/main/res/values-fr/translations.xml index 502b464fa7..018bf21379 100644 --- a/features/onboarding/impl/src/main/res/values-fr/translations.xml +++ b/features/onboarding/impl/src/main/res/values-fr/translations.xml @@ -1,5 +1,5 @@ - "Bienvenue dans la version %1$s Beta. Affiné pour plus de rapidité et de simplicité." + "Bienvenue dans %1$s. Affiné pour plus de rapidité et de simplicité." "Soyez dans votre Element" - \ No newline at end of file + diff --git a/features/onboarding/impl/src/main/res/values-it/translations.xml b/features/onboarding/impl/src/main/res/values-it/translations.xml index cd3c6a696c..652d9e6c22 100644 --- a/features/onboarding/impl/src/main/res/values-it/translations.xml +++ b/features/onboarding/impl/src/main/res/values-it/translations.xml @@ -1,5 +1,5 @@ - "Benvenuto nella beta di %1$s. Potenziato in velocità e semplicità." + "Benvenuto su %1$s. Potenziato in velocità e semplicità." "Sii nel tuo elemento" - \ No newline at end of file + diff --git a/features/onboarding/impl/src/main/res/values-ro/translations.xml b/features/onboarding/impl/src/main/res/values-ro/translations.xml index 03d967ab75..13e5eb505b 100644 --- a/features/onboarding/impl/src/main/res/values-ro/translations.xml +++ b/features/onboarding/impl/src/main/res/values-ro/translations.xml @@ -1,5 +1,9 @@ - "Bun venit la versiunea beta a %1$s. Supraalimentat, pentru viteză și simplitate." + "Conectați-vă manual" + "Conectați-vă cu un cod QR" + "Creați un cont" + "Comunicați și colaborați în siguranță" + "Bun venit în %1$s. Supraalimentat, pentru viteză și simplitate." "Fii în Elementul tău" - \ No newline at end of file + diff --git a/features/onboarding/impl/src/main/res/values/localazy.xml b/features/onboarding/impl/src/main/res/values/localazy.xml index ef91de7541..bad5b524da 100644 --- a/features/onboarding/impl/src/main/res/values/localazy.xml +++ b/features/onboarding/impl/src/main/res/values/localazy.xml @@ -6,4 +6,4 @@ "Communicate and collaborate securely" "Welcome to %1$s. Supercharged, for speed and simplicity." "Be in your Element" - \ No newline at end of file + diff --git a/features/rageshake/api/src/main/res/values-cs/translations.xml b/features/rageshake/api/src/main/res/values-cs/translations.xml index 7ae9126f29..20d6f31ed0 100644 --- a/features/rageshake/api/src/main/res/values-cs/translations.xml +++ b/features/rageshake/api/src/main/res/values-cs/translations.xml @@ -2,4 +2,4 @@ "%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?" "Zdá se, že jste frustrovaně třásli telefonem. Chcete otevřít obrazovku pro nahlášení chyby?" - \ No newline at end of file + diff --git a/features/rageshake/api/src/main/res/values-de/translations.xml b/features/rageshake/api/src/main/res/values-de/translations.xml index 1633cd340e..f2446a4028 100644 --- a/features/rageshake/api/src/main/res/values-de/translations.xml +++ b/features/rageshake/api/src/main/res/values-de/translations.xml @@ -2,4 +2,4 @@ "%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?" "Du scheinst frustriert das Telefon zu schütteln. Möchtest du den Fehlerberichtsbildschirm öffnen?" - \ No newline at end of file + diff --git a/features/rageshake/api/src/main/res/values-es/translations.xml b/features/rageshake/api/src/main/res/values-es/translations.xml index 26ff483b91..597ec74260 100644 --- a/features/rageshake/api/src/main/res/values-es/translations.xml +++ b/features/rageshake/api/src/main/res/values-es/translations.xml @@ -2,4 +2,4 @@ "%1$s se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?" "Parece que sacudes el teléfono con frustración. ¿Quieres abrir la pantalla de informe de errores?" - \ No newline at end of file + diff --git a/features/rageshake/api/src/main/res/values-fr/translations.xml b/features/rageshake/api/src/main/res/values-fr/translations.xml index 32bdaf4685..455ab1daef 100644 --- a/features/rageshake/api/src/main/res/values-fr/translations.xml +++ b/features/rageshake/api/src/main/res/values-fr/translations.xml @@ -2,4 +2,4 @@ "%1$s a planté la dernière fois qu\'il a été utilisé. Souhaitez-vous partager un rapport de crash avec nous ?" "Vous semblez secouer le téléphone de frustration. Voulez-vous ouvrir le formulaire de rapport de problème ?" - \ No newline at end of file + diff --git a/features/rageshake/api/src/main/res/values-it/translations.xml b/features/rageshake/api/src/main/res/values-it/translations.xml index e6ef37d287..6d5e7a74c0 100644 --- a/features/rageshake/api/src/main/res/values-it/translations.xml +++ b/features/rageshake/api/src/main/res/values-it/translations.xml @@ -2,4 +2,4 @@ "%1$s si è chiuso inaspettatamente l\'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull\'arresto anomalo?" "Sembra che tu stia scuotendo il telefono per la frustrazione. Vuoi aprire la schermata di segnalazione dei problemi?" - \ No newline at end of file + diff --git a/features/rageshake/api/src/main/res/values-ro/translations.xml b/features/rageshake/api/src/main/res/values-ro/translations.xml index 17180d5145..2c89703deb 100644 --- a/features/rageshake/api/src/main/res/values-ro/translations.xml +++ b/features/rageshake/api/src/main/res/values-ro/translations.xml @@ -2,4 +2,4 @@ "%1$s s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?" "Se pare că scuturați telefonul de frustrare. Doriți să deschdeți ecranul de raportare a unei erori?" - \ No newline at end of file + diff --git a/features/rageshake/api/src/main/res/values/localazy.xml b/features/rageshake/api/src/main/res/values/localazy.xml index 112cc427ba..bb694f2d00 100644 --- a/features/rageshake/api/src/main/res/values/localazy.xml +++ b/features/rageshake/api/src/main/res/values/localazy.xml @@ -2,4 +2,4 @@ "%1$s crashed the last time it was used. Would you like to share a crash report with us?" "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" - \ No newline at end of file + diff --git a/features/rageshake/impl/src/main/res/values-cs/translations.xml b/features/rageshake/impl/src/main/res/values-cs/translations.xml index 5a037a7596..5863cba70e 100644 --- a/features/rageshake/impl/src/main/res/values-cs/translations.xml +++ b/features/rageshake/impl/src/main/res/values-cs/translations.xml @@ -11,4 +11,4 @@ "Odeslat snímek obrazovky" "Aby bylo možné zkontrolovat, zda věci fungují podle očekávání, budou s vaší zprávou odeslány protokoly. Tyto budou soukromé. Chcete-li pouze odeslat zprávu, vypněte toto nastavení." "%1$s havaroval při posledním použití. Chcete se s námi podělit o zprávu o selhání?" - \ No newline at end of file + diff --git a/features/rageshake/impl/src/main/res/values-de/translations.xml b/features/rageshake/impl/src/main/res/values-de/translations.xml index 8712bba1a0..437f5fff6f 100644 --- a/features/rageshake/impl/src/main/res/values-de/translations.xml +++ b/features/rageshake/impl/src/main/res/values-de/translations.xml @@ -11,4 +11,4 @@ "Bildschirmfoto senden" "Um zu überprüfen, ob alles wie vorgesehen funktioniert, werden Protokolle mit deiner Nachricht gesendet. Diese werden privat sein. Um nur Ihre Nachricht zu senden, schalte diese Einstellung aus." "%1$s ist bei der letzten Verwendung abgestürzt. Möchtest du uns einen Absturzbericht senden?" - \ No newline at end of file + diff --git a/features/rageshake/impl/src/main/res/values-es/translations.xml b/features/rageshake/impl/src/main/res/values-es/translations.xml index 0b1a374b97..4191f67596 100644 --- a/features/rageshake/impl/src/main/res/values-es/translations.xml +++ b/features/rageshake/impl/src/main/res/values-es/translations.xml @@ -11,4 +11,4 @@ "Enviar captura de pantalla" "Para comprobar que todo funciona correctamente, se enviarán registros de fallos con su mensaje. Serán privados. Para enviar sólo tu mensaje, desactiva esta opción." "%1$s se cerró inesperadamente la última vez que se lo usaste. ¿Quieres compartir un informe de error con nosotros?" - \ No newline at end of file + diff --git a/features/rageshake/impl/src/main/res/values-fr/translations.xml b/features/rageshake/impl/src/main/res/values-fr/translations.xml index e5e88975a6..84b3ad3386 100644 --- a/features/rageshake/impl/src/main/res/values-fr/translations.xml +++ b/features/rageshake/impl/src/main/res/values-fr/translations.xml @@ -11,4 +11,4 @@ "Envoyer une capture d’écran" "Pour vérifier que les choses fonctionnent comme prévu, les journaux seront envoyés avec votre message. Ceux-ci seront privées. Pour simplement envoyer votre message, désactivez ce paramètre." "%1$s a planté la dernière fois qu\'il a été utilisé. Souhaitez-vous partager un rapport de crash avec nous ?" - \ No newline at end of file + diff --git a/features/rageshake/impl/src/main/res/values-it/translations.xml b/features/rageshake/impl/src/main/res/values-it/translations.xml index c8a15eeedf..2c95849db0 100644 --- a/features/rageshake/impl/src/main/res/values-it/translations.xml +++ b/features/rageshake/impl/src/main/res/values-it/translations.xml @@ -11,4 +11,4 @@ "Invia istantanea schermo" "Per verificare che le cose funzionino come previsto, i log verranno inviati con il tuo messaggio. Questi saranno privati. Per inviare solo il tuo messaggio, disattiva questa impostazione." "%1$s si è chiuso inaspettatamente l\'ultima volta che è stato usato. Vuoi condividere con noi un rapporto sull\'arresto anomalo?" - \ No newline at end of file + diff --git a/features/rageshake/impl/src/main/res/values-ro/translations.xml b/features/rageshake/impl/src/main/res/values-ro/translations.xml index 6d2657bc0c..db0398c0db 100644 --- a/features/rageshake/impl/src/main/res/values-ro/translations.xml +++ b/features/rageshake/impl/src/main/res/values-ro/translations.xml @@ -11,4 +11,4 @@ "Trimiteți captură de ecran" "Pentru a verifica că lucrurile funcționează conform așteptărilor, log-uri vor fi trimise împreună cu mesajul. Acestea vor fi private. Pentru a trimite doar mesajul, dezactivați această setare." "%1$s s-a blocat ultima dată când a fost folosit. Doriți să ne trimiteți un raport?" - \ No newline at end of file + diff --git a/features/rageshake/impl/src/main/res/values/localazy.xml b/features/rageshake/impl/src/main/res/values/localazy.xml index 192666fc1c..75db33350f 100644 --- a/features/rageshake/impl/src/main/res/values/localazy.xml +++ b/features/rageshake/impl/src/main/res/values/localazy.xml @@ -11,4 +11,4 @@ "Send screenshot" "To check things work as intended, logs will be sent with your message. These will be private. To just send your message, turn off this setting." "%1$s crashed the last time it was used. Would you like to share a crash report with us?" - \ No newline at end of file + diff --git a/features/roomdetails/impl/src/main/res/values-cs/translations.xml b/features/roomdetails/impl/src/main/res/values-cs/translations.xml index fad77eb62f..745764cee9 100644 --- a/features/roomdetails/impl/src/main/res/values-cs/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-cs/translations.xml @@ -13,6 +13,8 @@ "Nelze aktualizovat místnost" "Zprávy jsou zabezpečeny zámky. Pouze vy a příjemci máte jedinečné klíče k jejich odemčení." "Šifrování zpráv povoleno" + "Pozvat lidi" + "Oznámení" "Název místnosti" "Sdílet místnost" "Aktualizace místnosti…" @@ -28,4 +30,4 @@ "Lidé" "Zabezpečení" "Téma" - \ No newline at end of file + diff --git a/features/roomdetails/impl/src/main/res/values-de/translations.xml b/features/roomdetails/impl/src/main/res/values-de/translations.xml index feb3c47866..0211825920 100644 --- a/features/roomdetails/impl/src/main/res/values-de/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-de/translations.xml @@ -9,9 +9,14 @@ "Bereits eingeladen" "Raum bearbeiten" "Wir konnten nicht alle Informationen für diesen Raum aktualisieren." + "Raum konnte nicht aktualisiert werden" "Nachrichten sind mit Schlössern gesichert. Nur du und der Empfänger haben die eindeutigen Schlüssel, um sie zu entsperren." "Nachrichtenverschlüsselung aktiviert" + "Personen einladen" + "Raumname" "Raum teilen" + "Aktualisiere Raum…" + "Ausstehend" "Raummitglieder" "Blockieren" "Blockierte Benutzer können dir keine Nachrichten senden und alle Nachrichten von ihnen werden ausgeblendet. Du kannst diese Aktion jederzeit rückgängig machen." @@ -23,4 +28,4 @@ "Personen" "Sicherheit" "Thema" - \ No newline at end of file + diff --git a/features/roomdetails/impl/src/main/res/values-es/translations.xml b/features/roomdetails/impl/src/main/res/values-es/translations.xml index 58c486d6c3..42bce4b756 100644 --- a/features/roomdetails/impl/src/main/res/values-es/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-es/translations.xml @@ -18,4 +18,4 @@ "Personas" "Seguridad" "Tema" - \ No newline at end of file + diff --git a/features/roomdetails/impl/src/main/res/values-fr/translations.xml b/features/roomdetails/impl/src/main/res/values-fr/translations.xml index 4c2296fd97..6037e57a7a 100644 --- a/features/roomdetails/impl/src/main/res/values-fr/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-fr/translations.xml @@ -6,6 +6,7 @@ "Les messages sont sécurisés par des verrous. Seuls vous et les destinataires possédez les clés uniques pour les déverrouiller." "Chiffrement des messages activé" + "Inviter des personnes" "Partager la salle" "Bloquer" "Les utilisateurs bloqués ne pourront pas vous envoyer de messages et tous leurs messages seront masqués. Vous pouvez annuler cette action à tout moment." @@ -17,4 +18,4 @@ "Personnes" "Sécurité" "Sujet" - \ No newline at end of file + diff --git a/features/roomdetails/impl/src/main/res/values-it/translations.xml b/features/roomdetails/impl/src/main/res/values-it/translations.xml index a2e61a329c..190eda82ee 100644 --- a/features/roomdetails/impl/src/main/res/values-it/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-it/translations.xml @@ -18,4 +18,4 @@ "Persone" "Sicurezza" "Oggetto" - \ No newline at end of file + diff --git a/features/roomdetails/impl/src/main/res/values-ro/translations.xml b/features/roomdetails/impl/src/main/res/values-ro/translations.xml index 93feec60c4..61599d1ceb 100644 --- a/features/roomdetails/impl/src/main/res/values-ro/translations.xml +++ b/features/roomdetails/impl/src/main/res/values-ro/translations.xml @@ -7,10 +7,13 @@ "Adăugare subiect" "Deja membru" "Deja invitat" + "Editați camera" "A apărut o eroare la actualizarea detaliilor camerei" + "Nu s-a putut actualiza camera" "Mesajele sunt securizate cu încuietori. Doar dumneavoastră și destinatarii aveți cheile unice pentru a le debloca." "Criptarea mesajelor este activată" "Invitați persoane" + "Numele camerei" "Partajați camera" "Se actualizează camera…" "În așteptare" @@ -25,4 +28,4 @@ "Persoane" "Securitate" "Subiect" - \ No newline at end of file + diff --git a/features/roomdetails/impl/src/main/res/values/localazy.xml b/features/roomdetails/impl/src/main/res/values/localazy.xml index 5fffafb51d..17e5a56825 100644 --- a/features/roomdetails/impl/src/main/res/values/localazy.xml +++ b/features/roomdetails/impl/src/main/res/values/localazy.xml @@ -13,6 +13,7 @@ "Messages are secured with locks. Only you and the recipients have the unique keys to unlock them." "Message encryption enabled" "Invite people" + "Notification" "Room name" "Share room" "Updating room…" @@ -28,4 +29,4 @@ "People" "Security" "Topic" - \ No newline at end of file + diff --git a/features/roomlist/impl/src/main/res/values-cs/translations.xml b/features/roomlist/impl/src/main/res/values-cs/translations.xml index a222367f09..d355d2c70c 100644 --- a/features/roomlist/impl/src/main/res/values-cs/translations.xml +++ b/features/roomlist/impl/src/main/res/values-cs/translations.xml @@ -4,4 +4,4 @@ "Všechny chaty" "Zdá se, že používáte nové zařízení. Ověřte přihlášení, abyste měli přístup k zašifrovaným zprávám." "Přístup k historii zpráv" - \ No newline at end of file + diff --git a/features/roomlist/impl/src/main/res/values-de/translations.xml b/features/roomlist/impl/src/main/res/values-de/translations.xml index ab61ed4c4e..2ed1cd0263 100644 --- a/features/roomlist/impl/src/main/res/values-de/translations.xml +++ b/features/roomlist/impl/src/main/res/values-de/translations.xml @@ -4,4 +4,4 @@ "Alle Chats" "Es sieht so aus, als ob du ein neues Gerät verwendest. Verifiziere, dass du es bist, um auf deine verschlüsselten Nachrichten zuzugreifen." "Greife auf deine Nachrichten-Historie zu" - \ No newline at end of file + diff --git a/features/roomlist/impl/src/main/res/values-es/translations.xml b/features/roomlist/impl/src/main/res/values-es/translations.xml index 7edd6192a1..899b1e2cac 100644 --- a/features/roomlist/impl/src/main/res/values-es/translations.xml +++ b/features/roomlist/impl/src/main/res/values-es/translations.xml @@ -4,4 +4,4 @@ "Todos los chats" "Parece que estás usando un nuevo dispositivo. Verifica que eres tú para acceder a tus mensajes cifrados." "Accede a tu historial de mensajes" - \ No newline at end of file + diff --git a/features/roomlist/impl/src/main/res/values-fr/translations.xml b/features/roomlist/impl/src/main/res/values-fr/translations.xml index 6a049a4e72..3f22122014 100644 --- a/features/roomlist/impl/src/main/res/values-fr/translations.xml +++ b/features/roomlist/impl/src/main/res/values-fr/translations.xml @@ -4,4 +4,4 @@ "Toutes les conversations" "Il semblerait que vous utilisiez un nouvel appareil. Vérifiez que vous êtes bien autorisé à accéder à vos messages cryptés." "Accédez à l\'historique de vos messages" - \ No newline at end of file + diff --git a/features/roomlist/impl/src/main/res/values-it/translations.xml b/features/roomlist/impl/src/main/res/values-it/translations.xml index 6bfb8baa0c..cbe93e52d9 100644 --- a/features/roomlist/impl/src/main/res/values-it/translations.xml +++ b/features/roomlist/impl/src/main/res/values-it/translations.xml @@ -4,4 +4,4 @@ "Tutte le conversazioni" "Sembra che tu stia utilizzando un nuovo dispositivo. Verifica di essere tu per accedere ai tuoi messaggi crittografati." "Accedi alla cronologia dei messaggi" - \ No newline at end of file + diff --git a/features/roomlist/impl/src/main/res/values-ro/translations.xml b/features/roomlist/impl/src/main/res/values-ro/translations.xml index 7401b30b82..b8ffc57090 100644 --- a/features/roomlist/impl/src/main/res/values-ro/translations.xml +++ b/features/roomlist/impl/src/main/res/values-ro/translations.xml @@ -4,4 +4,4 @@ "Toate conversatiile" "Se pare că folosiți un dispozitiv nou. Verificați-vă identitatea pentru acces la mesajele dumneavoastră criptate." "Accesați istoricul mesajelor" - \ No newline at end of file + diff --git a/features/roomlist/impl/src/main/res/values/localazy.xml b/features/roomlist/impl/src/main/res/values/localazy.xml index 613e6681ae..e18d4c9017 100644 --- a/features/roomlist/impl/src/main/res/values/localazy.xml +++ b/features/roomlist/impl/src/main/res/values/localazy.xml @@ -4,4 +4,4 @@ "All Chats" "Looks like you’re using a new device. Verify it’s you to access your encrypted messages." "Access your message history" - \ No newline at end of file + diff --git a/features/verifysession/impl/src/main/res/values-cs/translations.xml b/features/verifysession/impl/src/main/res/values-cs/translations.xml index 41f95d41fe..6bf8db5ac0 100644 --- a/features/verifysession/impl/src/main/res/values-cs/translations.xml +++ b/features/verifysession/impl/src/main/res/values-cs/translations.xml @@ -16,4 +16,4 @@ "Čekání na přijetí žádosti" "Ověření zrušeno" "Začít" - \ No newline at end of file + diff --git a/features/verifysession/impl/src/main/res/values-de/translations.xml b/features/verifysession/impl/src/main/res/values-de/translations.xml index 53479844b4..f5f149cfd9 100644 --- a/features/verifysession/impl/src/main/res/values-de/translations.xml +++ b/features/verifysession/impl/src/main/res/values-de/translations.xml @@ -16,4 +16,4 @@ "Warten auf die Annahme der Anfrage" "Verifizierung abgebrochen" "Starten" - \ No newline at end of file + diff --git a/features/verifysession/impl/src/main/res/values-es/translations.xml b/features/verifysession/impl/src/main/res/values-es/translations.xml index ccc656e845..386ecfc37c 100644 --- a/features/verifysession/impl/src/main/res/values-es/translations.xml +++ b/features/verifysession/impl/src/main/res/values-es/translations.xml @@ -16,4 +16,4 @@ "A la espera de aceptar la solicitud" "Verificación cancelada" "Comenzar" - \ No newline at end of file + diff --git a/features/verifysession/impl/src/main/res/values-fr/translations.xml b/features/verifysession/impl/src/main/res/values-fr/translations.xml index cd06c5db8b..71e8015827 100644 --- a/features/verifysession/impl/src/main/res/values-fr/translations.xml +++ b/features/verifysession/impl/src/main/res/values-fr/translations.xml @@ -16,4 +16,4 @@ "En attente d\'acceptation de la demande" "Vérification annulée" "Démarrer" - \ No newline at end of file + diff --git a/features/verifysession/impl/src/main/res/values-it/translations.xml b/features/verifysession/impl/src/main/res/values-it/translations.xml index 1bf0e87ea9..7a6765adbf 100644 --- a/features/verifysession/impl/src/main/res/values-it/translations.xml +++ b/features/verifysession/impl/src/main/res/values-it/translations.xml @@ -16,4 +16,4 @@ "In attesa di accettare la richiesta" "Verifica annullata" "Inizia" - \ No newline at end of file + diff --git a/features/verifysession/impl/src/main/res/values-ro/translations.xml b/features/verifysession/impl/src/main/res/values-ro/translations.xml index 3ad0de6e56..e392438bcd 100644 --- a/features/verifysession/impl/src/main/res/values-ro/translations.xml +++ b/features/verifysession/impl/src/main/res/values-ro/translations.xml @@ -16,4 +16,4 @@ "Se așteptă acceptarea cererii" "Verificare anulată" "Începeți" - \ No newline at end of file + diff --git a/features/verifysession/impl/src/main/res/values/localazy.xml b/features/verifysession/impl/src/main/res/values/localazy.xml index c217f0d2a4..67dc975128 100644 --- a/features/verifysession/impl/src/main/res/values/localazy.xml +++ b/features/verifysession/impl/src/main/res/values/localazy.xml @@ -16,4 +16,4 @@ "Waiting to accept request" "Verification cancelled" "Start" - \ No newline at end of file + diff --git a/libraries/androidutils/src/main/res/values-cs/translations.xml b/libraries/androidutils/src/main/res/values-cs/translations.xml index ab592fee1d..345812c6ff 100644 --- a/libraries/androidutils/src/main/res/values-cs/translations.xml +++ b/libraries/androidutils/src/main/res/values-cs/translations.xml @@ -1,4 +1,4 @@ "Nebyla nalezena žádná kompatibilní aplikace, která by tuto akci zpracovala." - \ No newline at end of file + diff --git a/libraries/androidutils/src/main/res/values-de/translations.xml b/libraries/androidutils/src/main/res/values-de/translations.xml index a34a5b393b..d30d83f831 100644 --- a/libraries/androidutils/src/main/res/values-de/translations.xml +++ b/libraries/androidutils/src/main/res/values-de/translations.xml @@ -1,4 +1,4 @@ "Keine kompatible App für diese Aktion gefunden." - \ No newline at end of file + diff --git a/libraries/androidutils/src/main/res/values-es/translations.xml b/libraries/androidutils/src/main/res/values-es/translations.xml index 80b2b88347..d95373265c 100644 --- a/libraries/androidutils/src/main/res/values-es/translations.xml +++ b/libraries/androidutils/src/main/res/values-es/translations.xml @@ -1,4 +1,4 @@ "No se encontró ninguna aplicación compatible con esta acción." - \ No newline at end of file + diff --git a/libraries/androidutils/src/main/res/values-fr/translations.xml b/libraries/androidutils/src/main/res/values-fr/translations.xml index d564c18817..b974766fce 100644 --- a/libraries/androidutils/src/main/res/values-fr/translations.xml +++ b/libraries/androidutils/src/main/res/values-fr/translations.xml @@ -1,4 +1,4 @@ "Aucune application compatible n\'a été trouvée pour gérer cette action." - \ No newline at end of file + diff --git a/libraries/androidutils/src/main/res/values-it/translations.xml b/libraries/androidutils/src/main/res/values-it/translations.xml index 03aaf3ffd1..fcafd9fe3f 100644 --- a/libraries/androidutils/src/main/res/values-it/translations.xml +++ b/libraries/androidutils/src/main/res/values-it/translations.xml @@ -1,4 +1,4 @@ "Non è stata trovata alcuna app compatibile per gestire questa azione." - \ No newline at end of file + diff --git a/libraries/androidutils/src/main/res/values-ro/translations.xml b/libraries/androidutils/src/main/res/values-ro/translations.xml index d2149227c5..eac7dd0285 100644 --- a/libraries/androidutils/src/main/res/values-ro/translations.xml +++ b/libraries/androidutils/src/main/res/values-ro/translations.xml @@ -1,4 +1,4 @@ "Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune." - \ No newline at end of file + diff --git a/libraries/androidutils/src/main/res/values/localazy.xml b/libraries/androidutils/src/main/res/values/localazy.xml index 0599c8922b..741c1b20ec 100644 --- a/libraries/androidutils/src/main/res/values/localazy.xml +++ b/libraries/androidutils/src/main/res/values/localazy.xml @@ -1,4 +1,4 @@ "No compatible app was found to handle this action." - \ No newline at end of file + diff --git a/libraries/eventformatter/impl/src/main/res/values-cs/translations.xml b/libraries/eventformatter/impl/src/main/res/values-cs/translations.xml index ebb2826b86..69179b1276 100644 --- a/libraries/eventformatter/impl/src/main/res/values-cs/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-cs/translations.xml @@ -54,4 +54,4 @@ "%1$s zrušil(a) vykázání %2$s" "Zrušili jste vykázání pro %1$s" "%1$s provedl(a) neznámou změnu svého členství" - \ No newline at end of file + diff --git a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml index 62854b7d46..0ca17bc4fe 100644 --- a/libraries/eventformatter/impl/src/main/res/values-de/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-de/translations.xml @@ -54,4 +54,4 @@ "%1$s hat %2$s entbannt" "Du hast %1$s entbannt" "%1$s hat eine unbekannte Änderung an seiner Mitgliedschaft vorgenommen" - \ No newline at end of file + diff --git a/libraries/eventformatter/impl/src/main/res/values-es/translations.xml b/libraries/eventformatter/impl/src/main/res/values-es/translations.xml index 701f56f41c..dc732d9e97 100644 --- a/libraries/eventformatter/impl/src/main/res/values-es/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-es/translations.xml @@ -54,4 +54,4 @@ "%1$s readmitió a %2$s" "Readmitiste a %1$s" "%1$s realizó un cambio desconocido en su membresía" - \ No newline at end of file + diff --git a/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml b/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml index bdf693e976..f69ea03050 100644 --- a/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-fr/translations.xml @@ -54,4 +54,4 @@ "%1$s a débanni %2$s" "Vous avez débanni %1$s" "%1$s a apporté une modification inconnue à son adhésion" - \ No newline at end of file + diff --git a/libraries/eventformatter/impl/src/main/res/values-it/translations.xml b/libraries/eventformatter/impl/src/main/res/values-it/translations.xml index 0380d802f4..2e2e914dfe 100644 --- a/libraries/eventformatter/impl/src/main/res/values-it/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-it/translations.xml @@ -54,4 +54,4 @@ "%1$s ha sbloccato %2$s" "Hai sbloccato %1$s" "%1$s ha apportato una modifica sconosciuta alla propria iscrizione" - \ No newline at end of file + diff --git a/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml b/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml index 2e3abf93d0..2586ad3cd2 100644 --- a/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml +++ b/libraries/eventformatter/impl/src/main/res/values-ro/translations.xml @@ -54,4 +54,4 @@ "%1$s a anulat interdicția pentru %2$s" "Ați anulat interdicția pentru %1$s" "%1$s a făcut o modificare necunoscută asupra calității sale de membru" - \ No newline at end of file + diff --git a/libraries/eventformatter/impl/src/main/res/values/localazy.xml b/libraries/eventformatter/impl/src/main/res/values/localazy.xml index 2fd4217cd4..03a13bd29b 100644 --- a/libraries/eventformatter/impl/src/main/res/values/localazy.xml +++ b/libraries/eventformatter/impl/src/main/res/values/localazy.xml @@ -54,4 +54,4 @@ "%1$s unbanned %2$s" "You unbanned %1$s" "%1$s made an unknown change to their membership" - \ No newline at end of file + diff --git a/libraries/push/impl/src/main/res/values-cs/translations.xml b/libraries/push/impl/src/main/res/values-cs/translations.xml index 88eec52772..23fa1fb379 100644 --- a/libraries/push/impl/src/main/res/values-cs/translations.xml +++ b/libraries/push/impl/src/main/res/values-cs/translations.xml @@ -7,8 +7,10 @@ "** Nepodařilo se odeslat - otevřete prosím místnost" "Vstoupit" "Odmítnout" + "Vás pozval(a) do chatu" "Nové zprávy" "Označit jako přečtené" + "Vás pozval(a) do místnosti" "Já" "Prohlížíte si oznámení! Klikněte na mě!" "%1$s: %2$s" @@ -51,4 +53,4 @@ "Služby Google" "Nebyly nalezeny žádné funkční služby Google Play. Oznámení nemusí fungovat správně." "Rychlá odpověď" - \ No newline at end of file + diff --git a/libraries/push/impl/src/main/res/values-de/translations.xml b/libraries/push/impl/src/main/res/values-de/translations.xml index be957bf68a..281e00765e 100644 --- a/libraries/push/impl/src/main/res/values-de/translations.xml +++ b/libraries/push/impl/src/main/res/values-de/translations.xml @@ -7,6 +7,7 @@ "** Senden fehlgeschlagen - bitte Raum öffnen" "Beitreten" "Ablehnen" + "Hat dich eingeladen" "Neue Nachrichten" "Als gelesen markieren" "Ich" @@ -45,4 +46,4 @@ "Google-Dienste" "Keine gültigen Google Play-Dienste gefunden. Benachrichtigungen funktionieren möglicherweise nicht richtig." "Schnellantwort" - \ No newline at end of file + diff --git a/libraries/push/impl/src/main/res/values-es/translations.xml b/libraries/push/impl/src/main/res/values-es/translations.xml index 31df508dc3..90be7669d7 100644 --- a/libraries/push/impl/src/main/res/values-es/translations.xml +++ b/libraries/push/impl/src/main/res/values-es/translations.xml @@ -1,4 +1,4 @@ "Respuesta rápida" - \ No newline at end of file + diff --git a/libraries/push/impl/src/main/res/values-fr/translations.xml b/libraries/push/impl/src/main/res/values-fr/translations.xml index 3d40a01065..3afbf200b2 100644 --- a/libraries/push/impl/src/main/res/values-fr/translations.xml +++ b/libraries/push/impl/src/main/res/values-fr/translations.xml @@ -45,4 +45,4 @@ "Services Google" "Aucun service Google Play valide n\'a été trouvé. Les notifications peuvent ne pas fonctionner correctement." "Réponse rapide" - \ No newline at end of file + diff --git a/libraries/push/impl/src/main/res/values-it/translations.xml b/libraries/push/impl/src/main/res/values-it/translations.xml index 32957fe2ce..1526b805d6 100644 --- a/libraries/push/impl/src/main/res/values-it/translations.xml +++ b/libraries/push/impl/src/main/res/values-it/translations.xml @@ -1,4 +1,4 @@ "Risposta rapida" - \ No newline at end of file + diff --git a/libraries/push/impl/src/main/res/values-ro/translations.xml b/libraries/push/impl/src/main/res/values-ro/translations.xml index d603c0b2f8..d389fd286c 100644 --- a/libraries/push/impl/src/main/res/values-ro/translations.xml +++ b/libraries/push/impl/src/main/res/values-ro/translations.xml @@ -7,6 +7,7 @@ "** Trimiterea eșuată - vă rugăm să deschideți camera" "Alăturați-vă" "Respingeți" + "v-a invitat." "Mesaje noi" "Marcați ca citit" "Eu" @@ -45,4 +46,4 @@ "Servicii Google" "Nu au fost găsite servicii Google Play valide. Este posibil ca notificările să nu funcționeze corect." "Raspuns rapid" - \ No newline at end of file + diff --git a/libraries/push/impl/src/main/res/values/localazy.xml b/libraries/push/impl/src/main/res/values/localazy.xml index 922d35b3e1..a32e6899c0 100644 --- a/libraries/push/impl/src/main/res/values/localazy.xml +++ b/libraries/push/impl/src/main/res/values/localazy.xml @@ -7,9 +7,10 @@ "** Failed to send - please open room" "Join" "Reject" - "invited you" + "Invited you to chat" "New Messages" "Mark as read" + "Invited you to join the room" "Me" "You are viewing the notification! Click me!" "%1$s: %2$s" @@ -46,4 +47,4 @@ "Google Services" "No valid Google Play Services found. Notifications may not work properly." "Quick reply" - \ No newline at end of file + diff --git a/libraries/textcomposer/src/main/res/values-cs/translations.xml b/libraries/textcomposer/src/main/res/values-cs/translations.xml index 461b52e3b4..8e0524b69a 100644 --- a/libraries/textcomposer/src/main/res/values-cs/translations.xml +++ b/libraries/textcomposer/src/main/res/values-cs/translations.xml @@ -14,4 +14,4 @@ "Přepnout číslovaný seznam" "Přepnout citaci" "Zrušit odsazení" - \ No newline at end of file + diff --git a/libraries/textcomposer/src/main/res/values-de/translations.xml b/libraries/textcomposer/src/main/res/values-de/translations.xml index a9f1b3366e..a28c784792 100644 --- a/libraries/textcomposer/src/main/res/values-de/translations.xml +++ b/libraries/textcomposer/src/main/res/values-de/translations.xml @@ -14,4 +14,4 @@ "Nummerierte Liste ein-/ausschalten" "Zitat umschalten" "Einrücken aufheben" - \ No newline at end of file + diff --git a/libraries/textcomposer/src/main/res/values-es/translations.xml b/libraries/textcomposer/src/main/res/values-es/translations.xml index e302765a58..606e3bde8e 100644 --- a/libraries/textcomposer/src/main/res/values-es/translations.xml +++ b/libraries/textcomposer/src/main/res/values-es/translations.xml @@ -14,4 +14,4 @@ "Lista numérica" "Cita" "Quitar sangría" - \ No newline at end of file + diff --git a/libraries/textcomposer/src/main/res/values-fr/translations.xml b/libraries/textcomposer/src/main/res/values-fr/translations.xml index 03143f5059..4b239c0f93 100644 --- a/libraries/textcomposer/src/main/res/values-fr/translations.xml +++ b/libraries/textcomposer/src/main/res/values-fr/translations.xml @@ -14,4 +14,4 @@ "Afficher une liste numérotée" "Afficher une citation" "Décaler vers la gauche" - \ No newline at end of file + diff --git a/libraries/textcomposer/src/main/res/values-it/translations.xml b/libraries/textcomposer/src/main/res/values-it/translations.xml index 54ca270f28..e3034e8dfe 100644 --- a/libraries/textcomposer/src/main/res/values-it/translations.xml +++ b/libraries/textcomposer/src/main/res/values-it/translations.xml @@ -14,4 +14,4 @@ "Attiva/disattiva elenco numerato" "Attiva/disattiva citazione" "Rientro a sinistra" - \ No newline at end of file + diff --git a/libraries/textcomposer/src/main/res/values-ro/translations.xml b/libraries/textcomposer/src/main/res/values-ro/translations.xml index b053e0ecaa..a7e1a7135c 100644 --- a/libraries/textcomposer/src/main/res/values-ro/translations.xml +++ b/libraries/textcomposer/src/main/res/values-ro/translations.xml @@ -14,4 +14,4 @@ "Comutați lista numerotată" "Aplicați citatul" "Dez-identare" - \ No newline at end of file + diff --git a/libraries/textcomposer/src/main/res/values/localazy.xml b/libraries/textcomposer/src/main/res/values/localazy.xml index 1eab778890..a94173c1d2 100644 --- a/libraries/textcomposer/src/main/res/values/localazy.xml +++ b/libraries/textcomposer/src/main/res/values/localazy.xml @@ -14,4 +14,4 @@ "Toggle numbered list" "Toggle quote" "Unindent" - \ No newline at end of file + 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 0f2affe054..70a3c7a3ab 100644 --- a/libraries/ui-strings/src/main/res/values-cs/translations.xml +++ b/libraries/ui-strings/src/main/res/values-cs/translations.xml @@ -15,6 +15,7 @@ "Pokračovat" "Kopírovat" "Kopírovat odkaz" + "Kopírovat odkaz na zprávu" "Vytvořit" "Vytvořit místnost" "Odmítnout" @@ -23,6 +24,7 @@ "Upravit" "Povolit" "Zapomněli jste heslo?" + "Vpřed" "Pozvat" "Pozvat přátele" "Pozvat přátele do %1$s" @@ -34,6 +36,7 @@ "Ne" "Teď ne" "OK" + "Otevřít v aplikaci" "Rychlá odpověď" "Citovat" "Odstranit" @@ -52,13 +55,16 @@ "Začít" "Zahájit chat" "Zahájit ověření" + "Klepnutím načtete mapu" "Vyfotit" "Zobrazit zdroj" "Ano" "O aplikaci" + "Zásady používání" "Analytika" "Zvuk" "Bubliny" + "Autorská práva" "Vytváření místnosti…" "Opustit místnost" "Chyba dešifrování" @@ -69,6 +75,8 @@ "Šifrování povoleno" "Chyba" "Soubor" + "Soubor byl uložen do složky Stažené soubory" + "Přeposlat zprávu" "GIF" "Obrázek" "Nemůžeme ověřit Matrix ID tohoto uživatele. Pozvánka nemusí být přijata." @@ -79,17 +87,19 @@ "Rozložení zprávy" "Zpráva byla odstraněna" "Moderní" + "Ztlumit" "Žádné výsledky" "Offline" "Heslo" "Lidé" "Trvalý odkaz" + "Zásady ochrany osobních údajů" "Reakce" "Odpověď na %1$s" "Nahlásit chybu" "Zpráva odeslána" "Název místnosti" - "např. Produktový sprint" + "např. název vašeho projektu" "Hledat někoho" "Výsledky hledání" "Zabezpečení" @@ -102,11 +112,14 @@ "Nálepka" "Úspěch" "Návrhy" + "Synchronizace" + "Oznámení třetích stran" "Téma" "O čem je tato místnost?" "Nelze dešifrovat" "Nepodařilo se nám úspěšně odeslat pozvánky jednomu nebo více uživatelům." "Nelze odeslat pozvánky" + "Zrušit ztlumení" "Nepodporovaná událost" "Uživatelské jméno" "Ověření zrušeno" @@ -157,7 +170,7 @@ "en" "Chyba" "Úspěch" - "Pomozte nám identifikovat problémy a vylepšit %1$s sdílením anonymních údajů o používání." + "Sdílejte anonymní údaje o používání, které nám pomohou identifikovat problémy." "Můžete si přečíst všechny naše podmínky %1$s." "zde" "Zablokovat uživatele" 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 ad4c3571ad..f8d004d624 100644 --- a/libraries/ui-strings/src/main/res/values-de/translations.xml +++ b/libraries/ui-strings/src/main/res/values-de/translations.xml @@ -23,6 +23,7 @@ "Bearbeiten" "Aktivieren" "Passwort vergessen?" + "Weiterleiten" "Einladen" "Freunde einladen" "Freunde zu %1$s einladen" @@ -34,6 +35,7 @@ "Nein" "Nicht jetzt" "OK" + "Öffne mit" "Schnellantwort" "Zitieren" "Entfernen" @@ -52,24 +54,31 @@ "Starten" "Chat starten" "Verifizierung starten" + "Tippe, um die Karte zu laden" "Foto aufnehmen" "Quelltext anzeigen" "Ja" "Über" + "Allgemeine Geschäftsbedingungen" "Analyse" "Audio" "Blasen" + "Urheberrecht" "Erstelle Raum…" "Raum verlassen" "Entschlüsselungsfehler" "Entwickleroptionen" "(bearbeitet)" "Bearbeiten" + "* %1$s %2$s" "Verschlüsselung aktiviert" "Fehler" "Datei" + "Datei gespeichert unter Downloads" + "Nachricht weiterleiten" "GIF" "Bild" + "Wir können die Matrix-ID dieses Benutzers nicht validieren. Die Einladung wurde möglicherweise nicht empfangen." "Raum verlassen" "Link in Zwischenablage kopiert" "Wird geladen…" @@ -77,16 +86,19 @@ "Nachrichtenlayout" "Nachricht wurde entfernt" "Modern" + "Stummschalten" "Keine Ergebnisse" "Offline" "Passwort" "Personen" "Permalink" + "Datenschutzerklärung" "Reaktionen" "Auf %1$s antworten" "Melde einen Fehler" "Bericht gesendet" "Raumname" + "z.B. dein Projektname" "Suche nach jemandem" "Suchergebnisse" "Sicherheit" @@ -99,8 +111,13 @@ "Sticker" "Erfolg" "Vorschläge" + "Hinweise von Drittanbietern" "Thema" + "Worum geht es in diesem Raum?" "Entschlüsselung nicht möglich" + "Wir konnten Einladungen nicht erfolgreich an einen oder mehrere Benutzer senden." + "Einladung(en) können nicht gesendet werden" + "Stummschaltung aufheben" "Nicht unterstütztes Ereignis" "Benutzername" "Verifizierung abgebrochen" @@ -140,6 +157,7 @@ "Neu" "Teile Analyse-Daten" "Medienauswahl fehlgeschlagen, bitte versuche es erneut." + "Fehler bei der Verarbeitung von Medien zum Hochladen, bitte versuchen Sie 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" "Rageshake" @@ -149,7 +167,6 @@ "de" "Fehler" "Erfolg" - "Helfen Sie uns, Probleme zu identifizieren und %1$s zu verbessern, indem Sie anonyme Nutzungsdaten weitergeben." "Sie können alle unsere Nutzerbedingungen %1$s lesen." "hier" "Nutzer blockieren" diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index b1e73503fa..fa8a80f953 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -132,4 +132,4 @@ "Error" "Terminado" "Bloquear usuario" - \ No newline at end of file + diff --git a/libraries/ui-strings/src/main/res/values-fr/translations.xml b/libraries/ui-strings/src/main/res/values-fr/translations.xml index 76111046f8..7909e2d0e0 100644 --- a/libraries/ui-strings/src/main/res/values-fr/translations.xml +++ b/libraries/ui-strings/src/main/res/values-fr/translations.xml @@ -140,4 +140,4 @@ "Erreur" "Succès" "Bloquer l\'utilisateur" - \ No newline at end of file + diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index c8b16a7ea5..b15d570dfc 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -132,4 +132,4 @@ "Errore" "Operazione riuscita" "Blocca utente" - \ No newline at end of file + diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 57a783eac2..6b8d8e2e4f 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -22,6 +22,8 @@ "Efectuat" "Editați" "Activați" + "Ați uitat parola?" + "Redirecționați" "Invitați" "Invitați prieteni" "Invitați prieteni în %1$s" @@ -33,6 +35,7 @@ "Nu" "Nu acum" "OK" + "Deschideți cu" "Raspuns rapid" "Citat" "Ștergeți" @@ -55,9 +58,11 @@ "Vedeți sursă" "Da" "Despre" + "Politică de utilizare rezonabilă" "Analitice" "Audio" "Baloane" + "Drepturi de autor" "Se creează camera…" "Ați parăsit camera" "Eroare de decriptare" @@ -68,8 +73,10 @@ "Criptare activată" "Eroare" "Fişier" + "Fișier salvat în Descărcări" "GIF" "Imagine" + "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost trimisă." "Se părăsește conversația" "Linkul a fost copiat în clipboard" "Se încarcă…" @@ -77,16 +84,19 @@ "Aranjamentul mesajelor" "Mesaj sters" "Modern" + "Dezactivați sunetul" "Niciun rezultat" "Deconectat" "Parola" "Persoane" "Permalink" + "Politica de confidențialitate" "Reacții" "Răspuns pentru %1$s" "Raportați o eroare" "Raport trimis" "Numele camerei" + "de exemplu, numele proiectului dvs." "Căutați pe cineva" "Rezultatele căutării" "Securitate" @@ -99,10 +109,13 @@ "Autocolant" "Succes" "Sugestii" + "Notificări despre software de la terți" "Subiect" + "Despre ce este vorba în această cameră?" "Nu s-a putut decripta" "Nu am putut trimite cu succes invitații unuia sau mai multor utilizatori." "Nu s-a putut trimite invitația (invitațiile)" + "Activați sunetul" "Eveniment neacceptat" "Utilizator" "Verificare anulată" @@ -153,8 +166,7 @@ "ro" "Eroare" "Succes" - "Ajutați-ne să identificăm problemele și să îmbunătățim %1$s prin partajarea datelor de utilizare anonime." "Puteți citi toate condițiile noastre %1$s." "aici" "Blocați utilizatorul" - \ 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 93d2cc289e..ffbc7aded4 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -15,6 +15,7 @@ "Continue" "Copy" "Copy link" + "Copy link to message" "Create" "Create a room" "Decline" @@ -23,6 +24,7 @@ "Edit" "Enable" "Forgot password?" + "Forward" "Invite" "Invite friends" "Invite friends to %1$s" @@ -53,6 +55,7 @@ "Start" "Start chat" "Start verification" + "Tap to load map" "Take photo" "View Source" "Yes" @@ -73,6 +76,7 @@ "Error" "File" "File saved to Downloads" + "Forward message" "GIF" "Image" "We can’t validate this user’s Matrix ID. The invite might not be received." @@ -108,6 +112,7 @@ "Sticker" "Success" "Suggestions" + "Syncing" "Third-party notices" "Topic" "What is this room about?" @@ -152,20 +157,7 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" - "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" "Share analytics data" - "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" "Failed selecting media, please try again." "Failed processing media to upload, please try again." "Failed uploading media, please try again." @@ -178,8 +170,8 @@ "en" "Error" "Success" - "Help us identify issues and improve %1$s by sharing anonymous usage data." + "Share anonymous usage data to help us identify issues." "You can read all our terms %1$s." "here" "Block user" - \ No newline at end of file + diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.api.preferences_null_DefaultGroup_AnalyticsPreferencesViewDarkPreview_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.analytics.api.preferences_null_DefaultGroup_AnalyticsPreferencesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index b901fa8bd8..69cc122958 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.api.preferences_null_DefaultGroup_AnalyticsPreferencesViewDarkPreview_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.analytics.api.preferences_null_DefaultGroup_AnalyticsPreferencesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:514254ebfcc01b99ae6b85108c327cd2cfbe930ec793764049cf617e0d3b3580 -size 29278 +oid sha256:5c458a8c9ee5bc552c2f29b98bdb51555597d9207d9cc3283e57e007d7779852 +size 25616 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.api.preferences_null_DefaultGroup_AnalyticsPreferencesViewLightPreview_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.analytics.api.preferences_null_DefaultGroup_AnalyticsPreferencesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 29f66e2bf4..aac2729ec4 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.analytics.api.preferences_null_DefaultGroup_AnalyticsPreferencesViewLightPreview_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.analytics.api.preferences_null_DefaultGroup_AnalyticsPreferencesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:56d3de1fe930fa9c961265c2dc1c99ebecde06f3be27ab7d3cfb7299679456bb -size 28666 +oid sha256:0cbe5dfca48bc5750a08e2150fbf98cc78db58e918bc5e5740fc7dbc647ffaf1 +size 24992 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview--1_1_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.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png index fcdaeaa1c9..4a1e55c16c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview--1_1_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.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview--1_1_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9002796dbd6074f2cf6a2351dc5d65e772c1e7abedaf637dd81a30c11da11a3f -size 61210 +oid sha256:153126e98bb3f3a6d2a9586708af4a9f4c6110234fd9d253af164d77d8615d2d +size 59971 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview--1_1_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.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png index 0ea551a0b7..c325d9ecce 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview--1_1_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.preferences.impl.root_null_DefaultGroup_PreferencesRootViewDarkPreview--1_1_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3a6e702e87522cd2cf8fbb72b243f0d8e4f5426b04d8060a6a4b265c3de74d8b -size 64012 +oid sha256:0346f2949c18c8d5c46d7a77e158918781a3b2e0931062dbe5af62666809cd4c +size 59960 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview--0_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.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview--0_0_null_0,NEXUS_5,1.0,en].png index 098af44de0..90d413ca03 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview--0_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.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview--0_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d9010c13c864e8c6cf675a8335e9653387482108c897b6ffbc62ada1711aaeb9 -size 59639 +oid sha256:ea11564e4fb01fca55ad6a7e9542351d2cdebc27a99585a935f3dca36051fc7b +size 58513 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview--0_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.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview--0_0_null_1,NEXUS_5,1.0,en].png index 21ac080032..f6e99b3379 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview--0_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.preferences.impl.root_null_DefaultGroup_PreferencesRootViewLightPreview--0_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1592a00820108d4883a1ddaad265722653d7e4d5eabd99ab4b648672fd75850d -size 62606 +oid sha256:47bbbd2caafa859b445c72252a6aefd2e6804282fb31bf9be5cebf0a99987d6d +size 58650 From bf979576cc4d44dcdc1c10d1db44da505a32fff2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 19 Jun 2023 14:12:15 +0200 Subject: [PATCH 108/130] Create PR to sync Localazy strings in the name of `ElementBot`, to that created PR can trigger other GitHub actions. Ref: https://github.com/peter-evans/create-pull-request/blob/main/docs/concepts-guidelines.md#triggering-further-workflow-runs --- .github/workflows/sync-localazy.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 02f93cc3e1..5193fb7064 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -31,3 +31,5 @@ jobs: - Update Strings from Localazy branch: sync-localazy base: develop + env: + GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} From c63cba2cb85ca73ddc5b82afbd0442133f50b4e4 Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Mon, 19 Jun 2023 17:24:34 +0200 Subject: [PATCH 109/130] Remove send message section until it's actually implemented (#632) Fixes: #616 --- .../impl/members/details/RoomMemberDetailsView.kt | 7 ++++--- ...rDetailsViewDarkPreview--3_3_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...rDetailsViewDarkPreview--3_3_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...rDetailsViewDarkPreview--3_3_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...rDetailsViewDarkPreview--3_3_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...rDetailsViewDarkPreview--3_3_null_4,NEXUS_5,1.0,en].png | 4 ++-- ...DetailsViewLightPreview--2_2_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...DetailsViewLightPreview--2_2_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...DetailsViewLightPreview--2_2_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...DetailsViewLightPreview--2_2_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...DetailsViewLightPreview--2_2_null_4,NEXUS_5,1.0,en].png | 4 ++-- 11 files changed, 24 insertions(+), 23 deletions(-) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt index 62b3d07a85..f995cb14b6 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt @@ -89,9 +89,10 @@ fun RoomMemberDetailsView( Spacer(modifier = Modifier.height(26.dp)) - SendMessageSection(onSendMessage = { - // TODO implement send DM - }) + // TODO implement send DM + // SendMessageSection(onSendMessage = { + // ... + // }) if (!state.isCurrentUser) { BlockUserSection(state) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_0,NEXUS_5,1.0,en].png index eb3d4d754b..72cc9adfb6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dff0e2357ef0b08f415859b25470fed31dd80c5578e8fbe483ab8650d8f9aadb -size 28803 +oid sha256:063da4485de5cdadbbaa41d64fa0f615866ca89009225421a6bd2ba0ff0fcf01 +size 24666 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_1,NEXUS_5,1.0,en].png index 353ef28283..191a7bee34 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a895c48d5fcff6bf605f4c7518468b42390130843c096a135c185d368c1b01ef -size 26246 +oid sha256:edabce3917d2d066af9903a8312f9bf372aefa7e309c2f8d543faaf6220adcd9 +size 22549 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_2,NEXUS_5,1.0,en].png index dcccb87b8d..db138119d1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2bab9b94040850e9a0b9a15332e5e088f95b6a77ef31b6b08dc40d5f8e94ae7d -size 29679 +oid sha256:d4b782e88aebed6494276600b1792e3affa4d3a9b7619fecb0ceaf9a2137a391 +size 25670 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_3,NEXUS_5,1.0,en].png index eb3d4d754b..72cc9adfb6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dff0e2357ef0b08f415859b25470fed31dd80c5578e8fbe483ab8650d8f9aadb -size 28803 +oid sha256:063da4485de5cdadbbaa41d64fa0f615866ca89009225421a6bd2ba0ff0fcf01 +size 24666 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_4,NEXUS_5,1.0,en].png index eb3d4d754b..72cc9adfb6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview--3_3_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dff0e2357ef0b08f415859b25470fed31dd80c5578e8fbe483ab8650d8f9aadb -size 28803 +oid sha256:063da4485de5cdadbbaa41d64fa0f615866ca89009225421a6bd2ba0ff0fcf01 +size 24666 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_0,NEXUS_5,1.0,en].png index 57708e6118..856d9309cd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66d548cfe055f06ba42adabd08f31a54097d6c61decf4f2ddd8f4bd142d60f07 -size 28208 +oid sha256:1ebebc32c3b3f69ebab252cbfbe999b7e0d8d934ef1986cadd0027cd3cf02741 +size 24297 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_1,NEXUS_5,1.0,en].png index 8d0407b7ab..ea9e2cf526 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ca695e57d1ca585652544576a80cbb027e212842d727c83bf5e39adac599389e -size 25663 +oid sha256:66b7b86b54474c4c1d55022e99f5f270a148787cbe21890c68d1ea1e05e4a737 +size 22221 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_2,NEXUS_5,1.0,en].png index 13a0dacfdc..d50510272e 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:821c488d20c689230903245d64eb5740272ce6d568787f78bcd7e8875da371f7 -size 28743 +oid sha256:31da8ff7fa86ff31a63b448f82c1ddd49b03c5beba0227a77085606359656a75 +size 24626 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_3,NEXUS_5,1.0,en].png index 57708e6118..856d9309cd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66d548cfe055f06ba42adabd08f31a54097d6c61decf4f2ddd8f4bd142d60f07 -size 28208 +oid sha256:1ebebc32c3b3f69ebab252cbfbe999b7e0d8d934ef1986cadd0027cd3cf02741 +size 24297 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_4,NEXUS_5,1.0,en].png index 57708e6118..856d9309cd 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_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.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview--2_2_null_4,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66d548cfe055f06ba42adabd08f31a54097d6c61decf4f2ddd8f4bd142d60f07 -size 28208 +oid sha256:1ebebc32c3b3f69ebab252cbfbe999b7e0d8d934ef1986cadd0027cd3cf02741 +size 24297 From eba78ff722dc73ddbb0d99fdc0627ab5732bf90a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Mon, 19 Jun 2023 17:33:07 +0200 Subject: [PATCH 110/130] Add `workflow_dispatch` to be able to trigger this action. --- .github/workflows/sync-localazy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 5193fb7064..84253f822c 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -1,5 +1,6 @@ name: Sync Localazy on: + workflow_dispatch: schedule: # At 00:00 on every Monday UTC - cron: '0 0 * * 1' From 2cac32ef319328b4b40694a212a431f1ad07df32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jorge=20Mart=C3=ADn?= Date: Mon, 19 Jun 2023 18:00:32 +0200 Subject: [PATCH 111/130] Change the token used by the localazy sync so workflows are triggered in the resulting PR --- .github/workflows/sync-localazy.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/sync-localazy.yml b/.github/workflows/sync-localazy.yml index 84253f822c..3d63f8b542 100644 --- a/.github/workflows/sync-localazy.yml +++ b/.github/workflows/sync-localazy.yml @@ -26,11 +26,10 @@ jobs: - name: Create Pull Request for Strings uses: peter-evans/create-pull-request@v5 with: + token: ${{ secrets.DANGER_GITHUB_API_TOKEN }} commit-message: Sync Strings from Localazy title: Sync Strings body: | - Update Strings from Localazy branch: sync-localazy base: develop - env: - GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} From 07a2d309c45818f1dbccd6fde4c7729359b5b12f Mon Sep 17 00:00:00 2001 From: ganfra Date: Mon, 19 Jun 2023 21:38:54 +0200 Subject: [PATCH 112/130] Timeline media: kind of align with other messenger apps --- .../event/TimelineItemAspectRatioBox.kt | 17 +++++++++-------- .../components/event/TimelineItemImageView.kt | 3 +-- .../components/event/TimelineItemVideoView.kt | 4 +--- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt index b1240f9236..f733913bc8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemAspectRatioBox.kt @@ -25,22 +25,23 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +private const val MAX_HEIGHT_IN_DP = 360f +private const val MIN_ASPECT_RATIO = 0.6f +private const val MAX_ASPECT_RATIO = 4f +private const val DEFAULT_ASPECT_RATIO = 1.33f + @Composable fun TimelineItemAspectRatioBox( - height: Int?, aspectRatio: Float?, modifier: Modifier = Modifier, contentAlignment: Alignment = Alignment.TopStart, - content: @Composable BoxScope.() -> Unit, + content: @Composable (BoxScope.() -> Unit), ) { - val maxHeight = minOf(300, maxOf(100, height ?: Int.MAX_VALUE)) - val aspectRatioModifier = aspectRatio?.let { - Modifier.aspectRatio(it) - } ?: Modifier + val safeAspectRatio = (aspectRatio ?: DEFAULT_ASPECT_RATIO).coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) Box( modifier = modifier - .heightIn(max = maxHeight.dp) - .then(aspectRatioModifier), + .heightIn(max = MAX_HEIGHT_IN_DP.dp) + .aspectRatio(safeAspectRatio, true), contentAlignment = contentAlignment, content = content ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt index f6c1c069b9..6c7b51ddfa 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemImageView.kt @@ -34,14 +34,13 @@ fun TimelineItemImageView( modifier: Modifier = Modifier, ) { TimelineItemAspectRatioBox( - height = content.height, aspectRatio = content.aspectRatio, modifier = modifier ) { BlurHashAsyncImage( model = MediaRequestData(content.preferredMediaSource, MediaRequestData.Kind.File(content.body, content.mimeType)), blurHash = content.blurhash, - contentScale = ContentScale.Fit, + contentScale = ContentScale.Crop, ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt index f3bd4129d1..aeb2e7145e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/event/TimelineItemVideoView.kt @@ -43,7 +43,6 @@ fun TimelineItemVideoView( modifier: Modifier = Modifier, ) { TimelineItemAspectRatioBox( - height = content.height, aspectRatio = content.aspectRatio, modifier = modifier, contentAlignment = Alignment.Center, @@ -51,8 +50,7 @@ fun TimelineItemVideoView( BlurHashAsyncImage( model = MediaRequestData(content.thumbnailSource, MediaRequestData.Kind.File(content.body, content.mimeType)), blurHash = content.blurHash, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.Fit, + contentScale = ContentScale.Crop, ) Box( modifier = Modifier.roundedBackground(), From 2faa96a4e410e5f44bf1d3bf0bc450df558db58d Mon Sep 17 00:00:00 2001 From: bmarty Date: Mon, 19 Jun 2023 20:15:52 +0000 Subject: [PATCH 113/130] Sync Strings from Localazy --- features/messages/impl/src/main/res/values-cs/translations.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/features/messages/impl/src/main/res/values-cs/translations.xml b/features/messages/impl/src/main/res/values-cs/translations.xml index c684fe079e..7300efe1d6 100644 --- a/features/messages/impl/src/main/res/values-cs/translations.xml +++ b/features/messages/impl/src/main/res/values-cs/translations.xml @@ -13,6 +13,7 @@ "Nepodařilo se načíst údaje o uživateli" "Chtěli byste je pozvat zpět?" "V tomto chatu jste sami" + "Nemáte oprávnění vkládat příspěvky do této místnosti" "Odeslat znovu" "Vaši zprávu se nepodařilo odeslat" "Nahrání média se nezdařilo, zkuste to prosím znovu." From 11ccd8d35c0a5346fba73091406d22827711899d Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 20 Jun 2023 08:56:35 +0000 Subject: [PATCH 114/130] Update screenshots --- ...elineItemImageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...lineItemImageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...lineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...elineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...lineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...lineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...Group_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...roup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png | 4 ++-- 12 files changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index b1badf5a3f..affcb49660 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa17dd70b3c5eaaa37fbc5eed53997dd627090d9a5a3ffa518b9688647449a8e -size 99065 +oid sha256:c40273c36eb1479f284e75fa91d4a75b1ae97edd0242dda37a2d4a8f10394928 +size 138700 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index f5ff66af17..cb4b57910b 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4593d265265cea31c20b545250d20df54dfcc877d6e54eae28d1e84a1c693f16 -size 147443 +oid sha256:d7c47c713c74766c39d3b349d9e421ea588261e043105a5475cd8e68af782806 +size 185325 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index b1badf5a3f..affcb49660 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa17dd70b3c5eaaa37fbc5eed53997dd627090d9a5a3ffa518b9688647449a8e -size 99065 +oid sha256:c40273c36eb1479f284e75fa91d4a75b1ae97edd0242dda37a2d4a8f10394928 +size 138700 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index f5ff66af17..cb4b57910b 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemImageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4593d265265cea31c20b545250d20df54dfcc877d6e54eae28d1e84a1c693f16 -size 147443 +oid sha256:d7c47c713c74766c39d3b349d9e421ea588261e043105a5475cd8e68af782806 +size 185325 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 7407445780..5361cd1a24 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4e329e21d49bd79b633913edbbc1d5c63024874d866d96d399b1a8ffa5c1f18 -size 99209 +oid sha256:06617cda0f93ce0ea26b2a77a243371cdaf4f9a2956d8159ecb7196f7f6fe082 +size 139282 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index c803700190..0e14867b04 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5eb1c967db078a0333ef3fe26f94692264d0158d74d78768df32cc7c641faee3 -size 147537 +oid sha256:55d2d11ba729a68b1b62e9a69c3308594839fdb3612ff3db59850f0b346c28da +size 186118 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 7407445780..5361cd1a24 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f4e329e21d49bd79b633913edbbc1d5c63024874d866d96d399b1a8ffa5c1f18 -size 99209 +oid sha256:06617cda0f93ce0ea26b2a77a243371cdaf4f9a2956d8159ecb7196f7f6fe082 +size 139282 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index c803700190..0e14867b04 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_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.messages.impl.timeline.components.event_null_DefaultGroup_TimelineItemVideoViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5eb1c967db078a0333ef3fe26f94692264d0158d74d78768df32cc7c641faee3 -size 147537 +oid sha256:55d2d11ba729a68b1b62e9a69c3308594839fdb3612ff3db59850f0b346c28da +size 186118 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_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.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 8ccc3512a1..2ae60ffbaf 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_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.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ed788e9f85e922a81e077c2ad82b89fa822d9dec6d95442ad5aa5365535a87b9 -size 193815 +oid sha256:ead933d9b666f988a1c30997919ab364dacc4602c0cc62c03b14c63bd1b978d7 +size 226100 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_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.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png index 0d0752eeb4..2cdede3cc8 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_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.messages.impl.timeline_null_DefaultGroup_TimelineViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:169f1a6dd0d433a900c43867a3c2feacd3143fc8e58029d4f24eeb4e5a41d2f2 -size 193901 +oid sha256:2d753eeb5d8e83643f858e5190cde015321e32959111f6a45e70a60f790e94c8 +size 226976 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_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.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 9327685441..6351229d4c 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_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.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c8943194bce27da10d2f047e5617cbb45548170cc1f8c9606e62dae20fde44f5 -size 194104 +oid sha256:7647550e41280854b7dd68110250d8f217acc8bef70a25ca0ebdf28994802d63 +size 225809 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_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.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png index 373800dd3b..2c51bbc761 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_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.messages.impl.timeline_null_DefaultGroup_TimelineViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:dc4c738cf6e3c8961d0e97b8adc24d0ba8765cd8a8d1799b9783a5b1f098fc25 -size 194152 +oid sha256:35150cf00f6e4d899417ff5f456e8fb9168424a75def962f4df40155d3819104 +size 226734 From d55b1d5fcef18f2b0467c97de22bf2d05bf76bb1 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 20 Jun 2023 14:31:52 +0200 Subject: [PATCH 115/130] Try using ElementBot for recording screenshots too (#641) --- .github/workflows/recordScreenshots.yml | 4 +- .../workflows/scripts/recordScreenshots.sh | 42 ++++++++++++++++--- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index c28f4df26b..1790bb2a8d 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -16,6 +16,7 @@ jobs: - name: ⏬ Checkout with LFS uses: actions/checkout@v3 with: + persist-credentials: false lfs: 'true' - name: ☕️ Use JDK 17 uses: actions/setup-java@v3 @@ -30,5 +31,6 @@ jobs: - name: Record screenshots run: "./.github/workflows/scripts/recordScreenshots.sh" env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} GITHUB_REPOSITORY: ${{ secrets.GITHUB_REPOSITORY }} + diff --git a/.github/workflows/scripts/recordScreenshots.sh b/.github/workflows/scripts/recordScreenshots.sh index 8400d86913..2cfec13818 100755 --- a/.github/workflows/scripts/recordScreenshots.sh +++ b/.github/workflows/scripts/recordScreenshots.sh @@ -16,13 +16,43 @@ # limitations under the License. # -if [[ -z ${GITHUB_TOKEN} ]]; then - echo "Missing GITHUB_TOKEN variable" +TOKEN=$GITHUB_TOKEN +REPO=$GITHUB_REPOSITORY + +SHORT=t:,r: +LONG=token:,repo: +OPTS=$(getopt -a -n recordScreenshots --options $SHORT --longoptions $LONG -- "$@") + +eval set -- "$OPTS" +while : +do + case "$1" in + -t | --token ) + TOKEN="$2" + shift 2 + ;; + -r | --repo ) + REPO="$2" + shift 2 + ;; + --) + shift; + break + ;; + *) + echo "Unexpected option: $1" + help + ;; + esac +done + +if [[ -z ${TOKEN} ]]; then + echo "No token specified, either set the env var GITHUB_TOKEN or use the --token option" exit 1 fi -if [[ -z ${GITHUB_REPOSITORY} ]]; then - echo "Missing GITHUB_REPOSITORY variable" +if [[ -z ${REPO} ]]; then + echo "No repo specified, either set the env var GITHUB_REPOSITORY or use the --repo option" exit 1 fi @@ -35,6 +65,8 @@ git config user.email "benoitm+elementbot@element.io" git add -A git commit -m "Update screenshots" +BRANCH=$(git rev-parse --abbrev-ref HEAD) + echo "Pushing changes" -git push "https://$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY.git" +git push "https://$TOKEN@github.com/$REPO.git" $BRANCH:refs/heads/$BRANCH echo "Done!" From 51cb334185485efdc7aec03502768784bd0ed3c9 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 20 Jun 2023 15:59:04 +0200 Subject: [PATCH 116/130] Screenshots From ec9f0e286983139f270196a40dad5dfcbdc95f38 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Tue, 20 Jun 2023 17:07:53 +0200 Subject: [PATCH 117/130] Set max lines to 2 in replies for composer (#645) * Set `maxLines = 2` for the composer message preview in replies * Update screenshots --------- Co-authored-by: ElementBot --- .../io/element/android/libraries/textcomposer/TextComposer.kt | 2 +- ...up_TextComposerReplyDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...p_TextComposerReplyLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt index ccf4030ce1..0256d7e529 100644 --- a/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt +++ b/libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt @@ -302,7 +302,7 @@ private fun ReplyToModeView( style = ElementTextStyles.Regular.caption1, textAlign = TextAlign.Start, color = LocalColors.current.placeholder, - maxLines = 1, + maxLines = if (attachmentThumbnailInfo != null) 1 else 2, overflow = TextOverflow.Ellipsis, ) } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.textcomposer_null_DefaultGroup_TextComposerReplyDarkPreview_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.textcomposer_null_DefaultGroup_TextComposerReplyDarkPreview_0_null,NEXUS_5,1.0,en].png index c8904142b2..0ca8b318fb 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.textcomposer_null_DefaultGroup_TextComposerReplyDarkPreview_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.textcomposer_null_DefaultGroup_TextComposerReplyDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8922908539dc4978a23f72342e46a4c259322559d1f0dbe978dcf3ae8a4e79bf -size 66820 +oid sha256:ba55513ee2966dc6c02dbef9b66e3b780a8b883814d0389e5f119429a70716df +size 69445 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.textcomposer_null_DefaultGroup_TextComposerReplyLightPreview_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.textcomposer_null_DefaultGroup_TextComposerReplyLightPreview_0_null,NEXUS_5,1.0,en].png index e4c0408f4e..9696945634 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.textcomposer_null_DefaultGroup_TextComposerReplyLightPreview_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.textcomposer_null_DefaultGroup_TextComposerReplyLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a5b67e808ef8d171167e7fb241ba76b7e6d5ae48d193f6384bf3e2c4a0743f3 -size 66423 +oid sha256:155879c0c4035664cc622b3a27fb84a813c2115b305cb0034f17505fdb02d850 +size 68799 From 02c56df9914ed5a12d6f991b19c861bde7c5e645 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 21 Jun 2023 10:50:49 +0200 Subject: [PATCH 118/130] Extract `getCanSendEvent` to the `matrixui` module, rename to `canSendEventAsState` and use produceState. --- .../messages/impl/MessagesPresenter.kt | 13 ++------ .../matrix/ui/room/MatrixRoomState.kt | 31 +++++++++++++++++++ 2 files changed, 33 insertions(+), 11 deletions(-) create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index d78025dea0..50a66f3a8e 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -19,7 +19,6 @@ package io.element.android.features.messages.impl import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -59,6 +58,7 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType +import io.element.android.libraries.matrix.ui.room.canSendEventAsState import io.element.android.libraries.textcomposer.MessageComposerMode import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -88,7 +88,7 @@ class MessagesPresenter @Inject constructor( val retryState = retrySendMenuPresenter.present() val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L) - val userHasPermissionToSendMessage by getCanSendEvent(MessageEventType.ROOM_MESSAGE) + val userHasPermissionToSendMessage by room.canSendEventAsState(type = MessageEventType.ROOM_MESSAGE) val roomName: MutableState = rememberSaveable { mutableStateOf(null) } @@ -222,13 +222,4 @@ class MessagesPresenter @Inject constructor( MessageComposerEvents.SetMode(composerMode) ) } - - @Composable - private fun getCanSendEvent(type: MessageEventType): State { - val canSendEvent = remember(type) { mutableStateOf(false) } - LaunchedEffect(type) { - canSendEvent.value = room.canSendEvent(type).getOrElse { false } - } - return canSendEvent - } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt new file mode 100644 index 0000000000..19d7001342 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt @@ -0,0 +1,31 @@ +/* + * 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.matrix.ui.room + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.MessageEventType + +@Composable +fun MatrixRoom.canSendEventAsState(type: MessageEventType): State { + return produceState(initialValue = false, key1 = type) { + value = canSendEvent(type).getOrElse { false } + } +} + From 465ce0fc9b9f16f9bf169ef2a98d5578c63b8de8 Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Wed, 21 Jun 2023 11:12:47 +0200 Subject: [PATCH 119/130] Use github action that caches LFS files when checking out the repo (#647) Uses https://github.com/nschloe/action-cached-lfs-checkout where we're using git-lfs. This is a wrapper around `actions/checkout@v3` which uses `actions/cache@v3` to cache the files on LFS to avoid downloading them every time and spare LFS bandwidth. --- .github/workflows/nightlyReports.yml | 4 +--- .github/workflows/recordScreenshots.yml | 3 +-- .github/workflows/tests.yml | 3 +-- .github/workflows/validate-lfs.yml | 4 +--- 4 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/workflows/nightlyReports.yml b/.github/workflows/nightlyReports.yml index fad1e4a426..240a9f7cd7 100644 --- a/.github/workflows/nightlyReports.yml +++ b/.github/workflows/nightlyReports.yml @@ -18,9 +18,7 @@ jobs: if: ${{ github.repository == 'vector-im/element-x-android' }} steps: - name: ⏬ Checkout with LFS - uses: actions/checkout@v3 - with: - lfs: 'true' + uses: nschloe/action-cached-lfs-checkout@v1.2.1 - name: Use JDK 17 uses: actions/setup-java@v3 diff --git a/.github/workflows/recordScreenshots.yml b/.github/workflows/recordScreenshots.yml index 1790bb2a8d..8803ef156d 100644 --- a/.github/workflows/recordScreenshots.yml +++ b/.github/workflows/recordScreenshots.yml @@ -14,10 +14,9 @@ jobs: steps: - name: ⏬ Checkout with LFS - uses: actions/checkout@v3 + uses: nschloe/action-cached-lfs-checkout@v1.2.1 with: persist-credentials: false - lfs: 'true' - name: ☕️ Use JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3e9dbab5c6..a65c78c7a2 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,12 +22,11 @@ jobs: cancel-in-progress: true steps: - name: ⏬ Checkout with LFS - uses: actions/checkout@v3 + uses: nschloe/action-cached-lfs-checkout@v1.2.1 with: # Ensure we are building the branch and not the branch after being merged on develop # https://github.com/actions/checkout/issues/881 ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.ref }} - lfs: 'true' - name: ☕️ Use JDK 17 uses: actions/setup-java@v3 with: diff --git a/.github/workflows/validate-lfs.yml b/.github/workflows/validate-lfs.yml index 417b95ce2c..25fe50359c 100644 --- a/.github/workflows/validate-lfs.yml +++ b/.github/workflows/validate-lfs.yml @@ -7,9 +7,7 @@ jobs: runs-on: ubuntu-latest name: Validate steps: - - uses: actions/checkout@v3 - with: - lfs: 'true' + - uses: nschloe/action-cached-lfs-checkout@v1.2.1 - run: | ./tools/git/validate_lfs.sh From b3e11d184a11426b02019e3f98a53bcef096e3bd Mon Sep 17 00:00:00 2001 From: ganfra Date: Wed, 21 Jun 2023 17:43:08 +0200 Subject: [PATCH 120/130] CanSendEvent default to true and branch refresh mechanism... --- .../android/features/messages/impl/MessagesPresenter.kt | 2 +- .../android/libraries/matrix/ui/room/MatrixRoomState.kt | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 50a66f3a8e..7daf99283d 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -88,7 +88,7 @@ class MessagesPresenter @Inject constructor( val retryState = retrySendMenuPresenter.present() val syncUpdateFlow = room.syncUpdateFlow().collectAsState(0L) - val userHasPermissionToSendMessage by room.canSendEventAsState(type = MessageEventType.ROOM_MESSAGE) + val userHasPermissionToSendMessage by room.canSendEventAsState(type = MessageEventType.ROOM_MESSAGE, updateKey = syncUpdateFlow.value) val roomName: MutableState = rememberSaveable { mutableStateOf(null) } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt index 19d7001342..4533f1f5ef 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/room/MatrixRoomState.kt @@ -23,9 +23,9 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.MessageEventType @Composable -fun MatrixRoom.canSendEventAsState(type: MessageEventType): State { - return produceState(initialValue = false, key1 = type) { - value = canSendEvent(type).getOrElse { false } +fun MatrixRoom.canSendEventAsState(type: MessageEventType, updateKey: Long): State { + return produceState(initialValue = true, key1 = updateKey) { + value = canSendEvent(type).getOrElse { true } } } From 5126a18e4ddfc1275ad7ebbc358ad94ced2cb7dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jun 2023 16:44:09 +0000 Subject: [PATCH 121/130] Update dependency org.matrix.rustcomponents:sdk-android to v0.1.22 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e40f30b56b..4a3e80addc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -142,7 +142,7 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } appyx_core = { module = "com.bumble.appyx:core", version.ref = "appyx" } molecule-runtime = { module = "app.cash.molecule:molecule-runtime", version.ref = "molecule" } timber = "com.jakewharton.timber:timber:5.0.1" -matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.21" +matrix_sdk = "org.matrix.rustcomponents:sdk-android:0.1.22" sqldelight-driver-android = { module = "com.squareup.sqldelight:android-driver", version.ref = "sqldelight" } sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", version.ref = "sqldelight" } sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } From 24807852c66cb158235cd683cfe5cb7ab3db83e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 00:42:24 +0000 Subject: [PATCH 122/130] Update dependency androidx.compose:compose-bom to v2023.06.01 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e40f30b56b..c2e4e26114 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,7 +21,7 @@ media3 = "1.0.2" browser = "1.5.0" # Compose -compose_bom = "2023.06.00" +compose_bom = "2023.06.01" composecompiler = "1.4.7" # Coroutines From 21546df9ddbd67ba7b3b9154a4fdfc110ca84417 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 08:29:52 +0200 Subject: [PATCH 123/130] Update plugin ktlint to v11.4.1 (#653) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e40f30b56b..1179341d86 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -190,7 +190,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } anvil = { id = "com.squareup.anvil", version.ref = "anvil" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -ktlint = "org.jlleitschuh.gradle.ktlint:11.4.0" +ktlint = "org.jlleitschuh.gradle.ktlint:11.4.1" dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" } dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" } From 9f22b3e6d9a662afa3bf5c7e4f5fa181c6b558f8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jun 2023 08:30:04 +0200 Subject: [PATCH 124/130] Update dependency app.cash.turbine:turbine to v1 (#646) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1179341d86..3ca790765a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -124,7 +124,7 @@ test_mockk = "io.mockk:mockk:1.13.5" test_barista = "com.adevinta.android:barista:4.3.0" test_hamcrest = "org.hamcrest:hamcrest:2.2" test_orchestrator = "androidx.test:orchestrator:1.4.2" -test_turbine = "app.cash.turbine:turbine:0.13.0" +test_turbine = "app.cash.turbine:turbine:1.0.0" test_truth = "com.google.truth:truth:1.1.5" test_parameter_injector = "com.google.testparameterinjector:test-parameter-injector:1.12" test_robolectric = "org.robolectric:robolectric:4.10.3" From 95255ee04c5daa54fdefa22539246731a7da67f6 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Thu, 22 Jun 2023 07:21:19 +0000 Subject: [PATCH 125/130] Update screenshots --- ...ets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- ...ts_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetDarkPreview_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.theme.components_null_BottomSheets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png index fca921c50b..0a268ba362 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetDarkPreview_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.theme.components_null_BottomSheets_ModalBottomSheetDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c1c1eedbab868e0c2501220293608572850e052f685f7076ec939b3f1a9abf27 -size 4464 +oid sha256:4aee0816507bc71c8649aae95e57c3b75fb5e7fbd3888531817f659e1153f5be +size 7971 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetLightPreview_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.theme.components_null_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png index 665c8811ac..09a7bd0089 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_BottomSheets_ModalBottomSheetLightPreview_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.theme.components_null_BottomSheets_ModalBottomSheetLightPreview_0_null,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b -size 4457 +oid sha256:dac298843eacb59d4666d50712e5cdbe82620a25c5c01d23b77f4dcba9df9c43 +size 7979 From 0d0a5571afae8b6266c5bc40c71be9f34199b37e Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 22 Jun 2023 09:50:17 +0200 Subject: [PATCH 126/130] Rust sdk update: make the app compile --- .../configureroom/ConfigureRoomPresenter.kt | 2 +- .../libraries/matrix/api/MatrixClient.kt | 3 +- .../matrix/api/core/ProgressCallback.kt | 21 +++++++++++++ .../libraries/matrix/api/room/MatrixRoom.kt | 9 +++--- .../libraries/matrix/impl/RustMatrixClient.kt | 6 ++-- .../impl/core/ProgressWatcherWrapper.kt | 31 +++++++++++++++++++ .../impl/notification/TimelineEventMapper.kt | 3 +- .../matrix/impl/room/RustMatrixRoom.kt | 22 ++++++++----- .../timeline/item/event/EventMessageMapper.kt | 2 ++ .../libraries/matrix/test/FakeMatrixClient.kt | 7 ++++- .../matrix/test/room/FakeMatrixRoom.kt | 14 ++++++--- .../libraries/mediaupload/api/MediaSender.kt | 9 ++++-- 12 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ProgressCallback.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/ProgressWatcherWrapper.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index ca714b1e59..f2a03ca2c0 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -131,6 +131,6 @@ class ConfigureRoomPresenter @Inject constructor( private suspend fun uploadAvatar(avatarUri: Uri): String { val preprocessed = mediaPreProcessor.process(avatarUri, MimeTypes.Jpeg, compressIfPossible = false).getOrThrow() val byteArray = preprocessed.file.readBytes() - return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray).getOrThrow() + return matrixClient.uploadMedia(MimeTypes.Jpeg, byteArray, null).getOrThrow() } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index f0dec42855..4a018e18da 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.api +import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId @@ -52,7 +53,7 @@ interface MatrixClient : Closeable { suspend fun logout() suspend fun loadUserDisplayName(): Result suspend fun loadUserAvatarURLString(): Result - suspend fun uploadMedia(mimeType: String, data: ByteArray): Result + suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result fun onSlidingSyncUpdate() fun roomMembershipObserver(): RoomMembershipObserver } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ProgressCallback.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ProgressCallback.kt new file mode 100644 index 0000000000..2b41907eec --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ProgressCallback.kt @@ -0,0 +1,21 @@ +/* + * 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.matrix.api.core + +interface ProgressCallback { + fun onProgress(current: Long, total: Long) +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index c31db970e0..664ad9dbcb 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.api.room import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId @@ -73,13 +74,13 @@ interface MatrixRoom : Closeable { suspend fun redactEvent(eventId: EventId, reason: String? = null): Result - suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result + suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result - suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result + suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result - suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result + suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result - suspend fun sendFile(file: File, fileInfo: FileInfo): Result + suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result suspend fun sendReaction(emoji: String, eventId: EventId): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index d8cd8c480f..29009f9637 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -20,6 +20,7 @@ package io.element.android.libraries.matrix.impl import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters @@ -35,6 +36,7 @@ import io.element.android.libraries.matrix.api.timeline.item.event.EventType import io.element.android.libraries.matrix.api.user.MatrixSearchUserResults import io.element.android.libraries.matrix.api.user.MatrixUser import io.element.android.libraries.matrix.api.verification.SessionVerificationService +import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.pushers.RustPushersService @@ -351,9 +353,9 @@ class RustMatrixClient constructor( } @OptIn(ExperimentalUnsignedTypes::class) - override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result = withContext(dispatchers.io) { + override suspend fun uploadMedia(mimeType: String, data: ByteArray, progressCallback: ProgressCallback?): Result = withContext(dispatchers.io) { runCatching { - client.uploadMedia(mimeType, data.toUByteArray().toList()) + client.uploadMedia(mimeType, data.toUByteArray().toList(), progressCallback?.toProgressWatcher()) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/ProgressWatcherWrapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/ProgressWatcherWrapper.kt new file mode 100644 index 0000000000..f904e13bd6 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/core/ProgressWatcherWrapper.kt @@ -0,0 +1,31 @@ +/* + * 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.matrix.impl.core + +import io.element.android.libraries.matrix.api.core.ProgressCallback +import org.matrix.rustcomponents.sdk.ProgressWatcher +import org.matrix.rustcomponents.sdk.TransmissionProgress + +internal class ProgressWatcherWrapper(private val progressCallback: ProgressCallback) : ProgressWatcher { + override fun transmissionProgress(progress: TransmissionProgress) { + progressCallback.onProgress(progress.current.toLong(), progress.total.toLong()) + } +} + +internal fun ProgressCallback.toProgressWatcher(): ProgressWatcher { + return ProgressWatcherWrapper(this) +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt index adb9dcce72..9b70582308 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/notification/TimelineEventMapper.kt @@ -16,8 +16,6 @@ package io.element.android.libraries.matrix.impl.notification -import io.element.android.libraries.matrix.api.core.EventId -import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.notification.NotificationEvent import org.matrix.rustcomponents.sdk.MessageLikeEventContent import org.matrix.rustcomponents.sdk.MessageType @@ -105,5 +103,6 @@ private fun MessageType.toContent(): String { is MessageType.Notice -> content.body is MessageType.Text -> content.body is MessageType.Video -> content.use { it.body } + is MessageType.Location -> content.body } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index a0cfd24c05..8db9619c34 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.impl.room import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId @@ -31,6 +32,7 @@ import io.element.android.libraries.matrix.api.room.MessageEventType import io.element.android.libraries.matrix.api.room.StateEventType import io.element.android.libraries.matrix.api.room.roomMembers import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.map import io.element.android.libraries.matrix.impl.timeline.RustMatrixTimeline import io.element.android.services.toolbox.api.systemclock.SystemClock @@ -241,27 +243,31 @@ class RustMatrixRoom( } } - override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = withContext(coroutineDispatchers.io) { + override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo, progressCallback: ProgressCallback?): Result = withContext( + coroutineDispatchers.io + ) { runCatching { - innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map()) + innerRoom.sendImage(file.path, thumbnailFile.path, imageInfo.map(), progressCallback?.toProgressWatcher()) } } - override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = withContext(coroutineDispatchers.io) { + override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result = withContext( + coroutineDispatchers.io + ) { runCatching { - innerRoom.sendVideo(file.path, thumbnailFile.path, videoInfo.map()) + innerRoom.sendVideo(file.path, thumbnailFile.path, videoInfo.map(), progressCallback?.toProgressWatcher()) } } - override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result = withContext(coroutineDispatchers.io) { + override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result = withContext(coroutineDispatchers.io) { runCatching { - innerRoom.sendAudio(file.path, audioInfo.map()) + innerRoom.sendAudio(file.path, audioInfo.map(), progressCallback?.toProgressWatcher()) } } - override suspend fun sendFile(file: File, fileInfo: FileInfo): Result = withContext(coroutineDispatchers.io) { + override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result = withContext(coroutineDispatchers.io) { runCatching { - innerRoom.sendFile(file.path, fileInfo.map()) + innerRoom.sendFile(file.path, fileInfo.map(), progressCallback?.toProgressWatcher()) } } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt index 7fdbe06e8a..d45124bf40 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/timeline/item/event/EventMessageMapper.kt @@ -65,9 +65,11 @@ class EventMessageMapper { is MessageType.Video -> { VideoMessageType(type.content.body, type.content.source.map(), type.content.info?.map()) } + is MessageType.Location, null -> { UnknownMessageType } + } } val inReplyToId = it.inReplyTo()?.eventId?.let(::EventId) diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 85c3555844..8b38a74457 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.test import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId @@ -116,7 +117,11 @@ class FakeMatrixClient( return userAvatarURLString } - override suspend fun uploadMedia(mimeType: String, data: ByteArray): Result { + override suspend fun uploadMedia( + mimeType: String, + data: ByteArray, + progressCallback: ProgressCallback? + ): Result { return uploadMediaResult } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 912f0c629d..01f1d17f7c 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.matrix.test.room import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.ProgressCallback import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId @@ -204,13 +205,18 @@ class FakeMatrixRoom( return canSendEventResults[type] ?: Result.failure(IllegalStateException("No fake answer")) } - override suspend fun sendImage(file: File, thumbnailFile: File, imageInfo: ImageInfo): Result = fakeSendMedia() + override suspend fun sendImage( + file: File, + thumbnailFile: File, + imageInfo: ImageInfo, + progressCallback: ProgressCallback? + ): Result = fakeSendMedia() - override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo): Result = fakeSendMedia() + override suspend fun sendVideo(file: File, thumbnailFile: File, videoInfo: VideoInfo, progressCallback: ProgressCallback?): Result = fakeSendMedia() - override suspend fun sendAudio(file: File, audioInfo: AudioInfo): Result = fakeSendMedia() + override suspend fun sendAudio(file: File, audioInfo: AudioInfo, progressCallback: ProgressCallback?): Result = fakeSendMedia() - override suspend fun sendFile(file: File, fileInfo: FileInfo): Result = fakeSendMedia() + override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result = fakeSendMedia() private suspend fun fakeSendMedia(): Result = simulateLongTask { sendMediaResult.onSuccess { diff --git a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt index 5de8239e38..9f27824858 100644 --- a/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt +++ b/libraries/mediaupload/api/src/main/kotlin/io/element/android/libraries/mediaupload/api/MediaSender.kt @@ -47,7 +47,8 @@ class MediaSender @Inject constructor( sendImage( file = info.file, thumbnailFile = info.thumbnailFile, - imageInfo = info.info + imageInfo = info.info, + progressCallback = null ) } @@ -55,14 +56,16 @@ class MediaSender @Inject constructor( sendVideo( file = info.file, thumbnailFile = info.thumbnailFile, - videoInfo = info.info + videoInfo = info.info, + progressCallback = null ) } is MediaUploadInfo.AnyFile -> { sendFile( file = info.file, - fileInfo = info.info + fileInfo = info.info, + progressCallback = null ) } else -> Result.failure(IllegalStateException("Unexpected MediaUploadInfo format: $info")) From de7bbbd5cf7451bd4914d69f65fba4e5e3b30605 Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 22 Jun 2023 13:27:59 +0200 Subject: [PATCH 127/130] [Message Actions] Forward messages (#635) * Add forwarding messages base * Make forwarding single-selection --------- Co-authored-by: ElementBot --- .../android/appnav/LoggedInFlowNode.kt | 8 +- .../io/element/android/appnav/RoomFlowNode.kt | 10 + changelog.d/486.feature | 1 + .../impl/src/main/res/values/localazy.xml | 2 +- .../messages/api/MessagesEntryPoint.kt | 2 + .../messages/impl/MessagesFlowNode.kt | 18 ++ .../messages/impl/MessagesNavigator.kt | 25 ++ .../features/messages/impl/MessagesNode.kt | 14 +- .../messages/impl/MessagesPresenter.kt | 25 +- .../features/messages/impl/MessagesView.kt | 9 +- .../impl/forward/ForwardMessagesEvents.kt | 29 ++ .../impl/forward/ForwardMessagesNode.kt | 69 +++++ .../impl/forward/ForwardMessagesPresenter.kt | 136 ++++++++ .../impl/forward/ForwardMessagesState.kt | 33 ++ .../forward/ForwardMessagesStateProvider.kt | 106 +++++++ .../impl/forward/ForwardMessagesView.kt | 292 ++++++++++++++++++ .../messages/FakeMessagesNavigator.kt | 37 +++ .../messages/MessagesPresenterTest.kt | 12 +- .../forward/ForwardMessagesPresenterTests.kt | 177 +++++++++++ .../features/roomlist/impl/RoomListNode.kt | 2 +- .../matrix/api/room/ForwardEventException.kt | 26 ++ .../libraries/matrix/api/room/MatrixRoom.kt | 3 + .../libraries/matrix/impl/RustMatrixClient.kt | 7 + .../matrix/impl/room/RoomContentForwarder.kt | 85 +++++ .../matrix/impl/room/RustMatrixRoom.kt | 10 + .../impl/room/RustRoomSummaryDataSource.kt | 6 +- .../matrix/test/room/FakeMatrixRoom.kt | 9 + .../matrix/ui/components/SelectedRoom.kt | 118 +++++++ ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_6,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_7,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_6,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_7,NEXUS_5,1.0,en].png | 3 + ...RoomDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...oomLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + 46 files changed, 1300 insertions(+), 25 deletions(-) create mode 100644 changelog.d/486.feature create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/ForwardEventException.kt create mode 100644 libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt create mode 100644 libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_6,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_7,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_SelectedRoomDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_SelectedRoomLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index ba002b78ac..7a4d51f0ee 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -66,6 +66,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @ContributesNode(AppScope::class) @@ -238,8 +239,13 @@ class LoggedInFlowNode @AssistedInject constructor( } } else { val nodeLifecycleCallbacks = plugins() + val callback = object : RoomFlowNode.Callback { + override fun onForwardedToSingleRoom(roomId: RoomId) { + coroutineScope.launch { attachRoom(roomId) } + } + } val inputs = RoomFlowNode.Inputs(room, initialElement = navTarget.initialElement) - createNode(buildContext, plugins = listOf(inputs) + nodeLifecycleCallbacks) + createNode(buildContext, plugins = listOf(inputs, callback) + nodeLifecycleCallbacks) } } NavTarget.Settings -> { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 0bcf9000e7..64c0d200fd 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -39,6 +39,7 @@ import io.element.android.libraries.architecture.NodeInputs import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.inputs import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver @@ -67,6 +68,10 @@ class RoomFlowNode @AssistedInject constructor( plugins = plugins, ) { + interface Callback : Plugin { + fun onForwardedToSingleRoom(roomId: RoomId) + } + interface LifecycleCallback : NodeLifecycleCallback { fun onFlowCreated(identifier: String, room: MatrixRoom) = Unit fun onFlowReleased(identifier: String, room: MatrixRoom) = Unit @@ -78,6 +83,7 @@ class RoomFlowNode @AssistedInject constructor( ) : NodeInputs private val inputs: Inputs = inputs() + private val callbacks = plugins.filterIsInstance() init { lifecycle.subscribe( @@ -124,6 +130,10 @@ class RoomFlowNode @AssistedInject constructor( override fun onUserDataClicked(userId: UserId) { backstack.push(NavTarget.RoomMemberDetails(userId)) } + + override fun onForwardedToSingleRoom(roomId: RoomId) { + callbacks.forEach { it.onForwardedToSingleRoom(roomId) } + } } messagesEntryPoint.createNode(this, buildContext, callback) } diff --git a/changelog.d/486.feature b/changelog.d/486.feature new file mode 100644 index 0000000000..110c069cda --- /dev/null +++ b/changelog.d/486.feature @@ -0,0 +1 @@ +Allow forawrding messages from one room to another diff --git a/features/login/impl/src/main/res/values/localazy.xml b/features/login/impl/src/main/res/values/localazy.xml index 55324613ed..145ac2d238 100644 --- a/features/login/impl/src/main/res/values/localazy.xml +++ b/features/login/impl/src/main/res/values/localazy.xml @@ -38,4 +38,4 @@ "Password" "Continue" "Username" - + \ No newline at end of file diff --git a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt index f1ed5c18dd..482dfad8ea 100644 --- a/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt +++ b/features/messages/api/src/main/kotlin/io/element/android/features/messages/api/MessagesEntryPoint.kt @@ -20,6 +20,7 @@ import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin import io.element.android.libraries.architecture.FeatureEntryPoint +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId interface MessagesEntryPoint : FeatureEntryPoint { @@ -32,5 +33,6 @@ interface MessagesEntryPoint : FeatureEntryPoint { interface Callback : Plugin { fun onRoomDetailsClicked() fun onUserDataClicked(userId: UserId) + fun onForwardedToSingleRoom(roomId: RoomId) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index ab8a053716..e74c55395f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -32,6 +32,7 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.messages.api.MessagesEntryPoint import io.element.android.features.messages.impl.attachments.Attachment import io.element.android.features.messages.impl.attachments.preview.AttachmentsPreviewNode +import io.element.android.features.messages.impl.forward.ForwardMessagesNode import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.viewer.MediaViewerNode import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode @@ -43,6 +44,7 @@ import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.media.MediaSource import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo @@ -78,6 +80,9 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data class EventDebugInfo(val eventId: EventId, val debugInfo: TimelineItemDebugInfo) : NavTarget + + @Parcelize + data class ForwardEvent(val eventId: EventId) : NavTarget } private val callback = plugins().firstOrNull() @@ -105,6 +110,10 @@ class MessagesFlowNode @AssistedInject constructor( override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { backstack.push(NavTarget.EventDebugInfo(eventId, debugInfo)) } + + override fun onForwardEventClicked(eventId: EventId) { + backstack.push(NavTarget.ForwardEvent(eventId)) + } } createNode(buildContext, listOf(callback)) } @@ -124,6 +133,15 @@ class MessagesFlowNode @AssistedInject constructor( val inputs = EventDebugInfoNode.Inputs(navTarget.eventId, navTarget.debugInfo) createNode(buildContext, listOf(inputs)) } + is NavTarget.ForwardEvent -> { + val inputs = ForwardMessagesNode.Inputs(navTarget.eventId) + val callback = object : ForwardMessagesNode.Callback { + override fun onForwardedToSingleRoom(roomId: RoomId) { + this@MessagesFlowNode.callback?.onForwardedToSingleRoom(roomId) + } + } + createNode(buildContext, listOf(inputs, callback)) + } } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt new file mode 100644 index 0000000000..d4733da047 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.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.messages.impl + +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo + +interface MessagesNavigator { + fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) + fun onForwardEventClicked(eventId: EventId) +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 58d73a10f7..8624b62e75 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -37,9 +37,10 @@ import kotlinx.collections.immutable.ImmutableList class MessagesNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: MessagesPresenter, -) : Node(buildContext, plugins = plugins) { + private val presenterFactory: MessagesPresenter.Factory, +) : Node(buildContext, plugins = plugins), MessagesNavigator { + private val presenter = presenterFactory.create(this) private val callback = plugins().firstOrNull() interface Callback : Plugin { @@ -48,6 +49,7 @@ class MessagesNode @AssistedInject constructor( fun onPreviewAttachments(attachments: ImmutableList) fun onUserDataClicked(userId: UserId) fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) + fun onForwardEventClicked(eventId: EventId) } private fun onRoomDetailsClicked() { @@ -65,11 +67,14 @@ class MessagesNode @AssistedInject constructor( private fun onUserDataClicked(userId: UserId) { callback?.onUserDataClicked(userId) } - - private fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { + override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { callback?.onShowEventDebugInfoClicked(eventId, debugInfo) } + override fun onForwardEventClicked(eventId: EventId) { + callback?.onForwardEventClicked(eventId) + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -80,7 +85,6 @@ class MessagesNode @AssistedInject constructor( onEventClicked = this::onEventClicked, onPreviewAttachments = this::onPreviewAttachments, onUserDataClicked = this::onUserDataClicked, - onItemDebugInfoClicked = this::onShowEventDebugInfoClicked, modifier = modifier, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 7daf99283d..6b64dc8bdd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -25,6 +25,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import io.element.android.features.messages.impl.actionlist.ActionListEvents import io.element.android.features.messages.impl.actionlist.ActionListPresenter import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction @@ -65,7 +68,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -class MessagesPresenter @Inject constructor( +class MessagesPresenter @AssistedInject constructor( private val room: MatrixRoom, private val composerPresenter: MessageComposerPresenter, private val timelinePresenter: TimelinePresenter, @@ -76,8 +79,14 @@ class MessagesPresenter @Inject constructor( private val snackbarDispatcher: SnackbarDispatcher, private val messageSummaryFormatter: MessageSummaryFormatter, private val dispatchers: CoroutineDispatchers, + @Assisted private val navigator: MessagesNavigator, ) : Presenter { + @AssistedFactory + interface Factory { + fun create(navigator: MessagesNavigator): MessagesPresenter + } + @Composable override fun present(): MessagesState { val localCoroutineScope = rememberCoroutineScope() @@ -147,11 +156,11 @@ class MessagesPresenter @Inject constructor( ) = launch { when (action) { TimelineItemAction.Copy -> notImplementedYet() - TimelineItemAction.Forward -> notImplementedYet() TimelineItemAction.Redact -> handleActionRedact(targetEvent) TimelineItemAction.Edit -> handleActionEdit(targetEvent, composerState) TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState) - TimelineItemAction.Developer -> Unit // Handled at UI level + TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent) + TimelineItemAction.Forward -> handleForwardAction(targetEvent) TimelineItemAction.ReportContent -> notImplementedYet() } } @@ -222,4 +231,14 @@ class MessagesPresenter @Inject constructor( MessageComposerEvents.SetMode(composerMode) ) } + + private fun handleShowDebugInfoAction(event: TimelineItem.Event) { + if (event.eventId == null) return + navigator.onShowEventDebugInfoClicked(event.eventId, event.debugInfo) + } + + private fun handleForwardAction(event: TimelineItem.Event) { + if (event.eventId == null) return + navigator.onForwardEventClicked(event.eventId) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt index 93b74806d6..d9504894e8 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt @@ -95,7 +95,6 @@ fun MessagesView( onEventClicked: (event: TimelineItem.Event) -> Unit, onUserDataClicked: (UserId) -> Unit, onPreviewAttachments: (ImmutableList) -> Unit, - onItemDebugInfoClicked: (EventId, TimelineItemDebugInfo) -> Unit, modifier: Modifier = Modifier, ) { LogCompositions(tag = "MessagesScreen", msg = "Root") @@ -121,12 +120,7 @@ fun MessagesView( } fun onActionSelected(action: TimelineItemAction, event: TimelineItem.Event) { - when (action) { - is TimelineItemAction.Developer -> if (event.eventId != null) { - onItemDebugInfoClicked(event.eventId, event.debugInfo) - } - else -> state.eventSink(MessagesEvents.HandleAction(action, event)) - } + state.eventSink(MessagesEvents.HandleAction(action, event)) } fun onEmojiReactionClicked(emoji: String, event: TimelineItem.Event) { @@ -331,6 +325,5 @@ private fun ContentToPreview(state: MessagesState) { onEventClicked = {}, onPreviewAttachments = {}, onUserDataClicked = {}, - onItemDebugInfoClicked = { _, _ -> }, ) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.kt new file mode 100644 index 0000000000..6b74918d71 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesEvents.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.messages.impl.forward + +import io.element.android.libraries.matrix.api.room.RoomSummaryDetails + +sealed interface ForwardMessagesEvents { + data class SetSelectedRoom(val room: RoomSummaryDetails) : ForwardMessagesEvents + // TODO remove to restore multi-selection + object RemoveSelectedRoom : ForwardMessagesEvents + object ToggleSearchActive : ForwardMessagesEvents + data class UpdateQuery(val query: String) : ForwardMessagesEvents + object ForwardEvent : ForwardMessagesEvents + object ClearError : ForwardMessagesEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt new file mode 100644 index 0000000000..13d26b9881 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesNode.kt @@ -0,0 +1,69 @@ +/* + * 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.messages.impl.forward + +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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import kotlinx.collections.immutable.ImmutableList + +@ContributesNode(RoomScope::class) +class ForwardMessagesNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: ForwardMessagesPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + interface Callback : Plugin { + fun onForwardedToSingleRoom(roomId: RoomId) + } + + data class Inputs(val eventId: EventId) : NodeInputs + + private val inputs = inputs() + private val presenter = presenterFactory.create(inputs.eventId.value) + private val callbacks = plugins.filterIsInstance() + + private fun onSucceeded(roomIds: ImmutableList) { + navigateUp() + if (roomIds.size == 1) { + val targetRoomId = roomIds.first() + callbacks.forEach { it.onForwardedToSingleRoom(targetRoomId) } + } + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ForwardMessagesView( + state = state, + onDismiss = ::navigateUp, + onForwardingSucceeded = ::onSucceeded, + modifier = modifier + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt new file mode 100644 index 0000000000..17494f892e --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesPresenter.kt @@ -0,0 +1,136 @@ +/* + * 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.messages.impl.forward + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.isLoading +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomSummary +import io.element.android.libraries.matrix.api.room.RoomSummaryDetails +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toPersistentList +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class ForwardMessagesPresenter @AssistedInject constructor( + @Assisted eventId: String, + private val room: MatrixRoom, + private val matrixCoroutineScope: CoroutineScope, + private val client: MatrixClient, +) : Presenter { + + private val eventId: EventId = EventId(eventId) + + @AssistedFactory + interface Factory { + fun create(eventId: String): ForwardMessagesPresenter + } + + @Composable + override fun present(): ForwardMessagesState { + var selectedRooms by remember { mutableStateOf(persistentListOf()) } + var query by remember { mutableStateOf("") } + var isSearchActive by remember { mutableStateOf(false) } + var results: SearchBarResultState> by remember { mutableStateOf(SearchBarResultState.NotSearching()) } + val forwardingActionState: MutableState>> = remember { mutableStateOf(Async.Uninitialized) } + + val summaries by client.roomSummaryDataSource.roomSummaries().collectAsState() + + LaunchedEffect(query, summaries) { + val filteredSummaries = summaries.filterIsInstance() + .map { it.details } + .filter { it.name.contains(query, ignoreCase = true) } + .distinctBy { it.roomId } // This should be removed once we're sure no duplicate Rooms can be received + .toPersistentList() + results = if (filteredSummaries.isNotEmpty()) { + SearchBarResultState.Results(filteredSummaries) + } else { + SearchBarResultState.NoResults() + } + } + + val forwardingSucceeded by remember { + derivedStateOf { forwardingActionState.value.dataOrNull() } + } + + fun handleEvents(event: ForwardMessagesEvents) { + when (event) { + is ForwardMessagesEvents.SetSelectedRoom -> { + selectedRooms = persistentListOf(event.room) + // Restore for multi-selection +// val index = selectedRooms.indexOfFirst { it.roomId == event.room.roomId } +// selectedRooms = if (index >= 0) { +// selectedRooms.removeAt(index) +// } else { +// selectedRooms.add(event.room) +// } + } + ForwardMessagesEvents.RemoveSelectedRoom -> selectedRooms = persistentListOf() + is ForwardMessagesEvents.UpdateQuery -> query = event.query + ForwardMessagesEvents.ToggleSearchActive -> isSearchActive = !isSearchActive + ForwardMessagesEvents.ForwardEvent -> { + isSearchActive = false + val roomIds = selectedRooms.map { it.roomId }.toPersistentList() + matrixCoroutineScope.forwardEvent(eventId, roomIds, forwardingActionState) + } + ForwardMessagesEvents.ClearError -> forwardingActionState.value = Async.Uninitialized + } + } + + return ForwardMessagesState( + resultState = results, + query = query, + isSearchActive = isSearchActive, + selectedRooms = selectedRooms, + isForwarding = forwardingActionState.value.isLoading(), + error = (forwardingActionState.value as? Async.Failure)?.error, + forwardingSucceeded = forwardingSucceeded, + eventSink = { handleEvents(it) } + ) + } + + private fun CoroutineScope.forwardEvent( + eventId: EventId, + roomIds: ImmutableList, + isForwardMessagesState: MutableState>>, + ) = launch { + isForwardMessagesState.value = Async.Loading() + room.forwardEvent(eventId, roomIds).fold( + { isForwardMessagesState.value = Async.Success(roomIds) }, + { isForwardMessagesState.value = Async.Failure(it) } + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt new file mode 100644 index 0000000000..7540766097 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesState.kt @@ -0,0 +1,33 @@ +/* + * 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.messages.impl.forward + +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomSummaryDetails +import kotlinx.collections.immutable.ImmutableList + +data class ForwardMessagesState( + val resultState: SearchBarResultState>, + val query: String, + val isSearchActive: Boolean, + val selectedRooms: ImmutableList, + val isForwarding: Boolean, + val error: Throwable?, + val forwardingSucceeded: ImmutableList?, + val eventSink: (ForwardMessagesEvents) -> Unit +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt new file mode 100644 index 0000000000..75aacea616 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesStateProvider.kt @@ -0,0 +1,106 @@ +/* + * 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.messages.impl.forward + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomMember +import io.element.android.libraries.matrix.api.room.RoomSummaryDetails +import io.element.android.libraries.matrix.api.room.message.RoomMessage +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +open class ForwardMessagesStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aForwardMessagesState(), + aForwardMessagesState(query = "Test"), + aForwardMessagesState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList())), + aForwardMessagesState(resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), query = "Test"), + aForwardMessagesState( + resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), + query = "Test", + selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))) + ), + aForwardMessagesState( + resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), + query = "Test", + selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))), + isForwarding = true, + ), + aForwardMessagesState( + resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), + query = "Test", + selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))), + forwardingSucceeded = persistentListOf(RoomId("!room2:domain")), + ), + aForwardMessagesState( + resultState = SearchBarResultState.Results(aForwardMessagesRoomList()), + query = "Test", + selectedRooms = persistentListOf(aRoomDetailsState(roomId = RoomId("!room2:domain"))), + error = Throwable("error"), + ), + // Add other states here + ) +} + +fun aForwardMessagesState( + resultState: SearchBarResultState> = SearchBarResultState.NotSearching(), + query: String = "", + isSearchActive: Boolean = false, + selectedRooms: ImmutableList = persistentListOf(), + isForwarding: Boolean = false, + error: Throwable? = null, + forwardingSucceeded: ImmutableList? = null, +) = ForwardMessagesState( + resultState = resultState, + query = query, + isSearchActive = isSearchActive, + selectedRooms = selectedRooms, + isForwarding = isForwarding, + error = error, + forwardingSucceeded = forwardingSucceeded, + eventSink = {} +) + +internal fun aForwardMessagesRoomList() = listOf( + aRoomDetailsState(), + aRoomDetailsState(roomId = RoomId("!room2:domain"), canonicalAlias = "#element-x-room:matrix.org"), +) + +fun aRoomDetailsState( + roomId: RoomId = RoomId("!room:domain"), + name: String = "roomName", + canonicalAlias: String? = null, + isDirect: Boolean = true, + avatarURLString: String? = null, + lastMessage: RoomMessage? = null, + lastMessageTimestamp: Long? = null, + unreadNotificationCount: Int = 0, + inviter: RoomMember? = null, +) = RoomSummaryDetails( + roomId = roomId, + name = name, + canonicalAlias = canonicalAlias, + isDirect = isDirect, + avatarURLString = avatarURLString, + lastMessage = lastMessage, + lastMessageTimestamp = lastMessageTimestamp, + unreadNotificationCount = unreadNotificationCount, + inviter = inviter, + ) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt new file mode 100644 index 0000000000..329aff2881 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/forward/ForwardMessagesView.kt @@ -0,0 +1,292 @@ +/* + * 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.messages.impl.forward + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +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.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.components.ProgressDialog +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogDefaults +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar +import io.element.android.libraries.designsystem.theme.components.Divider +import io.element.android.libraries.designsystem.theme.components.RadioButton +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.SearchBar +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.designsystem.theme.roomListRoomMessage +import io.element.android.libraries.designsystem.theme.roomListRoomName +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomSummaryDetails +import io.element.android.libraries.matrix.ui.components.SelectedRoom +import kotlinx.collections.immutable.ImmutableList +import io.element.android.libraries.ui.strings.R as StringR + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun ForwardMessagesView( + state: ForwardMessagesState, + onDismiss: () -> Unit, + onForwardingSucceeded: (ImmutableList) -> Unit, + modifier: Modifier = Modifier, +) { + if (state.forwardingSucceeded != null) { + onForwardingSucceeded(state.forwardingSucceeded) + return + } + + fun onRoomRemoved(roomSummaryDetails: RoomSummaryDetails) { + // TODO toggle selection when multi-selection is enabled + state.eventSink(ForwardMessagesEvents.RemoveSelectedRoom) + } + + @Composable + fun SelectedRoomsHelper(isForwarding: Boolean, selectedRooms: ImmutableList) { + if (isForwarding) return + SelectedRooms( + selectedRooms = selectedRooms, + onRoomRemoved = ::onRoomRemoved, + modifier = Modifier.padding(vertical = 16.dp) + ) + } + + fun onBackButton(state: ForwardMessagesState) { + if (state.isSearchActive) { + state.eventSink(ForwardMessagesEvents.ToggleSearchActive) + } else { + onDismiss() + } + } + + BackHandler(onBack = { onBackButton(state) }) + + Scaffold( + modifier = modifier, + topBar = { + CenterAlignedTopAppBar( + title = { Text(stringResource(StringR.string.common_forward_message), style = ElementTextStyles.Bold.callout) }, + navigationIcon = { + BackButton(onClick = { onBackButton(state) }) + }, + actions = { + TextButton( + enabled = state.selectedRooms.isNotEmpty(), + onClick = { state.eventSink(ForwardMessagesEvents.ForwardEvent) } + ) { + Text(text = stringResource(StringR.string.action_send)) + } + } + ) + } + ) { paddingValues -> + Column( + Modifier + .padding(paddingValues) + .consumeWindowInsets(paddingValues) + ) { + SearchBar>( + placeHolderTitle = stringResource(StringR.string.action_search), + query = state.query, + onQueryChange = { state.eventSink(ForwardMessagesEvents.UpdateQuery(it)) }, + active = state.isSearchActive, + onActiveChange = { state.eventSink(ForwardMessagesEvents.ToggleSearchActive) }, + resultState = state.resultState, + showBackButton = false, + ) { summaries -> + LazyColumn { + item { + SelectedRoomsHelper( + isForwarding = state.isForwarding, + selectedRooms = state.selectedRooms + ) + } + items(summaries, key = { it.roomId.value }) { roomSummary -> + Column { + RoomSummaryView( + roomSummary, + isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId }, + onSelection = { roomSummary -> + state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary)) + } + ) + Divider(modifier = Modifier.fillMaxWidth()) + } + } + } + } + + if (!state.isSearchActive) { + // TODO restore for multi-selection +// SelectedRoomsHelper( +// isForwarding = state.isForwarding, +// selectedRooms = state.selectedRooms +// ) + Spacer(modifier = Modifier.height(20.dp)) + + if (state.resultState is SearchBarResultState.Results) { + LazyColumn { + items(state.resultState.results, key = { it.roomId.value }) { roomSummary -> + Column { + RoomSummaryView( + roomSummary, + isSelected = state.selectedRooms.any { it.roomId == roomSummary.roomId }, + onSelection = { roomSummary -> + state.eventSink(ForwardMessagesEvents.SetSelectedRoom(roomSummary)) + } + ) + Divider(modifier = Modifier.fillMaxWidth()) + } + } + } + } + } + + if (state.isForwarding) { + ProgressDialog() + } + + if (state.error != null) { + ForwardingErrorDialog(onDismiss = { state.eventSink(ForwardMessagesEvents.ClearError) }) + } + } + } +} + +@Composable +internal fun SelectedRooms( + selectedRooms: ImmutableList, + onRoomRemoved: (RoomSummaryDetails) -> Unit, + modifier: Modifier = Modifier, +) { + LazyRow( + modifier, + contentPadding = PaddingValues(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(32.dp) + ) { + items(selectedRooms, key = { it.roomId.value }) { roomSummary -> + SelectedRoom(roomSummary = roomSummary, onRoomRemoved = onRoomRemoved) + } + } +} + +@Composable +internal fun RoomSummaryView( + summary: RoomSummaryDetails, + isSelected: Boolean, + onSelection: (RoomSummaryDetails) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .clickable { onSelection(summary) } + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(IntrinsicSize.Min), + verticalAlignment = Alignment.CenterVertically + ) { + val roomAlias = summary.canonicalAlias ?: summary.roomId.value + Avatar( + avatarData = AvatarData(id = roomAlias, name = summary.name, url = summary.avatarURLString), + ) + Column( + modifier = Modifier + .padding(start = 12.dp, end = 4.dp, top = 8.dp, bottom = 8.dp) + .alignByBaseline() + .weight(1f) + ) { + // Name + Text( + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + text = summary.name, + color = MaterialTheme.roomListRoomName(), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + // Id + Text( + text = roomAlias, + color = MaterialTheme.roomListRoomMessage(), + fontSize = 14.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + RadioButton(selected = isSelected, onClick = { onSelection(summary) }) + } +} + +@Composable +private fun ForwardingErrorDialog(onDismiss: () -> Unit, modifier: Modifier = Modifier) { + ErrorDialog( + content = ErrorDialogDefaults.title, + onDismiss = onDismiss, + modifier = modifier, + ) +} + +@Preview +@Composable +fun ForwardMessagesViewLightPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun ForwardMessagesViewDarkPreview(@PreviewParameter(ForwardMessagesStateProvider::class) state: ForwardMessagesState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: ForwardMessagesState) { + ForwardMessagesView( + state = state, + onDismiss = {}, + onForwardingSucceeded = {} + ) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt new file mode 100644 index 0000000000..d9dd135eca --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt @@ -0,0 +1,37 @@ +/* + * 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.messages + +import io.element.android.features.messages.impl.MessagesNavigator +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo + +class FakeMessagesNavigator : MessagesNavigator { + var onShowEventDebugInfoClickedCount = 0 + private set + + var onForwardEventClickedCount = 0 + private set + + override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { + onShowEventDebugInfoClickedCount++ + } + + override fun onForwardEventClicked(eventId: EventId) { + onForwardEventClickedCount++ + } +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 4a71ec5546..375af5b8cd 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -100,7 +100,8 @@ class MessagesPresenterTest { @Test fun `present - handle action forward`() = runTest { - val presenter = createMessagePresenter() + val navigator = FakeMessagesNavigator() + val presenter = createMessagePresenter(navigator = navigator) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -108,6 +109,7 @@ class MessagesPresenterTest { val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Forward, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + assertThat(navigator.onForwardEventClickedCount).isEqualTo(1) } } @@ -308,7 +310,8 @@ class MessagesPresenterTest { @Test fun `present - handle action show developer info`() = runTest { - val presenter = createMessagePresenter() + val navigator = FakeMessagesNavigator() + val presenter = createMessagePresenter(navigator = navigator) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -316,6 +319,7 @@ class MessagesPresenterTest { val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.Developer, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + assertThat(navigator.onShowEventDebugInfoClickedCount).isEqualTo(1) } } @@ -347,7 +351,8 @@ class MessagesPresenterTest { private fun TestScope.createMessagePresenter( coroutineDispatchers: CoroutineDispatchers = testCoroutineDispatchers(), - matrixRoom: MatrixRoom = FakeMatrixRoom() + matrixRoom: MatrixRoom = FakeMatrixRoom(), + navigator: FakeMessagesNavigator = FakeMessagesNavigator(), ): MessagesPresenter { val messageComposerPresenter = MessageComposerPresenter( appCoroutineScope = this, @@ -388,6 +393,7 @@ class MessagesPresenterTest { networkMonitor = FakeNetworkMonitor(), snackbarDispatcher = SnackbarDispatcher(), messageSummaryFormatter = FakeMessageSummaryFormatter(), + navigator = navigator, dispatchers = coroutineDispatchers, ) } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt new file mode 100644 index 0000000000..b4efaca864 --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/forward/ForwardMessagesPresenterTests.kt @@ -0,0 +1,177 @@ +/* + * 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.messages.forward + +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.messages.impl.forward.ForwardMessagesEvents +import io.element.android.features.messages.impl.forward.ForwardMessagesPresenter +import io.element.android.libraries.designsystem.theme.components.SearchBarResultState +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.room.RoomSummary +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.FakeMatrixClient +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import io.element.android.libraries.matrix.test.room.FakeRoomSummaryDataSource +import io.element.android.libraries.matrix.test.room.aRoomSummaryDetail +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ForwardMessagesPresenterTests { + + @Test + fun `present - initial state`() = runTest { + val presenter = aPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.selectedRooms).isEmpty() + assertThat(initialState.resultState).isInstanceOf(SearchBarResultState.NotSearching::class.java) + assertThat(initialState.isSearchActive).isFalse() + assertThat(initialState.isForwarding).isFalse() + assertThat(initialState.error).isNull() + assertThat(initialState.forwardingSucceeded).isNull() + + // Search is run automatically + val searchState = awaitItem() + assertThat(searchState.resultState).isInstanceOf(SearchBarResultState.NoResults::class.java) + } + } + + @Test + fun `present - toggle search active`() = runTest { + val presenter = aPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + val summary = aRoomSummaryDetail() + + initialState.eventSink(ForwardMessagesEvents.ToggleSearchActive) + assertThat(awaitItem().isSearchActive).isTrue() + + initialState.eventSink(ForwardMessagesEvents.ToggleSearchActive) + assertThat(awaitItem().isSearchActive).isFalse() + } + } + + @Test + fun `present - update query`() = runTest { + val roomSummaryDataSource = FakeRoomSummaryDataSource().apply { + postRoomSummary(listOf(RoomSummary.Filled(aRoomSummaryDetail()))) + } + val client = FakeMatrixClient(roomSummaryDataSource = roomSummaryDataSource) + val presenter = aPresenter(client = client) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(awaitItem().resultState as? SearchBarResultState.Results).isEqualTo(SearchBarResultState.Results(listOf(aRoomSummaryDetail()))) + + initialState.eventSink(ForwardMessagesEvents.UpdateQuery("string not contained")) + assertThat(awaitItem().query).isEqualTo("string not contained") + assertThat(awaitItem().resultState).isInstanceOf(SearchBarResultState.NoResults::class.java) + } + } + + @Test + fun `present - select a room and forward successful`() = runTest { + val presenter = aPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + val summary = aRoomSummaryDetail() + + initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary)) + awaitItem() + + // Test successful forwarding + initialState.eventSink(ForwardMessagesEvents.ForwardEvent) + + val forwardingState = awaitItem() + assertThat(forwardingState.isSearchActive).isFalse() + assertThat(forwardingState.isForwarding).isTrue() + + val successfulForwardState = awaitItem() + assertThat(successfulForwardState.isForwarding).isFalse() + assertThat(successfulForwardState.forwardingSucceeded).isNotNull() + } + } + + @Test + fun `present - select a room and forward failed, then clear`() = runTest { + val room = FakeMatrixRoom() + val presenter = aPresenter(fakeMatrixRoom = room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + val summary = aRoomSummaryDetail() + + initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary)) + awaitItem() + + // Test failed forwarding + room.givenForwardEventResult(Result.failure(Throwable("error"))) + initialState.eventSink(ForwardMessagesEvents.ForwardEvent) + skipItems(1) + + val failedForwardState = awaitItem() + assertThat(failedForwardState.isForwarding).isFalse() + assertThat(failedForwardState.error).isNotNull() + + // Then clear error + initialState.eventSink(ForwardMessagesEvents.ClearError) + assertThat(awaitItem().error).isNull() + } + } + + @Test + fun `present - select and remove a room`() = runTest { + val presenter = aPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + skipItems(1) + val summary = aRoomSummaryDetail() + + initialState.eventSink(ForwardMessagesEvents.SetSelectedRoom(summary)) + assertThat(awaitItem().selectedRooms).isEqualTo(persistentListOf(summary)) + + initialState.eventSink(ForwardMessagesEvents.RemoveSelectedRoom) + assertThat(awaitItem().selectedRooms).isEmpty() + } + } + + private fun CoroutineScope.aPresenter( + eventId: EventId = AN_EVENT_ID, + fakeMatrixRoom: FakeMatrixRoom = FakeMatrixRoom(), + coroutineScope: CoroutineScope = this, + client: FakeMatrixClient = FakeMatrixClient(), + ) = ForwardMessagesPresenter(eventId.value, fakeMatrixRoom, coroutineScope, client) + +} diff --git a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt index 50a7a7bfbe..3eb2ab848d 100644 --- a/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt +++ b/features/roomlist/impl/src/main/kotlin/io/element/android/features/roomlist/impl/RoomListNode.kt @@ -33,7 +33,7 @@ import io.element.android.libraries.matrix.api.core.RoomId class RoomListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenter: RoomListPresenter, + private val presenter: RoomListPresenter, ) : Node(buildContext, plugins = plugins) { private fun onRoomClicked(roomId: RoomId) { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/ForwardEventException.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/ForwardEventException.kt new file mode 100644 index 0000000000..6b2813feb8 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/ForwardEventException.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.libraries.matrix.api.room + +import io.element.android.libraries.matrix.api.core.RoomId + +class ForwardEventException( + val roomIds: List +) : Exception() { + + override val message: String? = "Failed to deliver event to $roomIds rooms" +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 664ad9dbcb..9e8f8514b7 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -26,6 +26,7 @@ import io.element.android.libraries.matrix.api.media.FileInfo import io.element.android.libraries.matrix.api.media.ImageInfo import io.element.android.libraries.matrix.api.media.VideoInfo import io.element.android.libraries.matrix.api.timeline.MatrixTimeline +import io.element.android.libraries.matrix.api.timeline.item.event.MessageContent import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import java.io.Closeable @@ -84,6 +85,8 @@ interface MatrixRoom : Closeable { suspend fun sendReaction(emoji: String, eventId: EventId): Result + suspend fun forwardEvent(eventId: EventId, rooms: List): Result + suspend fun retrySendMessage(transactionId: String): Result suspend fun cancelSend(transactionId: String): Result diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index 29009f9637..268e09c764 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -29,6 +29,7 @@ import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.media.MatrixMediaLoader import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.pusher.PushersService +import io.element.android.libraries.matrix.api.room.ForwardEventException import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMembershipObserver import io.element.android.libraries.matrix.api.room.RoomSummaryDataSource @@ -40,6 +41,7 @@ import io.element.android.libraries.matrix.impl.core.toProgressWatcher import io.element.android.libraries.matrix.impl.media.RustMediaLoader import io.element.android.libraries.matrix.impl.notification.RustNotificationService import io.element.android.libraries.matrix.impl.pushers.RustPushersService +import io.element.android.libraries.matrix.impl.room.RoomContentForwarder import io.element.android.libraries.matrix.impl.room.RustMatrixRoom import io.element.android.libraries.matrix.impl.room.RustRoomSummaryDataSource import io.element.android.libraries.matrix.impl.sync.SlidingSyncObserverProxy @@ -52,6 +54,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job +import kotlinx.coroutines.async import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first @@ -62,6 +65,7 @@ import kotlinx.coroutines.withTimeout import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate import org.matrix.rustcomponents.sdk.RequiredState +import org.matrix.rustcomponents.sdk.RoomMessageEventContent import org.matrix.rustcomponents.sdk.SlidingSyncList import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder import org.matrix.rustcomponents.sdk.SlidingSyncListOnceBuilt @@ -199,6 +203,8 @@ class RustMatrixClient constructor( private val roomMembershipObserver = RoomMembershipObserver() + private val roomContentForwarder = RoomContentForwarder(slidingSync) + init { client.setDelegate(clientDelegate) rustRoomSummaryDataSource.init() @@ -220,6 +226,7 @@ class RustMatrixClient constructor( coroutineScope = coroutineScope, coroutineDispatchers = dispatchers, clock = clock, + roomContentForwarder = roomContentForwarder, ) } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.kt new file mode 100644 index 0000000000..1f68c14456 --- /dev/null +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomContentForwarder.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.libraries.matrix.impl.room + +import io.element.android.libraries.core.coroutine.parallelMap +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.ForwardEventException +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.withTimeout +import org.matrix.rustcomponents.sdk.Room +import org.matrix.rustcomponents.sdk.SlidingSync +import org.matrix.rustcomponents.sdk.TimelineDiff +import org.matrix.rustcomponents.sdk.TimelineListener +import org.matrix.rustcomponents.sdk.genTransactionId +import kotlin.time.Duration.Companion.milliseconds + +/** + * Helper to forward event contents from a room to a set of other rooms. + * @param slidingSync the [SlidingSync] to fetch room instances to forward the event to + */ +class RoomContentForwarder( + private val slidingSync: SlidingSync, +) { + + /** + * Forwards the event with the given [eventId] from the [fromRoom] to the given [toRoomIds]. + * @param fromRoom the room to forward the event from + * @param eventId the id of the event to forward + * @param toRoomIds the ids of the rooms to forward the event to + * @param timeoutMs the maximum time in milliseconds to wait for the event to be sent to a room + */ + suspend fun forward( + fromRoom: Room, + eventId: EventId, + toRoomIds: List, + timeoutMs: Long = 5000L + ) { + val content = fromRoom.getTimelineEventContentByEventId(eventId.value) + val targetSlidingSyncRooms = toRoomIds.mapNotNull { roomId -> slidingSync.getRoom(roomId.value) } + val targetRooms = targetSlidingSyncRooms.mapNotNull { slidingSyncRoom -> slidingSyncRoom.use { it.fullRoom() } } + val failedForwardingTo = mutableSetOf() + targetRooms.parallelMap { room -> + room.use { targetRoom -> + val result = runCatching { + // Sending a message requires a registered timeline listener + targetRoom.addTimelineListener(NoOpTimelineListener) + withTimeout(timeoutMs.milliseconds) { + targetRoom.send(content, genTransactionId()) + } + } + // After sending, we remove the timeline + targetRoom.removeTimeline() + result + }.onFailure { + failedForwardingTo.add(RoomId(room.id())) + if (it is CancellationException) { + throw it + } + } + } + + if (failedForwardingTo.isNotEmpty()) { + throw ForwardEventException(toRoomIds.toList()) + } + } + + private object NoOpTimelineListener: TimelineListener { + override fun onUpdate(diff: TimelineDiff) = Unit + } +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index 8db9619c34..c1adbc0cb9 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -50,6 +50,7 @@ import org.matrix.rustcomponents.sdk.SlidingSyncRoom import org.matrix.rustcomponents.sdk.UpdateSummary import org.matrix.rustcomponents.sdk.genTransactionId import org.matrix.rustcomponents.sdk.messageEventContentFromMarkdown +import timber.log.Timber import java.io.File class RustMatrixRoom( @@ -60,6 +61,7 @@ class RustMatrixRoom( private val coroutineScope: CoroutineScope, private val coroutineDispatchers: CoroutineDispatchers, private val clock: SystemClock, + private val roomContentForwarder: RoomContentForwarder, ) : MatrixRoom { override val membersStateFlow: StateFlow @@ -277,6 +279,14 @@ class RustMatrixRoom( } } + override suspend fun forwardEvent(eventId: EventId, roomIds: List): Result = withContext(coroutineDispatchers.io) { + runCatching { + roomContentForwarder.forward(fromRoom = innerRoom, eventId = eventId, toRoomIds = roomIds) + }.onFailure { + Timber.e(it) + } + } + override suspend fun retrySendMessage(transactionId: String): Result = withContext(coroutineDispatchers.io) { runCatching { diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt index 34c0a9cb48..41c602f0d2 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustRoomSummaryDataSource.kt @@ -75,9 +75,9 @@ internal class RustRoomSummaryDataSource( .launchIn(this) slidingSyncList.state(this) - .onEach { slidingSyncState -> - Timber.v("New sliding sync state: $slidingSyncState") - state.value = slidingSyncState + .onEach { SlidingSyncListLoadingState -> + Timber.v("New sliding sync state: $SlidingSyncListLoadingState") + state.value = SlidingSyncListLoadingState }.launchIn(this) } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 01f1d17f7c..5b641b8883 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -75,6 +75,7 @@ class FakeMatrixRoom( private var sendReactionResult = Result.success(Unit) private var retrySendMessageResult = Result.success(Unit) private var cancelSendResult = Result.success(Unit) + private var forwardEventResult = Result.success(Unit) var sendMediaCount = 0 private set @@ -218,6 +219,10 @@ class FakeMatrixRoom( override suspend fun sendFile(file: File, fileInfo: FileInfo, progressCallback: ProgressCallback?): Result = fakeSendMedia() + override suspend fun forwardEvent(eventId: EventId, rooms: List): Result = simulateLongTask { + forwardEventResult + } + private suspend fun fakeSendMedia(): Result = simulateLongTask { sendMediaResult.onSuccess { sendMediaCount++ @@ -329,4 +334,8 @@ class FakeMatrixRoom( fun givenCancelSendResult(result: Result) { cancelSendResult = result } + + fun givenForwardEventResult(result: Result) { + forwardEventResult = result + } } diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt new file mode 100644 index 0000000000..da305ce212 --- /dev/null +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/SelectedRoom.kt @@ -0,0 +1,118 @@ +/* + * 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.matrix.ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.Surface +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.room.RoomSummaryDetails +import io.element.android.libraries.matrix.api.user.MatrixUser +import io.element.android.libraries.ui.strings.R as StringR + +@Composable +fun SelectedRoom( + roomSummary: RoomSummaryDetails, + modifier: Modifier = Modifier, + onRoomRemoved: (RoomSummaryDetails) -> Unit = {}, +) { + Box(modifier = modifier + .width(56.dp) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Avatar(AvatarData(roomSummary.roomId.value, roomSummary.name, roomSummary.avatarURLString, AvatarSize.Custom(56.dp))) + Text( + text = roomSummary.name, + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.bodyLarge, + ) + } + Surface( + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .clip(CircleShape) + .size(20.dp) + .align(Alignment.TopEnd) + .clickable( + indication = rememberRipple(), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onRoomRemoved(roomSummary) } + ), + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(id = StringR.string.action_remove), + tint = MaterialTheme.colorScheme.onPrimary, + modifier = Modifier.padding(2.dp) + ) + } + } +} + +@Preview +@Composable +internal fun SelectedRoomLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun SelectedRoomDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + SelectedRoom(roomSummary = + RoomSummaryDetails( + roomId = RoomId("!room:domain"), + name = "roomName", + canonicalAlias = null, + isDirect = true, + avatarURLString = null, + lastMessage = null, + lastMessageTimestamp = null, + unreadNotificationCount = 0, + inviter = null, + ) + ) +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..96d99a3ad8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b410a2cd4cdadf7fb69fc0eb307882a1eedb70710ea3a2b8fefff9fe0f4ff3a9 +size 13266 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0230e1291e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c046931631c1d1ee9abc55e5c03d16ec7fb88d1829973342e3c358b2bd99d6c4 +size 12809 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d32361190b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ec44a976cb2a7572df5d18858ccfef5d0c8fe77ef0ed3a0c1d2bd7615aa32324 +size 33230 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..88d7384886 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad6debeeb7774b50e8f578c0b8c1b91f92ce15d99ac5ccd8401eec7286e098fb +size 32766 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0269187ab1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b27c645192fd1e2909483c56d6e4b0251cd1b2cb1e39d7b3d5987eb5a4aad851 +size 33038 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0269187ab1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b27c645192fd1e2909483c56d6e4b0251cd1b2cb1e39d7b3d5987eb5a4aad851 +size 33038 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..0269187ab1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewDarkPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b27c645192fd1e2909483c56d6e4b0251cd1b2cb1e39d7b3d5987eb5a4aad851 +size 33038 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e4bb2e40d3 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9d415f55a0fc2e463c53c2182a70fc56d08e969fd01492b5ba4dd712653aede3 +size 13018 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dc85231b66 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83fb3a09e70802c385ccda4cb46763cc9b949eaeaf9f572c4a38cb8bb1ab6516 +size 12518 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a1532918f4 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:42042704bffae9e397b139effe9fa6213ec9c2167c2139db11b2611a6ebfd9cc +size 31965 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..57c9f2d3d1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5726218ec996aff3e52aa6539ece216dfe20294d0ee13939b7f0c9da7bd7555f +size 31504 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..82280a75c0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a78aa398ce214169e551470b5a45d84c19739c4f222d6bf4bd0307f1c97d0cc +size 32230 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..82280a75c0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a78aa398ce214169e551470b5a45d84c19739c4f222d6bf4bd0307f1c97d0cc +size 32230 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_6,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_6,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_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.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_7,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..82280a75c0 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.forward_null_DefaultGroup_ForwardMessagesViewLightPreview_0_null_7,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a78aa398ce214169e551470b5a45d84c19739c4f222d6bf4bd0307f1c97d0cc +size 32230 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_SelectedRoomDarkPreview_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.matrix.ui.components_null_DefaultGroup_SelectedRoomDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..26ea6aa891 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_SelectedRoomDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbd3d24533bfa534b3379c8243c2a5af3744a6ef73ed294e1d78faea3ef855fa +size 12949 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_SelectedRoomLightPreview_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.matrix.ui.components_null_DefaultGroup_SelectedRoomLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..2d3ed5547d --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.matrix.ui.components_null_DefaultGroup_SelectedRoomLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:31a47c956fac22a7add0ede634c327112d42c65ffea42e19afdb75d157b55788 +size 12390 From 2e88f3214d05b90da7a886659abcf48c8ce6c75d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 23 Jun 2023 08:10:37 +0200 Subject: [PATCH 128/130] Update plugin ktlint to v11.4.2 (#661) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 046661f7e3..86a18b013b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -190,7 +190,7 @@ kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } anvil = { id = "com.squareup.anvil", version.ref = "anvil" } detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } -ktlint = "org.jlleitschuh.gradle.ktlint:11.4.1" +ktlint = "org.jlleitschuh.gradle.ktlint:11.4.2" dependencygraph = { id = "com.savvasdalkitsis.module-dependency-graph", version.ref = "dependencygraph" } dependencycheck = { id = "org.owasp.dependencycheck", version.ref = "dependencycheck" } dependencyanalysis = { id = "com.autonomousapps.dependency-analysis", version.ref = "dependencyanalysis" } From 898985cb9b03f88cd85f9484cafec2d870412a2c Mon Sep 17 00:00:00 2001 From: Marco Romano Date: Fri, 23 Jun 2023 09:57:21 +0200 Subject: [PATCH 129/130] Expose new `windowInsets` param from `ModalBottomSheet` (#662) Part of new public API in compose.material3:1.1.1 --- .../designsystem/theme/components/ModalBottomSheet.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt index f7ca7ba1ce..27d9f101f7 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/ModalBottomSheet.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.designsystem.theme.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.BottomSheetDefaults @@ -52,6 +53,7 @@ fun ModalBottomSheet( tonalElevation: Dp = BottomSheetDefaults.Elevation, scrimColor: Color = BottomSheetDefaults.ScrimColor, dragHandle: @Composable (() -> Unit)? = { BottomSheetDefaults.DragHandle() }, + windowInsets: WindowInsets = BottomSheetDefaults.windowInsets, content: @Composable ColumnScope.() -> Unit, ) { androidx.compose.material3.ModalBottomSheet( @@ -64,6 +66,7 @@ fun ModalBottomSheet( tonalElevation = tonalElevation, scrimColor = scrimColor, dragHandle = dragHandle, + windowInsets = windowInsets, content = content, ) } From bdb1841e44b51ef7da5e8f848962ba3c8e31f47f Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Fri, 23 Jun 2023 10:44:47 +0200 Subject: [PATCH 130/130] [Message Actions] Report messages (#642) * Add report messages feature * Try to improve how snackbars are delivered --------- Co-authored-by: ElementBot --- .../io/element/android/x/MainActivity.kt | 42 ++-- .../io/element/android/x/di/AppBindings.kt | 2 + changelog.d/489.feature | 1 + .../messages/impl/MessagesFlowNode.kt | 14 ++ .../messages/impl/MessagesNavigator.kt | 2 + .../features/messages/impl/MessagesNode.kt | 5 + .../messages/impl/MessagesPresenter.kt | 7 +- .../impl/report/ReportMessageEvents.kt | 24 +++ .../messages/impl/report/ReportMessageNode.kt | 60 ++++++ .../impl/report/ReportMessagePresenter.kt | 98 +++++++++ .../impl/report/ReportMessageState.kt | 26 +++ .../impl/report/ReportMessageStateProvider.kt | 44 +++++ .../messages/impl/report/ReportMessageView.kt | 186 ++++++++++++++++++ .../messages/FakeMessagesNavigator.kt | 8 + .../messages/MessagesPresenterTest.kt | 4 +- .../media/viewer/MediaViewerPresenterTest.kt | 20 +- .../report/ReportMessagePresenterTests.kt | 142 +++++++++++++ .../libraries/designsystem/utils/Snackbar.kt | 22 ++- .../libraries/matrix/api/room/MatrixRoom.kt | 2 + .../matrix/impl/room/RustMatrixRoom.kt | 9 + .../matrix/test/room/FakeMatrixRoom.kt | 17 ++ tests/uitests/build.gradle.kts | 1 + .../android/tests/uitests/ScreenshotTest.kt | 4 + ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_3,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_4,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_5,NEXUS_5,1.0,en].png | 3 + tools/detekt/detekt.yml | 3 +- 36 files changed, 739 insertions(+), 40 deletions(-) create mode 100644 changelog.d/489.feature create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageState.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt create mode 100644 features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt create mode 100644 features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_3,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_4,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_5,NEXUS_5,1.0,en].png diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index e8a51cca22..010035e1f7 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/app/src/main/kotlin/io/element/android/x/MainActivity.kt @@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Modifier import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.WindowCompat @@ -33,6 +34,7 @@ import com.bumble.appyx.core.plugin.NodeReadyObserver import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.utils.LocalSnackbarDispatcher import io.element.android.x.di.AppBindings import timber.log.Timber @@ -42,11 +44,13 @@ class MainActivity : NodeComponentActivity() { private lateinit var mainNode: MainNode + private lateinit var appBindings: AppBindings + override fun onCreate(savedInstanceState: Bundle?) { Timber.tag(loggerTag.value).w("onCreate, with savedInstanceState: ${savedInstanceState != null}") installSplashScreen() super.onCreate(savedInstanceState) - val appBindings = bindings() + appBindings = bindings() appBindings.matrixClientsHolder().restore(savedInstanceState) WindowCompat.setDecorFitsSystemWindows(window, false) setContent { @@ -57,25 +61,29 @@ class MainActivity : NodeComponentActivity() { @Composable private fun MainContent(appBindings: AppBindings) { ElementTheme { - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.background), + CompositionLocalProvider( + LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(), ) { - NodeHost(integrationPoint = appyxIntegrationPoint) { - MainNode( - it, - appBindings.mainDaggerComponentOwner(), - plugins = listOf( - object : NodeReadyObserver { - override fun init(node: MainNode) { - Timber.tag(loggerTag.value).w("onMainNodeInit") - mainNode = node - mainNode.handleIntent(intent) + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { + NodeHost(integrationPoint = appyxIntegrationPoint) { + MainNode( + it, + appBindings.mainDaggerComponentOwner(), + plugins = listOf( + object : NodeReadyObserver { + override fun init(node: MainNode) { + Timber.tag(loggerTag.value).w("onMainNodeInit") + mainNode = node + mainNode.handleIntent(intent) + } } - } + ) ) - ) + } } } } diff --git a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt index 9d5fa9446f..59f7e98d20 100644 --- a/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt +++ b/app/src/main/kotlin/io/element/android/x/di/AppBindings.kt @@ -18,10 +18,12 @@ package io.element.android.x.di import com.squareup.anvil.annotations.ContributesTo import io.element.android.appnav.di.MatrixClientsHolder +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import io.element.android.libraries.di.AppScope @ContributesTo(AppScope::class) interface AppBindings { fun matrixClientsHolder(): MatrixClientsHolder fun mainDaggerComponentOwner(): MainDaggerComponentsOwner + fun snackbarDispatcher(): SnackbarDispatcher } diff --git a/changelog.d/489.feature b/changelog.d/489.feature new file mode 100644 index 0000000000..4ffd2b7a4a --- /dev/null +++ b/changelog.d/489.feature @@ -0,0 +1 @@ +Add option to report inappropriate content diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt index e74c55395f..901716451f 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesFlowNode.kt @@ -35,12 +35,14 @@ import io.element.android.features.messages.impl.attachments.preview.Attachments import io.element.android.features.messages.impl.forward.ForwardMessagesNode import io.element.android.features.messages.impl.media.local.MediaInfo import io.element.android.features.messages.impl.media.viewer.MediaViewerNode +import io.element.android.features.messages.impl.report.ReportMessageNode import io.element.android.features.messages.impl.timeline.debug.EventDebugInfoNode import io.element.android.features.messages.impl.timeline.model.TimelineItem import io.element.android.features.messages.impl.timeline.model.event.TimelineItemFileContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemImageContent import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVideoContent import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.core.EventId @@ -83,6 +85,9 @@ class MessagesFlowNode @AssistedInject constructor( @Parcelize data class ForwardEvent(val eventId: EventId) : NavTarget + + @Parcelize + data class ReportMessage(val eventId: EventId, val senderId: UserId) : NavTarget } private val callback = plugins().firstOrNull() @@ -114,6 +119,10 @@ class MessagesFlowNode @AssistedInject constructor( override fun onForwardEventClicked(eventId: EventId) { backstack.push(NavTarget.ForwardEvent(eventId)) } + + override fun onReportMessage(eventId: EventId, senderId: UserId) { + backstack.push(NavTarget.ReportMessage(eventId, senderId)) + } } createNode(buildContext, listOf(callback)) } @@ -142,6 +151,10 @@ class MessagesFlowNode @AssistedInject constructor( } createNode(buildContext, listOf(inputs, callback)) } + is NavTarget.ReportMessage -> { + val inputs = ReportMessageNode.Inputs(navTarget.eventId, navTarget.senderId) + createNode(buildContext, listOf(inputs)) + } } } @@ -197,6 +210,7 @@ class MessagesFlowNode @AssistedInject constructor( Children( navModel = backstack, modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler(), ) } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt index d4733da047..201173a0bf 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNavigator.kt @@ -17,9 +17,11 @@ package io.element.android.features.messages.impl import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo interface MessagesNavigator { fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) fun onForwardEventClicked(eventId: EventId) + fun onReportContentClicked(eventId: EventId, senderId: UserId) } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt index 8624b62e75..651ae8670b 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesNode.kt @@ -50,6 +50,7 @@ class MessagesNode @AssistedInject constructor( fun onUserDataClicked(userId: UserId) fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) fun onForwardEventClicked(eventId: EventId) + fun onReportMessage(eventId: EventId, senderId: UserId) } private fun onRoomDetailsClicked() { @@ -75,6 +76,10 @@ class MessagesNode @AssistedInject constructor( callback?.onForwardEventClicked(eventId) } + override fun onReportContentClicked(eventId: EventId, senderId: UserId) { + callback?.onReportMessage(eventId, senderId) + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt index 6b64dc8bdd..e305b916cd 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt @@ -161,7 +161,7 @@ class MessagesPresenter @AssistedInject constructor( TimelineItemAction.Reply -> handleActionReply(targetEvent, composerState) TimelineItemAction.Developer -> handleShowDebugInfoAction(targetEvent) TimelineItemAction.Forward -> handleForwardAction(targetEvent) - TimelineItemAction.ReportContent -> notImplementedYet() + TimelineItemAction.ReportContent -> handleReportAction(targetEvent) } } @@ -241,4 +241,9 @@ class MessagesPresenter @AssistedInject constructor( if (event.eventId == null) return navigator.onForwardEventClicked(event.eventId) } + + private fun handleReportAction(event: TimelineItem.Event) { + if (event.eventId == null) return + navigator.onReportContentClicked(event.eventId, event.senderId) + } } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.kt new file mode 100644 index 0000000000..ed5ee029e7 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageEvents.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.messages.impl.report + +sealed interface ReportMessageEvents { + data class UpdateReason(val reason: String) : ReportMessageEvents + object ToggleBlockUser : ReportMessageEvents + object Report : ReportMessageEvents + object ClearError : ReportMessageEvents +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt new file mode 100644 index 0000000000..1be4571161 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageNode.kt @@ -0,0 +1,60 @@ +/* + * 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.messages.impl.report + +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.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId + +@ContributesNode(RoomScope::class) +class ReportMessageNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: ReportMessagePresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val eventId: EventId, + val senderId: UserId, + ) : NodeInputs + + private val inputs = inputs() + + private val presenter = presenterFactory.create( + ReportMessagePresenter.Inputs(inputs.eventId, inputs.senderId) + ) + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ReportMessageView( + state = state, + onBackClicked = ::navigateUp, + modifier = modifier + ) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt new file mode 100644 index 0000000000..09cad8e33b --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessagePresenter.kt @@ -0,0 +1,98 @@ +/* + * 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.messages.impl.report + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +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 androidx.compose.runtime.setValue +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.executeResult +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.designsystem.utils.SnackbarMessage +import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.MatrixRoom +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import io.element.android.libraries.ui.strings.R as StringR + +class ReportMessagePresenter @AssistedInject constructor( + private val room: MatrixRoom, + @Assisted private val inputs: Inputs, + private val snackbarDispatcher: SnackbarDispatcher, +) : Presenter { + + data class Inputs( + val eventId: EventId, + val senderId: UserId, + ) + + @AssistedFactory + interface Factory { + fun create(inputs: Inputs): ReportMessagePresenter + } + + @Composable + override fun present(): ReportMessageState { + val coroutineScope = rememberCoroutineScope() + var reason by rememberSaveable { mutableStateOf("") } + var blockUser by rememberSaveable { mutableStateOf(false) } + var result: MutableState> = remember { mutableStateOf(Async.Uninitialized) } + + fun handleEvents(event: ReportMessageEvents) { + when (event) { + is ReportMessageEvents.UpdateReason -> reason = event.reason + ReportMessageEvents.ToggleBlockUser -> blockUser = !blockUser + ReportMessageEvents.Report -> coroutineScope.report(inputs.eventId, inputs.senderId, reason, blockUser, result) + ReportMessageEvents.ClearError -> result.value = Async.Uninitialized + } + } + + return ReportMessageState( + reason = reason, + blockUser = blockUser, + result = result.value, + eventSink = ::handleEvents + ) + } + + private fun CoroutineScope.report( + eventId: EventId, + userId: UserId, + reason: String, + blockUser: Boolean, + result: MutableState>, + ) = launch { + suspend { + val userIdToBlock = userId.takeIf { blockUser } + room.reportContent(eventId, reason, userIdToBlock) + .onSuccess { + snackbarDispatcher.post(SnackbarMessage(StringR.string.common_report_submitted)) + } + }.executeResult(result) + } +} diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageState.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageState.kt new file mode 100644 index 0000000000..809668c88f --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageState.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.messages.impl.report + +import io.element.android.libraries.architecture.Async + +data class ReportMessageState( + val reason: String, + val blockUser: Boolean, + val result: Async, + val eventSink: (ReportMessageEvents) -> Unit +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt new file mode 100644 index 0000000000..89e6d7a220 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageStateProvider.kt @@ -0,0 +1,44 @@ +/* + * 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.messages.impl.report + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.architecture.Async + +open class ReportMessageStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aReportMessageState(), + aReportMessageState(reason = "This user is making the chat very toxic."), + aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true), + aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Loading()), + aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Failure(Throwable())), + aReportMessageState(reason = "This user is making the chat very toxic.", blockUser = true, result = Async.Success(Unit)), + // Add other states here + ) +} + +fun aReportMessageState( + reason: String = "", + blockUser: Boolean = false, + result: Async = Async.Uninitialized, +) = ReportMessageState( + reason = reason, + blockUser = blockUser, + result = result, + eventSink = {} +) diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt new file mode 100644 index 0000000000..1beee54519 --- /dev/null +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/report/ReportMessageView.kt @@ -0,0 +1,186 @@ +/* + * 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.messages.impl.report + +import androidx.compose.foundation.layout.Arrangement +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 +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +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.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +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.font.FontWeight +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.libraries.architecture.Async +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.button.ButtonWithProgress +import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar +import io.element.android.libraries.designsystem.theme.components.OutlinedTextField +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.ui.strings.R as StringR + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun ReportMessageView( + state: ReportMessageState, + onBackClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + val focusManager = LocalFocusManager.current + val isSending = state.result is Async.Loading + when (state.result) { + is Async.Success -> { + LaunchedEffect(state.result) { + onBackClicked() + } + return + } + is Async.Failure -> { + ErrorDialog( + content = stringResource(StringR.string.error_unknown), + onDismiss = { state.eventSink(ReportMessageEvents.ClearError) } + ) + } + else -> Unit + } + + Scaffold( + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + stringResource(StringR.string.action_report_content), + style = ElementTextStyles.Regular.callout, + fontWeight = FontWeight.Medium, + ) + }, + navigationIcon = { + BackButton(onClick = onBackClicked) + } + ) + }, + modifier = modifier + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .consumeWindowInsets(padding) + .imePadding() + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(16.dp) + ) { + Spacer(modifier = Modifier.height(20.dp)) + + OutlinedTextField( + value = state.reason, + onValueChange = { state.eventSink(ReportMessageEvents.UpdateReason(it)) }, + placeholder = { Text(stringResource(StringR.string.report_content_hint)) }, + enabled = !isSending, + modifier = Modifier + .fillMaxWidth() + .heightIn(min = 90.dp) + ) + Text( + text = stringResource(StringR.string.report_content_explanation), + style = ElementTextStyles.Regular.caption1, + color = MaterialTheme.colorScheme.secondary, + textAlign = TextAlign.Start, + modifier = Modifier.padding(top = 4.dp, bottom = 24.dp, start = 16.dp, end = 16.dp) + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 12.dp) + ) { + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = stringResource(StringR.string.screen_report_content_block_user), + style = ElementTextStyles.Regular.callout, + ) + Text( + text = stringResource(StringR.string.screen_report_content_block_user_hint), + style = ElementTextStyles.Regular.bodyMD, + color = MaterialTheme.colorScheme.secondary, + ) + } + Switch( + enabled = !isSending, + checked = state.blockUser, + onCheckedChange = { state.eventSink(ReportMessageEvents.ToggleBlockUser) }, + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + ButtonWithProgress( + text = stringResource(StringR.string.action_send), + enabled = state.reason.isNotBlank() && !isSending, + showProgress = isSending, + onClick = { + focusManager.clearFocus(force = true) + state.eventSink(ReportMessageEvents.Report) + }, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 4.dp) + ) + } + } +} + +@Preview +@Composable +fun ReportMessageViewLightPreview(@PreviewParameter(ReportMessageStateProvider::class) state: ReportMessageState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun ReportMessageViewDarkPreview(@PreviewParameter(ReportMessageStateProvider::class) state: ReportMessageState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: ReportMessageState) { + ReportMessageView( + onBackClicked = {}, + state = state, + ) +} diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt index d9dd135eca..8a374e5bcb 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/FakeMessagesNavigator.kt @@ -18,6 +18,7 @@ package io.element.android.features.messages import io.element.android.features.messages.impl.MessagesNavigator import io.element.android.libraries.matrix.api.core.EventId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.item.TimelineItemDebugInfo class FakeMessagesNavigator : MessagesNavigator { @@ -27,6 +28,9 @@ class FakeMessagesNavigator : MessagesNavigator { var onForwardEventClickedCount = 0 private set + var onReportContentClickedCount = 0 + private set + override fun onShowEventDebugInfoClicked(eventId: EventId, debugInfo: TimelineItemDebugInfo) { onShowEventDebugInfoClickedCount++ } @@ -34,4 +38,8 @@ class FakeMessagesNavigator : MessagesNavigator { override fun onForwardEventClicked(eventId: EventId) { onForwardEventClickedCount++ } + + override fun onReportContentClicked(eventId: EventId, senderId: UserId) { + onReportContentClickedCount++ + } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt index 375af5b8cd..40af424406 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt @@ -284,7 +284,8 @@ class MessagesPresenterTest { @Test fun `present - handle action report content`() = runTest { - val presenter = createMessagePresenter() + val navigator = FakeMessagesNavigator() + val presenter = createMessagePresenter(navigator = navigator) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -292,6 +293,7 @@ class MessagesPresenterTest { val initialState = awaitItem() initialState.eventSink.invoke(MessagesEvents.HandleAction(TimelineItemAction.ReportContent, aMessageEvent())) assertThat(awaitItem().actionListState.target).isEqualTo(ActionListState.Target.None) + assertThat(navigator.onReportContentClickedCount).isEqualTo(1) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt index 8a6025d3bd..2b66d2cf9b 100644 --- a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/media/viewer/MediaViewerPresenterTest.kt @@ -69,7 +69,8 @@ class MediaViewerPresenterTest { fun `present - check all actions `() = runTest { val mediaLoader = FakeMediaLoader() val mediaActions = FakeLocalMediaActions() - val presenter = aMediaViewerPresenter(mediaLoader, mediaActions) + val snackbarDispatcher = SnackbarDispatcher() + val presenter = aMediaViewerPresenter(mediaLoader, mediaActions, snackbarDispatcher) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -90,26 +91,24 @@ class MediaViewerPresenterTest { state.eventSink(MediaViewerEvents.SaveOnDisk) state = awaitItem() assertThat(state.snackbarMessage).isNotNull() - state = awaitItem() - assertThat(state.snackbarMessage).isNull() + snackbarDispatcher.clear() + assertThat(awaitItem().snackbarMessage).isNull() // Check failures mediaActions.shouldFail = true state.eventSink(MediaViewerEvents.OpenWith) state = awaitItem() assertThat(state.snackbarMessage).isNotNull() - state = awaitItem() - assertThat(state.snackbarMessage).isNull() + snackbarDispatcher.clear() + assertThat(awaitItem().snackbarMessage).isNull() state.eventSink(MediaViewerEvents.Share) state = awaitItem() assertThat(state.snackbarMessage).isNotNull() - state = awaitItem() - assertThat(state.snackbarMessage).isNull() + snackbarDispatcher.clear() + assertThat(awaitItem().snackbarMessage).isNull() state.eventSink(MediaViewerEvents.SaveOnDisk) state = awaitItem() assertThat(state.snackbarMessage).isNotNull() - state = awaitItem() - assertThat(state.snackbarMessage).isNull() } } @@ -145,6 +144,7 @@ class MediaViewerPresenterTest { private fun aMediaViewerPresenter( mediaLoader: FakeMediaLoader, localMediaActions: FakeLocalMediaActions, + snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), ): MediaViewerPresenter { return MediaViewerPresenter( inputs = MediaViewerNode.Inputs( @@ -155,7 +155,7 @@ class MediaViewerPresenterTest { localMediaFactory = localMediaFactory, mediaLoader = mediaLoader, localMediaActions = localMediaActions, - snackbarDispatcher = SnackbarDispatcher() + snackbarDispatcher = snackbarDispatcher, ) } } diff --git a/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt new file mode 100644 index 0000000000..090dd36dbe --- /dev/null +++ b/features/messages/impl/src/test/kotlin/io/element/android/features/messages/report/ReportMessagePresenterTests.kt @@ -0,0 +1,142 @@ +/* + * 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.messages.report + +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.messages.impl.report.ReportMessageEvents +import io.element.android.features.messages.impl.report.ReportMessagePresenter +import io.element.android.libraries.architecture.Async +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_USER_ID +import io.element.android.libraries.matrix.test.room.FakeMatrixRoom +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class ReportMessagePresenterTests { + + @Test + fun `presenter - initial state`() = runTest { + val presenter = aPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.reason).isEmpty() + assertThat(initialState.blockUser).isFalse() + assertThat(initialState.result).isInstanceOf(Async.Uninitialized::class.java) + } + } + + @Test + fun `presenter - update reason`() = runTest { + val presenter = aPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + val reason = "This user is making the chat very toxic." + initialState.eventSink(ReportMessageEvents.UpdateReason(reason)) + + assertThat(awaitItem().reason).isEqualTo(reason) + } + } + + @Test + fun `presenter - toggle block user`() = runTest { + val presenter = aPresenter() + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ReportMessageEvents.ToggleBlockUser) + + assertThat(awaitItem().blockUser).isTrue() + + initialState.eventSink(ReportMessageEvents.ToggleBlockUser) + + assertThat(awaitItem().blockUser).isFalse() + } + } + + @Test + fun `presenter - handle successful report and block user`() = runTest { + val room = FakeMatrixRoom() + val presenter = aPresenter(matrixRoom = room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ReportMessageEvents.ToggleBlockUser) + skipItems(1) + initialState.eventSink(ReportMessageEvents.Report) + assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().result).isInstanceOf(Async.Success::class.java) + assertThat(room.reportedContentCount).isEqualTo(1) + } + } + + @Test + fun `presenter - handle successful report`() = runTest { + val room = FakeMatrixRoom() + val presenter = aPresenter(matrixRoom = room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ReportMessageEvents.Report) + assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) + assertThat(awaitItem().result).isInstanceOf(Async.Success::class.java) + assertThat(room.reportedContentCount).isEqualTo(1) + } + } + + @Test + fun `presenter - handle failed report`() = runTest { + val room = FakeMatrixRoom().apply { + givenReportContentResult(Result.failure(Exception("Failed to report content"))) + } + val presenter = aPresenter(matrixRoom = room) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + initialState.eventSink(ReportMessageEvents.Report) + assertThat(awaitItem().result).isInstanceOf(Async.Loading::class.java) + val resultState = awaitItem() + assertThat(resultState.result).isInstanceOf(Async.Failure::class.java) + assertThat(room.reportedContentCount).isEqualTo(1) + + resultState.eventSink(ReportMessageEvents.ClearError) + assertThat(awaitItem().result).isInstanceOf(Async.Uninitialized::class.java) + } + } + + private fun aPresenter( + inputs: ReportMessagePresenter.Inputs = ReportMessagePresenter.Inputs(AN_EVENT_ID, A_USER_ID), + matrixRoom: MatrixRoom = FakeMatrixRoom(), + snackbarDispatcher: SnackbarDispatcher = SnackbarDispatcher(), + ) = ReportMessagePresenter( + inputs = inputs, + room = matrixRoom, + snackbarDispatcher = snackbarDispatcher, + ) +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt index 35b81ff324..1de46c78e0 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/utils/Snackbar.kt @@ -22,13 +22,14 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.res.stringResource import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.update +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock @@ -52,19 +53,16 @@ class SnackbarDispatcher { } } +/** Used to provide a [SnackbarDispatcher] to composable functions, it's needed for [rememberSnackbarHostState]. */ +val LocalSnackbarDispatcher = compositionLocalOf { + error("No SnackbarDispatcher provided") +} + @Composable fun handleSnackbarMessage( snackbarDispatcher: SnackbarDispatcher ): SnackbarMessage? { - val snackbarMessage by snackbarDispatcher.snackbarMessage.collectAsState(initial = null) - LaunchedEffect(snackbarMessage) { - if (snackbarMessage != null) { - launch { - snackbarDispatcher.clear() - } - } - } - return snackbarMessage + return snackbarDispatcher.snackbarMessage.collectAsState(initial = null).value } @Composable @@ -74,6 +72,7 @@ fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostSt val snackbarMessageText = snackbarMessage?.let { stringResource(id = snackbarMessage.messageResId) } + val dispatcher = LocalSnackbarDispatcher.current LaunchedEffect(snackbarMessage) { if (snackbarMessageText == null) return@LaunchedEffect coroutineScope.launch { @@ -81,6 +80,9 @@ fun rememberSnackbarHostState(snackbarMessage: SnackbarMessage?): SnackbarHostSt message = snackbarMessageText, duration = snackbarMessage.duration, ) + if (isActive) { + dispatcher.clear() + } } } return snackbarHostState diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 9e8f8514b7..afd0e8ea25 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -112,4 +112,6 @@ interface MatrixRoom : Closeable { suspend fun setName(name: String): Result suspend fun setTopic(topic: String): Result + + suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index c1adbc0cb9..bc8525d87b 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -329,4 +329,13 @@ class RustMatrixRoom( innerRoom.setTopic(topic) } } + + override suspend fun reportContent(eventId: EventId, reason: String, blockUserId: UserId?): Result = withContext(coroutineDispatchers.io) { + runCatching { + innerRoom.reportContent(eventId = eventId.value, score = null, reason = reason) + if (blockUserId != null) { + innerRoom.ignoreUser(blockUserId.value) + } + } + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 5b641b8883..d8187b0a1d 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -76,6 +76,7 @@ class FakeMatrixRoom( private var retrySendMessageResult = Result.success(Unit) private var cancelSendResult = Result.success(Unit) private var forwardEventResult = Result.success(Unit) + private var reportContentResult = Result.success(Unit) var sendMediaCount = 0 private set @@ -89,6 +90,9 @@ class FakeMatrixRoom( var cancelSendCount: Int = 0 private set + var reportedContentCount: Int = 0 + private set + var isInviteAccepted: Boolean = false private set @@ -249,6 +253,15 @@ class FakeMatrixRoom( setTopicResult } + override suspend fun reportContent( + eventId: EventId, + reason: String, + blockUserId: UserId? + ): Result = simulateLongTask { + reportedContentCount++ + return reportContentResult + } + override fun close() = Unit fun givenLeaveRoomError(throwable: Throwable?) { @@ -338,4 +351,8 @@ class FakeMatrixRoom( fun givenForwardEventResult(result: Result) { forwardEventResult = result } + + fun givenReportContentResult(result: Result) { + reportContentResult = result + } } diff --git a/tests/uitests/build.gradle.kts b/tests/uitests/build.gradle.kts index ed4f5298ce..13f43b22d4 100644 --- a/tests/uitests/build.gradle.kts +++ b/tests/uitests/build.gradle.kts @@ -31,6 +31,7 @@ android { dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.parameter.injector) + testImplementation(projects.libraries.designsystem) androidTestImplementation(libs.test.junitext) ksp(libs.showkase.processor) kspTest(libs.showkase.processor) diff --git a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt index f7afb1e4a8..a929f7d182 100644 --- a/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt +++ b/tests/uitests/src/test/kotlin/io/element/android/tests/uitests/ScreenshotTest.kt @@ -39,6 +39,8 @@ import com.android.ide.common.rendering.api.SessionParams import com.google.testing.junit.testparameterinjector.TestParameter import com.google.testing.junit.testparameterinjector.TestParameterInjector import io.element.android.libraries.designsystem.theme.ElementTheme +import io.element.android.libraries.designsystem.utils.LocalSnackbarDispatcher +import io.element.android.libraries.designsystem.utils.SnackbarDispatcher import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -96,6 +98,8 @@ class ScreenshotTest { LocalConfiguration provides Configuration().apply { setLocales(LocaleList(localeStr.toLocale())) }, + // Needed to display Snackbars and avoid crashes during screenshot tests + LocalSnackbarDispatcher provides SnackbarDispatcher(), // Needed so that UI that uses it don't crash during screenshot tests LocalOnBackPressedDispatcherOwner provides object : OnBackPressedDispatcherOwner { override val lifecycle: Lifecycle get() = lifecycleOwner.lifecycle diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..34557a047e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c78c731d2a100cd5632d3018a0a2f9ee3c4ecdd69feebb233f8fb60efb29808a +size 44647 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..7756aef065 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4a88c76ee497bc9544dda80517959d35862708ec25539aa910cfcf94d55dc17e +size 46580 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a37d6a00cb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c54a2b29fc3584b3de6e5d11b87200bae6ea9ce371c7382297496f0022110da5 +size 46232 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e6ba2b331c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c11907197357eaced1a3ac9dec28ada97ab871d5662ce270d0af000204ebdf11 +size 43878 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..feace46eb5 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a6b210c88398341ab21982e485f08c4503198fdc550a077ffea4f123f5513e0 +size 35451 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewDarkPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +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.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b89bf3802a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:18ec14976369b542ce426d9b559fe4d72faa8db8e7402884be20dca670106ade +size 43921 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..d74122f832 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:539ed1ad5366e2757a99c5589ea6052bb47cc494805164c0a17f8e4970f0b102 +size 45084 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ac7d452d59 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e788d5011a1635273949be92a350a7e4068831935dfae328e5699fc361a9cc6b +size 44662 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_3,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4c84f916e9 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_3,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:396fa2ca9b01b467fb5160d776130eb6b40351a93d4ed9cc4e61a027c977cac0 +size 42258 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_4,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..566e949537 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_4,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5cb0fb381934d4548d10f3b6654ecf04bc0fa29266ed58fa5653dafd322eade0 +size 34503 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_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.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_5,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..665c8811ac --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl.report_null_DefaultGroup_ReportMessageViewLightPreview_0_null_5,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb0d3bfcfd75cbd75fd9270ff1dc27090e5dbac79ca8db8a46d91a4c12bc966b +size 4457 diff --git a/tools/detekt/detekt.yml b/tools/detekt/detekt.yml index dc797aace1..f5be110ffa 100644 --- a/tools/detekt/detekt.yml +++ b/tools/detekt/detekt.yml @@ -113,7 +113,8 @@ Compose: CompositionLocalAllowlist: active: true # You can optionally define a list of CompositionLocals that are allowed here - allowedCompositionLocals: LocalColors, LocalCompoundColors + + allowedCompositionLocals: LocalColors, LocalCompoundColors, LocalSnackbarDispatcher CompositionLocalNaming: active: true ContentEmitterReturningValues: