Improve API and fix theme glitch when switching between accounts.

This commit is contained in:
Benoit Marty
2025-10-22 09:32:15 +02:00
parent ea2fc290b7
commit c2c77aad2a
13 changed files with 120 additions and 61 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
@@ -26,6 +29,7 @@ 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.theme.ElementTheme
import io.element.android.features.enterprise.api.SemanticColorsLightDark
import io.element.android.features.lockscreen.api.LockScreenEntryPoint
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
@@ -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.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SemanticColorsLightDark
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
@@ -66,6 +69,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
@@ -79,6 +84,7 @@ import io.element.android.libraries.matrix.api.core.toRoomIdOrAlias
import io.element.android.libraries.matrix.api.permalink.PermalinkData
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
@@ -122,6 +128,9 @@ class LoggedInFlowNode(
private val sessionEnterpriseService: SessionEnterpriseService,
private val networkMonitor: NetworkMonitor,
private val notificationConversationService: NotificationConversationService,
private val enterpriseService: EnterpriseService,
private val appPreferencesStore: AppPreferencesStore,
private val buildMeta: BuildMeta,
snackbarDispatcher: SnackbarDispatcher,
) : BaseFlowNode<LoggedInFlowNode.NavTarget>(
backstack = BackStack(
@@ -538,11 +547,21 @@ class LoggedInFlowNode(
@Composable
override fun View(modifier: Modifier) {
Box(modifier = modifier) {
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,
) {
Box(modifier = modifier) {
val ftueState by ftueService.state.collectAsState()
BackstackView()
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
}
}
}

View File

@@ -31,3 +31,10 @@ sealed interface CallType : NodeInputs, Parcelable {
}
}
}
fun CallType.getSessionId(): SessionId? {
return when (this) {
is CallType.ExternalUrl -> null
is CallType.RoomCall -> sessionId
}
}

View File

@@ -23,8 +23,10 @@ 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
@@ -33,6 +35,7 @@ import androidx.lifecycle.Lifecycle
import dev.zacsweers.metro.Inject
import io.element.android.features.call.api.CallType
import io.element.android.features.call.api.CallType.ExternalUrl
import io.element.android.features.call.api.getSessionId
import io.element.android.features.call.impl.DefaultElementCallEntryPoint
import io.element.android.features.call.impl.di.CallBindings
import io.element.android.features.call.impl.pip.PictureInPictureEvents
@@ -42,6 +45,7 @@ import io.element.android.features.call.impl.pip.PipView
import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SemanticColorsLightDark
import io.element.android.libraries.androidutils.browser.ConsoleMessageLogger
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.bindings
@@ -105,9 +109,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,6 +11,9 @@ 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
@@ -21,6 +24,7 @@ import io.element.android.features.call.impl.notifications.CallNotificationData
import io.element.android.features.call.impl.utils.ActiveCallManager
import io.element.android.features.call.impl.utils.CallState
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SemanticColorsLightDark
import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.meta.BuildMeta
import io.element.android.libraries.designsystem.theme.ElementThemeApp
@@ -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

@@ -7,9 +7,9 @@
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.tokens.generated.compoundColorsDark
import io.element.android.compound.tokens.generated.compoundColorsLight
import io.element.android.libraries.matrix.api.core.SessionId
import kotlinx.coroutines.flow.Flow
@@ -26,16 +26,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 = "*"
@@ -47,3 +43,15 @@ fun EnterpriseService.canConnectToAnyHomeserver(): Boolean {
it.isEmpty() || it.contains(EnterpriseService.ANY_ACCOUNT_PROVIDER)
}
}
data class SemanticColorsLightDark(
val light: SemanticColors,
val dark: SemanticColors,
) {
companion object {
val default = SemanticColorsLightDark(
light = compoundColorsLight,
dark = compoundColorsDark,
)
}
}

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.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SemanticColorsLightDark
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,11 +7,9 @@
package io.element.android.features.enterprise.test
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import io.element.android.compound.tokens.generated.SemanticColors
import io.element.android.features.enterprise.api.BugReportUrl
import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SemanticColorsLightDark
import io.element.android.libraries.matrix.api.core.SessionId
import io.element.android.tests.testutils.lambda.lambdaError
import io.element.android.tests.testutils.simulateLongTask
@@ -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,9 +14,13 @@ 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.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SemanticColorsLightDark
import io.element.android.features.lockscreen.api.LockScreenLockState
import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
@@ -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

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

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