diff --git a/app/src/main/kotlin/io/element/android/x/MainActivity.kt b/app/src/main/kotlin/io/element/android/x/MainActivity.kt index eb3f9450f5..b36f93f1dc 100644 --- a/app/src/main/kotlin/io/element/android/x/MainActivity.kt +++ b/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.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 @@ -38,16 +35,13 @@ 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.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.LockScreenLockState import io.element.android.features.lockscreen.api.LockScreenService import io.element.android.features.lockscreen.api.handleSecureFlag import io.element.android.libraries.architecture.bindings 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.x.di.AppBindings import io.element.android.x.intent.SafeUriHandler @@ -74,14 +68,8 @@ class MainActivity : NodeActivity() { @Composable private fun MainContent(appBindings: AppBindings) { - val theme by remember { - appBindings.preferencesStore().getThemeFlow().mapToTheme() - } - .collectAsState(initial = Theme.System) val migrationState = appBindings.migrationEntryPoint().present() - ElementTheme( - darkTheme = theme.isDark() - ) { + ElementThemeApp(appBindings.preferencesStore()) { CompositionLocalProvider( LocalSnackbarDispatcher provides appBindings.snackbarDispatcher(), LocalUriHandler provides SafeUriHandler(this), diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt index c770deff60..b0d41efe9e 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/ElementCallActivity.kt +++ b/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.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember 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.impl.DefaultElementCallEntryPoint 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.utils.CallIntentDataParser 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 javax.inject.Inject @@ -94,15 +88,9 @@ class ElementCallActivity : AppCompatActivity(), CallScreenNavigator { requestAudioFocus() setContent { - val theme by remember { - appPreferencesStore.getThemeFlow().mapToTheme() - } - .collectAsState(initial = Theme.System) - val state = presenter.present() val pipState = pictureInPicturePresenter.present() - ElementTheme( - darkTheme = theme.isDark() - ) { + ElementThemeApp(appPreferencesStore) { + val state = presenter.present() CallScreenView( state = state, pipState = pipState, diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt index 02cd612bcf..05d36434cc 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallActivity.kt +++ b/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.CallState 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.launchIn import kotlinx.coroutines.flow.onEach @@ -51,6 +53,9 @@ class IncomingCallActivity : AppCompatActivity() { @Inject lateinit var activeCallManager: ActiveCallManager + @Inject + lateinit var appPreferencesStore: AppPreferencesStore + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -68,11 +73,13 @@ class IncomingCallActivity : AppCompatActivity() { val notificationData = intent?.let { IntentCompat.getParcelableExtra(it, EXTRA_NOTIFICATION_DATA, CallNotificationData::class.java) } if (notificationData != null) { setContent { - IncomingCallScreen( - notificationData = notificationData, - onAnswer = ::onAnswer, - onCancel = ::onCancel, - ) + ElementThemeApp(appPreferencesStore) { + IncomingCallScreen( + notificationData = notificationData, + onAnswer = ::onAnswer, + onCancel = ::onCancel, + ) + } } } else { // No data, finish the activity diff --git a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt b/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt index 80dc2353ca..d663f15807 100644 --- a/features/call/impl/src/main/kotlin/io/element/android/features/call/impl/ui/IncomingCallScreen.kt +++ b/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, onCancel: () -> Unit, ) { - ElementTheme { - OnboardingBackground() + OnboardingBackground() + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Bottom + ) { Column( - modifier = Modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Bottom + modifier = Modifier + .fillMaxWidth() + .padding(start = 20.dp, end = 20.dp, top = 124.dp) + .weight(1f), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(start = 20.dp, end = 20.dp, top = 124.dp) - .weight(1f), - horizontalAlignment = Alignment.CenterHorizontally - ) { - 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 + 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 + ) - ActionButton( - size = 64.dp, - onClick = onCancel, - icon = CompoundIcons.EndCall(), - title = stringResource(CommonStrings.action_reject), - backgroundColor = ElementTheme.colors.iconCriticalPrimary, - borderColor = ElementTheme.colors.borderCriticalSubtle - ) - } + ActionButton( + size = 64.dp, + onClick = onCancel, + icon = CompoundIcons.EndCall(), + title = stringResource(CommonStrings.action_reject), + backgroundColor = ElementTheme.colors.iconCriticalPrimary, + borderColor = ElementTheme.colors.borderCriticalSubtle + ) } } } @@ -145,7 +143,8 @@ private fun ActionButton( horizontalAlignment = Alignment.CenterHorizontally ) { FilledIconButton( - modifier = Modifier.size(size + borderSize) + modifier = Modifier + .size(size + borderSize) .border(borderSize, borderColor, CircleShape), onClick = onClick, colors = IconButtonDefaults.filledIconButtonColors( @@ -171,22 +170,20 @@ private fun ActionButton( @PreviewsDayNight @Composable -internal fun IncomingCallScreenPreview() { - ElementPreview { - IncomingCallScreen( - notificationData = CallNotificationData( - sessionId = SessionId("@alice:matrix.org"), - roomId = RoomId("!1234:matrix.org"), - eventId = EventId("\$asdadadsad:matrix.org"), - senderId = UserId("@bob:matrix.org"), - roomName = "A room", - senderName = "Bob", - avatarUrl = null, - notificationChannelId = "incoming_call", - timestamp = 0L, - ), - onAnswer = {}, - onCancel = {}, - ) - } +internal fun IncomingCallScreenPreview() = ElementPreview { + IncomingCallScreen( + notificationData = CallNotificationData( + sessionId = SessionId("@alice:matrix.org"), + roomId = RoomId("!1234:matrix.org"), + eventId = EventId("\$asdadadsad:matrix.org"), + senderId = UserId("@bob:matrix.org"), + roomName = "A room", + senderName = "Bob", + avatarUrl = null, + notificationChannelId = "incoming_call", + timestamp = 0L, + ), + onAnswer = {}, + onCancel = {}, + ) } diff --git a/features/lockscreen/impl/build.gradle.kts b/features/lockscreen/impl/build.gradle.kts index 1cbe9692cf..21f3e05421 100644 --- a/features/lockscreen/impl/build.gradle.kts +++ b/features/lockscreen/impl/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.featureflag.api) implementation(projects.libraries.cryptography.api) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.uiStrings) implementation(projects.libraries.sessionStorage.api) implementation(projects.services.appnavstate.api) diff --git a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt b/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt index 9c228b0736..7b7b16790f 100644 --- a/features/lockscreen/impl/src/main/kotlin/io/element/android/features/lockscreen/impl/unlock/activity/PinUnlockActivity.kt +++ b/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.appcompat.app.AppCompatActivity 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.LockScreenService 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.di.PinUnlockBindings 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 javax.inject.Inject @@ -43,13 +44,14 @@ class PinUnlockActivity : AppCompatActivity() { @Inject lateinit var presenter: PinUnlockPresenter @Inject lateinit var lockScreenService: LockScreenService + @Inject lateinit var appPreferencesStore: AppPreferencesStore override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) bindings().inject(this) setContent { - ElementTheme { + ElementThemeApp(appPreferencesStore) { val state = presenter.present() PinUnlockView(state = state, isInAppUnlock = false) } diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 9934da3e13..e737220961 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -42,6 +42,7 @@ android { implementation(libs.coil.compose) implementation(libs.vanniktech.blurhash) implementation(projects.libraries.architecture) + implementation(projects.libraries.preferences.api) implementation(projects.libraries.testtags) implementation(projects.libraries.uiStrings) diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/ElementThemeApp.kt new file mode 100644 index 0000000000..4c20c02e9b --- /dev/null +++ b/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, + ) +}