Merge pull request #5584 from element-hq/bma/brandColorFix

Improve how brand color is applied.
This commit is contained in:
Benoit Marty
2025-10-22 13:08:54 +02:00
committed by GitHub
24 changed files with 293 additions and 97 deletions

View File

@@ -16,6 +16,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
@@ -25,6 +28,7 @@ import androidx.lifecycle.repeatOnLifecycle
import com.bumble.appyx.core.integration.NodeHost
import com.bumble.appyx.core.integrationpoint.NodeActivity
import com.bumble.appyx.core.plugin.NodeReadyObserver
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.theme.ElementTheme
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState
@@ -61,9 +65,13 @@ class MainActivity : NodeActivity() {
@Composable
private fun MainContent(appBindings: AppBindings) {
val migrationState = appBindings.migrationEntryPoint().present()
val colors by remember {
appBindings.enterpriseService().semanticColorsFlow(sessionId = null)
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appBindings.preferencesStore(),
enterpriseService = appBindings.enterpriseService(),
compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = appBindings.buildMeta()
) {
CompositionLocalProvider(

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import com.bumble.appyx.core.composable.PermanentChild
@@ -46,6 +47,8 @@ import io.element.android.appnav.loggedin.SendQueues
import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService
import io.element.android.features.ftue.api.FtueEntryPoint
import io.element.android.features.ftue.api.state.FtueService
@@ -67,6 +70,8 @@ import io.element.android.libraries.architecture.BackstackView
import io.element.android.libraries.architecture.BaseFlowNode
import io.element.android.libraries.architecture.createNode
import io.element.android.libraries.architecture.waitForNavTargetAttached
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.designsystem.utils.snackbar.SnackbarDispatcher
import io.element.android.libraries.di.SessionScope
import io.element.android.libraries.di.annotations.SessionCoroutineScope
@@ -81,6 +86,7 @@ import io.element.android.libraries.matrix.api.permalink.PermalinkData
import io.element.android.libraries.matrix.api.sync.SyncService
import io.element.android.libraries.matrix.api.verification.SessionVerificationServiceListener
import io.element.android.libraries.matrix.api.verification.VerificationRequest
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import io.element.android.libraries.push.api.notifications.conversations.NotificationConversationService
import io.element.android.libraries.ui.common.nodes.emptyNode
import io.element.android.services.appnavstate.api.AppNavigationStateService
@@ -125,6 +131,9 @@ class LoggedInFlowNode(
private val networkMonitor: NetworkMonitor,
private val notificationConversationService: NotificationConversationService,
private val syncService: SyncService,
private val enterpriseService: EnterpriseService,
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
@@ -541,16 +550,26 @@ class LoggedInFlowNode(
@Composable
override fun View(modifier: Modifier) {
val isOnline by syncService.isOnline.collectAsState()
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = modifier,
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView()
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = matrixClient.sessionId)
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
val isOnline by syncService.isOnline.collectAsState()
ConnectivityIndicatorContainer(
isOnline = isOnline,
modifier = modifier,
) { contentModifier ->
Box(modifier = contentModifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView()
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
}
}
}

View File

@@ -0,0 +1,18 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.impl.ui
import io.element.android.features.call.api.CallType
import io.element.android.libraries.matrix.api.core.SessionId
fun CallType.getSessionId(): SessionId? {
return when (this) {
is CallType.ExternalUrl -> null
is CallType.RoomCall -> sessionId
}
}

View File

@@ -23,14 +23,17 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.core.app.PictureInPictureModeChangedInfo
import androidx.core.content.IntentCompat
import androidx.core.util.Consumer
import androidx.lifecycle.Lifecycle
import dev.zacsweers.metro.Inject
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.CallType.ExternalUrl
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
@@ -105,9 +108,13 @@ class ElementCallActivity :
setContent {
val pipState = pictureInPicturePresenter.present()
ListenToAndroidEvents(pipState)
val colors by remember(webViewTarget.value?.getSessionId()) {
enterpriseService.semanticColorsFlow(sessionId = webViewTarget.value?.getSessionId())
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
enterpriseService = enterpriseService,
compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
val state = presenter.present()

View File

@@ -11,9 +11,13 @@ import android.os.Bundle
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope
import dev.zacsweers.metro.Inject
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.call.impl.di.CallBindings
@@ -78,9 +82,13 @@ class IncomingCallActivity : AppCompatActivity() {
val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
if (notificationData != null) {
setContent {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = notificationData.sessionId)
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
enterpriseService = enterpriseService,
compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
IncomingCallScreen(

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2025 New Vector Ltd.
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
package io.element.android.features.call.ui
import com.google.common.truth.Truth.assertThat
import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.ui.getSessionId
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import org.junit.Test
class CallTypeTest {
@Test
fun `getSessionId returns null for ExternalUrl`() {
assertThat(CallType.ExternalUrl("aURL").getSessionId()).isNull()
}
@Test
fun `getSessionId returns the sessionId for RoomCall`() {
assertThat(
CallType.RoomCall(
sessionId = A_SESSION_ID,
roomId = A_ROOM_ID,
).getSessionId()
).isEqualTo(A_SESSION_ID)
}
@Test
fun `ExternalUrl stringification does not contain the URL`() {
assertThat(CallType.ExternalUrl("aURL").toString()).isEqualTo("ExternalUrl")
}
@Test
fun `RoomCall stringification does not contain the URL`() {
assertThat(CallType.RoomCall(A_SESSION_ID, A_ROOM_ID).toString())
.isEqualTo("RoomCall(sessionId=$A_SESSION_ID, roomId=$A_ROOM_ID)")
}
}

View File

@@ -7,9 +7,7 @@
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.compound.colors.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
@@ -26,16 +24,12 @@ interface EnterpriseService {
*/
suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
@Composable
fun semanticColorsLight(): State<SemanticColors>
@Composable
fun semanticColorsDark(): State<SemanticColors>
fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark>
fun firebasePushGateway(): String?
fun unifiedPushDefaultPushGateway(): String?
val bugReportUrlFlow: Flow<BugReportUrl>
fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl>
companion object {
const val ANY_ACCOUNT_PROVIDER = "*"

View File

@@ -7,19 +7,14 @@
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
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.compound.tokens.generated.compoundColorsDark
import io.element.android.compound.tokens.generated.compoundColorsLight
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class)
@@ -34,18 +29,14 @@ class DefaultEnterpriseService : EnterpriseService {
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
@Composable
override fun semanticColorsLight(): State<SemanticColors> {
return remember { derivedStateOf { compoundColorsLight } }
}
@Composable
override fun semanticColorsDark(): State<SemanticColors> {
return remember { derivedStateOf { compoundColorsDark } }
override fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark> {
return flowOf(SemanticColorsLightDark.default)
}
override fun firebasePushGateway(): String? = null
override fun unifiedPushDefaultPushGateway(): String? = null
override val bugReportUrlFlow = flowOf(BugReportUrl.UseDefault)
override fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl> {
return flowOf(BugReportUrl.UseDefault)
}
}

View File

@@ -7,12 +7,10 @@
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.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
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
@@ -44,28 +42,49 @@ class DefaultEnterpriseServiceTest {
}
@Test
fun `semanticColorsLight always emits the same value`() = runTest {
fun `semanticColorsFlow always emits the same value`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
moleculeFlow(RecompositionMode.Immediate) {
defaultEnterpriseService.semanticColorsLight().value
}.test {
defaultEnterpriseService.semanticColorsFlow(null).test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(compoundColorsLight)
defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "#87654321")
expectNoEvents()
assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
awaitComplete()
}
}
@Test
fun `semanticColorsDark always emits the same value`() = runTest {
fun `semanticColorsFlow always emits the same value for a session`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
moleculeFlow(RecompositionMode.Immediate) {
defaultEnterpriseService.semanticColorsDark().value
}.test {
defaultEnterpriseService.semanticColorsFlow(A_SESSION_ID).test {
val initialState = awaitItem()
assertThat(initialState).isEqualTo(compoundColorsDark)
defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "#87654321")
expectNoEvents()
assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
awaitComplete()
}
}
@Test
fun `overrideBrandColor has no effect`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "aColor")
}
@Test
fun `firebasePushGateway returns null`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.firebasePushGateway()).isNull()
}
@Test
fun `unifiedPushDefaultPushGateway returns null`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
assertThat(defaultEnterpriseService.unifiedPushDefaultPushGateway()).isNull()
}
@Test
fun `bugReportUrlFlow only emits UseDefault`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService()
defaultEnterpriseService.bugReportUrlFlow(A_SESSION_ID).test {
assertThat(awaitItem()).isEqualTo(BugReportUrl.UseDefault)
awaitComplete()
}
}
}

View File

@@ -7,9 +7,7 @@
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.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId
@@ -24,12 +22,13 @@ class FakeEnterpriseService(
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> State<SemanticColors> = { lambdaError() },
private val semanticColorsDarkResult: () -> State<SemanticColors> = { lambdaError() },
initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default,
private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
) : EnterpriseService {
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {
isEnterpriseUserResult(sessionId)
}
@@ -46,14 +45,8 @@ class FakeEnterpriseService(
overrideBrandColorResult(sessionId, brandColor)
}
@Composable
override fun semanticColorsLight(): State<SemanticColors> {
return semanticColorsLightResult()
}
@Composable
override fun semanticColorsDark(): State<SemanticColors> {
return semanticColorsDarkResult()
override fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark> {
return semanticColorsState.asStateFlow()
}
override fun firebasePushGateway(): String? {
@@ -65,5 +58,7 @@ class FakeEnterpriseService(
}
val bugReportUrlMutableFlow = MutableStateFlow<BugReportUrl>(BugReportUrl.UseDefault)
override val bugReportUrlFlow: Flow<BugReportUrl> = bugReportUrlMutableFlow.asStateFlow()
override fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl> {
return bugReportUrlMutableFlow.asStateFlow()
}
}

View File

@@ -14,8 +14,12 @@ import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.lifecycle.lifecycleScope
import dev.zacsweers.metro.Inject
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
@@ -46,9 +50,13 @@ class PinUnlockActivity : AppCompatActivity() {
super.onCreate(savedInstanceState)
bindings<PinUnlockBindings>().inject(this)
setContent {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = null)
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp(
appPreferencesStore = appPreferencesStore,
enterpriseService = enterpriseService,
compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = buildMeta,
) {
val state = presenter.present()

View File

@@ -28,6 +28,7 @@ dependencies {
api(projects.features.messages.api)
implementation(projects.appconfig)
implementation(projects.features.call.api)
implementation(projects.features.enterprise.api)
implementation(projects.features.location.api)
implementation(projects.features.poll.api)
implementation(projects.features.roomcall.api)

View File

@@ -8,6 +8,9 @@
package io.element.android.features.messages.impl.attachments.preview
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@@ -15,12 +18,15 @@ import com.bumble.appyx.core.plugin.Plugin
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.messages.impl.attachments.Attachment
import io.element.android.libraries.architecture.NodeInputs
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.local.LocalMediaRenderer
@@ -31,6 +37,8 @@ class AttachmentsPreviewNode(
@Assisted plugins: List<Plugin>,
presenterFactory: AttachmentsPreviewPresenter.Factory,
private val localMediaRenderer: LocalMediaRenderer,
private val sessionId: SessionId,
private val enterpriseService: EnterpriseService,
) : Node(buildContext, plugins = plugins) {
data class Inputs(
val attachment: Attachment,
@@ -53,7 +61,12 @@ class AttachmentsPreviewNode(
@Composable
override fun View(modifier: Modifier) {
ForcedDarkElementTheme {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = sessionId)
}.collectAsState(SemanticColorsLightDark.default)
ForcedDarkElementTheme(
colors = colors,
) {
val state = presenter.present()
AttachmentsPreviewView(
state = state,

View File

@@ -13,7 +13,12 @@ import dev.zacsweers.metro.Inject
import io.element.android.appconfig.RageshakeConfig
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.api.sessionIdFlow
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import okhttp3.HttpUrl
@@ -24,17 +29,21 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
class DefaultBugReporterUrlProvider(
private val bugReportAppNameProvider: BugReportAppNameProvider,
private val enterpriseService: EnterpriseService,
private val sessionStore: SessionStore,
) : BugReporterUrlProvider {
@OptIn(ExperimentalCoroutinesApi::class)
override fun provide(): Flow<HttpUrl?> {
if (bugReportAppNameProvider.provide().isEmpty()) return flowOf(null)
return enterpriseService.bugReportUrlFlow
.map { bugReportUrl ->
when (bugReportUrl) {
is BugReportUrl.Custom -> bugReportUrl.url
BugReportUrl.Disabled -> null
BugReportUrl.UseDefault -> RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }
return sessionStore.sessionIdFlow().flatMapLatest { sessionId ->
enterpriseService.bugReportUrlFlow(sessionId?.let(::SessionId))
.map { bugReportUrl ->
when (bugReportUrl) {
is BugReportUrl.Custom -> bugReportUrl.url
BugReportUrl.Disabled -> null
BugReportUrl.UseDefault -> RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() }
}
}
}
.map { it?.toHttpUrl() }
.map { it?.toHttpUrl() }
}
}
}

View File

@@ -11,16 +11,19 @@ import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.RageshakeConfig
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.sessionstorage.api.SessionStore
import io.element.android.libraries.sessionstorage.test.InMemorySessionStore
import kotlinx.coroutines.test.runTest
import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.Test
class DefaultBugReporterUrlProviderTest {
@Test
fun `provide return values when there is an rageshake app name`() = runTest {
fun `provide returns values when there is an rageshake app name`() = runTest {
val enterpriseService = FakeEnterpriseService()
val sut = DefaultBugReporterUrlProvider(
val sut = createDefaultBugReporterUrlProvider(
bugReportAppNameProvider = { "rageshakeAppName" },
enterpriseService = enterpriseService,
)
@@ -36,15 +39,21 @@ class DefaultBugReporterUrlProviderTest {
}
@Test
fun `provide return null when there is no rageshake app name`() = runTest {
val enterpriseService = FakeEnterpriseService()
val sut = DefaultBugReporterUrlProvider(
bugReportAppNameProvider = { "" },
enterpriseService = enterpriseService,
)
fun `provide returns null when there is no rageshake app name`() = runTest {
val sut = createDefaultBugReporterUrlProvider()
sut.provide().test {
assertThat(awaitItem()).isNull()
awaitComplete()
}
}
}
private fun createDefaultBugReporterUrlProvider(
bugReportAppNameProvider: BugReportAppNameProvider = BugReportAppNameProvider { "" },
enterpriseService: EnterpriseService = FakeEnterpriseService(),
sessionStore: SessionStore = InMemorySessionStore(),
) = DefaultBugReporterUrlProvider(
bugReportAppNameProvider = bugReportAppNameProvider,
enterpriseService = enterpriseService,
sessionStore = sessionStore,
)

View File

@@ -0,0 +1,24 @@
/*
* 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.compound.colors
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.compound.tokens.generated.compoundColorsDark
import io.element.android.compound.tokens.generated.compoundColorsLight
data class SemanticColorsLightDark(
val light: SemanticColors,
val dark: SemanticColors,
) {
companion object {
val default = SemanticColorsLightDark(
light = compoundColorsLight,
dark = compoundColorsDark,
)
}
}

View File

@@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import io.element.android.compound.colors.SemanticColorsLightDark
/**
* Can be used to force a composable in dark theme.
@@ -23,6 +24,7 @@ import androidx.compose.ui.graphics.toArgb
*/
@Composable
fun ForcedDarkElementTheme(
colors: SemanticColorsLightDark,
lightStatusBar: Boolean = false,
content: @Composable () -> Unit,
) {
@@ -47,5 +49,11 @@ fun ForcedDarkElementTheme(
)
}
}
ElementTheme(darkTheme = true, lightStatusBar = lightStatusBar, content = content)
ElementTheme(
darkTheme = true,
compoundLight = colors.light,
compoundDark = colors.dark,
lightStatusBar = lightStatusBar,
content = content,
)
}

View File

@@ -19,6 +19,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.captureRoboImage
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.screenshot.utils.screenshotFile
import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.ForcedDarkElementTheme
@@ -42,7 +43,9 @@ class ForcedDarkElementThemeTest {
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
Text(text = "Outside")
ForcedDarkElementTheme {
ForcedDarkElementTheme(
colors = SemanticColorsLightDark.default,
) {
Surface {
Box(modifier = Modifier.fillMaxSize()) {
Text(text = "Inside ForcedDarkElementTheme", modifier = Modifier.align(Alignment.Center))

View File

@@ -33,7 +33,6 @@ android {
implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.coil.compose)
implementation(libs.vanniktech.blurhash)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
implementation(projects.libraries.core)

View File

@@ -19,7 +19,7 @@ import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.Theme
import io.element.android.compound.theme.isDark
import io.element.android.compound.theme.mapToTheme
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@@ -53,7 +53,8 @@ val LocalBuildMeta = staticCompositionLocalOf {
@Composable
fun ElementThemeApp(
appPreferencesStore: AppPreferencesStore,
enterpriseService: EnterpriseService,
compoundLight: SemanticColors,
compoundDark: SemanticColors,
buildMeta: BuildMeta,
content: @Composable () -> Unit,
) {
@@ -70,8 +71,6 @@ fun ElementThemeApp(
}
)
}
val compoundLight by enterpriseService.semanticColorsLight()
val compoundDark by enterpriseService.semanticColorsDark()
CompositionLocalProvider(
LocalBuildMeta provides buildMeta,
) {

View File

@@ -32,6 +32,7 @@ dependencies {
implementation(libs.vanniktech.blurhash)
implementation(libs.telephoto.flick)
implementation(projects.features.enterprise.api)
implementation(projects.features.viewfolder.api)
implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture)
@@ -54,6 +55,7 @@ dependencies {
implementation(projects.libraries.matrix.api)
testCommonDependencies(libs, true)
testImplementation(projects.features.enterprise.test)
testImplementation(projects.libraries.audio.test)
testImplementation(projects.libraries.dateformatter.test)
testImplementation(projects.libraries.featureflag.test)

View File

@@ -8,6 +8,9 @@
package io.element.android.libraries.mediaviewer.impl.viewer
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import com.bumble.appyx.core.modality.BuildContext
import com.bumble.appyx.core.node.Node
@@ -16,13 +19,16 @@ import com.bumble.appyx.core.plugin.plugins
import dev.zacsweers.metro.Assisted
import dev.zacsweers.metro.AssistedInject
import io.element.android.annotations.ContributesNode
import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.theme.ForcedDarkElementTheme
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.viewfolder.api.TextFileViewer
import io.element.android.libraries.architecture.inputs
import io.element.android.libraries.audio.api.AudioFocus
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.di.RoomScope
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.libraries.matrix.api.media.MatrixMediaLoader
import io.element.android.libraries.matrix.api.timeline.Timeline
import io.element.android.libraries.mediaviewer.api.MediaViewerEntryPoint
@@ -47,6 +53,8 @@ class MediaViewerNode(
pagerKeysHandler: PagerKeysHandler,
private val textFileViewer: TextFileViewer,
private val audioFocus: AudioFocus,
private val sessionId: SessionId,
private val enterpriseService: EnterpriseService,
) : Node(buildContext, plugins = plugins),
MediaViewerNavigator {
private val inputs = inputs<MediaViewerEntryPoint.Params>()
@@ -127,7 +135,12 @@ class MediaViewerNode(
@Composable
override fun View(modifier: Modifier) {
ForcedDarkElementTheme {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = sessionId)
}.collectAsState(SemanticColorsLightDark.default)
ForcedDarkElementTheme(
colors = colors,
) {
val state = presenter.present()
MediaViewerView(
state = state,

View File

@@ -11,10 +11,12 @@ import android.net.Uri
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.bumble.appyx.core.modality.BuildContext
import com.google.common.truth.Truth.assertThat
import io.element.android.features.enterprise.test.FakeEnterpriseService
import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.media.FakeMatrixMediaLoader
import io.element.android.libraries.mediaplayer.test.FakeAudioFocus
import io.element.android.libraries.mediaviewer.api.MediaInfo
@@ -63,6 +65,8 @@ class DefaultMediaViewerEntryPointTest {
pagerKeysHandler = PagerKeysHandler(),
textFileViewer = { _, _ -> lambdaError() },
audioFocus = FakeAudioFocus(),
sessionId = A_SESSION_ID,
enterpriseService = FakeEnterpriseService(),
)
}
val callback = object : MediaViewerEntryPoint.Callback {
@@ -104,6 +108,8 @@ class DefaultMediaViewerEntryPointTest {
pagerKeysHandler = PagerKeysHandler(),
textFileViewer = { _, _ -> lambdaError() },
audioFocus = FakeAudioFocus(),
sessionId = A_SESSION_ID,
enterpriseService = FakeEnterpriseService(),
)
}
val callback = object : MediaViewerEntryPoint.Callback {