diff --git a/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt b/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt index 8db7750..2fb5fe4 100644 --- a/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt +++ b/app/src/main/kotlin/app/dapk/st/SharedPreferencesDelegate.kt @@ -2,8 +2,8 @@ package app.dapk.st import android.content.Context import app.dapk.st.core.CoroutineDispatchers +import app.dapk.st.core.Preferences import app.dapk.st.core.withIoContext -import app.dapk.st.domain.Preferences internal class SharedPreferencesDelegate( context: Context, diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index 8f20819..ff26c53 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -96,32 +96,35 @@ internal class AppModule(context: Application, logger: MatrixLogger) { private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver) val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers) - val coreAndroidModule = CoreAndroidModule(intentFactory = object : IntentFactory { - override fun notificationOpenApp(context: Context) = PendingIntent.getActivity( - context, - 1000, - home(context) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK), - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE - ) + val coreAndroidModule = CoreAndroidModule( + intentFactory = object : IntentFactory { + override fun notificationOpenApp(context: Context) = PendingIntent.getActivity( + context, + 1000, + home(context) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) - override fun notificationOpenMessage(context: Context, roomId: RoomId) = PendingIntent.getActivity( - context, - roomId.hashCode(), - messenger(context, roomId) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK), - PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE - ) + override fun notificationOpenMessage(context: Context, roomId: RoomId) = PendingIntent.getActivity( + context, + roomId.hashCode(), + messenger(context, roomId) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) - override fun home(context: Context) = Intent(context, MainActivity::class.java) - override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId) - override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId) - override fun messengerAttachments(context: Context, roomId: RoomId, attachments: List) = MessengerActivity.newMessageAttachment( - context, - roomId, - attachments - ) - }) + override fun home(context: Context) = Intent(context, MainActivity::class.java) + override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId) + override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId) + override fun messengerAttachments(context: Context, roomId: RoomId, attachments: List) = MessengerActivity.newMessageAttachment( + context, + roomId, + attachments + ) + }, + unsafeLazy { storeModule.value.preferences } + ) val featureModules = FeatureModules( storeModule, @@ -187,7 +190,8 @@ internal class FeatureModules internal constructor( matrixModules.sync, context.contentResolver, buildMeta, - coroutineDispatchers + coroutineDispatchers, + coreAndroidModule.themeStore(), ) } val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) } diff --git a/core/src/main/kotlin/app/dapk/st/core/Preferences.kt b/core/src/main/kotlin/app/dapk/st/core/Preferences.kt new file mode 100644 index 0000000..4d1cc3f --- /dev/null +++ b/core/src/main/kotlin/app/dapk/st/core/Preferences.kt @@ -0,0 +1,12 @@ +package app.dapk.st.core + +interface Preferences { + + suspend fun store(key: String, value: String) + suspend fun readString(key: String): String? + suspend fun clear() + suspend fun remove(key: String) +} + +suspend fun Preferences.readBoolean(key: String) = this.readString(key)?.toBooleanStrict() +suspend fun Preferences.store(key: String, value: Boolean) = this.store(key, value.toString()) \ No newline at end of file diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt index 6fa88d3..45259ba 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Theme.kt @@ -1,6 +1,5 @@ package app.dapk.st.design.components -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme @@ -19,13 +18,13 @@ private val DARK_COLOURS = darkColorScheme( onPrimary = Color(0xDDFFFFFF), ) -private val LIGHT_COLOURS = DARK_COLOURS +private val DARK_EXTENDED = createExtended(DARK_COLOURS.primary, DARK_COLOURS.onPrimary) -private val DARK_EXTENDED = ExtendedColors( - selfBubble = DARK_COLOURS.primary, - onSelfBubble = DARK_COLOURS.onPrimary, +private fun createExtended(primary: Color, onPrimary: Color) = ExtendedColors( + selfBubble = primary, + onSelfBubble = onPrimary, othersBubble = Color(0x20EDEDED), - onOthersBubble = Color(0xFF000000), + onOthersBubble = DARK_COLOURS.onPrimary, selfBubbleReplyBackground = Color(0x40EAEAEA), otherBubbleReplyBackground = Color(0x20EAEAEA), missingImageColors = listOf( @@ -34,7 +33,6 @@ private val DARK_EXTENDED = ExtendedColors( Color(0xFFf6c8cb) to Color(0xFFda2535), ) ) -private val LIGHT_EXTENDED = DARK_EXTENDED @Immutable data class ExtendedColors( @@ -51,21 +49,22 @@ data class ExtendedColors( } } -private val LocalExtendedColors = staticCompositionLocalOf { LIGHT_EXTENDED } +private val LocalExtendedColors = staticCompositionLocalOf { DARK_EXTENDED } @Composable -fun SmallTalkTheme(content: @Composable () -> Unit) { +fun SmallTalkTheme(themeConfig: ThemeConfig, content: @Composable () -> Unit) { val systemUiController = rememberSystemUiController() - val systemInDarkTheme = isSystemInDarkTheme() - MaterialTheme( - colorScheme = dynamicDarkColorScheme(LocalContext.current) -// colorScheme = if (systemInDarkTheme) DARK_COLOURS else LIGHT_COLOURS, - ) { + val colorScheme = if (themeConfig.useDynamicTheme) { + dynamicDarkColorScheme(LocalContext.current) + } else { + DARK_COLOURS + } + MaterialTheme(colorScheme = colorScheme) { val backgroundColor = MaterialTheme.colorScheme.background SideEffect { systemUiController.setSystemBarsColor(backgroundColor) } - CompositionLocalProvider(LocalExtendedColors provides if (systemInDarkTheme) DARK_EXTENDED else LIGHT_EXTENDED) { + CompositionLocalProvider(LocalExtendedColors provides createExtended(MaterialTheme.colorScheme.primary, MaterialTheme.colorScheme.onPrimary)) { content() } } @@ -75,4 +74,8 @@ object SmallTalkTheme { val extendedColors: ExtendedColors @Composable get() = LocalExtendedColors.current -} \ No newline at end of file +} + +data class ThemeConfig( + val useDynamicTheme: Boolean, +) \ No newline at end of file diff --git a/domains/android/compose-core/build.gradle b/domains/android/compose-core/build.gradle index c79d69b..e04cf9f 100644 --- a/domains/android/compose-core/build.gradle +++ b/domains/android/compose-core/build.gradle @@ -3,5 +3,6 @@ applyAndroidComposeLibraryModule(project) dependencies { implementation project(":core") implementation project(":features:navigator") + implementation project(":design-library") api project(":domains:android:core") } diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt index a06b4b7..7458cd9 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt @@ -1,5 +1,8 @@ package app.dapk.st.core +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -53,4 +56,10 @@ fun LifecycleEffect(onStart: () -> Unit = {}, onStop: () -> Unit = {}) { lifecycleOwner.value.lifecycle.removeObserver(lifecycleObserver) } } -} \ No newline at end of file +} + +fun Context.getActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.getActivity() + else -> null +} diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/CoreAndroidModule.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/CoreAndroidModule.kt index 5829e0a..b84e2c7 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/CoreAndroidModule.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/CoreAndroidModule.kt @@ -1,9 +1,17 @@ package app.dapk.st.core +import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.navigator.IntentFactory -class CoreAndroidModule(private val intentFactory: IntentFactory): ProvidableModule { +class CoreAndroidModule( + private val intentFactory: IntentFactory, + private val preferences: Lazy, +) : ProvidableModule { fun intentFactory() = intentFactory + private val themeStore by unsafeLazy { ThemeStore(preferences.value) } + + fun themeStore() = themeStore + } \ No newline at end of file diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt index 3904972..59cec14 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/DapkActivity.kt @@ -6,20 +6,43 @@ import androidx.activity.ComponentActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import app.dapk.st.core.extensions.unsafeLazy +import app.dapk.st.design.components.SmallTalkTheme +import app.dapk.st.design.components.ThemeConfig import app.dapk.st.navigator.navigator +import androidx.activity.compose.setContent as _setContent abstract class DapkActivity : ComponentActivity(), EffectScope { private val coreAndroidModule by unsafeLazy { module() } + private val themeStore by unsafeLazy { coreAndroidModule.themeStore() } private val remembers = mutableMapOf() protected val navigator by navigator { coreAndroidModule.intentFactory() } + private lateinit var themeConfig: ThemeConfig + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + this.themeConfig = ThemeConfig(themeStore.isMaterialYouEnabled()) + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); } + protected fun setContent(content: @Composable () -> Unit) { + _setContent { + SmallTalkTheme(themeConfig) { + content() + } + } + } + + override fun onResume() { + super.onResume() + if (themeConfig.useDynamicTheme != themeStore.isMaterialYouEnabled()) { + recreate() + } + } + @Composable override fun OnceEffect(key: Any, sideEffect: () -> Unit) { val triggerSideEffect = remembers.containsKey(key).not() diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ThemeStore.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ThemeStore.kt new file mode 100644 index 0000000..ff3d2f5 --- /dev/null +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ThemeStore.kt @@ -0,0 +1,26 @@ +package app.dapk.st.core + +import kotlinx.coroutines.runBlocking + +private const val KEY_MATERIAL_YOU_ENABLED = "material_you_enabled" + +class ThemeStore( + private val preferences: Preferences +) { + + private var _isMaterialYouEnabled: Boolean? = null + + fun isMaterialYouEnabled() = _isMaterialYouEnabled ?: blockingInitialRead() + + private fun blockingInitialRead(): Boolean { + return runBlocking { + (preferences.readBoolean(KEY_MATERIAL_YOU_ENABLED) ?: false).also { _isMaterialYouEnabled = it } + } + } + + suspend fun storeMaterialYouEnabled(isEnabled: Boolean) { + _isMaterialYouEnabled = isEnabled + preferences.store(KEY_MATERIAL_YOU_ENABLED, isEnabled) + } + +} \ No newline at end of file diff --git a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt index dc8c77e..67d12ae 100644 --- a/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt +++ b/domains/android/push/src/main/kotlin/app/dapk/st/push/PushModule.kt @@ -2,10 +2,10 @@ package app.dapk.st.push import android.content.Context import app.dapk.st.core.CoroutineDispatchers +import app.dapk.st.core.Preferences import app.dapk.st.core.ProvidableModule import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy -import app.dapk.st.domain.Preferences import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.firebase.messaging.Messaging import app.dapk.st.push.messaging.MessagingPushTokenRegistrar diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt index 5570a73..a0a4d59 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/ApplicationPreferences.kt @@ -1,5 +1,7 @@ package app.dapk.st.domain +import app.dapk.st.core.Preferences + class ApplicationPreferences( private val preferences: Preferences, ) { diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/CredentialsPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/CredentialsPreferences.kt index 86cf790..3bab1e8 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/CredentialsPreferences.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/CredentialsPreferences.kt @@ -1,7 +1,8 @@ package app.dapk.st.domain -import app.dapk.st.matrix.common.UserCredentials +import app.dapk.st.core.Preferences import app.dapk.st.matrix.common.CredentialsStore +import app.dapk.st.matrix.common.UserCredentials internal class CredentialsPreferences( private val preferences: Preferences, diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/FilterPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/FilterPreferences.kt index 24409a0..ddbba29 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/FilterPreferences.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/FilterPreferences.kt @@ -1,5 +1,6 @@ package app.dapk.st.domain +import app.dapk.st.core.Preferences import app.dapk.st.matrix.sync.FilterStore internal class FilterPreferences( diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/Preferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/Preferences.kt deleted file mode 100644 index 73fcffb..0000000 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/Preferences.kt +++ /dev/null @@ -1,9 +0,0 @@ -package app.dapk.st.domain - -interface Preferences { - - suspend fun store(key: String, value: String) - suspend fun readString(key: String): String? - suspend fun clear() - suspend fun remove(key: String) -} diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt index a90c276..874b8f5 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/StoreModule.kt @@ -2,6 +2,7 @@ package app.dapk.st.domain import app.dapk.db.DapkDb import app.dapk.st.core.CoroutineDispatchers +import app.dapk.st.core.Preferences import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.domain.eventlog.EventLogPersistence @@ -22,7 +23,7 @@ import app.dapk.st.matrix.sync.SyncStore class StoreModule( private val database: DapkDb, private val databaseDropper: DatabaseDropper, - private val preferences: Preferences, + val preferences: Preferences, private val credentialPreferences: Preferences, private val errorTracker: ErrorTracker, private val coroutineDispatchers: CoroutineDispatchers, diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt index b4d5346..474df67 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/SyncTokenPreferences.kt @@ -1,7 +1,6 @@ package app.dapk.st.domain -import app.dapk.st.core.AppLogTag -import app.dapk.st.core.log +import app.dapk.st.core.Preferences import app.dapk.st.matrix.common.SyncToken import app.dapk.st.matrix.sync.SyncStore import app.dapk.st.matrix.sync.SyncStore.SyncKey diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/profile/ProfilePersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/profile/ProfilePersistence.kt index f133963..29cd267 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/profile/ProfilePersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/profile/ProfilePersistence.kt @@ -1,6 +1,6 @@ package app.dapk.st.domain.profile -import app.dapk.st.domain.Preferences +import app.dapk.st.core.Preferences import app.dapk.st.matrix.common.AvatarUrl import app.dapk.st.matrix.common.HomeServerUrl import app.dapk.st.matrix.common.UserId diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/push/PushTokenRegistrarPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/push/PushTokenRegistrarPreferences.kt index 38110ef..5facd7a 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/push/PushTokenRegistrarPreferences.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/push/PushTokenRegistrarPreferences.kt @@ -1,6 +1,6 @@ package app.dapk.st.domain.push -import app.dapk.st.domain.Preferences +import app.dapk.st.core.Preferences private const val SELECTION_KEY = "push_token_selection" diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt index 9faf065..57f852a 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeScreen.kt @@ -20,7 +20,6 @@ import app.dapk.st.profile.ProfileScreen @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen(homeViewModel: HomeViewModel) { - SmallTalkTheme { Surface(Modifier.fillMaxSize()) { LaunchedEffect(true) { homeViewModel.start() @@ -54,7 +53,6 @@ fun HomeScreen(homeViewModel: HomeViewModel) { } } } - } } @Composable diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt index 9b592e9..3697b26 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt @@ -46,11 +46,9 @@ class MessengerActivity : DapkActivity() { val payload = readPayload() log(AppLogTag.ERROR_NON_FATAL, payload) setContent { - SmallTalkTheme { Surface(Modifier.fillMaxSize()) { MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator) } - } } } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/roomsettings/RoomSettingsActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/roomsettings/RoomSettingsActivity.kt index 9508909..0844d46 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/roomsettings/RoomSettingsActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/roomsettings/RoomSettingsActivity.kt @@ -5,16 +5,14 @@ import android.content.Context import android.content.Intent import android.os.Bundle import android.os.Parcelable -import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier -import app.dapk.st.messenger.MessengerModule -import app.dapk.st.design.components.SmallTalkTheme import app.dapk.st.core.DapkActivity import app.dapk.st.core.module import app.dapk.st.core.viewModel import app.dapk.st.matrix.common.RoomId +import app.dapk.st.messenger.MessengerModule import kotlinx.parcelize.Parcelize class RoomSettingsActivity : DapkActivity() { @@ -33,10 +31,8 @@ class RoomSettingsActivity : DapkActivity() { super.onCreate(savedInstanceState) val payload = readPayload() setContent { - SmallTalkTheme { - Surface(Modifier.fillMaxSize()) { + Surface(Modifier.fillMaxSize()) { // MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator) - } } } } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt index fd3089a..b6086f8 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsActivity.kt @@ -1,7 +1,6 @@ package app.dapk.st.settings import android.os.Bundle -import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier @@ -9,7 +8,6 @@ import app.dapk.st.core.DapkActivity import app.dapk.st.core.module import app.dapk.st.core.resetModules import app.dapk.st.core.viewModel -import app.dapk.st.design.components.SmallTalkTheme class SettingsActivity : DapkActivity() { @@ -18,14 +16,12 @@ class SettingsActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - SmallTalkTheme { - Surface(Modifier.fillMaxSize()) { - SettingsScreen(settingsViewModel, onSignOut = { - resetModules() - navigator.navigate.toHome() - finish() - }, navigator) - } + Surface(Modifier.fillMaxSize()) { + SettingsScreen(settingsViewModel, onSignOut = { + resetModules() + navigator.navigate.toHome() + finish() + }, navigator) } } } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsItemFactory.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsItemFactory.kt index fbb7547..015c5cf 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsItemFactory.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsItemFactory.kt @@ -1,11 +1,13 @@ package app.dapk.st.settings import app.dapk.st.core.BuildMeta +import app.dapk.st.core.ThemeStore import app.dapk.st.push.PushTokenRegistrars internal class SettingsItemFactory( private val buildMeta: BuildMeta, private val pushTokenRegistrars: PushTokenRegistrars, + private val themeStore: ThemeStore, ) { suspend fun root() = listOf( @@ -13,6 +15,8 @@ internal class SettingsItemFactory( SettingItem.Text(SettingItem.Id.Encryption, "Encryption"), SettingItem.Text(SettingItem.Id.EventLog, "Event log"), SettingItem.Text(SettingItem.Id.PushProvider, "Push provider", pushTokenRegistrars.currentSelection().id), + SettingItem.Header("Theme"), + SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = themeStore.isMaterialYouEnabled()), SettingItem.Header("Data"), SettingItem.Text(SettingItem.Id.ClearCache, "Clear cache"), SettingItem.Header("Account"), @@ -22,4 +26,4 @@ internal class SettingsItemFactory( SettingItem.Text(SettingItem.Id.Ignored, "Version", buildMeta.versionName), ) -} \ No newline at end of file +} diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt index 0e93856..9240eb0 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsModule.kt @@ -4,6 +4,7 @@ import android.content.ContentResolver import app.dapk.st.core.BuildMeta import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.ProvidableModule +import app.dapk.st.core.ThemeStore import app.dapk.st.domain.StoreModule import app.dapk.st.matrix.crypto.CryptoService import app.dapk.st.matrix.sync.SyncService @@ -18,17 +19,21 @@ class SettingsModule( private val contentResolver: ContentResolver, private val buildMeta: BuildMeta, private val coroutineDispatchers: CoroutineDispatchers, + private val themeStore: ThemeStore, ) : ProvidableModule { - internal fun settingsViewModel() = SettingsViewModel( - storeModule.cacheCleaner(), - contentResolver, - cryptoService, - syncService, - UriFilenameResolver(contentResolver, coroutineDispatchers), - SettingsItemFactory(buildMeta, pushModule.pushTokenRegistrars()), - pushModule.pushTokenRegistrars(), - ) + internal fun settingsViewModel(): SettingsViewModel { + return SettingsViewModel( + storeModule.cacheCleaner(), + contentResolver, + cryptoService, + syncService, + UriFilenameResolver(contentResolver, coroutineDispatchers), + SettingsItemFactory(buildMeta, pushModule.pushTokenRegistrars(), themeStore), + pushModule.pushTokenRegistrars(), + themeStore, + ) + } internal fun eventLogViewModel(): EventLoggerViewModel { return EventLoggerViewModel(storeModule.eventLogStore()) diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt index 96e32ae..b10d9fa 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsScreen.kt @@ -5,6 +5,7 @@ import android.content.ClipboardManager import android.content.Context import android.content.Intent import android.widget.Toast +import androidx.activity.compose.LocalActivityResultRegistryOwner import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.clickable @@ -13,11 +14,11 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Visibility import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment @@ -37,10 +38,10 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.net.toUri import app.dapk.st.core.Lce -import app.dapk.st.core.LceWithProgress import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.components.Header +import app.dapk.st.core.getActivity import app.dapk.st.design.components.SettingsTextRow import app.dapk.st.design.components.Spider import app.dapk.st.design.components.SpiderPage @@ -154,7 +155,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, is ImportResult.Error -> { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - val message = when(val type = result.cause) { + val message = when (val type = result.cause) { ImportResult.Error.Type.NoKeysFound -> "No keys found in the file" ImportResult.Error.Type.UnexpectedDecryptionOutput -> "Unable to decrypt file, double check your passphrase" is ImportResult.Error.Type.Unknown -> "${type.cause::class.java.simpleName}: ${type.cause.message}" @@ -222,6 +223,9 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) { } is SettingItem.Header -> Header(item.label) + is SettingItem.Toggle -> Toggle(item, onToggle = { + onClick(item) + }) } } item { Spacer(Modifier.height(12.dp)) } @@ -238,6 +242,23 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) { } } +@Composable +private fun Toggle(item: SettingItem.Toggle, onToggle: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp, end = 24.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = item.content) + Switch( + checked = item.state, + onCheckedChange = { onToggle() } + ) + } +} + @Composable private fun Encryption(viewModel: SettingsViewModel, page: Page.Security) { Column { @@ -292,6 +313,9 @@ private fun SettingsViewModel.ObserveEvents(onSignOut: () -> Unit) { is OpenUrl -> { context.startActivity(Intent(Intent.ACTION_VIEW).apply { data = it.url.toUri() }) } + RecreateActivity -> { + context.getActivity()?.recreate() + } } } } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt index ca3b67a..ddf7c82 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsState.kt @@ -44,6 +44,7 @@ internal sealed interface SettingItem { data class Header(val label: String, override val id: Id = Id.Ignored) : SettingItem data class Text(override val id: Id, val content: String, val subtitle: String? = null) : SettingItem + data class Toggle(override val id: Id, val content: String, val state: Boolean) : SettingItem data class AccessToken(override val id: Id, val content: String, val accessToken: String) : SettingItem enum class Id { @@ -55,6 +56,7 @@ internal sealed interface SettingItem { Encryption, PrivacyPolicy, Ignored, + ToggleDynamicTheme, } } @@ -65,5 +67,6 @@ sealed interface SettingsEvent { object OpenEventLog : SettingsEvent data class OpenUrl(val url: String) : SettingsEvent data class CopyToClipboard(val message: String, val content: String) : SettingsEvent + object RecreateActivity : SettingsEvent } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt index 56dd839..eed096e 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/SettingsViewModel.kt @@ -4,6 +4,7 @@ import android.content.ContentResolver import android.net.Uri import androidx.lifecycle.viewModelScope import app.dapk.st.core.Lce +import app.dapk.st.core.ThemeStore import app.dapk.st.design.components.SpiderPage import app.dapk.st.domain.StoreCleaner import app.dapk.st.matrix.crypto.CryptoService @@ -30,6 +31,7 @@ internal class SettingsViewModel( private val uriFilenameResolver: UriFilenameResolver, private val settingsItemFactory: SettingsItemFactory, private val pushTokenRegistrars: PushTokenRegistrars, + private val themeStore: ThemeStore, factory: MutableStateFactory = defaultStateFactory(), ) : DapkViewModel( initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading()))), @@ -98,9 +100,17 @@ internal class SettingsViewModel( Ignored -> { // do nothing } + ToggleDynamicTheme -> { + viewModelScope.launch { + themeStore.storeMaterialYouEnabled(!themeStore.isMaterialYouEnabled()) + start() + _events.emit(RecreateActivity) + } + } } } + fun fetchPushProviders() { updatePageState { copy(options = Lce.Loading()) } viewModelScope.launch { diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogActivity.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogActivity.kt index 998d72a..33ceefa 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogActivity.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogActivity.kt @@ -1,7 +1,6 @@ package app.dapk.st.settings.eventlogger import android.os.Bundle -import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier @@ -9,7 +8,6 @@ import app.dapk.st.core.DapkActivity import app.dapk.st.core.module import app.dapk.st.core.viewModel import app.dapk.st.settings.SettingsModule -import app.dapk.st.design.components.SmallTalkTheme class EventLogActivity : DapkActivity() { @@ -18,10 +16,8 @@ class EventLogActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - SmallTalkTheme { - Surface(Modifier.fillMaxSize()) { - EventLogScreen(viewModel) - } + Surface(Modifier.fillMaxSize()) { + EventLogScreen(viewModel) } } } diff --git a/features/settings/src/test/kotlin/app/dapk/st/settings/FakeThemeStore.kt b/features/settings/src/test/kotlin/app/dapk/st/settings/FakeThemeStore.kt new file mode 100644 index 0000000..f41b68d --- /dev/null +++ b/features/settings/src/test/kotlin/app/dapk/st/settings/FakeThemeStore.kt @@ -0,0 +1,12 @@ +package app.dapk.st.settings + +import app.dapk.st.core.ThemeStore +import io.mockk.every +import io.mockk.mockk +import test.delegateReturn + +class FakeThemeStore { + val instance = mockk() + + fun givenMaterialYouIsEnabled() = every { instance.isMaterialYouEnabled() }.delegateReturn() +} \ No newline at end of file diff --git a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsItemFactoryTest.kt b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsItemFactoryTest.kt index d8885d9..95ffa3c 100644 --- a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsItemFactoryTest.kt +++ b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsItemFactoryTest.kt @@ -6,7 +6,6 @@ import app.dapk.st.push.Registrar import internalfixture.aSettingHeaderItem import internalfixture.aSettingTextItem import io.mockk.coEvery -import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo @@ -14,17 +13,20 @@ import org.junit.Test import test.delegateReturn private val A_SELECTION = Registrar("A_SELECTION") +private const val ENABLED_MATERIAL_YOU = true class SettingsItemFactoryTest { private val buildMeta = BuildMeta(versionName = "a-version-name", versionCode = 100) private val fakePushTokenRegistrars = FakePushRegistrars() + private val fakeThemeStore = FakeThemeStore() - private val settingsItemFactory = SettingsItemFactory(buildMeta, fakePushTokenRegistrars.instance) + private val settingsItemFactory = SettingsItemFactory(buildMeta, fakePushTokenRegistrars.instance, fakeThemeStore.instance) @Test fun `when creating root items, then is expected`() = runTest { fakePushTokenRegistrars.givenCurrentSelection().returns(A_SELECTION) + fakeThemeStore.givenMaterialYouIsEnabled().returns(ENABLED_MATERIAL_YOU) val result = settingsItemFactory.root() @@ -33,6 +35,8 @@ class SettingsItemFactoryTest { aSettingTextItem(SettingItem.Id.Encryption, "Encryption"), aSettingTextItem(SettingItem.Id.EventLog, "Event log"), aSettingTextItem(SettingItem.Id.PushProvider, "Push provider", A_SELECTION.id), + SettingItem.Header("Theme"), + SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = ENABLED_MATERIAL_YOU), aSettingHeaderItem("Data"), aSettingTextItem(SettingItem.Id.ClearCache, "Clear cache"), aSettingHeaderItem("Account"), diff --git a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsViewModelTest.kt b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsViewModelTest.kt index 0a2067c..6a6481e 100644 --- a/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsViewModelTest.kt +++ b/features/settings/src/test/kotlin/app/dapk/st/settings/SettingsViewModelTest.kt @@ -40,6 +40,7 @@ internal class SettingsViewModelTest { private val fakeUriFilenameResolver = FakeUriFilenameResolver() private val fakePushTokenRegistrars = FakePushRegistrars() private val fakeSettingsItemFactory = FakeSettingsItemFactory() + private val fakeThemeStore = FakeThemeStore() private val viewModel = SettingsViewModel( fakeStoreCleaner, @@ -49,6 +50,7 @@ internal class SettingsViewModelTest { fakeUriFilenameResolver.instance, fakeSettingsItemFactory.instance, fakePushTokenRegistrars.instance, + fakeThemeStore.instance, runViewModelTest.testMutableStateFactory(), ) diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt index 7a9be90..c91a4ba 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt @@ -4,14 +4,12 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.os.Parcelable -import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import app.dapk.st.core.DapkActivity import app.dapk.st.core.module import app.dapk.st.core.viewModel -import app.dapk.st.design.components.SmallTalkTheme class ShareEntryActivity : DapkActivity() { @@ -21,10 +19,8 @@ class ShareEntryActivity : DapkActivity() { super.onCreate(savedInstanceState) val urisToShare = intent.readSendUrisOrNull() ?: throw IllegalArgumentException("Expected deeplink uris but they were missing") setContent { - SmallTalkTheme { - Surface(Modifier.fillMaxSize()) { - ShareEntryScreen(navigator, viewModel) - } + Surface(Modifier.fillMaxSize()) { + ShareEntryScreen(navigator, viewModel) } } viewModel.withUris(urisToShare) diff --git a/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationActivity.kt b/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationActivity.kt index b568906..1ae4335 100644 --- a/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationActivity.kt +++ b/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationActivity.kt @@ -1,11 +1,9 @@ package app.dapk.st.verification import android.os.Bundle -import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier -import app.dapk.st.design.components.SmallTalkTheme import app.dapk.st.core.DapkActivity import app.dapk.st.core.module import app.dapk.st.core.viewModel @@ -17,10 +15,8 @@ class VerificationActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - SmallTalkTheme { - Surface(Modifier.fillMaxSize()) { - VerificationScreen(verificationViewModel) - } + Surface(Modifier.fillMaxSize()) { + VerificationScreen(verificationViewModel) } } } diff --git a/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationScreen.kt b/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationScreen.kt index b9c40a4..7f397de 100644 --- a/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationScreen.kt +++ b/features/verification/src/main/kotlin/app/dapk/st/verification/VerificationScreen.kt @@ -2,14 +2,12 @@ package app.dapk.st.verification import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.material.Button -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.Text import androidx.compose.runtime.Composable @Composable fun VerificationScreen(viewModel: VerificationViewModel) { - - Column { Text("Verification request") diff --git a/test-harness/src/test/kotlin/test/impl/InMemoryPreferences.kt b/test-harness/src/test/kotlin/test/impl/InMemoryPreferences.kt index f24a0b4..9818819 100644 --- a/test-harness/src/test/kotlin/test/impl/InMemoryPreferences.kt +++ b/test-harness/src/test/kotlin/test/impl/InMemoryPreferences.kt @@ -1,6 +1,6 @@ package test.impl -import app.dapk.st.domain.Preferences +import app.dapk.st.core.Preferences import test.unit class InMemoryPreferences : Preferences {