Merge pull request #5542 from element-hq/feature/bma/assetReader
Improve colors customization
This commit is contained in:
@@ -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")
|
||||
|
||||
Submodule enterprise updated: ffc02b8d0f...38992f58ef
@@ -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 {
|
||||
|
||||
@@ -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<String>
|
||||
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<SemanticColors>
|
||||
|
||||
@Composable
|
||||
fun semanticColorsDark(): State<SemanticColors>
|
||||
|
||||
fun firebasePushGateway(): String?
|
||||
fun unifiedPushDefaultPushGateway(): String?
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<String> = 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<SemanticColors> {
|
||||
return remember { derivedStateOf { compoundColorsLight } }
|
||||
}
|
||||
|
||||
@Composable
|
||||
override fun semanticColorsDark(): State<SemanticColors> {
|
||||
return remember { derivedStateOf { compoundColorsDark } }
|
||||
}
|
||||
|
||||
override fun firebasePushGateway(): String? = null
|
||||
override fun unifiedPushDefaultPushGateway(): String? = null
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String> = { emptyList() },
|
||||
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
|
||||
private val semanticColorsLightResult: () -> SemanticColors = { lambdaError() },
|
||||
private val semanticColorsDarkResult: () -> SemanticColors = { lambdaError() },
|
||||
private val semanticColorsLightResult: () -> State<SemanticColors> = { lambdaError() },
|
||||
private val semanticColorsDarkResult: () -> State<SemanticColors> = { 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<SemanticColors> {
|
||||
return semanticColorsLightResult()
|
||||
}
|
||||
|
||||
override fun semanticColorsDark(): SemanticColors {
|
||||
@Composable
|
||||
override fun semanticColorsDark(): State<SemanticColors> {
|
||||
return semanticColorsDarkResult()
|
||||
}
|
||||
|
||||
|
||||
@@ -37,12 +37,11 @@ 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
|
||||
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,19 +219,14 @@ 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 gradientColors = subtleColorStops(isEnterpriseBuild)
|
||||
val highlightedLineColor = ElementTheme.colors.borderAccentSubtle
|
||||
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 {
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -54,7 +58,6 @@ fun DeveloperSettingsView(
|
||||
// Note: this is OK to hardcode strings in this debug screen.
|
||||
PreferenceCategory(
|
||||
title = "Feature flags",
|
||||
showTopDivider = true,
|
||||
) {
|
||||
FeatureListContent(state)
|
||||
}
|
||||
@@ -99,7 +102,27 @@ fun DeveloperSettingsView(
|
||||
RageshakePreferencesView(
|
||||
state = state.rageshakeState,
|
||||
)
|
||||
PreferenceCategory(title = "Crash", showTopDivider = false) {
|
||||
if (state.isEnterpriseBuild) {
|
||||
PreferenceCategory(title = "Theme") {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text("Change brand color")
|
||||
},
|
||||
onClick = {
|
||||
state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(true))
|
||||
}
|
||||
)
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text("Reset brand color")
|
||||
},
|
||||
onClick = {
|
||||
state.eventSink(DeveloperSettingsEvents.ChangeBrandColor(null))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
PreferenceCategory(title = "Crash") {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text("Crash the app 💥")
|
||||
@@ -108,7 +131,7 @@ fun DeveloperSettingsView(
|
||||
)
|
||||
}
|
||||
val cache = state.cacheSize
|
||||
PreferenceCategory(title = "Cache", showTopDivider = false) {
|
||||
PreferenceCategory(title = "Cache") {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
Text("Clear cache")
|
||||
@@ -133,13 +156,25 @@ fun DeveloperSettingsView(
|
||||
)
|
||||
}
|
||||
}
|
||||
ColorPickerDialog(
|
||||
show = state.showColorPicker,
|
||||
type = ColorPickerType.Classic(
|
||||
showAlphaBar = false,
|
||||
),
|
||||
onDismissRequest = {
|
||||
state.eventSink(DeveloperSettingsEvents.SetShowColorPicker(false))
|
||||
},
|
||||
onPickedColor = {
|
||||
state.eventSink(DeveloperSettingsEvents.ChangeBrandColor(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
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()) {
|
||||
@@ -189,7 +224,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 = {},
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -203,6 +205,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"
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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<String, String?>()
|
||||
|
||||
/**
|
||||
* 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 { it.bufferedReader().readText() }
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "## readAssetFile() failed")
|
||||
null
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun clearCache() {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Pair<Float, Color>> {
|
||||
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 {
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -79,12 +79,10 @@ class KonsistPreviewTest {
|
||||
"AsyncIndicatorFailurePreview",
|
||||
"AsyncIndicatorLoadingPreview",
|
||||
"BackgroundVerticalGradientDisabledPreview",
|
||||
"BackgroundVerticalGradientEnterprisePreview",
|
||||
"BackgroundVerticalGradientPreview",
|
||||
"ColorAliasesPreview",
|
||||
"DefaultRoomListTopBarMultiAccountPreview",
|
||||
"DefaultRoomListTopBarWithIndicatorPreview",
|
||||
"FocusedEventEnterprisePreview",
|
||||
"FocusedEventPreview",
|
||||
"GradientFloatingActionButtonCircleShapePreview",
|
||||
"HeaderFooterPageScrollablePreview",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user