{
- return if ((this as? ClientException.Generic)?.message?.contains("404") == true) {
- WellknownRetrieverResult.NotFound
- } else {
- WellknownRetrieverResult.Error(this as Exception)
- }
- }
-
- private fun url(host: String, source: EnterpriseRemoteConfigSource): String {
- val elementWellKnownUrl = "https://$host/.well-known/element/element.json"
- return when (source) {
- EnterpriseRemoteConfigSource.ESS_CONFIG -> enterpriseService.essConfigEndpointUrl(host) ?: elementWellKnownUrl
- EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT -> elementWellKnownUrl
- }
- }
-}
diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt
deleted file mode 100644
index 317109a1c0..0000000000
--- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalElementWellKnown.kt
+++ /dev/null
@@ -1,71 +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.libraries.wellknown.impl
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/**
- * Example:
- *
- * {
- * "registration_helper_url": "https://element.io",
- * "enforce_element_pro": true,
- * "rageshake_url": "https://example.org/rageshake",
- * "brand_color": "#FF0000",
- * "notification_sound": "ring.flac",
- * "idp_app_scheme": "io.element.app",
- * "custom_recovery_passphrase": {
- * "min_character_count": 8
- * }
- * }
- *
- * .
- */
-@Serializable
-data class InternalElementWellKnown(
- @SerialName("registration_helper_url")
- val registrationHelperUrl: String? = null,
- @SerialName("enforce_element_pro")
- val enforceElementPro: Boolean? = null,
- @SerialName("rageshake_url")
- val rageshakeUrl: String? = null,
- @SerialName("brand_color")
- val brandColor: String? = null,
- @SerialName("notification_sound")
- val notificationSound: String? = null,
- @SerialName("idp_app_scheme")
- val identityProviderAppScheme: String? = null,
- @SerialName("custom_recovery_passphrase")
- val customRecoveryPassphrase: InternalCustomRecoveryPassphrase? = null,
- @SerialName("content_scanner_url")
- val contentScannerUrl: String? = null,
- @SerialName("force_disable_e2ee")
- val forceDisableE2EE: Boolean? = null,
- @SerialName("map_tiler")
- val mapTilerConfig: InternalMapTilerConfig? = null,
-)
-
-@Serializable
-data class InternalCustomRecoveryPassphrase(
- @SerialName("min_character_count")
- val minCharacterCount: Int? = null,
-)
-
-@Serializable
-data class InternalMapTilerConfig(
- @SerialName("api_key")
- val apiKey: String,
- @SerialName("light_style_id")
- val lightStyleId: String? = null,
- @SerialName("dark_style_id")
- val darkStyleId: String? = null,
- @SerialName("base_url")
- val baseUrl: String? = null,
-)
diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnown.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnown.kt
deleted file mode 100644
index bf43356c45..0000000000
--- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnown.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-/*
- * Copyright (c) 2025 Element Creations Ltd.
- * Copyright 2023-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.libraries.wellknown.impl
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/**
- * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
- *
- * {
- * "m.homeserver": {
- * "base_url": "https://matrix.org"
- * },
- * "m.identity_server": {
- * "base_url": "https://vector.im"
- * },
- * }
- *
- * .
- */
-@Serializable
-data class InternalWellKnown(
- @SerialName("m.homeserver")
- val homeServer: InternalWellKnownBaseConfig? = null,
- @SerialName("m.identity_server")
- val identityServer: InternalWellKnownBaseConfig? = null,
-)
diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnownBaseConfig.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnownBaseConfig.kt
deleted file mode 100644
index 342cd43395..0000000000
--- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/InternalWellKnownBaseConfig.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Copyright (c) 2025 Element Creations Ltd.
- * Copyright 2023-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.libraries.wellknown.impl
-
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-
-/**
- * https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery
- *
- * {
- * "base_url": "https://element.io"
- * }
- *
- * .
- */
-@Serializable
-data class InternalWellKnownBaseConfig(
- @SerialName("base_url")
- val baseURL: String? = null
-)
diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt
deleted file mode 100644
index 254036c5c3..0000000000
--- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/Mapper.kt
+++ /dev/null
@@ -1,59 +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.libraries.wellknown.impl
-
-import io.element.android.libraries.core.log.logger.LoggerTag
-import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
-import io.element.android.libraries.wellknown.api.ElementWellKnown
-import io.element.android.libraries.wellknown.api.MapTilerConfig
-import timber.log.Timber
-
-private val loggerTag = LoggerTag("Wellknown")
-
-/**
- * Floor for the recovery passphrase minimum length. A value of 1 guarantees the derived passphrase is
- * never empty (an empty passphrase derives a recoverable but effectively unprotected secret-storage
- * key). Anything stronger than non-empty is left to the homeserver operator's configuration.
- */
-private const val MINIMUM_PASSPHRASE_CHARACTER_COUNT = 1
-
-fun InternalElementWellKnown.map() = ElementWellKnown(
- registrationHelperUrl = registrationHelperUrl,
- enforceElementPro = enforceElementPro,
- rageshakeUrl = rageshakeUrl,
- brandColor = brandColor,
- notificationSound = notificationSound,
- identityProviderAppScheme = identityProviderAppScheme,
- customRecoveryPassphrase = customRecoveryPassphrase?.toPublic(),
- contentScannerUrl = contentScannerUrl,
- forceDisableE2EE = forceDisableE2EE,
- mapTilerConfig = mapTilerConfig?.let {
- MapTilerConfig(
- apiKey = it.apiKey,
- lightStyleId = it.lightStyleId,
- darkStyleId = it.darkStyleId,
- baseUrl = it.baseUrl,
- )
- }
-)
-
-private fun InternalCustomRecoveryPassphrase.toPublic(): CustomRecoveryPassphrase {
- // Whenever the homeserver advertises the settings block we run the custom passphrase flow, flooring
- // the minimum at 1 so the passphrase can never be empty even if the server omits min_character_count
- // or advertises a non-positive value. The operator owns any stronger minimum.
- val min = (minCharacterCount ?: MINIMUM_PASSPHRASE_CHARACTER_COUNT).coerceAtLeast(MINIMUM_PASSPHRASE_CHARACTER_COUNT)
- if (min != minCharacterCount) {
- Timber.tag(loggerTag.value).w(
- "custom_recovery_passphrase.min_character_count was %s; flooring to %d",
- minCharacterCount,
- min,
- )
- }
- return CustomRecoveryPassphrase(minCharacterCount = min)
-}
diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/SessionWellknownModule.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/SessionWellknownModule.kt
deleted file mode 100644
index 1587e95765..0000000000
--- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/SessionWellknownModule.kt
+++ /dev/null
@@ -1,41 +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.libraries.wellknown.impl
-
-import dev.zacsweers.metro.BindingContainer
-import dev.zacsweers.metro.ContributesTo
-import dev.zacsweers.metro.Provides
-import io.element.android.features.enterprise.api.EnterpriseService
-import io.element.android.libraries.di.SessionScope
-import io.element.android.libraries.di.annotations.SessionCoroutineScope
-import io.element.android.libraries.matrix.api.UrlContentFetcher
-import io.element.android.libraries.wellknown.api.ElementWellKnownParser
-import io.element.android.libraries.wellknown.api.ElementWellknownStore
-import io.element.android.libraries.wellknown.api.WellknownRetriever
-import kotlinx.coroutines.CoroutineScope
-
-@BindingContainer
-@ContributesTo(SessionScope::class)
-object SessionWellknownModule {
- @Provides
- fun provideWellKnownRetriever(
- elementWellknownStoreFactory: ElementWellknownStore.Factory,
- enterpriseService: EnterpriseService,
- elementWellKnownParser: ElementWellKnownParser,
- urlContentFetcher: UrlContentFetcher,
- @SessionCoroutineScope coroutineScope: CoroutineScope,
- ): WellknownRetriever {
- return DefaultWellknownRetriever(
- elementWellknownStoreFactory = elementWellknownStoreFactory,
- enterpriseService = enterpriseService,
- elementWellKnownParser = elementWellKnownParser,
- urlContentFetcher = urlContentFetcher,
- coroutineScope = coroutineScope
- )
- }
-}
diff --git a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/WellknownAPI.kt b/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/WellknownAPI.kt
deleted file mode 100644
index 54528f7c42..0000000000
--- a/libraries/wellknown/impl/src/main/kotlin/io/element/android/libraries/wellknown/impl/WellknownAPI.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-/*
- * Copyright (c) 2025 Element Creations Ltd.
- * Copyright 2023-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.libraries.wellknown.impl
-
-import retrofit2.http.GET
-
-internal interface WellknownAPI {
- @GET(".well-known/matrix/client")
- suspend fun getWellKnown(): InternalWellKnown
-
- @GET(".well-known/element/element.json")
- suspend fun getElementWellKnown(): InternalElementWellKnown
-}
diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseTest.kt
deleted file mode 100644
index d4e7ac112e..0000000000
--- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/api/CustomRecoveryPassphraseTest.kt
+++ /dev/null
@@ -1,33 +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.libraries.wellknown.api
-
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-
-class CustomRecoveryPassphraseTest {
- @Test
- fun `empty input never satisfies a positive minimum`() {
- assertThat(CustomRecoveryPassphrase(minCharacterCount = 1).isSatisfiedBy("")).isFalse()
- }
-
- @Test
- fun `input shorter than minimum is not satisfied`() {
- assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abc")).isFalse()
- }
-
- @Test
- fun `input exactly at minimum is satisfied`() {
- assertThat(CustomRecoveryPassphrase(minCharacterCount = 8).isSatisfiedBy("abcdefgh")).isTrue()
- }
-
- @Test
- fun `input longer than minimum is satisfied`() {
- assertThat(CustomRecoveryPassphrase(minCharacterCount = 4).isSatisfiedBy("abcdefgh")).isTrue()
- }
-}
diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStoreTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStoreTest.kt
deleted file mode 100644
index dc101c41a0..0000000000
--- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellKnownStoreTest.kt
+++ /dev/null
@@ -1,174 +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.libraries.wellknown.impl
-
-import com.google.common.truth.Truth.assertThat
-import io.element.android.features.wellknown.test.anElementWellKnown
-import io.element.android.libraries.androidutils.json.DefaultJsonProvider
-import io.element.android.libraries.cachestore.api.CacheData
-import io.element.android.libraries.sessionstorage.test.InMemoryCacheStore
-import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
-import io.element.android.libraries.wellknown.api.ElementWellKnownParser
-import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
-import io.element.android.services.toolbox.test.systemclock.A_FAKE_TIMESTAMP
-import io.element.android.services.toolbox.test.systemclock.FakeSystemClock
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-
-private const val A_DOMAIN = "matrix.example.com"
-private const val A_CACHE_KEY = "https://$A_DOMAIN/.well-known/element/element.json"
-
-// 1 day in millis
-private const val CACHE_VALIDITY_MILLIS = 1 * 24 * 60 * 60 * 1000L
-
-class DefaultWellKnownStoreTest {
- private val jsonProvider = DefaultJsonProvider()
-
- @Test
- fun `get returns NotFound when cache is empty`() = runTest {
- val sut = createDefaultWellKnownStore()
- assertThat(sut.get(A_DOMAIN)).isEqualTo(WellknownRetrieverResult.NotFound)
- }
-
- @Test
- fun `get returns Success when cache has fresh data`() = runTest {
- val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
- val jsonString = jsonProvider().encodeToString(wellKnown)
- val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
- val cacheStore = InMemoryCacheStore(
- initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, A_FAKE_TIMESTAMP))
- )
- val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
-
- val result = sut.get(A_DOMAIN)
-
- assertThat(result).isEqualTo(WellknownRetrieverResult.Success(wellKnown.map()))
- }
-
- @Test
- fun `get returns Outdated when cached data has expired`() = runTest {
- val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
- val jsonString = jsonProvider().encodeToString(wellKnown)
- // Store data at timestamp 0, but clock is past the validity window
- val expiredTimestamp = 0L
- val nowTimestamp = expiredTimestamp + CACHE_VALIDITY_MILLIS + 1
- val clock = FakeSystemClock(epochMillisResult = nowTimestamp)
- val cacheStore = InMemoryCacheStore(
- initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, expiredTimestamp))
- )
- val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
-
- val result = sut.get(A_DOMAIN)
-
- assertThat(result).isEqualTo(WellknownRetrieverResult.Outdated(wellKnown.map()))
- }
-
- @Test
- fun `get returns Error when cached data is invalid JSON`() = runTest {
- val cacheStore = InMemoryCacheStore(
- initialData = mapOf(A_CACHE_KEY to CacheData("not valid json at all", A_FAKE_TIMESTAMP))
- )
- val sut = createDefaultWellKnownStore(cacheStore = cacheStore)
-
- val result = sut.get(A_DOMAIN)
-
- assertThat(result).isInstanceOf(WellknownRetrieverResult.Error::class.java)
- }
-
- @Test
- fun `update stores data and get returns Success`() = runTest {
- val wellKnown = """
- {
- "registration_helper_url": "https://element.io",
- "enforce_element_pro": true,
- "rageshake_url": "https://example.org/rageshake",
- "brand_color": "#FF0000",
- "notification_sound": "ring.flac",
- "idp_app_scheme": "io.element.app",
- "custom_recovery_passphrase": {
- "min_character_count": 8
- }
- }
- """.trimIndent()
- val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
- val cacheStore = InMemoryCacheStore()
- val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
-
- val updateResult = sut.update(A_DOMAIN, wellKnown)
- assertThat(updateResult.isSuccess).isTrue()
-
- val result = sut.get(A_DOMAIN)
- assertThat(result).isEqualTo(WellknownRetrieverResult.Success(anElementWellKnown(
- registrationHelperUrl = "https://element.io",
- enforceElementPro = true,
- rageshakeUrl = "https://example.org/rageshake",
- brandColor = "#FF0000",
- notificationSound = "ring.flac",
- identityProviderAppScheme = "io.element.app",
- customRecoveryPassphrase = CustomRecoveryPassphrase(8),
- )))
- }
-
- @Test
- fun `delete removes data and get returns NotFound`() = runTest {
- val wellKnown = InternalElementWellKnown(rageshakeUrl = "https://rageshake.example.com")
- val jsonString = jsonProvider().encodeToString(wellKnown)
- val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
- val cacheStore = InMemoryCacheStore(
- initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, A_FAKE_TIMESTAMP))
- )
- val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
-
- val deleteResult = sut.delete(A_DOMAIN)
- assertThat(deleteResult.isSuccess).isTrue()
-
- assertThat(sut.get(A_DOMAIN)).isEqualTo(WellknownRetrieverResult.NotFound)
- }
-
- @Test
- fun `get returns data at the exact cache validity boundary as Success`() = runTest {
- val wellKnown = InternalElementWellKnown(notificationSound = "ping.flac")
- val jsonString = jsonProvider().encodeToString(wellKnown)
- // Exactly at the boundary: epochMillis == updatedAt + CACHE_VALIDITY_MILLIS → not expired
- val storedAt = 1000L
- val nowAtBoundary = storedAt + CACHE_VALIDITY_MILLIS
- val clock = FakeSystemClock(epochMillisResult = nowAtBoundary)
- val cacheStore = InMemoryCacheStore(
- initialData = mapOf(A_CACHE_KEY to CacheData(jsonString, storedAt))
- )
- val sut = createDefaultWellKnownStore(cacheStore = cacheStore, systemClock = clock)
-
- val result = sut.get(A_DOMAIN)
-
- assertThat(result).isEqualTo(WellknownRetrieverResult.Success(wellKnown.map()))
- }
-
- @Test
- fun `prefix is used`() = runTest {
- val prefix = "test-prefix"
- val clock = FakeSystemClock(epochMillisResult = A_FAKE_TIMESTAMP)
- val cacheStore = InMemoryCacheStore(
- initialData = mapOf("$prefix-$A_DOMAIN" to CacheData("{}", A_FAKE_TIMESTAMP))
- )
- val sut = createDefaultWellKnownStore(prefix = prefix, cacheStore = cacheStore, systemClock = clock)
-
- assertThat(sut.get(A_DOMAIN)).isInstanceOf(WellknownRetrieverResult.Success::class.java)
- }
-
- private fun createDefaultWellKnownStore(
- prefix: String? = null,
- cacheStore: InMemoryCacheStore = InMemoryCacheStore(),
- systemClock: FakeSystemClock = FakeSystemClock(),
- elementWellKnownParser: ElementWellKnownParser = DefaultElementWellKnownParser(jsonProvider),
- ) = DefaultWellKnownStore(
- prefix = prefix,
- cacheStore = cacheStore,
- systemClock = systemClock,
- elementWellKnownParser = elementWellKnownParser,
- )
-}
diff --git a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt b/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt
deleted file mode 100644
index a58aa0a347..0000000000
--- a/libraries/wellknown/impl/src/test/kotlin/io/element/android/libraries/wellknown/impl/DefaultWellknownRetrieverTest.kt
+++ /dev/null
@@ -1,241 +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.
- */
-
-@file:OptIn(ExperimentalCoroutinesApi::class)
-
-package io.element.android.libraries.wellknown.impl
-
-import com.google.common.truth.Truth.assertThat
-import io.element.android.features.enterprise.test.FakeEnterpriseService
-import io.element.android.features.wellknown.test.FakeElementWellknownStore
-import io.element.android.features.wellknown.test.aMapTilerConfig
-import io.element.android.features.wellknown.test.anElementWellKnown
-import io.element.android.libraries.core.uri.ensureProtocol
-import io.element.android.libraries.matrix.api.UrlContentFetcher
-import io.element.android.libraries.matrix.api.exception.ClientException
-import io.element.android.libraries.wellknown.api.ElementWellKnown
-import io.element.android.libraries.wellknown.api.ElementWellKnownParser
-import io.element.android.libraries.wellknown.api.ElementWellknownStore
-import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
-import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
-import io.element.android.tests.testutils.lambda.lambdaRecorder
-import io.element.android.tests.testutils.lambda.value
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import java.net.URL
-
-class DefaultWellknownRetrieverTest {
- @Test
- fun `get element wellknown calls resolver and parser`() = runTest {
- val parser = lambdaRecorder> { Result.success(anElementWellKnown()) }
- val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
- val sut = createDefaultWellknownRetriever(
- elementWellKnownParser = parser,
- urlResolver = resolver,
- )
-
- sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)
-
- resolver.assertions().isCalledOnce().with(value(WELLKNOWN_URL))
- parser.assertions().isCalledOnce().with(value(WELLKNOWN_CONTENT))
- }
-
- @Test
- fun `get element wellknown parser error`() = runTest {
- val sut = createDefaultWellknownRetriever(
- elementWellKnownParser = { Result.failure(IllegalStateException("Invalid JSON")) },
- )
- assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
- .isInstanceOf(WellknownRetrieverResult.Error::class.java)
- }
-
- @Test
- fun `get element wellknown network error`() = runTest {
- val resolver = SpyUrlContentFetcher { Result.failure(IllegalStateException("Network error")) }
- val sut = createDefaultWellknownRetriever(urlResolver = resolver)
- assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
- .isInstanceOf(WellknownRetrieverResult.Error::class.java)
- }
-
- @Test
- fun `get element wellknown 404 http error counts as not found`() = runTest {
- val resolver = SpyUrlContentFetcher { Result.failure(ClientException.Generic("Http error: 404", null)) }
- val sut = createDefaultWellknownRetriever(urlResolver = resolver)
- assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
- .isInstanceOf(WellknownRetrieverResult.NotFound::class.java)
- }
-
- @Test
- fun `get element wellknown will return an error if the provided host is not valid`() = runTest {
- val sut = createDefaultWellknownRetriever()
- assertThat(sut.getElementWellKnown("!not valid", EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
- .isInstanceOf(WellknownRetrieverResult.Error::class.java)
- }
-
- @Test
- fun `get element wellknown will return a cached value if present instead of fetching`() = runTest {
- val host = URL(WELLKNOWN_URL).host.ensureProtocol()
- val sut = createDefaultWellknownRetriever(
- cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Success(expectedElementWellKnown))) }
- )
- assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
- .isInstanceOf(WellknownRetrieverResult.Success::class.java)
- }
-
- @Test
- fun `get element wellknown will return an outdated value first then fetch the current one`() = runTest {
- val host = URL(WELLKNOWN_URL).host.ensureProtocol()
- val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
- val sut = createDefaultWellknownRetriever(
- urlResolver = resolver,
- cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Outdated(expectedElementWellKnown))) }
- )
- // The first call returns the outdated value
- assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
- .isInstanceOf(WellknownRetrieverResult.Outdated::class.java)
- // The resolver is not called yet because the fetch is done in the background
- resolver.assertions().isNeverCalled()
-
- // We give it some time to run the background fetch
- runCurrent()
-
- // The resolver is called during the background fetch
- resolver.assertions().isCalledOnce().with(value(WELLKNOWN_URL))
- }
-
- @Test
- fun `get element wellknown will return an error if it happens when trying to retrieve the cached value`() = runTest {
- val host = URL(WELLKNOWN_URL).host.ensureProtocol()
- val sut = createDefaultWellknownRetriever(
- cacheStoreFactory = { FakeElementWellknownStore(initialData = mapOf(host to WellknownRetrieverResult.Error(IllegalStateException("BOOM")))) }
- )
- assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
- .isInstanceOf(WellknownRetrieverResult.Error::class.java)
- }
-
- @Test
- fun `get element wellknown will return an error if it fails to serialize the retrieved value into the cache`() = runTest {
- val sut = createDefaultWellknownRetriever(
- elementWellKnownParser = { Result.failure(IllegalStateException("BOOM")) },
- )
- assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT))
- .isInstanceOf(WellknownRetrieverResult.Error::class.java)
- }
-
- @Test
- fun `WELLKNOWN_ENDPOINT source creates a store with no prefix`() = runTest {
- val storeFactory = lambdaRecorder { FakeElementWellknownStore() }
- val sut = createDefaultWellknownRetriever(cacheStoreFactory = storeFactory)
-
- sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)
-
- storeFactory.assertions().isCalledOnce().with(value(null))
- }
-
- @Test
- fun `ESS_CONFIG source creates a store with ess prefix and checks the ESS config endpoint`() = runTest {
- val essConfigUrl = "https://ess-config.example.com"
- val storeFactory = lambdaRecorder { FakeElementWellknownStore() }
- val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
- val sut = createDefaultWellknownRetriever(
- cacheStoreFactory = storeFactory,
- enterpriseService = FakeEnterpriseService(
- overrideWellKnownResult = { null },
- essConfigEndpointUrlResult = { essConfigUrl },
- ),
- urlResolver = resolver,
- )
-
- sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.ESS_CONFIG)
-
- storeFactory.assertions().isCalledOnce().with(value("ess_config"))
- resolver.assertions().isCalledOnce().with(value(essConfigUrl))
- }
-
- @Test
- fun `get element wellknown was overridden`() = runTest {
- val wellKnown = anElementWellKnown()
-
- val resolver = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) }
-
- val sut = createDefaultWellknownRetriever(
- enterpriseService = FakeEnterpriseService(
- overrideWellKnownResult = { wellKnown },
- ),
- urlResolver = resolver,
- )
-
- // The overridden value is returned
- assertThat(sut.getElementWellKnown(WELLKNOWN_URL, EnterpriseRemoteConfigSource.WELLKNOWN_ENDPOINT)).isEqualTo(
- WellknownRetrieverResult.Success(wellKnown)
- )
-
- // And the endpoint is never hit
- resolver.assertions().isNeverCalled()
- }
-
- private fun TestScope.createDefaultWellknownRetriever(
- cacheStoreFactory: ElementWellknownStore.Factory = { FakeElementWellknownStore() },
- elementWellKnownParser: ElementWellKnownParser = { Result.success(expectedElementWellKnown) },
- enterpriseService: FakeEnterpriseService = FakeEnterpriseService(overrideWellKnownResult = { null }),
- urlResolver: UrlContentFetcher = SpyUrlContentFetcher { Result.success(WELLKNOWN_CONTENT.toByteArray()) },
- ) = DefaultWellknownRetriever(
- elementWellknownStoreFactory = cacheStoreFactory,
- enterpriseService = enterpriseService,
- elementWellKnownParser = elementWellKnownParser,
- urlContentFetcher = urlResolver,
- coroutineScope = backgroundScope,
- )
-
- companion object {
- private const val WELLKNOWN_URL = "https://user.domain.org/.well-known/element/element.json"
- private const val WELLKNOWN_CONTENT = """{
- "registration_helper_url": "a_registration_url",
- "enforce_element_pro": true,
- "rageshake_url": "a_rageshake_url",
- "brand_color": "#FF0000",
- "notification_sound": "a_notification_sound.flac",
- "idp_app_scheme": "an_app_scheme",
- "content_scanner_url": "https://content-scanner.example.com",
- "force_disable_e2ee": false,
- "map_tiler": {
- "api_key": "test_api_key",
- "base_url": "https://tile-server.example.com"
- }
- }"""
-
- private val expectedElementWellKnown = ElementWellKnown(
- registrationHelperUrl = "a_registration_url",
- enforceElementPro = true,
- rageshakeUrl = "a_rageshake_url",
- brandColor = "#FF0000",
- notificationSound = "a_notification_sound.flac",
- identityProviderAppScheme = "an_app_scheme",
- contentScannerUrl = "https://content-scanner.example.com",
- customRecoveryPassphrase = null,
- forceDisableE2EE = false,
- mapTilerConfig = aMapTilerConfig(
- apiKey = "test_api_key",
- baseUrl = "https://tile-server.example.com",
- )
- )
- }
-}
-
-private class SpyUrlContentFetcher(lambda: (String) -> Result) : UrlContentFetcher {
- private val spy = lambdaRecorder> { lambda(it) }
-
- override suspend fun getUrl(url: String): Result {
- return spy(url)
- }
-
- fun assertions() = spy.assertions()
-}
diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeElementWellknownStore.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeElementWellknownStore.kt
deleted file mode 100644
index c5e2d2ae5b..0000000000
--- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeElementWellknownStore.kt
+++ /dev/null
@@ -1,53 +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.wellknown.test
-
-import io.element.android.libraries.androidutils.json.DefaultJsonProvider
-import io.element.android.libraries.androidutils.json.JsonProvider
-import io.element.android.libraries.wellknown.api.ElementWellKnown
-import io.element.android.libraries.wellknown.api.ElementWellknownStore
-import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
-import io.element.android.libraries.wellknown.impl.InternalElementWellKnown
-import io.element.android.libraries.wellknown.impl.map
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.json.JsonDecodingException
-import java.net.URL
-
-class FakeElementWellknownStore(
- initialData: Map> = emptyMap(),
- private val jsonProvider: JsonProvider = DefaultJsonProvider(),
-) : ElementWellknownStore {
- private val cachedData = mutableMapOf>()
-
- init {
- initialData.forEach { (url, result) ->
- cachedData[URL(url).host] = result
- }
- }
-
- override suspend fun get(domain: String): WellknownRetrieverResult {
- return cachedData[domain] ?: WellknownRetrieverResult.NotFound
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- override suspend fun update(domain: String, wellknown: String): Result {
- val json = jsonProvider()
- val mapped = try {
- json.decodeFromString(wellknown).map()
- } catch (e: JsonDecodingException) {
- return Result.failure(e)
- }
- cachedData[domain] = WellknownRetrieverResult.Success(mapped)
- return Result.success(Unit)
- }
-
- override suspend fun delete(domain: String): Result {
- cachedData.remove(domain)
- return Result.success(Unit)
- }
-}
diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt
deleted file mode 100644
index e08969d0f9..0000000000
--- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetriever.kt
+++ /dev/null
@@ -1,24 +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.wellknown.test
-
-import io.element.android.libraries.wellknown.api.ElementWellKnown
-import io.element.android.libraries.wellknown.api.EnterpriseRemoteConfigSource
-import io.element.android.libraries.wellknown.api.WellknownRetriever
-import io.element.android.libraries.wellknown.api.WellknownRetrieverResult
-
-class FakeWellknownRetriever(
- private val getElementWellKnownResult: (String, EnterpriseRemoteConfigSource) -> WellknownRetrieverResult = { _, _ ->
- WellknownRetrieverResult.NotFound
- },
-) : WellknownRetriever {
- override suspend fun getElementWellKnown(host: String, source: EnterpriseRemoteConfigSource): WellknownRetrieverResult {
- return getElementWellKnownResult(host, source)
- }
-}
diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetrieverFactory.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetrieverFactory.kt
deleted file mode 100644
index 718649dd5d..0000000000
--- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/FakeWellknownRetrieverFactory.kt
+++ /dev/null
@@ -1,17 +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.wellknown.test
-
-import io.element.android.libraries.matrix.api.UrlContentFetcher
-import io.element.android.libraries.wellknown.api.WellknownRetriever
-
-class FakeWellknownRetrieverFactory(
- private val wellknownRetriever: WellknownRetriever = FakeWellknownRetriever(),
-) : WellknownRetriever.Factory {
- override fun create(urlContentFetcher: UrlContentFetcher): WellknownRetriever = wellknownRetriever
-}
diff --git a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt b/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt
deleted file mode 100644
index 36d9f7bc82..0000000000
--- a/libraries/wellknown/test/src/main/kotlin/io/element/android/features/wellknown/test/Fixtures.kt
+++ /dev/null
@@ -1,55 +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.wellknown.test
-
-import io.element.android.libraries.wellknown.api.CustomRecoveryPassphrase
-import io.element.android.libraries.wellknown.api.ElementWellKnown
-import io.element.android.libraries.wellknown.api.MapTilerConfig
-
-fun anElementWellKnown(
- registrationHelperUrl: String? = null,
- enforceElementPro: Boolean? = null,
- rageshakeUrl: String? = null,
- brandColor: String? = null,
- notificationSound: String? = null,
- identityProviderAppScheme: String? = null,
- customRecoveryPassphrase: CustomRecoveryPassphrase? = null,
- contentScannerUrl: String? = null,
- forceDisableE2EE: Boolean? = null,
- mapTilerConfig: MapTilerConfig? = null,
-) = ElementWellKnown(
- registrationHelperUrl = registrationHelperUrl,
- enforceElementPro = enforceElementPro,
- rageshakeUrl = rageshakeUrl,
- brandColor = brandColor,
- notificationSound = notificationSound,
- identityProviderAppScheme = identityProviderAppScheme,
- customRecoveryPassphrase = customRecoveryPassphrase,
- contentScannerUrl = contentScannerUrl,
- forceDisableE2EE = forceDisableE2EE,
- mapTilerConfig = mapTilerConfig,
-)
-
-fun aCustomRecoveryPassphrase(
- minCharacterCount: Int = 8,
-) = CustomRecoveryPassphrase(
- minCharacterCount = minCharacterCount,
-)
-
-fun aMapTilerConfig(
- apiKey: String = "test_api_key",
- baseUrl: String? = null,
- lightStyleId: String? = null,
- darkStyleId: String? = null,
-) = MapTilerConfig(
- apiKey = apiKey,
- baseUrl = baseUrl,
- lightStyleId = lightStyleId,
- darkStyleId = darkStyleId,
-)
diff --git a/plugins/src/main/kotlin/Versions.kt b/plugins/src/main/kotlin/Versions.kt
index 1c77bea7cf..89ad4a63fc 100644
--- a/plugins/src/main/kotlin/Versions.kt
+++ b/plugins/src/main/kotlin/Versions.kt
@@ -45,7 +45,7 @@ private const val versionMonth = 8
* Release number in the month. Value must be in [0,99].
* Do not update this value. it is updated by the release script.
*/
-private const val versionReleaseNumber = 1
+private const val versionReleaseNumber = 2
object Versions {
/**
diff --git a/plugins/src/main/kotlin/config/BuildTimeConfig.kt b/plugins/src/main/kotlin/config/BuildTimeConfig.kt
index 83236f1a5a..4160efeb78 100644
--- a/plugins/src/main/kotlin/config/BuildTimeConfig.kt
+++ b/plugins/src/main/kotlin/config/BuildTimeConfig.kt
@@ -11,18 +11,14 @@ package config
object BuildTimeConfig {
const val APPLICATION_ID = "io.element.android.x"
const val APPLICATION_NAME = "Element X"
- const val GOOGLE_APP_ID_RELEASE = "1:912726360885:android:d097de99a4c23d2700427c"
- const val GOOGLE_APP_ID_DEBUG = "1:912726360885:android:def0a4e454042e9b00427c"
- const val GOOGLE_APP_ID_NIGHTLY = "1:912726360885:android:e17435e0beb0303000427c"
-
val METADATA_HOST_REVERSED: String? = null
+ val OAUTH_CLIENT_URL_PATH: String? = "apps/android"
val URL_WEBSITE: String? = null
val URL_LOGO: String? = null
val URL_COPYRIGHT: String? = null
val URL_ACCEPTABLE_USE: String? = null
val URL_PRIVACY: String? = null
val URL_POLICY: String? = null
- val OAUTH_CLIENT_URL_PATH: String? = "apps/android"
val SERVICES_MAPTILER_BASE_URL: String? = null
val SERVICES_MAPTILER_APIKEY: String? = null
val SERVICES_MAPTILER_LIGHT_MAPID: String? = null
@@ -33,7 +29,9 @@ object BuildTimeConfig {
val SERVICES_SENTRY_DSN_RUST: String? = null
val BUG_REPORT_URL: String? = null
val BUG_REPORT_APP_NAME: String? = null
-
- const val PUSH_CONFIG_INCLUDE_FIREBASE = true
- const val PUSH_CONFIG_INCLUDE_UNIFIED_PUSH = true
+ const val PUSH_CONFIG_INCLUDE_FIREBASE: Boolean = true
+ const val PUSH_CONFIG_INCLUDE_UNIFIED_PUSH: Boolean = true
+ val PUSHER_APP_ID_RELEASE: String? = null
+ val PUSHER_APP_ID_DEBUG: String? = null
+ val PUSHER_APP_ID_NIGHTLY: String? = null
}
diff --git a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt
index eb59579d79..01f4329111 100644
--- a/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt
+++ b/services/analyticsproviders/posthog/src/main/kotlin/io/element/android/services/analyticsproviders/posthog/PosthogEndpointConfigProvider.kt
@@ -9,7 +9,6 @@
package io.element.android.services.analyticsproviders.posthog
import dev.zacsweers.metro.Inject
-import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.core.extensions.isElement
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
@@ -17,10 +16,9 @@ import io.element.android.libraries.core.meta.BuildType
@Inject
class PosthogEndpointConfigProvider(
private val buildMeta: BuildMeta,
- private val enterpriseService: EnterpriseService,
) {
fun provide(): PosthogEndpointConfig? {
- return if (enterpriseService.isEnterpriseBuild) {
+ return if (buildMeta.isEnterpriseBuild) {
PosthogEndpointConfig(
host = BuildConfig.POSTHOG_HOST,
apiKey = BuildConfig.POSTHOG_APIKEY,
diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt
index 202432c6d8..53cdbbf225 100644
--- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt
+++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistClassNameTest.kt
@@ -103,6 +103,7 @@ class KonsistClassNameTest {
"FakeImageLoader",
"FakeKeyStore",
"FakeListenableFuture",
+ "FakeParentNode",
)
.assertTrue {
val interfaceName = it.name
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeNodeFactoriesBindings.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeNodeFactoriesBindings.kt
new file mode 100644
index 0000000000..cee2c74960
--- /dev/null
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeNodeFactoriesBindings.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.tests.testutils.node
+
+import com.bumble.appyx.core.node.Node
+import io.element.android.libraries.architecture.AssistedNodeFactory
+import io.element.android.libraries.architecture.NodeFactoriesBindings
+import kotlin.reflect.KClass
+
+class FakeNodeFactoriesBindings(
+ private val nodeFactories: Map, AssistedNodeFactory<*>>,
+) : NodeFactoriesBindings {
+ override fun nodeFactories() = nodeFactories
+}
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeParentNode.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeParentNode.kt
new file mode 100644
index 0000000000..1375043d48
--- /dev/null
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/node/FakeParentNode.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.tests.testutils.node
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.bumble.appyx.core.modality.BuildContext
+import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel
+import com.bumble.appyx.core.node.Node
+import com.bumble.appyx.core.node.ParentNode
+import com.bumble.appyx.core.node.node
+import io.element.android.libraries.di.DependencyInjectionGraphOwner
+
+/**
+ * A Node creates its children using the dependency injection graph of its ancestors, so a Node under test
+ * needs such a parent to be able to create its children.
+ */
+class FakeParentNode(
+ override val graph: Any,
+) : ParentNode(
+ navModel = PermanentNavModel(navTargets = setOf(Unit), savedStateMap = null),
+ buildContext = BuildContext.root(savedStateMap = null),
+),
+ DependencyInjectionGraphOwner {
+ override fun resolve(navTarget: Unit, buildContext: BuildContext): Node = node(buildContext) {}
+
+ @Composable
+ override fun View(modifier: Modifier) = Unit
+}
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/MutablePresenter.kt
similarity index 93%
rename from tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt
rename to tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/MutablePresenter.kt
index ae8768e0b0..13fcc920c2 100644
--- a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/MutablePresenter.kt
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/MutablePresenter.kt
@@ -6,7 +6,7 @@
* Please see LICENSE files in the repository root for full details.
*/
-package io.element.android.tests.testutils
+package io.element.android.tests.testutils.presenter
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/NotUsedPresenter.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/NotUsedPresenter.kt
new file mode 100644
index 0000000000..bd786c2484
--- /dev/null
+++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/presenter/NotUsedPresenter.kt
@@ -0,0 +1,19 @@
+/*
+ * 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.tests.testutils.presenter
+
+import androidx.compose.runtime.Composable
+import io.element.android.libraries.architecture.Presenter
+
+/**
+ * A presenter that cannot be used, util for testing Nodes.
+ */
+class NotUsedPresenter : Presenter {
+ @Composable
+ override fun present(): T = error("Not used")
+}
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_0_en.png
new file mode 100644
index 0000000000..bd73e322ab
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81
+size 3657
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png
new file mode 100644
index 0000000000..955af7bb9a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a856467fba0134aefcad2428493ce9169fccf90705b71d540c2eed935d9b625e
+size 22639
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png
new file mode 100644
index 0000000000..a4fa915963
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:70386b8f6a8998f246819c6d643543babcf41ba72db86db40559a755876c7650
+size 12144
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png
new file mode 100644
index 0000000000..25a66543da
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:18498dbff28491dd108900453eab13d8224aa7960eefbe25993dcc82198677dd
+size 35825
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png
new file mode 100644
index 0000000000..be7992706c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:346946f668bf18908480bf6b97158adcd7d9099d4be111055412ac2d4b7094c9
+size 77330
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png
new file mode 100644
index 0000000000..ca2936923c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Day_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cc3946915978e17cf404469273a5ff174c12f53da8fc3cff53ac6df8721e0182
+size 23522
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_0_en.png
new file mode 100644
index 0000000000..bd73e322ab
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81
+size 3657
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png
new file mode 100644
index 0000000000..cfef0a6883
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ad20c730bb320093a5c639938430e6851244e4425b46f3ae10262d9ca96215a4
+size 21972
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png
new file mode 100644
index 0000000000..ec806ea229
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:cb20125a38d4d9436bee64de8f8d09d4afa372a72b7e33325b8d69f5b5760cdd
+size 11694
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png
new file mode 100644
index 0000000000..8dc241988a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a7d01b051fda7a3f6dc7736bdbd631bd2e0f0cef61dbd9c973b48cee1e69d56c
+size 35624
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png
new file mode 100644
index 0000000000..8e618f0070
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6637c79e684bf350204756a93679163fb17b8cf5f54c835fe81464d644edc3ff
+size 75840
diff --git a/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png
new file mode 100644
index 0000000000..2864a9ffc8
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.home.impl.search_GlobalSearchView_Night_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e305c212a008854c19255f91824631d55d9fb523799ecad9b50039aa8b178323
+size 23053
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png
deleted file mode 100644
index c9bf9e3c1c..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Day_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:0990301d92d4862054ba5e54103dac359df37997242498766854d7bb13431657
-size 6068
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png
deleted file mode 100644
index fb829c4873..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.accountprovider_AccountProviderOtherView_Night_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:3022b2b813d9a3f77b0ba72706f0fb870a1c3f7b4a8efc90e611b66b6e9bdf8e
-size 5926
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png
deleted file mode 100644
index 23858ce575..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:c7544e2566517e8692a1cccd4607bc2ab0f2767bbe9e5b39eb439d7695ad2ce5
-size 46905
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png
deleted file mode 100644
index 9b505f3cf9..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Day_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:b5dbf45467305e83f302fd626a2c0adcd27eac6aeea8b7a83efdb6e728d974da
-size 44703
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png
deleted file mode 100644
index 815986cbc3..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:41907de78c86fca804af2af7dd351db6a1ab87515e71bb906ffa2219ad4f5879
-size 45880
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png
deleted file mode 100644
index f08662715a..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.changeaccountprovider_ChangeAccountProviderView_Night_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:62581b532695d10a42432dfcb37249c28c2bd617ecb49d7f3a8224855b78ae55
-size 43600
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png
index 721b1f8389..caa1dc1cbb 100644
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:9fb394bf8c063e5b4457fc75c171849b1ae7f435941e0d86d09298a846595861
-size 35160
+oid sha256:da11677aad93fcf9461c62a0fb9a1f384950dc69f0666914da5785265d56a4dd
+size 35877
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png
index 3d04fae696..cb6f870735 100644
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:4626f30754044cee4438c44f8972f1b643342aea9c7b38f3650874da8c7e7614
-size 37303
+oid sha256:99d28d3547d50e9ae23b012521ef5f8a157aa4b3a8a98b3e138847652f8cacff
+size 35761
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png
index c0d9a71d1d..d9eaefae3c 100644
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ccaed5beab96b32c7ba783136d1fe8483697de44d0ed7c7555cbef9dd3708e66
-size 38953
+oid sha256:84954d900742e981811f1471fbe175f57c927ffc4da9e801d5e5b48f073dc13a
+size 39610
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_3_en.png
new file mode 100644
index 0000000000..aa987474a3
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a524b0538ad4204ea66c46c139035671f55d874c45d46476fb885c15879b93f2
+size 31806
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png
index 0127885711..0b0b5f123c 100644
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5ae87930bb1123d432f7f4e2792ad1e372ec383f968ad335b4f2f069b8faa307
-size 34294
+oid sha256:bb5607ed2e0986d5035120a2e6276ff33e65a7bafa5325a0c0c22f1975b69c2b
+size 34986
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png
index b15dcf0142..4da08d87a3 100644
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_1_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:787b3708bfc0bebd19cde31589e175ee0ea593c4bd1461b9593e9705947e6ffc
-size 36280
+oid sha256:80c7e798346755382f913f72da9b18fc200867280e6f33adde3622a964f7d2d7
+size 34883
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png
index b9046df90d..3d6c836738 100644
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_2_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:e8d1a1013e5eaed74889193e4c6551ae5afa96a1d10fa3d32de3dc96a0c1ae13
-size 36582
+oid sha256:cbe35c9bb9ec509b64a2ce27afa879b8904269ae8f9afbd5ac8a91098d7fccb3
+size 38777
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_3_en.png
new file mode 100644
index 0000000000..bada75332c
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.confirmaccountprovider_ConfirmAccountProviderView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:efa31bac320466c41fbf33b462418716c1f4fb6def584b38834320d213d2c819
+size 29777
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_0_en.png
deleted file mode 100644
index 34cf030cba..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:09bf68ea40f2b1c109b015b8e9e9c767931ef70936a956652ac649b8e5a2561e
-size 13491
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_1_en.png
deleted file mode 100644
index 67186daea6..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:323218a1ce6e4afaf4632fed871cbc6e17ed6686243523aeb9da0d81a7825d31
-size 13602
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_2_en.png
deleted file mode 100644
index c0cc08611c..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_2_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:193e7af1adc71d5e1133694e234671ebb6fefe3766c9891733c3ae46a999b1cd
-size 13655
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_3_en.png
deleted file mode 100644
index 26e32e2174..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Day_3_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:cb76a05a00c3e41c7edaa853db6d3773fddfb23c750eea4f45d4f4b1acfe5447
-size 15466
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_0_en.png
deleted file mode 100644
index da53968272..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:3a153cb8df9aadaa9cee0fef7f140e43f33851f18815674edc9a9682a7c601d2
-size 12942
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png
deleted file mode 100644
index 5df3ef308b..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:cba9108df3dae3f9968825e13528087fdf5d8b70c03faabbf0b06e276392a7d7
-size 13048
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_2_en.png
deleted file mode 100644
index 91a5e6418a..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_2_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:79cd361b36c8be418871a8cf6773bcc8f3626fc8d09012340f6d245ea6b0574e
-size 12967
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_3_en.png
deleted file mode 100644
index c6b38f80b0..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.createaccount_CreateAccountView_Night_3_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:9063e4d7b900c397dacd964c25ce42865e292ea2a5a41f37cb7dfabd86f9246f
-size 14142
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en.png
deleted file mode 100644
index 86e3a6a0c1..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:64ca7f84a90416cac5161e379079edb3edadf1f82240214974a3216cd5d41090
-size 23418
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png
deleted file mode 100644
index 583a7beb59..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Day_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:bd5189b1079f9011162ff7ba2c9211fbfff543db892b86ea3b57ec78495d2019
-size 50344
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en.png
deleted file mode 100644
index fd9fce9c5c..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_0_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:5122ee1af4ff4ddc5e4c766f992e9833e4c1329bc634e82b0e52743326328447
-size 22757
diff --git a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png
deleted file mode 100644
index 1be9bd5d99..0000000000
--- a/tests/uitests/src/test/snapshots/images/features.login.impl.screens.searchaccountprovider_SearchAccountProviderView_Night_1_en.png
+++ /dev/null
@@ -1,3 +0,0 @@
-version https://git-lfs.github.com/spec/v1
-oid sha256:96742102f57f2e58027e3402ea01c91ee1eab6010dc46ef8ff8ee3175264a628
-size 49259
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_0_en.png
new file mode 100644
index 0000000000..bd73e322ab
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81
+size 3657
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_1_en.png
new file mode 100644
index 0000000000..98a271f16e
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3412f2c40e8bff9f152ad856050415a16b76376d46eda582168f75e126485676
+size 21803
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_2_en.png
new file mode 100644
index 0000000000..de396af500
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ec927c9bd1b1e0000521d476f3141a6d2912b558ee1fddd089936afd59ac4d27
+size 20905
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_3_en.png
new file mode 100644
index 0000000000..3edc968290
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:346138ce103462468652d51f969514a17b096795163ce1d2be2a99adf61471f4
+size 21766
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_4_en.png
new file mode 100644
index 0000000000..74989de70a
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1afe321e7c1f2a57d3317e952975655f89a4acae1386b425be089e03cc03a8d9
+size 22697
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_5_en.png
new file mode 100644
index 0000000000..7bd3c0d04f
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Day_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:aba767b7b1380ca1fcd49498376128b5e537da7f15b2512c6f6cf409b3a3f9df
+size 19567
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_0_en.png
new file mode 100644
index 0000000000..bd73e322ab
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_0_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:12020ba97720f2374f9ace172693f08be01522e4ca30d3970efa91d802581e81
+size 3657
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_1_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_1_en.png
new file mode 100644
index 0000000000..b1f589b037
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_1_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:05211670ecfed13426dae281438c0f51d4d98b1d33178ac1e3753bc36855b952
+size 20362
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_2_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_2_en.png
new file mode 100644
index 0000000000..c2ff59b834
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_2_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:9417e640c72190934981c3d7f29e9b60b241c69bcab800af7908b6b5d097089a
+size 19453
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_3_en.png
new file mode 100644
index 0000000000..7aa92967f6
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_3_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e38f946dfda1008370b1171f4daffb2afea93b6f8ba9d8e816b8a12124467087
+size 20365
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_4_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_4_en.png
new file mode 100644
index 0000000000..c5e55379a8
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_4_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:3996b007ecb0d6ccbba7b21d35a089b5ea5f0a314fe8a09b9f88e3829272a715
+size 21290
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_5_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_5_en.png
new file mode 100644
index 0000000000..5270ee0b32
--- /dev/null
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.sendfailure_SendFailureDialogView_Night_5_en.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:0d7b95deec3bb0c5b542885a8900144233434561b5488e50c1560cdb287b9a61
+size 18154
diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png
index f47e74ab7b..3c357233b9 100644
--- a/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png
+++ b/tests/uitests/src/test/snapshots/images/features.messages.impl_MessagesViewA11y_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:5823ae9a382421609200d569799505706180b94c6923f315caa81f8767ac58f8
-size 131340
+oid sha256:95c5746de02c584536b1178614d15460bbdda633ddf44485261007618bd99f50
+size 132268
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png
index 96215547bb..bae0e74d1e 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:d1622f9765fda65985a369d6958f7a5993696dd629c0c1013c0a1647cfb7b562
-size 50109
+oid sha256:25fdfa4b65263c02e10c6ddea16a3210a84722a742a66cd2d91b8b3f61968466
+size 49924
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png
index 70d41498a8..389ae383c8 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_MarkdownTextComposerEdit_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:a94de003f65d2fb30ab42e4ac60802707abf1358b0defed7598a3aad69992f40
-size 48136
+oid sha256:dc99c7505aa48b7ccdbae1de6daec75c12b9293172c804ef3cf027f6b46c45ea
+size 47883
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png
index c17cbfdb66..45f5ce2bde 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:fdab4bea33337547250355c29991f45c118e8c2c05abdf69b1824475feced5d6
-size 51241
+oid sha256:c446e3b322a1eb8634f2f643819672cfb626ca9e76c7e1fb5a10fc03a1ae7824
+size 51031
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png
index ef9c49d6c5..ff238fd979 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerAddCaption_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:0f92c594febc463911c8418deeff3b94e5a8ce4171461d812a0f4f527ab0cf8e
-size 49245
+oid sha256:86260ffd841034c31327df736e0e127271f875a6cf383c688b7ea8c3a9d1dbd4
+size 48943
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png
index 9ba3d99527..cebea0c88b 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:81d8acdb1158c3e5c19b5a602e3b9afe7643f1bb77d43f51d3f5c4327478a76a
-size 50282
+oid sha256:9669ba8ab19e7f7f1322e973e6f39855113b45ab5f6b26e7b5230a2fe9ec1d68
+size 50067
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png
index 969e5b04bf..50cd50736a 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditCaption_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:c225ad12c5a32e2392606f9afacc6dbc37478a141d5485e4942012c6b22ad052
-size 48046
+oid sha256:f1f66076e3b06df1471f367bed57f81d15272e3600eebc51cd131794e29d5dfb
+size 47767
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png
index a0192f0890..c4b21b86af 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:3cfb1d8b7828692d5c318206770f9484822a1749b3e80a618b502811cf512d76
-size 60489
+oid sha256:ffe11074abe52ccd1c34f687f780c5185be370b2b92867ee0b686bad85f28890
+size 60210
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png
index c745fa256a..839c4a88ba 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEditNotEncrypted_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:ca67409e4af247b43d8594d6351419cd12894485357596aff7b26745e2df009a
-size 57687
+oid sha256:e9914f3ffd70dd83013ff6059539bda348ae64fc39a2419d621f8d198f6471cc
+size 57380
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png
index 635e14be9e..f47016a59f 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Day_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:8b3a91d104a5949dde83ac38b3127afa342816c631a1e29d78e2ada1bcc906bb
-size 50150
+oid sha256:895ba0487ed933f4c8eddc99fe008032fe01454771ccf2987905aec357f3d9c0
+size 49972
diff --git a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png
index 573b6123ed..adfcb6a616 100644
--- a/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png
+++ b/tests/uitests/src/test/snapshots/images/libraries.textcomposer_TextComposerEdit_Night_0_en.png
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
-oid sha256:69c281829fbca15261f196d2134f933b789ff444cd7bebc88d05cdbb7553357f
-size 48012
+oid sha256:b5a435c5aa78ffda52d96d611ab869d006e6c95e8aaf4cd005d99128b4f60b1c
+size 47736
diff --git a/tools/adb/force_stop.sh b/tools/adb/force_stop.sh
new file mode 100755
index 0000000000..aaf9274010
--- /dev/null
+++ b/tools/adb/force_stop.sh
@@ -0,0 +1,12 @@
+#!/usr/bin/env bash
+
+# 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_name="io.element.android.x.debug"
+
+echo " => Force stop ${package_name}"
+
+adb shell am force-stop "${package_name}"
diff --git a/tools/localazy/config.json b/tools/localazy/config.json
index 8a7790be1e..8bd6f14c12 100644
--- a/tools/localazy/config.json
+++ b/tools/localazy/config.json
@@ -173,6 +173,7 @@
"includeRegex" : [
"screen_onboarding_.*",
"screen\\.onboarding\\..*",
+ "screen\\.change_server\\..*",
"screen\\.missing_key_backup\\..*",
"screen_login_.*",
"screen_server_confirmation_.*",
@@ -271,6 +272,7 @@
"screen_room_retry.*",
"screen_room_timeline.*",
"screen\\.room\\.timeline.*",
+ "error\\.sending_failure\\..*",
"screen\\.room_timeline.*",
"screen_room_typing.*",
"screen\\.image_edition\\..*",
diff --git a/tools/manifest/gplay/release/aaptDump.txt b/tools/manifest/gplay/release/aaptDump.txt
index fcb3669a59..7eae1a5a78 100644
--- a/tools/manifest/gplay/release/aaptDump.txt
+++ b/tools/manifest/gplay/release/aaptDump.txt
@@ -1,4 +1,4 @@
-package: name='io.element.android.x' versionCode='202608010' versionName='26.08.1' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
+package: name='io.element.android.x' versionCode='202608020' versionName='26.08.2' platformBuildVersionName='17' platformBuildVersionCode='37' compileSdkVersion='37' compileSdkVersionCodename='17'
sdkVersion:'24'
targetSdkVersion:'37'
uses-permission: name='android.permission.POST_NOTIFICATIONS'
diff --git a/tools/release/fix-pg-map-id.py b/tools/release/fix-pg-map-id.py
deleted file mode 100644
index c37fec2528..0000000000
--- a/tools/release/fix-pg-map-id.py
+++ /dev/null
@@ -1,227 +0,0 @@
-#!/usr/bin/python3
-# encoding: utf-8
-# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman
-# SPDX-License-Identifier: GPL-3.0-or-later.
-
-import hashlib
-import os
-import re
-import struct
-import zipfile
-import zlib
-
-from binascii import hexlify
-from typing import Any, Dict, Match, Tuple
-
-DEX_MAGIC = b"dex\n"
-DEX_MAGIC_RE = re.compile(rb"dex\n(\d{3})\x00")
-
-PROF_MAGIC = b"pro\x00"
-PROF_010_P = b"010\x00"
-
-CLASSES_DEX_RE = re.compile(r"classes\d*\.dex")
-ASSET_PROF = "assets/dexopt/baseline.prof"
-
-PG_MAP_ID_RE = re.compile(rb'(~~R8{"backend":"dex".*?"pg-map-id":")([0-9a-f]{7})(")')
-
-ATTRS = ("compress_type", "create_system", "create_version", "date_time",
- "external_attr", "extract_version", "flag_bits")
-LEVELS = (9, 6, 4, 1)
-
-
-class Error(RuntimeError):
- pass
-
-
-# FIXME: is there a better alternative?
-class ReproducibleZipInfo(zipfile.ZipInfo):
- """Reproducible ZipInfo hack."""
-
- if "_compresslevel" not in zipfile.ZipInfo.__slots__: # type: ignore[attr-defined]
- if "compress_level" not in zipfile.ZipInfo.__slots__: # type: ignore[attr-defined]
- raise Error("zipfile.ZipInfo has no ._compresslevel")
-
- _compresslevel: int
- _override: Dict[str, Any] = {}
-
- def __init__(self, zinfo: zipfile.ZipInfo, **override: Any) -> None:
- # pylint: disable=W0231
- if override:
- self._override = {**self._override, **override}
- for k in self.__slots__:
- if hasattr(zinfo, k):
- setattr(self, k, getattr(zinfo, k))
-
- def __getattribute__(self, name: str) -> Any:
- if name != "_override":
- try:
- return self._override[name]
- except KeyError:
- pass
- return object.__getattribute__(self, name)
-
-
-def fix_pg_map_id(input_dir: str, output_dir: str, map_id: str) -> None:
- file_data = {}
- for filename in [ASSET_PROF] + sorted(os.listdir(input_dir)):
- if re.fullmatch(CLASSES_DEX_RE, filename) or filename == ASSET_PROF:
- print(f"reading {filename!r}...")
- with open(os.path.join(input_dir, *filename.split("/")), "rb") as fh:
- file_data[filename] = fh.read()
- _fix_pg_map_id(file_data, map_id)
- for filename, data in file_data.items():
- print(f"writing {filename!r}...")
- if "/" in filename:
- os.makedirs(os.path.join(output_dir, *filename.split("/")[:-1]), exist_ok=True)
- with open(os.path.join(output_dir, *filename.split("/")), "wb") as fh:
- fh.write(data)
-
-
-def fix_pg_map_id_apk(input_apk: str, output_apk: str, map_id: str) -> None:
- with open(input_apk, "rb") as fh_raw:
- with zipfile.ZipFile(input_apk) as zf_in:
- with zipfile.ZipFile(output_apk, "w") as zf_out:
- file_data = {}
- for info in zf_in.infolist():
- if re.fullmatch(CLASSES_DEX_RE, info.filename) or info.filename == ASSET_PROF:
- print(f"reading {info.filename!r}...")
- file_data[info.filename] = zf_in.read(info)
- _fix_pg_map_id(file_data, map_id)
- for info in zf_in.infolist():
- attrs = {attr: getattr(info, attr) for attr in ATTRS}
- zinfo = ReproducibleZipInfo(info, **attrs)
- if info.compress_type == 8:
- fh_raw.seek(info.header_offset)
- n, m = struct.unpack(" 0:
- ccrc = zlib.crc32(fh_raw.read(min(size, 4096)), ccrc)
- size -= 4096
- with zf_in.open(info) as fh_in:
- comps = {lvl: zlib.compressobj(lvl, 8, -15) for lvl in LEVELS}
- ccrcs = {lvl: 0 for lvl in LEVELS}
- while True:
- data = fh_in.read(4096)
- if not data:
- break
- for lvl in LEVELS:
- ccrcs[lvl] = zlib.crc32(comps[lvl].compress(data), ccrcs[lvl])
- for lvl in LEVELS:
- if ccrc == zlib.crc32(comps[lvl].flush(), ccrcs[lvl]):
- zinfo._compresslevel = lvl
- break
- else:
- raise Error(f"Unable to determine compresslevel for {info.filename!r}")
- elif info.compress_type != 0:
- raise Error(f"Unsupported compress_type {info.compress_type}")
- if re.fullmatch(CLASSES_DEX_RE, info.filename) or info.filename == ASSET_PROF:
- print(f"writing {info.filename!r}...")
- zf_out.writestr(zinfo, file_data[info.filename])
- else:
- with zf_in.open(info) as fh_in:
- with zf_out.open(zinfo, "w") as fh_out:
- while True:
- data = fh_in.read(4096)
- if not data:
- break
- fh_out.write(data)
-
-
-def _fix_pg_map_id(file_data: Dict[str, bytes], map_id: str) -> None:
- crcs = {}
- for filename in file_data:
- if re.fullmatch(CLASSES_DEX_RE, filename):
- print(f"fixing {filename!r}...")
- data = _fix_dex_id_checksum(file_data[filename], map_id.encode())
- file_data[filename] = data
- crcs[filename] = zlib.crc32(data)
- if ASSET_PROF in file_data:
- print(f"fixing {ASSET_PROF!r}...")
- file_data[ASSET_PROF] = _fix_prof_checksum(file_data[ASSET_PROF], crcs)
-
-
-def _fix_dex_id_checksum(data: bytes, map_id: bytes) -> bytes:
- def repl(m: Match[bytes]) -> bytes:
- print(f"fixing pg-map-id: {m.group(2)!r} -> {map_id!r}")
- return m.group(1) + map_id + m.group(3)
-
- magic = data[:8]
- if magic[:4] != DEX_MAGIC or not DEX_MAGIC_RE.fullmatch(magic):
- raise Error(f"Unsupported magic {magic!r}")
- print(f"dex version={int(magic[4:7]):03d}")
- checksum, signature = struct.unpack(" {hexlify(fixed_sig).decode()}")
- fixed_data = fixed_sig + fixed_data
- fixed_checksum = zlib.adler32(fixed_data)
- print(f"fixing checksum: 0x{checksum:x} -> 0x{fixed_checksum:x}")
- return magic + int.to_bytes(fixed_checksum, 4, "little") + fixed_data
-
-
-def _fix_prof_checksum(data: bytes, crcs: Dict[str, int]) -> bytes:
- magic, data = _split(data, 4)
- version, data = _split(data, 4)
- if magic == PROF_MAGIC:
- if version == PROF_010_P:
- print("prof version=010 P")
- return PROF_MAGIC + PROF_010_P + _fix_prof_010_p_checksum(data, crcs)
- else:
- raise Error(f"Unsupported prof version {version!r}")
- else:
- raise Error(f"Unsupported magic {magic!r}")
-
-
-def _fix_prof_010_p_checksum(data: bytes, crcs: Dict[str, int]) -> bytes:
- num_dex_files, uncompressed_data_size, compressed_data_size, data = _unpack(" 0x{fixed_checksum:x}")
- dex_data_headers.append(struct.pack(
- " Any:
- assert all(c in " Tuple[bytes, bytes]:
- return data[:size], data[size:]
-
-
-if __name__ == "__main__":
- import argparse
- parser = argparse.ArgumentParser(prog="fix-pg-map-id.py")
- parser.add_argument("input_dir_or_apk", metavar="INPUT_DIR_OR_APK")
- parser.add_argument("output_dir_or_apk", metavar="OUTPUT_DIR_OR_APK")
- parser.add_argument("pg_map_id", metavar="PG_MAP_ID")
- args = parser.parse_args()
- if os.path.isdir(args.input_dir_or_apk):
- fix_pg_map_id(args.input_dir_or_apk, args.output_dir_or_apk, args.pg_map_id)
- else:
- fix_pg_map_id_apk(args.input_dir_or_apk, args.output_dir_or_apk, args.pg_map_id)
-
-# vim: set tw=80 sw=4 sts=4 et fdm=marker :
diff --git a/tools/release/inplace-fix.py b/tools/release/inplace-fix.py
deleted file mode 100644
index a791e69140..0000000000
--- a/tools/release/inplace-fix.py
+++ /dev/null
@@ -1,199 +0,0 @@
-#!/usr/bin/python3
-# encoding: utf-8
-# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman
-# SPDX-License-Identifier: GPL-3.0-or-later.
-
-import argparse
-import os
-import shutil
-import subprocess
-import sys
-import tempfile
-
-from typing import Optional, Tuple
-
-COMMANDS = (
- "fix-compresslevel",
- "fix-files",
- "fix-newlines",
- "fix-pg-map-id",
- "rm-files",
- "sort-apk",
- "sort-baseline",
-)
-
-BUILD_TOOLS_WITH_BROKEN_ZIPALIGN = ("31.0.0", "32.0.0")
-BUILD_TOOLS_WITH_PAGE_SIZE_FROM = "35.0.0-rc1"
-SDK_ENV = ("ANDROID_HOME", "ANDROID_SDK", "ANDROID_SDK_ROOT")
-
-
-def _zipalign_cmd(page_align: bool, page_size: Optional[int]) -> Tuple[str, ...]:
- if page_align:
- if page_size is not None:
- return ("zipalign", "-P", str(page_size), "4")
- return ("zipalign", "-p", "4")
- return ("zipalign", "4")
-
-
-ZIPALIGN = _zipalign_cmd(page_align=False, page_size=None)
-ZIPALIGN_P = _zipalign_cmd(page_align=True, page_size=None)
-
-
-class Error(RuntimeError):
- pass
-
-
-def inplace_fix(command: str, input_file: str, *args: str,
- zipalign: bool = False, page_align: bool = False,
- page_size: Optional[int] = None, internal: bool = False) -> None:
- if command not in COMMANDS:
- raise Error(f"Unknown command {command}")
- exe, script = _script_cmd(command)
- ext = os.path.splitext(input_file)[1]
- with tempfile.TemporaryDirectory() as tdir:
- fixed = os.path.join(tdir, "fixed" + ext)
- run_command(exe, script, input_file, fixed, *args, trim=2)
- if zipalign:
- aligned = os.path.join(tdir, "aligned" + ext)
- zac = zipalign_cmd(page_align=page_align, page_size=page_size, internal=internal)
- run_command(*zac, fixed, aligned, trim=2)
- print(f"[MOVE] {aligned} to {input_file}")
- shutil.move(aligned, input_file)
- else:
- print(f"[MOVE] {fixed} to {input_file}")
- shutil.move(fixed, input_file)
-
-
-def zipalign_cmd(page_align: bool = False, page_size: Optional[int] = None,
- internal: bool = False) -> Tuple[str, ...]:
- """
- Find zipalign command using $PATH or $ANDROID_HOME etc.
-
- >>> zipalign_cmd()
- ('zipalign', '4')
- >>> zipalign_cmd(page_align=True)
- ('zipalign', '-p', '4')
- >>> zipalign_cmd(page_align=True, page_size=16)
- ('zipalign', '-P', '16', '4')
- >>> cmd = zipalign_cmd(page_align=True, page_size=16, internal=True)
- >>> [x.split("/")[-1] for x in cmd]
- ['python3', 'zipalign.py', '-P', '16', '4']
- >>> os.environ["PATH"] = ""
- >>> for k in SDK_ENV:
- ... os.environ[k] = ""
- >>> cmd = zipalign_cmd()
- >>> [x.split("/")[-1] for x in cmd]
- ['python3', 'zipalign.py', '4']
- >>> os.environ["ANDROID_HOME"] = "test/fake-sdk"
- >>> zipalign_cmd()
- [SKIP BROKEN] 31.0.0
- [FOUND] test/fake-sdk/build-tools/30.0.3/zipalign
- ('test/fake-sdk/build-tools/30.0.3/zipalign', '4')
- >>> cmd = zipalign_cmd(page_align=True, page_size=16)
- [SKIP TOO OLD] 31.0.0
- [SKIP TOO OLD] 30.0.3
- [SKIP TOO OLD] 26.0.2
- >>> [x.split("/")[-1] for x in cmd]
- ['python3', 'zipalign.py', '-P', '16', '4']
- >>> os.environ["ANDROID_HOME"] = "test/fake-sdk-2"
- >>> zipalign_cmd(page_align=True, page_size=16)
- [FOUND] test/fake-sdk-2/build-tools/35.0.0-rc1/zipalign
- ('test/fake-sdk-2/build-tools/35.0.0-rc1/zipalign', '-P', '16', '4')
-
- """
- cmd, *args = _zipalign_cmd(page_align, page_size)
- if not internal:
- if shutil.which(cmd):
- return (cmd, *args)
- for k in SDK_ENV:
- if home := os.environ.get(k):
- tools = os.path.join(home, "build-tools")
- if os.path.exists(tools):
- for vsn in sorted(os.listdir(tools), key=_vsn, reverse=True):
- if page_size and _vsn(vsn) < _vsn(BUILD_TOOLS_WITH_PAGE_SIZE_FROM):
- print(f"[SKIP TOO OLD] {vsn}")
- continue
- for s in BUILD_TOOLS_WITH_BROKEN_ZIPALIGN:
- if vsn.startswith(s):
- print(f"[SKIP BROKEN] {vsn}")
- break
- else:
- c = os.path.join(tools, vsn, cmd)
- if shutil.which(c):
- print(f"[FOUND] {c}")
- return (c, *args)
- return (*_script_cmd(cmd), *args)
-
-
-def _vsn(v: str) -> Tuple[int, ...]:
- """
- >>> vs = "31.0.0 32.1.0-rc1 34.0.0-rc3 34.0.0 35.0.0-rc1".split()
- >>> for v in sorted(vs, key=_vsn, reverse=True):
- ... (_vsn(v), v)
- ((35, 0, 0, 0, 1), '35.0.0-rc1')
- ((34, 0, 0, 1, 0), '34.0.0')
- ((34, 0, 0, 0, 3), '34.0.0-rc3')
- ((32, 1, 0, 0, 1), '32.1.0-rc1')
- ((31, 0, 0, 1, 0), '31.0.0')
- """
- if "-rc" in v:
- v = v.replace("-rc", ".0.", 1)
- else:
- v = v + ".1.0"
- return tuple(int(x) if x.isdigit() else -1 for x in v.split("."))
-
-
-def _script_cmd(command: str) -> Tuple[str, str]:
- script_dir = os.path.dirname(__file__)
- for cmd in (command, command.replace("-", "_")):
- script = os.path.join(script_dir, cmd + ".py")
- if os.path.exists(script):
- break
- else:
- raise Error(f"Script for {command} not found")
- exe = sys.executable or "python3"
- return exe, script
-
-
-def run_command(*args: str, trim: int = 1) -> None:
- targs = tuple(os.path.basename(a) for a in args[:trim]) + args[trim:]
- print(f"[RUN] {' '.join(targs)}")
- try:
- subprocess.run(args, check=True)
- except subprocess.CalledProcessError as e:
- raise Error(f"{args[0]} command failed") from e
- except FileNotFoundError as e:
- raise Error(f"{args[0]} command not found") from e
-
-
-def main() -> None:
- prog = os.path.basename(sys.argv[0])
- usage = (f"{prog} [-h] [--zipalign] [--page-align] [--page-size N] [--internal]\n"
- f"{len('usage: ' + prog) * ' '} COMMAND INPUT_FILE [...]")
- epilog = f"Commands: {', '.join(COMMANDS)}."
- parser = argparse.ArgumentParser(usage=usage, epilog=epilog)
- parser.add_argument("--zipalign", action="store_true",
- help="run zipalign after COMMAND")
- parser.add_argument("--page-align", action="store_true",
- help="run zipalign w/ -p option (implies --zipalign)")
- parser.add_argument("--page-size", metavar="N", type=int,
- help="run zipalign w/ -P N option (implies --page-align)")
- parser.add_argument("--internal", action="store_true",
- help="use zipalign.py instead of searching $PATH/$ANDROID_HOME/etc.")
- parser.add_argument("command", metavar="COMMAND")
- parser.add_argument("input_file", metavar="INPUT_FILE")
- args, rest = parser.parse_known_args()
- try:
- inplace_fix(args.command, args.input_file, *rest,
- zipalign=bool(args.zipalign or args.page_align or args.page_size),
- page_align=bool(args.page_align or args.page_size),
- page_size=args.page_size, internal=args.internal)
- except Error as e:
- print(f"Error: {e}.", file=sys.stderr)
- sys.exit(1)
-
-
-if __name__ == "__main__":
- main()
-
-# vim: set tw=80 sw=4 sts=4 et fdm=marker :
diff --git a/tools/release/release.sh b/tools/release/release.sh
index 83ccc6d258..392074208a 100755
--- a/tools/release/release.sh
+++ b/tools/release/release.sh
@@ -225,15 +225,6 @@ printf "Unzipping the F-Droid artifact...\n"
fdroidTargetPath="${targetPath}/fdroid"
unzip "${targetPath}"/elementx-app-fdroid-apks-unsigned.zip -d "${fdroidTargetPath}"
-printf "\n================================================================================\n"
-printf "Patching the FDroid APKs using inplace-fix.py...\n"
-
-inplaceFixScript="./tools/release/inplace-fix.py"
-python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-arm64-v8a-release.apk '0000000'
-python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-armeabi-v7a-release.apk '0000000'
-python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-x86-release.apk '0000000'
-python3 "${inplaceFixScript}" --page-size 16 fix-pg-map-id "${fdroidTargetPath}"/app-fdroid-x86_64-release.apk '0000000'
-
printf "\n================================================================================\n"
printf "Signing the FDroid APKs...\n"