From 8b703ed04641d1f8fb6584c45d0ebf81e9569abc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 14 Oct 2025 17:13:05 +0200 Subject: [PATCH 01/16] Let the enterprise build be able to update the colors. --- app/build.gradle.kts | 1 + enterprise | 2 +- features/enterprise/api/build.gradle.kts | 2 +- .../enterprise/api/EnterpriseService.kt | 15 +++++- .../enterprise/impl-foss/build.gradle.kts | 2 +- .../impl/DefaultEnterpriseService.kt | 16 +++++- .../impl/DefaultEnterpriseServiceTest.kt | 31 +++++++++++ .../enterprise/test/FakeEnterpriseService.kt | 17 ++++-- .../timeline/components/TimelineItemRow.kt | 6 +-- features/preferences/impl/build.gradle.kts | 3 ++ .../impl/developer/DeveloperSettingsEvents.kt | 3 ++ .../developer/DeveloperSettingsPresenter.kt | 16 ++++++ .../impl/developer/DeveloperSettingsState.kt | 2 + .../DeveloperSettingsStateProvider.kt | 8 +++ .../impl/developer/DeveloperSettingsView.kt | 32 ++++++++++- .../DeveloperSettingsPresenterTest.kt | 35 ++++++++++++ gradle/libs.versions.toml | 1 + .../androidutils/assets/AssetReader.kt | 54 +++++++++++++++++++ .../designsystem/modifiers/Gradient.kt | 38 +------------ .../designsystem/theme/ElementThemeApp.kt | 4 +- .../tests/konsist/KonsistPreviewTest.kt | 1 - 21 files changed, 234 insertions(+), 55 deletions(-) create mode 100644 libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/assets/AssetReader.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1c7466f680..2019407657 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -324,6 +324,7 @@ licensee { allowUrl("https://jsoup.org/license") allowUrl("https://asm.ow2.io/license.html") allowUrl("https://www.gnu.org/licenses/agpl-3.0.txt") + allowUrl("https://github.com/mhssn95/compose-color-picker/blob/main/LICENSE") ignoreDependencies("com.github.matrix-org", "matrix-analytics-events") // Ignore dependency that are not third-party licenses to us. ignoreDependencies(groupId = "io.element.android") diff --git a/enterprise b/enterprise index ffc02b8d0f..58f37695d2 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit ffc02b8d0f35188c3ef8a876dc1532bfe3e533da +Subproject commit 58f37695d280da85306973586c43d8d63e1c571c diff --git a/features/enterprise/api/build.gradle.kts b/features/enterprise/api/build.gradle.kts index b32f42e31f..7208c71367 100644 --- a/features/enterprise/api/build.gradle.kts +++ b/features/enterprise/api/build.gradle.kts @@ -5,7 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { diff --git a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt index 03ecda80c0..5e5e45ffb9 100644 --- a/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt +++ b/features/enterprise/api/src/main/kotlin/io/element/android/features/enterprise/api/EnterpriseService.kt @@ -7,6 +7,8 @@ package io.element.android.features.enterprise.api +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.libraries.matrix.api.core.SessionId import kotlinx.coroutines.flow.Flow @@ -17,8 +19,17 @@ interface EnterpriseService { fun defaultHomeserverList(): List suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String): Boolean - fun semanticColorsLight(): SemanticColors - fun semanticColorsDark(): SemanticColors + /** + * Override the brand color. + * @param brandColor the color in hex format (#RRGGBBAA or #RRGGBB), or null to reset to default. + */ + fun overrideBrandColor(brandColor: String?) + + @Composable + fun semanticColorsLight(): State + + @Composable + fun semanticColorsDark(): State fun firebasePushGateway(): String? fun unifiedPushDefaultPushGateway(): String? diff --git a/features/enterprise/impl-foss/build.gradle.kts b/features/enterprise/impl-foss/build.gradle.kts index c5c194807f..cf3b571be9 100644 --- a/features/enterprise/impl-foss/build.gradle.kts +++ b/features/enterprise/impl-foss/build.gradle.kts @@ -8,7 +8,7 @@ import extension.testCommonDependencies * Please see LICENSE files in the repository root for full details. */ plugins { - id("io.element.android-library") + id("io.element.android-compose-library") } android { diff --git a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt index 4d52e83a8f..6251a0b4e6 100644 --- a/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt +++ b/features/enterprise/impl-foss/src/main/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseService.kt @@ -7,6 +7,10 @@ package io.element.android.features.enterprise.impl +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.remember import dev.zacsweers.metro.AppScope import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.Inject @@ -28,9 +32,17 @@ class DefaultEnterpriseService : EnterpriseService { override fun defaultHomeserverList(): List = emptyList() override suspend fun isAllowedToConnectToHomeserver(homeserverUrl: String) = true - override fun semanticColorsLight(): SemanticColors = compoundColorsLight + override fun overrideBrandColor(brandColor: String?) = Unit - override fun semanticColorsDark(): SemanticColors = compoundColorsDark + @Composable + override fun semanticColorsLight(): State { + return remember { derivedStateOf { compoundColorsLight } } + } + + @Composable + override fun semanticColorsDark(): State { + return remember { derivedStateOf { compoundColorsDark } } + } override fun firebasePushGateway(): String? = null override fun unifiedPushDefaultPushGateway(): String? = null diff --git a/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt index 9f3ebbb845..d3a4a63ad1 100644 --- a/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt +++ b/features/enterprise/impl-foss/src/test/kotlin/io/element/android/features/enterprise/impl/DefaultEnterpriseServiceTest.kt @@ -7,7 +7,12 @@ package io.element.android.features.enterprise.impl +import app.cash.molecule.RecompositionMode +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.compound.tokens.generated.compoundColorsDark +import io.element.android.compound.tokens.generated.compoundColorsLight import io.element.android.libraries.matrix.test.A_HOMESERVER_URL import io.element.android.libraries.matrix.test.A_SESSION_ID import kotlinx.coroutines.test.runTest @@ -37,4 +42,30 @@ class DefaultEnterpriseServiceTest { val defaultEnterpriseService = DefaultEnterpriseService() assertThat(defaultEnterpriseService.isEnterpriseUser(A_SESSION_ID)).isFalse() } + + @Test + fun `semanticColorsLight always emits the same value`() = runTest { + val defaultEnterpriseService = DefaultEnterpriseService() + moleculeFlow(RecompositionMode.Immediate) { + defaultEnterpriseService.semanticColorsLight().value + }.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(compoundColorsLight) + defaultEnterpriseService.overrideBrandColor("#87654321") + expectNoEvents() + } + } + + @Test + fun `semanticColorsDark always emits the same value`() = runTest { + val defaultEnterpriseService = DefaultEnterpriseService() + moleculeFlow(RecompositionMode.Immediate) { + defaultEnterpriseService.semanticColorsDark().value + }.test { + val initialState = awaitItem() + assertThat(initialState).isEqualTo(compoundColorsDark) + defaultEnterpriseService.overrideBrandColor("#87654321") + expectNoEvents() + } + } } diff --git a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt index 30ccc4c48d..f2e597c6fa 100644 --- a/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt +++ b/features/enterprise/test/src/main/kotlin/io/element/android/features/enterprise/test/FakeEnterpriseService.kt @@ -7,6 +7,8 @@ package io.element.android.features.enterprise.test +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.EnterpriseService @@ -22,8 +24,9 @@ class FakeEnterpriseService( private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() }, private val defaultHomeserverListResult: () -> List = { emptyList() }, private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, - private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() }, - private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() }, + private val semanticColorsLightResult: () -> State = { lambdaError() }, + private val semanticColorsDarkResult: () -> State = { lambdaError() }, + private val overrideBrandColorResult: (String?) -> Unit = { lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, ) : EnterpriseService { @@ -39,11 +42,17 @@ class FakeEnterpriseService( isAllowedToConnectToHomeserverResult(homeserverUrl) } - override fun semanticColorsLight(): SemanticColors { + override fun overrideBrandColor(brandColor: String?) { + overrideBrandColorResult(brandColor) + } + + @Composable + override fun semanticColorsLight(): State { return semanticColorsLightResult() } - override fun semanticColorsDark(): SemanticColors { + @Composable + override fun semanticColorsDark(): State { return semanticColorsDarkResult() } diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 119a235cf8..09332ef5c3 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -37,8 +37,8 @@ import io.element.android.features.messages.impl.timeline.model.event.TimelineIt import io.element.android.features.messages.impl.timeline.model.event.TimelineItemVoiceContent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionEvent import io.element.android.features.messages.impl.timeline.protection.TimelineProtectionState +import io.element.android.libraries.designsystem.colors.gradientSubtleColors import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenuAction -import io.element.android.libraries.designsystem.modifiers.subtleColorStops import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toPx @@ -227,12 +227,12 @@ private fun Modifier.focusedEvent( } else { ElementTheme.colors.borderAccentSubtle } - val gradientColors = subtleColorStops(isEnterpriseBuild) + val gradientColors = gradientSubtleColors() val verticalOffset = focusedEventOffset.toPx() val verticalRatio = 0.7f return drawWithCache { val brush = Brush.verticalGradient( - colorStops = gradientColors, + colors = gradientColors, endY = size.height * verticalRatio, ) onDrawBehind { diff --git a/features/preferences/impl/build.gradle.kts b/features/preferences/impl/build.gradle.kts index eb057a9d53..6858ebef51 100644 --- a/features/preferences/impl/build.gradle.kts +++ b/features/preferences/impl/build.gradle.kts @@ -72,6 +72,7 @@ dependencies { implementation(projects.features.rageshake.api) implementation(projects.features.lockscreen.api) implementation(projects.features.analytics.api) + implementation(projects.features.enterprise.api) implementation(projects.features.licenses.api) implementation(projects.features.logout.api) implementation(projects.features.deactivation.api) @@ -83,6 +84,7 @@ dependencies { implementation(projects.services.toolbox.api) implementation(libs.datetime) implementation(libs.coil.compose) + implementation(libs.color.picker) implementation(libs.androidx.browser) implementation(libs.androidx.datastore.preferences) api(projects.features.preferences.api) @@ -100,6 +102,7 @@ dependencies { testImplementation(projects.libraries.preferences.test) testImplementation(projects.libraries.push.test) testImplementation(projects.libraries.pushstore.test) + testImplementation(projects.features.enterprise.test) testImplementation(projects.features.invite.test) testImplementation(projects.features.rageshake.test) testImplementation(projects.features.logout.test) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index ced7b8d2b4..cb7a9f5de6 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -7,6 +7,7 @@ package io.element.android.features.preferences.impl.developer +import androidx.compose.ui.graphics.Color import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.matrix.api.tracing.TraceLogPack @@ -16,5 +17,7 @@ sealed interface DeveloperSettingsEvents { data class SetCustomElementCallBaseUrl(val baseUrl: String?) : DeveloperSettingsEvents data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents + data class SetShowColorPicker(val show: Boolean) : DeveloperSettingsEvents + data class ChangeBrandColor(val color: Color) : DeveloperSettingsEvents data object ClearCache : DeveloperSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index fe7a3461b8..cf2147f5bc 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -18,8 +18,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateMap import dev.zacsweers.metro.Inject +import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.preferences.impl.developer.tracing.toLogLevel import io.element.android.features.preferences.impl.developer.tracing.toLogLevelItem import io.element.android.features.preferences.impl.tasks.ClearCacheUseCase @@ -54,6 +56,7 @@ class DeveloperSettingsPresenter( private val rageshakePresenter: Presenter, private val appPreferencesStore: AppPreferencesStore, private val buildMeta: BuildMeta, + private val enterpriseService: EnterpriseService, ) : Presenter { @Composable override fun present(): DeveloperSettingsState { @@ -71,6 +74,9 @@ class DeveloperSettingsPresenter( val clearCacheAction = remember { mutableStateOf>(AsyncAction.Uninitialized) } + var showColorPicker by remember { + mutableStateOf(false) + } val customElementCallBaseUrl by remember { appPreferencesStore .getCustomElementCallBaseUrlFlow() @@ -136,6 +142,14 @@ class DeveloperSettingsPresenter( } appPreferencesStore.setTracingLogPacks(currentPacks) } + is DeveloperSettingsEvents.ChangeBrandColor -> { + showColorPicker = false + val color = event.color.value.toHexString(HexFormat.UpperCase).substring(2, 8) + enterpriseService.overrideBrandColor(color) + } + is DeveloperSettingsEvents.SetShowColorPicker -> { + showColorPicker = event.show + } } } @@ -150,6 +164,8 @@ class DeveloperSettingsPresenter( ), tracingLogLevel = tracingLogLevel, tracingLogPacks = tracingLogPacks, + isEnterpriseBuild = enterpriseService.isEnterpriseBuild, + showColorPicker = showColorPicker, eventSink = ::handleEvents ) } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt index 93e7b9ae7b..389297a46b 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsState.kt @@ -23,6 +23,8 @@ data class DeveloperSettingsState( val customElementCallBaseUrlState: CustomElementCallBaseUrlState, val tracingLogLevel: AsyncData, val tracingLogPacks: ImmutableList, + val isEnterpriseBuild: Boolean, + val showColorPicker: Boolean, val eventSink: (DeveloperSettingsEvents) -> Unit ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt index 6ccd857552..e6803d4149 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsStateProvider.kt @@ -28,6 +28,10 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider = AsyncAction.Uninitialized, customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(), traceLogPacks: List = emptyList(), + isEnterpriseBuild: Boolean = false, + showColorPicker: Boolean = false, eventSink: (DeveloperSettingsEvents) -> Unit = {}, ) = DeveloperSettingsState( features = aFeatureUiModelList(), @@ -44,6 +50,8 @@ fun aDeveloperSettingsState( customElementCallBaseUrlState = customElementCallBaseUrlState, tracingLogLevel = AsyncData.Success(LogLevelItem.INFO), tracingLogPacks = traceLogPacks.toImmutableList(), + isEnterpriseBuild = isEnterpriseBuild, + showColorPicker = showColorPicker, eventSink = eventSink, ) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 97f08f7a16..bfea30d2b4 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.progressSemantics import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType @@ -36,8 +37,11 @@ import io.element.android.libraries.featureflag.ui.FeatureListView import io.element.android.libraries.featureflag.ui.model.FeatureUiModel import io.element.android.libraries.matrix.api.tracing.TraceLogPack import io.element.android.libraries.ui.strings.CommonStrings +import io.mhssn.colorpicker.ColorPickerDialog +import io.mhssn.colorpicker.ColorPickerType import kotlinx.collections.immutable.toImmutableList +@OptIn(ExperimentalComposeUiApi::class) @Composable fun DeveloperSettingsView( state: DeveloperSettingsState, @@ -99,6 +103,18 @@ fun DeveloperSettingsView( RageshakePreferencesView( state = state.rageshakeState, ) + if (state.isEnterpriseBuild) { + PreferenceCategory(title = "Theme", showTopDivider = false) { + ListItem( + headlineContent = { + Text("Change brand color") + }, + onClick = { + state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true)) + } + ) + } + } PreferenceCategory(title = "Crash", showTopDivider = false) { ListItem( headlineContent = { @@ -133,6 +149,18 @@ fun DeveloperSettingsView( ) } } + ColorPickerDialog( + show = state.showColorPicker, + type = ColorPickerType.Classic( + showAlphaBar = false, + ), + onDismissRequest = { + state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(false)) + }, + onPickedColor = { + state.eventSink(DeveloperSettingsEvents.ChangeBrandColor(it)) + }, + ) } @Composable @@ -189,7 +217,9 @@ private fun FeatureListContent( @PreviewsDayNight @Composable -internal fun DeveloperSettingsViewPreview(@PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState) = ElementPreview { +internal fun DeveloperSettingsViewPreview( + @PreviewParameter(DeveloperSettingsStateProvider::class) state: DeveloperSettingsState +) = ElementPreview { DeveloperSettingsView( state = state, onOpenShowkase = {}, diff --git a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt index 76b22871f0..6761a37fc6 100644 --- a/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt +++ b/features/preferences/impl/src/test/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenterTest.kt @@ -9,7 +9,10 @@ package io.element.android.features.preferences.impl.developer +import androidx.compose.ui.graphics.Color import com.google.common.truth.Truth.assertThat +import io.element.android.features.enterprise.api.EnterpriseService +import io.element.android.features.enterprise.test.FakeEnterpriseService import io.element.android.features.preferences.impl.developer.tracing.LogLevelItem import io.element.android.features.preferences.impl.tasks.FakeClearCacheUseCase import io.element.android.features.preferences.impl.tasks.FakeComputeCacheSizeUseCase @@ -24,6 +27,8 @@ import io.element.android.libraries.featureflag.test.FakeFeatureFlagService import io.element.android.libraries.matrix.test.core.aBuildMeta import io.element.android.libraries.preferences.test.InMemoryAppPreferencesStore import io.element.android.tests.testutils.WarmUpRule +import io.element.android.tests.testutils.lambda.lambdaRecorder +import io.element.android.tests.testutils.lambda.value import io.element.android.tests.testutils.test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -57,6 +62,8 @@ class DeveloperSettingsPresenterTest { assertThat(state.rageshakeState.isSupported).isTrue() assertThat(state.rageshakeState.sensitivity).isEqualTo(0.3f) assertThat(state.tracingLogLevel).isEqualTo(AsyncData.Uninitialized) + assertThat(state.isEnterpriseBuild).isFalse() + assertThat(state.showColorPicker).isFalse() } awaitItem().also { state -> assertThat(state.features).isNotEmpty() @@ -170,6 +177,32 @@ class DeveloperSettingsPresenterTest { } } + @Test + fun `present - enterprise build can change the brand color`() = runTest { + val overrideBrandColorResult = lambdaRecorder { } + val presenter = createDeveloperSettingsPresenter( + enterpriseService = FakeEnterpriseService( + isEnterpriseBuild = true, + overrideBrandColorResult = overrideBrandColorResult, + ) + ) + presenter.test { + skipItems(1) + val initialState = awaitItem() + assertThat(initialState.isEnterpriseBuild).isTrue() + initialState.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true)) + assertThat(awaitItem().showColorPicker).isTrue() + initialState.eventSink(DeveloperSettingsEvents.SetShowColorPicker(false)) + assertThat(awaitItem().showColorPicker).isFalse() + initialState.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true)) + assertThat(awaitItem().showColorPicker).isTrue() + initialState.eventSink(DeveloperSettingsEvents.ChangeBrandColor(Color.Green)) + assertThat(awaitItem().showColorPicker).isFalse() + overrideBrandColorResult.assertions().isCalledOnce() + .with(value("00FF00")) + } + } + @Test fun `present - won't display features in labs or finished`() = runTest { val availableFeatures = listOf( @@ -219,6 +252,7 @@ class DeveloperSettingsPresenterTest { clearCacheUseCase: FakeClearCacheUseCase = FakeClearCacheUseCase(), preferencesStore: InMemoryAppPreferencesStore = InMemoryAppPreferencesStore(), buildMeta: BuildMeta = aBuildMeta(), + enterpriseService: EnterpriseService = FakeEnterpriseService(), ): DeveloperSettingsPresenter { return DeveloperSettingsPresenter( featureFlagService = featureFlagService, @@ -227,6 +261,7 @@ class DeveloperSettingsPresenterTest { rageshakePresenter = { aRageshakePreferencesState() }, appPreferencesStore = preferencesStore, buildMeta = buildMeta, + enterpriseService = enterpriseService, ) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e8ba007008..e4e713dca9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -203,6 +203,7 @@ opusencoder = "io.element.android:opusencoder:1.2.0" zxing_cpp = "io.github.zxing-cpp:android:2.3.0" haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" } haze_materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" } +color_picker = "io.mhssn:colorpicker:1.0.0" # Analytics posthog = "com.posthog:posthog-android:3.23.0" diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/assets/AssetReader.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/assets/AssetReader.kt new file mode 100644 index 0000000000..5de329a754 --- /dev/null +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/assets/AssetReader.kt @@ -0,0 +1,54 @@ +/* + * 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.androidutils.assets + +import android.content.Context +import dev.zacsweers.metro.Inject +import io.element.android.libraries.di.annotations.ApplicationContext +import timber.log.Timber +import java.util.concurrent.ConcurrentHashMap + +/** + * Read asset files. + */ +@Inject +class AssetReader( + @ApplicationContext private val context: Context, +) { + private val cache = ConcurrentHashMap() + + /** + * Read an asset from resource and return a String or null in case of error. + * + * @param assetFilename Asset filename + * @return the content of the asset file, or null in case of error + */ + fun readAssetFile(assetFilename: String): String? { + return cache.getOrPut(assetFilename, { + return try { + context.assets.open(assetFilename) + .use { asset -> + buildString { + var ch = asset.read() + while (ch != -1) { + append(ch.toChar()) + ch = asset.read() + } + } + } + } catch (e: Exception) { + Timber.e(e, "## readAssetFile() failed") + null + } + }) + } + + fun clearCache() { + cache.clear() + } +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Gradient.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Gradient.kt index 5d6f91e4b4..462588403c 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Gradient.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/modifiers/Gradient.kt @@ -15,13 +15,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.colors.gradientSubtleColors import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.LocalBuildMeta /** * Ref: https://www.figma.com/design/kcnHxunG1LDWXsJhaNuiHz/ER-145--Workspaces-V1?node-id=1141-24692 @@ -30,35 +27,15 @@ import io.element.android.libraries.designsystem.theme.LocalBuildMeta @Composable fun Modifier.backgroundVerticalGradient( isVisible: Boolean = true, - isEnterpriseBuild: Boolean = LocalBuildMeta.current.isEnterpriseBuild, ): Modifier { if (!isVisible) return this return background( brush = Brush.verticalGradient( - colorStops = subtleColorStops(isEnterpriseBuild), + colors = gradientSubtleColors(), ), ) } -@Composable -fun subtleColorStops( - isEnterpriseBuild: Boolean = LocalBuildMeta.current.isEnterpriseBuild, -): Array> { - return buildList { - if (isEnterpriseBuild) { - // For enterprise builds, ensure that we are theming the gradient - add(0f to ElementTheme.colors.textActionAccent.copy(alpha = 0.5f)) - add(0.75f to ElementTheme.colors.bgCanvasDefault) - add(1f to Color.Transparent) - } else { - val colors = gradientSubtleColors() - colors.forEachIndexed { index, color -> - add(index.toFloat() / (colors.size - 1) to color) - } - } - }.toTypedArray() -} - @PreviewsDayNight @Composable internal fun BackgroundVerticalGradientPreview() = ElementPreview { @@ -70,19 +47,6 @@ internal fun BackgroundVerticalGradientPreview() = ElementPreview { ) } -@PreviewsDayNight -@Composable -internal fun BackgroundVerticalGradientEnterprisePreview() = ElementPreview { - Box( - modifier = Modifier - .fillMaxWidth() - .height(height = 100.dp) - .backgroundVerticalGradient( - isEnterpriseBuild = true, - ) - ) -} - @PreviewsDayNight @Composable internal fun BackgroundVerticalGradientDisabledPreview() = ElementPreview { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt index bb0fcd971b..78d02c7f17 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt @@ -70,8 +70,8 @@ fun ElementThemeApp( } ) } - val compoundLight = remember { enterpriseService.semanticColorsLight() } - val compoundDark = remember { enterpriseService.semanticColorsDark() } + val compoundLight by enterpriseService.semanticColorsLight() + val compoundDark by enterpriseService.semanticColorsDark() CompositionLocalProvider( LocalBuildMeta provides buildMeta, ) { diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index 082a106be2..bf41ce7933 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -79,7 +79,6 @@ class KonsistPreviewTest { "AsyncIndicatorFailurePreview", "AsyncIndicatorLoadingPreview", "BackgroundVerticalGradientDisabledPreview", - "BackgroundVerticalGradientEnterprisePreview", "BackgroundVerticalGradientPreview", "ColorAliasesPreview", "DefaultRoomListTopBarMultiAccountPreview", From 175a9d2b26469711fc98107f4f4c792b8393f17b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Oct 2025 12:08:58 +0200 Subject: [PATCH 02/16] Fix tests. --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 58f37695d2..aec257fe69 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 58f37695d280da85306973586c43d8d63e1c571c +Subproject commit aec257fe697d15493bc6b7112b75ddf4b634d3b6 From 370300f81055a20ea55d63e867bd99fcbfb7f59e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Oct 2025 14:10:42 +0200 Subject: [PATCH 03/16] Remove custom color for enterprise build. --- .../timeline/components/TimelineItemRow.kt | 23 +------------------ .../tests/konsist/KonsistPreviewTest.kt | 1 - ...onents_FocusedEventEnterprise_Day_0_en.png | 3 --- ...ents_FocusedEventEnterprise_Night_0_en.png | 3 --- 4 files changed, 1 insertion(+), 29 deletions(-) delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEventEnterprise_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEventEnterprise_Night_0_en.png diff --git a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt index 09332ef5c3..917ea2eba0 100644 --- a/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt +++ b/features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/timeline/components/TimelineItemRow.kt @@ -42,7 +42,6 @@ import io.element.android.libraries.designsystem.modifiers.onKeyboardContextMenu import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight import io.element.android.libraries.designsystem.text.toPx -import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.timeline.Timeline import io.element.android.libraries.matrix.api.user.MatrixUser @@ -220,13 +219,8 @@ internal fun TimelineItemRow( @Composable private fun Modifier.focusedEvent( focusedEventOffset: Dp, - isEnterpriseBuild: Boolean = LocalBuildMeta.current.isEnterpriseBuild, ): Modifier { - val highlightedLineColor = if (isEnterpriseBuild) { - ElementTheme.colors.textActionAccent - } else { - ElementTheme.colors.borderAccentSubtle - } + val highlightedLineColor = ElementTheme.colors.borderAccentSubtle val gradientColors = gradientSubtleColors() val verticalOffset = focusedEventOffset.toPx() val verticalRatio = 0.7f @@ -261,18 +255,3 @@ internal fun FocusedEventPreview() = ElementPreview { .focusedEvent(0.dp), ) } - -@PreviewsDayNight -@Composable -internal fun FocusedEventEnterprisePreview() = ElementPreview { - Box( - modifier = Modifier - .padding(16.dp) - .fillMaxWidth() - .height(160.dp) - .focusedEvent( - focusedEventOffset = 0.dp, - isEnterpriseBuild = true, - ), - ) -} diff --git a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt index bf41ce7933..941fde0124 100644 --- a/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt +++ b/tests/konsist/src/test/kotlin/io/element/android/tests/konsist/KonsistPreviewTest.kt @@ -83,7 +83,6 @@ class KonsistPreviewTest { "ColorAliasesPreview", "DefaultRoomListTopBarMultiAccountPreview", "DefaultRoomListTopBarWithIndicatorPreview", - "FocusedEventEnterprisePreview", "FocusedEventPreview", "GradientFloatingActionButtonCircleShapePreview", "HeaderFooterPageScrollablePreview", diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEventEnterprise_Day_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEventEnterprise_Day_0_en.png deleted file mode 100644 index 581c5d919f..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEventEnterprise_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:6349c9b5e5697b05e8c12896cf5e230edfe09330323c4b1658dfd84d3ebdaa38 -size 9664 diff --git a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEventEnterprise_Night_0_en.png b/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEventEnterprise_Night_0_en.png deleted file mode 100644 index f4cf788891..0000000000 --- a/tests/uitests/src/test/snapshots/images/features.messages.impl.timeline.components_FocusedEventEnterprise_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:34bfc1aa893118f9e5f3da63896fcf85f946f0072c460d7f8a4f1d5722b6ec17 -size 9075 From 452489fbf132f71b15b201a65a01c61b50bb1836 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Oct 2025 14:17:00 +0200 Subject: [PATCH 04/16] GradientFloatingActionButton: use gradientActionColors(). --- .../button/GradientFloatingActionButton.kt | 25 +++---------------- 1 file changed, 4 insertions(+), 21 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/GradientFloatingActionButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/GradientFloatingActionButton.kt index d58e181353..bb6f55b8ef 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/GradientFloatingActionButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/GradientFloatingActionButton.kt @@ -37,12 +37,10 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.dp import io.element.android.compound.annotations.CoreColorToken -import io.element.android.compound.theme.ElementTheme import io.element.android.compound.tokens.generated.CompoundIcons -import io.element.android.compound.tokens.generated.internal.LightColorTokens +import io.element.android.libraries.designsystem.colors.gradientActionColors import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.designsystem.theme.components.Icon @OptIn(CoreColorToken::class) @@ -53,26 +51,14 @@ fun GradientFloatingActionButton( shape: Shape = RoundedCornerShape(25), content: @Composable () -> Unit, ) { - val color1 = if (LocalBuildMeta.current.isEnterpriseBuild) { - ElementTheme.colors.textActionAccent - } else { - LightColorTokens.colorGreen700 - } - val color2 = if (LocalBuildMeta.current.isEnterpriseBuild) { - ElementTheme.colors.textActionAccent - } else { - LightColorTokens.colorBlue900 - } + val colors = gradientActionColors() val linearShaderBrush = remember { object : ShaderBrush() { override fun createShader(size: Size): Shader { return LinearGradientShader( from = Offset(size.width, size.height), to = Offset(size.width, 0f), - colors = listOf( - color2, - color1, - ), + colors = colors, ) } } @@ -83,10 +69,7 @@ fun GradientFloatingActionButton( return RadialGradientShader( center = size.center, radius = size.width / 2, - colors = listOf( - color1, - color2, - ) + colors = colors, ) } } From b1a97e2f23e5262e676e2e0c155404a640d05516 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Oct 2025 14:18:52 +0200 Subject: [PATCH 05/16] SuperButton: always use gradientActionColors(). --- .../designsystem/components/button/SuperButton.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt index 90dd64d59d..d53bf61688 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/button/SuperButton.kt @@ -40,7 +40,6 @@ import io.element.android.compound.theme.ElementTheme import io.element.android.libraries.designsystem.colors.gradientActionColors import io.element.android.libraries.designsystem.preview.ElementPreview import io.element.android.libraries.designsystem.preview.PreviewsDayNight -import io.element.android.libraries.designsystem.theme.LocalBuildMeta import io.element.android.libraries.designsystem.theme.components.ButtonSize import io.element.android.libraries.designsystem.theme.components.HorizontalDivider import io.element.android.libraries.designsystem.theme.components.lowHorizontalPaddingValue @@ -63,15 +62,7 @@ fun SuperButton( ButtonSize.Small -> PaddingValues(horizontal = 16.dp, vertical = 5.dp) } } - val colors = if (LocalBuildMeta.current.isEnterpriseBuild) { - listOf( - ElementTheme.colors.textActionAccent, - ElementTheme.colors.textActionAccent, - ) - } else { - gradientActionColors() - } - + val colors = gradientActionColors() val shaderBrush = remember(colors) { object : ShaderBrush() { override fun createShader(size: Size): Shader { From 8c218634d661f0fd432463680ab3eae582a25cf8 Mon Sep 17 00:00:00 2001 From: ElementBot Date: Wed, 15 Oct 2025 12:37:27 +0000 Subject: [PATCH 06/16] Update screenshots --- ...ferences.impl.developer_DeveloperSettingsView_Day_3_en.png | 3 +++ ...rences.impl.developer_DeveloperSettingsView_Night_3_en.png | 3 +++ ...utton_GradientFloatingActionButtonCircleShape_Day_0_en.png | 4 ++-- ...ton_GradientFloatingActionButtonCircleShape_Night_0_en.png | 4 ++-- ...omponents.button_GradientFloatingActionButton_Day_0_en.png | 4 ++-- ...ponents.button_GradientFloatingActionButton_Night_0_en.png | 4 ++-- ...odifiers_BackgroundVerticalGradientEnterprise_Day_0_en.png | 3 --- ...ifiers_BackgroundVerticalGradientEnterprise_Night_0_en.png | 3 --- 8 files changed, 14 insertions(+), 14 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png create mode 100644 tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Day_0_en.png delete mode 100644 tests/uitests/src/test/snapshots/images/libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Night_0_en.png diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png new file mode 100644 index 0000000000..9f6f9bc434 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Day_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77847b759a0795c7c4f6ed529c0d20ffa060983c06196061f3ff61171774d1ab +size 39412 diff --git a/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png new file mode 100644 index 0000000000..2d9b113aa1 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/features.preferences.impl.developer_DeveloperSettingsView_Night_3_en.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dea0cb97b7f8363fb4581da232c2ec6cd5c7ad17a9dcbf5339b275f590e53a8e +size 38794 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en.png index 7bb9ad0c60..ca8b99159f 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d78695ba9a688d64996fb13ffa27f2870b613141c3bc4ca9ae565fc97de00e1e -size 10522 +oid sha256:45e0eed913d8987940b6745620e229c8ce1659f2603b3bc4a1d19d6753742f6d +size 12394 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en.png index ac4762f9c9..39a307604c 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButtonCircleShape_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:35b75a3b70d0b8b5d1279b99ba715150ecf8a8d64f8961c69394a7f7d6292d18 -size 10485 +oid sha256:98302375a306d4a396888cdbaecf358e246f25dcc5e300a2548b84d6fb9c4974 +size 11745 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en.png index fb1b6f0d0c..a9ee021ab5 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Day_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:454d022bb76f1469ee071bac71d0b2a9061ed2f97390abe591ba17dfb194b98f -size 10576 +oid sha256:52ddea2d8936364fe4b8fcf9c140830cd4354db214cc43f737eaab6070b84c6d +size 12713 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en.png index 8df3db1483..0857be997a 100644 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en.png +++ b/tests/uitests/src/test/snapshots/images/libraries.designsystem.components.button_GradientFloatingActionButton_Night_0_en.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3c39f527f535195124f0720f598df65665559e91fd6f2dbf35e17f9e71c25f6d -size 10533 +oid sha256:dc4d9958f19470f82c43cc7cc2eb6ae4c0b2ec954a66bd654c76759780c0ae22 +size 11873 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Day_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Day_0_en.png deleted file mode 100644 index f3a0c39c8e..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Day_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f6f581db727c01b084bc60ddb19cfe34e1f097cb91e6ac675c7721021f1d48c4 -size 8446 diff --git a/tests/uitests/src/test/snapshots/images/libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Night_0_en.png b/tests/uitests/src/test/snapshots/images/libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Night_0_en.png deleted file mode 100644 index b9746b8511..0000000000 --- a/tests/uitests/src/test/snapshots/images/libraries.designsystem.modifiers_BackgroundVerticalGradientEnterprise_Night_0_en.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ad28a3e8f78ccd0263eff3c3d42f506ea4ae54eea88fe81849eac71fbaf0dd05 -size 8458 From e1ccfb8c6f27b953a52bf3de40ba176c634f6318 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Oct 2025 15:31:53 +0200 Subject: [PATCH 07/16] Add tests --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index aec257fe69..3b9622f942 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit aec257fe697d15493bc6b7112b75ddf4b634d3b6 +Subproject commit 3b9622f94241c925bb8ad17693fe5855d8bd3259 From 4367669404078188eb785ea8c045e62d947faa9c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Oct 2025 17:36:03 +0200 Subject: [PATCH 08/16] Update ref to submodule. --- enterprise | 2 +- gradle/libs.versions.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 3b9622f942..42efee0022 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 3b9622f94241c925bb8ad17693fe5855d8bd3259 +Subproject commit 42efee00222c3d0789feaa332739706af1b3c093 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e4e713dca9..21b88ad6b5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -93,6 +93,7 @@ androidx_constraintlayout_compose = { module = "androidx.constraintlayout:constr androidx_camera_lifecycle = { module = "androidx.camera:camera-lifecycle", version.ref = "camera" } androidx_camera_view = { module = "androidx.camera:camera-view", version.ref = "camera" } androidx_camera_camera2 = { module = "androidx.camera:camera-camera2", version.ref = "camera" } +androidx_javascriptengine = "androidx.javascriptengine:javascriptengine:1.0.0" androidx_recyclerview = "androidx.recyclerview:recyclerview:1.4.0" androidx_browser = "androidx.browser:browser:1.9.0" @@ -126,6 +127,7 @@ androidx_compose_material_icons = { module = "androidx.compose.material:material # Coroutines coroutines_core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +coroutines_guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "coroutines" } coroutines_test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } # Accompanist From b3b92bc959b5dfe0b3bc85fb01b4d1614d81fce9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 15 Oct 2025 18:26:48 +0200 Subject: [PATCH 09/16] Cleanup --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 42efee0022..796be46999 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 42efee00222c3d0789feaa332739706af1b3c093 +Subproject commit 796be4699913b3a5e67ca1241f620b0df5b1cec5 From cf8c8652d1132ce8c804b3509bbca812d90c5111 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Oct 2025 09:58:11 +0200 Subject: [PATCH 10/16] Update ref. --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 796be46999..dc42b35239 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 796be4699913b3a5e67ca1241f620b0df5b1cec5 +Subproject commit dc42b35239830bac7d7f43b10999af5c50d82019 From 5b00fcc1297a9ffb7f7028b97cd505c419afc7ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Oct 2025 12:19:48 +0200 Subject: [PATCH 11/16] Always show dividers. --- .../preferences/impl/developer/DeveloperSettingsView.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index bfea30d2b4..4a9b47c0c9 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -58,7 +58,6 @@ fun DeveloperSettingsView( // Note: this is OK to hardcode strings in this debug screen. PreferenceCategory( title = "Feature flags", - showTopDivider = true, ) { FeatureListContent(state) } @@ -104,7 +103,7 @@ fun DeveloperSettingsView( state = state.rageshakeState, ) if (state.isEnterpriseBuild) { - PreferenceCategory(title = "Theme", showTopDivider = false) { + PreferenceCategory(title = "Theme") { ListItem( headlineContent = { Text("Change brand color") @@ -115,7 +114,7 @@ fun DeveloperSettingsView( ) } } - PreferenceCategory(title = "Crash", showTopDivider = false) { + PreferenceCategory(title = "Crash") { ListItem( headlineContent = { Text("Crash the app 💥") @@ -124,7 +123,7 @@ fun DeveloperSettingsView( ) } val cache = state.cacheSize - PreferenceCategory(title = "Cache", showTopDivider = false) { + PreferenceCategory(title = "Cache") { ListItem( headlineContent = { Text("Clear cache") @@ -167,7 +166,7 @@ fun DeveloperSettingsView( private fun ElementCallCategory( state: DeveloperSettingsState, ) { - PreferenceCategory(title = "Element Call", showTopDivider = true) { + PreferenceCategory(title = "Element Call") { val callUrlState = state.customElementCallBaseUrlState val supportingText = if (callUrlState.baseUrl.isNullOrEmpty()) { From 7d6971a9e57326e18bf681ba2e67517ed600b3cc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Oct 2025 15:19:19 +0200 Subject: [PATCH 12/16] Add a way to reset the colors. --- enterprise | 2 +- .../preferences/impl/developer/DeveloperSettingsEvents.kt | 2 +- .../impl/developer/DeveloperSettingsPresenter.kt | 2 +- .../preferences/impl/developer/DeveloperSettingsView.kt | 8 ++++++++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/enterprise b/enterprise index dc42b35239..5bb4720132 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit dc42b35239830bac7d7f43b10999af5c50d82019 +Subproject commit 5bb47201328b08d1c7d79a20f62268e8a4a51ba9 diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt index cb7a9f5de6..953ed61128 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsEvents.kt @@ -18,6 +18,6 @@ sealed interface DeveloperSettingsEvents { data class SetTracingLogLevel(val logLevel: LogLevelItem) : DeveloperSettingsEvents data class ToggleTracingLogPack(val logPack: TraceLogPack, val enabled: Boolean) : DeveloperSettingsEvents data class SetShowColorPicker(val show: Boolean) : DeveloperSettingsEvents - data class ChangeBrandColor(val color: Color) : DeveloperSettingsEvents + data class ChangeBrandColor(val color: Color?) : DeveloperSettingsEvents data object ClearCache : DeveloperSettingsEvents } diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt index cf2147f5bc..7106b53503 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsPresenter.kt @@ -144,7 +144,7 @@ class DeveloperSettingsPresenter( } is DeveloperSettingsEvents.ChangeBrandColor -> { showColorPicker = false - val color = event.color.value.toHexString(HexFormat.UpperCase).substring(2, 8) + val color = event.color?.value?.toHexString(HexFormat.UpperCase)?.substring(2, 8) enterpriseService.overrideBrandColor(color) } is DeveloperSettingsEvents.SetShowColorPicker -> { diff --git a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt index 4a9b47c0c9..e00716e2ce 100644 --- a/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt +++ b/features/preferences/impl/src/main/kotlin/io/element/android/features/preferences/impl/developer/DeveloperSettingsView.kt @@ -112,6 +112,14 @@ fun DeveloperSettingsView( state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true)) } ) + ListItem( + headlineContent = { + Text("Reset brand color") + }, + onClick = { + state.eventSink(DeveloperSettingsEvents.ChangeBrandColor(null)) + } + ) } } PreferenceCategory(title = "Crash") { From 535211631db02abb55f66afa1093583463823efa Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Oct 2025 16:46:40 +0200 Subject: [PATCH 13/16] Improve AssetReader. --- .../libraries/androidutils/assets/AssetReader.kt | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/assets/AssetReader.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/assets/AssetReader.kt index 5de329a754..c0499c97b4 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/assets/AssetReader.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/assets/AssetReader.kt @@ -31,16 +31,7 @@ class AssetReader( fun readAssetFile(assetFilename: String): String? { return cache.getOrPut(assetFilename, { return try { - context.assets.open(assetFilename) - .use { asset -> - buildString { - var ch = asset.read() - while (ch != -1) { - append(ch.toChar()) - ch = asset.read() - } - } - } + context.assets.open(assetFilename).use { it.bufferedReader().readText() } } catch (e: Exception) { Timber.e(e, "## readAssetFile() failed") null From 14cebee7b3ad78518322cfbe56475ff9d75ae856 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Oct 2025 20:58:39 +0200 Subject: [PATCH 14/16] Update Ref. --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 5bb4720132..9f3ff0cd6e 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 5bb47201328b08d1c7d79a20f62268e8a4a51ba9 +Subproject commit 9f3ff0cd6e2e69ce3b684ba40b667a29aa6a943c From ed50f2c9f943406bd029681b0425f5eb1a1d1204 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Oct 2025 21:16:10 +0200 Subject: [PATCH 15/16] Update Ref. --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index 9f3ff0cd6e..ef920ba9ad 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit 9f3ff0cd6e2e69ce3b684ba40b667a29aa6a943c +Subproject commit ef920ba9ade662c80f36dca59ab6e40276583bf9 From aefef9095b7f13bb1d525a756ae191a5b1c00373 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 16 Oct 2025 21:40:14 +0200 Subject: [PATCH 16/16] Update Ref. --- enterprise | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enterprise b/enterprise index ef920ba9ad..38992f58ef 160000 --- a/enterprise +++ b/enterprise @@ -1 +1 @@ -Subproject commit ef920ba9ade662c80f36dca59ab6e40276583bf9 +Subproject commit 38992f58ef472520a2192696c1bbf20e3066191e