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

View file

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

@ -1 +1 @@
Subproject commit f662f079f911b728e5769d10268e2c2775d7287a Subproject commit dac93821a6f9f9ad1494d3c69c115ef0696eb7ce

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

View file

@ -11,9 +11,13 @@ import android.os.Bundle
import android.view.WindowManager import android.view.WindowManager
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity 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.core.content.IntentCompat
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import dev.zacsweers.metro.Inject 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
import io.element.android.features.call.api.ElementCallEntryPoint import io.element.android.features.call.api.ElementCallEntryPoint
import io.element.android.features.call.impl.di.CallBindings 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) } val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) }
if (notificationData != null) { if (notificationData != null) {
setContent { setContent {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = notificationData.sessionId)
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp( ElementThemeApp(
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
enterpriseService = enterpriseService, compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = buildMeta, buildMeta = buildMeta,
) { ) {
IncomingCallScreen( 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 package io.element.android.features.enterprise.api
import androidx.compose.runtime.Composable import io.element.android.compound.colors.SemanticColorsLightDark
import androidx.compose.runtime.State
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -26,16 +24,12 @@ interface EnterpriseService {
*/ */
suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?)
@Composable fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark>
fun semanticColorsLight(): State<SemanticColors>
@Composable
fun semanticColorsDark(): State<SemanticColors>
fun firebasePushGateway(): String? fun firebasePushGateway(): String?
fun unifiedPushDefaultPushGateway(): String? fun unifiedPushDefaultPushGateway(): String?
val bugReportUrlFlow: Flow<BugReportUrl> fun bugReportUrlFlow(sessionId: SessionId?): Flow<BugReportUrl>
companion object { companion object {
const val ANY_ACCOUNT_PROVIDER = "*" const val ANY_ACCOUNT_PROVIDER = "*"

View file

@ -7,19 +7,14 @@
package io.element.android.features.enterprise.impl 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.AppScope
import dev.zacsweers.metro.ContributesBinding import dev.zacsweers.metro.ContributesBinding
import dev.zacsweers.metro.Inject import dev.zacsweers.metro.Inject
import io.element.android.compound.tokens.generated.SemanticColors import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.tokens.generated.compoundColorsDark
import io.element.android.compound.tokens.generated.compoundColorsLight
import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
@ContributesBinding(AppScope::class) @ContributesBinding(AppScope::class)
@ -34,18 +29,14 @@ class DefaultEnterpriseService : EnterpriseService {
override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit override suspend fun overrideBrandColor(sessionId: SessionId?, brandColor: String?) = Unit
@Composable override fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark> {
override fun semanticColorsLight(): State<SemanticColors> { return flowOf(SemanticColorsLightDark.default)
return remember { derivedStateOf { compoundColorsLight } }
}
@Composable
override fun semanticColorsDark(): State<SemanticColors> {
return remember { derivedStateOf { compoundColorsDark } }
} }
override fun firebasePushGateway(): String? = null override fun firebasePushGateway(): String? = null
override fun unifiedPushDefaultPushGateway(): 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 package io.element.android.features.enterprise.impl
import app.cash.molecule.RecompositionMode
import app.cash.molecule.moleculeFlow
import app.cash.turbine.test import app.cash.turbine.test
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import io.element.android.compound.tokens.generated.compoundColorsDark import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.compound.tokens.generated.compoundColorsLight 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_HOMESERVER_URL
import io.element.android.libraries.matrix.test.A_SESSION_ID import io.element.android.libraries.matrix.test.A_SESSION_ID
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
@ -44,28 +42,49 @@ class DefaultEnterpriseServiceTest {
} }
@Test @Test
fun `semanticColorsLight always emits the same value`() = runTest { fun `semanticColorsFlow always emits the same value`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService() val defaultEnterpriseService = DefaultEnterpriseService()
moleculeFlow(RecompositionMode.Immediate) { defaultEnterpriseService.semanticColorsFlow(null).test {
defaultEnterpriseService.semanticColorsLight().value
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState).isEqualTo(compoundColorsLight) assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "#87654321") awaitComplete()
expectNoEvents()
} }
} }
@Test @Test
fun `semanticColorsDark always emits the same value`() = runTest { fun `semanticColorsFlow always emits the same value for a session`() = runTest {
val defaultEnterpriseService = DefaultEnterpriseService() val defaultEnterpriseService = DefaultEnterpriseService()
moleculeFlow(RecompositionMode.Immediate) { defaultEnterpriseService.semanticColorsFlow(A_SESSION_ID).test {
defaultEnterpriseService.semanticColorsDark().value
}.test {
val initialState = awaitItem() val initialState = awaitItem()
assertThat(initialState).isEqualTo(compoundColorsDark) assertThat(initialState).isEqualTo(SemanticColorsLightDark.default)
defaultEnterpriseService.overrideBrandColor(A_SESSION_ID, "#87654321") awaitComplete()
expectNoEvents() }
}
@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 package io.element.android.features.enterprise.test
import androidx.compose.runtime.Composable import io.element.android.compound.colors.SemanticColorsLightDark
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.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.SessionId
@ -24,12 +22,13 @@ class FakeEnterpriseService(
private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() }, private val isEnterpriseUserResult: (SessionId) -> Boolean = { lambdaError() },
private val defaultHomeserverListResult: () -> List<String> = { emptyList() }, private val defaultHomeserverListResult: () -> List<String> = { emptyList() },
private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() }, private val isAllowedToConnectToHomeserverResult: (String) -> Boolean = { lambdaError() },
private val semanticColorsLightResult: () -> State<SemanticColors> = { lambdaError() }, initialSemanticColors: SemanticColorsLightDark = SemanticColorsLightDark.default,
private val semanticColorsDarkResult: () -> State<SemanticColors> = { lambdaError() },
private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() }, private val overrideBrandColorResult: (SessionId?, String?) -> Unit = { _, _ -> lambdaError() },
private val firebasePushGatewayResult: () -> String? = { lambdaError() }, private val firebasePushGatewayResult: () -> String? = { lambdaError() },
private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() }, private val unifiedPushDefaultPushGatewayResult: () -> String? = { lambdaError() },
) : EnterpriseService { ) : EnterpriseService {
private val semanticColorsState = MutableStateFlow(initialSemanticColors)
override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask { override suspend fun isEnterpriseUser(sessionId: SessionId): Boolean = simulateLongTask {
isEnterpriseUserResult(sessionId) isEnterpriseUserResult(sessionId)
} }
@ -46,14 +45,8 @@ class FakeEnterpriseService(
overrideBrandColorResult(sessionId, brandColor) overrideBrandColorResult(sessionId, brandColor)
} }
@Composable override fun semanticColorsFlow(sessionId: SessionId?): Flow<SemanticColorsLightDark> {
override fun semanticColorsLight(): State<SemanticColors> { return semanticColorsState.asStateFlow()
return semanticColorsLightResult()
}
@Composable
override fun semanticColorsDark(): State<SemanticColors> {
return semanticColorsDarkResult()
} }
override fun firebasePushGateway(): String? { override fun firebasePushGateway(): String? {
@ -65,5 +58,7 @@ class FakeEnterpriseService(
} }
val bugReportUrlMutableFlow = MutableStateFlow<BugReportUrl>(BugReportUrl.UseDefault) 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.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity 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 androidx.lifecycle.lifecycleScope
import dev.zacsweers.metro.Inject 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.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
@ -46,9 +50,13 @@ class PinUnlockActivity : AppCompatActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
bindings<PinUnlockBindings>().inject(this) bindings<PinUnlockBindings>().inject(this)
setContent { setContent {
val colors by remember {
enterpriseService.semanticColorsFlow(sessionId = null)
}.collectAsState(SemanticColorsLightDark.default)
ElementThemeApp( ElementThemeApp(
appPreferencesStore = appPreferencesStore, appPreferencesStore = appPreferencesStore,
enterpriseService = enterpriseService, compoundLight = colors.light,
compoundDark = colors.dark,
buildMeta = buildMeta, buildMeta = buildMeta,
) { ) {
val state = presenter.present() val state = presenter.present()

View file

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

View file

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

View file

@ -13,7 +13,12 @@ import dev.zacsweers.metro.Inject
import io.element.android.appconfig.RageshakeConfig import io.element.android.appconfig.RageshakeConfig
import io.element.android.features.enterprise.api.BugReportUrl import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService 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.Flow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import okhttp3.HttpUrl import okhttp3.HttpUrl
@ -24,17 +29,21 @@ import okhttp3.HttpUrl.Companion.toHttpUrl
class DefaultBugReporterUrlProvider( class DefaultBugReporterUrlProvider(
private val bugReportAppNameProvider: BugReportAppNameProvider, private val bugReportAppNameProvider: BugReportAppNameProvider,
private val enterpriseService: EnterpriseService, private val enterpriseService: EnterpriseService,
private val sessionStore: SessionStore,
) : BugReporterUrlProvider { ) : BugReporterUrlProvider {
@OptIn(ExperimentalCoroutinesApi::class)
override fun provide(): Flow<HttpUrl?> { override fun provide(): Flow<HttpUrl?> {
if (bugReportAppNameProvider.provide().isEmpty()) return flowOf(null) if (bugReportAppNameProvider.provide().isEmpty()) return flowOf(null)
return enterpriseService.bugReportUrlFlow return sessionStore.sessionIdFlow().flatMapLatest { sessionId ->
.map { bugReportUrl -> enterpriseService.bugReportUrlFlow(sessionId?.let(::SessionId))
when (bugReportUrl) { .map { bugReportUrl ->
is BugReportUrl.Custom -> bugReportUrl.url when (bugReportUrl) {
BugReportUrl.Disabled -> null is BugReportUrl.Custom -> bugReportUrl.url
BugReportUrl.UseDefault -> RageshakeConfig.BUG_REPORT_URL.takeIf { it.isNotEmpty() } 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 com.google.common.truth.Truth.assertThat
import io.element.android.appconfig.RageshakeConfig import io.element.android.appconfig.RageshakeConfig
import io.element.android.features.enterprise.api.BugReportUrl 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.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 kotlinx.coroutines.test.runTest
import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl
import org.junit.Test import org.junit.Test
class DefaultBugReporterUrlProviderTest { class DefaultBugReporterUrlProviderTest {
@Test @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 enterpriseService = FakeEnterpriseService()
val sut = DefaultBugReporterUrlProvider( val sut = createDefaultBugReporterUrlProvider(
bugReportAppNameProvider = { "rageshakeAppName" }, bugReportAppNameProvider = { "rageshakeAppName" },
enterpriseService = enterpriseService, enterpriseService = enterpriseService,
) )
@ -36,15 +39,21 @@ class DefaultBugReporterUrlProviderTest {
} }
@Test @Test
fun `provide return null when there is no rageshake app name`() = runTest { fun `provide returns null when there is no rageshake app name`() = runTest {
val enterpriseService = FakeEnterpriseService() val sut = createDefaultBugReporterUrlProvider()
val sut = DefaultBugReporterUrlProvider(
bugReportAppNameProvider = { "" },
enterpriseService = enterpriseService,
)
sut.provide().test { sut.provide().test {
assertThat(awaitItem()).isNull() assertThat(awaitItem()).isNull()
awaitComplete() 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.runtime.DisposableEffect
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import io.element.android.compound.colors.SemanticColorsLightDark
/** /**
* Can be used to force a composable in dark theme. * Can be used to force a composable in dark theme.
@ -23,6 +24,7 @@ import androidx.compose.ui.graphics.toArgb
*/ */
@Composable @Composable
fun ForcedDarkElementTheme( fun ForcedDarkElementTheme(
colors: SemanticColorsLightDark,
lightStatusBar: Boolean = false, lightStatusBar: Boolean = false,
content: @Composable () -> Unit, 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.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.captureRoboImage 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.screenshot.utils.screenshotFile
import io.element.android.compound.theme.ElementTheme import io.element.android.compound.theme.ElementTheme
import io.element.android.compound.theme.ForcedDarkElementTheme import io.element.android.compound.theme.ForcedDarkElementTheme
@ -42,7 +43,9 @@ class ForcedDarkElementThemeTest {
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Text(text = "Outside") Text(text = "Outside")
ForcedDarkElementTheme { ForcedDarkElementTheme(
colors = SemanticColorsLightDark.default,
) {
Surface { Surface {
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Text(text = "Inside ForcedDarkElementTheme", modifier = Modifier.align(Alignment.Center)) Text(text = "Inside ForcedDarkElementTheme", modifier = Modifier.align(Alignment.Center))

View file

@ -33,7 +33,6 @@ android {
implementation(libs.androidx.compose.material3.adaptive) implementation(libs.androidx.compose.material3.adaptive)
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.vanniktech.blurhash) implementation(libs.vanniktech.blurhash)
implementation(projects.features.enterprise.api)
implementation(projects.libraries.androidutils) implementation(projects.libraries.androidutils)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.core) 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.Theme
import io.element.android.compound.theme.isDark import io.element.android.compound.theme.isDark
import io.element.android.compound.theme.mapToTheme 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.BuildMeta
import io.element.android.libraries.core.meta.BuildType import io.element.android.libraries.core.meta.BuildType
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
@ -53,7 +53,8 @@ val LocalBuildMeta = staticCompositionLocalOf {
@Composable @Composable
fun ElementThemeApp( fun ElementThemeApp(
appPreferencesStore: AppPreferencesStore, appPreferencesStore: AppPreferencesStore,
enterpriseService: EnterpriseService, compoundLight: SemanticColors,
compoundDark: SemanticColors,
buildMeta: BuildMeta, buildMeta: BuildMeta,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
@ -70,8 +71,6 @@ fun ElementThemeApp(
} }
) )
} }
val compoundLight by enterpriseService.semanticColorsLight()
val compoundDark by enterpriseService.semanticColorsDark()
CompositionLocalProvider( CompositionLocalProvider(
LocalBuildMeta provides buildMeta, LocalBuildMeta provides buildMeta,
) { ) {

View file

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

View file

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

View file

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