From 31318037a75785e2ba95e7660e13fcef234457e2 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 13 Sep 2022 21:15:41 +0100 Subject: [PATCH 1/5] providing a caching preferences abstraction to reading from an in memory value --- .../kotlin/app/dapk/st/graph/AppModule.kt | 2 +- .../kotlin/app/dapk/st/core/Preferences.kt | 2 ++ .../app/dapk/st/core/CoreAndroidModule.kt | 7 ++--- .../kotlin/app/dapk/st/core/DapkActivity.kt | 11 +++++--- .../kotlin/app/dapk/st/core/ThemeStore.kt | 17 +++--------- .../kotlin/app/dapk/st/domain/StoreModule.kt | 6 +++++ .../st/domain/eventlog/EventLogPersistence.kt | 3 ++- .../domain/preference/CachingPreferences.kt | 26 +++++++++++++++++++ .../st/domain/preference/PropertyCache.kt | 16 ++++++++++++ .../dapk/st/settings/SettingsItemFactory.kt | 2 +- 10 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/preference/CachingPreferences.kt create mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/preference/PropertyCache.kt 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 4dc6890..8f79116 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -126,7 +126,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) { attachments ) }, - unsafeLazy { storeModule.value.preferences } + unsafeLazy { storeModule.value.cachingPreferences }, ) val featureModules = FeatureModules( diff --git a/core/src/main/kotlin/app/dapk/st/core/Preferences.kt b/core/src/main/kotlin/app/dapk/st/core/Preferences.kt index 4d1cc3f..6a27901 100644 --- a/core/src/main/kotlin/app/dapk/st/core/Preferences.kt +++ b/core/src/main/kotlin/app/dapk/st/core/Preferences.kt @@ -8,5 +8,7 @@ interface Preferences { suspend fun remove(key: String) } +interface CachedPreferences : Preferences + 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/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 b84e2c7..f9bc464 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,17 +1,14 @@ package app.dapk.st.core -import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.navigator.IntentFactory class CoreAndroidModule( private val intentFactory: IntentFactory, - private val preferences: Lazy, + private val preferences: Lazy, ) : ProvidableModule { fun intentFactory() = intentFactory - private val themeStore by unsafeLazy { ThemeStore(preferences.value) } - - fun themeStore() = themeStore + fun themeStore() = ThemeStore(preferences.value) } \ 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 fbcf0ed..4a9cc24 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 @@ -8,10 +8,13 @@ import androidx.activity.ComponentActivity import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.lifecycle.lifecycleScope 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 kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume import androidx.activity.compose.setContent as _setContent @@ -29,7 +32,7 @@ abstract class DapkActivity : ComponentActivity(), EffectScope { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - this.themeConfig = ThemeConfig(themeStore.isMaterialYouEnabled()) + this.themeConfig = runBlocking { ThemeConfig(themeStore.isMaterialYouEnabled()) } window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); @@ -45,8 +48,10 @@ abstract class DapkActivity : ComponentActivity(), EffectScope { override fun onResume() { super.onResume() - if (themeConfig.useDynamicTheme != themeStore.isMaterialYouEnabled()) { - recreate() + lifecycleScope.launch { + if (themeConfig.useDynamicTheme != themeStore.isMaterialYouEnabled()) { + recreate() + } } } 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 index ff3d2f5..cc46cac 100644 --- 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 @@ -1,26 +1,15 @@ 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 val preferences: CachedPreferences ) { - 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 isMaterialYouEnabled() = preferences.readBoolean(KEY_MATERIAL_YOU_ENABLED) ?: false.also { storeMaterialYouEnabled(false) } suspend fun storeMaterialYouEnabled(isEnabled: Boolean) { - _isMaterialYouEnabled = isEnabled preferences.store(KEY_MATERIAL_YOU_ENABLED, isEnabled) } -} \ No newline at end of file +} 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 874b8f5..94f978e 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 @@ -7,6 +7,8 @@ import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.domain.eventlog.EventLogPersistence import app.dapk.st.domain.localecho.LocalEchoPersistence +import app.dapk.st.domain.preference.CachingPreferences +import app.dapk.st.domain.preference.PropertyCache import app.dapk.st.domain.profile.ProfilePersistence import app.dapk.st.domain.push.PushTokenRegistrarPreferences import app.dapk.st.domain.sync.OverviewPersistence @@ -36,6 +38,9 @@ class StoreModule( fun filterStore(): FilterStore = FilterPreferences(preferences) val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) } + private val cache = PropertyCache() + val cachingPreferences = CachingPreferences(cache, preferences) + fun pushStore() = PushTokenRegistrarPreferences(preferences) fun applicationStore() = ApplicationPreferences(preferences) @@ -60,4 +65,5 @@ class StoreModule( fun memberStore(): MemberStore { return MemberPersistence(database, coroutineDispatchers) } + } diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/EventLogPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/EventLogPersistence.kt index ff65dca..4cd41ec 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/EventLogPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/EventLogPersistence.kt @@ -1,8 +1,8 @@ package app.dapk.st.domain.eventlog +import app.dapk.db.DapkDb import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.withIoContext -import app.dapk.db.DapkDb import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList import kotlinx.coroutines.flow.Flow @@ -42,6 +42,7 @@ class EventLogPersistence( ) } } + else -> database.eventLoggerQueries.selectLatestByLogFiltered(logKey, filter) .asFlow() .mapToList(context = coroutineDispatchers.io) diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/preference/CachingPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/preference/CachingPreferences.kt new file mode 100644 index 0000000..0b42a40 --- /dev/null +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/preference/CachingPreferences.kt @@ -0,0 +1,26 @@ +package app.dapk.st.domain.preference + +import app.dapk.st.core.CachedPreferences +import app.dapk.st.core.Preferences + +class CachingPreferences(private val cache: PropertyCache, private val preferences: Preferences) : CachedPreferences { + + override suspend fun store(key: String, value: String) { + cache.setValue(key, value) + preferences.store(key, value) + } + + override suspend fun readString(key: String): String? { + return cache.getValue(key) ?: preferences.readString(key)?.also { + cache.setValue(key, it) + } + } + + override suspend fun remove(key: String) { + preferences.remove(key) + } + + override suspend fun clear() { + preferences.clear() + } +} \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/preference/PropertyCache.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/preference/PropertyCache.kt new file mode 100644 index 0000000..41adfda --- /dev/null +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/preference/PropertyCache.kt @@ -0,0 +1,16 @@ +package app.dapk.st.domain.preference + +@Suppress("UNCHECKED_CAST") +class PropertyCache { + + private val map = mutableMapOf() + + fun getValue(key: String): T? { + return map[key] as? T? + } + + fun setValue(key: String, value: Any) { + map[key] = value + } + +} \ No newline at end of file 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 a36e2f1..0a87f3d 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 @@ -19,7 +19,7 @@ internal class SettingsItemFactory( SettingItem.Text(SettingItem.Id.PushProvider, "Push provider", pushTokenRegistrars.currentSelection().id) ) - private fun theme() = listOfNotNull( + private suspend fun theme() = listOfNotNull( SettingItem.Header("Theme"), SettingItem.Toggle(SettingItem.Id.ToggleDynamicTheme, "Enable Material You", state = themeStore.isMaterialYouEnabled()).takeIf { deviceMeta.isAtLeastS() From 43e53261b658bd030489f81ca361aa641ba39a9b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 13 Sep 2022 21:54:39 +0100 Subject: [PATCH 2/5] adding ability to opt in to on device logs, disabled by default --- .../app/dapk/st/SmallTalkApplication.kt | 7 ++- .../kotlin/app/dapk/st/graph/AppModule.kt | 1 + .../kotlin/app/dapk/st/core/Preferences.kt | 8 ++- .../app/dapk/st/design/components/itemRow.kt | 25 ++++++-- .../kotlin/app/dapk/st/core/ThemeStore.kt | 2 +- .../kotlin/app/dapk/st/domain/StoreModule.kt | 5 +- .../dapk/st/domain/eventlog/LoggingStore.kt | 17 ++++++ .../domain/preference/CachingPreferences.kt | 5 +- .../dapk/st/settings/SettingsItemFactory.kt | 19 +++++- .../app/dapk/st/settings/SettingsModule.kt | 5 +- .../app/dapk/st/settings/SettingsScreen.kt | 10 ++-- .../app/dapk/st/settings/SettingsState.kt | 3 +- .../app/dapk/st/settings/SettingsViewModel.kt | 60 ++++++++++--------- .../app/dapk/st/settings/FakeLoggingStore.kt | 12 ++++ .../app/dapk/st/settings/FakeThemeStore.kt | 3 +- .../st/settings/SettingsItemFactoryTest.kt | 15 ++++- .../dapk/st/settings/SettingsViewModelTest.kt | 2 + .../internalfixture/SettingItemFixture.kt | 5 +- 18 files changed, 149 insertions(+), 55 deletions(-) create mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/LoggingStore.kt create mode 100644 features/settings/src/test/kotlin/app/dapk/st/settings/FakeLoggingStore.kt diff --git a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt index 4997d90..8d906ef 100644 --- a/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt +++ b/app/src/main/kotlin/app/dapk/st/SmallTalkApplication.kt @@ -42,10 +42,15 @@ class SmallTalkApplication : Application(), ModuleProvider { val notificationsModule = featureModules.notificationsModule val storeModule = appModule.storeModule.value val eventLogStore = storeModule.eventLogStore() + val loggingStore = storeModule.loggingStore() val logger: (String, String) -> Unit = { tag, message -> Log.e(tag, message) - applicationScope.launch { eventLogStore.insert(tag, message) } + applicationScope.launch { + if (loggingStore.isEnabled()) { + eventLogStore.insert(tag, message) + } + } } attachAppLogger(logger) _appLogger = logger 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 8f79116..7c58486 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -205,6 +205,7 @@ internal class FeatureModules internal constructor( deviceMeta, coroutineDispatchers, coreAndroidModule.themeStore(), + storeModule.value.loggingStore(), ) } 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 index 6a27901..bac20a1 100644 --- a/core/src/main/kotlin/app/dapk/st/core/Preferences.kt +++ b/core/src/main/kotlin/app/dapk/st/core/Preferences.kt @@ -8,7 +8,13 @@ interface Preferences { suspend fun remove(key: String) } -interface CachedPreferences : Preferences +interface CachedPreferences : Preferences { + suspend fun readString(key: String, defaultValue: String): String +} + +suspend fun CachedPreferences.readBoolean(key: String, defaultValue: Boolean) = this + .readString(key, defaultValue.toString()) + .toBooleanStrict() 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/itemRow.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt index afe2169..7429e56 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt @@ -8,12 +8,20 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun TextRow(title: String, content: String? = null, includeDivider: Boolean = true, onClick: (() -> Unit)? = null, body: @Composable () -> Unit = {}) { +fun TextRow( + title: String, + content: String? = null, + includeDivider: Boolean = true, + onClick: (() -> Unit)? = null, + enabled: Boolean = true, + body: @Composable () -> Unit = {} +) { val modifier = Modifier.padding(horizontal = 24.dp) Column( Modifier @@ -21,14 +29,19 @@ fun TextRow(title: String, content: String? = null, includeDivider: Boolean = tr .clickable(enabled = onClick != null) { onClick?.invoke() }) { Spacer(modifier = Modifier.height(24.dp)) Column(modifier) { + val textModifier = when (enabled) { + true -> Modifier + false -> Modifier.alpha(0.5f) + } when (content) { null -> { - Text(text = title, fontSize = 18.sp) + Text(text = title, fontSize = 18.sp, modifier = textModifier) } + else -> { - Text(text = title, fontSize = 12.sp) + Text(text = title, fontSize = 12.sp, modifier = textModifier) Spacer(modifier = Modifier.height(2.dp)) - Text(text = content, fontSize = 18.sp) + Text(text = content, fontSize = 18.sp, modifier = textModifier) } } body() @@ -56,6 +69,6 @@ fun IconRow(icon: ImageVector, title: String, onClick: (() -> Unit)? = null) { } @Composable -fun SettingsTextRow(title: String, subtitle: String?, onClick: (() -> Unit)?) { - TextRow(title = title, subtitle, includeDivider = false, onClick) +fun SettingsTextRow(title: String, subtitle: String?, onClick: (() -> Unit)?, enabled: Boolean) { + TextRow(title = title, subtitle, includeDivider = false, onClick, enabled = enabled) } 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 index cc46cac..687a072 100644 --- 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 @@ -6,7 +6,7 @@ class ThemeStore( private val preferences: CachedPreferences ) { - suspend fun isMaterialYouEnabled() = preferences.readBoolean(KEY_MATERIAL_YOU_ENABLED) ?: false.also { storeMaterialYouEnabled(false) } + suspend fun isMaterialYouEnabled() = preferences.readBoolean(KEY_MATERIAL_YOU_ENABLED, defaultValue = false) suspend fun storeMaterialYouEnabled(isEnabled: Boolean) { preferences.store(KEY_MATERIAL_YOU_ENABLED, isEnabled) 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 94f978e..427ac9c 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 @@ -6,6 +6,7 @@ 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 +import app.dapk.st.domain.eventlog.LoggingStore import app.dapk.st.domain.localecho.LocalEchoPersistence import app.dapk.st.domain.preference.CachingPreferences import app.dapk.st.domain.preference.PropertyCache @@ -39,7 +40,7 @@ class StoreModule( val localEchoStore: LocalEchoStore by unsafeLazy { LocalEchoPersistence(errorTracker, database) } private val cache = PropertyCache() - val cachingPreferences = CachingPreferences(cache, preferences) + val cachingPreferences = CachingPreferences(cache, preferences) fun pushStore() = PushTokenRegistrarPreferences(preferences) @@ -62,6 +63,8 @@ class StoreModule( return EventLogPersistence(database, coroutineDispatchers) } + fun loggingStore(): LoggingStore = LoggingStore(cachingPreferences) + fun memberStore(): MemberStore { return MemberPersistence(database, coroutineDispatchers) } diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/LoggingStore.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/LoggingStore.kt new file mode 100644 index 0000000..2b9299a --- /dev/null +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/LoggingStore.kt @@ -0,0 +1,17 @@ +package app.dapk.st.domain.eventlog + +import app.dapk.st.core.CachedPreferences +import app.dapk.st.core.readBoolean +import app.dapk.st.core.store + +private const val KEY_LOGGING_ENABLED = "key_logging_enabled" + +class LoggingStore(private val cachedPreferences: CachedPreferences) { + + suspend fun isEnabled() = cachedPreferences.readBoolean(KEY_LOGGING_ENABLED, defaultValue = false) + + suspend fun setEnabled(isEnabled: Boolean) { + cachedPreferences.store(KEY_LOGGING_ENABLED, isEnabled) + } + +} \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/preference/CachingPreferences.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/preference/CachingPreferences.kt index 0b42a40..1d891b4 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/preference/CachingPreferences.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/preference/CachingPreferences.kt @@ -4,7 +4,6 @@ import app.dapk.st.core.CachedPreferences import app.dapk.st.core.Preferences class CachingPreferences(private val cache: PropertyCache, private val preferences: Preferences) : CachedPreferences { - override suspend fun store(key: String, value: String) { cache.setValue(key, value) preferences.store(key, value) @@ -16,6 +15,10 @@ class CachingPreferences(private val cache: PropertyCache, private val preferenc } } + override suspend fun readString(key: String, defaultValue: String): String { + return readString(key) ?: (defaultValue.also { cache.setValue(key, it) }) + } + override suspend fun remove(key: String) { preferences.remove(key) } 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 0a87f3d..8919eb9 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,6 +1,10 @@ package app.dapk.st.settings -import app.dapk.st.core.* +import app.dapk.st.core.BuildMeta +import app.dapk.st.core.DeviceMeta +import app.dapk.st.core.ThemeStore +import app.dapk.st.core.isAtLeastS +import app.dapk.st.domain.eventlog.LoggingStore import app.dapk.st.push.PushTokenRegistrars internal class SettingsItemFactory( @@ -8,14 +12,14 @@ internal class SettingsItemFactory( private val deviceMeta: DeviceMeta, private val pushTokenRegistrars: PushTokenRegistrars, private val themeStore: ThemeStore, + private val loggingStore: LoggingStore, ) { - suspend fun root() = general() + theme() + data() + account() + about() + suspend fun root() = general() + theme() + data() + account() + advanced() + about() private suspend fun general() = listOf( SettingItem.Header("General"), SettingItem.Text(SettingItem.Id.Encryption, "Encryption"), - SettingItem.Text(SettingItem.Id.EventLog, "Event log"), SettingItem.Text(SettingItem.Id.PushProvider, "Push provider", pushTokenRegistrars.currentSelection().id) ) @@ -36,6 +40,15 @@ internal class SettingsItemFactory( SettingItem.Text(SettingItem.Id.SignOut, "Sign out"), ) + private suspend fun advanced(): List { + val loggingIsEnabled = loggingStore.isEnabled() + return listOf( + SettingItem.Header("Advanced"), + SettingItem.Toggle(SettingItem.Id.ToggleEnableLogs, "Enable local logging", state = loggingIsEnabled), + SettingItem.Text(SettingItem.Id.EventLog, "Event log", enabled = loggingIsEnabled), + ) + } + private fun about() = listOf( SettingItem.Header("About"), SettingItem.Text(SettingItem.Id.PrivacyPolicy, "Privacy policy"), 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 4e24052..380cbdd 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 @@ -3,6 +3,7 @@ package app.dapk.st.settings import android.content.ContentResolver import app.dapk.st.core.* import app.dapk.st.domain.StoreModule +import app.dapk.st.domain.eventlog.LoggingStore import app.dapk.st.matrix.crypto.CryptoService import app.dapk.st.matrix.sync.SyncService import app.dapk.st.push.PushModule @@ -18,6 +19,7 @@ class SettingsModule( private val deviceMeta: DeviceMeta, private val coroutineDispatchers: CoroutineDispatchers, private val themeStore: ThemeStore, + private val loggingStore: LoggingStore, ) : ProvidableModule { internal fun settingsViewModel(): SettingsViewModel { @@ -27,9 +29,10 @@ class SettingsModule( cryptoService, syncService, UriFilenameResolver(contentResolver, coroutineDispatchers), - SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore), + SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore), pushModule.pushTokenRegistrars(), themeStore, + loggingStore, ) } 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 b10d9fa..5df70ed 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,7 +5,6 @@ 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 @@ -197,11 +196,11 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) { items(content.value) { item -> when (item) { is SettingItem.Text -> { - val itemOnClick = onClick.takeIf { item.id != SettingItem.Id.Ignored }?.let { - { it.invoke(item) } - } + val itemOnClick = onClick.takeIf { + item.id != SettingItem.Id.Ignored && item.enabled + }?.let { { it.invoke(item) } } - SettingsTextRow(item.content, item.subtitle, itemOnClick) + SettingsTextRow(item.content, item.subtitle, itemOnClick, enabled = item.enabled) } is SettingItem.AccessToken -> { @@ -313,6 +312,7 @@ 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 ddf7c82..5dd8144 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 @@ -43,7 +43,7 @@ internal sealed interface SettingItem { val id: Id 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 Text(override val id: Id, val content: String, val subtitle: String? = null, val enabled: Boolean = true) : 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 @@ -57,6 +57,7 @@ internal sealed interface SettingItem { PrivacyPolicy, Ignored, ToggleDynamicTheme, + ToggleEnableLogs, } } 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 eed096e..a952c75 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 @@ -7,6 +7,7 @@ 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.domain.eventlog.LoggingStore import app.dapk.st.matrix.crypto.CryptoService import app.dapk.st.matrix.crypto.ImportResult import app.dapk.st.matrix.sync.SyncService @@ -32,6 +33,7 @@ internal class SettingsViewModel( private val settingsItemFactory: SettingsItemFactory, private val pushTokenRegistrars: PushTokenRegistrars, private val themeStore: ThemeStore, + private val loggingStore: LoggingStore, factory: MutableStateFactory = defaultStateFactory(), ) : DapkViewModel( initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading()))), @@ -52,31 +54,23 @@ internal class SettingsViewModel( fun onClick(item: SettingItem) { when (item.id) { - SignOut -> { - viewModelScope.launch { - cacheCleaner.cleanCache(removeCredentials = true) - _events.emit(SignedOut) - } + SignOut -> viewModelScope.launch { + cacheCleaner.cleanCache(removeCredentials = true) + _events.emit(SignedOut) } - AccessToken -> { - viewModelScope.launch { - require(item is SettingItem.AccessToken) - _events.emit(CopyToClipboard("Token copied", item.accessToken)) - } + AccessToken -> viewModelScope.launch { + require(item is SettingItem.AccessToken) + _events.emit(CopyToClipboard("Token copied", item.accessToken)) } - ClearCache -> { - viewModelScope.launch { - cacheCleaner.cleanCache(removeCredentials = false) - _events.emit(Toast(message = "Cache deleted")) - } + ClearCache -> viewModelScope.launch { + cacheCleaner.cleanCache(removeCredentials = false) + _events.emit(Toast(message = "Cache deleted")) } - EventLog -> { - viewModelScope.launch { - _events.emit(OpenEventLog) - } + EventLog -> viewModelScope.launch { + _events.emit(OpenEventLog) } Encryption -> { @@ -85,10 +79,8 @@ internal class SettingsViewModel( } } - PrivacyPolicy -> { - viewModelScope.launch { - _events.emit(OpenUrl(PRIVACY_POLICY_URL)) - } + PrivacyPolicy -> viewModelScope.launch { + _events.emit(OpenUrl(PRIVACY_POLICY_URL)) } PushProvider -> { @@ -100,16 +92,24 @@ internal class SettingsViewModel( Ignored -> { // do nothing } - ToggleDynamicTheme -> { - viewModelScope.launch { - themeStore.storeMaterialYouEnabled(!themeStore.isMaterialYouEnabled()) - start() - _events.emit(RecreateActivity) - } + + ToggleDynamicTheme -> viewModelScope.launch { + themeStore.storeMaterialYouEnabled(!themeStore.isMaterialYouEnabled()) + refreshRoot() + _events.emit(RecreateActivity) + + } + + ToggleEnableLogs -> viewModelScope.launch { + loggingStore.setEnabled(!loggingStore.isEnabled()) + refreshRoot() } } } + private fun refreshRoot() { + start() + } fun fetchPushProviders() { updatePageState { copy(options = Lce.Loading()) } @@ -146,9 +146,11 @@ internal class SettingsViewModel( is ImportResult.Error -> { // do nothing } + is ImportResult.Update -> { // do nothing } + is ImportResult.Success -> { syncService.forceManualRefresh(it.roomIds.toList()) } diff --git a/features/settings/src/test/kotlin/app/dapk/st/settings/FakeLoggingStore.kt b/features/settings/src/test/kotlin/app/dapk/st/settings/FakeLoggingStore.kt new file mode 100644 index 0000000..6a3595f --- /dev/null +++ b/features/settings/src/test/kotlin/app/dapk/st/settings/FakeLoggingStore.kt @@ -0,0 +1,12 @@ +package app.dapk.st.settings + +import app.dapk.st.domain.eventlog.LoggingStore +import io.mockk.coEvery +import io.mockk.mockk +import test.delegateReturn + +class FakeLoggingStore { + val instance = mockk() + + fun givenLoggingIsEnabled() = coEvery { instance.isEnabled() }.delegateReturn() +} \ No newline at end of file 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 index f41b68d..9fe4b7e 100644 --- a/features/settings/src/test/kotlin/app/dapk/st/settings/FakeThemeStore.kt +++ b/features/settings/src/test/kotlin/app/dapk/st/settings/FakeThemeStore.kt @@ -1,6 +1,7 @@ package app.dapk.st.settings import app.dapk.st.core.ThemeStore +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import test.delegateReturn @@ -8,5 +9,5 @@ import test.delegateReturn class FakeThemeStore { val instance = mockk() - fun givenMaterialYouIsEnabled() = every { instance.isMaterialYouEnabled() }.delegateReturn() + fun givenMaterialYouIsEnabled() = coEvery { 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 b34c362..7216731 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 @@ -15,6 +15,7 @@ import test.delegateReturn private val A_SELECTION = Registrar("A_SELECTION") private const val ENABLED_MATERIAL_YOU = true +private const val DISABLED_LOGGING = false class SettingsItemFactoryTest { @@ -22,20 +23,27 @@ class SettingsItemFactoryTest { private val deviceMeta = DeviceMeta(apiVersion = 31) private val fakePushTokenRegistrars = FakePushRegistrars() private val fakeThemeStore = FakeThemeStore() + private val fakeLoggingStore = FakeLoggingStore() - private val settingsItemFactory = SettingsItemFactory(buildMeta, deviceMeta, fakePushTokenRegistrars.instance, fakeThemeStore.instance) + private val settingsItemFactory = SettingsItemFactory( + buildMeta, + deviceMeta, + fakePushTokenRegistrars.instance, + fakeThemeStore.instance, + fakeLoggingStore.instance + ) @Test fun `when creating root items, then is expected`() = runTest { fakePushTokenRegistrars.givenCurrentSelection().returns(A_SELECTION) fakeThemeStore.givenMaterialYouIsEnabled().returns(ENABLED_MATERIAL_YOU) + fakeLoggingStore.givenLoggingIsEnabled().returns(DISABLED_LOGGING) val result = settingsItemFactory.root() result shouldBeEqualTo listOf( aSettingHeaderItem("General"), 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), @@ -43,6 +51,9 @@ class SettingsItemFactoryTest { aSettingTextItem(SettingItem.Id.ClearCache, "Clear cache"), aSettingHeaderItem("Account"), aSettingTextItem(SettingItem.Id.SignOut, "Sign out"), + aSettingHeaderItem("Advanced"), + SettingItem.Toggle(SettingItem.Id.ToggleEnableLogs, "Enable local logging", state = DISABLED_LOGGING), + aSettingTextItem(SettingItem.Id.EventLog, "Event log", enabled = DISABLED_LOGGING), aSettingHeaderItem("About"), aSettingTextItem(SettingItem.Id.PrivacyPolicy, "Privacy policy"), aSettingTextItem(SettingItem.Id.Ignored, "Version", buildMeta.versionName), 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 6a6481e..374849c 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 @@ -41,6 +41,7 @@ internal class SettingsViewModelTest { private val fakePushTokenRegistrars = FakePushRegistrars() private val fakeSettingsItemFactory = FakeSettingsItemFactory() private val fakeThemeStore = FakeThemeStore() + private val fakeLoggingStore = FakeLoggingStore() private val viewModel = SettingsViewModel( fakeStoreCleaner, @@ -51,6 +52,7 @@ internal class SettingsViewModelTest { fakeSettingsItemFactory.instance, fakePushTokenRegistrars.instance, fakeThemeStore.instance, + fakeLoggingStore.instance, runViewModelTest.testMutableStateFactory(), ) diff --git a/features/settings/src/test/kotlin/internalfixture/SettingItemFixture.kt b/features/settings/src/test/kotlin/internalfixture/SettingItemFixture.kt index 5625fe7..3d01eb3 100644 --- a/features/settings/src/test/kotlin/internalfixture/SettingItemFixture.kt +++ b/features/settings/src/test/kotlin/internalfixture/SettingItemFixture.kt @@ -5,8 +5,9 @@ import app.dapk.st.settings.SettingItem internal fun aSettingTextItem( id: SettingItem.Id = SettingItem.Id.Ignored, content: String = "text-content", - subtitle: String? = null -) = SettingItem.Text(id, content, subtitle) + subtitle: String? = null, + enabled: Boolean = true, +) = SettingItem.Text(id, content, subtitle, enabled) internal fun aSettingHeaderItem( label: String = "header-label", From 0e4f6d6ac26530225e43892d4182208e2d43918a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 13 Sep 2022 22:06:37 +0100 Subject: [PATCH 3/5] lifting hidden/private read receipt option to the view model --- .../kotlin/app/dapk/st/messenger/MessengerViewModel.kt | 2 +- .../app/dapk/st/messenger/MessengerViewModelTest.kt | 2 +- .../main/kotlin/app/dapk/st/matrix/room/RoomService.kt | 2 +- .../dapk/st/matrix/room/internal/DefaultRoomService.kt | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index 4ba6163..95dc503 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -101,7 +101,7 @@ internal class MessengerViewModel( private fun CoroutineScope.updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState): Deferred { return async { runCatching { - roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent) + roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = true) roomStore.markRead(state.roomState.roomOverview.roomId) } } diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt index 2ce7567..5162a2a 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt @@ -69,7 +69,7 @@ class MessengerViewModelTest { @Test fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest { fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) } - fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID) } + fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID, isPrivate = true) } val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID) fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state)) diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt index 56ba0a1..a51817b 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/RoomService.kt @@ -15,7 +15,7 @@ private val SERVICE_KEY = RoomService::class interface RoomService : MatrixService { suspend fun joinedMembers(roomId: RoomId): List - suspend fun markFullyRead(roomId: RoomId, eventId: EventId) + suspend fun markFullyRead(roomId: RoomId, eventId: EventId, isPrivate: Boolean) suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember? suspend fun findMembers(roomId: RoomId, userIds: List): List diff --git a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt index 2448e4c..22e1551 100644 --- a/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt +++ b/matrix/services/room/src/main/kotlin/app/dapk/st/matrix/room/internal/DefaultRoomService.kt @@ -30,9 +30,9 @@ class DefaultRoomService( } } - override suspend fun markFullyRead(roomId: RoomId, eventId: EventId) { + override suspend fun markFullyRead(roomId: RoomId, eventId: EventId, isPrivate: Boolean) { logger.matrixLog(ROOM, "marking room fully read ${roomId.value}") - httpClient.execute(markFullyReadRequest(roomId, eventId)) + httpClient.execute(markFullyReadRequest(roomId, eventId, isPrivate)) } override suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember? { @@ -97,10 +97,10 @@ internal fun joinedMembersRequest(roomId: RoomId) = httpRequest( +internal fun markFullyReadRequest(roomId: RoomId, eventId: EventId, isPrivate: Boolean) = httpRequest( path = "_matrix/client/r0/rooms/${roomId.value}/read_markers", method = MatrixHttpClient.Method.POST, - body = jsonBody(MarkFullyReadRequest(eventId, eventId, hidden = true)) + body = jsonBody(MarkFullyReadRequest(eventId, eventId, hidden = isPrivate)) ) internal fun createRoomRequest(invites: List, isDM: Boolean, visibility: RoomVisibility, name: String? = null) = httpRequest( From 990cb083473202411ee54849390c1bd392ce10f7 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 8 Oct 2022 11:35:14 +0100 Subject: [PATCH 4/5] exposing option to enable/disable sending read receipts Disabled by default --- .../main/kotlin/app/dapk/st/graph/AppModule.kt | 2 ++ domains/store/build.gradle | 3 ++- .../kotlin/app/dapk/st/domain/StoreModule.kt | 7 +++++-- .../eventlog/EventLogPersistence.kt | 2 +- .../{ => application}/eventlog/LoggingStore.kt | 2 +- .../application/message/MessageOptionsStore.kt | 17 +++++++++++++++++ .../kotlin/fake}/FakeLoggingStore.kt | 6 +++--- .../kotlin/fake/FakeMessageOptionsStore.kt | 12 ++++++++++++ .../{fixture => fake}/FakeStoreCleaner.kt | 2 +- features/messenger/build.gradle | 1 + .../app/dapk/st/messenger/MessengerModule.kt | 14 +++++++++++++- .../app/dapk/st/messenger/MessengerViewModel.kt | 4 +++- .../dapk/st/messenger/MessengerViewModelTest.kt | 9 +++++++-- .../app/dapk/st/settings/SettingsItemFactory.kt | 10 +++++++++- .../app/dapk/st/settings/SettingsModule.kt | 7 +++++-- .../app/dapk/st/settings/SettingsState.kt | 3 ++- .../app/dapk/st/settings/SettingsViewModel.kt | 8 +++++++- .../st/settings/eventlogger/EventLoggerState.kt | 2 +- .../eventlogger/EventLoggerViewModel.kt | 2 +- .../dapk/st/settings/SettingsItemFactoryTest.kt | 14 +++++++++++++- .../dapk/st/settings/SettingsViewModelTest.kt | 4 +++- 21 files changed, 109 insertions(+), 22 deletions(-) rename domains/store/src/main/kotlin/app/dapk/st/domain/{ => application}/eventlog/EventLogPersistence.kt (97%) rename domains/store/src/main/kotlin/app/dapk/st/domain/{ => application}/eventlog/LoggingStore.kt (90%) create mode 100644 domains/store/src/main/kotlin/app/dapk/st/domain/application/message/MessageOptionsStore.kt rename {features/settings/src/test/kotlin/app/dapk/st/settings => domains/store/src/testFixtures/kotlin/fake}/FakeLoggingStore.kt (74%) create mode 100644 domains/store/src/testFixtures/kotlin/fake/FakeMessageOptionsStore.kt rename domains/store/src/testFixtures/kotlin/{fixture => fake}/FakeStoreCleaner.kt (87%) 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 7c58486..29abfe9 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -191,6 +191,7 @@ internal class FeatureModules internal constructor( context, base64, imageContentReader, + storeModule.value.messageStore(), ) } val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) } @@ -206,6 +207,7 @@ internal class FeatureModules internal constructor( coroutineDispatchers, coreAndroidModule.themeStore(), storeModule.value.loggingStore(), + storeModule.value.messageStore(), ) } val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) } diff --git a/domains/store/build.gradle b/domains/store/build.gradle index 7b6db9a..79da21b 100644 --- a/domains/store/build.gradle +++ b/domains/store/build.gradle @@ -25,4 +25,5 @@ dependencies { implementation "com.squareup.sqldelight:coroutines-extensions:1.5.3" kotlinFixtures(it) -} \ No newline at end of file + testImplementation(testFixtures(project(":core"))) + testFixturesImplementation(testFixtures(project(":core")))} \ No newline at end of file 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 427ac9c..aa8b1ef 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 @@ -5,8 +5,9 @@ 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 -import app.dapk.st.domain.eventlog.LoggingStore +import app.dapk.st.domain.application.eventlog.EventLogPersistence +import app.dapk.st.domain.application.eventlog.LoggingStore +import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.domain.localecho.LocalEchoPersistence import app.dapk.st.domain.preference.CachingPreferences import app.dapk.st.domain.preference.PropertyCache @@ -65,6 +66,8 @@ class StoreModule( fun loggingStore(): LoggingStore = LoggingStore(cachingPreferences) + fun messageStore(): MessageOptionsStore = MessageOptionsStore(cachingPreferences) + fun memberStore(): MemberStore { return MemberPersistence(database, coroutineDispatchers) } diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/EventLogPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/EventLogPersistence.kt similarity index 97% rename from domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/EventLogPersistence.kt rename to domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/EventLogPersistence.kt index 4cd41ec..718e34a 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/EventLogPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/EventLogPersistence.kt @@ -1,4 +1,4 @@ -package app.dapk.st.domain.eventlog +package app.dapk.st.domain.application.eventlog import app.dapk.db.DapkDb import app.dapk.st.core.CoroutineDispatchers diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/LoggingStore.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/LoggingStore.kt similarity index 90% rename from domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/LoggingStore.kt rename to domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/LoggingStore.kt index 2b9299a..a4b4f22 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/eventlog/LoggingStore.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/application/eventlog/LoggingStore.kt @@ -1,4 +1,4 @@ -package app.dapk.st.domain.eventlog +package app.dapk.st.domain.application.eventlog import app.dapk.st.core.CachedPreferences import app.dapk.st.core.readBoolean diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/application/message/MessageOptionsStore.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/application/message/MessageOptionsStore.kt new file mode 100644 index 0000000..bf4584d --- /dev/null +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/application/message/MessageOptionsStore.kt @@ -0,0 +1,17 @@ +package app.dapk.st.domain.application.message + +import app.dapk.st.core.CachedPreferences +import app.dapk.st.core.readBoolean +import app.dapk.st.core.store + +private const val KEY_READ_RECEIPTS_DISABLED = "key_read_receipts_disabled" + +class MessageOptionsStore(private val cachedPreferences: CachedPreferences) { + + suspend fun isReadReceiptsDisabled() = cachedPreferences.readBoolean(KEY_READ_RECEIPTS_DISABLED, defaultValue = true) + + suspend fun setReadReceiptsDisabled(isDisabled: Boolean) { + cachedPreferences.store(KEY_READ_RECEIPTS_DISABLED, isDisabled) + } + +} \ No newline at end of file diff --git a/features/settings/src/test/kotlin/app/dapk/st/settings/FakeLoggingStore.kt b/domains/store/src/testFixtures/kotlin/fake/FakeLoggingStore.kt similarity index 74% rename from features/settings/src/test/kotlin/app/dapk/st/settings/FakeLoggingStore.kt rename to domains/store/src/testFixtures/kotlin/fake/FakeLoggingStore.kt index 6a3595f..78cc176 100644 --- a/features/settings/src/test/kotlin/app/dapk/st/settings/FakeLoggingStore.kt +++ b/domains/store/src/testFixtures/kotlin/fake/FakeLoggingStore.kt @@ -1,6 +1,6 @@ -package app.dapk.st.settings +package fake -import app.dapk.st.domain.eventlog.LoggingStore +import app.dapk.st.domain.application.eventlog.LoggingStore import io.mockk.coEvery import io.mockk.mockk import test.delegateReturn @@ -9,4 +9,4 @@ class FakeLoggingStore { val instance = mockk() fun givenLoggingIsEnabled() = coEvery { instance.isEnabled() }.delegateReturn() -} \ No newline at end of file +} diff --git a/domains/store/src/testFixtures/kotlin/fake/FakeMessageOptionsStore.kt b/domains/store/src/testFixtures/kotlin/fake/FakeMessageOptionsStore.kt new file mode 100644 index 0000000..d9ad43e --- /dev/null +++ b/domains/store/src/testFixtures/kotlin/fake/FakeMessageOptionsStore.kt @@ -0,0 +1,12 @@ +package fake + +import app.dapk.st.domain.application.message.MessageOptionsStore +import io.mockk.coEvery +import io.mockk.mockk +import test.delegateReturn + +class FakeMessageOptionsStore { + val instance = mockk() + + fun givenReadReceiptsDisabled() = coEvery { instance.isReadReceiptsDisabled() }.delegateReturn() +} \ No newline at end of file diff --git a/domains/store/src/testFixtures/kotlin/fixture/FakeStoreCleaner.kt b/domains/store/src/testFixtures/kotlin/fake/FakeStoreCleaner.kt similarity index 87% rename from domains/store/src/testFixtures/kotlin/fixture/FakeStoreCleaner.kt rename to domains/store/src/testFixtures/kotlin/fake/FakeStoreCleaner.kt index 19ac519..ea2c635 100644 --- a/domains/store/src/testFixtures/kotlin/fixture/FakeStoreCleaner.kt +++ b/domains/store/src/testFixtures/kotlin/fake/FakeStoreCleaner.kt @@ -1,4 +1,4 @@ -package fixture +package fake import app.dapk.st.domain.StoreCleaner import io.mockk.mockk diff --git a/features/messenger/build.gradle b/features/messenger/build.gradle index e5686f7..9d31472 100644 --- a/features/messenger/build.gradle +++ b/features/messenger/build.gradle @@ -8,6 +8,7 @@ dependencies { implementation project(":matrix:services:room") implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") + implementation project(":domains:store") implementation project(":core") implementation project(":features:navigator") implementation project(":design-library") diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt index 4caff56..9b48609 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerModule.kt @@ -3,6 +3,7 @@ package app.dapk.st.messenger import android.content.Context import app.dapk.st.core.Base64 import app.dapk.st.core.ProvidableModule +import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.message.MessageService @@ -22,10 +23,21 @@ class MessengerModule( private val context: Context, private val base64: Base64, private val imageMetaReader: ImageContentReader, + private val messageOptionsStore: MessageOptionsStore, ) : ProvidableModule { internal fun messengerViewModel(): MessengerViewModel { - return MessengerViewModel(messageService, roomService, roomStore, credentialsStore, timelineUseCase(), LocalIdFactory(), imageMetaReader, clock) + return MessengerViewModel( + messageService, + roomService, + roomStore, + credentialsStore, + timelineUseCase(), + LocalIdFactory(), + imageMetaReader, + messageOptionsStore, + clock + ) } private fun timelineUseCase(): TimelineUseCaseImpl { diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index 95dc503..d780da5 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -3,6 +3,7 @@ package app.dapk.st.messenger import androidx.lifecycle.viewModelScope import app.dapk.st.core.Lce import app.dapk.st.core.extensions.takeIfContent +import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId @@ -30,6 +31,7 @@ internal class MessengerViewModel( private val observeTimeline: ObserveTimelineUseCase, private val localIdFactory: LocalIdFactory, private val imageContentReader: ImageContentReader, + private val messageOptionsStore: MessageOptionsStore, private val clock: Clock, factory: MutableStateFactory = defaultStateFactory(), ) : DapkViewModel( @@ -101,7 +103,7 @@ internal class MessengerViewModel( private fun CoroutineScope.updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState): Deferred { return async { runCatching { - roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = true) + roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = messageOptionsStore.isReadReceiptsDisabled()) roomStore.markRead(state.roomState.roomOverview.roomId) } } diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt index 5162a2a..e2449e3 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt +++ b/features/messenger/src/test/kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt @@ -11,6 +11,7 @@ import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.SyncService import fake.FakeCredentialsStore +import fake.FakeMessageOptionsStore import fake.FakeRoomStore import fixture.* import internalfake.FakeLocalIdFactory @@ -25,6 +26,7 @@ import java.time.Instant import java.time.ZoneOffset private const val A_CURRENT_TIMESTAMP = 10000L +private const val READ_RECEIPTS_ARE_DISABLED = true private val A_ROOM_ID = aRoomId("messenger state room id") private const val A_MESSAGE_CONTENT = "message content" private const val A_LOCAL_ID = "local.1111-2222-3333" @@ -40,6 +42,7 @@ class MessengerViewModelTest { private val fakeRoomStore = FakeRoomStore() private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(aUserCredentials(userId = A_SELF_ID)) } private val fakeObserveTimelineUseCase = FakeObserveTimelineUseCase() + private val fakeMessageOptionsStore = FakeMessageOptionsStore() private val viewModel = MessengerViewModel( fakeMessageService, @@ -49,6 +52,7 @@ class MessengerViewModelTest { fakeObserveTimelineUseCase, localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance, imageContentReader = FakeImageContentReader(), + messageOptionsStore = fakeMessageOptionsStore.instance, clock = fixedClock(A_CURRENT_TIMESTAMP), factory = runViewModelTest.testMutableStateFactory(), ) @@ -68,8 +72,9 @@ class MessengerViewModelTest { @Test fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest { + fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED) fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) } - fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID, isPrivate = true) } + fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID, isPrivate = READ_RECEIPTS_ARE_DISABLED) } val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID) fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state)) @@ -153,4 +158,4 @@ class FakeRoomService : RoomService by mockk() { fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC) -class FakeImageContentReader: ImageContentReader by mockk() \ No newline at end of file +class FakeImageContentReader : ImageContentReader by mockk() \ No newline at end of file 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 8919eb9..d054e64 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 @@ -4,7 +4,8 @@ import app.dapk.st.core.BuildMeta import app.dapk.st.core.DeviceMeta import app.dapk.st.core.ThemeStore import app.dapk.st.core.isAtLeastS -import app.dapk.st.domain.eventlog.LoggingStore +import app.dapk.st.domain.application.eventlog.LoggingStore +import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.push.PushTokenRegistrars internal class SettingsItemFactory( @@ -13,6 +14,7 @@ internal class SettingsItemFactory( private val pushTokenRegistrars: PushTokenRegistrars, private val themeStore: ThemeStore, private val loggingStore: LoggingStore, + private val messageOptionsStore: MessageOptionsStore, ) { suspend fun root() = general() + theme() + data() + account() + advanced() + about() @@ -44,6 +46,12 @@ internal class SettingsItemFactory( val loggingIsEnabled = loggingStore.isEnabled() return listOf( SettingItem.Header("Advanced"), + SettingItem.Toggle( + SettingItem.Id.ToggleSendReadReceipts, + "Don't send message read receipts", + subtitle = "Requires the Homeserver to be running Synapse 1.65+", + state = messageOptionsStore.isReadReceiptsDisabled() + ), SettingItem.Toggle(SettingItem.Id.ToggleEnableLogs, "Enable local logging", state = loggingIsEnabled), SettingItem.Text(SettingItem.Id.EventLog, "Event log", enabled = loggingIsEnabled), ) 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 380cbdd..a0c695a 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 @@ -3,7 +3,8 @@ package app.dapk.st.settings import android.content.ContentResolver import app.dapk.st.core.* import app.dapk.st.domain.StoreModule -import app.dapk.st.domain.eventlog.LoggingStore +import app.dapk.st.domain.application.eventlog.LoggingStore +import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.matrix.crypto.CryptoService import app.dapk.st.matrix.sync.SyncService import app.dapk.st.push.PushModule @@ -20,6 +21,7 @@ class SettingsModule( private val coroutineDispatchers: CoroutineDispatchers, private val themeStore: ThemeStore, private val loggingStore: LoggingStore, + private val messageOptionsStore: MessageOptionsStore, ) : ProvidableModule { internal fun settingsViewModel(): SettingsViewModel { @@ -29,10 +31,11 @@ class SettingsModule( cryptoService, syncService, UriFilenameResolver(contentResolver, coroutineDispatchers), - SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore), + SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore), pushModule.pushTokenRegistrars(), themeStore, loggingStore, + messageOptionsStore, ) } 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 5dd8144..0c73de2 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,7 +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, val enabled: Boolean = true) : SettingItem - data class Toggle(override val id: Id, val content: String, val state: Boolean) : SettingItem + data class Toggle(override val id: Id, val content: String, val subtitle: String? = null, val state: Boolean) : SettingItem data class AccessToken(override val id: Id, val content: String, val accessToken: String) : SettingItem enum class Id { @@ -58,6 +58,7 @@ internal sealed interface SettingItem { Ignored, ToggleDynamicTheme, ToggleEnableLogs, + ToggleSendReadReceipts, } } 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 a952c75..2956d62 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 @@ -7,7 +7,8 @@ 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.domain.eventlog.LoggingStore +import app.dapk.st.domain.application.eventlog.LoggingStore +import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.matrix.crypto.CryptoService import app.dapk.st.matrix.crypto.ImportResult import app.dapk.st.matrix.sync.SyncService @@ -34,6 +35,7 @@ internal class SettingsViewModel( private val pushTokenRegistrars: PushTokenRegistrars, private val themeStore: ThemeStore, private val loggingStore: LoggingStore, + private val messageOptionsStore: MessageOptionsStore, factory: MutableStateFactory = defaultStateFactory(), ) : DapkViewModel( initialState = SettingsScreenState(SpiderPage(Page.Routes.root, "Settings", null, Page.Root(Lce.Loading()))), @@ -104,6 +106,10 @@ internal class SettingsViewModel( loggingStore.setEnabled(!loggingStore.isEnabled()) refreshRoot() } + + ToggleSendReadReceipts -> viewModelScope.launch { + messageOptionsStore.setReadReceiptsDisabled(!messageOptionsStore.isReadReceiptsDisabled()) + } } } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLoggerState.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLoggerState.kt index 60bf449..08a28ef 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLoggerState.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLoggerState.kt @@ -1,7 +1,7 @@ package app.dapk.st.settings.eventlogger import app.dapk.st.core.Lce -import app.dapk.st.domain.eventlog.LogLine +import app.dapk.st.domain.application.eventlog.LogLine data class EventLoggerState( val logs: Lce>, diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLoggerViewModel.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLoggerViewModel.kt index b85e057..aa9d6af 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLoggerViewModel.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLoggerViewModel.kt @@ -2,7 +2,7 @@ package app.dapk.st.settings.eventlogger import androidx.lifecycle.viewModelScope import app.dapk.st.core.Lce -import app.dapk.st.domain.eventlog.EventLogPersistence +import app.dapk.st.domain.application.eventlog.EventLogPersistence import app.dapk.st.viewmodel.DapkViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.collect 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 7216731..46a1620 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 @@ -4,6 +4,8 @@ import app.dapk.st.core.BuildMeta import app.dapk.st.core.DeviceMeta import app.dapk.st.push.PushTokenRegistrars import app.dapk.st.push.Registrar +import fake.FakeLoggingStore +import fake.FakeMessageOptionsStore import internalfixture.aSettingHeaderItem import internalfixture.aSettingTextItem import io.mockk.coEvery @@ -16,6 +18,7 @@ import test.delegateReturn private val A_SELECTION = Registrar("A_SELECTION") private const val ENABLED_MATERIAL_YOU = true private const val DISABLED_LOGGING = false +private const val DISABLED_READ_RECEIPTS = true class SettingsItemFactoryTest { @@ -24,13 +27,15 @@ class SettingsItemFactoryTest { private val fakePushTokenRegistrars = FakePushRegistrars() private val fakeThemeStore = FakeThemeStore() private val fakeLoggingStore = FakeLoggingStore() + private val fakeMessageOptionsStore = FakeMessageOptionsStore() private val settingsItemFactory = SettingsItemFactory( buildMeta, deviceMeta, fakePushTokenRegistrars.instance, fakeThemeStore.instance, - fakeLoggingStore.instance + fakeLoggingStore.instance, + fakeMessageOptionsStore.instance, ) @Test @@ -38,6 +43,7 @@ class SettingsItemFactoryTest { fakePushTokenRegistrars.givenCurrentSelection().returns(A_SELECTION) fakeThemeStore.givenMaterialYouIsEnabled().returns(ENABLED_MATERIAL_YOU) fakeLoggingStore.givenLoggingIsEnabled().returns(DISABLED_LOGGING) + fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(DISABLED_READ_RECEIPTS) val result = settingsItemFactory.root() @@ -52,6 +58,12 @@ class SettingsItemFactoryTest { aSettingHeaderItem("Account"), aSettingTextItem(SettingItem.Id.SignOut, "Sign out"), aSettingHeaderItem("Advanced"), + SettingItem.Toggle( + SettingItem.Id.ToggleSendReadReceipts, + "Don't send message read receipts", + subtitle = "Requires the Homeserver to be running Synapse 1.65+", + state = DISABLED_READ_RECEIPTS + ), SettingItem.Toggle(SettingItem.Id.ToggleEnableLogs, "Enable local logging", state = DISABLED_LOGGING), aSettingTextItem(SettingItem.Id.EventLog, "Event log", enabled = DISABLED_LOGGING), aSettingHeaderItem("About"), 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 374849c..87477c4 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 @@ -5,7 +5,7 @@ import app.dapk.st.core.Lce import app.dapk.st.design.components.SpiderPage import app.dapk.st.matrix.crypto.ImportResult import fake.* -import fixture.FakeStoreCleaner +import fake.FakeStoreCleaner import fixture.aRoomId import internalfake.FakeSettingsItemFactory import internalfake.FakeUriFilenameResolver @@ -42,6 +42,7 @@ internal class SettingsViewModelTest { private val fakeSettingsItemFactory = FakeSettingsItemFactory() private val fakeThemeStore = FakeThemeStore() private val fakeLoggingStore = FakeLoggingStore() + private val fakeMessageOptionsStore = FakeMessageOptionsStore() private val viewModel = SettingsViewModel( fakeStoreCleaner, @@ -53,6 +54,7 @@ internal class SettingsViewModelTest { fakePushTokenRegistrars.instance, fakeThemeStore.instance, fakeLoggingStore.instance, + fakeMessageOptionsStore.instance, runViewModelTest.testMutableStateFactory(), ) From 0d0a5767d6959e7901bfb82212e1348e4ffbb170 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sat, 8 Oct 2022 12:08:43 +0100 Subject: [PATCH 5/5] add subtitle to toggle item --- .../app/dapk/st/design/components/itemRow.kt | 42 ++++++++++++++++--- .../app/dapk/st/settings/SettingsScreen.kt | 24 +---------- .../app/dapk/st/settings/SettingsViewModel.kt | 1 + 3 files changed, 40 insertions(+), 27 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt index 7429e56..15384da 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/itemRow.kt @@ -2,9 +2,7 @@ package app.dapk.st.design.components import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material3.Divider -import androidx.compose.material3.Icon -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -22,12 +20,13 @@ fun TextRow( enabled: Boolean = true, body: @Composable () -> Unit = {} ) { + val verticalPadding = 24.dp val modifier = Modifier.padding(horizontal = 24.dp) Column( Modifier .fillMaxWidth() .clickable(enabled = onClick != null) { onClick?.invoke() }) { - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(verticalPadding)) Column(modifier) { val textModifier = when (enabled) { true -> Modifier @@ -45,7 +44,7 @@ fun TextRow( } } body() - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(verticalPadding)) } if (includeDivider) { Divider(modifier = Modifier.fillMaxWidth()) @@ -72,3 +71,36 @@ fun IconRow(icon: ImageVector, title: String, onClick: (() -> Unit)? = null) { fun SettingsTextRow(title: String, subtitle: String?, onClick: (() -> Unit)?, enabled: Boolean) { TextRow(title = title, subtitle, includeDivider = false, onClick, enabled = enabled) } + +@Composable +fun SettingsToggleRow(title: String, subtitle: String?, state: Boolean, onToggle: () -> Unit) { + Toggle(title, subtitle, state, onToggle) +} + +@Composable +private fun Toggle(title: String, subtitle: String?, state: Boolean, onToggle: () -> Unit) { + val verticalPadding = 16.dp + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = verticalPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + if (subtitle == null) { + Text(text = title) + } else { + Column(modifier = Modifier.weight(1f)) { + Text(text = title) + Spacer(Modifier.height(4.dp)) + Text(text = subtitle, fontSize = 12.sp, color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f)) + } + } + Switch( + modifier = Modifier.wrapContentWidth(), + checked = state, + onCheckedChange = { onToggle() } + ) + } +} + 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 5df70ed..570c632 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 @@ -41,10 +41,7 @@ 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 -import app.dapk.st.design.components.TextRow +import app.dapk.st.design.components.* import app.dapk.st.matrix.crypto.ImportResult import app.dapk.st.navigator.Navigator import app.dapk.st.settings.SettingsEvent.* @@ -222,7 +219,7 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) { } is SettingItem.Header -> Header(item.label) - is SettingItem.Toggle -> Toggle(item, onToggle = { + is SettingItem.Toggle -> SettingsToggleRow(item.content, item.subtitle, item.state, onToggle = { onClick(item) }) } @@ -241,23 +238,6 @@ 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 { 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 2956d62..5510e5d 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 @@ -109,6 +109,7 @@ internal class SettingsViewModel( ToggleSendReadReceipts -> viewModelScope.launch { messageOptionsStore.setReadReceiptsDisabled(!messageOptionsStore.isReadReceiptsDisabled()) + refreshRoot() } } }