Let the enterprise build be able to update the colors.

This commit is contained in:
Benoit Marty
2025-10-14 17:13:05 +02:00
committed by Benoit Marty
parent 447f6a1b11
commit 8b703ed046
21 changed files with 234 additions and 55 deletions

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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<RageshakePreferencesState>,
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
private val enterpriseService: EnterpriseService,
) : Presenter<DeveloperSettingsState> {
@Composable
override fun present(): DeveloperSettingsState {
@@ -71,6 +74,9 @@ class DeveloperSettingsPresenter(
val clearCacheAction = remember {
mutableStateOf<AsyncAction<Unit>>(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
)
}

View File

@@ -23,6 +23,8 @@ data class DeveloperSettingsState(
val customElementCallBaseUrlState: CustomElementCallBaseUrlState,
val tracingLogLevel: AsyncData<LogLevelItem>,
val tracingLogPacks: ImmutableList<TraceLogPack>,
val isEnterpriseBuild: Boolean,
val showColorPicker: Boolean,
val eventSink: (DeveloperSettingsEvents) -> Unit
)

View File

@@ -28,6 +28,10 @@ open class DeveloperSettingsStateProvider : PreviewParameterProvider<DeveloperSe
baseUrl = "https://call.element.ahoy",
)
),
aDeveloperSettingsState(
isEnterpriseBuild = true,
showColorPicker = true,
),
)
}
@@ -35,6 +39,8 @@ fun aDeveloperSettingsState(
clearCacheAction: AsyncAction<Unit> = AsyncAction.Uninitialized,
customElementCallBaseUrlState: CustomElementCallBaseUrlState = aCustomElementCallBaseUrlState(),
traceLogPacks: List<TraceLogPack> = 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,
)

View File

@@ -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 = {},

View File

@@ -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<String?, Unit> { }
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,
)
}
}