Merge pull request #5584 from element-hq/bma/brandColorFix
Improve how brand color is applied.
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Submodule enterprise updated: f662f079f9...dac93821a6
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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)")
|
||||
}
|
||||
}
|
||||
@@ -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 = "*"
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user