Browse Source

Merge pull request #3165 from element-hq/feature/bma/realDarkTheme

Always follow the desired theme for Pin, Incoming Call and Element Call screens
pull/3171/head
Benoit Marty 2 months ago committed by GitHub
parent
commit
23ac85fb8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 16
      app/src/main/kotlin/io/element/android/x/MainActivity.kt
  2. 18
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt
  3. 17
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt
  4. 149
      features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt
  5. 1
      features/lockscreen/impl/build.gradle.kts
  6. 6
      features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt
  7. 1
      libraries/designsystem/build.gradle.kts
  8. 46
      libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt

16
app/src/main/kotlin/io/element/android/x/MainActivity.kt

@ -26,9 +26,6 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
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
@ -38,16 +35,13 @@ 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.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.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
import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.LockScreenService
import io.element.android.features.lockscreen.api.handleSecureFlag import io.element.android.features.lockscreen.api.handleSecureFlag
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.log.logger.LoggerTag
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher import io.element.android.libraries.designsystem.utils.snackbar.LocalSnackbarDispatcher
import io.element.android.x.di.AppBindings import io.element.android.x.di.AppBindings
import io.element.android.x.intent.SafeUriHandler import io.element.android.x.intent.SafeUriHandler
@ -74,14 +68,8 @@ class MainActivity : NodeActivity() {
@Composable @Composable
private fun MainContent(appBindings: AppBindings) { private fun MainContent(appBindings: AppBindings) {
val theme by remember {
appBindings.preferencesStore().getThemeFlow().mapToTheme()
}
.collectAsState(initial = Theme.System)
val migrationState = appBindings.migrationEntryPoint().present() val migrationState = appBindings.migrationEntryPoint().present()
ElementTheme( ElementThemeApp(appBindings.preferencesStore()) {
darkTheme = theme.isDark()
) {
CompositionLocalProvider( CompositionLocalProvider(
LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(), LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(),
LocalUriHandler provides SafeUriHandler(this), LocalUriHandler provides SafeUriHandler(this),

18
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt

@ -30,15 +30,8 @@ import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.core.content.IntentCompat import androidx.core.content.IntentCompat
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.call.api.CallType import io.element.android.features.call.api.CallType
import io.element.android.features.call.impl.DefaultElementCallEntryPoint import io.element.android.features.call.impl.DefaultElementCallEntryPoint
import io.element.android.features.call.impl.di.CallBindings import io.element.android.features.call.impl.di.CallBindings
@ -46,6 +39,7 @@ import io.element.android.features.call.impl.pip.PictureInPicturePresenter
import io.element.android.features.call.impl.services.CallForegroundService import io.element.android.features.call.impl.services.CallForegroundService
import io.element.android.features.call.impl.utils.CallIntentDataParser import io.element.android.features.call.impl.utils.CallIntentDataParser
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.preferences.api.store.AppPreferencesStore import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import javax.inject.Inject import javax.inject.Inject
@ -94,15 +88,9 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator {
requestAudioFocus() requestAudioFocus()
setContent { setContent {
val theme by remember {
appPreferencesStore.getThemeFlow().mapToTheme()
}
.collectAsState(initial = Theme.System)
val state = presenter.present()
val pipState = pictureInPicturePresenter.present() val pipState = pictureInPicturePresenter.present()
ElementTheme( ElementThemeApp(appPreferencesStore) {
darkTheme = theme.isDark() val state = presenter.present()
) {
CallScreenView( CallScreenView(
state = state, state = state,
pipState = pipState, pipState = pipState,

17
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt

@ -29,6 +29,8 @@ 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.ActiveCallManager
import io.element.android.features.call.impl.utils.CallState import io.element.android.features.call.impl.utils.CallState
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -51,6 +53,9 @@ class IncomingCallActivity : AppCompatActivity() {
@Inject @Inject
lateinit var activeCallManager: ActiveCallManager lateinit var activeCallManager: ActiveCallManager
@Inject
lateinit var appPreferencesStore: AppPreferencesStore
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -68,11 +73,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 {
IncomingCallScreen( ElementThemeApp(appPreferencesStore) {
notificationData = notificationData, IncomingCallScreen(
onAnswer = ::onAnswer, notificationData = notificationData,
onCancel = ::onCancel, onAnswer = ::onAnswer,
) onCancel = ::onCancel,
)
}
} }
} else { } else {
// No data, finish the activity // No data, finish the activity

149
features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt

@ -64,67 +64,65 @@ internal fun IncomingCallScreen(
onAnswer: (CallNotificationData) -> Unit, onAnswer: (CallNotificationData) -> Unit,
onCancel: () -> Unit, onCancel: () -> Unit,
) { ) {
ElementTheme { OnboardingBackground()
OnboardingBackground() Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom
) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier
horizontalAlignment = Alignment.CenterHorizontally, .fillMaxWidth()
verticalArrangement = Arrangement.Bottom .padding(start = 20.dp, end = 20.dp, top = 124.dp)
.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Column( Avatar(
modifier = Modifier avatarData = AvatarData(
.fillMaxWidth() id = notificationData.senderId.value,
.padding(start = 20.dp, end = 20.dp, top = 124.dp) name = notificationData.senderName,
.weight(1f), url = notificationData.avatarUrl,
horizontalAlignment = Alignment.CenterHorizontally size = AvatarSize.IncomingCall,
) {
Avatar(
avatarData = AvatarData(
id = notificationData.senderId.value,
name = notificationData.senderName,
url = notificationData.avatarUrl,
size = AvatarSize.IncomingCall,
)
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = notificationData.senderName ?: notificationData.senderId.value,
style = ElementTheme.typography.fontHeadingMdBold,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.screen_incoming_call_subtitle_android),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
ActionButton(
size = 64.dp,
onClick = { onAnswer(notificationData) },
icon = CompoundIcons.VoiceCall(),
title = stringResource(CommonStrings.action_accept),
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
borderColor = ElementTheme.colors.borderSuccessSubtle
) )
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = notificationData.senderName ?: notificationData.senderId.value,
style = ElementTheme.typography.fontHeadingMdBold,
textAlign = TextAlign.Center,
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(R.string.screen_incoming_call_subtitle_android),
style = ElementTheme.typography.fontBodyLgRegular,
color = ElementTheme.colors.textSecondary,
textAlign = TextAlign.Center,
)
}
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = 24.dp, end = 24.dp, bottom = 64.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
ActionButton(
size = 64.dp,
onClick = { onAnswer(notificationData) },
icon = CompoundIcons.VoiceCall(),
title = stringResource(CommonStrings.action_accept),
backgroundColor = ElementTheme.colors.iconSuccessPrimary,
borderColor = ElementTheme.colors.borderSuccessSubtle
)
ActionButton( ActionButton(
size = 64.dp, size = 64.dp,
onClick = onCancel, onClick = onCancel,
icon = CompoundIcons.EndCall(), icon = CompoundIcons.EndCall(),
title = stringResource(CommonStrings.action_reject), title = stringResource(CommonStrings.action_reject),
backgroundColor = ElementTheme.colors.iconCriticalPrimary, backgroundColor = ElementTheme.colors.iconCriticalPrimary,
borderColor = ElementTheme.colors.borderCriticalSubtle borderColor = ElementTheme.colors.borderCriticalSubtle
) )
}
} }
} }
} }
@ -145,7 +143,8 @@ private fun ActionButton(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
FilledIconButton( FilledIconButton(
modifier = Modifier.size(size + borderSize) modifier = Modifier
.size(size + borderSize)
.border(borderSize, borderColor, CircleShape), .border(borderSize, borderColor, CircleShape),
onClick = onClick, onClick = onClick,
colors = IconButtonDefaults.filledIconButtonColors( colors = IconButtonDefaults.filledIconButtonColors(
@ -171,22 +170,20 @@ private fun ActionButton(
@PreviewsDayNight @PreviewsDayNight
@Composable @Composable
internal fun IncomingCallScreenPreview() { internal fun IncomingCallScreenPreview() = ElementPreview {
ElementPreview { IncomingCallScreen(
IncomingCallScreen( notificationData = CallNotificationData(
notificationData = CallNotificationData( sessionId = SessionId("@alice:matrix.org"),
sessionId = SessionId("@alice:matrix.org"), roomId = RoomId("!1234:matrix.org"),
roomId = RoomId("!1234:matrix.org"), eventId = EventId("\$asdadadsad:matrix.org"),
eventId = EventId("\$asdadadsad:matrix.org"), senderId = UserId("@bob:matrix.org"),
senderId = UserId("@bob:matrix.org"), roomName = "A room",
roomName = "A room", senderName = "Bob",
senderName = "Bob", avatarUrl = null,
avatarUrl = null, notificationChannelId = "incoming_call",
notificationChannelId = "incoming_call", timestamp = 0L,
timestamp = 0L, ),
), onAnswer = {},
onAnswer = {}, onCancel = {},
onCancel = {}, )
)
}
} }

1
features/lockscreen/impl/build.gradle.kts

@ -40,6 +40,7 @@ dependencies {
implementation(projects.libraries.designsystem) implementation(projects.libraries.designsystem)
implementation(projects.libraries.featureflag.api) implementation(projects.libraries.featureflag.api)
implementation(projects.libraries.cryptography.api) implementation(projects.libraries.cryptography.api)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)
implementation(projects.libraries.sessionStorage.api) implementation(projects.libraries.sessionStorage.api)
implementation(projects.services.appnavstate.api) implementation(projects.services.appnavstate.api)

6
features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt

@ -24,13 +24,14 @@ 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.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import io.element.android.compound.theme.ElementTheme
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
import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter import io.element.android.features.lockscreen.impl.unlock.PinUnlockPresenter
import io.element.android.features.lockscreen.impl.unlock.PinUnlockView import io.element.android.features.lockscreen.impl.unlock.PinUnlockView
import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings import io.element.android.features.lockscreen.impl.unlock.di.PinUnlockBindings
import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.bindings
import io.element.android.libraries.designsystem.theme.ElementThemeApp
import io.element.android.libraries.preferences.api.store.AppPreferencesStore
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@ -43,13 +44,14 @@ class PinUnlockActivity : AppCompatActivity() {
@Inject lateinit var presenter: PinUnlockPresenter @Inject lateinit var presenter: PinUnlockPresenter
@Inject lateinit var lockScreenService: LockScreenService @Inject lateinit var lockScreenService: LockScreenService
@Inject lateinit var appPreferencesStore: AppPreferencesStore
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge() enableEdgeToEdge()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
bindings<PinUnlockBindings>().inject(this) bindings<PinUnlockBindings>().inject(this)
setContent { setContent {
ElementTheme { ElementThemeApp(appPreferencesStore) {
val state = presenter.present() val state = presenter.present()
PinUnlockView(state = state, isInAppUnlock = false) PinUnlockView(state = state, isInAppUnlock = false)
} }

1
libraries/designsystem/build.gradle.kts

@ -42,6 +42,7 @@ android {
implementation(libs.coil.compose) implementation(libs.coil.compose)
implementation(libs.vanniktech.blurhash) implementation(libs.vanniktech.blurhash)
implementation(projects.libraries.architecture) implementation(projects.libraries.architecture)
implementation(projects.libraries.preferences.api)
implementation(projects.libraries.testtags) implementation(projects.libraries.testtags)
implementation(projects.libraries.uiStrings) implementation(projects.libraries.uiStrings)

46
libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt

@ -0,0 +1,46 @@
/*
* Copyright (c) 2024 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.libraries.designsystem.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.libraries.preferences.api.store.AppPreferencesStore
/**
* Theme to use for all the regular screens of the application.
* Will manage the light / dark theme based on the user preference.
*/
@Composable
fun ElementThemeApp(
appPreferencesStore: AppPreferencesStore,
content: @Composable () -> Unit,
) {
val theme by remember {
appPreferencesStore.getThemeFlow().mapToTheme()
}
.collectAsState(initial = Theme.System)
ElementTheme(
darkTheme = theme.isDark(),
content = content,
)
}
Loading…
Cancel
Save