From 74bc2c0f5d062099a90d278a299b00ad18b29cf8 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:37:32 +0200 Subject: [PATCH 01/10] Add local network address classifier and permission advisor --- .../androidutils/src/main/AndroidManifest.xml | 1 + .../androidutils/network/DnsResolver.kt | 25 ++++ .../network/LocalNetworkAddressClassifier.kt | 75 +++++++++++ .../androidutils/network/FakeDnsResolver.kt | 33 +++++ .../LocalNetworkAddressClassifierTest.kt | 124 ++++++++++++++++++ .../api/LocalNetworkPermissionAdvisor.kt | 16 +++ .../DefaultLocalNetworkPermissionAdvisor.kt | 36 +++++ ...efaultLocalNetworkPermissionAdvisorTest.kt | 79 +++++++++++ .../test/FakeLocalNetworkPermissionAdvisor.kt | 16 +++ 9 files changed, 405 insertions(+) create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/DnsResolver.kt create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifier.kt create mode 100644 libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/FakeDnsResolver.kt create mode 100644 libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifierTest.kt create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt create mode 100644 libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt create mode 100644 libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt create mode 100644 libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt diff --git a/libraries/androidutils/src/main/AndroidManifest.xml b/libraries/androidutils/src/main/AndroidManifest.xml index 446c4606d73..c27910aee31 100644 --- a/libraries/androidutils/src/main/AndroidManifest.xml +++ b/libraries/androidutils/src/main/AndroidManifest.xml @@ -8,4 +8,5 @@ + diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/DnsResolver.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/DnsResolver.kt new file mode 100644 index 00000000000..da84a99a176 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/DnsResolver.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.network + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.net.InetAddress + +interface DnsResolver { + suspend fun resolve(host: String): List +} + +@ContributesBinding(AppScope::class) +class DefaultDnsResolver : DnsResolver { + override suspend fun resolve(host: String): List = withContext(Dispatchers.IO) { + InetAddress.getAllByName(host).toList() + } +} diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifier.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifier.kt new file mode 100644 index 00000000000..b26ef49ac01 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifier.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.network + +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.core.data.tryOrNull +import java.net.Inet4Address +import java.net.Inet6Address +import java.net.InetAddress +import java.net.URI + +sealed interface LocalNetworkClassification { + data object PublicIp : LocalNetworkClassification + data object LocalIp : LocalNetworkClassification + data object Unresolvable : LocalNetworkClassification +} + +interface LocalNetworkAddressClassifier { + /** + * Classify [url] as pointing to a local network, a public host, or unresolvable. + */ + suspend fun classify(url: String): LocalNetworkClassification +} + +@ContributesBinding(AppScope::class) +class DefaultLocalNetworkAddressClassifier( + private val dnsResolver: DnsResolver, +) : LocalNetworkAddressClassifier { + override suspend fun classify(url: String): LocalNetworkClassification { + val host = extractHost(url) ?: return LocalNetworkClassification.Unresolvable + // `.local` domains are always link-local by definition + if (host.endsWith(".local", ignoreCase = true)) return LocalNetworkClassification.LocalIp + + val resolved = tryOrNull { dnsResolver.resolve(host) } + + if (resolved.isNullOrEmpty()) return LocalNetworkClassification.Unresolvable + + return if (resolved.any { it.isLocalRange() }) { + LocalNetworkClassification.LocalIp + } else { + LocalNetworkClassification.PublicIp + } + } + + private fun extractHost(url: String): String? { + return tryOrNull { + val uri = URI(url) + uri.host?.takeIf { it.isNotBlank() } + } + } + + private fun InetAddress.isLocalRange(): Boolean { + if (isLoopbackAddress || isLinkLocalAddress || isSiteLocalAddress) return true + // Cases not covered by jvm + // 1. IPV4 100.64.0.0/10 + if (this is Inet4Address) { + val bytes = address + val b0 = bytes[0].toInt() and 0xff + val b1 = bytes[1].toInt() and 0xff + if (b0 == 100 && b1 in 64..127) return true + } + // 2. IPV6 fc00::/7 + if (this is Inet6Address) { + val firstByte = address[0].toInt() and 0xff + if (firstByte and 0xfe == 0xfc) return true + } + return false + } +} diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/FakeDnsResolver.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/FakeDnsResolver.kt new file mode 100644 index 00000000000..5015abc3073 --- /dev/null +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/FakeDnsResolver.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.network + +import io.element.android.libraries.core.data.tryOrNull +import java.net.InetAddress +import java.net.UnknownHostException + +class FakeDnsResolver( + private val results: Map> = emptyMap(), + private val throwOnUnknown: Boolean = true, +) : DnsResolver { + override suspend fun resolve(host: String): List { + // Mimic production DefaultDnsResolver: InetAddress resolves IP literals synchronously + // (no DNS lookup), so tests can pass literals through without registering them in [results]. + if (looksLikeIpLiteral(host)) { + tryOrNull { InetAddress.getAllByName(host).toList() }?.let { return it } + } + return results[host] + ?: if (throwOnUnknown) throw UnknownHostException(host) else emptyList() + } + + private fun looksLikeIpLiteral(host: String): Boolean { + if (':' in host) return true + val parts = host.split('.') + return parts.size == 4 && parts.all { it.toIntOrNull() != null } + } +} diff --git a/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifierTest.kt b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifierTest.kt new file mode 100644 index 00000000000..4c567822b28 --- /dev/null +++ b/libraries/androidutils/src/test/kotlin/io/element/android/libraries/androidutils/network/LocalNetworkAddressClassifierTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.androidutils.network + +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import java.net.InetAddress + +class LocalNetworkAddressClassifierTest { + private fun classifier( + resolver: DnsResolver = FakeDnsResolver(), + ) = DefaultLocalNetworkAddressClassifier(resolver) + + @Test + fun `IPv4 loopback literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://127.0.0.1:8008")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 RFC1918 10 literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://10.0.0.5")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 RFC1918 172_16 literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://172.20.1.2")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 RFC1918 192_168 literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://192.168.1.10")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 link local literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://169.254.10.20")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 CGNAT 100_64_0_0_10 literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://100.100.0.1")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv4 public literal is PublicIp`() = runTest { + assertThat(classifier().classify("https://8.8.8.8")).isEqualTo(LocalNetworkClassification.PublicIp) + } + + @Test + fun `IPv6 loopback literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://[::1]")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv6 link local literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://[fe80::1]")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv6 unique local literal is LocalIp`() = runTest { + assertThat(classifier().classify("https://[fc00::1]")).isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `IPv6 public literal is PublicIp`() = runTest { + assertThat(classifier().classify("https://[2606:4700:4700::1111]")).isEqualTo(LocalNetworkClassification.PublicIp) + } + + @Test + fun `dot local mDNS name is LocalIp without DNS`() = runTest { + assertThat(classifier().classify("https://matrix.local")) + .isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `hostname resolving to public IP is PublicIp`() = runTest { + val resolver = FakeDnsResolver( + results = mapOf("matrix.org" to listOf(InetAddress.getByName("8.8.8.8"))) + ) + assertThat(classifier(resolver).classify("https://matrix.org")) + .isEqualTo(LocalNetworkClassification.PublicIp) + } + + @Test + fun `hostname resolving to private IP is LocalIp`() = runTest { + val resolver = FakeDnsResolver( + results = mapOf("matrix.corp.internal" to listOf(InetAddress.getByName("10.0.0.5"))) + ) + assertThat(classifier(resolver).classify("https://matrix.corp.internal")) + .isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `hostname with mixed public and private addresses is LocalIp`() = runTest { + val resolver = FakeDnsResolver( + results = mapOf( + "matrix.example" to listOf( + InetAddress.getByName("8.8.8.8"), + InetAddress.getByName("10.0.0.5"), + ) + ) + ) + assertThat(classifier(resolver).classify("https://matrix.example")) + .isEqualTo(LocalNetworkClassification.LocalIp) + } + + @Test + fun `unresolvable hostname is Unresolvable`() = runTest { + val resolver = FakeDnsResolver(throwOnUnknown = true) + assertThat(classifier(resolver).classify("https://nonexistent.example")) + .isEqualTo(LocalNetworkClassification.Unresolvable) + } + + @Test + fun `malformed URL is Unresolvable`() = runTest { + assertThat(classifier().classify("not a url")).isEqualTo(LocalNetworkClassification.Unresolvable) + } +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt new file mode 100644 index 00000000000..7ed46a63e16 --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.api + +interface LocalNetworkPermissionAdvisor { + /** + * Returns true when the app should request the ACCESS_LOCAL_NETWORK permission before making + * network requests to [homeserverUrl]. + */ + suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt new file mode 100644 index 00000000000..a34ece0859c --- /dev/null +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.impl + +import android.Manifest.permission.ACCESS_LOCAL_NETWORK +import android.os.Build +import dev.zacsweers.metro.AppScope +import dev.zacsweers.metro.ContributesBinding +import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier +import io.element.android.libraries.androidutils.network.LocalNetworkClassification +import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.PermissionStateProvider +import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider + +@ContributesBinding(AppScope::class) +class DefaultLocalNetworkPermissionAdvisor( + private val classifier: LocalNetworkAddressClassifier, + private val permissionStateProvider: PermissionStateProvider, + private val buildVersionSdkIntProvider: BuildVersionSdkIntProvider, +) : LocalNetworkPermissionAdvisor { + override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean { + if (!buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.CINNAMON_BUN)) return false + if (permissionStateProvider.isPermissionGranted(ACCESS_LOCAL_NETWORK)) return false + return when (classifier.classify(homeserverUrl)) { + LocalNetworkClassification.LocalIp -> true + LocalNetworkClassification.PublicIp, + // Unresolvable: we can't confirm the host is local, so we don't prompt, acceptable trade-off vs. over-prompting. + LocalNetworkClassification.Unresolvable -> false + } + } +} diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt new file mode 100644 index 00000000000..2ed1dd7548e --- /dev/null +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.impl + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier +import io.element.android.libraries.androidutils.network.LocalNetworkClassification +import io.element.android.libraries.permissions.test.FakePermissionStateProvider +import io.element.android.services.toolbox.test.sdk.FakeBuildVersionSdkIntProvider +import kotlinx.coroutines.test.runTest +import org.junit.Test + +private const val LOCAL_URL = "https://matrix.corp.internal" +private const val PUBLIC_URL = "https://matrix.org" + +class DefaultLocalNetworkPermissionAdvisorTest { + private fun advisor( + sdkInt: Int, + permissionGranted: Boolean, + classification: LocalNetworkClassification, + ) = DefaultLocalNetworkPermissionAdvisor( + classifier = FakeLocalNetworkAddressClassifier(classification), + permissionStateProvider = FakePermissionStateProvider(permissionGranted = permissionGranted), + buildVersionSdkIntProvider = FakeBuildVersionSdkIntProvider(sdkInt), + ) + + @Test + fun `returns false when SDK below 37`() = runTest { + val result = advisor(sdkInt = 36, permissionGranted = false, classification = LocalNetworkClassification.LocalIp) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isFalse() + } + + @Test + fun `returns false when permission already granted`() = runTest { + val result = advisor(sdkInt = 37, permissionGranted = true, classification = LocalNetworkClassification.LocalIp) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isFalse() + } + + @Test + fun `returns false when URL classified as public IP`() = runTest { + val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.PublicIp) + .shouldRequestPermissionFor(PUBLIC_URL) + assertThat(result).isFalse() + } + + @Test + fun `returns true when SDK 37, permission missing, URL is local`() = runTest { + val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.LocalIp) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isTrue() + } + + @Test + fun `returns false when SDK 37, permission missing, URL unresolvable`() = runTest { + val result = advisor(sdkInt = 37, permissionGranted = false, classification = LocalNetworkClassification.Unresolvable) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isFalse() + } + + @Test + fun `returns true when SDK above 37, permission missing, URL is local`() = runTest { + val result = advisor(sdkInt = 38, permissionGranted = false, classification = LocalNetworkClassification.LocalIp) + .shouldRequestPermissionFor(LOCAL_URL) + assertThat(result).isTrue() + } +} + +private class FakeLocalNetworkAddressClassifier( + private val classification: LocalNetworkClassification, +) : LocalNetworkAddressClassifier { + override suspend fun classify(url: String): LocalNetworkClassification = classification +} diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt new file mode 100644 index 00000000000..1a3363d6dcc --- /dev/null +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.test + +import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor + +class FakeLocalNetworkPermissionAdvisor( + private val shouldPrompt: Boolean = false, +) : LocalNetworkPermissionAdvisor { + override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean = shouldPrompt +} From 7f4d264ee040210d7e43a5599eb0fa99f61b1ef0 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:37:37 +0200 Subject: [PATCH 02/10] Add ForceRequestPermissions event to bypass soft-denial intercept --- .../permissions/api/PermissionsEvent.kt | 16 ++++++++++++++++ .../impl/DefaultPermissionsPresenter.kt | 3 +++ .../permissions/test/FakePermissionsPresenter.kt | 1 + 3 files changed, 20 insertions(+) diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvent.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvent.kt index fd01e121342..86049590680 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvent.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsEvent.kt @@ -9,7 +9,23 @@ package io.element.android.libraries.permissions.api sealed interface PermissionsEvent { + /** + * Request the runtime permission. If the app has already recorded a soft denial for this + * permission, this event is intercepted to raise [PermissionsState.showDialog] instead of + * hitting the OS — the intent is for the caller to render `PermissionsView` to prompt the + * user to open system settings. + * + * If the caller renders its own "already denied" UI and wants the OS prompt regardless, + * use [ForceRequestPermissions]. + */ data object RequestPermissions : PermissionsEvent + + /** + * Same as [RequestPermissions] but bypasses the soft-denial intercept — always launches + * the OS permission dialog. The OS may still auto-deny for permanently-denied permissions. + */ + data object ForceRequestPermissions : PermissionsEvent + data object CloseDialog : PermissionsEvent data object OpenSystemSettingAndCloseDialog : PermissionsEvent } diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index 42331e001a4..e3979e5ed2c 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -112,6 +112,9 @@ class DefaultPermissionsPresenter( permissionState.launchPermissionRequest() } } + PermissionsEvent.ForceRequestPermissions -> { + permissionState.launchPermissionRequest() + } PermissionsEvent.OpenSystemSettingAndCloseDialog -> { permissionActions.openSettings(permission) showDialog.value = false diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt index efd584585ff..4c04a919daf 100644 --- a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakePermissionsPresenter.kt @@ -21,6 +21,7 @@ class FakePermissionsPresenter( private fun handleEvent(event: PermissionsEvent) { when (event) { PermissionsEvent.RequestPermissions -> state.value = state.value.copy(showDialog = true, permissionAlreadyAsked = true) + PermissionsEvent.ForceRequestPermissions -> state.value = state.value.copy(permissionAlreadyAsked = true) PermissionsEvent.CloseDialog -> state.value = state.value.copy(showDialog = false) PermissionsEvent.OpenSystemSettingAndCloseDialog -> state.value = state.value.copy(showDialog = false) } From e9abed9178c0430f93224bf2ae04a85f1929a73e Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:37:43 +0200 Subject: [PATCH 03/10] Expose homeserverUrl on MatrixClient --- .../io/element/android/libraries/matrix/api/MatrixClient.kt | 3 +++ .../element/android/libraries/matrix/impl/RustMatrixClient.kt | 1 + .../libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt | 3 +++ .../element/android/libraries/matrix/test/FakeMatrixClient.kt | 1 + 4 files changed, 8 insertions(+) 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 6546e4c41ac..a360dd04ed4 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 @@ -54,6 +54,9 @@ import java.util.Optional interface MatrixClient { val sessionId: SessionId val deviceId: DeviceId + + /** Base URL of the homeserver this client is connected to. */ + val homeserverUrl: String val sessionPaths: SessionPaths val userProfile: StateFlow val roomListService: RoomListService 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 2cfbbbdc73a..19d8d7fec95 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 @@ -153,6 +153,7 @@ class RustMatrixClient( ) : MatrixClient { override val sessionId: UserId = UserId(innerClient.userId()) override val deviceId: DeviceId = DeviceId(innerClient.deviceId()) + override val homeserverUrl: String = innerClient.homeserver() override val sessionCoroutineScope = appCoroutineScope.childScope(dispatchers.main, "Session-$sessionId") private val sessionDispatcher = dispatchers.io.limitedParallelism(64) diff --git a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt index 43c36f7bfb7..4c23744d531 100644 --- a/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt +++ b/libraries/matrix/impl/src/test/kotlin/io/element/android/libraries/matrix/impl/fixtures/fakes/FakeFfiClient.kt @@ -10,6 +10,7 @@ package io.element.android.libraries.matrix.impl.fixtures.fakes import io.element.android.libraries.matrix.impl.fixtures.factories.aRustSession import io.element.android.libraries.matrix.test.A_DEVICE_ID +import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_USER_ID import io.element.android.tests.testutils.lambda.lambdaError import io.element.android.tests.testutils.simulateLongTask @@ -41,6 +42,7 @@ import uniffi.matrix_sdk_base.MediaRetentionPolicy class FakeFfiClient( private val userId: String = A_USER_ID.value, private val deviceId: String = A_DEVICE_ID.value, + private val homeserver: String = A_HOMESERVER_URL, private val notificationClient: NotificationClient = FakeFfiNotificationClient(), private val notificationSettings: NotificationSettings = FakeFfiNotificationSettings(), private val encryption: Encryption = FakeFfiEncryption(), @@ -56,6 +58,7 @@ class FakeFfiClient( ) : Client(NoHandle) { override fun userId(): String = userId override fun deviceId(): String = deviceId + override fun homeserver(): String = homeserver override suspend fun notificationClient(processSetup: NotificationProcessSetup) = notificationClient override suspend fun getNotificationSettings(): NotificationSettings = notificationSettings override fun encryption(): Encryption = encryption 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 2397f124436..8f69b621c84 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 @@ -74,6 +74,7 @@ class FakeMatrixClient( override val sessionId: SessionId = A_SESSION_ID, override val sessionPaths: SessionPaths = SessionPaths(fileDirectory = File("files"), cacheDirectory = File("cache")), override val deviceId: DeviceId = A_DEVICE_ID, + override val homeserverUrl: String = A_HOMESERVER_URL, override val sessionCoroutineScope: CoroutineScope = TestScope(), private val userDisplayName: String? = A_USER_NAME, private val userAvatarUrl: String? = AN_AVATAR_URL, From c2e8912fe209753c59647b044c35787edff5b154 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:37:48 +0200 Subject: [PATCH 04/10] Add LocalNetworkPermissionGate with rationale dialog --- .../LocalNetworkPermissionDialogView.kt | 44 +++++++++ .../LocalNetworkPermissionGate.kt | 95 +++++++++++++++++++ .../LocalNetworkPermissionGateState.kt | 27 ++++++ .../src/main/res/values/temporary.xml | 2 + 4 files changed, 168 insertions(+) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt new file mode 100644 index 00000000000..216b84fb11e --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.localnetwork + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LocalNetworkPermissionDialogView( + dialog: LocalNetworkPermissionDialog, + onSubmit: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + when (dialog) { + LocalNetworkPermissionDialog.None -> Unit + LocalNetworkPermissionDialog.Rationale -> ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(CommonStrings.dialog_allow_access), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = onSubmit, + onDismiss = onDismiss, + modifier = modifier, + ) + LocalNetworkPermissionDialog.Settings -> ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(CommonStrings.action_open_settings), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = onSubmit, + onDismiss = onDismiss, + modifier = modifier, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt new file mode 100644 index 00000000000..857a8f92871 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.localnetwork + +import android.Manifest +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.rememberUpdatedState +import androidx.compose.runtime.setValue +import dev.zacsweers.metro.Inject +import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.PermissionsEvent +import io.element.android.libraries.permissions.api.PermissionsPresenter +import kotlinx.coroutines.launch + +@Inject +class LocalNetworkPermissionGate( + private val advisor: LocalNetworkPermissionAdvisor, + permissionsPresenterFactory: PermissionsPresenter.Factory, +) { + private val permissionsPresenter: PermissionsPresenter = + permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK) + + @Composable + fun present( + urlOf: (T) -> String, + onProceed: suspend (T) -> Unit, + ): LocalNetworkPermissionGateState { + val coroutineScope = rememberCoroutineScope() + val permissionsState = permissionsPresenter.present() + var pendingSubmit by remember { mutableStateOf(null) } + + val urlOf by rememberUpdatedState(urlOf) + val onProceed by rememberUpdatedState(onProceed) + + LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) { + val pending = pendingSubmit + if (pending != null && permissionsState.permissionGranted) { + coroutineScope.launch { onProceed(pending) } + pendingSubmit = null + } + } + + val dialog by rememberUpdatedState( + when { + pendingSubmit == null -> LocalNetworkPermissionDialog.None + permissionsState.permissionGranted -> LocalNetworkPermissionDialog.None + permissionsState.shouldShowRationale || !permissionsState.permissionAlreadyAsked -> LocalNetworkPermissionDialog.Rationale + else -> LocalNetworkPermissionDialog.Settings + } + ) + + fun submit(value: T) { + coroutineScope.launch { + if (advisor.shouldRequestPermissionFor(urlOf(value))) { + pendingSubmit = value + } else { + onProceed(value) + } + } + } + + fun requestPermission() { + when (dialog) { + LocalNetworkPermissionDialog.Settings -> { + permissionsState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog) + } + LocalNetworkPermissionDialog.Rationale -> { + permissionsState.eventSink(PermissionsEvent.ForceRequestPermissions) + } + else -> Unit + } + } + + fun abort() { + pendingSubmit = null + } + + return LocalNetworkPermissionGateState( + dialog = dialog, + submit = ::submit, + requestPermission = ::requestPermission, + abort = ::abort, + ) + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt new file mode 100644 index 00000000000..3213a96cd80 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.localnetwork + +data class LocalNetworkPermissionGateState( + val dialog: LocalNetworkPermissionDialog, + val submit: (T) -> Unit, + val requestPermission: () -> Unit, + val abort: () -> Unit, +) + +/** + * Which rationale dialog (if any) should be rendered on top of the caller's UI. + * + * [Rationale] is shown before the runtime prompt so the user knows why the permission is needed. + * [Settings] is shown when the OS says a rationale can no longer be shown (permanently denied). + */ +enum class LocalNetworkPermissionDialog { + None, + Rationale, + Settings, +} diff --git a/libraries/ui-strings/src/main/res/values/temporary.xml b/libraries/ui-strings/src/main/res/values/temporary.xml index fd1edb07595..a192e2c9dfe 100644 --- a/libraries/ui-strings/src/main/res/values/temporary.xml +++ b/libraries/ui-strings/src/main/res/values/temporary.xml @@ -8,4 +8,6 @@ "Mark as read" "Jump to first unread message" + Allow access to local network + Your homeserver is on your local network. To connect, Element needs permission to reach other devices on this network. From e92923a77d86bf85bb08345df76ba562a70ac0e4 Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:37:52 +0200 Subject: [PATCH 05/10] Prompt for local network permission on logged-in home --- appnav/build.gradle.kts | 2 + .../android/appnav/loggedin/LoggedInEvents.kt | 3 ++ .../appnav/loggedin/LoggedInPresenter.kt | 37 +++++++++++++++++++ .../android/appnav/loggedin/LoggedInState.kt | 7 ++++ .../appnav/loggedin/LoggedInStateProvider.kt | 4 ++ .../android/appnav/loggedin/LoggedInView.kt | 34 +++++++++++++++++ .../appnav/loggedin/LoggedInPresenterTest.kt | 6 +++ 7 files changed, 93 insertions(+) diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 7440ecd2bf0..ce9764360ba 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -34,6 +34,7 @@ dependencies { implementation(projects.libraries.featureflag.api) implementation(projects.libraries.matrix.api) implementation(projects.libraries.oauth.api) + implementation(projects.libraries.permissions.api) implementation(projects.libraries.preferences.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) @@ -61,6 +62,7 @@ dependencies { testImplementation(projects.features.share.test) testImplementation(projects.libraries.matrix.test) testImplementation(projects.libraries.oauth.test) + testImplementation(projects.libraries.permissions.test) testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushproviders.test) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt index d2a9c9298e6..66fffce9e8e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInEvents.kt @@ -12,4 +12,7 @@ sealed interface LoggedInEvents { data class CloseErrorDialog(val doNotShowAgain: Boolean) : LoggedInEvents data object CheckSlidingSyncProxyAvailability : LoggedInEvents data object LogoutAndMigrateToNativeSlidingSync : LoggedInEvents + data object DismissLocalNetworkPermissionPrompt : LoggedInEvents + + data object RequestLocationNetworkPermission : LoggedInEvents } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 752d10e7a93..fab9797c769 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -8,6 +8,7 @@ package io.element.android.appnav.loggedin +import android.Manifest import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -15,6 +16,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -36,6 +38,9 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus +import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.PermissionsEvent +import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.services.analytics.api.AnalyticsService @@ -58,7 +63,12 @@ class LoggedInPresenter( private val encryptionService: EncryptionService, private val buildMeta: BuildMeta, private val networkMonitor: NetworkMonitor, + private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor, + permissionsPresenterFactory: PermissionsPresenter.Factory, ) : Presenter { + private val localNetworkPermissionsPresenter: PermissionsPresenter = + permissionsPresenterFactory.create(Manifest.permission.ACCESS_LOCAL_NETWORK) + @Composable override fun present(): LoggedInState { val coroutineScope = rememberCoroutineScope() @@ -117,6 +127,21 @@ class LoggedInPresenter( } } + val localNetworkPermissionState = localNetworkPermissionsPresenter.present() + var localNetworkPromptDismissedThisSession by remember { mutableStateOf(false) } + val advisorRequestsPrompt by produceState(initialValue = false, localNetworkPermissionState.permissionGranted) { + value = localNetworkPermissionAdvisor.shouldRequestPermissionFor(matrixClient.homeserverUrl) + } + LaunchedEffect(localNetworkPermissionState.permissionGranted) { + if (localNetworkPermissionState.permissionGranted) localNetworkPromptDismissedThisSession = false + } + val localNetworkPermissionDialog = when { + !advisorRequestsPrompt -> LocalNetworkPermissionDialog.None + localNetworkPromptDismissedThisSession -> LocalNetworkPermissionDialog.None + localNetworkPermissionState.shouldShowRationale -> LocalNetworkPermissionDialog.Rationale + else -> LocalNetworkPermissionDialog.Settings + } + fun handleEvent(event: LoggedInEvents) { when (event) { is LoggedInEvents.CloseErrorDialog -> { @@ -134,6 +159,17 @@ class LoggedInPresenter( // Force the logout since Native Sliding Sync is already enforced by the SDK matrixClient.logout(userInitiated = true, ignoreSdkError = true) } + LoggedInEvents.DismissLocalNetworkPermissionPrompt -> { + localNetworkPromptDismissedThisSession = true + } + LoggedInEvents.RequestLocationNetworkPermission -> { + if (localNetworkPermissionDialog == LocalNetworkPermissionDialog.Settings) { + localNetworkPermissionState.eventSink(PermissionsEvent.OpenSystemSettingAndCloseDialog) + } else { + localNetworkPermissionState.eventSink(PermissionsEvent.RequestPermissions) + } + localNetworkPromptDismissedThisSession = true + } } } @@ -143,6 +179,7 @@ class LoggedInPresenter( ignoreRegistrationError = ignoreRegistrationError, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, appName = buildMeta.applicationName, + localNetworkPermissionDialog = localNetworkPermissionDialog, eventSink = ::handleEvent, ) } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index b066f9f867b..fdfff9bf753 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -16,5 +16,12 @@ data class LoggedInState( val ignoreRegistrationError: Boolean, val forceNativeSlidingSyncMigration: Boolean, val appName: String, + val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val eventSink: (LoggedInEvents) -> Unit, ) + +enum class LocalNetworkPermissionDialog { + None, + Rationale, + Settings, +} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index b2f54075198..afe09ec78b1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -19,6 +19,8 @@ open class LoggedInStateProvider : PreviewParameterProvider { aLoggedInState(showSyncSpinner = true), aLoggedInState(pusherRegistrationState = AsyncData.Failure(PusherRegistrationFailure.NoDistributorsAvailable())), aLoggedInState(forceNativeSlidingSyncMigration = true), + aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Rationale), + aLoggedInState(localNetworkPermissionDialog = LocalNetworkPermissionDialog.Settings), ) } @@ -27,11 +29,13 @@ fun aLoggedInState( pusherRegistrationState: AsyncData = AsyncData.Uninitialized, forceNativeSlidingSyncMigration: Boolean = false, appName: String = "Element X", + localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, ) = LoggedInState( showSyncSpinner = showSyncSpinner, pusherRegistrationState = pusherRegistrationState, ignoreRegistrationError = false, forceNativeSlidingSyncMigration = forceNativeSlidingSyncMigration, appName = appName, + localNetworkPermissionDialog = localNetworkPermissionDialog, eventSink = {}, ) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 62d8de8c297..198a731e492 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.Lifecycle import io.element.android.appnav.R import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain import io.element.android.libraries.designsystem.preview.ElementPreview @@ -82,6 +83,39 @@ fun LoggedInView( } ) } + + LocalNetworkPermissionDialog(state = state) +} + +@Composable +private fun LocalNetworkPermissionDialog(state: LoggedInState) { + when (state.localNetworkPermissionDialog) { + LocalNetworkPermissionDialog.None -> Unit + LocalNetworkPermissionDialog.Rationale -> ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(CommonStrings.dialog_allow_access), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = { + state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) + }, + onDismiss = { + state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) + }, + ) + LocalNetworkPermissionDialog.Settings -> ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(CommonStrings.action_open_settings), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = { + state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) + }, + onDismiss = { + state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) + }, + ) + } } private fun Throwable.getReason(): String? { diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 18c8cfd7b99..faa19d9e61e 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -36,6 +36,8 @@ import io.element.android.libraries.matrix.test.encryption.FakeEncryptionService import io.element.android.libraries.matrix.test.roomlist.FakeRoomListService import io.element.android.libraries.matrix.test.sync.FakeSyncService import io.element.android.libraries.matrix.test.verification.FakeSessionVerificationService +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.push.test.FakePushService @@ -124,6 +126,8 @@ class LoggedInPresenterTest { encryptionService = encryptionService, buildMeta = buildMeta, networkMonitor = networkMonitor, + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory = FakePermissionsPresenterFactory(), ).test { encryptionService.emitRecoveryState(RecoveryState.UNKNOWN) encryptionService.emitRecoveryState(RecoveryState.INCOMPLETE) @@ -368,6 +372,8 @@ class LoggedInPresenterTest { encryptionService = encryptionService, buildMeta = buildMeta, networkMonitor = networkMonitor, + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory = FakePermissionsPresenterFactory(), ) } } From 6f0788fd3410344ba4a998716a1e265eb97f1b6f Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:37:58 +0200 Subject: [PATCH 06/10] Introduce LoginModePresenter with local network permission gate --- .../features/login/impl/di/LoginModule.kt | 5 + .../features/login/impl/login/LoginHelper.kt | 128 ------------------ .../login/impl/login/LoginModeEvent.kt | 23 ++++ .../login/impl/login/LoginModePresenter.kt | 117 ++++++++++++++++ .../login/{LoginMode.kt => LoginModeState.kt} | 11 +- .../impl/login/LoginModeStateProvider.kt | 21 +++ .../login/impl/resolver/HomeserverResolver.kt | 13 ++ .../ChooseAccountProviderPresenter.kt | 35 +++-- .../ChooseAccountProviderState.kt | 7 +- .../ChooseAccountProviderStateProvider.kt | 9 +- .../ChooseAccountProviderView.kt | 17 ++- .../LoginWithClassicPresenter.kt | 23 ++-- .../loginwithclassic/LoginWithClassicState.kt | 5 +- .../LoginWithClassicStateProvider.kt | 8 +- .../loginwithclassic/LoginWithClassicView.kt | 17 ++- .../ConfirmAccountProviderPresenter.kt | 21 ++- .../ConfirmAccountProviderState.kt | 8 +- .../ConfirmAccountProviderStateProvider.kt | 9 +- .../ConfirmAccountProviderView.kt | 18 ++- .../screens/onboarding/OnBoardingPresenter.kt | 23 ++-- .../screens/onboarding/OnBoardingState.kt | 7 +- .../onboarding/OnBoardingStateProvider.kt | 8 +- .../impl/screens/onboarding/OnBoardingView.kt | 17 ++- .../ChooseAccountProviderPresenterTest.kt | 20 +-- .../ChooseAccountProviderStateTest.kt | 5 +- .../ChooseAccountProviderViewTest.kt | 3 +- .../LoginWithClassicPresenterTest.kt | 18 +-- .../ConfirmAccountProviderPresenterTest.kt | 76 +++++------ .../onboarding/OnBoardingPresenterTest.kt | 25 ++-- .../screens/onboarding/OnboardingViewTest.kt | 9 +- .../SearchAccountProviderPresenterTest.kt | 11 +- 31 files changed, 420 insertions(+), 297 deletions(-) delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt rename features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/{LoginMode.kt => LoginModeState.kt} (55%) create mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt index 4523e6f45e8..037acf859bf 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/di/LoginModule.kt @@ -14,6 +14,8 @@ import dev.zacsweers.metro.Binds import dev.zacsweers.metro.ContributesTo import io.element.android.features.login.impl.changeserver.ChangeServerPresenter import io.element.android.features.login.impl.changeserver.ChangeServerState +import io.element.android.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.Presenter @ContributesTo(AppScope::class) @@ -21,4 +23,7 @@ import io.element.android.libraries.architecture.Presenter interface LoginModule { @Binds fun bindChangeServerPresenter(presenter: ChangeServerPresenter): Presenter + + @Binds + fun bindLoginModePresenter(presenter: LoginModePresenter): Presenter } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt deleted file mode 100644 index 3c871a8a1dc..00000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginHelper.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.login - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf -import dev.zacsweers.metro.Inject -import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter -import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter -import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported -import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter -import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever -import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.architecture.runCatchingUpdatingState -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.auth.OAuthPrompt -import io.element.android.libraries.oauth.api.OAuthAction -import io.element.android.libraries.oauth.api.OAuthActionFlow - -/** - * This class is responsible for managing the login flow, including handling OIDC actions and - * submitting login requests. - * It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter] - * and [ChooseAccountProviderPresenter]. - */ -@Inject -class LoginHelper( - private val oAuthActionFlow: OAuthActionFlow, - private val authenticationService: MatrixAuthenticationService, - private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, -) { - private val loginModeState: MutableState> = mutableStateOf(AsyncData.Uninitialized) - - @Composable - fun collectLoginMode(): State> { - LaunchedEffect(Unit) { - oAuthActionFlow.collect { oAuthAction -> - if (oAuthAction != null) { - onOAuthAction(oAuthAction) - } - } - } - return loginModeState - } - - fun clearError() { - loginModeState.value = AsyncData.Uninitialized - } - - suspend fun submit( - isAccountCreation: Boolean, - homeserverUrl: String, - resolvedHomeserverUrl: String?, - loginHint: String?, - ) { - suspend { - authenticationService.setHomeserver(homeserverUrl).recoverCatching { - // No .well-known file? - // If the homeserver is not reachable, try using resolvedHomeserverUrl. - if (resolvedHomeserverUrl != null && resolvedHomeserverUrl != homeserverUrl) { - authenticationService.setHomeserver(resolvedHomeserverUrl).getOrThrow() - } else { - throw it - } - }.map { matrixHomeServerDetails -> - if (matrixHomeServerDetails.supportsOAuthLogin) { - // Retrieve the details right now - val oAuthPrompt = if (isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login - LoginMode.OAuth( - authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = loginHint).getOrThrow() - ) - } else if (isAccountCreation) { - val url = webClientUrlForAuthenticationRetriever.retrieve(homeserverUrl) - LoginMode.AccountCreation(url) - } else if (matrixHomeServerDetails.supportsPasswordLogin) { - LoginMode.PasswordLogin - } else { - error("Unsupported login flow") - } - }.getOrThrow() - }.runCatchingUpdatingState( - state = loginModeState, - errorTransform = { - when (it) { - is AccountCreationNotSupported -> it - else -> ChangeServerError.from(it) - } - } - ) - } - - private suspend fun onOAuthAction(oAuthAction: OAuthAction) { - if (oAuthAction is OAuthAction.GoBack && oAuthAction.toUnblock && loginModeState.value !is AsyncData.Loading) { - // Ignore GoBack action if the current state is not Loading. This GoBack action is coming from LoginFlowNode. - // This can happen if there is an error, for instance attempt to login again on the same account. - return - } - loginModeState.value = AsyncData.Loading() - when (oAuthAction) { - is OAuthAction.GoBack -> { - authenticationService.cancelOAuthLogin() - .onSuccess { - loginModeState.value = AsyncData.Uninitialized - } - .onFailure { failure -> - loginModeState.value = AsyncData.Failure(failure) - } - } - is OAuthAction.Success -> { - authenticationService.loginWithOAuth(oAuthAction.url) - .onFailure { failure -> - loginModeState.value = AsyncData.Failure(failure) - } - } - } - oAuthActionFlow.reset() - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt new file mode 100644 index 00000000000..73400890d56 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeEvent.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.login + +sealed interface LoginModeEvent { + data class Submit( + val isAccountCreation: Boolean, + val homeserverUrl: String, + val resolvedHomeserverUrl: String?, + val loginHint: String?, + ) : LoginModeEvent + + data object ClearError : LoginModeEvent + + data object RequestLocalNetworkPermission : LoginModeEvent + + data object DismissLocalNetworkPermission : LoginModeEvent +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt new file mode 100644 index 00000000000..43c03bfbe39 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.login + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import dev.zacsweers.metro.Inject +import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate +import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever +import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.architecture.runCatchingUpdatingState +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.auth.OAuthPrompt +import io.element.android.libraries.oauth.api.OAuthAction +import io.element.android.libraries.oauth.api.OAuthActionFlow + +@Inject +class LoginModePresenter( + private val oAuthActionFlow: OAuthActionFlow, + private val authenticationService: MatrixAuthenticationService, + private val webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever, + private val localNetworkPermissionGate: LocalNetworkPermissionGate, +) : Presenter { + @Composable + override fun present(): LoginModeState { + val loginMode: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } + + val gateState = localNetworkPermissionGate.present( + urlOf = { it.homeserverUrl }, + onProceed = { request -> performSubmit(request, loginMode) }, + ) + + // Forward OAuth navigation events to the presenter's own loginMode state. + LaunchedEffect(Unit) { + oAuthActionFlow.collect { action -> + if (action != null) handleOAuthAction(action, loginMode) + } + } + + fun handleEvent(event: LoginModeEvent) { + when (event) { + LoginModeEvent.ClearError -> loginMode.value = AsyncData.Uninitialized + is LoginModeEvent.Submit -> gateState.submit(event) + LoginModeEvent.DismissLocalNetworkPermission -> gateState.abort() + LoginModeEvent.RequestLocalNetworkPermission -> gateState.requestPermission() + } + } + return LoginModeState( + loginMode = loginMode.value, + localNetworkPermissionDialog = gateState.dialog, + eventSink = ::handleEvent, + ) + } + + private suspend fun performSubmit(request: LoginModeEvent.Submit, loginMode: MutableState>) { + suspend { + authenticationService.setHomeserver(request.homeserverUrl).recoverCatching { + // Fallback to the well-known-resolved URL if the primary URL failed and the caller supplied one. + if (request.resolvedHomeserverUrl != null && request.resolvedHomeserverUrl != request.homeserverUrl) { + authenticationService.setHomeserver(request.resolvedHomeserverUrl).getOrThrow() + } else { + throw it + } + }.map { matrixHomeServerDetails -> + when { + matrixHomeServerDetails.supportsOAuthLogin -> { + val oAuthPrompt = if (request.isAccountCreation) OAuthPrompt.Create else OAuthPrompt.Login + LoginMode.OAuth( + authenticationService.getOAuthUrl(prompt = oAuthPrompt, loginHint = request.loginHint).getOrThrow() + ) + } + request.isAccountCreation -> LoginMode.AccountCreation( + webClientUrlForAuthenticationRetriever.retrieve(request.homeserverUrl) + ) + matrixHomeServerDetails.supportsPasswordLogin -> LoginMode.PasswordLogin + else -> error("Unsupported login flow") + } + }.getOrThrow() + }.runCatchingUpdatingState( + state = loginMode, + errorTransform = { + when (it) { + is AccountCreationNotSupported -> it + else -> ChangeServerError.from(it) + } + } + ) + } + + private suspend fun handleOAuthAction(action: OAuthAction, loginMode: MutableState>) { + if (action is OAuthAction.GoBack && action.toUnblock && loginMode.value !is AsyncData.Loading) { + // Ignore GoBack that isn't tied to an in-flight login — it comes from LoginFlowNode + // when the user backs out after a completed login attempt. + return + } + loginMode.value = AsyncData.Loading() + when (action) { + is OAuthAction.GoBack -> authenticationService.cancelOAuthLogin() + .onSuccess { loginMode.value = AsyncData.Uninitialized } + .onFailure { loginMode.value = AsyncData.Failure(it) } + is OAuthAction.Success -> authenticationService.loginWithOAuth(action.url) + .onFailure { loginMode.value = AsyncData.Failure(it) } + } + oAuthActionFlow.reset() + } +} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt similarity index 55% rename from features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt rename to features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt index 5ea52e0ebd6..3ca3a42a5db 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginMode.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt @@ -1,6 +1,5 @@ /* - * Copyright (c) 2025 Element Creations Ltd. - * Copyright 2025 New Vector Ltd. + * Copyright (c) 2026 Element Creations Ltd. * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. * Please see LICENSE files in the repository root for full details. @@ -8,8 +7,16 @@ package io.element.android.features.login.impl.login +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog +import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails +data class LoginModeState( + val loginMode: AsyncData, + val localNetworkPermissionDialog: LocalNetworkPermissionDialog, + val eventSink: (LoginModeEvent) -> Unit, +) + sealed interface LoginMode { data object PasswordLogin : LoginMode data class OAuth(val oAuthDetails: OAuthDetails) : LoginMode diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt new file mode 100644 index 00000000000..a0adc327fe0 --- /dev/null +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.features.login.impl.login + +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog +import io.element.android.libraries.architecture.AsyncData + +fun aLoginModeState( + loginMode: AsyncData = AsyncData.Uninitialized, + localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, + eventSink: (LoginModeEvent) -> Unit = {}, +) = LoginModeState( + loginMode = loginMode, + localNetworkPermissionDialog = localNetworkPermissionDialog, + eventSink = eventSink, +) 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 1c4ef8cd09e..b6eb26c87f0 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 @@ -14,6 +14,7 @@ import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker +import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -28,6 +29,7 @@ import java.util.Collections class HomeserverResolver( private val dispatchers: CoroutineDispatchers, private val homeServerLoginCompatibilityChecker: HomeServerLoginCompatibilityChecker, + private val localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor, ) { fun resolve(userInput: String): Flow> = flow { val flowContext = currentCoroutineContext() @@ -39,6 +41,17 @@ class HomeserverResolver( // Run all the requests in parallel withContext(dispatchers.io) { list.parallelMap { url -> + // Skip the compatibility probe if we'd need ACCESS_LOCAL_NETWORK first — + // otherwise the probe hangs on the TCP timeout for ~30s. Emit the URL as a + // candidate directly; the actual sign-in flow triggers the permission prompt. + if (localNetworkPermissionAdvisor.shouldRequestPermissionFor(url)) { + currentList.add(HomeserverData(homeserverUrl = url)) + withContext(flowContext) { + emit(currentList.toList()) + } + return@parallelMap + } + val isValid = homeServerLoginCompatibilityChecker.check(url) .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } .getOrNull() diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt index c6d6d76486f..a9983a19d12 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenter.kt @@ -12,50 +12,49 @@ 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 dev.zacsweers.metro.Inject import io.element.android.appconfig.AuthenticationConfig import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.uri.ensureProtocol import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.launch @Inject class ChooseAccountProviderPresenter( private val enterpriseService: EnterpriseService, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, ) : Presenter { @Composable override fun present(): ChooseAccountProviderState { - val localCoroutineScope = rememberCoroutineScope() - val loginMode by loginHelper.collectLoginMode() - + val loginModeState = loginModePresenter.present() var selectedAccountProvider: AccountProvider? by remember { mutableStateOf(null) } fun handleEvent(event: ChooseAccountProviderEvents) { when (event) { - ChooseAccountProviderEvents.Continue -> localCoroutineScope.launch { - selectedAccountProvider?.let { - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = it.url, - resolvedHomeserverUrl = null, - loginHint = null, + ChooseAccountProviderEvents.Continue -> { + selectedAccountProvider?.let { provider -> + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = provider.url, + resolvedHomeserverUrl = null, + loginHint = null, + ) ) } } is ChooseAccountProviderEvents.SelectAccountProvider -> { - // Ensure that the user do not change the server during processing - if (loginMode is AsyncData.Uninitialized) { + // Ensure that the user does not change the server during processing. + if (loginModeState.loginMode is AsyncData.Uninitialized) { selectedAccountProvider = event.accountProvider } } - ChooseAccountProviderEvents.ClearError -> loginHelper.clearError() + ChooseAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) } } @@ -77,7 +76,7 @@ class ChooseAccountProviderPresenter( return ChooseAccountProviderState( accountProviders = staticAccountProviderList, selectedAccountProvider = selectedAccountProvider, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt index e34fbccb413..a31e5c464f0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderState.kt @@ -9,16 +9,17 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.ImmutableList data class ChooseAccountProviderState( val accountProviders: ImmutableList, val selectedAccountProvider: AccountProvider?, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (ChooseAccountProviderEvents) -> Unit, ) { val submitEnabled: Boolean - get() = selectedAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + get() = selectedAccountProvider != null && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt index 93bd6650118..3fea548fcab 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateProvider.kt @@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import kotlinx.collections.immutable.toImmutableList @@ -58,7 +59,7 @@ open class ChooseAccountProviderStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (ChooseAccountProviderEvents) -> Unit = {}, ) = ChooseAccountProviderState( accountProviders = accountProviders.toImmutableList(), selectedAccountProvider = selectedAccountProvider, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt index f05606dbc3e..be37775965d 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R import io.element.android.features.login.impl.accountprovider.AccountProviderView +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView +import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.IconTitleSubtitleMolecule @@ -56,9 +58,9 @@ fun ChooseAccountProviderView( onCreateAccountContinue: (url: String) -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } @@ -124,7 +126,7 @@ fun ChooseAccountProviderView( Spacer(modifier = Modifier.height(48.dp)) } LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(ChooseAccountProviderEvents.ClearError) }, @@ -135,6 +137,15 @@ fun ChooseAccountProviderView( ) } } + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt index 90a528c3ae2..765dfaadd12 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenter.kt @@ -20,7 +20,8 @@ import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.classic.ElementClassicConnection import io.element.android.features.login.impl.classic.ElementClassicConnectionState -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncAction import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -32,7 +33,7 @@ import kotlinx.coroutines.launch class LoginWithClassicPresenter( @Assisted private val userId: UserId, @Assisted private val navigator: LoginWithClassicNavigator, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, private val elementClassicConnection: ElementClassicConnection, private val accountProviderDataSource: AccountProviderDataSource, private val buildMeta: BuildMeta, @@ -51,7 +52,7 @@ class LoginWithClassicPresenter( var loginWithClassicAction by remember { mutableStateOf>(AsyncAction.Uninitialized) } - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() val elementClassicConnectionState by elementClassicConnection.stateFlow.collectAsState() fun handleEvent(event: LoginWithClassicEvent) { @@ -69,11 +70,13 @@ class LoginWithClassicPresenter( val elementClassicUserId = currentState.elementClassicSession.userId val accountProvider = elementClassicUserId.domainName.orEmpty().ensureProtocol() accountProviderDataSource.setUrl(accountProvider) - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = accountProvider, - resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, - loginHint = "mxid:" + elementClassicUserId.value, + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = accountProvider, + resolvedHomeserverUrl = currentState.elementClassicSession.homeserverUrl, + loginHint = "mxid:" + elementClassicUserId.value, + ) ) } } @@ -83,7 +86,7 @@ class LoginWithClassicPresenter( } LoginWithClassicEvent.ClearError -> { loginWithClassicAction = AsyncAction.Uninitialized - loginHelper.clearError() + loginModeState.eventSink(LoginModeEvent.ClearError) } } } @@ -94,7 +97,7 @@ class LoginWithClassicPresenter( userId = userId, displayName = elementClassicReady?.displayName, avatar = elementClassicReady?.avatar, - loginMode = loginMode, + loginModeState = loginModeState, loginWithClassicAction = loginWithClassicAction, eventSink = ::handleEvent, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt index 275a444768f..8e93298e762 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicState.kt @@ -9,9 +9,8 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic import android.graphics.Bitmap import androidx.compose.runtime.Stable -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.core.UserId @Stable @@ -21,6 +20,6 @@ data class LoginWithClassicState( val displayName: String?, val avatar: Bitmap?, val loginWithClassicAction: AsyncAction, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (LoginWithClassicEvent) -> Unit, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt index 31b1770f632..6dfc292f6f1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicStateProvider.kt @@ -9,9 +9,9 @@ package io.element.android.features.login.impl.screens.classic.loginwithclassic import android.graphics.Bitmap import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncAction -import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.preview.USER_NAME_ALICE import io.element.android.libraries.matrix.api.core.UserId @@ -29,7 +29,7 @@ fun aLoginWithClassicState( displayName: String? = null, avatar: Bitmap? = null, loginWithClassicAction: AsyncAction = AsyncAction.Uninitialized, - loginMode: AsyncData = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (LoginWithClassicEvent) -> Unit = {}, ) = LoginWithClassicState( isElementPro = isElementPro, @@ -37,6 +37,6 @@ fun aLoginWithClassicState( displayName = displayName, avatar = avatar, loginWithClassicAction = loginWithClassicAction, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt index b1ca50fe613..389d07ea1af 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt @@ -35,6 +35,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView +import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule @@ -65,9 +67,9 @@ fun LoginWithClassicView( onCreateAccountContinue: (url: String) -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } @@ -195,7 +197,7 @@ fun LoginWithClassicView( } ) LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(LoginWithClassicEvent.ClearError) }, @@ -204,6 +206,15 @@ fun LoginWithClassicView( onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight 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 bf06613830f..0111b1e7d79 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 @@ -11,20 +11,19 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import dev.zacsweers.metro.Assisted import dev.zacsweers.metro.AssistedFactory import dev.zacsweers.metro.AssistedInject import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.Presenter -import kotlinx.coroutines.launch @AssistedInject class ConfirmAccountProviderPresenter( @Assisted private val params: Params, private val accountProviderDataSource: AccountProviderDataSource, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, ) : Presenter { data class Params( val isAccountCreation: Boolean, @@ -38,28 +37,26 @@ class ConfirmAccountProviderPresenter( @Composable override fun present(): ConfirmAccountProviderState { val accountProvider by accountProviderDataSource.flow.collectAsState() - val localCoroutineScope = rememberCoroutineScope() - - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() fun handleEvent(event: ConfirmAccountProviderEvents) { when (event) { - ConfirmAccountProviderEvents.Continue -> localCoroutineScope.launch { - loginHelper.submit( + ConfirmAccountProviderEvents.Continue -> loginModeState.eventSink( + LoginModeEvent.Submit( isAccountCreation = params.isAccountCreation, homeserverUrl = accountProvider.url, resolvedHomeserverUrl = null, loginHint = null, ) - } - ConfirmAccountProviderEvents.ClearError -> loginHelper.clearError() + ) + ConfirmAccountProviderEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) } } return ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = params.isAccountCreation, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = ::handleEvent, ) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt index b29b610b3ec..6e6deb8c2cc 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderState.kt @@ -9,14 +9,16 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData data class ConfirmAccountProviderState( val accountProvider: AccountProvider, val isAccountCreation: Boolean, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (ConfirmAccountProviderEvents) -> Unit ) { - val submitEnabled: Boolean get() = accountProvider.url.isNotEmpty() && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + val submitEnabled: Boolean + get() = accountProvider.url.isNotEmpty() && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt index f3a48a86b49..f597ad6b543 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderStateProvider.kt @@ -11,7 +11,8 @@ package io.element.android.features.login.impl.screens.confirmaccountprovider import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.accountprovider.AccountProvider import io.element.android.features.login.impl.accountprovider.anAccountProvider -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported import io.element.android.libraries.architecture.AsyncData @@ -24,7 +25,7 @@ open class ConfirmAccountProviderStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (ConfirmAccountProviderEvents) -> Unit = {}, ) = ConfirmAccountProviderState( accountProvider = accountProvider, isAccountCreation = isAccountCreation, - loginMode = loginMode, + loginModeState = loginModeState, eventSink = eventSink ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderView.kt index c2525f37565..3db0748a040 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 @@ -20,6 +20,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView +import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.molecules.ButtonColumnMolecule @@ -45,9 +47,9 @@ fun ConfirmAccountProviderView( onChange: () -> Unit, modifier: Modifier = Modifier, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } val eventSink = state.eventSink @@ -98,7 +100,7 @@ fun ConfirmAccountProviderView( } ) { LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { eventSink(ConfirmAccountProviderEvents.ClearError) }, @@ -108,6 +110,16 @@ fun ConfirmAccountProviderView( onCreateAccountContinue = onCreateAccountContinue, ) } + + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt index 306549d11bb..29e4b1afe5e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenter.kt @@ -25,7 +25,8 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.canConnectToAnyHomeserver import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.login.LoginModeEvent +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.features.rageshake.api.RageshakeFeatureAvailability import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.core.meta.BuildMeta @@ -41,7 +42,7 @@ class OnBoardingPresenter( private val enterpriseService: EnterpriseService, private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl, private val rageshakeFeatureAvailability: RageshakeFeatureAvailability, - private val loginHelper: LoginHelper, + private val loginModePresenter: Presenter, private val onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider, private val sessionStore: SessionStore, private val accountProviderDataSource: AccountProviderDataSource, @@ -98,21 +99,23 @@ class OnBoardingPresenter( value = sessionStore.numberOfSessions() > 0 } - val loginMode by loginHelper.collectLoginMode() + val loginModeState = loginModePresenter.present() fun handleEvent(event: OnBoardingEvents) { when (event) { is OnBoardingEvents.OnSignIn -> localCoroutineScope.launch { // Ensure that the current account provider is set accountProviderDataSource.setUrl(event.defaultAccountProvider) - loginHelper.submit( - isAccountCreation = false, - homeserverUrl = event.defaultAccountProvider, - resolvedHomeserverUrl = null, - loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, + loginModeState.eventSink( + LoginModeEvent.Submit( + isAccountCreation = false, + homeserverUrl = event.defaultAccountProvider, + resolvedHomeserverUrl = null, + loginHint = params.loginHint?.takeIf { forcedAccountProvider == null }, + ) ) } - OnBoardingEvents.ClearError -> loginHelper.clearError() + OnBoardingEvents.ClearError -> loginModeState.eventSink(LoginModeEvent.ClearError) OnBoardingEvents.OnVersionClick -> { if (canReportBug) { if (multipleTapToUnlock.unlock(localCoroutineScope)) { @@ -133,7 +136,7 @@ class OnBoardingPresenter( canLoginWithQrCode = canLoginWithQrCode, canCreateAccount = defaultAccountProvider == null && canConnectToAnyHomeserver && OnBoardingConfig.CAN_CREATE_ACCOUNT, canReportBug = canReportBug && showReportBug, - loginMode = loginMode, + loginModeState = loginModeState, version = buildMeta.versionName, onBoardingLogoResId = onBoardingLogoResId, eventSink = ::handleEvent, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt index 316efb03ef8..4fb4b8090e2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingState.kt @@ -9,7 +9,7 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes -import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.LoginModeState import io.element.android.libraries.architecture.AsyncData data class OnBoardingState( @@ -25,9 +25,10 @@ data class OnBoardingState( val version: String, @DrawableRes val onBoardingLogoResId: Int?, - val loginMode: AsyncData, + val loginModeState: LoginModeState, val eventSink: (OnBoardingEvents) -> Unit, ) { val submitEnabled: Boolean - get() = defaultAccountProvider != null && (loginMode is AsyncData.Uninitialized || loginMode is AsyncData.Loading) + get() = defaultAccountProvider != null && + (loginModeState.loginMode is AsyncData.Uninitialized || loginModeState.loginMode is AsyncData.Loading) } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt index 249a904dc32..41e4a818185 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingStateProvider.kt @@ -10,8 +10,8 @@ package io.element.android.features.login.impl.screens.onboarding import androidx.annotation.DrawableRes import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.features.login.impl.login.LoginMode -import io.element.android.libraries.architecture.AsyncData +import io.element.android.features.login.impl.login.LoginModeState +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.designsystem.R open class OnBoardingStateProvider : PreviewParameterProvider { @@ -49,7 +49,7 @@ fun anOnBoardingState( version: String = "1.0.0", @DrawableRes customLogoResId: Int? = null, - loginMode: AsyncData = AsyncData.Uninitialized, + loginModeState: LoginModeState = aLoginModeState(), eventSink: (OnBoardingEvents) -> Unit = {}, ) = OnBoardingState( isAddingAccount = isAddingAccount, @@ -62,7 +62,7 @@ fun anOnBoardingState( canCreateAccount = canCreateAccount, canReportBug = canReportBug, version = version, - loginMode = loginMode, + loginModeState = loginModeState, onBoardingLogoResId = customLogoResId, eventSink = eventSink, ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 420bb61475d..74fdbffab10 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView +import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.atomic.atoms.ElementLogoAtom @@ -74,7 +76,7 @@ fun OnBoardingView( ) { val loginView = @Composable { LoginModeView( - loginMode = state.loginMode, + loginMode = state.loginModeState.loginMode, onClearError = { state.eventSink(OnBoardingEvents.ClearError) }, @@ -83,6 +85,15 @@ fun OnBoardingView( onNeedLoginPassword = onNeedLoginPassword, onCreateAccountContinue = onCreateAccountContinue, ) + LocalNetworkPermissionDialogView( + dialog = state.loginModeState.localNetworkPermissionDialog, + onSubmit = { + state.loginModeState.eventSink(LoginModeEvent.RequestLocalNetworkPermission) + }, + onDismiss = { + state.loginModeState.eventSink(LoginModeEvent.DismissLocalNetworkPermission) + } + ) } val buttons = @Composable { OnBoardingButtons( @@ -260,9 +271,9 @@ private fun OnBoardingButtons( onCreateAccount: () -> Unit, onReportProblem: () -> Unit, ) { - val isLoading by remember(state.loginMode) { + val isLoading by remember(state.loginModeState.loginMode) { derivedStateOf { - state.loginMode is AsyncData.Loading + state.loginModeState.loginMode is AsyncData.Loading } } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt index 7461a7db551..4f00bb002b8 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderPresenterTest.kt @@ -12,8 +12,8 @@ import com.google.common.truth.Truth.assertThat import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProvider -import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.matrix.test.AN_ACCOUNT_PROVIDER_2 @@ -81,7 +81,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -105,7 +105,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -121,12 +121,12 @@ class ChooseAccountProviderPresenterTest { // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(ChooseAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } } @@ -138,7 +138,7 @@ class ChooseAccountProviderPresenterTest { enterpriseService = FakeEnterpriseService( defaultHomeserverListResult = { listOf(ACCOUNT_PROVIDER_FROM_CONFIG_1, ACCOUNT_PROVIDER_FROM_CONFIG_2) }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -152,7 +152,7 @@ class ChooseAccountProviderPresenterTest { it.eventSink(ChooseAccountProviderEvents.Continue) } awaitItem().also { - assertThat(it.loginMode.isLoading()).isTrue() + assertThat(it.loginModeState.loginMode.isLoading()).isTrue() it.eventSink(ChooseAccountProviderEvents.SelectAccountProvider(accountProvider2)) } expectNoEvents() @@ -162,8 +162,8 @@ class ChooseAccountProviderPresenterTest { private fun createPresenter( enterpriseService: EnterpriseService = FakeEnterpriseService(), - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), ) = ChooseAccountProviderPresenter( enterpriseService = enterpriseService, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt index ba1eff39e88..a5cb54e7e1b 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderStateTest.kt @@ -11,6 +11,7 @@ package io.element.android.features.login.impl.screens.chooseaccountprovider import com.google.common.truth.Truth.assertThat import io.element.android.features.login.impl.accountprovider.anAccountProvider import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import org.junit.Test @@ -35,7 +36,7 @@ class ChooseAccountProviderStateTest { fun `submitEnabled returns false when there is a selectedAccountProvider but there is an error`() { val sut = aChooseAccountProviderState( selectedAccountProvider = anAccountProvider(), - loginMode = AsyncData.Failure(Throwable("Error")), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(Throwable("Error"))), ) assertThat(sut.submitEnabled).isFalse() } @@ -44,7 +45,7 @@ class ChooseAccountProviderStateTest { fun `submitEnabled returns false when there is a selectedAccountProvider but the result is successful`() { val sut = aChooseAccountProviderState( selectedAccountProvider = anAccountProvider(), - loginMode = AsyncData.Success(LoginMode.PasswordLogin), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)), ) assertThat(sut.submitEnabled).isFalse() } diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt index b036e07e928..f1ce703d1c1 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderViewTest.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.test.v2.runAndroidComposeUiTest import io.element.android.features.login.impl.accountprovider.anAccountProvider +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -69,7 +70,7 @@ class ChooseAccountProviderViewTest : RobolectricTest() { val eventSink = EventsRecorder() setChooseAccountProviderView( state = aChooseAccountProviderState( - loginMode = AsyncData.Failure(AN_EXCEPTION), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)), eventSink = eventSink, ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt index 6b2a4fb0e1e..8d56386fa4e 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicPresenterTest.kt @@ -16,8 +16,8 @@ import io.element.android.features.login.impl.classic.FakeElementClassicConnecti import io.element.android.features.login.impl.classic.ROOM_KEYS_VERSION import io.element.android.features.login.impl.classic.anElementClassicReady import io.element.android.features.login.impl.classic.anElementClassicSession -import io.element.android.features.login.impl.login.LoginHelper -import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.features.login.impl.login.LoginModePresenter +import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_FAILURE_REASON @@ -42,7 +42,7 @@ class LoginWithClassicPresenterTest { assertThat(initialState.displayName).isNull() assertThat(initialState.avatar).isNull() assertThat(initialState.loginWithClassicAction.isUninitialized()).isTrue() - assertThat(initialState.loginMode.isUninitialized()).isTrue() + assertThat(initialState.loginModeState.loginMode.isUninitialized()).isTrue() } } @@ -69,7 +69,7 @@ class LoginWithClassicPresenterTest { ) val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -108,7 +108,7 @@ class LoginWithClassicPresenterTest { ) val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), ) @@ -147,7 +147,7 @@ class LoginWithClassicPresenterTest { val navigateToMissingKeyBackupResult = lambdaRecorder { } val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), navigator = FakeLoginWithClassicNavigator( @@ -188,7 +188,7 @@ class LoginWithClassicPresenterTest { val navigateToMissingKeyBackupResult = lambdaRecorder { } val presenter = createPresenter( elementClassicConnection = elementClassicConnection, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), navigator = FakeLoginWithClassicNavigator( @@ -247,14 +247,14 @@ class LoginWithClassicPresenterTest { private fun createPresenter( userId: UserId = A_USER_ID, navigator: LoginWithClassicNavigator = FakeLoginWithClassicNavigator(), - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), elementClassicConnection: ElementClassicConnection = FakeElementClassicConnection(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), isEnterpriseBuild: Boolean = false, ) = LoginWithClassicPresenter( userId = userId, navigator = navigator, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, elementClassicConnection = elementClassicConnection, accountProviderDataSource = accountProviderDataSource, buildMeta = aBuildMeta( diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt index a9045ab152f..c20896870be 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/confirmaccountprovider/ConfirmAccountProviderPresenterTest.kt @@ -14,7 +14,7 @@ import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource import io.element.android.features.login.impl.login.LoginMode import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported -import io.element.android.features.login.impl.screens.onboarding.createLoginHelper +import io.element.android.features.login.impl.screens.onboarding.createLoginModePresenter import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.libraries.architecture.AsyncData @@ -43,7 +43,7 @@ class ConfirmAccountProviderPresenterTest { assertThat(initialState.isAccountCreation).isFalse() assertThat(initialState.submitEnabled).isTrue() assertThat(initialState.accountProvider.url).isEqualTo(AuthenticationConfig.MATRIX_ORG_URL) - assertThat(initialState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(initialState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -62,11 +62,11 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.PasswordLogin) } } @@ -85,11 +85,11 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -110,15 +110,15 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenOAuthCancelError(AN_EXCEPTION) defaultOAuthActionFlow.post(OAuthAction.GoBack()) val cancelFailureState = awaitItem() - assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -139,14 +139,14 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) defaultOAuthActionFlow.post(OAuthAction.GoBack()) val cancelFinalState = awaitItem() - assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @@ -167,10 +167,10 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) defaultOAuthActionFlow.post(OAuthAction.GoBack(toUnblock = true)) val cancelFinalState = awaitItem() - assertThat(cancelFinalState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) + assertThat(cancelFinalState.loginModeState.loginMode).isInstanceOf(AsyncData.Uninitialized::class.java) } } @@ -191,17 +191,17 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) authenticationService.givenLoginError(AN_EXCEPTION) defaultOAuthActionFlow.post(OAuthAction.Success("aUrl")) val cancelLoadingState = awaitItem() - assertThat(cancelLoadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(cancelLoadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val cancelFailureState = awaitItem() - assertThat(cancelFailureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(cancelFailureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -222,14 +222,14 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink.invoke(ConfirmAccountProviderEvents.Continue) val loadingState = awaitItem() assertThat(loadingState.submitEnabled).isTrue() - assertThat(loadingState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(loadingState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) val successState = awaitItem() assertThat(successState.submitEnabled).isFalse() - assertThat(successState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(successState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(successState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(successState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) defaultOidcActionFlow.post(OAuthAction.Success("aUrl")) val successSuccessState = awaitItem() - assertThat(successSuccessState.loginMode).isInstanceOf(AsyncData.Loading::class.java) + assertThat(successSuccessState.loginModeState.loginMode).isInstanceOf(AsyncData.Loading::class.java) } } @@ -249,7 +249,7 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading val failureState = awaitItem() assertThat(failureState.submitEnabled).isFalse() - assertThat(failureState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(failureState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) } } @@ -273,12 +273,12 @@ class ConfirmAccountProviderPresenterTest { // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -302,11 +302,11 @@ class ConfirmAccountProviderPresenterTest { skipItems(1) // Loading // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) + assertThat(submittedState.loginModeState.loginMode.errorOrNull()).isInstanceOf(AccountCreationNotSupported::class.java) // Assert the error is then cleared submittedState.eventSink(ConfirmAccountProviderEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } @@ -326,8 +326,8 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -349,8 +349,8 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Success::class.java) - assertThat(submittedState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Success::class.java) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isInstanceOf(LoginMode.OAuth::class.java) } } @@ -372,7 +372,7 @@ class ConfirmAccountProviderPresenterTest { initialState.eventSink(ConfirmAccountProviderEvents.Continue) skipItems(1) // Loading val submittedState = awaitItem() - assertThat(submittedState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl)) + assertThat(submittedState.loginModeState.loginMode.dataOrNull()).isEqualTo(LoginMode.AccountCreation(aUrl)) } } @@ -385,7 +385,7 @@ class ConfirmAccountProviderPresenterTest { ) = ConfirmAccountProviderPresenter( params = params, accountProviderDataSource = accountProviderDataSource, - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = matrixAuthenticationService, oAuthActionFlow = defaultOAuthActionFlow, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 82496942786..56ccc230b05 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -15,7 +15,8 @@ import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl import io.element.android.features.login.impl.accountprovider.AccountProviderDataSource -import io.element.android.features.login.impl.login.LoginHelper +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate +import io.element.android.features.login.impl.login.LoginModePresenter import io.element.android.features.login.impl.web.FakeWebClientUrlForAuthenticationRetriever import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.features.wellknown.test.FakeWellknownRetriever @@ -255,7 +256,7 @@ class OnBoardingPresenterTest { enterpriseService = FakeEnterpriseService( isAllowedToConnectToHomeserverResult = { true }, ), - loginHelper = createLoginHelper( + loginModePresenter = createLoginModePresenter( authenticationService = authenticationService, ), accountProviderDataSource = accountProviderDataSource, @@ -271,12 +272,12 @@ class OnBoardingPresenterTest { assertThat(accountProviderDataSource.flow.first().url).isEqualTo(A_HOMESERVER_URL_2) // Check an error was returned val submittedState = awaitItem() - assertThat(submittedState.loginMode).isInstanceOf(AsyncData.Failure::class.java) + assertThat(submittedState.loginModeState.loginMode).isInstanceOf(AsyncData.Failure::class.java) // Assert the error is then cleared submittedState.eventSink(OnBoardingEvents.ClearError) val clearedState = awaitItem() - assertThat(clearedState.loginMode).isEqualTo(AsyncData.Uninitialized) + assertThat(clearedState.loginModeState.loginMode).isEqualTo(AsyncData.Uninitialized) } } } @@ -292,7 +293,7 @@ private fun createPresenter( enterpriseService: EnterpriseService = FakeEnterpriseService(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), rageshakeFeatureAvailability: () -> Flow = { flowOf(true) }, - loginHelper: LoginHelper = createLoginHelper(), + loginModePresenter: LoginModePresenter = createLoginModePresenter(), onBoardingLogoResIdProvider: OnBoardingLogoResIdProvider = OnBoardingLogoResIdProvider { null }, sessionStore: SessionStore = InMemorySessionStore(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), @@ -305,18 +306,26 @@ private fun createPresenter( wellknownRetriever = wellknownRetriever, ), rageshakeFeatureAvailability = rageshakeFeatureAvailability, - loginHelper = loginHelper, + loginModePresenter = loginModePresenter, onBoardingLogoResIdProvider = onBoardingLogoResIdProvider, sessionStore = sessionStore, accountProviderDataSource = accountProviderDataSource, ) -fun createLoginHelper( +fun createLoginModePresenter( oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), -): LoginHelper = LoginHelper( + localNetworkPermissionAdvisor: io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor = + io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(), + permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory = + io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(), +): LoginModePresenter = LoginModePresenter( oAuthActionFlow = oAuthActionFlow, authenticationService = authenticationService, webClientUrlForAuthenticationRetriever = webClientUrlForAuthenticationRetriever, + localNetworkPermissionGate = LocalNetworkPermissionGate( + advisor = localNetworkPermissionAdvisor, + permissionsPresenterFactory = permissionsPresenterFactory, + ), ) diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt index 52d16676872..1bdcb9bda18 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnboardingViewTest.kt @@ -21,6 +21,7 @@ import com.google.testing.junit.testparameterinjector.KotlinTestParameters.named import com.google.testing.junit.testparameterinjector.TestParameter import io.element.android.features.login.impl.R import io.element.android.features.login.impl.login.LoginMode +import io.element.android.features.login.impl.login.aLoginModeState import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails import io.element.android.libraries.matrix.test.AN_EXCEPTION @@ -148,7 +149,7 @@ class OnboardingViewTest : RobolectricTestParameter() { setOnboardingView( state = anOnBoardingState( defaultAccountProvider = "element.io", - loginMode = AsyncData.Failure(AN_EXCEPTION), + loginModeState = aLoginModeState(loginMode = AsyncData.Failure(AN_EXCEPTION)), eventSink = eventSink, ), ) @@ -208,7 +209,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnce { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.PasswordLogin), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.PasswordLogin)), eventSink = eventSink, ), onNeedLoginPassword = callback, @@ -224,7 +225,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnceWithParam(oAuthDetails) { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails)), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.OAuth(oAuthDetails))), eventSink = eventSink, ), onOAuthDetails = callback, @@ -240,7 +241,7 @@ class OnboardingViewTest : RobolectricTestParameter() { ensureCalledOnceWithParam(oAuthDetails.url) { callback -> setOnboardingView( state = anOnBoardingState( - loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl")), + loginModeState = aLoginModeState(loginMode = AsyncData.Success(LoginMode.AccountCreation("aUrl"))), eventSink = eventSink, ), onCreateAccountContinue = callback, 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 87afb774743..29c5d4a86c7 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 @@ -13,6 +13,7 @@ import io.element.android.features.login.impl.changeserver.aChangeServerState import io.element.android.features.login.impl.resolver.HomeserverResolver import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.test.auth.FakeHomeServerLoginCompatibilityChecker +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor import io.element.android.tests.testutils.WarmUpRule import io.element.android.tests.testutils.lambda.lambdaRecorder import io.element.android.tests.testutils.lambda.value @@ -30,7 +31,7 @@ class SearchAccountProviderPresenterTest { fun `present - initial state`() = runTest { val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(true) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -44,7 +45,7 @@ class SearchAccountProviderPresenterTest { fun `present - error while checking login compatibility`() = runTest { val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.failure(IllegalStateException("Oops")) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -68,7 +69,7 @@ class SearchAccountProviderPresenterTest { fun `present - enter text no result`() = runTest { val fakeWellknownRetriever = FakeHomeServerLoginCompatibilityChecker(checkResult = { Result.success(false) }) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeWellknownRetriever, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -101,7 +102,7 @@ class SearchAccountProviderPresenterTest { } val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { @@ -141,7 +142,7 @@ class SearchAccountProviderPresenterTest { } val fakeLoginCompatibilityChecker = FakeHomeServerLoginCompatibilityChecker(checkResult = checkResult) val presenter = SearchAccountProviderPresenter( - homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker), + homeserverResolver = HomeserverResolver(testCoroutineDispatchers(), fakeLoginCompatibilityChecker, FakeLocalNetworkPermissionAdvisor()), changeServerPresenter = { aChangeServerState() } ) presenter.test { From a8b3db4621bc98ce6a95493b4b3c21b61d69c4df Mon Sep 17 00:00:00 2001 From: ganfra Date: Tue, 7 Jul 2026 21:38:03 +0200 Subject: [PATCH 07/10] Prompt for local network permission when changing server --- .../impl/changeserver/ChangeServerEvents.kt | 2 + .../changeserver/ChangeServerPresenter.kt | 21 +++-- .../impl/changeserver/ChangeServerState.kt | 2 + .../changeserver/ChangeServerStateProvider.kt | 4 + .../impl/changeserver/ChangeServerView.kt | 10 +++ .../changeserver/ChangeServerPresenterTest.kt | 80 +++++++++++++++++++ 6 files changed, 111 insertions(+), 8 deletions(-) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt index 27e6128dd07..4f0d228dd21 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerEvents.kt @@ -13,4 +13,6 @@ import io.element.android.features.login.impl.accountprovider.AccountProvider sealed interface ChangeServerEvents { data class ChangeServer(val accountProvider: AccountProvider) : ChangeServerEvents data object ClearError : ChangeServerEvents + data object RequestLocalNetworkPermission : ChangeServerEvents + data object DismissLocalNetworkPermission : ChangeServerEvents } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenter.kt index 4a4fb3ca412..79f76d5fb5e 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 @@ -12,50 +12,55 @@ 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 dev.zacsweers.metro.Inject import io.element.android.features.login.impl.accesscontrol.DefaultAccountProviderAccessControl 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.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter import io.element.android.libraries.architecture.runCatchingUpdatingState import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch @Inject class ChangeServerPresenter( private val authenticationService: MatrixAuthenticationService, private val accountProviderDataSource: AccountProviderDataSource, private val defaultAccountProviderAccessControl: DefaultAccountProviderAccessControl, + private val localNetworkPermissionGate: LocalNetworkPermissionGate, ) : Presenter { @Composable override fun present(): ChangeServerState { - val localCoroutineScope = rememberCoroutineScope() - val changeServerAction: MutableState> = remember { mutableStateOf(AsyncData.Uninitialized) } + val gateState = localNetworkPermissionGate.present( + urlOf = { provider -> provider.url }, + onProceed = { provider -> changeServer(provider, changeServerAction) }, + ) + fun handleEvent(event: ChangeServerEvents) { when (event) { - is ChangeServerEvents.ChangeServer -> localCoroutineScope.changeServer(event.accountProvider, changeServerAction) ChangeServerEvents.ClearError -> changeServerAction.value = AsyncData.Uninitialized + is ChangeServerEvents.ChangeServer -> gateState.submit(event.accountProvider) + ChangeServerEvents.DismissLocalNetworkPermission -> gateState.abort() + ChangeServerEvents.RequestLocalNetworkPermission -> gateState.requestPermission() } } return ChangeServerState( changeServerAction = changeServerAction.value, + localNetworkPermissionDialog = gateState.dialog, eventSink = ::handleEvent, ) } - private fun CoroutineScope.changeServer( + private suspend fun changeServer( data: AccountProvider, changeServerAction: MutableState>, - ) = launch { + ) { suspend { defaultAccountProviderAccessControl.assertIsAllowedToConnectToAccountProvider( title = data.title, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt index eea3ad78cd4..a3467b21bad 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt @@ -8,9 +8,11 @@ package io.element.android.features.login.impl.changeserver +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData data class ChangeServerState( val changeServerAction: AsyncData, + val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val eventSink: (ChangeServerEvents) -> Unit ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerStateProvider.kt index 1a94bb56315..693bca34bd8 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 @@ -10,6 +10,7 @@ package io.element.android.features.login.impl.changeserver import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.error.ChangeServerError +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData open class ChangeServerStateProvider : PreviewParameterProvider { @@ -39,12 +40,15 @@ open class ChangeServerStateProvider : PreviewParameterProvider = AsyncData.Uninitialized, + localNetworkPermissionDialog: LocalNetworkPermissionDialog = LocalNetworkPermissionDialog.None, ) = ChangeServerState( changeServerAction = changeServerAction, + localNetworkPermissionDialog = localNetworkPermissionDialog, eventSink = {} ) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerView.kt index d6b9c1709bd..15b987ce162 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 @@ -19,6 +19,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter 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.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.androidutils.system.openGooglePlay import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.ProgressDialog @@ -121,6 +122,15 @@ fun ChangeServerView( } AsyncData.Uninitialized -> Unit } + LocalNetworkPermissionDialogView( + dialog = state.localNetworkPermissionDialog, + onSubmit = { + eventSink.invoke(ChangeServerEvents.RequestLocalNetworkPermission) + }, + onDismiss = { + eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission) + } + ) } @PreviewsDayNight diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index 274b58ee49c..02ee9d50425 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -15,6 +15,8 @@ import io.element.android.features.login.impl.accesscontrol.DefaultAccountProvid 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.features.login.impl.localnetwork.LocalNetworkPermissionDialog +import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.features.wellknown.test.anElementWellKnown import io.element.android.libraries.architecture.AsyncData @@ -23,6 +25,9 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails +import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.test.FakePermissionsPresenter +import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory import io.element.android.libraries.wellknown.api.ElementWellKnown import io.element.android.libraries.wellknown.api.WellknownRetriever import io.element.android.libraries.wellknown.api.WellknownRetrieverResult @@ -43,6 +48,7 @@ class ChangeServerPresenterTest { createPresenter().test { val initialState = awaitItem() assertThat(initialState.changeServerAction).isEqualTo(AsyncData.Uninitialized) + assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None) } } @@ -183,11 +189,81 @@ class ChangeServerPresenterTest { } } + @Test + fun `present - advisor advises prompt, dialog shown before setHomeserver runs`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ) + createPresenter( + authenticationService = authenticationService, + enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }), + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true), + permissionsPresenter = FakePermissionsPresenter(), + ).test { + val initialState = awaitItem() + assertThat(initialState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None) + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + val promptState = expectMostRecentItem() + // Dialog is shown, permission has not been requested yet, setHomeserver has not been called. + assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) + assertThat(promptState.changeServerAction).isEqualTo(AsyncData.Uninitialized) + } + } + + @Test + fun `present - dismissing the dialog aborts the submit`() = runTest { + createPresenter( + authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ), + enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }), + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true), + permissionsPresenter = FakePermissionsPresenter(), + ).test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + val promptState = expectMostRecentItem() + assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) + promptState.eventSink.invoke(ChangeServerEvents.DismissLocalNetworkPermission) + val dismissedState = expectMostRecentItem() + assertThat(dismissedState.localNetworkPermissionDialog).isEqualTo(LocalNetworkPermissionDialog.None) + assertThat(dismissedState.changeServerAction).isEqualTo(AsyncData.Uninitialized) + } + } + + @Test + fun `present - advisor advises prompt, permission granted resumes setHomeserver`() = runTest { + val authenticationService = FakeMatrixAuthenticationService( + setHomeserverResult = { Result.success(aMatrixHomeServerDetails(supportsOAuthLogin = true)) }, + ) + val permissionsPresenter = FakePermissionsPresenter() + createPresenter( + authenticationService = authenticationService, + enterpriseService = FakeEnterpriseService(isAllowedToConnectToHomeserverResult = { true }), + localNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(shouldPrompt = true), + permissionsPresenter = permissionsPresenter, + ).test { + val initialState = awaitItem() + initialState.eventSink.invoke(ChangeServerEvents.ChangeServer(AccountProvider(url = A_HOMESERVER_URL))) + val promptState = expectMostRecentItem() + assertThat(promptState.localNetworkPermissionDialog).isNotEqualTo(LocalNetworkPermissionDialog.None) + permissionsPresenter.setPermissionGranted() + // Await recompositions until the deferred setHomeserver completes. + var finalState = awaitItem() + while (finalState.changeServerAction !is AsyncData.Success) { + finalState = awaitItem() + } + assertThat(finalState.changeServerAction).isEqualTo(AsyncData.Success(Unit)) + } + } + private fun createPresenter( authenticationService: FakeMatrixAuthenticationService = FakeMatrixAuthenticationService(), accountProviderDataSource: AccountProviderDataSource = AccountProviderDataSource(FakeEnterpriseService()), enterpriseService: EnterpriseService = FakeEnterpriseService(), wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(), + localNetworkPermissionAdvisor: FakeLocalNetworkPermissionAdvisor = FakeLocalNetworkPermissionAdvisor(), + permissionsPresenter: FakePermissionsPresenter = FakePermissionsPresenter(), ) = ChangeServerPresenter( authenticationService = authenticationService, accountProviderDataSource = accountProviderDataSource, @@ -195,5 +271,9 @@ class ChangeServerPresenterTest { enterpriseService = enterpriseService, wellknownRetriever = wellknownRetriever, ), + localNetworkPermissionGate = LocalNetworkPermissionGate( + advisor = localNetworkPermissionAdvisor, + permissionsPresenterFactory = FakePermissionsPresenterFactory(permissionsPresenter), + ), ) } From f25ca5b96318b23a29a75110f4feaaf102261eed Mon Sep 17 00:00:00 2001 From: ElementBot Date: Tue, 7 Jul 2026 20:18:30 +0000 Subject: [PATCH 08/10] Update screenshots --- .../images/appnav.loggedin_LoggedInView_Day_4_en.png | 3 +++ .../images/appnav.loggedin_LoggedInView_Day_5_en.png | 3 +++ .../images/appnav.loggedin_LoggedInView_Night_4_en.png | 3 +++ .../images/appnav.loggedin_LoggedInView_Night_5_en.png | 3 +++ ...ures.login.impl.changeserver_ChangeServerView_Day_6_en.png | 3 +++ ...es.login.impl.changeserver_ChangeServerView_Night_6_en.png | 3 +++ ...nts.preview.imageeditor_AttachmentImageEditorView_2_en.png | 4 ++-- ...nts.preview.imageeditor_AttachmentImageEditorView_5_en.png | 4 ++-- ...rypto.identity_MessagesViewWithIdentityChange_Day_0_en.png | 4 ++-- ...rypto.identity_MessagesViewWithIdentityChange_Day_1_en.png | 4 ++-- ...rypto.identity_MessagesViewWithIdentityChange_Day_2_en.png | 4 ++-- ...pto.identity_MessagesViewWithIdentityChange_Night_0_en.png | 4 ++-- ...pto.identity_MessagesViewWithIdentityChange_Night_1_en.png | 4 ++-- ...pto.identity_MessagesViewWithIdentityChange_Night_2_en.png | 4 ++-- ...re.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png | 4 ++-- ...re.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png | 4 ++-- ....resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png | 4 ++-- ....resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png | 4 ++-- ...meline.components.event_ATimelineItemEventRow_Day_0_en.png | 4 ++-- ...line.components.event_ATimelineItemEventRow_Night_0_en.png | 4 ++-- ....components.event_TimelineImageWithCaptionRow_Day_0_en.png | 4 ++-- ...omponents.event_TimelineImageWithCaptionRow_Night_0_en.png | 4 ++-- ....components.event_TimelineVideoWithCaptionRow_Day_0_en.png | 4 ++-- ...omponents.event_TimelineVideoWithCaptionRow_Night_0_en.png | 4 ++-- ....components_TimelineItemEventRowDisambiguated_Day_0_en.png | 4 ++-- ...omponents_TimelineItemEventRowDisambiguated_Night_0_en.png | 4 ++-- ...ents_TimelineItemEventRowWithReplyInformative_Day_0_en.png | 4 ++-- ...ents_TimelineItemEventRowWithReplyInformative_Day_1_en.png | 4 ++-- ...ts_TimelineItemEventRowWithReplyInformative_Night_0_en.png | 4 ++-- ...ts_TimelineItemEventRowWithReplyInformative_Night_1_en.png | 4 ++-- ...components_TimelineItemEventRowWithReplyOther_Day_0_en.png | 4 ++-- ...components_TimelineItemEventRowWithReplyOther_Day_1_en.png | 4 ++-- ...mponents_TimelineItemEventRowWithReplyOther_Night_0_en.png | 4 ++-- ...mponents_TimelineItemEventRowWithReplyOther_Night_1_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_0_en.png | 4 ++-- ...ine.components_TimelineItemEventRowWithReply_Day_10_en.png | 4 ++-- ...ine.components_TimelineItemEventRowWithReply_Day_11_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_1_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_2_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_3_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_4_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_5_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_6_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_7_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_8_en.png | 4 ++-- ...line.components_TimelineItemEventRowWithReply_Day_9_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_0_en.png | 4 ++-- ...e.components_TimelineItemEventRowWithReply_Night_10_en.png | 4 ++-- ...e.components_TimelineItemEventRowWithReply_Night_11_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_1_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_2_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_3_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_4_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_5_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_6_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_7_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_8_en.png | 4 ++-- ...ne.components_TimelineItemEventRowWithReply_Night_9_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_0_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_10_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_11_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_1_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_2_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_3_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_4_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_5_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_6_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_7_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_8_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Day_9_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_0_en.png | 4 ++-- .../features.messages.impl_MessagesView_Night_10_en.png | 4 ++-- .../features.messages.impl_MessagesView_Night_11_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_1_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_2_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_3_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_4_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_5_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_6_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_7_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_8_en.png | 4 ++-- .../images/features.messages.impl_MessagesView_Night_9_en.png | 4 ++-- ...references.impl.blockedusers_BlockedUsersView_Day_4_en.png | 4 ++-- ...references.impl.blockedusers_BlockedUsersView_Day_5_en.png | 4 ++-- ...ferences.impl.blockedusers_BlockedUsersView_Night_4_en.png | 4 ++-- ...ferences.impl.blockedusers_BlockedUsersView_Night_5_en.png | 4 ++-- ...embermoderation.impl_RoomMemberModerationView_Day_5_en.png | 4 ++-- ...embermoderation.impl_RoomMemberModerationView_Day_7_en.png | 4 ++-- ...bermoderation.impl_RoomMemberModerationView_Night_5_en.png | 4 ++-- ...bermoderation.impl_RoomMemberModerationView_Night_7_en.png | 4 ++-- ...eme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png | 4 ++-- ...e.components_HorizontalFloatingToolbarNoFab_Night_0_en.png | 4 ++-- ...em.theme.components_HorizontalFloatingToolbar_Day_0_en.png | 4 ++-- ....theme.components_HorizontalFloatingToolbar_Night_0_en.png | 4 ++-- 94 files changed, 194 insertions(+), 176 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_4_en.png create mode 100644 tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_5_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_6_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_6_en.png diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_4_en.png new file mode 100644 index 00000000000..5cfd616c0e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:924b97967296d3f50e1f911a14bd63b9d5d0d3d5d171053c1a0e6a5048c4d336 +size 34048 diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_5_en.png new file mode 100644 index 00000000000..ea86b6f571f --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Day_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5f98480a126f3cd955fc10e42404d8aa812e2f7fb59a9655593bbebb1c8fa972 +size 33249 diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_4_en.png new file mode 100644 index 00000000000..348422394ba --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_4_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c89fc51cf4c9a8b623bb7582aa1490109220cec3b0bfc9ce58e150c5a7702029 +size 32158 diff --git a/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_5_en.png new file mode 100644 index 00000000000..d66f1ab4f24 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/appnav.loggedin_LoggedInView_Night_5_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:794613b9e61283cab807a35b23875b4ea9c1198789c844958c755699279fa3b7 +size 31413 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_6_en.png new file mode 100644 index 00000000000..7ff27996e2b --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Day_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ee3174b63b166511e6c51725a8b6fe00b31eafff011f9059f2f728b77823b465 +size 33670 diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_6_en.png new file mode 100644 index 00000000000..2c304a26950 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.login.impl.changeserver_ChangeServerView_Night_6_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7910112f7ab526dbf0e5326213e03c3f3fe109dc8cc1bd3a8bf5fb05ddb5b0a7 +size 32054 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png index 8edee63bc1e..3192cf2a5e3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:de82970da241172b94f822f8792e6fc721dcfc7f0f67e3523962e253e9750884 -size 255933 +oid sha256:1885bbe8b0ea497ac20d7bcdd0c6a93ba0533e898378d35e1cafdc8c9924fd56 +size 279984 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png index ccfdd7d9ede..f899ad5d436 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.attachments.preview.imageeditor_AttachmentImageEditorView_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fe6cb72de5079be2ac02ec39f74bac0d20492cd6f5509b71ad9cfc4cc52955d3 -size 326468 +oid sha256:1ca032575e4ebd75a2ebc5a4cb9f966e48cdfa56f4ff42d6d9309fa3577a1adb +size 310609 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png index 5f51b27f5e6..693a36b7dd8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3274314ab9e39c23a6b9d66adf80ac8bb7b364ce60a300f96582374a18bc821f -size 54590 +oid sha256:3dbb8ef39b6a7a0c2f576d8bd101aa97c89da696f23e6d5836846494471c01a1 +size 55799 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png index e1f6ee64303..b1850a50a5b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5e27c7c51fe07cca51b92a601aa9c58deab609bfa5e82f8f6f88eaab6ae59ec3 -size 64307 +oid sha256:f1dd0da147212e13a6c0ad5ad8f85ce55666c444076c238ea27eba5605cfab1f +size 56909 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en.png index 5289eaca63b..4185f46b51c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:65f9ee083a73669a0d3f9d88141a610ea53f3c39e89d8f706d414b04555b4764 -size 59321 +oid sha256:085963fcb425147d35a9ad947a7a3e4ba4712bd3d2f32e93099ff15cffe83b88 +size 64761 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png index d3f7da6320b..2e70435fa50 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a94a4cc176b5d52ef9b21339749324adeef9857b6cdaa2ae973903e2408662a -size 55904 +oid sha256:7a3667390d9d88894bbdd5d72b0b714b29c9ff160653e310e97b0da87b6ba8b0 +size 54724 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png index bd3b0196d64..0f6de1b844e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6f7f7415abbe51858f938e16ad03c6aab983fb3d5e2a53a028bae3c031446646 -size 67848 +oid sha256:28512b0fda0b71b98eab03cee15c9d3163825b1a3545670cfa277ddd5434c87a +size 58417 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png index f4d51ccc614..6c774d4cec4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.identity_MessagesViewWithIdentityChange_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8a8f5aaaa7ebb0e61b07867590d99a8791c5d04cad1ce0f334d23dce6f26c6e6 -size 67276 +oid sha256:6ed6a5f1a2055e19a6b8fb0af4738845bd2184f755c323022c52154cdfa07e05 +size 60909 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png index bd73e322abe..02d0fc8eebf 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 -size 3657 +oid sha256:f77d8243c23d3f37dff86dec53671cbde9fafe1314f6e6b445abdfe47d751ad6 +size 56187 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png index 93622cedfe5..bd73e322abe 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:87193a7c9eb9d40234970e5f5be02e80f2b3ffe91e2c9ca6afd58b8bccdd5911 -size 55075 +oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 +size 3657 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png index bd73e322abe..0cf6c10c4e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 -size 3657 +oid sha256:49f0bb16d3d57b59242e704e3dd4e8885c59721a42129837ff6a4f0d7d02de11 +size 54433 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png index bd73e322abe..adec213a8cb 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.crypto.sendfailure.resolve_ResolveVerifiedUserSendFailureView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81 -size 3657 +oid sha256:8ebee4aec3dbab6e1bb05fda7c5e423f0e931560743d0f0ccbac6b70fa9ad808 +size 52921 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png index 08169845d58..38a81d10b8a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:86eac241e9084cc830ff13d231908da2eb0dff246aa5cac64dddaac68a5d5f13 -size 295298 +oid sha256:4420dfe9f6f95411093eb1c17825a389a31d7a5d5ef146129e5872cdd5d87aff +size 295295 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png index 3f12875a5be..06dd93b269e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_ATimelineItemEventRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7076c9a44a433cd9eeca536365ae262d39b4ad99a8f854faadec272cb1b82031 -size 294992 +oid sha256:578a3707102c7754f607c3a14f94f32cafa30ee570db174386533c484cb4773c +size 295031 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png index 29e75829716..fc985de87d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d6441c572f64d99bc119d649e5e6422fe516b9d097f310ccbd99a9ddded9609f -size 612494 +oid sha256:3f358da2b9b0521c14e525bcb2bd41c29c96aacaf5f007546e6d362bde2ee3fb +size 612526 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png index 176ebfa36b8..90bee6c1177 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineImageWithCaptionRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a867dda82242d2b13bd4bc84a9f1c9b8b7b99c030e19a6cce39b2f828b517f3 -size 611488 +oid sha256:a709de55bc68cf12064ebdbde92c0a99bef3252958d7966540458d253e8c12c2 +size 611422 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png index f792a1663ab..80857358e78 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e3bb005a88e9a4d2bb850478ee8ac2c4f31d32625bd6d2abf455fade461cd399 -size 607806 +oid sha256:54e71f45db31e8664c42f8c05feb91ae839589e00cb89d12088c15c85fb9f25f +size 607824 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png index 18319e718fb..8733429e34c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.event_TimelineVideoWithCaptionRow_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4a2a57f5547f98b9ac2d8bf7195a1e4b49e03834e8866c6cc76660a6a3b16e74 -size 606823 +oid sha256:8bb505fe6d67669339c9366f8ee5bc75974e223837d95e76e637455a51c18711 +size 606787 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png index 0435f55de6a..ab6392a424e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ff839731834d7d0c94fa9a2c412014ddbaeaa053c5af1c8cbd00a3f3a583f364 -size 379511 +oid sha256:94f89a37549a3f198442db467bc7d8539f0b5649d97927bfe731f090d6dfb03d +size 379505 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png index f0b5506b89f..9cbd5c1d95e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowDisambiguated_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04ea6c2947a21d3d4a3d43e9c4750c4f2a49056536a4da3f34c95df8353e9de3 -size 377990 +oid sha256:d0e170b4bea262de612bf3374d475fcd25470ff91b7554d20809c51857a78cec +size 377963 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png index 1f0fdabe6de..3ac59caeea4 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b8bb5a6de6601da8d7fe59f110a054577e2e25dae4bb449f60c04e7d7a1b9551 -size 365073 +oid sha256:1a94d5198e27338675c0e15df5874ba3272ee315b8d77291dc7ccc7c754ed1f4 +size 365082 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png index 195aa04fe23..86018764ac0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:76e5ae44362792d570cd82f3910c8bd0acde509b8418c5892e0385e7d636f042 -size 370223 +oid sha256:8344427a0fe87cbf0883ff53a9ee4dcf90976a7011f6618d85961df48b2c53a9 +size 370228 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png index 058a3992c39..29771235f00 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:00fe1e676086766a3eac3e4457eff52c989789fc5d8d0df2624c019ed25ce9af -size 363322 +oid sha256:60b9f2b920cd2a2a414805d1f0f77925232d9ae60bd607fa18d4395cda2dd73d +size 363291 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png index db4ea8296a6..c6d4541184a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyInformative_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2f0d24b8011564ee7a8b2f8c2e6a24eda668ee42d451d862f884acb7926aa06c -size 368529 +oid sha256:c00e504593d8eb61b598f05f46ee4d591757975a80c0b4227b240847d2cd4868 +size 368510 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png index 84d9d4e5c3f..94428abb56c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7c609a633ab23eae5729ecb57a17a385bac65e81cbf0180c01e0847f1028039e -size 347890 +oid sha256:36d7cf2510d546d2b330b1d1b1674ce58df0558c547e443794a1e7b2222add87 +size 347897 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png index 73b7e674e24..7ddac027174 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:991deacdfb7bc371a2df960a2364ae3b2187e588aa5afe7f39409e367040dd9c -size 363893 +oid sha256:a4ac79f3b8fb4e231244bd96b244a4f2526d6ed28d558f9d50734d83bbb74992 +size 363909 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png index bac23aab98f..22ff4965d10 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e30686c7c037ef102717e0316543faf4d3ed2671c21eca96a667150daab380d6 -size 346614 +oid sha256:787151f8f242a8794a804bf14831f6ca680814d617e85feb3652103e8a0f20cd +size 346565 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png index b6e4650d9de..4e17de6908c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReplyOther_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2cd357a1fbaa425eca95587bff2f768a45e773f983f75664875653ba1900e534 -size 361625 +oid sha256:c1e15400fa2c5ee6087316501445c551a3932b65e16de31e68acc5efbd16a559 +size 361603 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png index 01d4dca84b8..67490ba7009 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ef2bf449671b652cb152b3baab92ab100eab77a54229ab49daeab156f2f484d0 -size 369724 +oid sha256:f4431e2d9f30123949ea5676ce60f642234adcf9b5a33f2acc3f80ebf6609a66 +size 369730 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png index f0dfbc33218..1600afff711 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7a6302320bd1e4258e03487539bf46b0b12781dfc8a22b3f9f267017a6146fa -size 354751 +oid sha256:273351cc9c533b2424859a0de00a8a3786628abc3a88175705c4205dedf7e9aa +size 354737 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png index c4943b892cf..e38cf03ad6f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c410e853089cc2c4a2c48a00dd9a8babc8095aed72bb4625057444710559c6c -size 368129 +oid sha256:41a0e404dea7f52359b24b3ec3926b1405429b165cfed44c63b3922ea6d3cb46 +size 368114 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png index b5dac8434ca..68f81488794 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a92f8af94682facac66d4aacb6e49f345561c15be2f8b689041b78aeaaae831 -size 345062 +oid sha256:6ca8cf82aa8ab1bdad56a27584b27789402aeb7ad8fadd038280a376fb75a72c +size 345094 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png index 8aa2cca147d..1b99468924a 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:02ecb903124273e24151ea2088f6b99144f7dcadba00a582b6210dd23835b5c9 -size 357743 +oid sha256:3a9509411777f342261cab58b92a022b3c9f8f0f7a9e4d0527120a91976575f0 +size 357733 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png index cececc229af..b5032c29b3b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f0c6dafaa41849af00d5097a85bd88e3a054a0a9e41c0466a08e6ff8dd6102b2 -size 356857 +oid sha256:dce23463c335d01eb67293cfcede5d85e96ab2ba035cc9a188d42e8e6866bc63 +size 356831 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png index a3327443584..14a40a56764 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:588967869cbd25ed8b82aff5618175660a8a9e45a5a861b1c7867cf1913f575b -size 364646 +oid sha256:a11044fe90227f1f316a1ade8587e545bb620e11cca76865fc2eb83cdb817fc5 +size 364655 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png index d7b969fd5b0..133fd3903f7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2528df873713755f542f61cd794080929586c1bd5f316c0378ce9e837c21d50b -size 397145 +oid sha256:426d1c844a8c88281c5fa4c61916d93278fdfd38f8cdf2dc54bf1a821f2de3eb +size 397167 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png index 0041a870b93..63bd9c3f6d1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a2bd77c08be705c66225f16c29fd6826decd92b41922d0f8ffc0d1a63d8c59d2 -size 356193 +oid sha256:a6cd94664f89bdad2a9761ea8b20acb83fed3ed7cb5be41af8fe4852643c0bfc +size 356175 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png index 9fcf389708e..f67906a37c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9daeee1aaa04247da3c8bb5aaae9947aab1ed2d2057995e53d6ebdb63e5cd356 -size 355848 +oid sha256:ffa68c8398d789444151855496e5df4ffa03218a383db6ea46fd4f979829b823 +size 355831 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png index 6e83d6491d8..d235670def7 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:34152c742abc486a48234e22101833bc163cf11979890d06aac41e68e3c404d0 -size 364846 +oid sha256:a00bd766a00b7e7c6615fc53bf469b3ca5056a6c514c096431ae38d99d604306 +size 364845 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png index 0021619c44d..b10f9741b9e 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b23d3a37774283067a8919b8c332b1bb5a79e90dceb7216b484b985491282e5 -size 355389 +oid sha256:3ec1b0244dab22414c6719b3309426e13128d0c70fb67dc66cd8a60d42521fd0 +size 355411 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png index 77abe209821..422c324695f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5c3d87862c6d7fc88d0f23cd54c5b9f77028992fdd7c282a195b964dd17e85ac -size 367855 +oid sha256:a1df8d22766981fc76d6d767dc3f40d98cadc7c51be20417b067b91af604232b +size 367884 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png index 1d34761a9bd..448bcec6773 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f7d9b5fc0ff5d7828ad6f4dc0c9bd9520b4920553da97ed7b4e030ee844c0208 -size 353054 +oid sha256:fe0daf9161bc2da194992a2fc9505bf2009c5dbed78cdb35d2eb7ed1380a7df5 +size 353037 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png index c081fa16a53..5243218ae36 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6a92bcf5885322e11a706071969fb674b1f6e60e021af153aaa125baa4a602ae -size 366285 +oid sha256:9a85db2f5958db867578663533f204b2e803372d470da890fd8056ffed0cf46c +size 366254 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png index 47a9a6b6306..b6d1f07279f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:54372845e922d047408dd960d048b9b5eae2263d589117d8e7d197741aa34f32 -size 342827 +oid sha256:4bee12a6bdb18fea5ac6997671bfcb903296da7278cf69b2456330066ca31dfb +size 342870 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png index ceca184bd74..5560c219671 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e82011a72f36889c53ab61ed2b9947c7fb5f868c9b7010904fc10f4ca587082 -size 356082 +oid sha256:af4a48ee80535f2c0a226c4ffa4344054fb63b7b0a88ea2c030a4eb8dd5e9644 +size 356078 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png index 5cc5cfe4fcd..5cdad1df92c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6524cc87257cc9d3f135904f4e14594b56df695e71fe576418d2319c472d710b -size 355285 +oid sha256:9e2d8caf5aaae7d3406c703288e491f328552f0acd18e56afd5265b4ceaf1cd1 +size 355272 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png index a431a9429ae..e9d85990e8d 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93c2df04d65f6078ce3f7067fbae8a7317ec42ed0fc013dee962a3953269f3b7 -size 362813 +oid sha256:66d4b6ea4a697735bbb0fac936b89cde99c5077973ba828894bf50b5dae233b3 +size 362848 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png index b51a89586b7..0eb106f4a95 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e642229c58169775fe702769a306bbef22698516ae6f7ac75a761213c3f195fa -size 395446 +oid sha256:217ac95bf476c53d2dc6d3d0143b21312bf38b8ab86c8dfd4f9360582b584f9d +size 395476 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png index ae8495618a9..aa322fc2e48 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08057fdc1e1184720b438ddd2a8753f00791baf2456cc30b45de8e6bdeb57e8e -size 354404 +oid sha256:5fae87094ea13b14e2dc7c16ca96e4f8096ff39e962799468fb4ead4c666620f +size 354381 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png index 40105c14982..9d617f6688f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5ed24d190f10c079f8cf176404372d40eb25cc78ce595a22febc540315057b86 -size 354075 +oid sha256:dabad9dcd5b8f0d4bb964a0187ad7c94b023b04552dacd29a077daddfd48d550 +size 354052 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png index 1a8ea3e4e91..0cfd1dc9850 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:66aba7b4271a990fac85b0bbc7cbdc371fa1e38a4da5bb4e2f62a80b21cb3a8f -size 363012 +oid sha256:545c9e8933e420cc3f8fbba0fbd5162c946c65a20721faeb77c568596ca720cd +size 363045 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png index c9227ebf947..c8306263a04 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_TimelineItemEventRowWithReply_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e614f4a8657637c584a283d69124236d00fe1251c2b8d739c5688a57a6fd152e -size 353693 +oid sha256:756f0cff90ddd61adce2f467c0384d74d430f6d51c091e4980e45bcc9ee6f0fd +size 353705 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png index 7a5e5e1512c..144411969ff 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0e3467a7606a5a7beaafb510faf008ed567b1e3a9915ede6e2ec09c491af0bb9 -size 56552 +oid sha256:84415250503d5e586af7b7504aaa915c1e24dec0a3d28446487c474c354e05c9 +size 55534 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png index 843adcdff00..52e023ec498 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:446ae29cbb211665f6d91129a7dee0f718b407fcb2a64d3f2c0e962e1cb47af7 -size 50168 +oid sha256:c8420417cf8ba717747dead34b9646515f35eb94e1f71aa952b77c3ff433043e +size 49684 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png index ee23a7111d2..adabb6dde52 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:38ae233a128e887efc48da73be3d6ce50a7095601315ed49a3c9cb209ffdda26 -size 58352 +oid sha256:9b071ba6e99b68dcc75f5aaed3a879f2e30614c954b88c6dc05e1dfec57f1079 +size 65771 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png index 9685e6cbc92..875fb85bdaa 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d1a28720d554bc4698eb52e27e7c6228ac9c14b3703a8c139c3113f8b8fa284e -size 40355 +oid sha256:f966bd2eade0180013c2c019963ad9f89c04f16cadcc04954bf3a4427c854635 +size 40002 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png index 7ce91f46844..a1dc68a3d58 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7b749e1260d6ba2b7fef5ae9efd58612f6d67d1788f6c1e5cebc22d58d0fd7b7 -size 58563 +oid sha256:871d92a7565dd233ea87d53658be4e5202775def8e17fea8d386b8975c3b065e +size 53868 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png index f6456f0ec15..479d07ae5f9 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22153168f6f3e84ab789faea29c94fffbc1a5e4c1b9a98deccb0460192fdde23 -size 57213 +oid sha256:0d4f03ac10bbb676d272dba906e758b25d633a1da1e249fe23ba825f8d2f4901 +size 55298 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png index 6a3ffb84b63..c90dc8d08f2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48b91e52306daa750b4b5792535e1f1865ae3b74a36215376a51dafdd0dcd506 -size 54837 +oid sha256:e48a364a28a67d7d090faa23ef88b27dbd47f0a2f552857490728cb6952ce08b +size 53454 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png index abf8eb7086c..6c0a7b4edcf 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1e79e1cc59ba62ca8eb99300eb520b05d7ac6d52c077e3010c03b474865b15f4 -size 58374 +oid sha256:f5aa22c3d79a392625ae231abc046f07f7cdabd91613d5e6f07aede5382aba9b +size 60410 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png index 5d4f5bdaeb6..ac1d94a4dd3 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:08fb14008b08d86755c3473e7194ef0a39bb73c3f0d48122ca80c6a019ec98eb -size 48676 +oid sha256:be5b086ab6934bae88b39987bc51e709c5adb81bdd04fa028e97832c725fd68a +size 50219 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png index b3af9d831a3..2209fcb54f1 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ce795c4e32a1be3d57f6b1c87588d8c7ed0442996ab9d8a96f1ec5a07627e598 -size 59663 +oid sha256:bd0de7412503e93c33cf92dde0cff38bb68555ef176080d913320af5e4942e7a +size 58642 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png index 2c88d322d47..82ef5e7d6a2 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:963fcec9647d0e5e57bd3fbe3edd389c17be7558c1da7e93fa44f34db34307ee -size 57726 +oid sha256:41c4bd5cd1bcbedd91de0a64967fb3f3e03b5f0937ae1ea1a93a7c4a1b24a4a0 +size 56262 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png index 281aad62a75..135d84dd907 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Day_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:85f6f4c64f67faa99b9af65307a0cd3b46a68db52503c9c929375408597995d9 -size 62094 +oid sha256:4ff538a81d013fdc90dd7be466f710088eea93e081d23448b0731d301a4a3bb2 +size 60702 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png index cfa6e4dbc3c..89fc6708d2c 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7a3281f35461edb900ea434c68543dc684a5fad7dab7c9aabac510bc59d442b3 -size 56479 +oid sha256:89467c5b84d0a15210d4013013598d9994d3005a2299e44583d78355e6718ad8 +size 55538 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png index 5f3a6190977..17df34a50a6 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_10_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:19d14e3d779b163053c28e66d3381f0cabe3b540744606e955cd733f9e044089 -size 51713 +oid sha256:6548f5bf1906e1dddcbcc03bb8f76b42e604075751ea8efbc3ead826bf4cfad7 +size 51258 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png index 6e8468054fd..b3109663f24 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_11_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:262e8d4b8daa2d25593834fd0ab5d7019b0a3dd13707aa96e0701bf9240c6525 -size 59809 +oid sha256:10bce6df962a5198f019f509b1cdb55e28195587de4b74742ddc29833f1333a4 +size 69216 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png index d7eb0275580..5b842a7e615 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_1_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5789d10db607ad77acd5d9391b970b2e0a0f962c205b3bf40db5c78d0dcea48b -size 38821 +oid sha256:529c7a98771dbdef837f3aea963871aca1ec086de9d9f8b86c5f6758d21468a8 +size 38724 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png index f4ab74029b4..f425dd2c4c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_2_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:51a5b682243b969bc2d7d609decb823b0509d240f3ff14ee78319260af76a47f -size 58740 +oid sha256:bd6dc088e16f722b36c2198d6a8611af87dec2451f94ca3c2b6880b97f709b98 +size 54095 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png index 3974618f398..b0cd5bc6d47 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_3_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:718d5c9f24c114f3bf88e000b637ea19598d7bb9b97001ba137fc12a6a1285e4 -size 52608 +oid sha256:5e900ff75d02c9db4200e2032444dd7e2f8635973cca3389dd96d7d2bb734061 +size 50757 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png index 0ad1e67deba..486d5da5c6f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:82e6e38576a507b19e58fce9dda0734ec00bcedcb0baf8f4c7919dbaaf7d2b85 -size 55046 +oid sha256:e482603e6d03fffb57d4eb135edb6209c748c51b1f07e638a4ccf133b1b87a79 +size 52920 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png index a009d0f5849..5a877a59e12 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:478851e9e2e8c80095aa552b98a5d574749200ff49899c9472c913b07459c4b5 -size 53758 +oid sha256:5a582219d3143acb08563193bf7106e1ee8f6612e359d0a192e9a2f67c6799e3 +size 55375 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png index f82a7a82195..0383bb8f952 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:22ea7fe121a63b35a2c2bb47609e692f8bf2b913871a6baf3d420c04334c6c4a -size 44344 +oid sha256:7c88b3ed8cc69dee59d4d0a726a9e383d00f2815b6525315f7dcd96f9c25ec2a +size 45618 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png index 7261c9b6716..952751a9624 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7cf3d727da78b39ecf24d4df7db0884af9008db67cc92274849c1b0978f87726 -size 59252 +oid sha256:ed13a2778b8ced86ee41d5460d274b42b714a87f59749e3b26bf75114ef40dd9 +size 58265 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png index cfa62a66265..0f7fe2db38f 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_8_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bfe848e4c365cf4b4d663145f74e5f8b3f47a4c8994961c530603f0248400316 -size 57117 +oid sha256:a576d52b840730334a38046cf8b57993693fc760b4fbdc00376cb48b7268d76e +size 55890 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png index 89065e6bf07..4d6afdbbb1b 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesView_Night_9_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a966c9a1fa4fdd933f4ef9239592e90458a2819ff4a45c857714d2e38c0df5b2 -size 64405 +oid sha256:35069d4fae829d9afe479308dd72ad025fa011290d23133a231f4e70f601ee75 +size 62987 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png index 7cca911c455..074820e157d 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26e14b77fa3c3aeaea58782309e6de8b9047e07945009322990ebf5f4c283309 -size 52770 +oid sha256:7caacb336793b8ae4b83e9a10a82325e25e8b3c036f881898ed28688250eb6e0 +size 55744 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png index 7cca911c455..2af459c1581 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:26e14b77fa3c3aeaea58782309e6de8b9047e07945009322990ebf5f4c283309 -size 52770 +oid sha256:28a95d4b7ebca92c4e0467f3ecd9acdaf7f6257604d190f5d088a9a25e87d637 +size 55305 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en.png index a68f886f731..616c06bf469 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_4_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e5a2e306087a967849f61872b56a83f194d1a76df5723f0bbae6e6e2f4a98c63 -size 55725 +oid sha256:0ce2519906f1aaf34700ca9ba4bdeaa72030f1f5aff3f7e24352a1435ac67d98 +size 53966 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png index 616c06bf469..36d19c6b725 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.blockedusers_BlockedUsersView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0ce2519906f1aaf34700ca9ba4bdeaa72030f1f5aff3f7e24352a1435ac67d98 -size 53966 +oid sha256:7dfa2767c55a81d3f121b8fa55443b7b05f9369e91639b80e0be349863799ad7 +size 55438 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png index 5c597300e46..73b8b8e50c8 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:21aee17ee9a292bb2f0d3d2ec2db565ebb8d5df753454a5a002f033ed5ed39c5 -size 3853 +oid sha256:e8a3ad99763929d2783147814e44fa9ec7e7efb7b99949e3580f23c38fb5d971 +size 9398 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png index f8a1f24b34d..5c597300e46 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Day_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1342b6427f5b2e3075ae3ccee60bf1d3b11afaccf3e152643d6ef2d65c21b54e -size 9108 +oid sha256:21aee17ee9a292bb2f0d3d2ec2db565ebb8d5df753454a5a002f033ed5ed39c5 +size 3853 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png index 4ae7279fe75..d47bf906641 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:410a8596d4c70b9a3856d959d6d71ade8e831e8581ff410c9900957a93b3eb50 -size 8114 +oid sha256:520cae2544153ba45f8fe4b021286c2d22da06aa1b60be7f1b13879723ff994c +size 3664 diff --git a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png index d47bf906641..a87d89c7013 100644 --- a/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png +++ b/tests/uitests/src/test/snapshots/images/features.roommembermoderation.impl_RoomMemberModerationView_Night_7_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:520cae2544153ba45f8fe4b021286c2d22da06aa1b60be7f1b13879723ff994c -size 3664 +oid sha256:0f3a0cb5d9bae6725a1c527f3d9575aabf85d3edf63a28d9faa23f4f0b65e2d0 +size 7624 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png index 58546b8522b..ee5c582240f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:30357bf18792db5ab20085010251170a724ffe8c00313facd04ee704e806af64 -size 10982 +oid sha256:75de341f705bb22955f9399180519b65ad59c2e1917317d56e50ea4db78f35bc +size 12102 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png index 52fd9b96936..fedead0fa8c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbarNoFab_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:58b9491aaa5554b89d86c62a5c9c44975367538c6baa5562f8dc5768f76d8264 -size 8705 +oid sha256:4811667dec0f32accc5e2c8a5dd5c0b34017fcff8d1e0376ee7a4708426bca0f +size 9838 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en.png index 83302d6ba49..38ad0e4b563 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16dee1dfe746cd40dac9fe8f0af3ecb42cd14bab5cc490917b589beed065ed1b -size 14686 +oid sha256:326d59cf768b2dbd96eb7964d54c8eda509a5cd57c6e62087625e3314da0bb4d +size 13658 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png index 687e0d436a8..4da2a98aca7 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.theme.components_HorizontalFloatingToolbar_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d57df7110371c9224e232b707d6a1b10ce14afcb56c803a78220792f2a0d4cf2 -size 11354 +oid sha256:ef0dc4d53daeb986f378b6e4d85928b4b6729336c89cf21e642c88c53688a3ab +size 10292 From 5f4bb30b18a3f0f45bfe001e3bf9caba37bdb097 Mon Sep 17 00:00:00 2001 From: ganfra Date: Thu, 9 Jul 2026 17:22:15 +0200 Subject: [PATCH 09/10] Address PR review comments --- .../appnav/loggedin/LoggedInPresenter.kt | 3 +- .../android/appnav/loggedin/LoggedInState.kt | 7 +-- .../appnav/loggedin/LoggedInStateProvider.kt | 1 + .../android/appnav/loggedin/LoggedInView.kt | 39 +++------------- .../impl/changeserver/ChangeServerState.kt | 2 +- .../changeserver/ChangeServerStateProvider.kt | 2 +- .../impl/changeserver/ChangeServerView.kt | 2 +- .../LocalNetworkPermissionDialogView.kt | 44 ------------------- .../LocalNetworkPermissionGate.kt | 13 +++--- .../LocalNetworkPermissionGateState.kt | 14 +----- .../login/impl/login/LoginModePresenter.kt | 10 +++++ .../login/impl/login/LoginModeState.kt | 2 +- .../impl/login/LoginModeStateProvider.kt | 2 +- .../login/impl/resolver/HomeserverResolver.kt | 21 +++------ .../ChooseAccountProviderView.kt | 2 +- .../loginwithclassic/LoginWithClassicView.kt | 2 +- .../ConfirmAccountProviderView.kt | 2 +- .../impl/screens/onboarding/OnBoardingView.kt | 2 +- .../changeserver/ChangeServerPresenterTest.kt | 2 +- .../onboarding/OnBoardingPresenterTest.kt | 3 +- .../LocalNetworkPermissionAdvisor.kt | 2 +- .../LocalNetworkPermissionDialog.kt | 20 +++++++++ .../LocalNetworkPermissionDialogView.kt | 37 ++++++++++++++++ .../DefaultLocalNetworkPermissionAdvisor.kt | 8 ++-- ...efaultLocalNetworkPermissionAdvisorTest.kt | 2 +- .../test/FakeLocalNetworkPermissionAdvisor.kt | 2 +- 26 files changed, 112 insertions(+), 134 deletions(-) delete mode 100644 features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt rename libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/{ => localnetwork}/LocalNetworkPermissionAdvisor.kt (87%) create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt create mode 100644 libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt rename libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/{ => localnetwork}/DefaultLocalNetworkPermissionAdvisor.kt (83%) rename libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/{ => localnetwork}/DefaultLocalNetworkPermissionAdvisorTest.kt (98%) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index fab9797c769..0723ad4606f 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -38,9 +38,10 @@ import io.element.android.libraries.matrix.api.sync.SlidingSyncVersion import io.element.android.libraries.matrix.api.sync.SyncService import io.element.android.libraries.matrix.api.verification.SessionVerificationService import io.element.android.libraries.matrix.api.verification.SessionVerifiedStatus -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.api.PermissionsEvent import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.push.api.PushService import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.services.analytics.api.AnalyticsService diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt index fdfff9bf753..edc12383bf2 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInState.kt @@ -9,6 +9,7 @@ package io.element.android.appnav.loggedin import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class LoggedInState( val showSyncSpinner: Boolean, @@ -19,9 +20,3 @@ data class LoggedInState( val localNetworkPermissionDialog: LocalNetworkPermissionDialog, val eventSink: (LoggedInEvents) -> Unit, ) - -enum class LocalNetworkPermissionDialog { - None, - Rationale, - Settings, -} diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt index afe09ec78b1..67fcb95c2c7 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInStateProvider.kt @@ -10,6 +10,7 @@ package io.element.android.appnav.loggedin import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.push.api.PusherRegistrationFailure open class LoggedInStateProvider : PreviewParameterProvider { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 198a731e492..e57d378da48 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -19,13 +19,13 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.lifecycle.Lifecycle import io.element.android.appnav.R import io.element.android.libraries.architecture.AsyncData -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.components.dialogs.ErrorDialogWithDoNotShowAgain import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.matrix.api.exception.isNetworkError +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.push.api.PusherRegistrationFailure import io.element.android.libraries.ui.strings.CommonStrings @@ -84,38 +84,11 @@ fun LoggedInView( ) } - LocalNetworkPermissionDialog(state = state) -} - -@Composable -private fun LocalNetworkPermissionDialog(state: LoggedInState) { - when (state.localNetworkPermissionDialog) { - LocalNetworkPermissionDialog.None -> Unit - LocalNetworkPermissionDialog.Rationale -> ConfirmationDialog( - title = stringResource(CommonStrings.screen_local_network_opt_in_title), - content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), - submitText = stringResource(CommonStrings.dialog_allow_access), - cancelText = stringResource(CommonStrings.action_not_now), - onSubmitClick = { - state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) - }, - onDismiss = { - state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) - }, - ) - LocalNetworkPermissionDialog.Settings -> ConfirmationDialog( - title = stringResource(CommonStrings.screen_local_network_opt_in_title), - content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), - submitText = stringResource(CommonStrings.action_open_settings), - cancelText = stringResource(CommonStrings.action_not_now), - onSubmitClick = { - state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) - }, - onDismiss = { - state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) - }, - ) - } + LocalNetworkPermissionDialogView( + dialog = state.localNetworkPermissionDialog, + onSubmit = { state.eventSink(LoggedInEvents.RequestLocationNetworkPermission) }, + onDismiss = { state.eventSink(LoggedInEvents.DismissLocalNetworkPermissionPrompt) }, + ) } private fun Throwable.getReason(): String? { diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt index a3467b21bad..6d8ce25d75e 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerState.kt @@ -8,8 +8,8 @@ package io.element.android.features.login.impl.changeserver -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class ChangeServerState( val changeServerAction: AsyncData, 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 693bca34bd8..183158bceb4 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 @@ -10,8 +10,8 @@ package io.element.android.features.login.impl.changeserver import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.login.impl.error.ChangeServerError -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog open class ChangeServerStateProvider : PreviewParameterProvider { override val values: Sequence 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 15b987ce162..30ae644a702 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 @@ -19,7 +19,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter 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.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.androidutils.system.openGooglePlay import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.designsystem.components.ProgressDialog @@ -28,6 +27,7 @@ import io.element.android.libraries.designsystem.components.dialogs.ErrorDialog import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.LocalBuildMeta +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt deleted file mode 100644 index 216b84fb11e..00000000000 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionDialogView.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) 2026 Element Creations Ltd. - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. - * Please see LICENSE files in the repository root for full details. - */ - -package io.element.android.features.login.impl.localnetwork - -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog -import io.element.android.libraries.ui.strings.CommonStrings - -@Composable -fun LocalNetworkPermissionDialogView( - dialog: LocalNetworkPermissionDialog, - onSubmit: () -> Unit, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, -) { - when (dialog) { - LocalNetworkPermissionDialog.None -> Unit - LocalNetworkPermissionDialog.Rationale -> ConfirmationDialog( - title = stringResource(CommonStrings.screen_local_network_opt_in_title), - content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), - submitText = stringResource(CommonStrings.dialog_allow_access), - cancelText = stringResource(CommonStrings.action_not_now), - onSubmitClick = onSubmit, - onDismiss = onDismiss, - modifier = modifier, - ) - LocalNetworkPermissionDialog.Settings -> ConfirmationDialog( - title = stringResource(CommonStrings.screen_local_network_opt_in_title), - content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), - submitText = stringResource(CommonStrings.action_open_settings), - cancelText = stringResource(CommonStrings.action_not_now), - onSubmitClick = onSubmit, - onDismiss = onDismiss, - modifier = modifier, - ) - } -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt index 857a8f92871..37a97e71ddf 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGate.kt @@ -17,9 +17,10 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import dev.zacsweers.metro.Inject -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.api.PermissionsEvent import io.element.android.libraries.permissions.api.PermissionsPresenter +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import kotlinx.coroutines.launch @Inject @@ -39,13 +40,13 @@ class LocalNetworkPermissionGate( val permissionsState = permissionsPresenter.present() var pendingSubmit by remember { mutableStateOf(null) } - val urlOf by rememberUpdatedState(urlOf) - val onProceed by rememberUpdatedState(onProceed) + val latestUrlOf by rememberUpdatedState(urlOf) + val latestOnProceed by rememberUpdatedState(onProceed) LaunchedEffect(permissionsState.permissionGranted, pendingSubmit) { val pending = pendingSubmit if (pending != null && permissionsState.permissionGranted) { - coroutineScope.launch { onProceed(pending) } + coroutineScope.launch { latestOnProceed(pending) } pendingSubmit = null } } @@ -61,10 +62,10 @@ class LocalNetworkPermissionGate( fun submit(value: T) { coroutineScope.launch { - if (advisor.shouldRequestPermissionFor(urlOf(value))) { + if (advisor.shouldRequestPermissionFor(latestUrlOf(value))) { pendingSubmit = value } else { - onProceed(value) + latestOnProceed(value) } } } diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt index 3213a96cd80..f3ecee729ec 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/localnetwork/LocalNetworkPermissionGateState.kt @@ -7,21 +7,11 @@ package io.element.android.features.login.impl.localnetwork +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog + data class LocalNetworkPermissionGateState( val dialog: LocalNetworkPermissionDialog, val submit: (T) -> Unit, val requestPermission: () -> Unit, val abort: () -> Unit, ) - -/** - * Which rationale dialog (if any) should be rendered on top of the caller's UI. - * - * [Rationale] is shown before the runtime prompt so the user knows why the permission is needed. - * [Settings] is shown when the OS says a rationale can no longer be shown (permanently denied). - */ -enum class LocalNetworkPermissionDialog { - None, - Rationale, - Settings, -} diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt index 43c03bfbe39..34188095b02 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModePresenter.kt @@ -15,7 +15,11 @@ import androidx.compose.runtime.remember import dev.zacsweers.metro.Inject import io.element.android.features.login.impl.error.ChangeServerError import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate +import io.element.android.features.login.impl.screens.chooseaccountprovider.ChooseAccountProviderPresenter +import io.element.android.features.login.impl.screens.classic.loginwithclassic.LoginWithClassicPresenter +import io.element.android.features.login.impl.screens.confirmaccountprovider.ConfirmAccountProviderPresenter import io.element.android.features.login.impl.screens.createaccount.AccountCreationNotSupported +import io.element.android.features.login.impl.screens.onboarding.OnBoardingPresenter import io.element.android.features.login.impl.web.WebClientUrlForAuthenticationRetriever import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.architecture.Presenter @@ -25,6 +29,12 @@ import io.element.android.libraries.matrix.api.auth.OAuthPrompt import io.element.android.libraries.oauth.api.OAuthAction import io.element.android.libraries.oauth.api.OAuthActionFlow +/** + * Presenter responsible for managing the login flow, including handling OAuth actions and + * submitting login requests. + * It's a helper to avoid code duplication. It is used by [OnBoardingPresenter], [ConfirmAccountProviderPresenter], + * [ChooseAccountProviderPresenter] and [LoginWithClassicPresenter]. + */ @Inject class LoginModePresenter( private val oAuthActionFlow: OAuthActionFlow, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt index 3ca3a42a5db..849ff31f859 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeState.kt @@ -7,9 +7,9 @@ package io.element.android.features.login.impl.login -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog data class LoginModeState( val loginMode: AsyncData, diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt index a0adc327fe0..1d33a1c48a1 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/login/LoginModeStateProvider.kt @@ -7,8 +7,8 @@ package io.element.android.features.login.impl.login -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.architecture.AsyncData +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog fun aLoginModeState( loginMode: AsyncData = AsyncData.Uninitialized, 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 b6eb26c87f0..8c6cab79ce8 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 @@ -14,7 +14,7 @@ import io.element.android.libraries.core.coroutine.parallelMap import io.element.android.libraries.core.uri.ensureProtocol import io.element.android.libraries.core.uri.isValidUrl import io.element.android.libraries.matrix.api.auth.HomeServerLoginCompatibilityChecker -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -44,21 +44,14 @@ class HomeserverResolver( // Skip the compatibility probe if we'd need ACCESS_LOCAL_NETWORK first — // otherwise the probe hangs on the TCP timeout for ~30s. Emit the URL as a // candidate directly; the actual sign-in flow triggers the permission prompt. - if (localNetworkPermissionAdvisor.shouldRequestPermissionFor(url)) { - currentList.add(HomeserverData(homeserverUrl = url)) - withContext(flowContext) { - emit(currentList.toList()) - } - return@parallelMap - } - - val isValid = homeServerLoginCompatibilityChecker.check(url) - .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } - .getOrNull() - ?: return@parallelMap + val shouldRequestPermissionOrIsValid = localNetworkPermissionAdvisor.shouldRequestPermissionFor(url) || + homeServerLoginCompatibilityChecker.check(url) + .onFailure { Timber.w(it, "Failed to check compatibility with homeserver $url") } + .getOrNull() + ?: return@parallelMap // Emit the list as soon as possible - if (isValid) { + if (shouldRequestPermissionOrIsValid) { currentList.add(HomeserverData(homeserverUrl = url)) withContext(flowContext) { emit(currentList.toList()) diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt index be37775965d..17e7d10efb2 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/chooseaccountprovider/ChooseAccountProviderView.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R import io.element.android.features.login.impl.accountprovider.AccountProviderView -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData @@ -46,6 +45,7 @@ import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.TopAppBar import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.ui.strings.CommonStrings @Composable diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt index 389d07ea1af..75181a068ea 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/classic/loginwithclassic/LoginWithClassicView.kt @@ -35,7 +35,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData @@ -52,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.OutlinedButton import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings 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 3db0748a040..95fbff25abb 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 @@ -20,7 +20,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData @@ -33,6 +32,7 @@ import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.theme.components.Button import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.matrix.api.auth.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings diff --git a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt index 74fdbffab10..be86bbee9c0 100644 --- a/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt +++ b/features/login/impl/src/main/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingView.kt @@ -33,7 +33,6 @@ import androidx.compose.ui.unit.dp import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons import io.element.android.features.login.impl.R -import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionDialogView import io.element.android.features.login.impl.login.LoginModeEvent import io.element.android.features.login.impl.login.LoginModeView import io.element.android.libraries.architecture.AsyncData @@ -52,6 +51,7 @@ import io.element.android.libraries.designsystem.theme.components.IconSource 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.OAuthDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialogView import io.element.android.libraries.testtags.TestTags import io.element.android.libraries.testtags.testTag import io.element.android.libraries.ui.strings.CommonStrings diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt index 02ee9d50425..5d9040adac5 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/changeserver/ChangeServerPresenterTest.kt @@ -15,7 +15,6 @@ import io.element.android.features.login.impl.accesscontrol.DefaultAccountProvid 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.features.login.impl.localnetwork.LocalNetworkPermissionDialog import io.element.android.features.login.impl.localnetwork.LocalNetworkPermissionGate import io.element.android.features.wellknown.test.FakeWellknownRetriever import io.element.android.features.wellknown.test.anElementWellKnown @@ -25,6 +24,7 @@ import io.element.android.libraries.matrix.test.AN_EXCEPTION import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationService import io.element.android.libraries.matrix.test.auth.aMatrixHomeServerDetails +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionDialog import io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.test.FakePermissionsPresenter import io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory diff --git a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt index 56ccc230b05..79268aa1e46 100644 --- a/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt +++ b/features/login/impl/src/test/kotlin/io/element/android/features/login/impl/screens/onboarding/OnBoardingPresenterTest.kt @@ -34,6 +34,7 @@ import io.element.android.libraries.matrix.test.auth.FakeMatrixAuthenticationSer import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.oauth.api.OAuthActionFlow import io.element.android.libraries.oauth.test.customtab.FakeOAuthActionFlow +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import io.element.android.libraries.sessionstorage.api.SessionStore import io.element.android.libraries.sessionstorage.test.InMemorySessionStore import io.element.android.libraries.sessionstorage.test.aSessionData @@ -316,7 +317,7 @@ fun createLoginModePresenter( oAuthActionFlow: OAuthActionFlow = FakeOAuthActionFlow(), authenticationService: MatrixAuthenticationService = FakeMatrixAuthenticationService(), webClientUrlForAuthenticationRetriever: WebClientUrlForAuthenticationRetriever = FakeWebClientUrlForAuthenticationRetriever(), - localNetworkPermissionAdvisor: io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor = + localNetworkPermissionAdvisor: LocalNetworkPermissionAdvisor = io.element.android.libraries.permissions.test.FakeLocalNetworkPermissionAdvisor(), permissionsPresenterFactory: io.element.android.libraries.permissions.api.PermissionsPresenter.Factory = io.element.android.libraries.permissions.test.FakePermissionsPresenterFactory(), diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt similarity index 87% rename from libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt rename to libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt index 7ed46a63e16..9863dbb7512 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/LocalNetworkPermissionAdvisor.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionAdvisor.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.permissions.api +package io.element.android.libraries.permissions.api.localnetwork interface LocalNetworkPermissionAdvisor { /** diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt new file mode 100644 index 00000000000..68a440680f3 --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialog.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.api.localnetwork + +/** + * Which rationale dialog (if any) should be rendered on top of the caller's UI. + * + * [Rationale] is shown before the runtime prompt so the user knows why the permission is needed. + * [Settings] is shown when the OS says a rationale can no longer be shown (permanently denied). + */ +enum class LocalNetworkPermissionDialog { + None, + Rationale, + Settings, +} diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt new file mode 100644 index 00000000000..d2337fdf5bb --- /dev/null +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/localnetwork/LocalNetworkPermissionDialogView.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2026 Element Creations Ltd. + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial. + * Please see LICENSE files in the repository root for full details. + */ + +package io.element.android.libraries.permissions.api.localnetwork + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog +import io.element.android.libraries.ui.strings.CommonStrings + +@Composable +fun LocalNetworkPermissionDialogView( + dialog: LocalNetworkPermissionDialog, + onSubmit: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, +) { + val submitTextRes = when (dialog) { + LocalNetworkPermissionDialog.None -> return + LocalNetworkPermissionDialog.Rationale -> CommonStrings.dialog_allow_access + LocalNetworkPermissionDialog.Settings -> CommonStrings.action_open_settings + } + ConfirmationDialog( + title = stringResource(CommonStrings.screen_local_network_opt_in_title), + content = stringResource(CommonStrings.screen_local_network_opt_in_subtitle), + submitText = stringResource(submitTextRes), + cancelText = stringResource(CommonStrings.action_not_now), + onSubmitClick = onSubmit, + onDismiss = onDismiss, + modifier = modifier, + ) +} diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt similarity index 83% rename from libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt rename to libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt index a34ece0859c..a1f4022c3cf 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisor.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisor.kt @@ -5,16 +5,16 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.permissions.impl +package io.element.android.libraries.permissions.impl.localnetwork -import android.Manifest.permission.ACCESS_LOCAL_NETWORK +import android.Manifest import android.os.Build import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier import io.element.android.libraries.androidutils.network.LocalNetworkClassification -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor import io.element.android.libraries.permissions.api.PermissionStateProvider +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor import io.element.android.services.toolbox.api.sdk.BuildVersionSdkIntProvider @ContributesBinding(AppScope::class) @@ -25,7 +25,7 @@ class DefaultLocalNetworkPermissionAdvisor( ) : LocalNetworkPermissionAdvisor { override suspend fun shouldRequestPermissionFor(homeserverUrl: String): Boolean { if (!buildVersionSdkIntProvider.isAtLeast(Build.VERSION_CODES.CINNAMON_BUN)) return false - if (permissionStateProvider.isPermissionGranted(ACCESS_LOCAL_NETWORK)) return false + if (permissionStateProvider.isPermissionGranted(Manifest.permission.ACCESS_LOCAL_NETWORK)) return false return when (classifier.classify(homeserverUrl)) { LocalNetworkClassification.LocalIp -> true LocalNetworkClassification.PublicIp, diff --git a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt similarity index 98% rename from libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt rename to libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt index 2ed1dd7548e..1609ee1f207 100644 --- a/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/DefaultLocalNetworkPermissionAdvisorTest.kt +++ b/libraries/permissions/impl/src/test/kotlin/io/element/android/libraries/permissions/impl/localnetwork/DefaultLocalNetworkPermissionAdvisorTest.kt @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ -package io.element.android.libraries.permissions.impl +package io.element.android.libraries.permissions.impl.localnetwork import com.google.common.truth.Truth.assertThat import io.element.android.libraries.androidutils.network.LocalNetworkAddressClassifier diff --git a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt index 1a3363d6dcc..f16872441c9 100644 --- a/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt +++ b/libraries/permissions/test/src/main/kotlin/io/element/android/libraries/permissions/test/FakeLocalNetworkPermissionAdvisor.kt @@ -7,7 +7,7 @@ package io.element.android.libraries.permissions.test -import io.element.android.libraries.permissions.api.LocalNetworkPermissionAdvisor +import io.element.android.libraries.permissions.api.localnetwork.LocalNetworkPermissionAdvisor class FakeLocalNetworkPermissionAdvisor( private val shouldPrompt: Boolean = false, From 3ad0ca797412efa2b5f37b46ff56ba97eabce146 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Fri, 10 Jul 2026 07:46:06 +0000 Subject: [PATCH 10/10] Update screenshots --- ....components.customreaction_EmojiItemWithPopup_Day_0_en.png | 4 ++-- ...omponents.customreaction_EmojiItemWithPopup_Night_0_en.png | 4 ++-- ...es.preferences.impl.userstatus_UserStatusView_Day_5_en.png | 4 ++-- ...es.preferences.impl.userstatus_UserStatusView_Day_6_en.png | 4 ++-- ....preferences.impl.userstatus_UserStatusView_Night_5_en.png | 4 ++-- ....preferences.impl.userstatus_UserStatusView_Night_6_en.png | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Day_0_en.png index 912b90b5373..a2b35f18cac 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4746d11c30648f7c517b0880021e5a1dc6ed2819af6c800b081b0d842ac2c384 -size 5325 +oid sha256:c5572491b675a16724cd9e33ebb477c82edf285fa07d57ebef09c670fcaaddf1 +size 21285 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png index 45011fea4d3..9c425cec6e0 100644 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components.customreaction_EmojiItemWithPopup_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eede6f990a7a1dbddee1cd9e722b30600160d475ad2ae72faebcb773f52aaa7b -size 20793 +oid sha256:290a2be6838445739b0181006f9fea87337a85cfed3573c153e9f503c66624a6 +size 4994 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_5_en.png index 1f73455d54f..7ef9b7ae0a5 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b5dc5c492ed3fd552e77c0be85b639d55bc7e976f15b761f927b0b80ecca601d -size 12115 +oid sha256:d19e7a09114e76294a0626e5d617cf55fe7c13a0188a7d5b364f57c0d9daedc1 +size 12123 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_6_en.png index 0ca6f664380..3e188588737 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Day_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5896eb5df6f5398bdb94e74280d0d5c04b6a5a53c2378b7377c60fa58eef544b -size 12013 +oid sha256:42f03759fcffbb750181168256130b6a94a1fcf91ee028e69a19a6e3b68ed0e8 +size 11711 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_5_en.png index a6872411518..1e04c3e7d7d 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_5_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_5_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e7ce8a8c6b4d1692e4b127b7f17f507552666c631893025082c385b93d835a73 -size 11726 +oid sha256:6bbc8c05e6f6818d00282228f52424ebe93e6e3dca3431d742e8c652d56460d6 +size 11712 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_6_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_6_en.png index a1d1c5ec8e2..8e1b9c99d23 100644 --- a/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_6_en.png +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.userstatus_UserStatusView_Night_6_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8b93c08531efbeacb55f84b17ac793a7b226e5a2db7bc5072e94bc040cd1e9bc -size 11386 +oid sha256:66f6d0d6b03ee4c999cd4edc9c6ae567d31b8b537ca6f9cc8fbe436d06ce80ab +size 11643