From d2cbb37066362a11a770e55c931245f7dbcd3b25 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Oct 2022 05:49:17 +0000 Subject: [PATCH 01/43] Bump sqldelightVer from 1.5.3 to 1.5.4 Bumps `sqldelightVer` from 1.5.3 to 1.5.4. Updates `gradle-plugin` from 1.5.3 to 1.5.4 - [Release notes](https://github.com/square/sqldelight/releases) - [Changelog](https://github.com/cashapp/sqldelight/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/sqldelight/compare/1.5.3...1.5.4) Updates `android-driver` from 1.5.3 to 1.5.4 - [Release notes](https://github.com/square/sqldelight/releases) - [Changelog](https://github.com/cashapp/sqldelight/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/sqldelight/compare/1.5.3...1.5.4) Updates `sqlite-driver` from 1.5.3 to 1.5.4 - [Release notes](https://github.com/square/sqldelight/releases) - [Changelog](https://github.com/cashapp/sqldelight/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/sqldelight/compare/1.5.3...1.5.4) --- updated-dependencies: - dependency-name: com.squareup.sqldelight:gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.squareup.sqldelight:android-driver dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: com.squareup.sqldelight:sqlite-driver dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index ec82c32..c3d6a08 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -98,7 +98,7 @@ ext.Dependencies.with { } def kotlinVer = "1.7.10" - def sqldelightVer = "1.5.3" + def sqldelightVer = "1.5.4" def composeVer = "1.2.1" def ktorVer = "2.1.2" From 3ba01777dfe9a4ab01cc9d539e61fc75dc83ea35 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 7 Oct 2022 05:49:23 +0000 Subject: [PATCH 02/43] Bump coroutines-extensions from 1.5.3 to 1.5.4 Bumps [coroutines-extensions](https://github.com/square/sqldelight) from 1.5.3 to 1.5.4. - [Release notes](https://github.com/square/sqldelight/releases) - [Changelog](https://github.com/cashapp/sqldelight/blob/master/CHANGELOG.md) - [Commits](https://github.com/square/sqldelight/compare/1.5.3...1.5.4) --- updated-dependencies: - dependency-name: com.squareup.sqldelight:coroutines-extensions dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- domains/store/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/domains/store/build.gradle b/domains/store/build.gradle index 7b6db9a..efc925c 100644 --- a/domains/store/build.gradle +++ b/domains/store/build.gradle @@ -22,7 +22,7 @@ dependencies { implementation project(":core") implementation Dependencies.mavenCentral.kotlinSerializationJson implementation Dependencies.mavenCentral.kotlinCoroutinesCore - implementation "com.squareup.sqldelight:coroutines-extensions:1.5.3" + implementation "com.squareup.sqldelight:coroutines-extensions:1.5.4" kotlinFixtures(it) } \ No newline at end of file From f1be275e79ccc3958289b409c44768cf24ea23d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 8 Oct 2022 09:59:32 +0000 Subject: [PATCH 03/43] Bump kotlinVer from 1.6.10 to 1.7.20 Bumps `kotlinVer` from 1.6.10 to 1.7.20. Updates `kotlin-test-junit` from 1.6.10 to 1.7.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.7.20/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.6.10...v1.7.20) Updates `kotlin-gradle-plugin` from 1.7.10 to 1.7.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.7.20/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.10...v1.7.20) Updates `kotlin-serialization` from 1.7.10 to 1.7.20 - [Release notes](https://github.com/JetBrains/kotlin/releases) - [Changelog](https://github.com/JetBrains/kotlin/blob/v1.7.20/ChangeLog.md) - [Commits](https://github.com/JetBrains/kotlin/compare/v1.7.10...v1.7.20) --- updated-dependencies: - dependency-name: org.jetbrains.kotlin:kotlin-test-junit dependency-type: direct:production update-type: version-update:semver-minor - dependency-name: org.jetbrains.kotlin:kotlin-gradle-plugin dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: org.jetbrains.kotlin:kotlin-serialization dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- dependencies.gradle | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 5ac2108..cc4e835 100644 --- a/build.gradle +++ b/build.gradle @@ -131,7 +131,7 @@ ext.applyCrashlyticsIfRelease = { project -> ext.kotlinTest = { dependencies -> dependencies.testImplementation Dependencies.mavenCentral.kluent dependencies.testImplementation Dependencies.mavenCentral.kotlinTest - dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10" + dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.7.20" dependencies.testImplementation 'io.mockk:mockk:1.13.2' dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' diff --git a/dependencies.gradle b/dependencies.gradle index c3d6a08..b6d405f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -97,7 +97,7 @@ ext.Dependencies.with { } } - def kotlinVer = "1.7.10" + def kotlinVer = "1.7.20" def sqldelightVer = "1.5.4" def composeVer = "1.2.1" def ktorVer = "2.1.2" From 31318037a75785e2ba95e7660e13fcef234457e2 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 13 Sep 2022 21:15:41 +0100 Subject: [PATCH 04/43] 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 05/43] 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 06/43] 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 07/43] 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 08/43] 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() } } } From 3a8c4c11ce8e9f017f156fb009862f4b41defbd3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 15:09:56 +0100 Subject: [PATCH 09/43] adding dedicated error for missing room keys cipher --- .../app/dapk/st/settings/SettingsScreen.kt | 1 + .../dapk/st/matrix/crypto/CryptoService.kt | 1 + .../matrix/crypto/internal/RoomKeyImporter.kt | 24 ++++++++++++------- 3 files changed, 18 insertions(+), 8 deletions(-) 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 570c632..fba9c23 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 @@ -156,6 +156,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, 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}" ImportResult.Error.Type.UnableToOpenFile -> "Unable to open file" + ImportResult.Error.Type.InvalidFile -> "Unable to process file" } Text(text = "Import failed\n$message", textAlign = TextAlign.Center) diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt index ba5936b..d4ac8d8 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/CryptoService.kt @@ -184,6 +184,7 @@ sealed interface ImportResult { object NoKeysFound : Type object UnexpectedDecryptionOutput : Type object UnableToOpenFile : Type + object InvalidFile : Type } } diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt index 24311a9..7adcc3c 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/internal/RoomKeyImporter.kt @@ -86,21 +86,27 @@ class RoomKeyImporter( val line = it.joinToString(separator = "").replace("\n", "") val toByteArray = base64.decode(line) if (index == 0) { - decryptCipher.initialize(toByteArray, password) - toByteArray - .copyOfRange(37, toByteArray.size) - .decrypt(decryptCipher) - .also { - if (!it.startsWith("[{")) { - throw ImportException(ImportResult.Error.Type.UnexpectedDecryptionOutput) - } + toByteArray.ensureHasCipherPayloadOrThrow() + val initializer = toByteArray.copyOfRange(0, 37) + decryptCipher.initialize(initializer, password) + val content = toByteArray.copyOfRange(37, toByteArray.size) + content.decrypt(decryptCipher).also { + if (!it.startsWith("[{")) { + throw ImportException(ImportResult.Error.Type.UnexpectedDecryptionOutput) } + } } else { toByteArray.decrypt(decryptCipher) } } } + private fun ByteArray.ensureHasCipherPayloadOrThrow() { + if (this.size < 37) { + throw ImportException(ImportResult.Error.Type.InvalidFile) + } + } + private fun Cipher.initialize(payload: ByteArray, passphrase: String) { val salt = payload.copyOfRange(1, 1 + 16) val iv = payload.copyOfRange(17, 17 + 16) @@ -176,6 +182,7 @@ private class JsonAccumulator { jsonSegment = withLatest null } + else -> { val string = withLatest.substring(objectRange) importJson.decodeFromString(ElementMegolmExportObject.serializer(), string).also { @@ -200,6 +207,7 @@ private class JsonAccumulator { } opens++ } + c == '}' -> { opens-- if (opens == 0) { From 2e91ae5b670831de76e229f3319e2d6c2adefb1f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 15:29:38 +0100 Subject: [PATCH 10/43] add more options to generic error component --- .../app/dapk/st/design/components/Error.kt | 39 +++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt index 722ff2c..20db56c 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt @@ -1,21 +1,44 @@ package app.dapk.st.design.components -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp @Composable -fun GenericError(retryAction: () -> Unit) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { +fun GenericError(message: String = "Something went wrong...", label: String = "Retry", moreDetails: String? = null, action: () -> Unit) { + val openDetailsDialog = remember { mutableStateOf(false) } + if (openDetailsDialog.value) { + AlertDialog( + onDismissRequest = { openDetailsDialog.value = false }, + confirmButton = { + Button(onClick = { openDetailsDialog.value = false }) { + Text("OK") + } + }, + title = { Text("Details") }, + text = { + Text(moreDetails!!) + } + ) + } + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Something went wrong...") - Button(onClick = { retryAction() }) { - Text("Retry") + Text(message) + if (moreDetails != null) { + Text("Tap for more details".uppercase(), fontSize = 12.sp, modifier = Modifier.clickable { openDetailsDialog.value = true }.padding(12.dp)) + } + Spacer(modifier = Modifier.height(12.dp)) + Button(onClick = { action() }) { + Text(label.uppercase()) } } } From 9ec9797d17f56c82a7dfcb073997d63bcd30e0cb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 15:30:00 +0100 Subject: [PATCH 11/43] providing more details to the import errors via dialog --- .../st/core/extensions/GlobalExtensions.kt | 7 +++++ .../app/dapk/st/settings/SettingsScreen.kt | 30 +++++++++---------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/core/src/main/kotlin/app/dapk/st/core/extensions/GlobalExtensions.kt b/core/src/main/kotlin/app/dapk/st/core/extensions/GlobalExtensions.kt index 1c947a7..fee8de7 100644 --- a/core/src/main/kotlin/app/dapk/st/core/extensions/GlobalExtensions.kt +++ b/core/src/main/kotlin/app/dapk/st/core/extensions/GlobalExtensions.kt @@ -3,6 +3,13 @@ package app.dapk.st.core.extensions inline fun T?.ifNull(block: () -> T): T = this ?: block() inline fun ifOrNull(condition: Boolean, block: () -> T): T? = if (condition) block() else null +inline fun Any.takeAs(): T? { + return when (this) { + is T -> this + else -> null + } +} + @Suppress("UNCHECKED_CAST") inline fun Iterable.firstOrNull(predicate: (T) -> Boolean, predicate2: (T) -> Boolean): Pair? { var firstValue: T1? = null 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 fba9c23..402654b 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 @@ -40,6 +40,7 @@ import app.dapk.st.core.Lce 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.extensions.takeAs import app.dapk.st.core.getActivity import app.dapk.st.design.components.* import app.dapk.st.matrix.crypto.ImportResult @@ -149,22 +150,21 @@ 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) { - 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}" - ImportResult.Error.Type.UnableToOpenFile -> "Unable to open file" - ImportResult.Error.Type.InvalidFile -> "Unable to process file" - } - - Text(text = "Import failed\n$message", textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = { navigator.navigate.upToHome() }) { - Text(text = "Close".uppercase()) - } + val message = when (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 -> "Unknown error" + ImportResult.Error.Type.UnableToOpenFile -> "Unable to open file" + ImportResult.Error.Type.InvalidFile -> "Unable to process file" + } + GenericError( + message = message, + label = "Close", + moreDetails = result.cause.takeAs()?.let { + "${it.cause::class.java.simpleName}: ${it.cause.message}" } + ) { + navigator.navigate.upToHome() } } From 7e0d4d60139f7391bb1799fe57c81eda75d901af Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 15:40:07 +0100 Subject: [PATCH 12/43] add error handling to the image gallery --- .../kotlin/app/dapk/st/design/components/Error.kt | 4 +++- .../st/messenger/gallery/ImageGalleryScreen.kt | 14 ++++++-------- .../st/messenger/gallery/ImageGalleryViewModel.kt | 4 ++-- .../kotlin/app/dapk/st/settings/SettingsScreen.kt | 5 +---- 4 files changed, 12 insertions(+), 15 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt index 20db56c..ef06b5f 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Error.kt @@ -14,7 +14,9 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @Composable -fun GenericError(message: String = "Something went wrong...", label: String = "Retry", moreDetails: String? = null, action: () -> Unit) { +fun GenericError(message: String = "Something went wrong...", label: String = "Retry", cause: Throwable? = null, action: () -> Unit) { + val moreDetails = cause?.let { "${it::class.java.simpleName}: ${it.message}" } + val openDetailsDialog = remember { mutableStateOf(false) } if (openDetailsDialog.value) { AlertDialog( diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt index acb46c1..8770857 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt @@ -42,12 +42,10 @@ fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> U Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) { item(ImageGalleryPage.Routes.folders) { - ImageGalleryFolders(it) { folder -> - viewModel.selectFolder(folder) - } + ImageGalleryFolders(it, onClick = { viewModel.selectFolder(it) }, onRetry = { viewModel.start() }) } item(ImageGalleryPage.Routes.files) { - ImageGalleryMedia(it, onImageSelected) + ImageGalleryMedia(it, onImageSelected, onRetry = { viewModel.selectFolder(it.folder) }) } } @@ -55,7 +53,7 @@ fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> U @Composable -fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit) { +fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit, onRetry: () -> Unit) { val screenWidth = LocalConfiguration.current.screenWidthDp val gradient = Brush.verticalGradient( @@ -106,12 +104,12 @@ fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Un } } - is Lce.Error -> GenericError { } + is Lce.Error -> GenericError(cause = content.cause, action = onRetry) } } @Composable -fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit) { +fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> Unit, onRetry: () -> Unit) { val screenWidth = LocalConfiguration.current.screenWidthDp Column { @@ -149,7 +147,7 @@ fun ImageGalleryMedia(state: ImageGalleryPage.Files, onFileSelected: (Media) -> } } - is Lce.Error -> GenericError { } + is Lce.Error -> GenericError(cause = content.cause, action = onRetry) } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt index 59cbbb4..0620315 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryViewModel.kt @@ -48,7 +48,7 @@ class ImageGalleryViewModel( route = ImageGalleryPage.Routes.files, label = page.label, parent = ImageGalleryPage.Routes.folders, - state = ImageGalleryPage.Files(Lce.Loading()) + state = ImageGalleryPage.Files(Lce.Loading(), folder) ) ) } @@ -78,7 +78,7 @@ data class ImageGalleryState( sealed interface ImageGalleryPage { data class Folders(val content: Lce>) : ImageGalleryPage - data class Files(val content: Lce>) : ImageGalleryPage + data class Files(val content: Lce>, val folder: Folder) : ImageGalleryPage object Routes { val folders = Route("Folders") 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 402654b..4ffb0a9 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 @@ -31,7 +31,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -160,9 +159,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, GenericError( message = message, label = "Close", - moreDetails = result.cause.takeAs()?.let { - "${it.cause::class.java.simpleName}: ${it.cause.message}" - } + cause = result.cause.takeAs()?.cause ) { navigator.navigate.upToHome() } From ec5c58a95c8ad06f4cd751d931d410df9b20caf0 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 15:53:26 +0100 Subject: [PATCH 13/43] adding error views to profile, messenger and settings screens --- .../app/dapk/st/design/components/Spider.kt | 25 +++++++++++++------ .../app/dapk/st/messenger/MessengerScreen.kt | 20 +++------------ .../messenger/gallery/ImageGalleryActivity.kt | 6 ++++- .../messenger/gallery/ImageGalleryScreen.kt | 1 - .../app/dapk/st/profile/ProfileScreen.kt | 4 +-- .../app/dapk/st/settings/SettingsScreen.kt | 12 ++++----- .../st/settings/eventlogger/EventLogScreen.kt | 16 +++++++----- 7 files changed, 43 insertions(+), 41 deletions(-) diff --git a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt index 826694b..55b8a12 100644 --- a/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt +++ b/design-library/src/main/kotlin/app/dapk/st/design/components/Spider.kt @@ -10,22 +10,27 @@ fun Spider(currentPage: SpiderPage, onNavigate: (SpiderPage? val pageCache = remember { mutableMapOf, SpiderPage>() } pageCache[currentPage.route] = currentPage + val navigateAndPopStack = { + pageCache.remove(currentPage.route) + onNavigate(pageCache[currentPage.parent]) + } + val itemScope = object : SpiderItemScope { + override fun goBack() { + navigateAndPopStack() + } + } + val computedWeb = remember(true) { mutableMapOf, @Composable (T) -> Unit>().also { computedWeb -> val scope = object : SpiderScope { - override fun item(route: Route, content: @Composable (T) -> Unit) { - computedWeb[route] = { content(it as T) } + override fun item(route: Route, content: @Composable SpiderItemScope.(T) -> Unit) { + computedWeb[route] = { content(itemScope, it as T) } } } graph.invoke(scope) } } - val navigateAndPopStack = { - pageCache.remove(currentPage.route) - onNavigate(pageCache[currentPage.parent]) - } - Column { if (currentPage.hasToolbar) { Toolbar( @@ -40,7 +45,11 @@ fun Spider(currentPage: SpiderPage, onNavigate: (SpiderPage? interface SpiderScope { - fun item(route: Route, content: @Composable (T) -> Unit) + fun item(route: Route, content: @Composable SpiderItemScope.(T) -> Unit) +} + +interface SpiderItemScope { + fun goBack() } data class SpiderPage( diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index d130dd3..5d1447e 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -43,10 +43,7 @@ import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.extensions.takeIfContent -import app.dapk.st.design.components.MessengerUrlIcon -import app.dapk.st.design.components.MissingAvatarIcon -import app.dapk.st.design.components.SmallTalkTheme -import app.dapk.st.design.components.Toolbar +import app.dapk.st.design.components.* import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.sync.MessageMeta @@ -95,7 +92,7 @@ internal fun MessengerScreen( }) when (state.composerState) { is ComposerState.Text -> { - Room(state.roomState, replyActions) + Room(state.roomState, replyActions, onRetry = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) }) TextComposer( state.composerState, onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) }, @@ -132,7 +129,7 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun } @Composable -private fun ColumnScope.Room(roomStateLce: Lce, replyActions: ReplyActions) { +private fun ColumnScope.Room(roomStateLce: Lce, replyActions: ReplyActions, onRetry: () -> Unit) { when (val state = roomStateLce) { is Lce.Loading -> CenteredLoading() is Lce.Content -> { @@ -165,16 +162,7 @@ private fun ColumnScope.Room(roomStateLce: Lce, replyActions: Re } } - is Lce.Error -> { - Box(contentAlignment = Alignment.Center) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Something went wrong...") - Button(onClick = {}) { - Text("Retry") - } - } - } - } + is Lce.Error -> GenericError(cause = state.cause, action = onRetry) } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt index 06faf13..7ccd939 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt @@ -15,6 +15,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.lifecycleScope import app.dapk.st.core.* import app.dapk.st.core.extensions.unsafeLazy +import app.dapk.st.design.components.GenericError import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize @@ -59,7 +60,10 @@ fun Activity.PermissionGuard(state: State>, onGranted: @Co PermissionResult.ShowRational -> finish() } - is Lce.Error -> finish() + is Lce.Error -> GenericError(message = "Store permission required", label = "Close") { + finish() + } + is Lce.Loading -> { // loading should be quick, let's avoid displaying anything } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt index 8770857..daae82d 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryScreen.kt @@ -51,7 +51,6 @@ fun ImageGalleryScreen(viewModel: ImageGalleryViewModel, onTopLevelBack: () -> U } - @Composable fun ImageGalleryFolders(state: ImageGalleryPage.Folders, onClick: (Folder) -> Unit, onRetry: () -> Unit) { val screenWidth = LocalConfiguration.current.screenWidthDp diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt index 6d57470..889ecf4 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt @@ -119,7 +119,7 @@ private fun ProfilePage(context: Context, viewModel: ProfileViewModel, profile: } @Composable -private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) { +private fun SpiderItemScope.Invitations(viewModel: ProfileViewModel, invitations: Page.Invitations) { when (val state = invitations.content) { is Lce.Loading -> CenteredLoading() is Lce.Content -> { @@ -147,7 +147,7 @@ private fun Invitations(viewModel: ProfileViewModel, invitations: Page.Invitatio } } - is Lce.Error -> TODO() + is Lce.Error -> GenericError(label = "Go back", cause = state.cause) { goBack() } } } 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 4ffb0a9..edf9bc3 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 @@ -63,7 +63,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, } Spider(currentPage = viewModel.state.page, onNavigate = onNavigate) { item(Page.Routes.root) { - RootSettings(it) { viewModel.onClick(it) } + RootSettings(it, onClick = { viewModel.onClick(it) }, onRetry = { viewModel.start() }) } item(Page.Routes.encryption) { Encryption(viewModel, it) @@ -180,7 +180,7 @@ internal fun SettingsScreen(viewModel: SettingsViewModel, onSignOut: () -> Unit, } @Composable -private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) { +private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit, onRetry: () -> Unit) { when (val content = page.content) { is Lce.Content -> { LazyColumn( @@ -226,12 +226,10 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) { } } - is Lce.Error -> { - // TODO - } + is Lce.Error -> GenericError(cause = content.cause, action = onRetry) is Lce.Loading -> { - // TODO + // Should be quick enough to avoid needing a loading state } } } @@ -264,7 +262,7 @@ private fun PushProviders(viewModel: SettingsViewModel, state: Page.PushProvider } } - is Lce.Error -> TODO() + is Lce.Error -> GenericError(cause = lce.cause) { viewModel.fetchPushProviders() } } } diff --git a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogScreen.kt b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogScreen.kt index 8a7d76e..50cbfee 100644 --- a/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogScreen.kt +++ b/features/settings/src/main/kotlin/app/dapk/st/settings/eventlogger/EventLogScreen.kt @@ -14,6 +14,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.dapk.st.core.AppLogTag import app.dapk.st.core.Lce +import app.dapk.st.core.components.CenteredLoading +import app.dapk.st.design.components.GenericError import app.dapk.st.matrix.common.MatrixLogTag private val filterItems = listOf(null) + (MatrixLogTag.values().map { it.key } + AppLogTag.values().map { it.key }).distinct() @@ -33,11 +35,13 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) { viewModel.selectLog(it, filter = null) } } + else -> { Events( selectedPageContent = state.selectedState, onExit = { viewModel.exitLog() }, - onSelectTag = { viewModel.selectLog(state.selectedState.selectedPage, it) } + onSelectTag = { viewModel.selectLog(state.selectedState.selectedPage, it) }, + onRetry = { viewModel.start() }, ) } } @@ -46,6 +50,7 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) { is Lce.Error -> { // TODO } + is Lce.Loading -> { // TODO } @@ -69,7 +74,7 @@ private fun LogKeysList(keys: List, onSelected: (String) -> Unit) { } @Composable -private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSelectTag: (String?) -> Unit) { +private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSelectTag: (String?) -> Unit, onRetry: () -> Unit) { BackHandler(onBack = onExit) when (val content = selectedPageContent.content) { is Lce.Content -> { @@ -112,9 +117,8 @@ private fun Events(selectedPageContent: SelectedState, onExit: () -> Unit, onSel } } } - is Lce.Error -> TODO() - is Lce.Loading -> { - // TODO - } + + is Lce.Error -> GenericError(cause = content.cause, action = onRetry) + is Lce.Loading -> CenteredLoading() } } \ No newline at end of file From 94731d006e2c8c3c0831b71c3149066c9f4ce19b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 15:55:10 +0100 Subject: [PATCH 14/43] reusing error component in login screen --- features/login/build.gradle | 1 + .../kotlin/app/dapk/st/login/LoginScreen.kt | 41 ++----------------- 2 files changed, 5 insertions(+), 37 deletions(-) diff --git a/features/login/build.gradle b/features/login/build.gradle index 3a3af95..21dcd44 100644 --- a/features/login/build.gradle +++ b/features/login/build.gradle @@ -7,5 +7,6 @@ dependencies { implementation project(":matrix:services:auth") implementation project(":matrix:services:profile") implementation project(":matrix:services:crypto") + implementation project(":design-library") implementation project(":core") } \ No newline at end of file diff --git a/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt b/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt index 2271c9a..fd56d73 100644 --- a/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt +++ b/features/login/src/main/kotlin/app/dapk/st/login/LoginScreen.kt @@ -1,7 +1,6 @@ package app.dapk.st.login import android.widget.Toast -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -32,6 +31,8 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.dapk.st.core.StartObserving +import app.dapk.st.core.components.CenteredLoading +import app.dapk.st.design.components.GenericError import app.dapk.st.login.LoginEvent.LoginComplete import app.dapk.st.login.LoginScreenState.* @@ -49,42 +50,8 @@ fun LoginScreen(loginViewModel: LoginViewModel, onLoggedIn: () -> Unit) { val keyboardController = LocalSoftwareKeyboardController.current when (val state = loginViewModel.state) { - is Error -> { - val openDetailsDialog = remember { mutableStateOf(false) } - - if (openDetailsDialog.value) { - AlertDialog( - onDismissRequest = { openDetailsDialog.value = false }, - confirmButton = { - Button(onClick = { openDetailsDialog.value = false }) { - Text("OK") - } - }, - title = { Text("Details") }, - text = { - Text(state.cause.message ?: "Unknown") - } - ) - } - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Column(horizontalAlignment = Alignment.CenterHorizontally) { - Text("Something went wrong") - Text("Tap for more details".uppercase(), fontSize = 12.sp, modifier = Modifier.clickable { openDetailsDialog.value = true }.padding(12.dp)) - Spacer(modifier = Modifier.height(12.dp)) - Button(onClick = { - loginViewModel.start() - }) { - Text("Retry".uppercase()) - } - } - } - } - - Loading -> { - Box(contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } + is Error -> GenericError(cause = state.cause, action = { loginViewModel.start() }) + Loading -> CenteredLoading() is Content -> Row { From 128c2db4321ad7999f9ac7180bb05d22a916dff4 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 16:52:33 +0100 Subject: [PATCH 15/43] starting engine migration - add a chat-engine abstraction - create a matrix-chat-engine implementation - starts by porting the initial creation and directory listing --- app/build.gradle | 3 + .../kotlin/app/dapk/st/graph/AppModule.kt | 31 +- chat-engine/build.gradle | 12 + .../kotlin/app/dapk/st/engine/ChatEngine.kt | 46 +++ features/directory/build.gradle | 4 +- .../st/directory/DirectoryListingScreen.kt | 9 +- .../app/dapk/st/directory/DirectoryModule.kt | 20 +- .../app/dapk/st/directory/DirectoryState.kt | 2 + .../dapk/st/directory/DirectoryViewModel.kt | 5 +- .../app/dapk/st/directory/ShortcutHandler.kt | 2 +- matrix-chat-engine/build.gradle | 23 ++ .../app/dapk/st/engine}/DirectoryUseCase.kt | 29 +- .../app/dapk/st/engine/MappingExtensions.kt | 27 ++ .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 317 ++++++++++++++++++ settings.gradle | 3 + 15 files changed, 483 insertions(+), 50 deletions(-) create mode 100644 chat-engine/build.gradle create mode 100644 chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt create mode 100644 matrix-chat-engine/build.gradle rename {features/directory/src/main/kotlin/app/dapk/st/directory => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/DirectoryUseCase.kt (87%) create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt diff --git a/app/build.gradle b/app/build.gradle index a52087c..0b091ca 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -106,6 +106,9 @@ dependencies { implementation project(":core") + implementation project(":chat-engine") + implementation project(":matrix-chat-engine") + implementation Dependencies.google.androidxComposeUi implementation Dependencies.mavenCentral.ktorAndroid implementation Dependencies.mavenCentral.sqldelightAndroid 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 29abfe9..1bc4ddd 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -17,6 +17,7 @@ import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.directory.DirectoryModule import app.dapk.st.domain.StoreModule +import app.dapk.st.engine.MatrixEngine import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.home.HomeModule import app.dapk.st.home.MainActivity @@ -164,12 +165,8 @@ internal class FeatureModules internal constructor( val directoryModule by unsafeLazy { DirectoryModule( - syncService = matrixModules.sync, - messageService = matrixModules.message, context = context, - credentialsStore = storeModule.value.credentialsStore(), - roomStore = storeModule.value.roomStore(), - roomService = matrixModules.room, + chatEngine = matrixModules.engine, ) } val loginModule by unsafeLazy { @@ -252,6 +249,30 @@ internal class MatrixModules( private val buildMeta: BuildMeta, ) { + val engine by unsafeLazy { + val store = storeModule.value + MatrixEngine.Factory().create( + base64, + buildMeta, + logger, + SmallTalkDeviceNameGenerator(), + coroutineDispatchers, + trackingModule.errorTracker, + imageContentReader, + BackgroundWorkAdapter(workModule.workScheduler()), + store.memberStore(), + store.roomStore(), + store.profileStore(), + store.syncStore(), + store.overviewStore(), + store.filterStore(), + store.localEchoStore, + store.credentialsStore(), + store.knownDevicesStore(), + OlmPersistenceWrapper(store.olmStore(), base64), + ) + } + val matrix by unsafeLazy { val store = storeModule.value val credentialsStore = store.credentialsStore() diff --git a/chat-engine/build.gradle b/chat-engine/build.gradle new file mode 100644 index 0000000..ee08897 --- /dev/null +++ b/chat-engine/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'kotlin' +} + +dependencies { + api Dependencies.mavenCentral.kotlinCoroutinesCore + api project(":matrix:common") + + implementation project(":matrix:services:sync") + implementation project(":matrix:services:message") + implementation project(":matrix:services:room") +} \ No newline at end of file diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt new file mode 100644 index 0000000..556b478 --- /dev/null +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -0,0 +1,46 @@ +package app.dapk.st.engine + +import app.dapk.st.matrix.common.AvatarUrl +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.RoomMember +import kotlinx.coroutines.flow.Flow + +interface ChatEngine { + + fun directory(): Flow + +} + +typealias DirectoryState = List +typealias OverviewState = List + +data class DirectoryItem( + val overview: RoomOverview, + val unreadCount: UnreadCount, + val typing: Typing? +) + +data class RoomOverview( + val roomId: RoomId, + val roomCreationUtc: Long, + val roomName: String?, + val roomAvatarUrl: AvatarUrl?, + val lastMessage: LastMessage?, + val isGroup: Boolean, + val readMarker: EventId?, + val isEncrypted: Boolean, +) { + + data class LastMessage( + val content: String, + val utcTimestamp: Long, + val author: RoomMember, + ) + +} + +@JvmInline +value class UnreadCount(val value: Int) + +data class Typing(val roomId: RoomId, val members: List) diff --git a/features/directory/build.gradle b/features/directory/build.gradle index b005881..44351f5 100644 --- a/features/directory/build.gradle +++ b/features/directory/build.gradle @@ -1,9 +1,7 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":matrix:services:sync") - implementation project(":matrix:services:message") - implementation project(":matrix:services:room") + implementation project(":chat-engine") implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") implementation project(":features:messenger") diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt index 284f728..1c14523 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryListingScreen.kt @@ -38,9 +38,10 @@ import app.dapk.st.design.components.Toolbar import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl import app.dapk.st.directory.DirectoryScreenState.Content import app.dapk.st.directory.DirectoryScreenState.EmptyLoading +import app.dapk.st.engine.DirectoryItem +import app.dapk.st.engine.RoomOverview +import app.dapk.st.engine.Typing import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomOverview -import app.dapk.st.matrix.sync.SyncService import app.dapk.st.messenger.MessengerActivity import kotlinx.coroutines.launch import java.time.Clock @@ -147,7 +148,7 @@ private fun Content(listState: LazyListState, state: Content) { } @Composable -private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock) { +private fun DirectoryItem(room: DirectoryItem, onClick: (RoomId) -> Unit, clock: Clock) { val overview = room.overview val roomName = overview.roomName ?: "Empty room" val hasUnread = room.unreadCount.value > 0 @@ -233,7 +234,7 @@ private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock } @Composable -private fun body(overview: RoomOverview, secondaryText: Color, typing: SyncService.SyncEvent.Typing?) { +private fun body(overview: RoomOverview, secondaryText: Color, typing: Typing?) { val bodySize = 14.sp when { diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt index 5ef40d9..a21ede3 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryModule.kt @@ -2,31 +2,17 @@ package app.dapk.st.directory import android.content.Context import app.dapk.st.core.ProvidableModule -import app.dapk.st.matrix.common.CredentialsStore -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.RoomStore -import app.dapk.st.matrix.sync.SyncService +import app.dapk.st.engine.ChatEngine class DirectoryModule( - private val syncService: SyncService, - private val messageService: MessageService, - private val roomService: RoomService, private val context: Context, - private val credentialsStore: CredentialsStore, - private val roomStore: RoomStore, + private val chatEngine: ChatEngine, ) : ProvidableModule { fun directoryViewModel(): DirectoryViewModel { return DirectoryViewModel( ShortcutHandler(context), - DirectoryUseCase( - syncService, - messageService, - roomService, - credentialsStore, - roomStore, - ) + chatEngine, ) } } \ No newline at end of file diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryState.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryState.kt index 2cf51fa..0dd1a41 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryState.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryState.kt @@ -1,5 +1,7 @@ package app.dapk.st.directory +import app.dapk.st.engine.DirectoryState + sealed interface DirectoryScreenState { object EmptyLoading : DirectoryScreenState diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryViewModel.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryViewModel.kt index d8c4fdc..92757f9 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryViewModel.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryViewModel.kt @@ -2,6 +2,7 @@ package app.dapk.st.directory import androidx.lifecycle.viewModelScope import app.dapk.st.directory.DirectoryScreenState.* +import app.dapk.st.engine.ChatEngine import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.MutableStateFactory import app.dapk.st.viewmodel.defaultStateFactory @@ -12,7 +13,7 @@ import kotlinx.coroutines.launch class DirectoryViewModel( private val shortcutHandler: ShortcutHandler, - private val directoryUseCase: DirectoryUseCase, + private val chatEngine: ChatEngine, factory: MutableStateFactory = defaultStateFactory(), ) : DapkViewModel( initialState = EmptyLoading, @@ -23,7 +24,7 @@ class DirectoryViewModel( fun start() { syncJob = viewModelScope.launch { - directoryUseCase.state().onEach { + chatEngine.directory().onEach { shortcutHandler.onDirectoryUpdate(it.map { it.overview }) state = when (it.isEmpty()) { true -> Empty diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/ShortcutHandler.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/ShortcutHandler.kt index 87b4980..276a8d3 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/ShortcutHandler.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/ShortcutHandler.kt @@ -5,8 +5,8 @@ import android.content.pm.ShortcutInfo import androidx.core.app.Person import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat +import app.dapk.st.engine.RoomOverview import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.messenger.MessengerActivity class ShortcutHandler(private val context: Context) { diff --git a/matrix-chat-engine/build.gradle b/matrix-chat-engine/build.gradle new file mode 100644 index 0000000..140832c --- /dev/null +++ b/matrix-chat-engine/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'kotlin' +} + +dependencies { + api Dependencies.mavenCentral.kotlinCoroutinesCore + + implementation project(":core") + implementation project(":chat-engine") + + implementation project(":domains:olm") + + implementation project(":matrix:matrix") + implementation project(":matrix:matrix-http-ktor") + implementation project(":matrix:services:auth") + implementation project(":matrix:services:sync") + implementation project(":matrix:services:room") + implementation project(":matrix:services:push") + implementation project(":matrix:services:message") + implementation project(":matrix:services:device") + implementation project(":matrix:services:crypto") + implementation project(":matrix:services:profile") +} \ No newline at end of file diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt similarity index 87% rename from features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt index b6a7bf6..b2bec77 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt @@ -1,4 +1,4 @@ -package app.dapk.st.directory +package app.dapk.st.engine import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.RoomId @@ -6,22 +6,12 @@ import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.* +import app.dapk.st.matrix.sync.RoomStore +import app.dapk.st.matrix.sync.SyncService import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing import kotlinx.coroutines.flow.* -@JvmInline -value class UnreadCount(val value: Int) - -typealias DirectoryState = List - -data class RoomFoo( - val overview: RoomOverview, - val unreadCount: UnreadCount, - val typing: Typing? -) - -class DirectoryUseCase( +internal class DirectoryUseCase( private val syncService: SyncService, private val messageService: MessageService, private val roomService: RoomService, @@ -38,10 +28,10 @@ class DirectoryUseCase( syncService.events() ) { overviewState, localEchos, unread, events -> overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview -> - RoomFoo( + DirectoryItem( overview = roomOverview, unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0), - typing = events.filterIsInstance().firstOrNull { it.roomId == roomOverview.roomId } + typing = events.filterIsInstance().firstOrNull { it.roomId == roomOverview.roomId }?.engine() ) } } @@ -50,7 +40,7 @@ class DirectoryUseCase( private fun overviewDatasource() = combine( syncService.startSyncing().map { false }.onStart { emit(true) }, - syncService.overview() + syncService.overview().map { it.map { it.engine() } } ) { isFirstLoad, overview -> when { isFirstLoad && overview.isEmpty() -> null @@ -81,7 +71,7 @@ class DirectoryUseCase( val latestEcho = echos.maxByOrNull { it.timestampUtc } return if (latestEcho != null && latestEcho.timestampUtc > (this.lastMessage?.utcTimestamp ?: 0)) { this.copy( - lastMessage = LastMessage( + lastMessage = RoomOverview.LastMessage( content = when (val message = latestEcho.message) { is MessageService.Message.TextMessage -> message.content.body is MessageService.Message.ImageMessage -> "\uD83D\uDCF7" @@ -96,3 +86,6 @@ class DirectoryUseCase( } } + + + diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt new file mode 100644 index 0000000..0486699 --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt @@ -0,0 +1,27 @@ +package app.dapk.st.engine + +import app.dapk.st.matrix.sync.LastMessage as MatrixLastMessage +import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview +import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing as MatrixTyping + +fun MatrixRoomOverview.engine() = RoomOverview( + this.roomId, + this.roomCreationUtc, + this.roomName, + this.roomAvatarUrl, + this.lastMessage?.engine(), + this.isGroup, + this.readMarker, + this.isEncrypted +) + +fun MatrixLastMessage.engine() = RoomOverview.LastMessage( + this.content, + this.utcTimestamp, + this.author, +) + +fun MatrixTyping.engine() = Typing( + this.roomId, + this.members, +) \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt new file mode 100644 index 0000000..e58235d --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -0,0 +1,317 @@ +package app.dapk.st.engine + +import app.dapk.st.core.Base64 +import app.dapk.st.core.BuildMeta +import app.dapk.st.core.CoroutineDispatchers +import app.dapk.st.core.SingletonFlows +import app.dapk.st.core.extensions.ErrorTracker +import app.dapk.st.matrix.MatrixClient +import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator +import app.dapk.st.matrix.auth.installAuthService +import app.dapk.st.matrix.common.* +import app.dapk.st.matrix.crypto.RoomMembersProvider +import app.dapk.st.matrix.crypto.Verification +import app.dapk.st.matrix.crypto.cryptoService +import app.dapk.st.matrix.crypto.installCryptoService +import app.dapk.st.matrix.device.KnownDeviceStore +import app.dapk.st.matrix.device.deviceService +import app.dapk.st.matrix.device.installEncryptionService +import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory +import app.dapk.st.matrix.message.* +import app.dapk.st.matrix.message.internal.ImageContentReader +import app.dapk.st.matrix.push.installPushService +import app.dapk.st.matrix.room.* +import app.dapk.st.matrix.sync.* +import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent +import app.dapk.st.matrix.sync.internal.room.MessageDecrypter +import app.dapk.st.olm.DeviceKeyFactory +import app.dapk.st.olm.OlmStore +import app.dapk.st.olm.OlmWrapper +import java.time.Clock + +class MatrixEngine internal constructor( + private val directoryUseCase: Lazy, +) : ChatEngine { + + override fun directory() = directoryUseCase.value.state() + + class Factory { + + fun create( + base64: Base64, + buildMeta: BuildMeta, + logger: MatrixLogger, + nameGenerator: DeviceDisplayNameGenerator, + coroutineDispatchers: CoroutineDispatchers, + errorTracker: ErrorTracker, + imageContentReader: ImageContentReader, + backgroundScheduler: BackgroundScheduler, + memberStore: MemberStore, + roomStore: RoomStore, + profileStore: ProfileStore, + syncStore: SyncStore, + overviewStore: OverviewStore, + filterStore: FilterStore, + localEchoStore: LocalEchoStore, + credentialsStore: CredentialsStore, + knownDeviceStore: KnownDeviceStore, + olmStore: OlmStore, + ): ChatEngine { + val matrix = MatrixFactory.createMatrix( + base64, + buildMeta, + logger, + nameGenerator, + coroutineDispatchers, + errorTracker, + imageContentReader, + backgroundScheduler, + memberStore, + roomStore, + profileStore, + syncStore, + overviewStore, + filterStore, + localEchoStore, + credentialsStore, + knownDeviceStore, + olmStore + ) + + val directoryUseCase = unsafeLazy { + DirectoryUseCase( + matrix.syncService(), + matrix.messageService(), + matrix.roomService(), + credentialsStore, + roomStore + ) + } + + return MatrixEngine(directoryUseCase) + + } + + } + +} + + +object MatrixFactory { + + fun createMatrix( + base64: Base64, + buildMeta: BuildMeta, + logger: MatrixLogger, + nameGenerator: DeviceDisplayNameGenerator, + coroutineDispatchers: CoroutineDispatchers, + errorTracker: ErrorTracker, + imageContentReader: ImageContentReader, + backgroundScheduler: BackgroundScheduler, + memberStore: MemberStore, + roomStore: RoomStore, + profileStore: ProfileStore, + syncStore: SyncStore, + overviewStore: OverviewStore, + filterStore: FilterStore, + localEchoStore: LocalEchoStore, + credentialsStore: CredentialsStore, + knownDeviceStore: KnownDeviceStore, + olmStore: OlmStore, + ) = MatrixClient( + KtorMatrixHttpClientFactory( + credentialsStore, + includeLogging = buildMeta.isDebug, + ), + logger + ).also { + it.install { + installAuthService(credentialsStore, nameGenerator) + installEncryptionService(knownDeviceStore) + + val singletonFlows = SingletonFlows(coroutineDispatchers) + val olm = OlmWrapper( + olmStore = olmStore, + singletonFlows = singletonFlows, + jsonCanonicalizer = JsonCanonicalizer(), + deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()), + errorTracker = errorTracker, + logger = logger, + clock = Clock.systemUTC(), + coroutineDispatchers = coroutineDispatchers, + ) + installCryptoService( + credentialsStore, + olm, + roomMembersProvider = { services -> + RoomMembersProvider { + services.roomService().joinedMembers(it).map { it.userId } + } + }, + base64 = base64, + coroutineDispatchers = coroutineDispatchers, + ) + installMessageService( + localEchoStore, + backgroundScheduler, + imageContentReader, + messageEncrypter = { + val cryptoService = it.cryptoService() + MessageEncrypter { message -> + val result = cryptoService.encrypt( + roomId = message.roomId, + credentials = credentialsStore.credentials()!!, + messageJson = message.contents, + ) + + MessageEncrypter.EncryptedMessagePayload( + result.algorithmName, + result.senderKey, + result.cipherText, + result.sessionId, + result.deviceId, + ) + } + }, + mediaEncrypter = { + val cryptoService = it.cryptoService() + MediaEncrypter { input -> + val result = cryptoService.encrypt(input) + MediaEncrypter.Result( + uri = result.uri, + contentLength = result.contentLength, + algorithm = result.algorithm, + ext = result.ext, + keyOperations = result.keyOperations, + kty = result.kty, + k = result.k, + iv = result.iv, + hashes = result.hashes, + v = result.v, + ) + } + }, + ) + + installRoomService( + memberStore, + roomMessenger = { + val messageService = it.messageService() + object : RoomMessenger { + override suspend fun enableEncryption(roomId: RoomId) { + messageService.sendEventMessage( + roomId, MessageService.EventMessage.Encryption( + algorithm = AlgorithmName("m.megolm.v1.aes-sha2") + ) + ) + } + } + }, + roomInviteRemover = { + overviewStore.removeInvites(listOf(it)) + } + ) + + installProfileService(profileStore, singletonFlows, credentialsStore) + + installSyncService( + credentialsStore, + overviewStore, + roomStore, + syncStore, + filterStore, + deviceNotifier = { services -> + val encryption = services.deviceService() + val crypto = services.cryptoService() + DeviceNotifier { userIds, syncToken -> + encryption.updateStaleDevices(userIds) + crypto.updateOlmSession(userIds, syncToken) + } + }, + messageDecrypter = { serviceProvider -> + val cryptoService = serviceProvider.cryptoService() + MessageDecrypter { + cryptoService.decrypt(it) + } + }, + keySharer = { serviceProvider -> + val cryptoService = serviceProvider.cryptoService() + KeySharer { sharedRoomKeys -> + cryptoService.importRoomKeys(sharedRoomKeys) + } + }, + verificationHandler = { services -> + val cryptoService = services.cryptoService() + VerificationHandler { apiEvent -> + logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it") + cryptoService.onVerificationEvent( + when (apiEvent) { + is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested( + apiEvent.sender, + apiEvent.content.fromDevice, + apiEvent.content.transactionId, + apiEvent.content.methods, + apiEvent.content.timestampPosix, + ) + + is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready( + apiEvent.sender, + apiEvent.content.fromDevice, + apiEvent.content.transactionId, + apiEvent.content.methods, + ) + + is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started( + apiEvent.sender, + apiEvent.content.fromDevice, + apiEvent.content.method, + apiEvent.content.protocols, + apiEvent.content.hashes, + apiEvent.content.codes, + apiEvent.content.short, + apiEvent.content.transactionId, + ) + + is ApiToDeviceEvent.VerificationCancel -> TODO() + is ApiToDeviceEvent.VerificationAccept -> TODO() + is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key( + apiEvent.sender, + apiEvent.content.transactionId, + apiEvent.content.key + ) + + is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac( + apiEvent.sender, + apiEvent.content.transactionId, + apiEvent.content.keys, + apiEvent.content.mac, + ) + } + ) + } + }, + oneTimeKeyProducer = { services -> + val cryptoService = services.cryptoService() + MaybeCreateMoreKeys { + cryptoService.maybeCreateMoreKeys(it) + } + }, + roomMembersService = { services -> + val roomService = services.roomService() + object : RoomMembersService { + override suspend fun find(roomId: RoomId, userIds: List) = roomService.findMembers(roomId, userIds) + override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId) + override suspend fun insert(roomId: RoomId, members: List) = roomService.insertMembers(roomId, members) + } + }, + errorTracker = errorTracker, + coroutineDispatchers = coroutineDispatchers, + ) + + installPushService(credentialsStore) + } + } + +} + +fun unsafeLazy(initializer: () -> T): Lazy = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer) diff --git a/settings.gradle b/settings.gradle index 4b8bbf4..bb28dab 100644 --- a/settings.gradle +++ b/settings.gradle @@ -55,3 +55,6 @@ include ':matrix:services:profile' include ':core' include ':test-harness' + +include ':chat-engine' +include ':matrix-chat-engine' \ No newline at end of file From 492d7df9ac1c6d12928b7ed9160c0921f36756ae Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 17:24:05 +0100 Subject: [PATCH 16/43] moving login and home modules to the chat engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 7 +- chat-engine/build.gradle | 4 -- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 42 ++---------- .../main/kotlin/app/dapk/st/engine/Models.kt | 64 +++++++++++++++++++ features/home/build.gradle | 4 +- .../kotlin/app/dapk/st/home/HomeModule.kt | 9 +-- .../main/kotlin/app/dapk/st/home/HomeState.kt | 4 +- .../kotlin/app/dapk/st/home/HomeViewModel.kt | 12 ++-- features/login/build.gradle | 4 +- .../kotlin/app/dapk/st/login/LoginModule.kt | 8 +-- .../app/dapk/st/login/LoginViewModel.kt | 20 +++--- .../app/dapk/st/engine/MappingExtensions.kt | 39 ++++++++++- .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 61 +++++++++++------- 13 files changed, 174 insertions(+), 104 deletions(-) create mode 100644 chat-engine/src/main/kotlin/app/dapk/st/engine/Models.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 1bc4ddd..28d6b80 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -25,7 +25,6 @@ import app.dapk.st.imageloader.ImageLoaderModule import app.dapk.st.login.LoginModule import app.dapk.st.matrix.MatrixClient import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator -import app.dapk.st.matrix.auth.authService import app.dapk.st.matrix.auth.installAuthService import app.dapk.st.matrix.common.* import app.dapk.st.matrix.crypto.RoomMembersProvider @@ -171,9 +170,8 @@ internal class FeatureModules internal constructor( } val loginModule by unsafeLazy { LoginModule( - matrixModules.auth, + matrixModules.engine, domainModules.pushModule, - matrixModules.profile, trackingModule.errorTracker ) } @@ -191,7 +189,7 @@ internal class FeatureModules internal constructor( storeModule.value.messageStore(), ) } - val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) } + val homeModule by unsafeLazy { HomeModule(matrixModules.engine, storeModule.value, buildMeta) } val settingsModule by unsafeLazy { SettingsModule( storeModule.value, @@ -473,7 +471,6 @@ internal class MatrixModules( } } - val auth by unsafeLazy { matrix.authService() } val push by unsafeLazy { matrix.pushService() } val sync by unsafeLazy { matrix.syncService() } val message by unsafeLazy { matrix.messageService() } diff --git a/chat-engine/build.gradle b/chat-engine/build.gradle index ee08897..05013e0 100644 --- a/chat-engine/build.gradle +++ b/chat-engine/build.gradle @@ -5,8 +5,4 @@ plugins { dependencies { api Dependencies.mavenCentral.kotlinCoroutinesCore api project(":matrix:common") - - implementation project(":matrix:services:sync") - implementation project(":matrix:services:message") - implementation project(":matrix:services:room") } \ No newline at end of file diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 556b478..339ba79 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -1,46 +1,14 @@ package app.dapk.st.engine -import app.dapk.st.matrix.common.AvatarUrl -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.RoomMember import kotlinx.coroutines.flow.Flow interface ChatEngine { fun directory(): Flow + fun invites(): Flow + + suspend fun login(request: LoginRequest): LoginResult + + suspend fun me(forceRefresh: Boolean): Me } - -typealias DirectoryState = List -typealias OverviewState = List - -data class DirectoryItem( - val overview: RoomOverview, - val unreadCount: UnreadCount, - val typing: Typing? -) - -data class RoomOverview( - val roomId: RoomId, - val roomCreationUtc: Long, - val roomName: String?, - val roomAvatarUrl: AvatarUrl?, - val lastMessage: LastMessage?, - val isGroup: Boolean, - val readMarker: EventId?, - val isEncrypted: Boolean, -) { - - data class LastMessage( - val content: String, - val utcTimestamp: Long, - val author: RoomMember, - ) - -} - -@JvmInline -value class UnreadCount(val value: Int) - -data class Typing(val roomId: RoomId, val members: List) diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt new file mode 100644 index 0000000..14d1189 --- /dev/null +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt @@ -0,0 +1,64 @@ +package app.dapk.st.engine + +import app.dapk.st.matrix.common.* + +typealias DirectoryState = List +typealias OverviewState = List +typealias InviteState = List + +data class DirectoryItem( + val overview: RoomOverview, + val unreadCount: UnreadCount, + val typing: Typing? +) + +data class RoomOverview( + val roomId: RoomId, + val roomCreationUtc: Long, + val roomName: String?, + val roomAvatarUrl: AvatarUrl?, + val lastMessage: LastMessage?, + val isGroup: Boolean, + val readMarker: EventId?, + val isEncrypted: Boolean, +) { + + data class LastMessage( + val content: String, + val utcTimestamp: Long, + val author: RoomMember, + ) + +} + +data class RoomInvite( + val from: RoomMember, + val roomId: RoomId, + val inviteMeta: InviteMeta, +) { + sealed class InviteMeta { + object DirectMessage : InviteMeta() + data class Room(val roomName: String? = null) : InviteMeta() + } + +} + +@JvmInline +value class UnreadCount(val value: Int) + +data class Typing(val roomId: RoomId, val members: List) + +data class LoginRequest(val userName: String, val password: String, val serverUrl: String?) + +sealed interface LoginResult { + data class Success(val userCredentials: UserCredentials) : LoginResult + object MissingWellKnown : LoginResult + data class Error(val cause: Throwable) : LoginResult +} + +data class Me( + val userId: UserId, + val displayName: String?, + val avatarUrl: AvatarUrl?, + val homeServerUrl: HomeServerUrl, +) diff --git a/features/home/build.gradle b/features/home/build.gradle index 0422dfa..41343e5 100644 --- a/features/home/build.gradle +++ b/features/home/build.gradle @@ -1,9 +1,7 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":matrix:services:profile") - implementation project(":matrix:services:crypto") - implementation project(":matrix:services:sync") + implementation project(":chat-engine") implementation project(":features:directory") implementation project(":features:login") implementation project(":features:settings") diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt index b49c5cc..210b5aa 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeModule.kt @@ -4,31 +4,28 @@ import app.dapk.st.core.BuildMeta import app.dapk.st.core.ProvidableModule import app.dapk.st.directory.DirectoryViewModel import app.dapk.st.domain.StoreModule +import app.dapk.st.engine.ChatEngine import app.dapk.st.login.LoginViewModel -import app.dapk.st.matrix.room.ProfileService -import app.dapk.st.matrix.sync.SyncService import app.dapk.st.profile.ProfileViewModel class HomeModule( + private val chatEngine: ChatEngine, private val storeModule: StoreModule, - private val profileService: ProfileService, - private val syncService: SyncService, private val buildMeta: BuildMeta, ) : ProvidableModule { fun homeViewModel(directory: DirectoryViewModel, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel { return HomeViewModel( + chatEngine, storeModule.credentialsStore(), directory, login, profileViewModel, - profileService, storeModule.cacheCleaner(), BetaVersionUpgradeUseCase( storeModule.applicationStore(), buildMeta, ), - syncService, ) } diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt index 7bf0114..c7d74a3 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt @@ -4,13 +4,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Settings import androidx.compose.ui.graphics.vector.ImageVector -import app.dapk.st.matrix.room.ProfileService +import app.dapk.st.engine.Me sealed interface HomeScreenState { object Loading : HomeScreenState object SignedOut : HomeScreenState - data class SignedIn(val page: Page, val me: ProfileService.Me, val invites: Int) : HomeScreenState + data class SignedIn(val page: Page, val me: Me, val invites: Int) : HomeScreenState enum class Page(val icon: ImageVector) { Directory(Icons.Filled.Menu), diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index 728c6c0..456f0a0 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -3,12 +3,11 @@ package app.dapk.st.home import androidx.lifecycle.viewModelScope import app.dapk.st.directory.DirectoryViewModel import app.dapk.st.domain.StoreCleaner +import app.dapk.st.engine.ChatEngine import app.dapk.st.home.HomeScreenState.* import app.dapk.st.login.LoginViewModel import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.isSignedIn -import app.dapk.st.matrix.room.ProfileService -import app.dapk.st.matrix.sync.SyncService import app.dapk.st.profile.ProfileViewModel import app.dapk.st.viewmodel.DapkViewModel import kotlinx.coroutines.CoroutineScope @@ -20,14 +19,13 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch class HomeViewModel( + private val chatEngine: ChatEngine, private val credentialsProvider: CredentialsStore, private val directoryViewModel: DirectoryViewModel, private val loginViewModel: LoginViewModel, private val profileViewModel: ProfileViewModel, - private val profileService: ProfileService, private val cacheCleaner: StoreCleaner, private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase, - private val syncService: SyncService, ) : DapkViewModel( initialState = Loading ) { @@ -56,8 +54,8 @@ class HomeViewModel( } private suspend fun initialHomeContent(): SignedIn { - val me = profileService.me(forceRefresh = false) - val initialInvites = syncService.invites().first().size + val me = chatEngine.me(forceRefresh = false) + val initialInvites = chatEngine.invites().first().size return SignedIn(Page.Directory, me, invites = initialInvites) } @@ -70,7 +68,7 @@ class HomeViewModel( private fun CoroutineScope.listenForInviteChanges() { listenForInvitesJob?.cancel() - listenForInvitesJob = syncService.invites() + listenForInvitesJob = chatEngine.invites() .onEach { invites -> when (val currentState = state) { is SignedIn -> updateState { currentState.copy(invites = invites.size) } diff --git a/features/login/build.gradle b/features/login/build.gradle index 21dcd44..ac3340b 100644 --- a/features/login/build.gradle +++ b/features/login/build.gradle @@ -1,12 +1,10 @@ applyAndroidComposeLibraryModule(project) dependencies { + implementation project(":chat-engine") implementation project(":domains:android:compose-core") implementation project(":domains:android:push") implementation project(":domains:android:viewmodel") - implementation project(":matrix:services:auth") - implementation project(":matrix:services:profile") - implementation project(":matrix:services:crypto") implementation project(":design-library") implementation project(":core") } \ No newline at end of file diff --git a/features/login/src/main/kotlin/app/dapk/st/login/LoginModule.kt b/features/login/src/main/kotlin/app/dapk/st/login/LoginModule.kt index 9180e52..c745f9f 100644 --- a/features/login/src/main/kotlin/app/dapk/st/login/LoginModule.kt +++ b/features/login/src/main/kotlin/app/dapk/st/login/LoginModule.kt @@ -2,18 +2,16 @@ package app.dapk.st.login import app.dapk.st.core.ProvidableModule import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.matrix.auth.AuthService -import app.dapk.st.matrix.room.ProfileService +import app.dapk.st.engine.ChatEngine import app.dapk.st.push.PushModule class LoginModule( - private val authService: AuthService, + private val chatEngine: ChatEngine, private val pushModule: PushModule, - private val profileService: ProfileService, private val errorTracker: ErrorTracker, ) : ProvidableModule { fun loginViewModel(): LoginViewModel { - return LoginViewModel(authService, pushModule.pushTokenRegistrar(), profileService, errorTracker) + return LoginViewModel(chatEngine, pushModule.pushTokenRegistrar(), errorTracker) } } \ No newline at end of file diff --git a/features/login/src/main/kotlin/app/dapk/st/login/LoginViewModel.kt b/features/login/src/main/kotlin/app/dapk/st/login/LoginViewModel.kt index f82c80d..c8efb13 100644 --- a/features/login/src/main/kotlin/app/dapk/st/login/LoginViewModel.kt +++ b/features/login/src/main/kotlin/app/dapk/st/login/LoginViewModel.kt @@ -3,10 +3,11 @@ package app.dapk.st.login import androidx.lifecycle.viewModelScope import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.logP +import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.LoginRequest +import app.dapk.st.engine.LoginResult import app.dapk.st.login.LoginEvent.LoginComplete import app.dapk.st.login.LoginScreenState.* -import app.dapk.st.matrix.auth.AuthService -import app.dapk.st.matrix.room.ProfileService import app.dapk.st.push.PushTokenRegistrar import app.dapk.st.viewmodel.DapkViewModel import kotlinx.coroutines.async @@ -14,9 +15,8 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch class LoginViewModel( - private val authService: AuthService, + private val chatEngine: ChatEngine, private val pushTokenRegistrar: PushTokenRegistrar, - private val profileService: ProfileService, private val errorTracker: ErrorTracker, ) : DapkViewModel( initialState = Content(showServerUrl = false) @@ -28,8 +28,8 @@ class LoginViewModel( state = Loading viewModelScope.launch { logP("login") { - when (val result = authService.login(AuthService.LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) { - is AuthService.LoginResult.Success -> { + when (val result = chatEngine.login(LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) { + is LoginResult.Success -> { runCatching { listOf( async { pushTokenRegistrar.registerCurrentToken() }, @@ -38,11 +38,13 @@ class LoginViewModel( } _events.tryEmit(LoginComplete) } - is AuthService.LoginResult.Error -> { + + is LoginResult.Error -> { errorTracker.track(result.cause) state = Error(result.cause) } - AuthService.LoginResult.MissingWellKnown -> { + + LoginResult.MissingWellKnown -> { _events.tryEmit(LoginEvent.WellKnownMissing) state = Content(showServerUrl = true) } @@ -51,7 +53,7 @@ class LoginViewModel( } } - private suspend fun preloadMe() = profileService.me(forceRefresh = false) + private suspend fun preloadMe() = chatEngine.me(forceRefresh = false) fun start() { val showServerUrl = previousState?.let { it is Content && it.showServerUrl } ?: false diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt index 0486699..6e26cc4 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt @@ -1,6 +1,12 @@ package app.dapk.st.engine +import app.dapk.st.matrix.auth.AuthService +import app.dapk.st.matrix.sync.InviteMeta +import app.dapk.st.matrix.auth.AuthService.LoginRequest as MatrixLoginRequest +import app.dapk.st.matrix.auth.AuthService.LoginResult as MatrixLoginResult +import app.dapk.st.matrix.room.ProfileService.Me as MatrixMe import app.dapk.st.matrix.sync.LastMessage as MatrixLastMessage +import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing as MatrixTyping @@ -24,4 +30,35 @@ fun MatrixLastMessage.engine() = RoomOverview.LastMessage( fun MatrixTyping.engine() = Typing( this.roomId, this.members, -) \ No newline at end of file +) + +fun LoginRequest.engine() = MatrixLoginRequest( + this.userName, + this.password, + this.serverUrl +) + +fun MatrixLoginResult.engine() = when (this) { + is AuthService.LoginResult.Error -> LoginResult.Error(this.cause) + AuthService.LoginResult.MissingWellKnown -> LoginResult.MissingWellKnown + is AuthService.LoginResult.Success -> LoginResult.Success(this.userCredentials) +} + +fun MatrixMe.engine() = Me( + this.userId, + this.displayName, + this.avatarUrl, + this.homeServerUrl, +) + +fun MatrixRoomInvite.engine() = RoomInvite( + this.from, + this.roomId, + this.inviteMeta.engine(), +) + +fun InviteMeta.engine() = when (this) { + InviteMeta.DirectMessage -> RoomInvite.InviteMeta.DirectMessage + is InviteMeta.Room -> RoomInvite.InviteMeta.Room(this.roomName) +} + diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index e58235d..9caefa5 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -7,6 +7,7 @@ import app.dapk.st.core.SingletonFlows import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.matrix.MatrixClient import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator +import app.dapk.st.matrix.auth.authService import app.dapk.st.matrix.auth.installAuthService import app.dapk.st.matrix.common.* import app.dapk.st.matrix.crypto.RoomMembersProvider @@ -27,13 +28,27 @@ import app.dapk.st.matrix.sync.internal.room.MessageDecrypter import app.dapk.st.olm.DeviceKeyFactory import app.dapk.st.olm.OlmStore import app.dapk.st.olm.OlmWrapper +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import java.time.Clock class MatrixEngine internal constructor( private val directoryUseCase: Lazy, + private val matrix: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() + override fun invites(): Flow { + return matrix.value.syncService().invites().map { it.map { it.engine() } } + } + + override suspend fun login(request: LoginRequest): LoginResult { + return matrix.value.authService().login(request.engine()).engine() + } + + override suspend fun me(forceRefresh: Boolean): Me { + return matrix.value.profileService().me(forceRefresh).engine() + } class Factory { @@ -57,28 +72,30 @@ class MatrixEngine internal constructor( knownDeviceStore: KnownDeviceStore, olmStore: OlmStore, ): ChatEngine { - val matrix = MatrixFactory.createMatrix( - base64, - buildMeta, - logger, - nameGenerator, - coroutineDispatchers, - errorTracker, - imageContentReader, - backgroundScheduler, - memberStore, - roomStore, - profileStore, - syncStore, - overviewStore, - filterStore, - localEchoStore, - credentialsStore, - knownDeviceStore, - olmStore - ) - + val lazyMatrix = unsafeLazy { + MatrixFactory.createMatrix( + base64, + buildMeta, + logger, + nameGenerator, + coroutineDispatchers, + errorTracker, + imageContentReader, + backgroundScheduler, + memberStore, + roomStore, + profileStore, + syncStore, + overviewStore, + filterStore, + localEchoStore, + credentialsStore, + knownDeviceStore, + olmStore + ) + } val directoryUseCase = unsafeLazy { + val matrix = lazyMatrix.value DirectoryUseCase( matrix.syncService(), matrix.messageService(), @@ -88,7 +105,7 @@ class MatrixEngine internal constructor( ) } - return MatrixEngine(directoryUseCase) + return MatrixEngine(directoryUseCase, lazyMatrix) } From d2e8a29af8060b8b13f0516c3d7b6284959580cb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 17:44:18 +0100 Subject: [PATCH 17/43] porting settings module to chat engine --- .../main/kotlin/app/dapk/st/graph/AppModule.kt | 3 +-- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 7 +++++++ .../main/kotlin/app/dapk/st/engine/Models.kt | 17 +++++++++++++++++ features/navigator/build.gradle | 2 +- features/settings/build.gradle | 3 +-- .../app/dapk/st/settings/SettingsModule.kt | 9 +++------ .../app/dapk/st/settings/SettingsScreen.kt | 2 +- .../app/dapk/st/settings/SettingsState.kt | 3 +-- .../app/dapk/st/settings/SettingsViewModel.kt | 12 +++++------- .../app/dapk/st/engine/MappingExtensions.kt | 15 +++++++++++++++ .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 14 +++++++++++++- 11 files changed, 65 insertions(+), 22 deletions(-) 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 28d6b80..a1ea937 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -192,10 +192,9 @@ internal class FeatureModules internal constructor( val homeModule by unsafeLazy { HomeModule(matrixModules.engine, storeModule.value, buildMeta) } val settingsModule by unsafeLazy { SettingsModule( + matrixModules.engine, storeModule.value, pushModule, - matrixModules.crypto, - matrixModules.sync, context.contentResolver, buildMeta, deviceMeta, diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 339ba79..f5e7fe8 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -1,14 +1,21 @@ package app.dapk.st.engine +import app.dapk.st.matrix.common.RoomId import kotlinx.coroutines.flow.Flow +import java.io.InputStream interface ChatEngine { fun directory(): Flow + fun invites(): Flow suspend fun login(request: LoginRequest): LoginResult suspend fun me(forceRefresh: Boolean): Me + suspend fun refresh(roomIds: List) + + suspend fun InputStream.importRoomKeys(password: String): Flow + } diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt index 14d1189..df5631a 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt @@ -62,3 +62,20 @@ data class Me( val avatarUrl: AvatarUrl?, val homeServerUrl: HomeServerUrl, ) + +sealed interface ImportResult { + data class Success(val roomIds: Set, val totalImportedKeysCount: Long) : ImportResult + data class Error(val cause: Type) : ImportResult { + + sealed interface Type { + data class Unknown(val cause: Throwable) : Type + object NoKeysFound : Type + object UnexpectedDecryptionOutput : Type + object UnableToOpenFile : Type + object InvalidFile : Type + } + + } + + data class Update(val importedKeysCount: Long) : ImportResult +} diff --git a/features/navigator/build.gradle b/features/navigator/build.gradle index 6c97c96..f3bacd7 100644 --- a/features/navigator/build.gradle +++ b/features/navigator/build.gradle @@ -4,5 +4,5 @@ apply plugin: 'kotlin-parcelize' dependencies { compileOnly project(":domains:android:stub") implementation project(":core") - implementation project(":matrix:common") + implementation project(":chat-engine") } \ No newline at end of file diff --git a/features/settings/build.gradle b/features/settings/build.gradle index 00ef2c0..fc553d4 100644 --- a/features/settings/build.gradle +++ b/features/settings/build.gradle @@ -1,8 +1,7 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":matrix:services:sync") - implementation project(":matrix:services:crypto") + implementation project(":chat-engine") implementation project(":features:navigator") implementation project(':domains:store') implementation project(':domains:android:push') 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 a0c695a..b498f95 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 @@ -5,16 +5,14 @@ import app.dapk.st.core.* import app.dapk.st.domain.StoreModule 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.engine.ChatEngine import app.dapk.st.push.PushModule import app.dapk.st.settings.eventlogger.EventLoggerViewModel class SettingsModule( + private val chatEngine: ChatEngine, private val storeModule: StoreModule, private val pushModule: PushModule, - private val cryptoService: CryptoService, - private val syncService: SyncService, private val contentResolver: ContentResolver, private val buildMeta: BuildMeta, private val deviceMeta: DeviceMeta, @@ -26,10 +24,9 @@ class SettingsModule( internal fun settingsViewModel(): SettingsViewModel { return SettingsViewModel( + chatEngine, storeModule.cacheCleaner(), contentResolver, - cryptoService, - syncService, UriFilenameResolver(contentResolver, coroutineDispatchers), SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore), pushModule.pushTokenRegistrars(), 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 edf9bc3..09522f2 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 @@ -42,7 +42,7 @@ import app.dapk.st.core.components.Header import app.dapk.st.core.extensions.takeAs import app.dapk.st.core.getActivity import app.dapk.st.design.components.* -import app.dapk.st.matrix.crypto.ImportResult +import app.dapk.st.engine.ImportResult import app.dapk.st.navigator.Navigator import app.dapk.st.settings.SettingsEvent.* import app.dapk.st.settings.eventlogger.EventLogActivity 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 0c73de2..ff796c7 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 @@ -2,10 +2,9 @@ package app.dapk.st.settings import android.net.Uri import app.dapk.st.core.Lce -import app.dapk.st.core.LceWithProgress import app.dapk.st.design.components.Route import app.dapk.st.design.components.SpiderPage -import app.dapk.st.matrix.crypto.ImportResult +import app.dapk.st.engine.ImportResult import app.dapk.st.push.Registrar internal data class SettingsScreenState( 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 5510e5d..a05783d 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 @@ -9,9 +9,8 @@ import app.dapk.st.design.components.SpiderPage import app.dapk.st.domain.StoreCleaner 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 +import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.ImportResult import app.dapk.st.push.PushTokenRegistrars import app.dapk.st.push.Registrar import app.dapk.st.settings.SettingItem.Id.* @@ -26,10 +25,9 @@ import kotlinx.coroutines.launch private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/" internal class SettingsViewModel( + private val chatEngine: ChatEngine, private val cacheCleaner: StoreCleaner, private val contentResolver: ContentResolver, - private val cryptoService: CryptoService, - private val syncService: SyncService, private val uriFilenameResolver: UriFilenameResolver, private val settingsItemFactory: SettingsItemFactory, private val pushTokenRegistrars: PushTokenRegistrars, @@ -142,7 +140,7 @@ internal class SettingsViewModel( fun importFromFileKeys(file: Uri, passphrase: String) { updatePageState { copy(importProgress = ImportResult.Update(0)) } viewModelScope.launch { - with(cryptoService) { + with(chatEngine) { runCatching { contentResolver.openInputStream(file)!! } .fold( onSuccess = { fileStream -> @@ -159,7 +157,7 @@ internal class SettingsViewModel( } is ImportResult.Success -> { - syncService.forceManualRefresh(it.roomIds.toList()) + chatEngine.refresh(it.roomIds.toList()) } } } diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt index 6e26cc4..8e61404 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt @@ -4,6 +4,7 @@ import app.dapk.st.matrix.auth.AuthService import app.dapk.st.matrix.sync.InviteMeta import app.dapk.st.matrix.auth.AuthService.LoginRequest as MatrixLoginRequest import app.dapk.st.matrix.auth.AuthService.LoginResult as MatrixLoginResult +import app.dapk.st.matrix.crypto.ImportResult as MatrixImportResult import app.dapk.st.matrix.room.ProfileService.Me as MatrixMe import app.dapk.st.matrix.sync.LastMessage as MatrixLastMessage import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite @@ -62,3 +63,17 @@ fun InviteMeta.engine() = when (this) { is InviteMeta.Room -> RoomInvite.InviteMeta.Room(this.roomName) } +fun MatrixImportResult.engine() = when (this) { + is MatrixImportResult.Error -> ImportResult.Error( + when (val error = this.cause) { + MatrixImportResult.Error.Type.InvalidFile -> ImportResult.Error.Type.InvalidFile + MatrixImportResult.Error.Type.NoKeysFound -> ImportResult.Error.Type.NoKeysFound + MatrixImportResult.Error.Type.UnableToOpenFile -> ImportResult.Error.Type.UnableToOpenFile + MatrixImportResult.Error.Type.UnexpectedDecryptionOutput -> ImportResult.Error.Type.UnexpectedDecryptionOutput + is MatrixImportResult.Error.Type.Unknown -> ImportResult.Error.Type.Unknown(error.cause) + } + ) + + is MatrixImportResult.Success -> ImportResult.Success(this.roomIds, this.totalImportedKeysCount) + is MatrixImportResult.Update -> ImportResult.Update(this.importedKeysCount) +} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index 9caefa5..5fd7dda 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -30,6 +30,7 @@ import app.dapk.st.olm.OlmStore import app.dapk.st.olm.OlmWrapper import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import java.io.InputStream import java.time.Clock class MatrixEngine internal constructor( @@ -50,6 +51,17 @@ class MatrixEngine internal constructor( return matrix.value.profileService().me(forceRefresh).engine() } + override suspend fun refresh(roomIds: List) { + matrix.value.syncService().forceManualRefresh(roomIds) + + } + + override suspend fun InputStream.importRoomKeys(password: String): Flow { + return with(matrix.value.cryptoService()) { + importRoomKeys(password).map { it.engine() } + } + } + class Factory { fun create( @@ -72,7 +84,7 @@ class MatrixEngine internal constructor( knownDeviceStore: KnownDeviceStore, olmStore: OlmStore, ): ChatEngine { - val lazyMatrix = unsafeLazy { + val lazyMatrix = lazy { MatrixFactory.createMatrix( base64, buildMeta, From baf7cfc17acd708714c9a1689ab838297ada052f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 18:58:03 +0100 Subject: [PATCH 18/43] porting message observing to the chat engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 5 +- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 2 + .../main/kotlin/app/dapk/st/engine/Models.kt | 119 ++++++++++++++++++ features/messenger/build.gradle | 1 + .../app/dapk/st/messenger/MessengerModule.kt | 16 +-- .../app/dapk/st/messenger/MessengerScreen.kt | 16 +-- .../app/dapk/st/messenger/MessengerState.kt | 3 +- .../dapk/st/messenger/MessengerViewModel.kt | 52 ++------ .../app/dapk/st/engine}/LocalEchoMapper.kt | 4 +- .../app/dapk/st/engine/LocalIdFactory.kt | 7 ++ .../app/dapk/st/engine/MappingExtensions.kt | 37 ++++++ .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 13 +- .../st/engine}/MergeWithLocalEchosUseCase.kt | 4 +- .../app/dapk/st/engine/ReadMarkingTimeline.kt | 55 ++++++++ .../app/dapk/st/engine}/TimelineUseCase.kt | 14 +-- 15 files changed, 263 insertions(+), 85 deletions(-) rename {features/messenger/src/main/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/LocalEchoMapper.kt (95%) create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt rename {features/messenger/src/main/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/MergeWithLocalEchosUseCase.kt (94%) create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt rename {features/messenger/src/main/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/TimelineUseCase.kt (86%) 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 a1ea937..e9f20f1 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -177,11 +177,8 @@ internal class FeatureModules internal constructor( } val messengerModule by unsafeLazy { MessengerModule( - matrixModules.sync, + matrixModules.engine, matrixModules.message, - matrixModules.room, - storeModule.value.credentialsStore(), - storeModule.value.roomStore(), clock, context, base64, diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index f5e7fe8..76a89aa 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -10,6 +10,8 @@ interface ChatEngine { fun invites(): Flow + suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow + suspend fun login(request: LoginRequest): LoginResult suspend fun me(forceRefresh: Boolean): Me diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt index df5631a..fa975d1 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt @@ -1,6 +1,10 @@ package app.dapk.st.engine import app.dapk.st.matrix.common.* +import java.time.Instant +import java.time.ZoneId +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter typealias DirectoryState = List typealias OverviewState = List @@ -79,3 +83,118 @@ sealed interface ImportResult { data class Update(val importedKeysCount: Long) : ImportResult } + +data class MessengerState( + val self: UserId, + val roomState: RoomState, + val typing: Typing? +) + +data class RoomState( + val roomOverview: RoomOverview, + val events: List, +) + +internal val DEFAULT_ZONE = ZoneId.systemDefault() +internal val MESSAGE_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm") + +sealed class RoomEvent { + + abstract val eventId: EventId + abstract val utcTimestamp: Long + abstract val author: RoomMember + abstract val meta: MessageMeta + + data class Message( + override val eventId: EventId, + override val utcTimestamp: Long, + val content: String, + override val author: RoomMember, + override val meta: MessageMeta, + val edited: Boolean = false, + val redacted: Boolean = false, + ) : RoomEvent() { + + val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + } + + data class Reply( + val message: RoomEvent, + val replyingTo: RoomEvent, + ) : RoomEvent() { + + override val eventId: EventId = message.eventId + override val utcTimestamp: Long = message.utcTimestamp + override val author: RoomMember = message.author + override val meta: MessageMeta = message.meta + + val replyingToSelf = replyingTo.author == message.author + + val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + } + + data class Image( + override val eventId: EventId, + override val utcTimestamp: Long, + val imageMeta: ImageMeta, + override val author: RoomMember, + override val meta: MessageMeta, + val edited: Boolean = false, + ) : RoomEvent() { + + val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + + data class ImageMeta( + val width: Int?, + val height: Int?, + val url: String, + val keys: Keys?, + ) { + + data class Keys( + val k: String, + val iv: String, + val v: String, + val hashes: Map, + ) + + } + } + +} + +sealed class MessageMeta { + + object FromServer : MessageMeta() + + data class LocalEcho( + val echoId: String, + val state: State + ) : MessageMeta() { + + sealed class State { + object Sending : State() + + object Sent : State() + + data class Error( + val message: String, + val type: Type, + ) : State() { + + enum class Type { + UNKNOWN + } + } + } + } +} \ No newline at end of file diff --git a/features/messenger/build.gradle b/features/messenger/build.gradle index 9d31472..e2a2096 100644 --- a/features/messenger/build.gradle +++ b/features/messenger/build.gradle @@ -2,6 +2,7 @@ applyAndroidComposeLibraryModule(project) apply plugin: 'kotlin-parcelize' dependencies { + implementation project(":chat-engine") implementation project(":matrix:services:sync") implementation project(":matrix:services:message") implementation project(":matrix:services:crypto") 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 9b48609..292cfae 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 @@ -4,6 +4,7 @@ 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.engine.ChatEngine import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.message.MessageService @@ -14,11 +15,8 @@ import app.dapk.st.matrix.sync.SyncService import java.time.Clock class MessengerModule( - private val syncService: SyncService, + private val chatEngine: ChatEngine, private val messageService: MessageService, - private val roomService: RoomService, - private val credentialsStore: CredentialsStore, - private val roomStore: RoomStore, private val clock: Clock, private val context: Context, private val base64: Base64, @@ -28,11 +26,8 @@ class MessengerModule( internal fun messengerViewModel(): MessengerViewModel { return MessengerViewModel( + chatEngine, messageService, - roomService, - roomStore, - credentialsStore, - timelineUseCase(), LocalIdFactory(), imageMetaReader, messageOptionsStore, @@ -40,10 +35,5 @@ class MessengerModule( ) } - private fun timelineUseCase(): TimelineUseCaseImpl { - val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper())) - return TimelineUseCaseImpl(syncService, messageService, roomService, mergeWithLocalEchosUseCase) - } - internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, base64, roomId) } \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index 5d1447e..b6673e0 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -44,12 +44,12 @@ import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.design.components.* +import app.dapk.st.engine.MessageMeta +import app.dapk.st.engine.MessengerState +import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.RoomState import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.sync.MessageMeta -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomEvent.Message -import app.dapk.st.matrix.sync.RoomState import app.dapk.st.messenger.gallery.ImageGalleryActivityPayload import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.Navigator @@ -196,7 +196,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) { when (item) { is RoomEvent.Image -> MessageImage(it as BubbleContent) - is Message -> TextBubbleContent(it as BubbleContent) + is RoomEvent.Message -> TextBubbleContent(it as BubbleContent) is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent) } } @@ -482,7 +482,7 @@ private fun ReplyBubbleContent(content: BubbleContent) { ) Spacer(modifier = Modifier.height(2.dp)) when (val replyingTo = content.message.replyingTo) { - is Message -> { + is RoomEvent.Message -> { Text( text = replyingTo.content, color = content.textColor().copy(alpha = 0.8f), @@ -525,7 +525,7 @@ private fun ReplyBubbleContent(content: BubbleContent) { ) } when (val message = content.message.message) { - is Message -> { + is RoomEvent.Message -> { Text( text = message.content, color = content.textColor(), @@ -642,7 +642,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un ) } ) { - if (it is Message) { + if (it is RoomEvent.Message) { Box(Modifier.padding(12.dp)) { Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) { Icon( diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt index 7c0259c..ce4d829 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt @@ -1,8 +1,9 @@ package app.dapk.st.messenger import app.dapk.st.core.Lce +import app.dapk.st.engine.MessengerState +import app.dapk.st.engine.RoomEvent import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.navigator.MessageAttachment data class MessengerScreenState( 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 d780da5..03ecc8f 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 @@ -4,31 +4,24 @@ 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.engine.ChatEngine +import app.dapk.st.engine.RoomEvent import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.navigator.MessageAttachment import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.MutableStateFactory import app.dapk.st.viewmodel.defaultStateFactory -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.collect -import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.launch import java.time.Clock internal class MessengerViewModel( + private val chatEngine: ChatEngine, private val messageService: MessageService, - private val roomService: RoomService, - private val roomStore: RoomStore, - private val credentialsStore: CredentialsStore, - private val observeTimeline: ObserveTimelineUseCase, private val localIdFactory: LocalIdFactory, private val imageContentReader: ImageContentReader, private val messageOptionsStore: MessageOptionsStore, @@ -83,29 +76,10 @@ internal class MessengerViewModel( private fun start(action: MessengerAction.OnMessengerVisible) { updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) } - syncJob = viewModelScope.launch { - roomStore.markRead(action.roomId) - - val credentials = credentialsStore.credentials()!! - var lastKnownReadEvent: EventId? = null - observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state -> - state.latestMessageEventFromOthers(self = credentials.userId)?.let { - if (lastKnownReadEvent != it) { - updateRoomReadStateAsync(latestReadEvent = it, state) - lastKnownReadEvent = it - } - } - updateState { copy(roomState = Lce.Content(state)) } - }.collect() - } - } - - private fun CoroutineScope.updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState): Deferred { - return async { - runCatching { - roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = messageOptionsStore.isReadReceiptsDisabled()) - roomStore.markRead(state.roomState.roomOverview.roomId) - } + viewModelScope.launch { + syncJob = chatEngine.messages(action.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled()) + .onEach { updateState { copy(roomState = Lce.Content(it)) } } + .launchIn(this) } } @@ -190,12 +164,6 @@ internal class MessengerViewModel( } -private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events - .filterIsInstance() - .filterNot { it.author.id == self } - .firstOrNull() - ?.eventId - sealed interface MessengerAction { data class ComposerTextUpdate(val newValue: String) : MessengerAction data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt similarity index 95% rename from features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt index 8fc2fa7..1880fcc 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt @@ -1,10 +1,8 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.sync.MessageMeta -import app.dapk.st.matrix.sync.RoomEvent internal class LocalEchoMapper(private val metaMapper: MetaMapper) { diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt new file mode 100644 index 0000000..57b37eb --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalIdFactory.kt @@ -0,0 +1,7 @@ +package app.dapk.st.engine + +import java.util.* + +internal class LocalIdFactory { + fun create() = "local.${UUID.randomUUID()}" +} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt index 8e61404..9317c81 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt @@ -7,8 +7,11 @@ import app.dapk.st.matrix.auth.AuthService.LoginResult as MatrixLoginResult import app.dapk.st.matrix.crypto.ImportResult as MatrixImportResult import app.dapk.st.matrix.room.ProfileService.Me as MatrixMe import app.dapk.st.matrix.sync.LastMessage as MatrixLastMessage +import app.dapk.st.matrix.sync.MessageMeta as MatrixMessageMeta +import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview +import app.dapk.st.matrix.sync.RoomState as MatrixRoomState import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing as MatrixTyping fun MatrixRoomOverview.engine() = RoomOverview( @@ -76,4 +79,38 @@ fun MatrixImportResult.engine() = when (this) { is MatrixImportResult.Success -> ImportResult.Success(this.roomIds, this.totalImportedKeysCount) is MatrixImportResult.Update -> ImportResult.Update(this.importedKeysCount) +} + +fun MatrixRoomState.engine() = RoomState( + this.roomOverview.engine(), + this.events.map { it.engine() } +) + +fun MatrixRoomEvent.engine(): RoomEvent = when (this) { + is MatrixRoomEvent.Image -> RoomEvent.Image(this.eventId, this.utcTimestamp, this.imageMeta.engine(), this.author, this.meta.engine(), this.edited) + is MatrixRoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, this.content, this.author, this.meta.engine(), this.edited, this.redacted) + is MatrixRoomEvent.Reply -> RoomEvent.Reply(this.message.engine(), this.replyingTo.engine()) +} + +fun MatrixRoomEvent.Image.ImageMeta.engine() = RoomEvent.Image.ImageMeta( + this.width, + this.height, + this.url, + this.keys?.let { RoomEvent.Image.ImageMeta.Keys(it.k, it.iv, it.v, it.hashes) } +) + +fun MatrixMessageMeta.engine() = when (this) { + MatrixMessageMeta.FromServer -> MessageMeta.FromServer + is MatrixMessageMeta.LocalEcho -> MessageMeta.LocalEcho( + this.echoId, when (val echo = this.state) { + is MatrixMessageMeta.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error( + echo.message, when (echo.type) { + MatrixMessageMeta.LocalEcho.State.Error.Type.UNKNOWN -> MessageMeta.LocalEcho.State.Error.Type.UNKNOWN + } + ) + + MatrixMessageMeta.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending + MatrixMessageMeta.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent + } + ) } \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index 5fd7dda..b4f9e0e 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -36,6 +36,7 @@ import java.time.Clock class MatrixEngine internal constructor( private val directoryUseCase: Lazy, private val matrix: Lazy, + private val timelineUseCase: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() @@ -43,6 +44,10 @@ class MatrixEngine internal constructor( return matrix.value.syncService().invites().map { it.map { it.engine() } } } + override suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow { + return timelineUseCase.value.foo(roomId, isReadReceiptsDisabled = disableReadReceipts) + } + override suspend fun login(request: LoginRequest): LoginResult { return matrix.value.authService().login(request.engine()).engine() } @@ -116,8 +121,14 @@ class MatrixEngine internal constructor( roomStore ) } + val timelineUseCase = unsafeLazy { + val matrix = lazyMatrix.value + val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper())) + val timeline = TimelineUseCaseImpl(matrix.syncService(), matrix.messageService(), matrix.roomService(), mergeWithLocalEchosUseCase) + ReadMarkingTimeline(roomStore, credentialsStore, timeline, matrix.roomService()) + } - return MatrixEngine(directoryUseCase, lazyMatrix) + return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase) } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt similarity index 94% rename from features/messenger/src/main/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCase.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt index ba23d34..8f91848 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCase.kt @@ -1,10 +1,8 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomState internal typealias MergeWithLocalEchosUseCase = (RoomState, RoomMember, List) -> RoomState diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt new file mode 100644 index 0000000..ee1c641 --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt @@ -0,0 +1,55 @@ +package app.dapk.st.engine + +import app.dapk.st.matrix.common.CredentialsStore +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.UserId +import app.dapk.st.matrix.room.RoomService +import app.dapk.st.matrix.sync.RoomEvent +import app.dapk.st.matrix.sync.RoomStore +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onEach + +class ReadMarkingTimeline( + private val roomStore: RoomStore, + private val credentialsStore: CredentialsStore, + private val observeTimelineUseCase: ObserveTimelineUseCase, + private val roomService: RoomService, +) { + + suspend fun foo(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow { + var lastKnownReadEvent: EventId? = null + val credentials = credentialsStore.credentials()!! + roomStore.markRead(roomId) + return observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state -> + state.latestMessageEventFromOthers(self = credentials.userId)?.let { + if (lastKnownReadEvent != it) { + updateRoomReadStateAsync(latestReadEvent = it, state, isReadReceiptsDisabled) + lastKnownReadEvent = it + } + } + } + } + + private suspend fun updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState, isReadReceiptsDisabled: Boolean): Deferred<*> { + return coroutineScope { + async { + runCatching { + roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = isReadReceiptsDisabled) + roomStore.markRead(state.roomState.roomOverview.roomId) + } + } + } + } + +} + +private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events + .filterIsInstance() + .filterNot { it.author.id == self } + .firstOrNull() + ?.eventId diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt similarity index 86% rename from features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt index c1d9538..55b2f30 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/TimelineUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt @@ -1,4 +1,4 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.core.extensions.startAndIgnoreEmissions import app.dapk.st.matrix.common.RoomId @@ -6,10 +6,10 @@ import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.SyncService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow @@ -37,7 +37,7 @@ internal class TimelineUseCaseImpl( ) } }, - typing = events.filterIsInstance().firstOrNull { it.roomId == roomId }, + typing = events.filterIsInstance().firstOrNull { it.roomId == roomId }?.engine(), self = userId, ) } @@ -45,14 +45,8 @@ internal class TimelineUseCaseImpl( private fun roomDatasource(roomId: RoomId) = combine( syncService.startSyncing().startAndIgnoreEmissions(), - syncService.room(roomId) + syncService.room(roomId).map { it.engine() } ) { _, room -> room } } private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null) - -data class MessengerState( - val self: UserId, - val roomState: RoomState, - val typing: SyncService.SyncEvent.Typing? -) From e61dea7ba75930370c73d8ef32834a25e52e1e91 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 19:22:03 +0100 Subject: [PATCH 19/43] porting the messenger module to chat-engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 3 - .../kotlin/app/dapk/st/engine/ChatEngine.kt | 1 + .../main/kotlin/app/dapk/st/engine/Models.kt | 21 +++++++ features/messenger/build.gradle | 4 -- .../app/dapk/st/messenger/MessengerModule.kt | 14 ----- .../dapk/st/messenger/MessengerViewModel.kt | 46 +++----------- .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 12 +++- .../app/dapk/st/engine/SendMessageUseCase.kt | 60 +++++++++++++++++++ 8 files changed, 102 insertions(+), 59 deletions(-) create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/SendMessageUseCase.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 e9f20f1..e904613 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -178,11 +178,8 @@ internal class FeatureModules internal constructor( val messengerModule by unsafeLazy { MessengerModule( matrixModules.engine, - matrixModules.message, - clock, context, base64, - imageContentReader, storeModule.value.messageStore(), ) } diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 76a89aa..a47c76e 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -20,4 +20,5 @@ interface ChatEngine { suspend fun InputStream.importRoomKeys(password: String): Flow + suspend fun send(message: SendMessage, room: RoomOverview) } diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt index fa975d1..dcc9b65 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt @@ -197,4 +197,25 @@ sealed class MessageMeta { } } } +} + +sealed interface SendMessage { + + data class TextMessage( + val content: String, + val reply: Reply? = null, + ) : SendMessage { + + data class Reply( + val author: RoomMember, + val originalMessage: String, + val eventId: EventId, + val timestampUtc: Long, + ) + } + + data class ImageMessage( + val uri: String, + ) : SendMessage + } \ No newline at end of file diff --git a/features/messenger/build.gradle b/features/messenger/build.gradle index e2a2096..13f5e1b 100644 --- a/features/messenger/build.gradle +++ b/features/messenger/build.gradle @@ -3,10 +3,6 @@ apply plugin: 'kotlin-parcelize' dependencies { implementation project(":chat-engine") - implementation project(":matrix:services:sync") - implementation project(":matrix:services:message") - implementation project(":matrix:services:crypto") - implementation project(":matrix:services:room") implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") implementation project(":domains:store") 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 292cfae..85b10ac 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 @@ -5,33 +5,19 @@ import app.dapk.st.core.Base64 import app.dapk.st.core.ProvidableModule import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.engine.ChatEngine -import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.RoomStore -import app.dapk.st.matrix.sync.SyncService -import java.time.Clock class MessengerModule( private val chatEngine: ChatEngine, - private val messageService: MessageService, - private val clock: Clock, private val context: Context, private val base64: Base64, - private val imageMetaReader: ImageContentReader, private val messageOptionsStore: MessageOptionsStore, ) : ProvidableModule { internal fun messengerViewModel(): MessengerViewModel { return MessengerViewModel( chatEngine, - messageService, - LocalIdFactory(), - imageMetaReader, messageOptionsStore, - clock ) } 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 03ecc8f..cb27a18 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 @@ -6,9 +6,8 @@ import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.engine.ChatEngine import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.SendMessage import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.navigator.MessageAttachment import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.MutableStateFactory @@ -17,15 +16,10 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import java.time.Clock internal class MessengerViewModel( private val chatEngine: ChatEngine, - private val messageService: MessageService, - private val localIdFactory: LocalIdFactory, - private val imageContentReader: ImageContentReader, private val messageOptionsStore: MessageOptionsStore, - private val clock: Clock, factory: MutableStateFactory = defaultStateFactory(), ) : DapkViewModel( initialState = MessengerScreenState( @@ -83,6 +77,7 @@ internal class MessengerViewModel( } } + private fun sendMessage() { when (val composerState = state.composerState) { is ComposerState.Text -> { @@ -92,27 +87,23 @@ internal class MessengerViewModel( state.roomState.takeIfContent()?.let { content -> val roomState = content.roomState viewModelScope.launch { - messageService.scheduleMessage( - MessageService.Message.TextMessage( - MessageService.Message.Content.TextContent(body = copy.value), - roomId = roomState.roomOverview.roomId, - sendEncrypted = roomState.roomOverview.isEncrypted, - localId = localIdFactory.create(), - timestampUtc = clock.millis(), + chatEngine.send( + message = SendMessage.TextMessage( + content = copy.value, reply = copy.reply?.let { - MessageService.Message.TextMessage.Reply( + SendMessage.TextMessage.Reply( author = it.author, originalMessage = when (it) { is RoomEvent.Image -> TODO() is RoomEvent.Reply -> TODO() is RoomEvent.Message -> it.content }, - replyContent = copy.value, eventId = it.eventId, timestampUtc = it.utcTimestamp, ) } - ) + ), + room = roomState.roomOverview, ) } } @@ -125,26 +116,7 @@ internal class MessengerViewModel( state.roomState.takeIfContent()?.let { content -> val roomState = content.roomState viewModelScope.launch { - val imageUri = copy.values.first().uri.value - val meta = imageContentReader.meta(imageUri) - - messageService.scheduleMessage( - MessageService.Message.ImageMessage( - MessageService.Message.Content.ImageContent( - uri = imageUri, MessageService.Message.Content.ImageContent.Meta( - height = meta.height, - width = meta.width, - size = meta.size, - fileName = meta.fileName, - mimeType = meta.mimeType, - ) - ), - roomId = roomState.roomOverview.roomId, - sendEncrypted = roomState.roomOverview.isEncrypted, - localId = localIdFactory.create(), - timestampUtc = clock.millis(), - ) - ) + chatEngine.send(SendMessage.ImageMessage(uri = copy.values.first().uri.value), roomState.roomOverview) } } diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index b4f9e0e..6312713 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -37,6 +37,7 @@ class MatrixEngine internal constructor( private val directoryUseCase: Lazy, private val matrix: Lazy, private val timelineUseCase: Lazy, + private val sendMessageUseCase: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() @@ -67,6 +68,10 @@ class MatrixEngine internal constructor( } } + override suspend fun send(message: SendMessage, room: RoomOverview) { + sendMessageUseCase.value.send(message, room) + } + class Factory { fun create( @@ -128,7 +133,12 @@ class MatrixEngine internal constructor( ReadMarkingTimeline(roomStore, credentialsStore, timeline, matrix.roomService()) } - return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase) + val sendMessageUseCase = unsafeLazy { + val matrix = lazyMatrix.value + SendMessageUseCase(matrix.messageService(), LocalIdFactory(), imageContentReader, Clock.systemUTC()) + } + + return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase, sendMessageUseCase) } diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/SendMessageUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/SendMessageUseCase.kt new file mode 100644 index 0000000..4c4054d --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/SendMessageUseCase.kt @@ -0,0 +1,60 @@ +package app.dapk.st.engine + +import app.dapk.st.matrix.message.MessageService +import app.dapk.st.matrix.message.internal.ImageContentReader +import java.time.Clock + +internal class SendMessageUseCase( + private val messageService: MessageService, + private val localIdFactory: LocalIdFactory, + private val imageContentReader: ImageContentReader, + private val clock: Clock, +) { + + suspend fun send(message: SendMessage, room: RoomOverview) { + when (message) { + is SendMessage.ImageMessage -> createImageMessage(message, room) + is SendMessage.TextMessage -> messageService.scheduleMessage(createTextMessage(message, room)) + } + } + + private suspend fun createImageMessage(message: SendMessage.ImageMessage, room: RoomOverview) { + val meta = imageContentReader.meta(message.uri) + messageService.scheduleMessage( + MessageService.Message.ImageMessage( + MessageService.Message.Content.ImageContent( + uri = message.uri, + MessageService.Message.Content.ImageContent.Meta( + height = meta.height, + width = meta.width, + size = meta.size, + fileName = meta.fileName, + mimeType = meta.mimeType, + ) + ), + roomId = room.roomId, + sendEncrypted = room.isEncrypted, + localId = localIdFactory.create(), + timestampUtc = clock.millis(), + ) + ) + } + + private fun createTextMessage(message: SendMessage.TextMessage, room: RoomOverview) = MessageService.Message.TextMessage( + content = MessageService.Message.Content.TextContent(message.content), + roomId = room.roomId, + sendEncrypted = room.isEncrypted, + localId = localIdFactory.create(), + timestampUtc = clock.millis(), + reply = message.reply?.let { + MessageService.Message.TextMessage.Reply( + author = it.author, + originalMessage = it.originalMessage, + replyContent = message.content, + eventId = it.eventId, + timestampUtc = it.timestampUtc, + ) + } + ) + +} \ No newline at end of file From 8bbf7258bea7b8560e48f51d8b62f9d27dde904f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 19:38:33 +0100 Subject: [PATCH 20/43] porting media decryption to the chat engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 3 -- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 14 ++++++- .../dapk/st/messenger/DecryptingFetcher.kt | 13 ++++--- .../app/dapk/st/messenger/MessengerModule.kt | 4 +- .../app/dapk/st/settings/SettingsViewModel.kt | 13 ------- .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 37 +++++++++++++------ ...iaDecrypter.kt => MatrixMediaDecrypter.kt} | 2 +- .../app/dapk/st/matrix/sync/SyncService.kt | 2 +- .../sync/internal/DefaultSyncService.kt | 2 +- test-harness/src/test/kotlin/test/Test.kt | 4 +- 10 files changed, 51 insertions(+), 43 deletions(-) rename matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/{MediaDecrypter.kt => MatrixMediaDecrypter.kt} (96%) 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 e904613..87c181d 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -179,7 +179,6 @@ internal class FeatureModules internal constructor( MessengerModule( matrixModules.engine, context, - base64, storeModule.value.messageStore(), ) } @@ -466,10 +465,8 @@ internal class MatrixModules( val push by unsafeLazy { matrix.pushService() } val sync by unsafeLazy { matrix.syncService() } - val message by unsafeLazy { matrix.messageService() } val room by unsafeLazy { matrix.roomService() } val profile by unsafeLazy { matrix.profileService() } - val crypto by unsafeLazy { matrix.cryptoService() } } internal class DomainModules( diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index a47c76e..1d0544f 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -16,9 +16,19 @@ interface ChatEngine { suspend fun me(forceRefresh: Boolean): Me - suspend fun refresh(roomIds: List) - suspend fun InputStream.importRoomKeys(password: String): Flow suspend fun send(message: SendMessage, room: RoomOverview) + + fun mediaDecrypter(): MediaDecrypter } + +interface MediaDecrypter { + + fun decrypt(input: InputStream, k: String, iv: String): Collector + + fun interface Collector { + fun collect(partial: (ByteArray) -> Unit) + } + +} \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/DecryptingFetcher.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/DecryptingFetcher.kt index a345ffc..d77e319 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/DecryptingFetcher.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/DecryptingFetcher.kt @@ -2,10 +2,9 @@ package app.dapk.st.messenger import android.content.Context import android.os.Environment -import app.dapk.st.core.Base64 +import app.dapk.st.engine.MediaDecrypter +import app.dapk.st.engine.RoomEvent import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.crypto.MediaDecrypter -import app.dapk.st.matrix.sync.RoomEvent import coil.ImageLoader import coil.decode.DataSource import coil.decode.ImageSource @@ -20,9 +19,11 @@ import okio.BufferedSource import okio.Path.Companion.toOkioPath import java.io.File -class DecryptingFetcherFactory(private val context: Context, base64: Base64, private val roomId: RoomId) : Fetcher.Factory { - - private val mediaDecrypter = MediaDecrypter(base64) +class DecryptingFetcherFactory( + private val context: Context, + private val roomId: RoomId, + private val mediaDecrypter: MediaDecrypter, +) : Fetcher.Factory { override fun create(data: RoomEvent.Image, options: Options, imageLoader: ImageLoader): Fetcher { return DecryptingFetcher(data, context, mediaDecrypter, roomId) 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 85b10ac..e46f37a 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 @@ -1,7 +1,6 @@ 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.engine.ChatEngine @@ -10,7 +9,6 @@ import app.dapk.st.matrix.common.RoomId class MessengerModule( private val chatEngine: ChatEngine, private val context: Context, - private val base64: Base64, private val messageOptionsStore: MessageOptionsStore, ) : ProvidableModule { @@ -21,5 +19,5 @@ class MessengerModule( ) } - internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, base64, roomId) + internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, roomId, chatEngine.mediaDecrypter()) } \ No newline at end of file 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 a05783d..b74daf8 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 @@ -147,19 +147,6 @@ internal class SettingsViewModel( fileStream.importRoomKeys(passphrase) .onEach { updatePageState { copy(importProgress = it) } - when (it) { - is ImportResult.Error -> { - // do nothing - } - - is ImportResult.Update -> { - // do nothing - } - - is ImportResult.Success -> { - chatEngine.refresh(it.roomIds.toList()) - } - } } .launchIn(viewModelScope) }, diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index 6312713..305aa56 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -10,10 +10,7 @@ import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.auth.authService import app.dapk.st.matrix.auth.installAuthService import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.RoomMembersProvider -import app.dapk.st.matrix.crypto.Verification -import app.dapk.st.matrix.crypto.cryptoService -import app.dapk.st.matrix.crypto.installCryptoService +import app.dapk.st.matrix.crypto.* import app.dapk.st.matrix.device.KnownDeviceStore import app.dapk.st.matrix.device.deviceService import app.dapk.st.matrix.device.installEncryptionService @@ -30,6 +27,7 @@ import app.dapk.st.olm.OlmStore import app.dapk.st.olm.OlmWrapper import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach import java.io.InputStream import java.time.Clock @@ -38,6 +36,7 @@ class MatrixEngine internal constructor( private val matrix: Lazy, private val timelineUseCase: Lazy, private val sendMessageUseCase: Lazy, + private val matrixMediaDecrypter: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() @@ -57,14 +56,18 @@ class MatrixEngine internal constructor( return matrix.value.profileService().me(forceRefresh).engine() } - override suspend fun refresh(roomIds: List) { - matrix.value.syncService().forceManualRefresh(roomIds) - - } - override suspend fun InputStream.importRoomKeys(password: String): Flow { return with(matrix.value.cryptoService()) { - importRoomKeys(password).map { it.engine() } + importRoomKeys(password).map { it.engine() }.onEach { + when (it) { + is ImportResult.Error, + is ImportResult.Update -> { + // do nothing + } + + is ImportResult.Success -> matrix.value.syncService().forceManualRefresh(it.roomIds) + } + } } } @@ -72,6 +75,17 @@ class MatrixEngine internal constructor( sendMessageUseCase.value.send(message, room) } + override fun mediaDecrypter(): MediaDecrypter { + val mediaDecrypter = matrixMediaDecrypter.value + return object : MediaDecrypter { + override fun decrypt(input: InputStream, k: String, iv: String): MediaDecrypter.Collector { + return MediaDecrypter.Collector { + mediaDecrypter.decrypt(input, k, iv).collect(it) + } + } + } + } + class Factory { fun create( @@ -138,8 +152,9 @@ class MatrixEngine internal constructor( SendMessageUseCase(matrix.messageService(), LocalIdFactory(), imageContentReader, Clock.systemUTC()) } - return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase, sendMessageUseCase) + val mediaDecrypter = unsafeLazy { MatrixMediaDecrypter(base64) } + return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase, sendMessageUseCase, mediaDecrypter) } } diff --git a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MediaDecrypter.kt b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MatrixMediaDecrypter.kt similarity index 96% rename from matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MediaDecrypter.kt rename to matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MatrixMediaDecrypter.kt index df513d2..65dde9e 100644 --- a/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MediaDecrypter.kt +++ b/matrix/services/crypto/src/main/kotlin/app/dapk/st/matrix/crypto/MatrixMediaDecrypter.kt @@ -12,7 +12,7 @@ private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding" private const val SECRET_KEY_SPEC_ALGORITHM = "AES" private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" -class MediaDecrypter(private val base64: Base64) { +class MatrixMediaDecrypter(private val base64: Base64) { fun decrypt(input: InputStream, k: String, iv: String): Collector { val key = base64.decode(k.replace('-', '+').replace('_', '/')) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt index 20ddf80..7ad567c 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt @@ -21,7 +21,7 @@ interface SyncService : MatrixService { fun startSyncing(): Flow fun events(roomId: RoomId? = null): Flow> suspend fun observeEvent(eventId: EventId): Flow - suspend fun forceManualRefresh(roomIds: List) + suspend fun forceManualRefresh(roomIds: Set) @JvmInline value class FilterId(val value: String) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt index 779c2f0..aa4c0c7 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt @@ -110,7 +110,7 @@ internal class DefaultSyncService( override fun room(roomId: RoomId) = roomStore.latest(roomId) override fun events(roomId: RoomId?) = roomId?.let { syncEventsFlow.map { it.filter { it.roomId == roomId } }.distinctUntilChanged() } ?: syncEventsFlow override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId) - override suspend fun forceManualRefresh(roomIds: List) { + override suspend fun forceManualRefresh(roomIds: Set) { coroutineDispatchers.withIoContext { roomIds.map { async { diff --git a/test-harness/src/test/kotlin/test/Test.kt b/test-harness/src/test/kotlin/test/Test.kt index 7ff5604..e0403a2 100644 --- a/test-harness/src/test/kotlin/test/Test.kt +++ b/test-harness/src/test/kotlin/test/Test.kt @@ -7,7 +7,7 @@ import TestUser import app.dapk.st.core.extensions.ifNull import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.crypto.MediaDecrypter +import app.dapk.st.matrix.crypto.MatrixMediaDecrypter import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.messageService import app.dapk.st.matrix.sync.RoomEvent @@ -153,7 +153,7 @@ class MatrixTestScope(private val testScope: TestScope) { null -> output.readBytes().md5Hash() else -> { val byteStream = ByteArrayOutputStream() - MediaDecrypter(this.base64).decrypt(output.inputStream(), keys.k, keys.iv).collect { + MatrixMediaDecrypter(this.base64).decrypt(output.inputStream(), keys.k, keys.iv).collect { byteStream.write(it) } byteStream.toByteArray().md5Hash() From 0b02e6d0280d31eb709584a94ae04cbef0937c5d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 21:21:38 +0100 Subject: [PATCH 21/43] porting pushes to chat engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 36 ++++++++----------- .../kotlin/app/dapk/st/graph/AppTaskRunner.kt | 6 ++-- .../dapk/st/graph/BackgroundWorkAdapter.kt | 2 +- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 13 ++++++- domains/android/push/build.gradle | 1 - features/notifications/build.gradle | 2 -- .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 11 +++++- .../app/dapk/st/engine}/MatrixPushHandler.kt | 22 ++++++------ .../st/matrix/message/BackgroundScheduler.kt | 4 ++- .../message/internal/DefaultMessageService.kt | 5 +-- 10 files changed, 56 insertions(+), 46 deletions(-) rename {features/notifications/src/main/kotlin/app/dapk/st/notifications => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/MatrixPushHandler.kt (84%) 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 87c181d..4cf8b85 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -37,7 +37,6 @@ import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory import app.dapk.st.matrix.message.* import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.matrix.push.installPushService -import app.dapk.st.matrix.push.pushService import app.dapk.st.matrix.room.* import app.dapk.st.matrix.sync.* import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent @@ -47,13 +46,14 @@ import app.dapk.st.messenger.MessengerModule import app.dapk.st.messenger.gallery.ImageGalleryModule import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.MessageAttachment -import app.dapk.st.notifications.MatrixPushHandler import app.dapk.st.notifications.NotificationsModule import app.dapk.st.olm.DeviceKeyFactory import app.dapk.st.olm.OlmPersistenceWrapper import app.dapk.st.olm.OlmWrapper import app.dapk.st.profile.ProfileModule +import app.dapk.st.push.PushHandler import app.dapk.st.push.PushModule +import app.dapk.st.push.PushTokenPayload import app.dapk.st.push.messaging.MessagingServiceAdapter import app.dapk.st.settings.SettingsModule import app.dapk.st.share.ShareEntryModule @@ -62,6 +62,7 @@ import app.dapk.st.work.TaskRunnerModule import app.dapk.st.work.WorkModule import com.squareup.sqldelight.android.AndroidSqliteDriver import kotlinx.coroutines.Dispatchers +import kotlinx.serialization.json.Json import java.io.InputStream import java.time.Clock @@ -77,7 +78,6 @@ internal class AppModule(context: Application, logger: MatrixLogger) { private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db") private val database = DapkDb(driver) - private val clock = Clock.systemUTC() val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO) val base64 = AndroidBase64() @@ -97,7 +97,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) { private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) } private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta) - val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers) + val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, context, coroutineDispatchers) val coreAndroidModule = CoreAndroidModule( intentFactory = object : IntentFactory { @@ -136,13 +136,10 @@ internal class AppModule(context: Application, logger: MatrixLogger) { trackingModule, coreAndroidModule, imageLoaderModule, - imageContentReader, context, buildMeta, deviceMeta, coroutineDispatchers, - clock, - base64, ) } @@ -153,13 +150,10 @@ internal class FeatureModules internal constructor( private val trackingModule: TrackingModule, private val coreAndroidModule: CoreAndroidModule, imageLoaderModule: ImageLoaderModule, - imageContentReader: ImageContentReader, context: Context, buildMeta: BuildMeta, deviceMeta: DeviceMeta, coroutineDispatchers: CoroutineDispatchers, - clock: Clock, - base64: Base64, ) { val directoryModule by unsafeLazy { @@ -463,7 +457,6 @@ internal class MatrixModules( } } - val push by unsafeLazy { matrix.pushService() } val sync by unsafeLazy { matrix.syncService() } val room by unsafeLazy { matrix.roomService() } val profile by unsafeLazy { matrix.profileService() } @@ -472,20 +465,19 @@ internal class MatrixModules( internal class DomainModules( private val matrixModules: MatrixModules, private val errorTracker: ErrorTracker, - private val workModule: WorkModule, - private val storeModule: Lazy, private val context: Application, private val dispatchers: CoroutineDispatchers, ) { - val pushHandler by unsafeLazy { - val store = storeModule.value - MatrixPushHandler( - workScheduler = workModule.workScheduler(), - credentialsStore = store.credentialsStore(), - matrixModules.sync, - store.roomStore(), - ) + private val pushHandler by unsafeLazy { + val enginePushHandler = matrixModules.engine.pushHandler() + object : PushHandler { + override fun onNewToken(payload: PushTokenPayload) { + enginePushHandler.onNewToken(JsonString(Json.encodeToString(PushTokenPayload.serializer(), payload))) + } + + override fun onMessageReceived(eventId: EventId?, roomId: RoomId?) = enginePushHandler.onMessageReceived(eventId, roomId) + } } val messaging by unsafeLazy { MessagingModule(MessagingServiceAdapter(pushHandler), context) } @@ -500,7 +492,7 @@ internal class DomainModules( messaging.messaging, ) } - val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) } + val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.engine))) } } diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt b/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt index ca8bbf9..c2c6890 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppTaskRunner.kt @@ -1,13 +1,13 @@ package app.dapk.st.graph -import app.dapk.st.matrix.push.PushService +import app.dapk.st.engine.ChatEngine import app.dapk.st.push.PushTokenPayload import app.dapk.st.work.TaskRunner import io.ktor.client.plugins.* import kotlinx.serialization.json.Json class AppTaskRunner( - private val pushService: PushService, + private val chatEngine: ChatEngine, ) { suspend fun run(workTask: TaskRunner.RunnableWorkTask): TaskRunner.TaskResult { @@ -15,7 +15,7 @@ class AppTaskRunner( "push_token" -> { runCatching { val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload) - pushService.registerPush(payload.token, payload.gatewayUrl) + chatEngine.registerPushToken(payload.token, payload.gatewayUrl) }.fold( onSuccess = { TaskRunner.TaskResult.Success(workTask.source) }, onFailure = { diff --git a/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt b/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt index c35db37..4e9de87 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/BackgroundWorkAdapter.kt @@ -9,7 +9,7 @@ class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : Backgrou WorkScheduler.WorkTask( jobId = 1, type = task.type, - jsonPayload = task.jsonPayload, + jsonPayload = task.jsonPayload.value, ) ) } diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 1d0544f..cf83a79 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -1,5 +1,7 @@ package app.dapk.st.engine +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.JsonString import app.dapk.st.matrix.common.RoomId import kotlinx.coroutines.flow.Flow import java.io.InputStream @@ -20,7 +22,11 @@ interface ChatEngine { suspend fun send(message: SendMessage, room: RoomOverview) + suspend fun registerPushToken(token: String, gatewayUrl: String) + fun mediaDecrypter(): MediaDecrypter + + fun pushHandler(): PushHandler } interface MediaDecrypter { @@ -31,4 +37,9 @@ interface MediaDecrypter { fun collect(partial: (ByteArray) -> Unit) } -} \ No newline at end of file +} + +interface PushHandler { + fun onNewToken(payload: JsonString) + fun onMessageReceived(eventId: EventId?, roomId: RoomId?) +} diff --git a/domains/android/push/build.gradle b/domains/android/push/build.gradle index deafddc..0e2d176 100644 --- a/domains/android/push/build.gradle +++ b/domains/android/push/build.gradle @@ -5,7 +5,6 @@ dependencies { implementation project(':core') implementation project(':domains:android:core') implementation project(':domains:store') - implementation project(':matrix:services:push') firebase(it, "messaging") diff --git a/features/notifications/build.gradle b/features/notifications/build.gradle index 85ca262..e579217 100644 --- a/features/notifications/build.gradle +++ b/features/notifications/build.gradle @@ -1,8 +1,6 @@ applyAndroidLibraryModule(project) dependencies { - implementation project(":matrix:services:push") - implementation project(":matrix:services:sync") implementation project(':domains:store') implementation project(":domains:android:work") implementation project(':domains:android:push') diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index 305aa56..f921cfd 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -18,6 +18,7 @@ import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory import app.dapk.st.matrix.message.* import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.matrix.push.installPushService +import app.dapk.st.matrix.push.pushService import app.dapk.st.matrix.room.* import app.dapk.st.matrix.sync.* import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent @@ -37,6 +38,7 @@ class MatrixEngine internal constructor( private val timelineUseCase: Lazy, private val sendMessageUseCase: Lazy, private val matrixMediaDecrypter: Lazy, + private val matrixPushHandler: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() @@ -75,6 +77,10 @@ class MatrixEngine internal constructor( sendMessageUseCase.value.send(message, room) } + override suspend fun registerPushToken(token: String, gatewayUrl: String) { + matrix.value.pushService().registerPush(token, gatewayUrl) + } + override fun mediaDecrypter(): MediaDecrypter { val mediaDecrypter = matrixMediaDecrypter.value return object : MediaDecrypter { @@ -86,6 +92,8 @@ class MatrixEngine internal constructor( } } + override fun pushHandler() = matrixPushHandler.value + class Factory { fun create( @@ -153,8 +161,9 @@ class MatrixEngine internal constructor( } val mediaDecrypter = unsafeLazy { MatrixMediaDecrypter(base64) } + val pushHandler = unsafeLazy { MatrixPushHandler(backgroundScheduler, credentialsStore, lazyMatrix.value.syncService(), roomStore) } - return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase, sendMessageUseCase, mediaDecrypter) + return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase, sendMessageUseCase, mediaDecrypter, pushHandler) } } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/MatrixPushHandler.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt similarity index 84% rename from features/notifications/src/main/kotlin/app/dapk/st/notifications/MatrixPushHandler.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt index 5b39268..3853c90 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/MatrixPushHandler.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt @@ -1,36 +1,34 @@ -package app.dapk.st.notifications +package app.dapk.st.engine import app.dapk.st.core.AppLogTag import app.dapk.st.core.log import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.JsonString import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.message.BackgroundScheduler import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.matrix.sync.SyncService -import app.dapk.st.push.PushHandler -import app.dapk.st.push.PushTokenPayload -import app.dapk.st.work.WorkScheduler import kotlinx.coroutines.* import kotlinx.coroutines.flow.* -import kotlinx.serialization.json.Json private var previousJob: Job? = null @OptIn(DelicateCoroutinesApi::class) class MatrixPushHandler( - private val workScheduler: WorkScheduler, + private val backgroundScheduler: BackgroundScheduler, private val credentialsStore: CredentialsStore, private val syncService: SyncService, private val roomStore: RoomStore, ) : PushHandler { - override fun onNewToken(payload: PushTokenPayload) { + override fun onNewToken(payload: JsonString) { log(AppLogTag.PUSH, "new push token received") - workScheduler.schedule( - WorkScheduler.WorkTask( + backgroundScheduler.schedule( + key = "2", + task = BackgroundScheduler.Task( type = "push_token", - jobId = 2, - jsonPayload = Json.encodeToString(PushTokenPayload.serializer(), payload) + jsonPayload = payload ) ) } @@ -82,4 +80,4 @@ class MatrixPushHandler( } private fun Flow.startInstantly() = this.onStart { emit(Unit) } -} +} \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/BackgroundScheduler.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/BackgroundScheduler.kt index e740ade..04241e0 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/BackgroundScheduler.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/BackgroundScheduler.kt @@ -1,9 +1,11 @@ package app.dapk.st.matrix.message +import app.dapk.st.matrix.common.JsonString + interface BackgroundScheduler { fun schedule(key: String, task: Task) - data class Task(val type: String, val jsonPayload: String) + data class Task(val type: String, val jsonPayload: JsonString) } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt index c6b7374..7dd056f 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt @@ -1,6 +1,7 @@ package app.dapk.st.matrix.message.internal import app.dapk.st.matrix.MatrixTaskRunner +import app.dapk.st.matrix.common.JsonString import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.message.* @@ -69,13 +70,13 @@ internal class DefaultMessageService( is MessageService.Message.TextMessage -> { BackgroundScheduler.Task( type = MATRIX_MESSAGE_TASK_TYPE, - Json.encodeToString(MessageService.Message.TextMessage.serializer(), this) + JsonString(Json.encodeToString(MessageService.Message.TextMessage.serializer(), this)) ) } is MessageService.Message.ImageMessage -> BackgroundScheduler.Task( type = MATRIX_IMAGE_MESSAGE_TASK_TYPE, - Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this) + JsonString(Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this)) ) } } From b987d1e21ca4be65744b6c8872064c3c076e46cd Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 21:37:43 +0100 Subject: [PATCH 22/43] converting profile module to engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 3 +-- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 4 +++ features/profile/build.gradle | 4 +-- .../app/dapk/st/profile/ProfileModule.kt | 10 +++----- .../app/dapk/st/profile/ProfileScreen.kt | 4 +-- .../app/dapk/st/profile/ProfileState.kt | 9 +++---- .../app/dapk/st/profile/ProfileViewModel.kt | 23 ++++++----------- .../app/dapk/st/engine/InviteUseCase.kt | 25 +++++++++++++++++++ .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 25 ++++++++++++++++--- 9 files changed, 69 insertions(+), 38 deletions(-) create mode 100644 matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.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 4cf8b85..99e2d26 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -191,7 +191,7 @@ internal class FeatureModules internal constructor( storeModule.value.messageStore(), ) } - val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) } + val profileModule by unsafeLazy { ProfileModule(matrixModules.engine, trackingModule.errorTracker) } val notificationsModule by unsafeLazy { NotificationsModule( imageLoaderModule.iconLoader(), @@ -459,7 +459,6 @@ internal class MatrixModules( val sync by unsafeLazy { matrix.syncService() } val room by unsafeLazy { matrix.roomService() } - val profile by unsafeLazy { matrix.profileService() } } internal class DomainModules( diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index cf83a79..ad64a3a 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -24,6 +24,10 @@ interface ChatEngine { suspend fun registerPushToken(token: String, gatewayUrl: String) + suspend fun joinRoom(roomId: RoomId) + + suspend fun rejectJoinRoom(roomId: RoomId) + fun mediaDecrypter(): MediaDecrypter fun pushHandler(): PushHandler diff --git a/features/profile/build.gradle b/features/profile/build.gradle index 6a2f7b0..89e50f1 100644 --- a/features/profile/build.gradle +++ b/features/profile/build.gradle @@ -1,9 +1,7 @@ applyAndroidComposeLibraryModule(project) dependencies { - implementation project(":matrix:services:sync") - implementation project(":matrix:services:room") - implementation project(":matrix:services:profile") + implementation project(":chat-engine") implementation project(":features:settings") implementation project(':domains:store') implementation project(":domains:android:compose-core") diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt index 43d7d44..4766144 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileModule.kt @@ -2,19 +2,15 @@ package app.dapk.st.profile import app.dapk.st.core.ProvidableModule import app.dapk.st.core.extensions.ErrorTracker -import app.dapk.st.matrix.room.ProfileService -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.SyncService +import app.dapk.st.engine.ChatEngine class ProfileModule( - private val profileService: ProfileService, - private val syncService: SyncService, - private val roomService: RoomService, + private val chatEngine: ChatEngine, private val errorTracker: ErrorTracker, ) : ProvidableModule { fun profileViewModel(): ProfileViewModel { - return ProfileViewModel(profileService, syncService, roomService, errorTracker) + return ProfileViewModel(chatEngine, errorTracker) } } \ No newline at end of file diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt index 889ecf4..d08d2b4 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileScreen.kt @@ -21,8 +21,8 @@ import app.dapk.st.core.Lce import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.components.CenteredLoading import app.dapk.st.design.components.* -import app.dapk.st.matrix.sync.InviteMeta -import app.dapk.st.matrix.sync.RoomInvite +import app.dapk.st.engine.RoomInvite +import app.dapk.st.engine.RoomInvite.InviteMeta import app.dapk.st.settings.SettingsActivity @Composable diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt index 5e0b6e2..b7754df 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileState.kt @@ -3,9 +3,8 @@ package app.dapk.st.profile import app.dapk.st.core.Lce import app.dapk.st.design.components.Route import app.dapk.st.design.components.SpiderPage -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.room.ProfileService -import app.dapk.st.matrix.sync.RoomInvite +import app.dapk.st.engine.Me +import app.dapk.st.engine.RoomInvite data class ProfileScreenState( val page: SpiderPage, @@ -14,12 +13,12 @@ data class ProfileScreenState( sealed interface Page { data class Profile(val content: Lce) : Page { data class Content( - val me: ProfileService.Me, + val me: Me, val invitationsCount: Int, ) } - data class Invitations(val content: Lce>): Page + data class Invitations(val content: Lce>) : Page object Routes { val profile = Route("Profile") diff --git a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt index bec102a..bb3cfb6 100644 --- a/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt +++ b/features/profile/src/main/kotlin/app/dapk/st/profile/ProfileViewModel.kt @@ -5,25 +5,20 @@ import androidx.lifecycle.viewModelScope import app.dapk.st.core.Lce import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.design.components.SpiderPage +import app.dapk.st.engine.ChatEngine import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.room.ProfileService -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.SyncService import app.dapk.st.viewmodel.DapkViewModel import kotlinx.coroutines.Job import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch class ProfileViewModel( - private val profileService: ProfileService, - private val syncService: SyncService, - private val roomService: RoomService, + private val chatEngine: ChatEngine, private val errorTracker: ErrorTracker, ) : DapkViewModel( ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false)) ) { - private var syncingJob: Job? = null private var currentPageJob: Job? = null fun start() { @@ -31,15 +26,13 @@ class ProfileViewModel( } private fun goToProfile() { - syncingJob = syncService.startSyncing().launchIn(viewModelScope) - combine( flow { - val result = runCatching { profileService.me(forceRefresh = true) } + val result = runCatching { chatEngine.me(forceRefresh = true) } .onFailure { errorTracker.track(it, "Loading profile") } emit(result) }, - syncService.invites(), + chatEngine.invites(), transform = { me, invites -> me to invites } ) .onEach { (me, invites) -> @@ -57,7 +50,7 @@ class ProfileViewModel( fun goToInvitations() { updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) } - syncService.invites() + chatEngine.invites() .onEach { updatePageState { copy(content = Lce.Content(it)) @@ -89,13 +82,13 @@ class ProfileViewModel( } fun acceptRoomInvite(roomId: RoomId) { - launchCatching { roomService.joinRoom(roomId) }.fold( + launchCatching { chatEngine.joinRoom(roomId) }.fold( onError = {} ) } fun rejectRoomInvite(roomId: RoomId) { - launchCatching { roomService.rejectJoinRoom(roomId) }.fold( + launchCatching { chatEngine.rejectJoinRoom(roomId) }.fold( onError = { Log.e("!!!", it.message, it) } @@ -115,7 +108,7 @@ class ProfileViewModel( } fun stop() { - syncingJob?.cancel() + currentPageJob?.cancel() } } diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt new file mode 100644 index 0000000..740a8ce --- /dev/null +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt @@ -0,0 +1,25 @@ +package app.dapk.st.engine + +import app.dapk.st.matrix.sync.SyncService +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +class InviteUseCase( + private val syncService: SyncService +) { + + fun invites() = invitesDatasource() + + private fun invitesDatasource() = combine( + syncService.startSyncing().map { false }.onStart { emit(true) }, + syncService.invites().map { it.map { it.engine() } } + ) { isFirstLoad, invites -> + when { + isFirstLoad && invites.isEmpty() -> null + else -> invites + } + }.filterNotNull() + +} \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index f921cfd..3bda72d 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -39,12 +39,11 @@ class MatrixEngine internal constructor( private val sendMessageUseCase: Lazy, private val matrixMediaDecrypter: Lazy, private val matrixPushHandler: Lazy, + private val inviteUseCase: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() - override fun invites(): Flow { - return matrix.value.syncService().invites().map { it.map { it.engine() } } - } + override fun invites() = inviteUseCase.value.invites() override suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow { return timelineUseCase.value.foo(roomId, isReadReceiptsDisabled = disableReadReceipts) @@ -81,6 +80,14 @@ class MatrixEngine internal constructor( matrix.value.pushService().registerPush(token, gatewayUrl) } + override suspend fun joinRoom(roomId: RoomId) { + matrix.value.roomService().joinRoom(roomId) + } + + override suspend fun rejectJoinRoom(roomId: RoomId) { + matrix.value.roomService().rejectJoinRoom(roomId) + } + override fun mediaDecrypter(): MediaDecrypter { val mediaDecrypter = matrixMediaDecrypter.value return object : MediaDecrypter { @@ -163,7 +170,17 @@ class MatrixEngine internal constructor( val mediaDecrypter = unsafeLazy { MatrixMediaDecrypter(base64) } val pushHandler = unsafeLazy { MatrixPushHandler(backgroundScheduler, credentialsStore, lazyMatrix.value.syncService(), roomStore) } - return MatrixEngine(directoryUseCase, lazyMatrix, timelineUseCase, sendMessageUseCase, mediaDecrypter, pushHandler) + val invitesUseCase = unsafeLazy { InviteUseCase(lazyMatrix.value.syncService()) } + + return MatrixEngine( + directoryUseCase, + lazyMatrix, + timelineUseCase, + sendMessageUseCase, + mediaDecrypter, + pushHandler, + invitesUseCase, + ) } } From 489f45056c7a6a265d3737c0b1ab9ed79101b1c9 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 21:44:06 +0100 Subject: [PATCH 23/43] converting share module to engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 4 +--- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 3 +++ features/share-entry/build.gradle | 4 +--- .../app/dapk/st/share/FetchRoomsUseCase.kt | 19 +++++++++---------- .../app/dapk/st/share/ShareEntryModule.kt | 8 +++----- .../app/dapk/st/share/ShareEntryViewModel.kt | 2 +- .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 2 ++ 7 files changed, 20 insertions(+), 22 deletions(-) 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 99e2d26..becb8af 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -205,7 +205,7 @@ internal class FeatureModules internal constructor( } val shareEntryModule by unsafeLazy { - ShareEntryModule(matrixModules.sync, matrixModules.room) + ShareEntryModule(matrixModules.engine) } val imageGalleryModule by unsafeLazy { @@ -457,8 +457,6 @@ internal class MatrixModules( } } - val sync by unsafeLazy { matrix.syncService() } - val room by unsafeLazy { matrix.roomService() } } internal class DomainModules( diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index ad64a3a..03819e1 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -3,6 +3,7 @@ package app.dapk.st.engine import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.JsonString import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.RoomMember import kotlinx.coroutines.flow.Flow import java.io.InputStream @@ -28,6 +29,8 @@ interface ChatEngine { suspend fun rejectJoinRoom(roomId: RoomId) + suspend fun findMembersSummary(roomId: RoomId): List + fun mediaDecrypter(): MediaDecrypter fun pushHandler(): PushHandler diff --git a/features/share-entry/build.gradle b/features/share-entry/build.gradle index 814f902..176afb2 100644 --- a/features/share-entry/build.gradle +++ b/features/share-entry/build.gradle @@ -4,9 +4,7 @@ dependencies { implementation project(":domains:android:compose-core") implementation project(":domains:android:viewmodel") implementation project(':domains:store') - implementation project(':matrix:services:sync') - implementation project(':matrix:services:room') - implementation project(':matrix:services:message') + implementation project(':chat-engine') implementation project(":core") implementation project(":design-library") implementation project(":features:navigator") diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/FetchRoomsUseCase.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/FetchRoomsUseCase.kt index 18aca0c..2b681ea 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/FetchRoomsUseCase.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/FetchRoomsUseCase.kt @@ -1,21 +1,20 @@ package app.dapk.st.share -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.SyncService +import app.dapk.st.engine.ChatEngine import kotlinx.coroutines.flow.first class FetchRoomsUseCase( - private val syncSyncService: SyncService, - private val roomService: RoomService, + private val chatEngine: ChatEngine, ) { - suspend fun bar(): List { - return syncSyncService.overview().first().map { + suspend fun fetch(): List { + return chatEngine.directory().first().map { + val overview = it.overview Item( - it.roomId, - it.roomAvatarUrl, - it.roomName ?: "", - roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value } + overview.roomId, + overview.roomAvatarUrl, + overview.roomName ?: "", + chatEngine.findMembersSummary(overview.roomId).map { it.displayName ?: it.id.value } ) } } diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryModule.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryModule.kt index ac0f61b..0697547 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryModule.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryModule.kt @@ -1,15 +1,13 @@ package app.dapk.st.share import app.dapk.st.core.ProvidableModule -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.SyncService +import app.dapk.st.engine.ChatEngine class ShareEntryModule( - private val syncService: SyncService, - private val roomService: RoomService, + private val chatEngine: ChatEngine, ) : ProvidableModule { fun shareEntryViewModel(): ShareEntryViewModel { - return ShareEntryViewModel(FetchRoomsUseCase(syncService, roomService)) + return ShareEntryViewModel(FetchRoomsUseCase(chatEngine)) } } \ No newline at end of file diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt index ddd6fbe..4e7d6bf 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt @@ -22,7 +22,7 @@ class ShareEntryViewModel( fun start() { syncJob = viewModelScope.launch { - state = DirectoryScreenState.Content(fetchRoomsUseCase.bar()) + state = DirectoryScreenState.Content(fetchRoomsUseCase.fetch()) } } diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index 3bda72d..d6ad686 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -88,6 +88,8 @@ class MatrixEngine internal constructor( matrix.value.roomService().rejectJoinRoom(roomId) } + override suspend fun findMembersSummary(roomId: RoomId) = matrix.value.roomService().findMembersSummary(roomId) + override fun mediaDecrypter(): MediaDecrypter { val mediaDecrypter = matrixMediaDecrypter.value return object : MediaDecrypter { From 2b55c7dffadedfa4a4cfe52682a4ceebfafa6c1f Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Sun, 9 Oct 2022 21:54:55 +0100 Subject: [PATCH 24/43] converting task running to the engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 258 ++---------------- .../app/dapk/st/graph/TaskRunnerAdapter.kt | 12 +- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 18 +- .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 8 + 4 files changed, 55 insertions(+), 241 deletions(-) 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 becb8af..5fa89c3 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -23,33 +23,19 @@ import app.dapk.st.home.HomeModule import app.dapk.st.home.MainActivity import app.dapk.st.imageloader.ImageLoaderModule import app.dapk.st.login.LoginModule -import app.dapk.st.matrix.MatrixClient import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator -import app.dapk.st.matrix.auth.installAuthService -import app.dapk.st.matrix.common.* -import app.dapk.st.matrix.crypto.RoomMembersProvider -import app.dapk.st.matrix.crypto.Verification -import app.dapk.st.matrix.crypto.cryptoService -import app.dapk.st.matrix.crypto.installCryptoService -import app.dapk.st.matrix.device.deviceService -import app.dapk.st.matrix.device.installEncryptionService -import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory -import app.dapk.st.matrix.message.* +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.JsonString +import app.dapk.st.matrix.common.MatrixLogger +import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.message.internal.ImageContentReader -import app.dapk.st.matrix.push.installPushService -import app.dapk.st.matrix.room.* -import app.dapk.st.matrix.sync.* -import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent -import app.dapk.st.matrix.sync.internal.room.MessageDecrypter import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerModule import app.dapk.st.messenger.gallery.ImageGalleryModule import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.MessageAttachment import app.dapk.st.notifications.NotificationsModule -import app.dapk.st.olm.DeviceKeyFactory import app.dapk.st.olm.OlmPersistenceWrapper -import app.dapk.st.olm.OlmWrapper import app.dapk.st.profile.ProfileModule import app.dapk.st.push.PushHandler import app.dapk.st.push.PushModule @@ -64,7 +50,6 @@ import com.squareup.sqldelight.android.AndroidSqliteDriver import kotlinx.coroutines.Dispatchers import kotlinx.serialization.json.Json import java.io.InputStream -import java.time.Clock internal class AppModule(context: Application, logger: MatrixLogger) { @@ -79,7 +64,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) { private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db") private val database = DapkDb(driver) val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO) - val base64 = AndroidBase64() + private val base64 = AndroidBase64() val storeModule = unsafeLazy { StoreModule( @@ -95,9 +80,10 @@ internal class AppModule(context: Application, logger: MatrixLogger) { private val imageLoaderModule = ImageLoaderModule(context) private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) } - private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta) + private val chatEngineModule = + ChatEngineModule(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta) - val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, context, coroutineDispatchers) + val domainModules = DomainModules(chatEngineModule, trackingModule.errorTracker, context, coroutineDispatchers) val coreAndroidModule = CoreAndroidModule( intentFactory = object : IntentFactory { @@ -131,7 +117,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) { val featureModules = FeatureModules( storeModule, - matrixModules, + chatEngineModule, domainModules, trackingModule, coreAndroidModule, @@ -145,7 +131,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) { internal class FeatureModules internal constructor( private val storeModule: Lazy, - private val matrixModules: MatrixModules, + private val chatEngineModule: ChatEngineModule, private val domainModules: DomainModules, private val trackingModule: TrackingModule, private val coreAndroidModule: CoreAndroidModule, @@ -159,27 +145,27 @@ internal class FeatureModules internal constructor( val directoryModule by unsafeLazy { DirectoryModule( context = context, - chatEngine = matrixModules.engine, + chatEngine = chatEngineModule.engine, ) } val loginModule by unsafeLazy { LoginModule( - matrixModules.engine, + chatEngineModule.engine, domainModules.pushModule, trackingModule.errorTracker ) } val messengerModule by unsafeLazy { MessengerModule( - matrixModules.engine, + chatEngineModule.engine, context, storeModule.value.messageStore(), ) } - val homeModule by unsafeLazy { HomeModule(matrixModules.engine, storeModule.value, buildMeta) } + val homeModule by unsafeLazy { HomeModule(chatEngineModule.engine, storeModule.value, buildMeta) } val settingsModule by unsafeLazy { SettingsModule( - matrixModules.engine, + chatEngineModule.engine, storeModule.value, pushModule, context.contentResolver, @@ -191,7 +177,7 @@ internal class FeatureModules internal constructor( storeModule.value.messageStore(), ) } - val profileModule by unsafeLazy { ProfileModule(matrixModules.engine, trackingModule.errorTracker) } + val profileModule by unsafeLazy { ProfileModule(chatEngineModule.engine, trackingModule.errorTracker) } val notificationsModule by unsafeLazy { NotificationsModule( imageLoaderModule.iconLoader(), @@ -205,7 +191,7 @@ internal class FeatureModules internal constructor( } val shareEntryModule by unsafeLazy { - ShareEntryModule(matrixModules.engine) + ShareEntryModule(chatEngineModule.engine) } val imageGalleryModule by unsafeLazy { @@ -222,7 +208,7 @@ internal class FeatureModules internal constructor( } -internal class MatrixModules( +internal class ChatEngineModule( private val storeModule: Lazy, private val trackingModule: TrackingModule, private val workModule: WorkModule, @@ -257,217 +243,17 @@ internal class MatrixModules( ) } - val matrix by unsafeLazy { - val store = storeModule.value - val credentialsStore = store.credentialsStore() - MatrixClient( - KtorMatrixHttpClientFactory( - credentialsStore, - includeLogging = buildMeta.isDebug, - ), - logger - ).also { - it.install { - installAuthService(credentialsStore, SmallTalkDeviceNameGenerator()) - installEncryptionService(store.knownDevicesStore()) - - val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), base64) - val singletonFlows = SingletonFlows(coroutineDispatchers) - val olm = OlmWrapper( - olmStore = olmAccountStore, - singletonFlows = singletonFlows, - jsonCanonicalizer = JsonCanonicalizer(), - deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()), - errorTracker = trackingModule.errorTracker, - logger = logger, - clock = Clock.systemUTC(), - coroutineDispatchers = coroutineDispatchers, - ) - installCryptoService( - credentialsStore, - olm, - roomMembersProvider = { services -> - RoomMembersProvider { - services.roomService().joinedMembers(it).map { it.userId } - } - }, - base64 = base64, - coroutineDispatchers = coroutineDispatchers, - ) - installMessageService( - store.localEchoStore, - BackgroundWorkAdapter(workModule.workScheduler()), - imageContentReader, - messageEncrypter = { - val cryptoService = it.cryptoService() - MessageEncrypter { message -> - val result = cryptoService.encrypt( - roomId = message.roomId, - credentials = credentialsStore.credentials()!!, - messageJson = message.contents, - ) - - MessageEncrypter.EncryptedMessagePayload( - result.algorithmName, - result.senderKey, - result.cipherText, - result.sessionId, - result.deviceId, - ) - } - }, - mediaEncrypter = { - val cryptoService = it.cryptoService() - MediaEncrypter { input -> - val result = cryptoService.encrypt(input) - MediaEncrypter.Result( - uri = result.uri, - contentLength = result.contentLength, - algorithm = result.algorithm, - ext = result.ext, - keyOperations = result.keyOperations, - kty = result.kty, - k = result.k, - iv = result.iv, - hashes = result.hashes, - v = result.v, - ) - } - }, - ) - - val overviewStore = store.overviewStore() - installRoomService( - storeModule.value.memberStore(), - roomMessenger = { - val messageService = it.messageService() - object : RoomMessenger { - override suspend fun enableEncryption(roomId: RoomId) { - messageService.sendEventMessage( - roomId, MessageService.EventMessage.Encryption( - algorithm = AlgorithmName("m.megolm.v1.aes-sha2") - ) - ) - } - } - }, - roomInviteRemover = { - overviewStore.removeInvites(listOf(it)) - } - ) - - installProfileService(storeModule.value.profileStore(), singletonFlows, credentialsStore) - - installSyncService( - credentialsStore, - overviewStore, - store.roomStore(), - store.syncStore(), - store.filterStore(), - deviceNotifier = { services -> - val encryption = services.deviceService() - val crypto = services.cryptoService() - DeviceNotifier { userIds, syncToken -> - encryption.updateStaleDevices(userIds) - crypto.updateOlmSession(userIds, syncToken) - } - }, - messageDecrypter = { serviceProvider -> - val cryptoService = serviceProvider.cryptoService() - MessageDecrypter { - cryptoService.decrypt(it) - } - }, - keySharer = { serviceProvider -> - val cryptoService = serviceProvider.cryptoService() - KeySharer { sharedRoomKeys -> - cryptoService.importRoomKeys(sharedRoomKeys) - } - }, - verificationHandler = { services -> - val cryptoService = services.cryptoService() - VerificationHandler { apiEvent -> - logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it") - cryptoService.onVerificationEvent( - when (apiEvent) { - is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.transactionId, - apiEvent.content.methods, - apiEvent.content.timestampPosix, - ) - - is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.transactionId, - apiEvent.content.methods, - ) - - is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started( - apiEvent.sender, - apiEvent.content.fromDevice, - apiEvent.content.method, - apiEvent.content.protocols, - apiEvent.content.hashes, - apiEvent.content.codes, - apiEvent.content.short, - apiEvent.content.transactionId, - ) - - is ApiToDeviceEvent.VerificationCancel -> TODO() - is ApiToDeviceEvent.VerificationAccept -> TODO() - is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key( - apiEvent.sender, - apiEvent.content.transactionId, - apiEvent.content.key - ) - - is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac( - apiEvent.sender, - apiEvent.content.transactionId, - apiEvent.content.keys, - apiEvent.content.mac, - ) - } - ) - } - }, - oneTimeKeyProducer = { services -> - val cryptoService = services.cryptoService() - MaybeCreateMoreKeys { - cryptoService.maybeCreateMoreKeys(it) - } - }, - roomMembersService = { services -> - val roomService = services.roomService() - object : RoomMembersService { - override suspend fun find(roomId: RoomId, userIds: List) = roomService.findMembers(roomId, userIds) - override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId) - override suspend fun insert(roomId: RoomId, members: List) = roomService.insertMembers(roomId, members) - } - }, - errorTracker = trackingModule.errorTracker, - coroutineDispatchers = coroutineDispatchers, - ) - - installPushService(credentialsStore) - } - } - } - } internal class DomainModules( - private val matrixModules: MatrixModules, + private val chatEngineModule: ChatEngineModule, private val errorTracker: ErrorTracker, private val context: Application, private val dispatchers: CoroutineDispatchers, ) { private val pushHandler by unsafeLazy { - val enginePushHandler = matrixModules.engine.pushHandler() + val enginePushHandler = chatEngineModule.engine.pushHandler() object : PushHandler { override fun onNewToken(payload: PushTokenPayload) { enginePushHandler.onNewToken(JsonString(Json.encodeToString(PushTokenPayload.serializer(), payload))) @@ -489,7 +275,9 @@ internal class DomainModules( messaging.messaging, ) } - val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.engine))) } + val taskRunnerModule by unsafeLazy { + TaskRunnerModule(TaskRunnerAdapter(chatEngineModule.engine, AppTaskRunner(chatEngineModule.engine))) + } } diff --git a/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt b/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt index 5f9f717..e914fb4 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/TaskRunnerAdapter.kt @@ -1,10 +1,11 @@ package app.dapk.st.graph -import app.dapk.st.matrix.MatrixTaskRunner +import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.ChatEngineTask import app.dapk.st.work.TaskRunner class TaskRunnerAdapter( - private val matrixTaskRunner: suspend (MatrixTaskRunner.MatrixTask) -> MatrixTaskRunner.TaskResult, + private val chatEngine: ChatEngine, private val appTaskRunner: AppTaskRunner, ) : TaskRunner { @@ -12,11 +13,12 @@ class TaskRunnerAdapter( return tasks.map { when { it.task.type.startsWith("matrix") -> { - when (val result = matrixTaskRunner(MatrixTaskRunner.MatrixTask(it.task.type, it.task.jsonPayload))) { - is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry) - MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source) + when (val result = chatEngine.runTask(ChatEngineTask(it.task.type, it.task.jsonPayload))) { + is app.dapk.st.engine.TaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry) + app.dapk.st.engine.TaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source) } } + else -> appTaskRunner.run(it) } } diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 03819e1..16d4def 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -7,7 +7,7 @@ import app.dapk.st.matrix.common.RoomMember import kotlinx.coroutines.flow.Flow import java.io.InputStream -interface ChatEngine { +interface ChatEngine: TaskRunner { fun directory(): Flow @@ -34,8 +34,24 @@ interface ChatEngine { fun mediaDecrypter(): MediaDecrypter fun pushHandler(): PushHandler + } +interface TaskRunner { + + suspend fun runTask(task: ChatEngineTask): TaskResult + + sealed interface TaskResult { + object Success : TaskResult + data class Failure(val canRetry: Boolean) : TaskResult + } + +} + + + +data class ChatEngineTask(val type: String, val jsonPayload: String) + interface MediaDecrypter { fun decrypt(input: InputStream, k: String, iv: String): Collector diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index d6ad686..74386ac 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -6,6 +6,7 @@ import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.SingletonFlows import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.matrix.MatrixClient +import app.dapk.st.matrix.MatrixTaskRunner import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.auth.authService import app.dapk.st.matrix.auth.installAuthService @@ -103,6 +104,13 @@ class MatrixEngine internal constructor( override fun pushHandler() = matrixPushHandler.value + override suspend fun runTask(task: ChatEngineTask): TaskRunner.TaskResult { + return when (val result = matrix.value.run(MatrixTaskRunner.MatrixTask(task.type, task.jsonPayload))) { + is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(result.canRetry) + MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success + } + } + class Factory { fun create( From 6e076e7c9f78e86a51caa2f052f80efd849947f3 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 10 Oct 2022 20:03:53 +0100 Subject: [PATCH 25/43] ensuring the sync emits instantly or after the initial sync --- .../app/dapk/st/core/extensions/FlowExtensions.kt | 6 +++--- .../kotlin/app/dapk/st/engine/DirectoryUseCase.kt | 9 ++------- .../kotlin/app/dapk/st/engine/InviteUseCase.kt | 10 ++-------- .../app/dapk/st/engine/MatrixPushHandler.kt | 6 ++---- .../kotlin/app/dapk/st/engine/TimelineUseCase.kt | 5 ++--- .../kotlin/app/dapk/st/matrix/sync/SyncService.kt | 6 ++++++ .../st/matrix/sync/internal/DefaultSyncService.kt | 15 +++++++++++++-- 7 files changed, 30 insertions(+), 27 deletions(-) diff --git a/core/src/main/kotlin/app/dapk/st/core/extensions/FlowExtensions.kt b/core/src/main/kotlin/app/dapk/st/core/extensions/FlowExtensions.kt index 92be2fb..967c5b5 100644 --- a/core/src/main/kotlin/app/dapk/st/core/extensions/FlowExtensions.kt +++ b/core/src/main/kotlin/app/dapk/st/core/extensions/FlowExtensions.kt @@ -1,6 +1,8 @@ package app.dapk.st.core.extensions -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.takeWhile suspend fun Flow.firstOrNull(count: Int, predicate: suspend (T) -> Boolean): T? { var counter = 0 @@ -18,5 +20,3 @@ suspend fun Flow.firstOrNull(count: Int, predicate: suspend (T) -> Boolea return result } - -fun Flow.startAndIgnoreEmissions(): Flow = this.map { false }.onStart { emit(true) }.filter { it } \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt index b2bec77..e21596f 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/DirectoryUseCase.kt @@ -39,14 +39,9 @@ internal class DirectoryUseCase( } private fun overviewDatasource() = combine( - syncService.startSyncing().map { false }.onStart { emit(true) }, + syncService.startSyncing(), syncService.overview().map { it.map { it.engine() } } - ) { isFirstLoad, overview -> - when { - isFirstLoad && overview.isEmpty() -> null - else -> overview - } - }.filterNotNull() + ) { _, overview -> overview }.filterNotNull() private suspend fun OverviewState.mergeWithLocalEchos(localEchos: Map>, userId: UserId): OverviewState { return when { diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt index 740a8ce..d688bdf 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/InviteUseCase.kt @@ -4,7 +4,6 @@ import app.dapk.st.matrix.sync.SyncService import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onStart class InviteUseCase( private val syncService: SyncService @@ -13,13 +12,8 @@ class InviteUseCase( fun invites() = invitesDatasource() private fun invitesDatasource() = combine( - syncService.startSyncing().map { false }.onStart { emit(true) }, + syncService.startSyncing(), syncService.invites().map { it.map { it.engine() } } - ) { isFirstLoad, invites -> - when { - isFirstLoad && invites.isEmpty() -> null - else -> invites - } - }.filterNotNull() + ) { _, invites -> invites }.filterNotNull() } \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt index 3853c90..f9cac91 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixPushHandler.kt @@ -64,7 +64,7 @@ class MatrixPushHandler( private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? { return withTimeoutOrNull(timeout) { - combine(syncService.startSyncing().startInstantly(), syncService.observeEvent(eventId)) { _, event -> event } + combine(syncService.startSyncing(), syncService.observeEvent(eventId)) { _, event -> event } .firstOrNull { it == eventId } @@ -73,11 +73,9 @@ class MatrixPushHandler( private suspend fun waitForUnreadChange(timeout: Long): String? { return withTimeoutOrNull(timeout) { - combine(syncService.startSyncing().startInstantly(), roomStore.observeUnread()) { _, unread -> unread } + combine(syncService.startSyncing(), roomStore.observeUnread()) { _, unread -> unread } .first() "ignored" } } - - private fun Flow.startInstantly() = this.onStart { emit(Unit) } } \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt index 55b2f30..b25cbe4 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/TimelineUseCase.kt @@ -1,6 +1,5 @@ package app.dapk.st.engine -import app.dapk.st.core.extensions.startAndIgnoreEmissions import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.UserId @@ -44,8 +43,8 @@ internal class TimelineUseCaseImpl( } private fun roomDatasource(roomId: RoomId) = combine( - syncService.startSyncing().startAndIgnoreEmissions(), - syncService.room(roomId).map { it.engine() } + syncService.startSyncing(), + syncService.room(roomId).map { it.engine() } ) { _, room -> room } } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt index 7ad567c..6cba46e 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/SyncService.kt @@ -18,6 +18,11 @@ interface SyncService : MatrixService { fun invites(): Flow fun overview(): Flow fun room(roomId: RoomId): Flow + + /** + * Subscribe to keep the background syncing alive + * Emits once, either when the initial sync completes or immediately if has already sync'd once + */ fun startSyncing(): Flow fun events(roomId: RoomId? = null): Flow> suspend fun observeEvent(eventId: EventId): Flow @@ -31,6 +36,7 @@ interface SyncService : MatrixService { data class Typing(override val roomId: RoomId, val members: List) : SyncEvent } + } fun MatrixServiceInstaller.installSyncService( diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt index aa4c0c7..fa7e04f 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/DefaultSyncService.kt @@ -24,7 +24,7 @@ private val syncSubscriptionCount = AtomicInteger() internal class DefaultSyncService( httpClient: MatrixHttpClient, - syncStore: SyncStore, + private val syncStore: SyncStore, private val overviewStore: OverviewStore, private val roomStore: RoomStore, filterStore: FilterStore, @@ -104,7 +104,18 @@ internal class DefaultSyncService( } } - override fun startSyncing() = syncFlow + override fun startSyncing(): Flow { + return flow { emit(syncStore.read(SyncStore.SyncKey.Overview) != null) }.flatMapMerge { hasSynced -> + when (hasSynced) { + true -> syncFlow.filter { false }.onStart { emit(Unit) } + false -> { + var counter = 0 + syncFlow.filter { counter < 1 }.onEach { counter++ } + } + } + } + } + override fun invites() = overviewStore.latestInvites() override fun overview() = overviewStore.latest() override fun room(roomId: RoomId) = roomStore.latest(roomId) From d49c9cba461874fa0ac2309b76f528f619ab6376 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Oct 2022 05:32:23 +0000 Subject: [PATCH 26/43] Bump turbine from 0.11.0 to 0.12.0 Bumps [turbine](https://github.com/cashapp/turbine) from 0.11.0 to 0.12.0. - [Release notes](https://github.com/cashapp/turbine/releases) - [Changelog](https://github.com/cashapp/turbine/blob/trunk/CHANGELOG.md) - [Commits](https://github.com/cashapp/turbine/compare/0.11.0...0.12.0) --- updated-dependencies: - dependency-name: app.cash.turbine:turbine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- test-harness/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test-harness/build.gradle b/test-harness/build.gradle index fa5f378..5d445a7 100644 --- a/test-harness/build.gradle +++ b/test-harness/build.gradle @@ -9,7 +9,7 @@ test { dependencies { kotlinTest(it) - testImplementation 'app.cash.turbine:turbine:0.11.0' + testImplementation 'app.cash.turbine:turbine:0.12.0' testImplementation Dependencies.mavenCentral.kotlinSerializationJson From 8f1d8cdcc12fcf03feb3a95f56b805ab6a57801e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 12 Oct 2022 19:57:16 +0100 Subject: [PATCH 27/43] porting messenger tests to the engine module --- chat-engine/build.gradle | 4 + .../kotlin/app/dapk/st/engine/ChatEngine.kt | 7 +- .../testFixtures/kotlin/fixture/Fixtures.kt | 31 ++++++++ .../kotlin/test}/FlowTestObserver.kt | 2 + .../kotlin/ViewModelTestScopeImpl.kt | 1 + features/messenger/build.gradle | 3 +- .../st/messenger/MessengerViewModelTest.kt | 77 ++++--------------- matrix-chat-engine/build.gradle | 11 +++ .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 4 +- .../app/dapk/st/engine/ReadMarkingTimeline.kt | 26 ++++--- .../dapk/st/engine}/LocalEchoMapperTest.kt | 16 ++-- .../engine}/MergeWithLocalEchosUseCaseTest.kt | 21 +++-- .../app/dapk/st/engine}/MetaMapperTest.kt | 3 +- .../dapk/st/engine}/TimelineUseCaseTest.kt | 39 ++++++++-- .../test/kotlin/fake}/FakeLocalEventMapper.kt | 4 +- .../test/kotlin/fake}/FakeLocalIdFactory.kt | 4 +- .../src/test/kotlin/fake}/FakeMetaMapper.kt | 4 +- .../kotlin/fake/FakeSyncService.kt | 3 +- 18 files changed, 140 insertions(+), 120 deletions(-) create mode 100644 chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt rename {domains/android/viewmodel/src/testFixtures/kotlin => core/src/testFixtures/kotlin/test}/FlowTestObserver.kt (97%) rename {features/messenger/src/test/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/test/kotlin/app/dapk/st/engine}/LocalEchoMapperTest.kt (82%) rename {features/messenger/src/test/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/test/kotlin/app/dapk/st/engine}/MergeWithLocalEchosUseCaseTest.kt (85%) rename {features/messenger/src/test/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/test/kotlin/app/dapk/st/engine}/MetaMapperTest.kt (96%) rename {features/messenger/src/test/kotlin/app/dapk/st/messenger => matrix-chat-engine/src/test/kotlin/app/dapk/st/engine}/TimelineUseCaseTest.kt (77%) rename {features/messenger/src/test/kotlin/internalfake => matrix-chat-engine/src/test/kotlin/fake}/FakeLocalEventMapper.kt (84%) rename {features/messenger/src/test/kotlin/internalfake => matrix-chat-engine/src/test/kotlin/fake}/FakeLocalIdFactory.kt (77%) rename {features/messenger/src/test/kotlin/internalfake => matrix-chat-engine/src/test/kotlin/fake}/FakeMetaMapper.kt (82%) diff --git a/chat-engine/build.gradle b/chat-engine/build.gradle index 05013e0..8aa6dcf 100644 --- a/chat-engine/build.gradle +++ b/chat-engine/build.gradle @@ -1,8 +1,12 @@ plugins { id 'kotlin' + id 'java-test-fixtures' } dependencies { api Dependencies.mavenCentral.kotlinCoroutinesCore api project(":matrix:common") + + kotlinFixtures(it) + testFixturesImplementation(testFixtures(project(":matrix:common"))) } \ No newline at end of file diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 16d4def..605e552 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -7,13 +7,11 @@ import app.dapk.st.matrix.common.RoomMember import kotlinx.coroutines.flow.Flow import java.io.InputStream -interface ChatEngine: TaskRunner { +interface ChatEngine : TaskRunner { fun directory(): Flow - fun invites(): Flow - - suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow + fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow suspend fun login(request: LoginRequest): LoginResult @@ -49,7 +47,6 @@ interface TaskRunner { } - data class ChatEngineTask(val type: String, val jsonPayload: String) interface MediaDecrypter { diff --git a/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt b/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt new file mode 100644 index 0000000..707e318 --- /dev/null +++ b/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt @@ -0,0 +1,31 @@ +package fixture + +import app.dapk.st.engine.* +import app.dapk.st.matrix.common.* + +fun aMessengerState( + self: UserId = aUserId(), + roomState: RoomState, + typing: Typing? = null +) = MessengerState(self, roomState, typing) + +fun aRoomOverview( + roomId: RoomId = aRoomId(), + roomCreationUtc: Long = 0L, + roomName: String? = null, + roomAvatarUrl: AvatarUrl? = null, + lastMessage: RoomOverview.LastMessage? = null, + isGroup: Boolean = false, + readMarker: EventId? = null, + isEncrypted: Boolean = false, +) = RoomOverview(roomId, roomCreationUtc, roomName, roomAvatarUrl, lastMessage, isGroup, readMarker, isEncrypted) + +fun anEncryptedRoomMessageEvent( + eventId: EventId = anEventId(), + utcTimestamp: Long = 0L, + content: String = "encrypted-content", + author: RoomMember = aRoomMember(), + meta: MessageMeta = MessageMeta.FromServer, + edited: Boolean = false, + redacted: Boolean = false, +) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited, redacted) diff --git a/domains/android/viewmodel/src/testFixtures/kotlin/FlowTestObserver.kt b/core/src/testFixtures/kotlin/test/FlowTestObserver.kt similarity index 97% rename from domains/android/viewmodel/src/testFixtures/kotlin/FlowTestObserver.kt rename to core/src/testFixtures/kotlin/test/FlowTestObserver.kt index 657f258..3ab82cf 100644 --- a/domains/android/viewmodel/src/testFixtures/kotlin/FlowTestObserver.kt +++ b/core/src/testFixtures/kotlin/test/FlowTestObserver.kt @@ -1,3 +1,5 @@ +package test + import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow diff --git a/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTestScopeImpl.kt b/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTestScopeImpl.kt index 83c2e24..d2f0b0f 100644 --- a/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTestScopeImpl.kt +++ b/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTestScopeImpl.kt @@ -3,6 +3,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import org.amshove.kluent.internal.assertEquals import test.ExpectTestScope +import test.FlowTestObserver @Suppress("UNCHECKED_CAST") internal class ViewModelTestScopeImpl( diff --git a/features/messenger/build.gradle b/features/messenger/build.gradle index 13f5e1b..f9edf4e 100644 --- a/features/messenger/build.gradle +++ b/features/messenger/build.gradle @@ -13,11 +13,10 @@ dependencies { kotlinTest(it) - androidImportFixturesWorkaround(project, project(":matrix:services:sync")) - androidImportFixturesWorkaround(project, project(":matrix:services:message")) androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) + androidImportFixturesWorkaround(project, project(":chat-engine")) } \ No newline at end of file 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 e2449e3..fce4662 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 @@ -2,58 +2,38 @@ package app.dapk.st.messenger import ViewModelTest import app.dapk.st.core.Lce -import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.common.UserId -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.message.internal.ImageContentReader -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 app.dapk.st.engine.* +import app.dapk.st.matrix.common.* import fake.FakeMessageOptionsStore -import fake.FakeRoomStore import fixture.* -import internalfake.FakeLocalIdFactory -import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import org.junit.Test import test.delegateReturn -import java.time.Clock -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" private val AN_EVENT_ID = anEventId("state event") private val A_SELF_ID = aUserId("self") +class FakeChatEngine : ChatEngine by mockk() { + + fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn() + +} + class MessengerViewModelTest { private val runViewModelTest = ViewModelTest() - private val fakeMessageService = FakeMessageService() - private val fakeRoomService = FakeRoomService() - 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 fakeChatEngine = FakeChatEngine() private val viewModel = MessengerViewModel( - fakeMessageService, - fakeRoomService, - fakeRoomStore, - fakeCredentialsStore, - fakeObserveTimelineUseCase, - localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance, - imageContentReader = FakeImageContentReader(), - messageOptionsStore = fakeMessageOptionsStore.instance, - clock = fixedClock(A_CURRENT_TIMESTAMP), + fakeChatEngine, + fakeMessageOptionsStore.instance, factory = runViewModelTest.testMutableStateFactory(), ) @@ -73,10 +53,8 @@ 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 = READ_RECEIPTS_ARE_DISABLED) } val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID) - fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state)) + fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state)) viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, attachments = null)) @@ -98,7 +76,7 @@ class MessengerViewModelTest { @Test fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest { - fakeMessageService.expectUnit { it.scheduleMessage(expectEncryptedMessage(A_ROOM_ID, A_LOCAL_ID, A_CURRENT_TIMESTAMP, A_MESSAGE_CONTENT)) } + fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT), aRoomOverview()) } viewModel.test(initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)).post(MessengerAction.ComposerSendText) @@ -114,9 +92,8 @@ class MessengerViewModelTest { return aMessageScreenState(roomId, aMessengerState(roomState = roomState), messageContent) } - private fun expectEncryptedMessage(roomId: RoomId, localId: String, timestamp: Long, messageContent: String): MessageService.Message.TextMessage { - val content = MessageService.Message.Content.TextContent(body = messageContent) - return MessageService.Message.TextMessage(content, sendEncrypted = true, roomId, localId, timestamp) + private fun expectTextMessage(messageContent: String): SendMessage.TextMessage { + return SendMessage.TextMessage(messageContent, reply = null) } private fun aMessengerStateWithEvent(eventId: EventId, selfId: UserId) = aRoomStateWithEventId(eventId).toMessengerState(selfId) @@ -135,27 +112,3 @@ fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerState, m roomState = Lce.Content(roomState), composerState = ComposerState.Text(value = messageContent ?: "", reply = null) ) - -fun aMessengerState( - self: UserId = aUserId(), - roomState: RoomState, - typing: SyncService.SyncEvent.Typing? = null -) = MessengerState(self, roomState, typing) - -class FakeObserveTimelineUseCase : ObserveTimelineUseCase by mockk() { - fun given(roomId: RoomId, selfId: UserId) = coEvery { this@FakeObserveTimelineUseCase.invoke(roomId, selfId) }.delegateReturn() -} - -class FakeMessageService : MessageService by mockk() { - - fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn() - -} - -class FakeRoomService : RoomService by mockk() { - fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn() -} - -fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC) - -class FakeImageContentReader : ImageContentReader by mockk() \ No newline at end of file diff --git a/matrix-chat-engine/build.gradle b/matrix-chat-engine/build.gradle index 140832c..7d6b221 100644 --- a/matrix-chat-engine/build.gradle +++ b/matrix-chat-engine/build.gradle @@ -1,4 +1,5 @@ plugins { + id 'java-test-fixtures' id 'kotlin' } @@ -20,4 +21,14 @@ dependencies { implementation project(":matrix:services:device") implementation project(":matrix:services:crypto") implementation project(":matrix:services:profile") + + kotlinTest(it) + kotlinFixtures(it) + + testImplementation(testFixtures(project(":matrix:services:sync"))) + testImplementation(testFixtures(project(":matrix:services:message"))) + testImplementation(testFixtures(project(":matrix:common"))) + testImplementation(testFixtures(project(":core"))) + testImplementation(testFixtures(project(":domains:store"))) + testImplementation(testFixtures(project(":chat-engine"))) } \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index 74386ac..3494ee9 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -46,8 +46,8 @@ class MatrixEngine internal constructor( override fun directory() = directoryUseCase.value.state() override fun invites() = inviteUseCase.value.invites() - override suspend fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow { - return timelineUseCase.value.foo(roomId, isReadReceiptsDisabled = disableReadReceipts) + override fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow { + return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts) } override suspend fun login(request: LoginRequest): LoginResult { diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt index ee1c641..0e0d5f8 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ReadMarkingTimeline.kt @@ -10,9 +10,7 @@ import app.dapk.st.matrix.sync.RoomStore import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.* class ReadMarkingTimeline( private val roomStore: RoomStore, @@ -21,15 +19,19 @@ class ReadMarkingTimeline( private val roomService: RoomService, ) { - suspend fun foo(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow { - var lastKnownReadEvent: EventId? = null - val credentials = credentialsStore.credentials()!! - roomStore.markRead(roomId) - return observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state -> - state.latestMessageEventFromOthers(self = credentials.userId)?.let { - if (lastKnownReadEvent != it) { - updateRoomReadStateAsync(latestReadEvent = it, state, isReadReceiptsDisabled) - lastKnownReadEvent = it + fun fetch(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow { + return flow { + val credentials = credentialsStore.credentials()!! + roomStore.markRead(roomId) + emit(credentials) + }.flatMapMerge { credentials -> + var lastKnownReadEvent: EventId? = null + observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state -> + state.latestMessageEventFromOthers(self = credentials.userId)?.let { + if (lastKnownReadEvent != it) { + updateRoomReadStateAsync(latestReadEvent = it, state, isReadReceiptsDisabled) + lastKnownReadEvent = it + } } } } diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/LocalEchoMapperTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt similarity index 82% rename from features/messenger/src/test/kotlin/app/dapk/st/messenger/LocalEchoMapperTest.kt rename to matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt index 29b8e59..a58e61b 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/LocalEchoMapperTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt @@ -1,10 +1,10 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.sync.MessageMeta +import fake.FakeMetaMapper import fixture.* -import internalfake.FakeMetaMapper import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -28,7 +28,7 @@ class LocalEchoMapperTest { eventId = echo.eventId!!, content = AN_ECHO_CONTENT.content.body, meta = A_META - ) + ).engine() } @Test @@ -41,24 +41,24 @@ class LocalEchoMapperTest { eventId = anEventId(echo.localId), content = AN_ECHO_CONTENT.content.body, meta = A_META - ) + ).engine() } @Test fun `when merging with echo then updates meta with the echos meta`() = runWith(localEchoMapper) { val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending) - val event = aRoomMessageEvent(meta = previousMeta) + val event = aRoomMessageEvent(meta = previousMeta).engine() val echo = aLocalEcho() - fakeMetaMapper.given(echo).returns(A_META) + fakeMetaMapper.given(echo).returns(A_META.engine() as app.dapk.st.engine.MessageMeta.LocalEcho) val result = event.mergeWith(echo) - result shouldBeEqualTo aRoomMessageEvent(meta = A_META) + result shouldBeEqualTo aRoomMessageEvent(meta = A_META).engine() } private fun givenEcho(eventId: EventId? = null, localId: String = "", meta: MessageMeta.LocalEcho = A_META): MessageService.LocalEcho { return aLocalEcho(eventId = eventId, message = aTextMessage(localId = localId)).also { - fakeMetaMapper.given(it).returns(meta) + fakeMetaMapper.given(it).returns(meta.engine() as app.dapk.st.engine.MessageMeta.LocalEcho) } } } diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt similarity index 85% rename from features/messenger/src/test/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCaseTest.kt rename to matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt index f5bcdaf..b5edabd 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MergeWithLocalEchosUseCaseTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt @@ -1,11 +1,8 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.matrix.common.EventId -import app.dapk.st.matrix.common.MessageType -import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.message.MessageService import fixture.* -import internalfake.FakeLocalEventMapper import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -18,12 +15,12 @@ private val ANOTHER_ROOM_MESSAGE_EVENT = A_ROOM_MESSAGE_EVENT.copy(eventId = anE class MergeWithLocalEchosUseCaseTest { - private val fakeLocalEchoMapper = FakeLocalEventMapper() + private val fakeLocalEchoMapper = fake.FakeLocalEventMapper() private val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance) @Test fun `given no local echos, when merging text message, then returns original state`() { - val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)) + val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)).engine() val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList()) @@ -32,7 +29,7 @@ class MergeWithLocalEchosUseCaseTest { @Test fun `given no local echos, when merging events, then returns original ordered by timestamp descending`() { - val roomState = aRoomState(events = listOf(A_ROOM_IMAGE_MESSAGE_EVENT.copy(utcTimestamp = 1500), A_ROOM_MESSAGE_EVENT.copy(utcTimestamp = 1000))) + val roomState = aRoomState(events = listOf(A_ROOM_IMAGE_MESSAGE_EVENT.copy(utcTimestamp = 1500), A_ROOM_MESSAGE_EVENT.copy(utcTimestamp = 1000))).engine() val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList()) @@ -42,15 +39,15 @@ class MergeWithLocalEchosUseCaseTest { @Test fun `given local echo with sending state, when merging then maps to room event with local echo state`() { val second = createLocalEcho(A_LOCAL_ECHO_EVENT_ID, A_LOCAL_ECHO_BODY, state = MessageService.LocalEcho.State.Sending) - fakeLocalEchoMapper.givenMapping(second, A_ROOM_MEMBER).returns(ANOTHER_ROOM_MESSAGE_EVENT) - val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)) + fakeLocalEchoMapper.givenMapping(second, A_ROOM_MEMBER).returns(ANOTHER_ROOM_MESSAGE_EVENT.engine()) + val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)).engine() val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, listOf(second)) result shouldBeEqualTo roomState.copy( events = listOf( - A_ROOM_MESSAGE_EVENT, - ANOTHER_ROOM_MESSAGE_EVENT, + A_ROOM_MESSAGE_EVENT.engine(), + ANOTHER_ROOM_MESSAGE_EVENT.engine(), ) ) } @@ -60,4 +57,4 @@ class MergeWithLocalEchosUseCaseTest { aTextMessage(aTextContent(body)), state, ) -} +} \ No newline at end of file diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MetaMapperTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MetaMapperTest.kt similarity index 96% rename from features/messenger/src/test/kotlin/app/dapk/st/messenger/MetaMapperTest.kt rename to matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MetaMapperTest.kt index 27419ce..eb2cda1 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/MetaMapperTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MetaMapperTest.kt @@ -1,7 +1,6 @@ -package app.dapk.st.messenger +package app.dapk.st.engine import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.sync.MessageMeta import fixture.aLocalEcho import fixture.aTextMessage import org.amshove.kluent.shouldBeEqualTo diff --git a/features/messenger/src/test/kotlin/app/dapk/st/messenger/TimelineUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt similarity index 77% rename from features/messenger/src/test/kotlin/app/dapk/st/messenger/TimelineUseCaseTest.kt rename to matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt index 8f5447f..4acd7bf 100644 --- a/features/messenger/src/test/kotlin/app/dapk/st/messenger/TimelineUseCaseTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt @@ -1,13 +1,15 @@ -package app.dapk.st.messenger +package app.dapk.st.engine -import FlowTestObserver import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomMember +import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.message.MessageService +import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.SyncService import fake.FakeSyncService import fixture.* +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.CoroutineScope @@ -16,6 +18,7 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Test +import test.FlowTestObserver import test.delegateReturn private val A_ROOM_ID = aRoomId() @@ -47,7 +50,7 @@ class TimelineUseCaseTest { .test(this) .assertValues( listOf( - aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE) + aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE.engine()) ) ) } @@ -57,13 +60,13 @@ class TimelineUseCaseTest { givenSyncEmission(roomState = A_ROOM_STATE, echos = A_LOCAL_ECHOS_LIST) fakeRoomService.givenFindMember(A_ROOM_ID, AN_USER_ID).returns(A_ROOM_MEMBER) - fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE) + fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE.engine()) timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID) .test(this) .assertValues( listOf( - aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE) + aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE.engine()) ) ) } @@ -81,7 +84,11 @@ class TimelineUseCaseTest { .test(this) .assertValues( listOf( - aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE, typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER))) + aMessengerState( + self = AN_USER_ID, + roomState = A_ROOM_STATE.engine(), + typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER)).engine() + ) ) ) } @@ -104,11 +111,27 @@ suspend fun Flow.test(scope: CoroutineScope) = FlowTestObserver(scope, th class FakeMergeWithLocalEchosUseCase : MergeWithLocalEchosUseCase by mockk() { fun givenMerging(roomState: RoomState, roomMember: RoomMember, echos: List) = every { - this@FakeMergeWithLocalEchosUseCase.invoke(roomState, roomMember, echos) + this@FakeMergeWithLocalEchosUseCase.invoke(roomState.engine(), roomMember, echos) }.delegateReturn() } fun aTypingSyncEvent( roomId: RoomId = aRoomId(), members: List = listOf(aRoomMember()) -) = SyncService.SyncEvent.Typing(roomId, members) \ No newline at end of file +) = SyncService.SyncEvent.Typing(roomId, members) + +class FakeMessageService : MessageService by mockk() { + + fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn() + +} + +class FakeRoomService : RoomService by mockk() { + fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn() +} + +fun aMessengerState( + self: UserId = aUserId(), + roomState: app.dapk.st.engine.RoomState, + typing: Typing? = null +) = MessengerState(self, roomState, typing) \ No newline at end of file diff --git a/features/messenger/src/test/kotlin/internalfake/FakeLocalEventMapper.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeLocalEventMapper.kt similarity index 84% rename from features/messenger/src/test/kotlin/internalfake/FakeLocalEventMapper.kt rename to matrix-chat-engine/src/test/kotlin/fake/FakeLocalEventMapper.kt index 7e3d6e1..2365e9d 100644 --- a/features/messenger/src/test/kotlin/internalfake/FakeLocalEventMapper.kt +++ b/matrix-chat-engine/src/test/kotlin/fake/FakeLocalEventMapper.kt @@ -1,8 +1,8 @@ -package internalfake +package fake +import app.dapk.st.engine.LocalEchoMapper import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.message.MessageService -import app.dapk.st.messenger.LocalEchoMapper import io.mockk.every import io.mockk.mockk diff --git a/features/messenger/src/test/kotlin/internalfake/FakeLocalIdFactory.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeLocalIdFactory.kt similarity index 77% rename from features/messenger/src/test/kotlin/internalfake/FakeLocalIdFactory.kt rename to matrix-chat-engine/src/test/kotlin/fake/FakeLocalIdFactory.kt index 6b70cfb..994f22b 100644 --- a/features/messenger/src/test/kotlin/internalfake/FakeLocalIdFactory.kt +++ b/matrix-chat-engine/src/test/kotlin/fake/FakeLocalIdFactory.kt @@ -1,6 +1,6 @@ -package internalfake +package fake -import app.dapk.st.messenger.LocalIdFactory +import app.dapk.st.engine.LocalIdFactory import io.mockk.every import io.mockk.mockk import test.delegateReturn diff --git a/features/messenger/src/test/kotlin/internalfake/FakeMetaMapper.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeMetaMapper.kt similarity index 82% rename from features/messenger/src/test/kotlin/internalfake/FakeMetaMapper.kt rename to matrix-chat-engine/src/test/kotlin/fake/FakeMetaMapper.kt index 6762d99..8722d9a 100644 --- a/features/messenger/src/test/kotlin/internalfake/FakeMetaMapper.kt +++ b/matrix-chat-engine/src/test/kotlin/fake/FakeMetaMapper.kt @@ -1,7 +1,7 @@ -package internalfake +package fake +import app.dapk.st.engine.MetaMapper import app.dapk.st.matrix.message.MessageService -import app.dapk.st.messenger.MetaMapper import io.mockk.every import io.mockk.mockk import test.delegateReturn diff --git a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt index 3909f95..2c1b0bf 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fake/FakeSyncService.kt @@ -5,11 +5,12 @@ import app.dapk.st.matrix.sync.SyncService import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf import test.delegateReturn class FakeSyncService : SyncService by mockk() { fun givenStartsSyncing() { - every { startSyncing() }.returns(emptyFlow()) + every { startSyncing() }.returns(flowOf(Unit)) } fun givenRoom(roomId: RoomId) = every { room(roomId) }.delegateReturn() From d491bb844a5230d65139847b4b578ea90d80351d Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 12 Oct 2022 20:03:44 +0100 Subject: [PATCH 28/43] porting directory tests to engine --- chat-engine/build.gradle | 1 + .../testFixtures/kotlin/fake/FakeChatEngine.kt | 15 +++++++++++++++ features/directory/build.gradle | 3 +-- .../st/directory/DirectoryViewModelTest.kt | 18 +++++++----------- .../st/messenger/MessengerViewModelTest.kt | 18 +++++++----------- .../test/kotlin/fake/FakeDirectoryUseCase.kt | 11 +++++++++++ 6 files changed, 42 insertions(+), 24 deletions(-) create mode 100644 chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt create mode 100644 matrix-chat-engine/src/test/kotlin/fake/FakeDirectoryUseCase.kt diff --git a/chat-engine/build.gradle b/chat-engine/build.gradle index 8aa6dcf..beab4c8 100644 --- a/chat-engine/build.gradle +++ b/chat-engine/build.gradle @@ -9,4 +9,5 @@ dependencies { kotlinFixtures(it) testFixturesImplementation(testFixtures(project(":matrix:common"))) + testFixturesImplementation(testFixtures(project(":core"))) } \ No newline at end of file diff --git a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt new file mode 100644 index 0000000..823bc26 --- /dev/null +++ b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt @@ -0,0 +1,15 @@ +package fake + +import app.dapk.st.engine.ChatEngine +import app.dapk.st.matrix.common.RoomId +import io.mockk.every +import io.mockk.mockk +import test.delegateReturn + +class FakeChatEngine : ChatEngine by mockk() { + + fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn() + + fun givenDirectory() = every { directory() }.delegateReturn() + +} \ No newline at end of file diff --git a/features/directory/build.gradle b/features/directory/build.gradle index 44351f5..67b4807 100644 --- a/features/directory/build.gradle +++ b/features/directory/build.gradle @@ -11,11 +11,10 @@ dependencies { kotlinTest(it) - androidImportFixturesWorkaround(project, project(":matrix:services:sync")) - androidImportFixturesWorkaround(project, project(":matrix:services:message")) androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) + androidImportFixturesWorkaround(project, project(":chat-engine")) } \ No newline at end of file diff --git a/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryViewModelTest.kt b/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryViewModelTest.kt index 752805a..4b0b5a8 100644 --- a/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryViewModelTest.kt +++ b/features/directory/src/test/kotlin/app/dapk/st/directory/DirectoryViewModelTest.kt @@ -1,25 +1,26 @@ package app.dapk.st.directory import ViewModelTest +import app.dapk.st.engine.DirectoryItem +import app.dapk.st.engine.UnreadCount +import fake.FakeChatEngine import fixture.aRoomOverview -import io.mockk.every import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import org.junit.Test -import test.delegateReturn private val AN_OVERVIEW = aRoomOverview() -private val AN_OVERVIEW_STATE = RoomFoo(AN_OVERVIEW, UnreadCount(1), null) +private val AN_OVERVIEW_STATE = DirectoryItem(AN_OVERVIEW, UnreadCount(1), null) class DirectoryViewModelTest { private val runViewModelTest = ViewModelTest() - private val fakeDirectoryUseCase = FakeDirectoryUseCase() private val fakeShortcutHandler = FakeShortcutHandler() + private val fakeChatEngine = FakeChatEngine() private val viewModel = DirectoryViewModel( fakeShortcutHandler.instance, - fakeDirectoryUseCase.instance, + fakeChatEngine, runViewModelTest.testMutableStateFactory(), ) @@ -33,7 +34,7 @@ class DirectoryViewModelTest { @Test fun `when starting, then updates shortcuts and emits room state`() = runViewModelTest { fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) } - fakeDirectoryUseCase.given().returns(flowOf(listOf(AN_OVERVIEW_STATE))) + fakeChatEngine.givenDirectory().returns(flowOf(listOf(AN_OVERVIEW_STATE))) viewModel.test().start() @@ -44,9 +45,4 @@ class DirectoryViewModelTest { class FakeShortcutHandler { val instance = mockk() -} - -class FakeDirectoryUseCase { - val instance = mockk() - fun given() = every { instance.state() }.delegateReturn() } \ No newline at end of file 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 fce4662..fb8680a 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 @@ -2,15 +2,17 @@ package app.dapk.st.messenger import ViewModelTest import app.dapk.st.core.Lce -import app.dapk.st.engine.* -import app.dapk.st.matrix.common.* +import app.dapk.st.engine.MessengerState +import app.dapk.st.engine.RoomState +import app.dapk.st.engine.SendMessage +import app.dapk.st.matrix.common.EventId +import app.dapk.st.matrix.common.RoomId +import app.dapk.st.matrix.common.UserId +import fake.FakeChatEngine import fake.FakeMessageOptionsStore import fixture.* -import io.mockk.every -import io.mockk.mockk import kotlinx.coroutines.flow.flowOf import org.junit.Test -import test.delegateReturn private const val READ_RECEIPTS_ARE_DISABLED = true private val A_ROOM_ID = aRoomId("messenger state room id") @@ -18,12 +20,6 @@ private const val A_MESSAGE_CONTENT = "message content" private val AN_EVENT_ID = anEventId("state event") private val A_SELF_ID = aUserId("self") -class FakeChatEngine : ChatEngine by mockk() { - - fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn() - -} - class MessengerViewModelTest { private val runViewModelTest = ViewModelTest() diff --git a/matrix-chat-engine/src/test/kotlin/fake/FakeDirectoryUseCase.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeDirectoryUseCase.kt new file mode 100644 index 0000000..434e0db --- /dev/null +++ b/matrix-chat-engine/src/test/kotlin/fake/FakeDirectoryUseCase.kt @@ -0,0 +1,11 @@ +package fake + +import app.dapk.st.engine.DirectoryUseCase +import io.mockk.every +import io.mockk.mockk +import test.delegateReturn + +internal class FakeDirectoryUseCase { + val instance = mockk() + fun given() = every { instance.state() }.delegateReturn() +} \ No newline at end of file From 8a95a77d7aa3d4e014648ad3d5072438cb37c759 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 12 Oct 2022 20:07:23 +0100 Subject: [PATCH 29/43] porting settings tests to engine --- .../src/testFixtures/kotlin/fake/FakeChatEngine.kt | 4 ++++ features/settings/build.gradle | 3 +-- .../app/dapk/st/settings/SettingsViewModelTest.kt | 12 ++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt index 823bc26..8028923 100644 --- a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt +++ b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt @@ -2,9 +2,11 @@ package fake import app.dapk.st.engine.ChatEngine import app.dapk.st.matrix.common.RoomId +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import test.delegateReturn +import java.io.InputStream class FakeChatEngine : ChatEngine by mockk() { @@ -12,4 +14,6 @@ class FakeChatEngine : ChatEngine by mockk() { fun givenDirectory() = every { directory() }.delegateReturn() + fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn() + } \ No newline at end of file diff --git a/features/settings/build.gradle b/features/settings/build.gradle index fc553d4..7eb18c8 100644 --- a/features/settings/build.gradle +++ b/features/settings/build.gradle @@ -12,11 +12,10 @@ dependencies { kotlinTest(it) - androidImportFixturesWorkaround(project, project(":matrix:services:sync")) - androidImportFixturesWorkaround(project, project(":matrix:services:crypto")) androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) + androidImportFixturesWorkaround(project, project(":chat-engine")) } \ No newline at end of file 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 87477c4..98c4f12 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 @@ -3,9 +3,8 @@ package app.dapk.st.settings import ViewModelTest import app.dapk.st.core.Lce import app.dapk.st.design.components.SpiderPage -import app.dapk.st.matrix.crypto.ImportResult +import app.dapk.st.engine.ImportResult import fake.* -import fake.FakeStoreCleaner import fixture.aRoomId import internalfake.FakeSettingsItemFactory import internalfake.FakeUriFilenameResolver @@ -35,20 +34,18 @@ internal class SettingsViewModelTest { private val fakeStoreCleaner = FakeStoreCleaner() private val fakeContentResolver = FakeContentResolver() - private val fakeCryptoService = FakeCryptoService() - private val fakeSyncService = FakeSyncService() private val fakeUriFilenameResolver = FakeUriFilenameResolver() private val fakePushTokenRegistrars = FakePushRegistrars() private val fakeSettingsItemFactory = FakeSettingsItemFactory() private val fakeThemeStore = FakeThemeStore() private val fakeLoggingStore = FakeLoggingStore() private val fakeMessageOptionsStore = FakeMessageOptionsStore() + private val fakeChatEngine = FakeChatEngine() private val viewModel = SettingsViewModel( + fakeChatEngine, fakeStoreCleaner, fakeContentResolver.instance, - fakeCryptoService, - fakeSyncService, fakeUriFilenameResolver.instance, fakeSettingsItemFactory.instance, fakePushTokenRegistrars.instance, @@ -174,9 +171,8 @@ internal class SettingsViewModelTest { @Test fun `given success when importing room keys, then emits progress`() = runViewModelTest { - fakeSyncService.expectUnit { it.forceManualRefresh(A_LIST_OF_ROOM_IDS) } fakeContentResolver.givenFile(A_URI.instance).returns(AN_INPUT_STREAM.instance) - fakeCryptoService.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(flowOf(AN_IMPORT_SUCCESS)) + fakeChatEngine.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(flowOf(AN_IMPORT_SUCCESS)) viewModel .test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION)) From 86ad2a8a32b050f1b4e1279f85d4c369883fddef Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 12 Oct 2022 20:10:35 +0100 Subject: [PATCH 30/43] fixing wrong room overview in test state --- .../kotlin/app/dapk/st/messenger/MessengerViewModelTest.kt | 6 ++++-- test-harness/src/test/kotlin/test/impl/InstantScheduler.kt | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) 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 fb8680a..fdaaadc 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 @@ -2,6 +2,7 @@ package app.dapk.st.messenger import ViewModelTest import app.dapk.st.core.Lce +import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.engine.MessengerState import app.dapk.st.engine.RoomState import app.dapk.st.engine.SendMessage @@ -72,9 +73,10 @@ class MessengerViewModelTest { @Test fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest { - fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT), aRoomOverview()) } + val initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT) + fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT), initialState.roomState.takeIfContent()!!.roomState.roomOverview) } - viewModel.test(initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)).post(MessengerAction.ComposerSendText) + viewModel.test(initialState = initialState).post(MessengerAction.ComposerSendText) assertStates({ copy(composerState = ComposerState.Text("", reply = null)) }) verifyExpects() diff --git a/test-harness/src/test/kotlin/test/impl/InstantScheduler.kt b/test-harness/src/test/kotlin/test/impl/InstantScheduler.kt index f0d7788..2cdf443 100644 --- a/test-harness/src/test/kotlin/test/impl/InstantScheduler.kt +++ b/test-harness/src/test/kotlin/test/impl/InstantScheduler.kt @@ -12,7 +12,7 @@ class InstantScheduler(private val matrixClient: MatrixClient) : BackgroundSched matrixClient.run( MatrixTaskRunner.MatrixTask( task.type, - task.jsonPayload + task.jsonPayload.value, ) ) } From 4d033230e44e19ede9e3abd65c7520070c7c1e05 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 12 Oct 2022 20:52:02 +0100 Subject: [PATCH 31/43] porting notifications to chat engine --- .../kotlin/app/dapk/st/graph/AppModule.kt | 3 +- .../kotlin/app/dapk/st/engine/ChatEngine.kt | 17 +++++ .../fixture/NotificationDiffFixtures.kt | 2 +- features/notifications/build.gradle | 4 +- .../st/notifications/NotificationFactory.kt | 4 +- .../NotificationInviteRenderer.kt | 4 +- .../NotificationMessageRenderer.kt | 4 +- .../notifications/NotificationStyleFactory.kt | 2 +- .../st/notifications/NotificationsModule.kt | 11 ++- .../RenderNotificationsUseCase.kt | 13 ++-- .../RoomEventsToNotifiableMapper.kt | 2 +- .../notifications/NotificationFactoryTest.kt | 2 +- .../RenderNotificationsUseCaseTest.kt | 5 +- .../fake/FakeNotificationMessageRenderer.kt | 4 +- .../FakeObserveInviteNotificationsUseCase.kt | 4 +- .../FakeObserveUnreadNotificationsUseCase.kt | 4 +- .../kotlin/app/dapk/st/engine/MatrixEngine.kt | 12 ++++ .../ObserveInviteNotificationsUseCase.kt | 12 +--- .../ObserveUnreadNotificationsUseCaseImpl.kt | 70 +++++++++---------- ...rveUnreadRenderNotificationsUseCaseTest.kt | 35 ++++++---- .../kotlin/fixture/RoomOverviewFixture.kt | 2 +- .../kotlin/fixture/RoomStateFixture.kt | 2 +- 22 files changed, 122 insertions(+), 96 deletions(-) rename {features/notifications/src/test => chat-engine/src/testFixtures}/kotlin/fixture/NotificationDiffFixtures.kt (89%) rename {features/notifications/src/main/kotlin/app/dapk/st/notifications => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/ObserveInviteNotificationsUseCase.kt (82%) rename {features/notifications/src/main/kotlin/app/dapk/st/notifications => matrix-chat-engine/src/main/kotlin/app/dapk/st/engine}/ObserveUnreadNotificationsUseCaseImpl.kt (76%) rename {features/notifications/src/test/kotlin/app/dapk/st/notifications => matrix-chat-engine/src/test/kotlin/app/dapk/st/engine}/ObserveUnreadRenderNotificationsUseCaseTest.kt (69%) 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 5fa89c3..a77fc82 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -180,9 +180,8 @@ internal class FeatureModules internal constructor( val profileModule by unsafeLazy { ProfileModule(chatEngineModule.engine, trackingModule.errorTracker) } val notificationsModule by unsafeLazy { NotificationsModule( + chatEngineModule.engine, imageLoaderModule.iconLoader(), - storeModule.value.roomStore(), - storeModule.value.overviewStore(), context, intentFactory = coreAndroidModule.intentFactory(), dispatchers = coroutineDispatchers, diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt index 605e552..fd88d3d 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/ChatEngine.kt @@ -13,6 +13,9 @@ interface ChatEngine : TaskRunner { fun invites(): Flow fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow + fun notificationsMessages(): Flow + fun notificationsInvites(): Flow + suspend fun login(request: LoginRequest): LoginResult suspend fun me(forceRefresh: Boolean): Me @@ -63,3 +66,17 @@ interface PushHandler { fun onNewToken(payload: JsonString) fun onMessageReceived(eventId: EventId?, roomId: RoomId?) } + +typealias UnreadNotifications = Pair>, NotificationDiff> + +data class NotificationDiff( + val unchanged: Map>, + val changedOrNew: Map>, + val removed: Map>, + val newRooms: Set +) + +data class InviteNotification( + val content: String, + val roomId: RoomId +) \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/fixture/NotificationDiffFixtures.kt b/chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt similarity index 89% rename from features/notifications/src/test/kotlin/fixture/NotificationDiffFixtures.kt rename to chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt index 7b9f0e9..bd50723 100644 --- a/features/notifications/src/test/kotlin/fixture/NotificationDiffFixtures.kt +++ b/chat-engine/src/testFixtures/kotlin/fixture/NotificationDiffFixtures.kt @@ -2,7 +2,7 @@ package fixture import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId -import app.dapk.st.notifications.NotificationDiff +import app.dapk.st.engine.NotificationDiff object NotificationDiffFixtures { diff --git a/features/notifications/build.gradle b/features/notifications/build.gradle index e579217..b7d1171 100644 --- a/features/notifications/build.gradle +++ b/features/notifications/build.gradle @@ -1,6 +1,7 @@ applyAndroidLibraryModule(project) dependencies { + implementation project(":chat-engine") implementation project(':domains:store') implementation project(":domains:android:work") implementation project(':domains:android:push') @@ -10,12 +11,13 @@ dependencies { implementation project(":features:messenger") implementation project(":features:navigator") + implementation Dependencies.mavenCentral.kotlinSerializationJson kotlinTest(it) androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":matrix:common")) - androidImportFixturesWorkaround(project, project(":matrix:services:sync")) + androidImportFixturesWorkaround(project, project(":chat-engine")) androidImportFixturesWorkaround(project, project(":domains:android:stub")) } \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt index 503e074..43b7a9f 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationFactory.kt @@ -4,9 +4,9 @@ import android.app.Notification import android.content.Context import app.dapk.st.core.DeviceMeta import app.dapk.st.core.whenPOrHigher +import app.dapk.st.engine.RoomOverview import app.dapk.st.imageloader.IconLoader import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.navigator.IntentFactory import java.time.Clock @@ -87,7 +87,7 @@ class NotificationFactory( ) } - fun createInvite(inviteNotification: InviteNotification): AndroidNotification { + fun createInvite(inviteNotification: app.dapk.st.engine.InviteNotification): AndroidNotification { val openAppIntent = intentFactory.notificationOpenApp(context) return AndroidNotification( channelId = INVITE_CHANNEL_ID, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt index 8987ea9..63fe6d1 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationInviteRenderer.kt @@ -10,7 +10,7 @@ class NotificationInviteRenderer( private val androidNotificationBuilder: AndroidNotificationBuilder, ) { - fun render(inviteNotification: InviteNotification) { + fun render(inviteNotification: app.dapk.st.engine.InviteNotification) { notificationManager.notify( inviteNotification.roomId.value, INVITE_NOTIFICATION_ID, @@ -18,7 +18,7 @@ class NotificationInviteRenderer( ) } - private fun InviteNotification.toAndroidNotification() = androidNotificationBuilder.build( + private fun app.dapk.st.engine.InviteNotification.toAndroidNotification() = androidNotificationBuilder.build( notificationFactory.createInvite(this) ) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt index 1a12ddb..ab97978 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationMessageRenderer.kt @@ -5,9 +5,9 @@ import app.dapk.st.core.AppLogTag import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.extensions.ifNull import app.dapk.st.core.log +import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.RoomOverview import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import kotlinx.coroutines.withContext private const val SUMMARY_NOTIFICATION_ID = 101 diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStyleFactory.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStyleFactory.kt index 232c273..4cfb12b 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStyleFactory.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStyleFactory.kt @@ -3,8 +3,8 @@ package app.dapk.st.notifications import android.annotation.SuppressLint import app.dapk.st.core.DeviceMeta import app.dapk.st.core.whenPOrHigher +import app.dapk.st.engine.RoomOverview import app.dapk.st.imageloader.IconLoader -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.notifications.AndroidNotificationStyle.Inbox import app.dapk.st.notifications.AndroidNotificationStyle.Messaging diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt index 3368110..5c87c93 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationsModule.kt @@ -5,16 +5,14 @@ import android.content.Context import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.DeviceMeta import app.dapk.st.core.ProvidableModule +import app.dapk.st.engine.ChatEngine import app.dapk.st.imageloader.IconLoader -import app.dapk.st.matrix.sync.OverviewStore -import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.navigator.IntentFactory import java.time.Clock class NotificationsModule( + private val chatEngine: ChatEngine, private val iconLoader: IconLoader, - private val roomStore: RoomStore, - private val overviewStore: OverviewStore, private val context: Context, private val intentFactory: IntentFactory, private val dispatchers: CoroutineDispatchers, @@ -40,10 +38,9 @@ class NotificationsModule( ) return RenderNotificationsUseCase( notificationRenderer = notificationMessageRenderer, - observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore), notificationChannels = NotificationChannels(notificationManager), - observeInviteNotificationsUseCase = ObserveInviteNotificationsUseCaseImpl(overviewStore), - inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder) + inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder), + chatEngine = chatEngine, ) } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt index d51f6e8..097b0c6 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RenderNotificationsUseCase.kt @@ -1,7 +1,9 @@ package app.dapk.st.notifications -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview +import app.dapk.st.engine.ChatEngine +import app.dapk.st.engine.NotificationDiff +import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.RoomOverview import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -9,18 +11,17 @@ import kotlinx.coroutines.flow.onEach class RenderNotificationsUseCase( private val notificationRenderer: NotificationMessageRenderer, private val inviteRenderer: NotificationInviteRenderer, - private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase, - private val observeInviteNotificationsUseCase: ObserveInviteNotificationsUseCase, + private val chatEngine: ChatEngine, private val notificationChannels: NotificationChannels, ) { suspend fun listenForNotificationChanges(scope: CoroutineScope) { notificationChannels.initChannels() - observeRenderableUnreadEventsUseCase() + chatEngine.notificationsMessages() .onEach { (each, diff) -> renderUnreadChange(each, diff) } .launchIn(scope) - observeInviteNotificationsUseCase() + chatEngine.notificationsInvites() .onEach { inviteRenderer.render(it) } .launchIn(scope) } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt index 9f5c05d..49ec278 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt @@ -1,7 +1,7 @@ package app.dapk.st.notifications +import app.dapk.st.engine.RoomEvent import app.dapk.st.matrix.common.RoomMember -import app.dapk.st.matrix.sync.RoomEvent class RoomEventsToNotifiableMapper { diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt index 014cce6..69bc836 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt @@ -137,7 +137,7 @@ class NotificationFactoryTest { fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) val content = "Content message" val result = notificationFactory.createInvite( - InviteNotification( + app.dapk.st.engine.InviteNotification( content = content, A_ROOM_ID, ) diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt index 5175956..c4a30ba 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt @@ -1,5 +1,6 @@ package app.dapk.st.notifications +import app.dapk.st.engine.UnreadNotifications import fake.* import fixture.NotificationDiffFixtures.aNotificationDiff import kotlinx.coroutines.test.TestScope @@ -19,12 +20,12 @@ class RenderNotificationsUseCaseTest { private val fakeNotificationChannels = FakeNotificationChannels().also { it.instance.expect { it.initChannels() } } + private val fakeChatEngine = FakeChatEngine() private val renderNotificationsUseCase = RenderNotificationsUseCase( fakeNotificationMessageRenderer.instance, fakeNotificationInviteRenderer.instance, - fakeObserveUnreadNotificationsUseCase, - fakeObserveInviteNotificationsUseCase, + fakeChatEngine, fakeNotificationChannels.instance, ) diff --git a/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt b/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt index 288d694..c5fbab4 100644 --- a/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt +++ b/features/notifications/src/test/kotlin/fake/FakeNotificationMessageRenderer.kt @@ -2,14 +2,14 @@ package fake import app.dapk.st.notifications.NotificationMessageRenderer import app.dapk.st.notifications.NotificationState -import app.dapk.st.notifications.UnreadNotifications +import app.dapk.st.engine.UnreadNotifications import io.mockk.coVerify import io.mockk.mockk class FakeNotificationMessageRenderer { val instance = mockk() - fun verifyRenders(vararg unreadNotifications: UnreadNotifications) { + fun verifyRenders(vararg unreadNotifications: app.dapk.st.engine.UnreadNotifications) { unreadNotifications.forEach { unread -> coVerify { instance.render( diff --git a/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt b/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt index fba079f..5e1b7eb 100644 --- a/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt +++ b/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt @@ -1,10 +1,10 @@ package fake -import app.dapk.st.notifications.ObserveInviteNotificationsUseCase +import app.dapk.st.engine.ObserveInviteNotificationsUseCase import io.mockk.coEvery import io.mockk.mockk import test.delegateEmit -class FakeObserveInviteNotificationsUseCase : ObserveInviteNotificationsUseCase by mockk() { +class FakeObserveInviteNotificationsUseCase : app.dapk.st.engine.ObserveInviteNotificationsUseCase by mockk() { fun given() = coEvery { this@FakeObserveInviteNotificationsUseCase.invoke() }.delegateEmit() } \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt b/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt index dd881d2..b90b035 100644 --- a/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt +++ b/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt @@ -1,10 +1,10 @@ package fake -import app.dapk.st.notifications.ObserveUnreadNotificationsUseCase +import app.dapk.st.engine.ObserveUnreadNotificationsUseCase import io.mockk.coEvery import io.mockk.mockk import test.delegateEmit -class FakeObserveUnreadNotificationsUseCase : ObserveUnreadNotificationsUseCase by mockk() { +class FakeObserveUnreadNotificationsUseCase : app.dapk.st.engine.ObserveUnreadNotificationsUseCase by mockk() { fun given() = coEvery { this@FakeObserveUnreadNotificationsUseCase.invoke() }.delegateEmit() } \ No newline at end of file diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt index 3494ee9..71981fc 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MatrixEngine.kt @@ -41,6 +41,8 @@ class MatrixEngine internal constructor( private val matrixMediaDecrypter: Lazy, private val matrixPushHandler: Lazy, private val inviteUseCase: Lazy, + private val notificationMessagesUseCase: Lazy, + private val notificationInvitesUseCase: Lazy, ) : ChatEngine { override fun directory() = directoryUseCase.value.state() @@ -50,6 +52,14 @@ class MatrixEngine internal constructor( return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts) } + override fun notificationsMessages(): Flow { + return notificationMessagesUseCase.value.invoke() + } + + override fun notificationsInvites(): Flow { + return notificationInvitesUseCase.value.invoke() + } + override suspend fun login(request: LoginRequest): LoginResult { return matrix.value.authService().login(request.engine()).engine() } @@ -190,6 +200,8 @@ class MatrixEngine internal constructor( mediaDecrypter, pushHandler, invitesUseCase, + unsafeLazy { ObserveUnreadNotificationsUseCaseImpl(roomStore) }, + unsafeLazy { ObserveInviteNotificationsUseCaseImpl(overviewStore) }, ) } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt similarity index 82% rename from features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt index 67d88c5..b926f75 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveInviteNotificationsUseCase.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveInviteNotificationsUseCase.kt @@ -1,17 +1,16 @@ -package app.dapk.st.notifications +package app.dapk.st.engine -import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.sync.InviteMeta import app.dapk.st.matrix.sync.OverviewStore import app.dapk.st.matrix.sync.RoomInvite import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.* -internal typealias ObserveInviteNotificationsUseCase = suspend () -> Flow +internal typealias ObserveInviteNotificationsUseCase = () -> Flow class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase { - override suspend fun invoke(): Flow { + override fun invoke(): Flow { return overviewStore.latestInvites() .diff() .drop(1) @@ -43,8 +42,3 @@ class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewS private fun Flow>.flatten() = this.flatMapConcat { items -> flow { items.forEach { this.emit(it) } } } - -data class InviteNotification( - val content: String, - val roomId: RoomId -) \ No newline at end of file diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt similarity index 76% rename from features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt rename to matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt index 8a3860d..83f2981 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/ObserveUnreadNotificationsUseCaseImpl.kt @@ -1,4 +1,4 @@ -package app.dapk.st.notifications +package app.dapk.st.engine import app.dapk.st.core.AppLogTag import app.dapk.st.core.extensions.clearAndPutAll @@ -6,17 +6,16 @@ import app.dapk.st.core.extensions.containsKey import app.dapk.st.core.log import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.matrix.sync.RoomStore import kotlinx.coroutines.flow.* +import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent +import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview -typealias UnreadNotifications = Pair>, NotificationDiff> -internal typealias ObserveUnreadNotificationsUseCase = suspend () -> Flow +internal typealias ObserveUnreadNotificationsUseCase = () -> Flow class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase { - override suspend fun invoke(): Flow { + override fun invoke(): Flow { return roomStore.observeUnread() .mapWithDiff() .avoidShowingPreviousNotificationsOnLaunch() @@ -25,28 +24,7 @@ class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : } -private fun Flow.onlyRenderableChanges(): Flow { - val inferredCurrentNotifications = mutableMapOf>() - return this - .filter { (_, diff) -> - when { - diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> { - log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes") - false - } - - inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> { - log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read") - false - } - - else -> true - } - } - .onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) } -} - -private fun Flow>>.mapWithDiff(): Flow>, NotificationDiff>> { +private fun Flow>>.mapWithDiff(): Flow>, NotificationDiff>> { val previousUnreadEvents = mutableMapOf>() return this.map { each -> val allUnreadIds = each.toTimestampedIds() @@ -83,19 +61,39 @@ private fun Map>?.toLatestTimestamps() = this?. private fun Map>.toEventIds() = this.mapValues { it.value.map { it.first } } -private fun Map>.toTimestampedIds() = this +private fun Map>.toTimestampedIds() = this .mapValues { it.value.toEventIds() } .mapKeys { it.key.roomId } -private fun List.toEventIds() = this.map { it.eventId to it.utcTimestamp } +private fun List.toEventIds() = this.map { it.eventId to it.utcTimestamp } private fun Flow.avoidShowingPreviousNotificationsOnLaunch() = drop(1) -data class NotificationDiff( - val unchanged: Map>, - val changedOrNew: Map>, - val removed: Map>, - val newRooms: Set -) +private fun Flow>, NotificationDiff>>.onlyRenderableChanges(): Flow { + val inferredCurrentNotifications = mutableMapOf>() + return this + .filter { (_, diff) -> + when { + diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> { + log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes") + false + } + + inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> { + log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read") + false + } + + else -> true + } + } + .onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) } + .map { + val engineModels = it.first + .mapKeys { it.key.engine() } + .mapValues { it.value.map { it.engine() } } + engineModels to it.second + } +} typealias TimestampedEventId = Pair \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt similarity index 69% rename from features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt rename to matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt index 1a932fe..bc77daa 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt @@ -1,24 +1,27 @@ -package app.dapk.st.notifications +package app.dapk.st.engine -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import fake.FakeRoomStore import fixture.NotificationDiffFixtures.aNotificationDiff +import fixture.aMatrixRoomOverview import fixture.aRoomId import fixture.aRoomMessageEvent -import fixture.aRoomOverview import fixture.anEventId import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent +import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview -private val NO_UNREADS = emptyMap>() +private val NO_UNREADS = emptyMap>() private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000) private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000) -private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1")) -private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2")) +private val A_ROOM_OVERVIEW = aMatrixRoomOverview(roomId = aRoomId("1")) +private val A_ROOM_OVERVIEW_2 = aMatrixRoomOverview(roomId = aRoomId("2")) + +private fun MatrixRoomOverview.withUnreads(vararg events: MatrixRoomEvent) = mapOf(this to events.toList()) +private fun MatrixRoomOverview.toDiff(vararg events: MatrixRoomEvent) = mapOf(this.roomId to events.map { it.eventId }) class ObserveUnreadRenderNotificationsUseCaseTest { @@ -33,7 +36,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest { val result = useCase.invoke().toList() result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), newRooms = setOf(A_ROOM_OVERVIEW.roomId) ) @@ -47,11 +50,11 @@ class ObserveUnreadRenderNotificationsUseCaseTest { val result = useCase.invoke().toList() result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), newRooms = setOf(A_ROOM_OVERVIEW.roomId) ), - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) ) } @@ -64,7 +67,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest { val result = useCase.invoke().toList() result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) ) } @@ -92,7 +95,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest { val result = useCase.invoke().toList() result shouldBeEqualTo listOf( - A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff( changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), newRooms = setOf(A_ROOM_OVERVIEW.roomId) ), @@ -110,8 +113,10 @@ class ObserveUnreadRenderNotificationsUseCaseTest { result shouldBeEqualTo emptyList() } - private fun givenNoInitialUnreads(vararg unreads: Map>) = fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads)) + private fun givenNoInitialUnreads(vararg unreads: Map>) = + fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads)) } -private fun RoomOverview.withUnreads(vararg events: RoomEvent) = mapOf(this to events.toList()) -private fun RoomOverview.toDiff(vararg events: RoomEvent) = mapOf(this.roomId to events.map { it.eventId }) +private fun Map>.engine() = this + .mapKeys { it.key.engine() } + .mapValues { it.value.map { it.engine() } } diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt index ecc363c..03c019f 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomOverviewFixture.kt @@ -7,7 +7,7 @@ import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.sync.LastMessage import app.dapk.st.matrix.sync.RoomOverview -fun aRoomOverview( +fun aMatrixRoomOverview( roomId: RoomId = aRoomId(), roomCreationUtc: Long = 0L, roomName: String? = null, diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt index 260e46d..dbb646d 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt @@ -5,6 +5,6 @@ import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.matrix.sync.RoomState fun aRoomState( - roomOverview: RoomOverview = aRoomOverview(), + roomOverview: RoomOverview = aMatrixRoomOverview(), events: List = listOf(aRoomMessageEvent()), ) = RoomState(roomOverview, events) \ No newline at end of file From 709d6c0e1f2284ec68fa0c40bd21544cf948ed1b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 12 Oct 2022 21:05:56 +0100 Subject: [PATCH 32/43] porting notification tests to the engine --- .../kotlin/fake/FakeChatEngine.kt | 5 +++ .../testFixtures/kotlin/fixture/Fixtures.kt | 35 +++++++++++++++++++ .../notifications/NotificationFactoryTest.kt | 2 +- .../notifications/NotificationRendererTest.kt | 4 +-- .../NotificationStateMapperTest.kt | 4 +-- .../RenderNotificationsUseCaseTest.kt | 10 +++--- .../app/dapk/st/engine/LocalEchoMapperTest.kt | 14 ++++---- .../engine/MergeWithLocalEchosUseCaseTest.kt | 12 +++---- ...rveUnreadRenderNotificationsUseCaseTest.kt | 6 ++-- .../app/dapk/st/engine/TimelineUseCaseTest.kt | 4 +-- .../FakeObserveInviteNotificationsUseCase.kt | 2 +- .../FakeObserveUnreadNotificationsUseCase.kt | 2 +- .../internal/room/RoomEventsDecrypterTest.kt | 4 +-- .../internal/sync/EventLookupUseCaseTest.kt | 6 ++-- .../internal/sync/RoomEventCreatorTest.kt | 33 +++++++++-------- .../sync/internal/sync/RoomRefresherTest.kt | 11 +++--- .../sync/TimelineEventsProcessorTest.kt | 4 +-- .../sync/UnreadEventsProcessorTest.kt | 6 ++-- .../kotlin/fixture/RoomEventFixture.kt | 6 ++-- .../kotlin/fixture/RoomStateFixture.kt | 4 +-- 20 files changed, 106 insertions(+), 68 deletions(-) rename {features/notifications => matrix-chat-engine}/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt (68%) rename {features/notifications => matrix-chat-engine}/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt (68%) diff --git a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt index 8028923..bf5ff6f 100644 --- a/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt +++ b/chat-engine/src/testFixtures/kotlin/fake/FakeChatEngine.kt @@ -5,6 +5,7 @@ import app.dapk.st.matrix.common.RoomId import io.mockk.coEvery import io.mockk.every import io.mockk.mockk +import test.delegateEmit import test.delegateReturn import java.io.InputStream @@ -16,4 +17,8 @@ class FakeChatEngine : ChatEngine by mockk() { fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn() + fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit() + + fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit() + } \ No newline at end of file diff --git a/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt b/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt index 707e318..68f252b 100644 --- a/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt +++ b/chat-engine/src/testFixtures/kotlin/fixture/Fixtures.kt @@ -29,3 +29,38 @@ fun anEncryptedRoomMessageEvent( edited: Boolean = false, redacted: Boolean = false, ) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited, redacted) + +fun aRoomImageMessageEvent( + eventId: EventId = anEventId(), + utcTimestamp: Long = 0L, + content: RoomEvent.Image.ImageMeta = anImageMeta(), + author: RoomMember = aRoomMember(), + meta: MessageMeta = MessageMeta.FromServer, + edited: Boolean = false, +) = RoomEvent.Image(eventId, utcTimestamp, content, author, meta, edited) + +fun aRoomReplyMessageEvent( + message: RoomEvent = aRoomMessageEvent(), + replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")), +) = RoomEvent.Reply(message, replyingTo) + +fun aRoomMessageEvent( + eventId: EventId = anEventId(), + utcTimestamp: Long = 0L, + content: String = "message-content", + author: RoomMember = aRoomMember(), + meta: MessageMeta = MessageMeta.FromServer, + edited: Boolean = false, +) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited) + +fun anImageMeta( + width: Int? = 100, + height: Int? = 100, + url: String = "https://a-url.com", + keys: RoomEvent.Image.ImageMeta.Keys? = null +) = RoomEvent.Image.ImageMeta(width, height, url, keys) + +fun aRoomState( + roomOverview: RoomOverview = aRoomOverview(), + events: List = listOf(aRoomMessageEvent()), +) = RoomState(roomOverview, events) \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt index 69bc836..5c26faa 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationFactoryTest.kt @@ -4,8 +4,8 @@ import android.app.Notification import android.app.PendingIntent import android.os.Build import app.dapk.st.core.DeviceMeta +import app.dapk.st.engine.RoomOverview import app.dapk.st.matrix.common.AvatarUrl -import app.dapk.st.matrix.sync.RoomOverview import fake.FakeContext import fixture.NotificationDelegateFixtures.anAndroidNotification import fixture.NotificationDelegateFixtures.anInboxStyle diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt index 0be9e2f..724aa50 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationRendererTest.kt @@ -1,8 +1,8 @@ package app.dapk.st.notifications +import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.RoomOverview import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import fake.FakeNotificationFactory import fake.FakeNotificationManager import fake.aFakeNotification diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationStateMapperTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationStateMapperTest.kt index 045ed36..2711f43 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationStateMapperTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/NotificationStateMapperTest.kt @@ -1,9 +1,9 @@ package app.dapk.st.notifications import android.content.Context +import app.dapk.st.engine.RoomEvent +import app.dapk.st.engine.RoomOverview import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.sync.RoomEvent -import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.navigator.IntentFactory import fixture.NotificationDelegateFixtures.anAndroidNotification import fixture.NotificationFixtures.aDismissRoomNotification diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt index c4a30ba..e99921a 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/RenderNotificationsUseCaseTest.kt @@ -15,8 +15,6 @@ class RenderNotificationsUseCaseTest { private val fakeNotificationMessageRenderer = FakeNotificationMessageRenderer() private val fakeNotificationInviteRenderer = FakeNotificationInviteRenderer() - private val fakeObserveUnreadNotificationsUseCase = FakeObserveUnreadNotificationsUseCase() - private val fakeObserveInviteNotificationsUseCase = FakeObserveInviteNotificationsUseCase() private val fakeNotificationChannels = FakeNotificationChannels().also { it.instance.expect { it.initChannels() } } @@ -32,8 +30,8 @@ class RenderNotificationsUseCaseTest { @Test fun `given events, when listening for changes then initiates channels once`() = runTest { fakeNotificationMessageRenderer.instance.expect { it.render(any()) } - fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) - fakeObserveInviteNotificationsUseCase.given().emits() + fakeChatEngine.givenNotificationsMessages().emits(AN_UNREAD_NOTIFICATIONS) + fakeChatEngine.givenNotificationsInvites().emits() renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher())) @@ -43,8 +41,8 @@ class RenderNotificationsUseCaseTest { @Test fun `given renderable unread events, when listening for changes, then renders change`() = runTest { fakeNotificationMessageRenderer.instance.expect { it.render(any()) } - fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) - fakeObserveInviteNotificationsUseCase.given().emits() + fakeChatEngine.givenNotificationsMessages().emits(AN_UNREAD_NOTIFICATIONS) + fakeChatEngine.givenNotificationsInvites().emits() renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher())) diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt index a58e61b..a2f794d 100644 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/LocalEchoMapperTest.kt @@ -27,8 +27,8 @@ class LocalEchoMapperTest { result shouldBeEqualTo aRoomMessageEvent( eventId = echo.eventId!!, content = AN_ECHO_CONTENT.content.body, - meta = A_META - ).engine() + meta = A_META.engine() + ) } @Test @@ -40,20 +40,20 @@ class LocalEchoMapperTest { result shouldBeEqualTo aRoomMessageEvent( eventId = anEventId(echo.localId), content = AN_ECHO_CONTENT.content.body, - meta = A_META - ).engine() + meta = A_META.engine() + ) } @Test fun `when merging with echo then updates meta with the echos meta`() = runWith(localEchoMapper) { - val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending) - val event = aRoomMessageEvent(meta = previousMeta).engine() + val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending).engine() + val event = aRoomMessageEvent(meta = previousMeta) val echo = aLocalEcho() fakeMetaMapper.given(echo).returns(A_META.engine() as app.dapk.st.engine.MessageMeta.LocalEcho) val result = event.mergeWith(echo) - result shouldBeEqualTo aRoomMessageEvent(meta = A_META).engine() + result shouldBeEqualTo aRoomMessageEvent(meta = A_META.engine()) } private fun givenEcho(eventId: EventId? = null, localId: String = "", meta: MessageMeta.LocalEcho = A_META): MessageService.LocalEcho { diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt index b5edabd..5603193 100644 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/MergeWithLocalEchosUseCaseTest.kt @@ -20,7 +20,7 @@ class MergeWithLocalEchosUseCaseTest { @Test fun `given no local echos, when merging text message, then returns original state`() { - val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)).engine() + val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)) val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList()) @@ -29,7 +29,7 @@ class MergeWithLocalEchosUseCaseTest { @Test fun `given no local echos, when merging events, then returns original ordered by timestamp descending`() { - val roomState = aRoomState(events = listOf(A_ROOM_IMAGE_MESSAGE_EVENT.copy(utcTimestamp = 1500), A_ROOM_MESSAGE_EVENT.copy(utcTimestamp = 1000))).engine() + val roomState = aRoomState(events = listOf(A_ROOM_IMAGE_MESSAGE_EVENT.copy(utcTimestamp = 1500), A_ROOM_MESSAGE_EVENT.copy(utcTimestamp = 1000))) val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, emptyList()) @@ -39,15 +39,15 @@ class MergeWithLocalEchosUseCaseTest { @Test fun `given local echo with sending state, when merging then maps to room event with local echo state`() { val second = createLocalEcho(A_LOCAL_ECHO_EVENT_ID, A_LOCAL_ECHO_BODY, state = MessageService.LocalEcho.State.Sending) - fakeLocalEchoMapper.givenMapping(second, A_ROOM_MEMBER).returns(ANOTHER_ROOM_MESSAGE_EVENT.engine()) - val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)).engine() + fakeLocalEchoMapper.givenMapping(second, A_ROOM_MEMBER).returns(ANOTHER_ROOM_MESSAGE_EVENT) + val roomState = aRoomState(events = listOf(A_ROOM_MESSAGE_EVENT)) val result = mergeWithLocalEchosUseCase.invoke(roomState, A_ROOM_MEMBER, listOf(second)) result shouldBeEqualTo roomState.copy( events = listOf( - A_ROOM_MESSAGE_EVENT.engine(), - ANOTHER_ROOM_MESSAGE_EVENT.engine(), + A_ROOM_MESSAGE_EVENT, + ANOTHER_ROOM_MESSAGE_EVENT, ) ) } diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt index bc77daa..50bd2b9 100644 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/ObserveUnreadRenderNotificationsUseCaseTest.kt @@ -2,9 +2,9 @@ package app.dapk.st.engine import fake.FakeRoomStore import fixture.NotificationDiffFixtures.aNotificationDiff +import fixture.aMatrixRoomMessageEvent import fixture.aMatrixRoomOverview import fixture.aRoomId -import fixture.aRoomMessageEvent import fixture.anEventId import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.toList @@ -15,8 +15,8 @@ import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview private val NO_UNREADS = emptyMap>() -private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000) -private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000) +private val A_MESSAGE = aMatrixRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000) +private val A_MESSAGE_2 = aMatrixRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000) private val A_ROOM_OVERVIEW = aMatrixRoomOverview(roomId = aRoomId("1")) private val A_ROOM_OVERVIEW_2 = aMatrixRoomOverview(roomId = aRoomId("2")) diff --git a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt index 4acd7bf..c2edd7d 100644 --- a/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt +++ b/matrix-chat-engine/src/test/kotlin/app/dapk/st/engine/TimelineUseCaseTest.kt @@ -23,8 +23,8 @@ import test.delegateReturn private val A_ROOM_ID = aRoomId() private val AN_USER_ID = aUserId() -private val A_ROOM_STATE = aRoomState() -private val A_MERGED_ROOM_STATE = A_ROOM_STATE.copy(events = listOf(aRoomMessageEvent(content = "a merged event"))) +private val A_ROOM_STATE = aMatrixRoomState() +private val A_MERGED_ROOM_STATE = A_ROOM_STATE.copy(events = listOf(aMatrixRoomMessageEvent(content = "a merged event"))) private val A_LOCAL_ECHOS_LIST = listOf(aLocalEcho()) private val A_ROOM_MEMBER = aRoomMember() diff --git a/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt similarity index 68% rename from features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt rename to matrix-chat-engine/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt index 5e1b7eb..886fe27 100644 --- a/features/notifications/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt +++ b/matrix-chat-engine/src/test/kotlin/fake/FakeObserveInviteNotificationsUseCase.kt @@ -5,6 +5,6 @@ import io.mockk.coEvery import io.mockk.mockk import test.delegateEmit -class FakeObserveInviteNotificationsUseCase : app.dapk.st.engine.ObserveInviteNotificationsUseCase by mockk() { +class FakeObserveInviteNotificationsUseCase : ObserveInviteNotificationsUseCase by mockk() { fun given() = coEvery { this@FakeObserveInviteNotificationsUseCase.invoke() }.delegateEmit() } \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt b/matrix-chat-engine/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt similarity index 68% rename from features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt rename to matrix-chat-engine/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt index b90b035..d04b782 100644 --- a/features/notifications/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt +++ b/matrix-chat-engine/src/test/kotlin/fake/FakeObserveUnreadNotificationsUseCase.kt @@ -5,6 +5,6 @@ import io.mockk.coEvery import io.mockk.mockk import test.delegateEmit -class FakeObserveUnreadNotificationsUseCase : app.dapk.st.engine.ObserveUnreadNotificationsUseCase by mockk() { +class FakeObserveUnreadNotificationsUseCase : ObserveUnreadNotificationsUseCase by mockk() { fun given() = coEvery { this@FakeObserveUnreadNotificationsUseCase.invoke() }.delegateEmit() } \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt index 3178ae9..23a5623 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt @@ -15,7 +15,7 @@ import org.junit.Test private const val A_DECRYPTED_MESSAGE_CONTENT = "decrypted - content" private val AN_ENCRYPTED_ROOM_CONTENT = aMegolmV1() -private val AN_ENCRYPTED_ROOM_MESSAGE = aRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT) +private val AN_ENCRYPTED_ROOM_MESSAGE = aMatrixRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT) private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent( message = AN_ENCRYPTED_ROOM_MESSAGE, replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event")) @@ -37,7 +37,7 @@ class RoomEventsDecrypterTest { @Test fun `given clear message event, when decrypting, then does nothing`() = runTest { - val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null) + val aClearMessageEvent = aMatrixRoomMessageEvent(encryptedContent = null) val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent)) result shouldBeEqualTo listOf(aClearMessageEvent) diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt index b837fb6..5bc47b6 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/EventLookupUseCaseTest.kt @@ -1,7 +1,7 @@ package app.dapk.st.matrix.sync.internal.sync import fake.FakeRoomStore -import fixture.aRoomMessageEvent +import fixture.aMatrixRoomMessageEvent import fixture.anEventId import internalfixture.aTimelineTextEventContent import internalfixture.anApiTimelineTextEvent @@ -11,8 +11,8 @@ import org.junit.Test private val AN_EVENT_ID = anEventId() private val A_TIMELINE_EVENT = anApiTimelineTextEvent(AN_EVENT_ID, content = aTimelineTextEventContent(body = "timeline event")) -private val A_ROOM_EVENT = aRoomMessageEvent(AN_EVENT_ID, content = "previous room event") -private val A_PERSISTED_EVENT = aRoomMessageEvent(AN_EVENT_ID, content = "persisted event") +private val A_ROOM_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "previous room event") +private val A_PERSISTED_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "persisted event") class EventLookupUseCaseTest { diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt index f0b9a94..b265549 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt @@ -5,7 +5,6 @@ import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent import fake.FakeErrorTracker -import fake.FakeMatrixLogger import fake.FakeRoomMembersService import fixture.* import internalfixture.* @@ -41,7 +40,7 @@ internal class RoomEventCreatorTest { val result = with(roomEventCreator) { megolmEvent.toRoomEvent(A_ROOM_ID) } - result shouldBeEqualTo aRoomMessageEvent( + result shouldBeEqualTo aMatrixRoomMessageEvent( eventId = megolmEvent.eventId, utcTimestamp = megolmEvent.utcTimestamp, content = "Encrypted message", @@ -74,7 +73,7 @@ internal class RoomEventCreatorTest { val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } - result shouldBeEqualTo aRoomMessageEvent( + result shouldBeEqualTo aMatrixRoomMessageEvent( eventId = A_TEXT_EVENT.id, utcTimestamp = A_TEXT_EVENT.utcTimestamp, content = A_TEXT_EVENT_MESSAGE, @@ -88,7 +87,7 @@ internal class RoomEventCreatorTest { val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } - result shouldBeEqualTo aRoomMessageEvent( + result shouldBeEqualTo aMatrixRoomMessageEvent( eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id, utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp, content = "redacted", @@ -103,7 +102,7 @@ internal class RoomEventCreatorTest { val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } - result shouldBeEqualTo aRoomMessageEvent( + result shouldBeEqualTo aMatrixRoomMessageEvent( eventId = editEvent.id, utcTimestamp = editEvent.utcTimestamp, content = editEvent.asTextContent().body!!, @@ -121,7 +120,7 @@ internal class RoomEventCreatorTest { val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - result shouldBeEqualTo aRoomMessageEvent( + result shouldBeEqualTo aMatrixRoomMessageEvent( eventId = originalMessage.id, utcTimestamp = editedMessage.utcTimestamp, content = A_TEXT_EVENT_MESSAGE, @@ -133,13 +132,13 @@ internal class RoomEventCreatorTest { @Test fun `given edited event which relates to a room event then updates existing message`() = runTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = aRoomMessageEvent() + val originalMessage = aMatrixRoomMessageEvent() val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } - result shouldBeEqualTo aRoomMessageEvent( + result shouldBeEqualTo aMatrixRoomMessageEvent( eventId = originalMessage.eventId, utcTimestamp = editedMessage.utcTimestamp, content = A_TEXT_EVENT_MESSAGE, @@ -151,7 +150,7 @@ internal class RoomEventCreatorTest { @Test fun `given edited event which relates to a room reply event then only updates message`() = runTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = aRoomReplyMessageEvent(message = aRoomMessageEvent()) + val originalMessage = aRoomReplyMessageEvent(message = aMatrixRoomMessageEvent()) val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) @@ -159,7 +158,7 @@ internal class RoomEventCreatorTest { result shouldBeEqualTo aRoomReplyMessageEvent( replyingTo = originalMessage.replyingTo, - message = aRoomMessageEvent( + message = aMatrixRoomMessageEvent( eventId = originalMessage.eventId, utcTimestamp = editedMessage.utcTimestamp, content = A_TEXT_EVENT_MESSAGE, @@ -182,7 +181,7 @@ internal class RoomEventCreatorTest { @Test fun `given edited event is older than related room event then ignores edit`() = runTest { - val originalMessage = aRoomMessageEvent(utcTimestamp = 1000) + val originalMessage = aMatrixRoomMessageEvent(utcTimestamp = 1000) val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) @@ -199,7 +198,7 @@ internal class RoomEventCreatorTest { println(replyEvent.content) val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } - result shouldBeEqualTo aRoomMessageEvent( + result shouldBeEqualTo aMatrixRoomMessageEvent( eventId = replyEvent.id, utcTimestamp = replyEvent.utcTimestamp, content = replyEvent.asTextContent().body!!, @@ -217,13 +216,13 @@ internal class RoomEventCreatorTest { val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } result shouldBeEqualTo aRoomReplyMessageEvent( - replyingTo = aRoomMessageEvent( + replyingTo = aMatrixRoomMessageEvent( eventId = originalMessage.id, utcTimestamp = originalMessage.utcTimestamp, content = originalMessage.asTextContent().body!!, author = A_SENDER, ), - message = aRoomMessageEvent( + message = aMatrixRoomMessageEvent( eventId = replyMessage.id, utcTimestamp = replyMessage.utcTimestamp, content = A_REPLY_EVENT_MESSAGE, @@ -235,7 +234,7 @@ internal class RoomEventCreatorTest { @Test fun `given reply event which relates to a room event then maps to reply`() = runTest { fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) - val originalMessage = aRoomMessageEvent() + val originalMessage = aMatrixRoomMessageEvent() val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE) val lookup = givenLookup(originalMessage) @@ -243,7 +242,7 @@ internal class RoomEventCreatorTest { result shouldBeEqualTo aRoomReplyMessageEvent( replyingTo = originalMessage, - message = aRoomMessageEvent( + message = aMatrixRoomMessageEvent( eventId = replyMessage.id, utcTimestamp = replyMessage.utcTimestamp, content = A_REPLY_EVENT_MESSAGE, @@ -263,7 +262,7 @@ internal class RoomEventCreatorTest { result shouldBeEqualTo aRoomReplyMessageEvent( replyingTo = originalMessage.message, - message = aRoomMessageEvent( + message = aMatrixRoomMessageEvent( eventId = replyMessage.id, utcTimestamp = replyMessage.utcTimestamp, content = A_REPLY_EVENT_MESSAGE, diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt index 60a76fb..b0b1ef3 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomRefresherTest.kt @@ -5,8 +5,8 @@ import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomState import fake.FakeMatrixLogger import fake.FakeRoomDataSource -import internalfake.FakeRoomEventsDecrypter import fixture.* +import internalfake.FakeRoomEventsDecrypter import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test @@ -15,13 +15,14 @@ import test.expect private val A_ROOM_ID = aRoomId() private object ARoom { - val MESSAGE_EVENT = aRoomMessageEvent(utcTimestamp = 0) + val MESSAGE_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 0) val ENCRYPTED_EVENT = anEncryptedRoomMessageEvent(utcTimestamp = 1) - val DECRYPTED_EVENT = aRoomMessageEvent(utcTimestamp = 2) - val PREVIOUS_STATE = RoomState(aRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_EVENT)) + val DECRYPTED_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 2) + val PREVIOUS_STATE = RoomState(aMatrixRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_EVENT)) val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT) - val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS) + val NEW_STATE = RoomState(aMatrixRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS) } + private val A_USER_CREDENTIALS = aUserCredentials() internal class RoomRefresherTest { diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt index 31ecef5..d8a1d4a 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/TimelineEventsProcessorTest.kt @@ -19,7 +19,7 @@ private val A_ROOM_ID = aRoomId() private val ANY_LOOKUP_RESULT = LookupResult(anApiTimelineTextEvent(), roomEvent = null) private val AN_ENCRYPTED_TIMELINE_EVENT = anEncryptedApiTimelineEvent() private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent() -private val A_MESSAGE_ROOM_EVENT = aRoomMessageEvent(anEventId("a-message")) +private val A_MESSAGE_ROOM_EVENT = aMatrixRoomMessageEvent(anEventId("a-message")) private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message")) private val A_LOOKUP_EVENT_ID = anEventId("lookup-id") private val A_USER_CREDENTIALS = aUserCredentials() @@ -52,7 +52,7 @@ class TimelineEventsProcessorTest { @Test fun `given encrypted and text timeline events when processing then maps to room events`() = runTest { - val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event"))) + val previousEvents = listOf(aMatrixRoomMessageEvent(eventId = anEventId("previous-event"))) val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT) val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents))) fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents) diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessorTest.kt index 9fd79c2..28d2be1 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessorTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessorTest.kt @@ -8,8 +8,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import test.expect -private val A_ROOM_OVERVIEW = aRoomOverview() -private val A_ROOM_MESSAGE_FROM_OTHER = aRoomMessageEvent( +private val A_ROOM_OVERVIEW = aMatrixRoomOverview() +private val A_ROOM_MESSAGE_FROM_OTHER = aMatrixRoomMessageEvent( eventId = anEventId("a-new-message-event"), author = aRoomMember(id = aUserId("a-different-user")) ) @@ -27,7 +27,7 @@ internal class UnreadEventsProcessorTest { fun `given initial sync when processing unread then does mark any events as unread`() = runTest { unreadEventsProcessor.processUnreadState( isInitialSync = true, - overview = aRoomOverview(), + overview = aMatrixRoomOverview(), previousState = null, newEvents = emptyList(), selfId = aUserId() diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt index 65b1c09..9b4d641 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt @@ -4,7 +4,7 @@ import app.dapk.st.matrix.common.* import app.dapk.st.matrix.sync.MessageMeta import app.dapk.st.matrix.sync.RoomEvent -fun aRoomMessageEvent( +fun aMatrixRoomMessageEvent( eventId: EventId = anEventId(), utcTimestamp: Long = 0L, content: String = "message-content", @@ -25,8 +25,8 @@ fun aRoomImageMessageEvent( ) = RoomEvent.Image(eventId, utcTimestamp, content, author, meta, encryptedContent, edited) fun aRoomReplyMessageEvent( - message: RoomEvent = aRoomMessageEvent(), - replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")), + message: RoomEvent = aMatrixRoomMessageEvent(), + replyingTo: RoomEvent = aMatrixRoomMessageEvent(eventId = anEventId("in-reply-to-id")), ) = RoomEvent.Reply(message, replyingTo) fun anEncryptedRoomMessageEvent( diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt index dbb646d..511bcf4 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomStateFixture.kt @@ -4,7 +4,7 @@ import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.matrix.sync.RoomState -fun aRoomState( +fun aMatrixRoomState( roomOverview: RoomOverview = aMatrixRoomOverview(), - events: List = listOf(aRoomMessageEvent()), + events: List = listOf(aMatrixRoomMessageEvent()), ) = RoomState(roomOverview, events) \ No newline at end of file From cf9d94a72b3974ae356dece85388456b1990c306 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 12 Oct 2022 22:36:17 +0100 Subject: [PATCH 33/43] Fix Android 13 being unable to launch attachment picker - Android 13 requires the read_media_images permission instead of read external --- .../dapk/st/messenger/gallery/ImageGalleryActivity.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt index 7ccd939..1b4a23e 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/gallery/ImageGalleryActivity.kt @@ -5,6 +5,7 @@ import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri +import android.os.Build import android.os.Bundle import android.os.Parcelable import androidx.activity.result.contract.ActivityResultContract @@ -32,7 +33,7 @@ class ImageGalleryActivity : DapkActivity() { val permissionState = mutableStateOf>(Lce.Loading()) lifecycleScope.launch { - permissionState.value = runCatching { ensurePermission(Manifest.permission.READ_EXTERNAL_STORAGE) }.fold( + permissionState.value = runCatching { ensurePermission(mediaPermission()) }.fold( onSuccess = { Lce.Content(it) }, onFailure = { Lce.Error(it) } ) @@ -49,6 +50,12 @@ class ImageGalleryActivity : DapkActivity() { } } } + + private fun mediaPermission() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + Manifest.permission.READ_MEDIA_IMAGES + } else { + Manifest.permission.READ_EXTERNAL_STORAGE + } } @Composable From b8689292c3b11818ee5adab01e182d9a37b88670 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 12 Oct 2022 22:59:31 +0100 Subject: [PATCH 34/43] request notification permission on android 13 when showing home content --- .../main/kotlin/app/dapk/st/core/DapkActivity.kt | 10 ++++++++++ .../src/main/kotlin/app/dapk/st/home/HomeState.kt | 1 + .../main/kotlin/app/dapk/st/home/HomeViewModel.kt | 2 ++ .../main/kotlin/app/dapk/st/home/MainActivity.kt | 13 ++++++++++++- features/notifications/src/main/AndroidManifest.xml | 4 +++- 5 files changed, 28 insertions(+), 2 deletions(-) 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 4a9cc24..4c75c4f 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 @@ -75,6 +75,16 @@ abstract class DapkActivity : ComponentActivity(), EffectScope { } } + protected fun registerForPermission(permission: String, callback: () -> Unit = {}): () -> Unit { + val resultCallback: (result: Boolean) -> Unit = { result -> + if (result) { + callback() + } + } + val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission(), resultCallback) + return { launcher.launch(permission) } + } + protected suspend fun ensurePermission(permission: String): PermissionResult { return when { checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED -> PermissionResult.Granted diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt index c7d74a3..22ec6aa 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeState.kt @@ -21,5 +21,6 @@ sealed interface HomeScreenState { sealed interface HomeEvent { object Relaunch : HomeEvent + object OnShowContent : HomeEvent } diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index 456f0a0..6de8f9a 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -39,6 +39,7 @@ class HomeViewModel( fun start() { viewModelScope.launch { state = if (credentialsProvider.isSignedIn()) { + _events.emit(HomeEvent.OnShowContent) initialHomeContent() } else { SignedOut @@ -62,6 +63,7 @@ class HomeViewModel( fun loggedIn() { viewModelScope.launch { state = initialHomeContent() + _events.emit(HomeEvent.OnShowContent) listenForInviteChanges() } } diff --git a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt index c308346..22567c4 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/MainActivity.kt @@ -1,5 +1,7 @@ package app.dapk.st.home +import android.Manifest +import android.os.Build import android.os.Bundle import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.AlertDialog @@ -27,10 +29,11 @@ class MainActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - + val pushPermissionLauncher = registerPushPermission() homeViewModel.events.onEach { when (it) { HomeEvent.Relaunch -> recreate() + HomeEvent.OnShowContent -> pushPermissionLauncher?.invoke() } }.launchIn(lifecycleScope) @@ -45,6 +48,14 @@ class MainActivity : DapkActivity() { } } + private fun registerPushPermission(): (() -> Unit)? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + registerForPermission(Manifest.permission.POST_NOTIFICATIONS) + } else { + null + } + } + @Composable private fun BetaUpgradeDialog() { AlertDialog( diff --git a/features/notifications/src/main/AndroidManifest.xml b/features/notifications/src/main/AndroidManifest.xml index e76fe66..ebdd9bb 100644 --- a/features/notifications/src/main/AndroidManifest.xml +++ b/features/notifications/src/main/AndroidManifest.xml @@ -1,2 +1,4 @@ - \ No newline at end of file + + + \ No newline at end of file From 0168271d88a97ef8035f9cfd9290652dcb6cfa5e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 05:30:34 +0000 Subject: [PATCH 35/43] Bump kluent from 1.68 to 1.70 Bumps [kluent](https://github.com/MarkusAmshove/Kluent) from 1.68 to 1.70. - [Release notes](https://github.com/MarkusAmshove/Kluent/releases) - [Changelog](https://github.com/MarkusAmshove/Kluent/blob/master/CHANGELOG.md) - [Commits](https://github.com/MarkusAmshove/Kluent/compare/v1.68...v1.70) --- updated-dependencies: - dependency-name: org.amshove.kluent:kluent dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index c3d6a08..d9795bf 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -144,7 +144,7 @@ ext.Dependencies.with { accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.25.1" junit = "junit:junit:4.13.2" - kluent = "org.amshove.kluent:kluent:1.68" + kluent = "org.amshove.kluent:kluent:1.70" mockk = 'io.mockk:mockk:1.13.2' matrixOlm = "org.matrix.android:olm-sdk:3.2.12" From d324a938026a31447b621e14e9ae055f68561d51 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Oct 2022 18:35:20 +0000 Subject: [PATCH 36/43] Bump kotlinx-serialization-json from 1.4.0 to 1.4.1 Bumps [kotlinx-serialization-json](https://github.com/Kotlin/kotlinx.serialization) from 1.4.0 to 1.4.1. - [Release notes](https://github.com/Kotlin/kotlinx.serialization/releases) - [Changelog](https://github.com/Kotlin/kotlinx.serialization/blob/master/CHANGELOG.md) - [Commits](https://github.com/Kotlin/kotlinx.serialization/compare/v1.4.0...v1.4.1) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-serialization-json dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index d9795bf..ae0e186 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -121,7 +121,7 @@ ext.Dependencies.with { mavenCentral.with { kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}" kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}" - kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.0" + kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4" kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}" From b61815e6eecd45ffd87134bdc84f8a49ed7f25e1 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 17 Oct 2022 19:37:49 +0100 Subject: [PATCH 37/43] upgrade compose compiler with kotlin 1.7.20 support --- dependencies.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies.gradle b/dependencies.gradle index b6d405f..196bcf7 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -111,7 +111,7 @@ ext.Dependencies.with { androidxComposeMaterial = "androidx.compose.material3:material3:1.0.0-beta03" androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}" androidxActivityCompose = "androidx.activity:activity-compose:1.4.0" - kotlinCompilerExtensionVersion = "1.3.1" + kotlinCompilerExtensionVersion = "1.3.2" firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1" jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5" From f47a44063b64f6b275f0f9ef654cdd83f5006547 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 17 Oct 2022 20:47:04 +0100 Subject: [PATCH 38/43] fixing images causing decryption errors - introduces a dedicated encrypted room event type --- .../main/kotlin/app/dapk/st/engine/Models.kt | 13 ++++ .../app/dapk/st/messenger/MessengerScreen.kt | 69 +++++++++++++++++ .../dapk/st/messenger/MessengerViewModel.kt | 1 + .../RoomEventsToNotifiableMapper.kt | 9 +-- .../app/dapk/st/engine/LocalEchoMapper.kt | 1 + .../app/dapk/st/engine/MappingExtensions.kt | 1 + .../dapk/st/matrix/message/MessageService.kt | 11 ++- .../app/dapk/st/matrix/sync/RoomState.kt | 30 ++++++-- .../sync/internal/room/RoomEventsDecrypter.kt | 76 ++++++++++--------- .../sync/internal/sync/RoomDataSource.kt | 6 +- .../sync/internal/sync/RoomEventCreator.kt | 15 +++- .../sync/internal/sync/RoomProcessor.kt | 1 + .../internal/sync/UnreadEventsProcessor.kt | 1 + 13 files changed, 174 insertions(+), 60 deletions(-) diff --git a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt index dcc9b65..b823b03 100644 --- a/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt +++ b/chat-engine/src/main/kotlin/app/dapk/st/engine/Models.kt @@ -105,6 +105,19 @@ sealed class RoomEvent { abstract val author: RoomMember abstract val meta: MessageMeta + data class Encrypted( + override val eventId: EventId, + override val utcTimestamp: Long, + override val author: RoomMember, + override val meta: MessageMeta, + ) : RoomEvent() { + + val time: String by lazy(mode = LazyThreadSafetyMode.NONE) { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + } + data class Message( override val eventId: EventId, override val utcTimestamp: Long, diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index b6673e0..8662e70 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -198,6 +198,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions is RoomEvent.Image -> MessageImage(it as BubbleContent) is RoomEvent.Message -> TextBubbleContent(it as BubbleContent) is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent) + is RoomEvent.Encrypted -> EncryptedBubbleContent(it as BubbleContent) } } } @@ -445,6 +446,54 @@ private fun TextBubbleContent(content: BubbleContent) { } } +@Composable +private fun EncryptedBubbleContent(content: BubbleContent) { + Box(modifier = Modifier.padding(start = 6.dp)) { + Box( + Modifier + .padding(4.dp) + .clip(content.shape) + .background(content.background) + .height(IntrinsicSize.Max), + ) { + Column( + Modifier + .padding(8.dp) + .width(IntrinsicSize.Max) + .defaultMinSize(minWidth = 50.dp) + ) { + if (content.isNotSelf) { + Text( + fontSize = 11.sp, + text = content.message.author.displayName ?: content.message.author.id.value, + maxLines = 1, + color = content.textColor() + ) + } + Text( + text = "Encrypted message", + color = content.textColor(), + fontSize = 15.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + + Spacer(modifier = Modifier.height(2.dp)) + Row(horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Text( + fontSize = 9.sp, + text = "${content.message.time}", + textAlign = TextAlign.End, + color = content.textColor(), + modifier = Modifier.wrapContentSize() + ) + SendStatus(content.message) + } + } + } + } +} + @Composable private fun ReplyBubbleContent(content: BubbleContent) { Box(modifier = Modifier.padding(start = 6.dp)) { @@ -511,6 +560,16 @@ private fun ReplyBubbleContent(content: BubbleContent) { is RoomEvent.Reply -> { // TODO - a reply to a reply } + + is RoomEvent.Encrypted -> { + Text( + text = "Encrypted message", + color = content.textColor().copy(alpha = 0.8f), + fontSize = 14.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + } } } @@ -554,6 +613,16 @@ private fun ReplyBubbleContent(content: BubbleContent) { is RoomEvent.Reply -> { // TODO - a reply to a reply } + + is RoomEvent.Encrypted -> { + Text( + text = "Encrypted message", + color = content.textColor(), + fontSize = 15.sp, + modifier = Modifier.wrapContentSize(), + textAlign = TextAlign.Start, + ) + } } Spacer(modifier = Modifier.height(2.dp)) 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 cb27a18..6c87dc0 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 @@ -97,6 +97,7 @@ internal class MessengerViewModel( is RoomEvent.Image -> TODO() is RoomEvent.Reply -> TODO() is RoomEvent.Message -> it.content + is RoomEvent.Encrypted -> error("Should never happen") }, eventId = it.eventId, timestampUtc = it.utcTimestamp, diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt index 49ec278..ef649cd 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/RoomEventsToNotifiableMapper.kt @@ -6,19 +6,14 @@ import app.dapk.st.matrix.common.RoomMember class RoomEventsToNotifiableMapper { fun map(events: List): List { - return events.map { - when (it) { - is RoomEvent.Image -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author) - is RoomEvent.Message -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author) - is RoomEvent.Reply -> Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author) - } - } + return events.map { Notifiable(content = it.toNotifiableContent(), it.utcTimestamp, it.author) } } private fun RoomEvent.toNotifiableContent(): String = when (this) { is RoomEvent.Image -> "\uD83D\uDCF7" is RoomEvent.Message -> this.content is RoomEvent.Reply -> this.message.toNotifiableContent() + is RoomEvent.Encrypted -> "Encrypted message" } } diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt index 1880fcc..93e3740 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/LocalEchoMapper.kt @@ -47,6 +47,7 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) { is RoomEvent.Message -> this.copy(meta = metaMapper.toMeta(echo)) is RoomEvent.Reply -> this.copy(message = this.message.mergeWith(echo)) is RoomEvent.Image -> this.copy(meta = metaMapper.toMeta(echo)) + is RoomEvent.Encrypted -> this.copy(meta = metaMapper.toMeta(echo)) } } diff --git a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt index 9317c81..94a7587 100644 --- a/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt +++ b/matrix-chat-engine/src/main/kotlin/app/dapk/st/engine/MappingExtensions.kt @@ -90,6 +90,7 @@ fun MatrixRoomEvent.engine(): RoomEvent = when (this) { is MatrixRoomEvent.Image -> RoomEvent.Image(this.eventId, this.utcTimestamp, this.imageMeta.engine(), this.author, this.meta.engine(), this.edited) is MatrixRoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, this.content, this.author, this.meta.engine(), this.edited, this.redacted) is MatrixRoomEvent.Reply -> RoomEvent.Reply(this.message.engine(), this.replyingTo.engine()) + is MatrixRoomEvent.Encrypted -> RoomEvent.Encrypted(this.eventId, this.utcTimestamp, this.author, this.meta.engine()) } fun MatrixRoomEvent.Image.ImageMeta.engine() = RoomEvent.Image.ImageMeta( diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index 94ee5a9..3a39d8b 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -77,13 +77,12 @@ interface MessageService : MatrixService { @Serializable data class Meta( - val height: Int, - val width: Int, - val size: Long, - val fileName: String, - val mimeType: String, + @SerialName("height") val height: Int, + @SerialName("width") val width: Int, + @SerialName("size") val size: Long, + @SerialName("file_name") val fileName: String, + @SerialName("mime_type") val mimeType: String, ) - } } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt index 68dcd8c..2f59a5f 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt @@ -24,18 +24,19 @@ sealed class RoomEvent { abstract val utcTimestamp: Long abstract val author: RoomMember abstract val meta: MessageMeta + abstract val redacted: Boolean @Serializable - @SerialName("message") - data class Message( + @SerialName("encrypted") + data class Encrypted( @SerialName("event_id") override val eventId: EventId, @SerialName("timestamp") override val utcTimestamp: Long, @SerialName("content") val content: String, @SerialName("author") override val author: RoomMember, @SerialName("meta") override val meta: MessageMeta, - @SerialName("encrypted_content") val encryptedContent: MegOlmV1? = null, + @SerialName("encrypted_content") val encryptedContent: MegOlmV1, @SerialName("edited") val edited: Boolean = false, - @SerialName("redacted") val redacted: Boolean = false, + @SerialName("redacted") override val redacted: Boolean = false, ) : RoomEvent() { @Serializable @@ -52,6 +53,24 @@ sealed class RoomEvent { } } + @Serializable + @SerialName("message") + data class Message( + @SerialName("event_id") override val eventId: EventId, + @SerialName("timestamp") override val utcTimestamp: Long, + @SerialName("content") val content: String, + @SerialName("author") override val author: RoomMember, + @SerialName("meta") override val meta: MessageMeta, + @SerialName("edited") val edited: Boolean = false, + @SerialName("redacted") override val redacted: Boolean = false, + ) : RoomEvent() { + + val time: String by unsafeLazy { + val instant = Instant.ofEpochMilli(utcTimestamp) + ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) + } + } + @Serializable @SerialName("reply") data class Reply( @@ -63,6 +82,7 @@ sealed class RoomEvent { override val utcTimestamp: Long = message.utcTimestamp override val author: RoomMember = message.author override val meta: MessageMeta = message.meta + override val redacted: Boolean = message.redacted val replyingToSelf = replyingTo.author == message.author @@ -80,8 +100,8 @@ sealed class RoomEvent { @SerialName("image_meta") val imageMeta: ImageMeta, @SerialName("author") override val author: RoomMember, @SerialName("meta") override val meta: MessageMeta, - @SerialName("encrypted_content") val encryptedContent: Message.MegOlmV1? = null, @SerialName("edited") val edited: Boolean = false, + @SerialName("redacted") override val redacted: Boolean = false, ) : RoomEvent() { val time: String by unsafeLazy { diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt index b14f5b4..1194b74 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypter.kt @@ -3,6 +3,8 @@ package app.dapk.st.matrix.sync.internal.room import app.dapk.st.matrix.common.* import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent +import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent.TimelineMessage.Content.Image +import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent.TimelineMessage.Content.Text import app.dapk.st.matrix.sync.internal.request.DecryptedContent import kotlinx.serialization.json.Json @@ -17,56 +19,60 @@ internal class RoomEventsDecrypter( } private suspend fun decryptEvent(event: RoomEvent, userCredentials: UserCredentials): RoomEvent = when (event) { - is RoomEvent.Message -> event.decrypt() + is RoomEvent.Encrypted -> event.decrypt(userCredentials) + is RoomEvent.Message -> event is RoomEvent.Reply -> RoomEvent.Reply( message = decryptEvent(event.message, userCredentials), replyingTo = decryptEvent(event.replyingTo, userCredentials), ) - is RoomEvent.Image -> event.decrypt(userCredentials) + + is RoomEvent.Image -> event } - private suspend fun RoomEvent.Image.decrypt(userCredentials: UserCredentials) = when (this.encryptedContent) { - null -> this - else -> when (val result = messageDecrypter.decrypt(this.encryptedContent.toModel())) { - is DecryptionResult.Failed -> this.also { logger.crypto("Failed to decrypt ${it.eventId}") } - is DecryptionResult.Success -> when (val model = result.payload.toModel()) { - DecryptedContent.Ignored -> this - is DecryptedContent.TimelineText -> { - val content = model.content as ApiTimelineEvent.TimelineMessage.Content.Image - this.copy( - imageMeta = RoomEvent.Image.ImageMeta( - width = content.info?.width, - height = content.info?.height, - url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer), - keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) } - ), - encryptedContent = null, - ) - } + private suspend fun RoomEvent.Encrypted.decrypt(userCredentials: UserCredentials) = when (val result = this.decryptContent()) { + is DecryptionResult.Failed -> this.also { logger.crypto("Failed to decrypt ${it.eventId}") } + is DecryptionResult.Success -> when (val model = result.payload.toModel()) { + DecryptedContent.Ignored -> this + is DecryptedContent.TimelineText -> when (val content = model.content) { + ApiTimelineEvent.TimelineMessage.Content.Ignored -> this + is Image -> createImageEvent(content, userCredentials) + is Text -> createMessageEvent(content) } } } - private suspend fun RoomEvent.Message.decrypt() = when (this.encryptedContent) { - null -> this - else -> when (val result = messageDecrypter.decrypt(this.encryptedContent.toModel())) { - is DecryptionResult.Failed -> this.also { logger.crypto("Failed to decrypt ${it.eventId}") } - is DecryptionResult.Success -> when (val model = result.payload.toModel()) { - DecryptedContent.Ignored -> this - is DecryptedContent.TimelineText -> this.copyWithDecryptedContent(model) - } - } - } + private suspend fun RoomEvent.Encrypted.decryptContent() = messageDecrypter.decrypt(this.encryptedContent.toModel()) + + private fun RoomEvent.Encrypted.createMessageEvent(content: Text) = RoomEvent.Message( + eventId = this.eventId, + utcTimestamp = this.utcTimestamp, + author = this.author, + meta = this.meta, + edited = this.edited, + redacted = this.redacted, + content = content.body ?: "" + ) + + private fun RoomEvent.Encrypted.createImageEvent(content: Image, userCredentials: UserCredentials) = RoomEvent.Image( + eventId = this.eventId, + utcTimestamp = this.utcTimestamp, + author = this.author, + meta = this.meta, + edited = this.edited, + redacted = this.redacted, + imageMeta = RoomEvent.Image.ImageMeta( + width = content.info?.width, + height = content.info?.height, + url = content.file?.url?.convertMxUrToUrl(userCredentials.homeServer) ?: content.url!!.convertMxUrToUrl(userCredentials.homeServer), + keys = content.file?.let { RoomEvent.Image.ImageMeta.Keys(it.key.k, it.iv, it.v, it.hashes) } + ), + ) private fun JsonString.toModel() = json.decodeFromString(DecryptedContent.serializer(), this.value) - private fun RoomEvent.Message.copyWithDecryptedContent(decryptedContent: DecryptedContent.TimelineText) = this.copy( - content = (decryptedContent.content as ApiTimelineEvent.TimelineMessage.Content.Text).body ?: "", - encryptedContent = null - ) } -private fun RoomEvent.Message.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1( +private fun RoomEvent.Encrypted.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1( this.cipherText, this.deviceId, this.senderKey, diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt index 7ab8f17..f3d7e6f 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomDataSource.kt @@ -51,11 +51,7 @@ class RoomDataSource( } } -private fun RoomEvent.redact() = when (this) { - is RoomEvent.Image -> RoomEvent.Message(this.eventId, this.utcTimestamp, "Redacted", this.author, this.meta, redacted = true) - is RoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, "Redacted", this.author, this.meta, redacted = true) - is RoomEvent.Reply -> RoomEvent.Message(this.eventId, this.utcTimestamp, "Redacted", this.author, this.meta, redacted = true) -} +private fun RoomEvent.redact() = RoomEvent.Message(this.eventId, this.utcTimestamp, "Redacted", this.author, this.meta, redacted = true) private fun RoomState.replaceEvent(old: RoomEvent, new: RoomEvent): RoomState { val updatedEvents = this.events.toMutableList().apply { diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt index 7363989..ea39048 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt @@ -24,13 +24,13 @@ internal class RoomEventCreator( suspend fun ApiTimelineEvent.Encrypted.toRoomEvent(roomId: RoomId): RoomEvent? { return when (this.encryptedContent) { is ApiEncryptedContent.MegOlmV1 -> { - RoomEvent.Message( + RoomEvent.Encrypted( eventId = this.eventId, author = roomMembersService.find(roomId, this.senderId)!!, utcTimestamp = this.utcTimestamp, meta = MessageMeta.FromServer, content = "Encrypted message", - encryptedContent = RoomEvent.Message.MegOlmV1( + encryptedContent = RoomEvent.Encrypted.MegOlmV1( this.encryptedContent.cipherText, this.encryptedContent.deviceId, this.encryptedContent.senderKey, @@ -38,6 +38,7 @@ internal class RoomEventCreator( ) ) } + is ApiEncryptedContent.OlmV1 -> errorTracker.nullAndTrack(IllegalStateException("unexpected encryption, got OlmV1 for a room event")) ApiEncryptedContent.Unknown -> errorTracker.nullAndTrack(IllegalStateException("unknown room event encryption")) } @@ -79,6 +80,7 @@ internal class TimelineEventMapper( is RoomEvent.Message -> relationEvent is RoomEvent.Reply -> relationEvent.message is RoomEvent.Image -> relationEvent + is RoomEvent.Encrypted -> relationEvent } ) } @@ -110,12 +112,19 @@ internal class TimelineEventMapper( is RoomEvent.Image -> original.message is RoomEvent.Message -> original.message.edited(incomingEdit) is RoomEvent.Reply -> original.message + is RoomEvent.Encrypted -> original.message } ) + is RoomEvent.Image -> { // can't edit images null } + + is RoomEvent.Encrypted -> { + // can't edit encrypted messages + null + } } } } @@ -127,11 +136,13 @@ internal class TimelineEventMapper( utcTimestamp = incomingEdit.utcTimestamp, edited = true, ) + is ApiTimelineEvent.TimelineMessage.Content.Text -> original.toTextMessage( utcTimestamp = incomingEdit.utcTimestamp, content = incomingEdit.asTextContent().body?.removePrefix(" * ")?.trim() ?: "redacted", edited = true, ) + ApiTimelineEvent.TimelineMessage.Content.Ignored -> null } } diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt index d73f9f7..eca9c77 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomProcessor.kt @@ -81,4 +81,5 @@ private fun RoomEvent.toTextContent(): String = when (this) { is RoomEvent.Image -> "\uD83D\uDCF7" is RoomEvent.Message -> this.content is RoomEvent.Reply -> this.message.toTextContent() + is RoomEvent.Encrypted -> "Encrypted message" } \ No newline at end of file diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessor.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessor.kt index b4f82a7..7178154 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessor.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/UnreadEventsProcessor.kt @@ -43,6 +43,7 @@ internal class UnreadEventsProcessor( is RoomEvent.Message -> it.author.id == selfId is RoomEvent.Reply -> it.message.author.id == selfId is RoomEvent.Image -> it.author.id == selfId + is RoomEvent.Encrypted -> it.author.id == selfId } }.map { it.eventId } roomStore.insertUnread(overview.roomId, eventsFromOthers) From a3daea199d17c3fe3885227bb745e73e25a3d636 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 17 Oct 2022 20:49:04 +0100 Subject: [PATCH 39/43] removing unused time fields --- .../app/dapk/st/matrix/sync/RoomState.kt | 31 +------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt index 2f59a5f..0dd9bbb 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt @@ -1,22 +1,14 @@ package app.dapk.st.matrix.sync -import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.matrix.common.* import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import java.time.Instant -import java.time.ZoneId -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter data class RoomState( val roomOverview: RoomOverview, val events: List, ) -internal val DEFAULT_ZONE = ZoneId.systemDefault() -internal val MESSAGE_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm") - @Serializable sealed class RoomEvent { @@ -47,10 +39,6 @@ sealed class RoomEvent { @SerialName("session_id") val sessionId: SessionId, ) - val time: String by unsafeLazy { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } } @Serializable @@ -63,13 +51,7 @@ sealed class RoomEvent { @SerialName("meta") override val meta: MessageMeta, @SerialName("edited") val edited: Boolean = false, @SerialName("redacted") override val redacted: Boolean = false, - ) : RoomEvent() { - - val time: String by unsafeLazy { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } - } + ) : RoomEvent() @Serializable @SerialName("reply") @@ -84,12 +66,6 @@ sealed class RoomEvent { override val meta: MessageMeta = message.meta override val redacted: Boolean = message.redacted - val replyingToSelf = replyingTo.author == message.author - - val time: String by unsafeLazy { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } } @Serializable @@ -104,11 +80,6 @@ sealed class RoomEvent { @SerialName("redacted") override val redacted: Boolean = false, ) : RoomEvent() { - val time: String by unsafeLazy { - val instant = Instant.ofEpochMilli(utcTimestamp) - ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT) - } - @Serializable data class ImageMeta( @SerialName("width") val width: Int?, From c9fb1b102e5cde5369aba764d4f8610b2cd79dca Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 17 Oct 2022 21:15:21 +0100 Subject: [PATCH 40/43] updating tests --- .../app/dapk/st/matrix/sync/RoomState.kt | 3 +- .../sync/internal/sync/RoomEventCreator.kt | 1 - .../internal/room/RoomEventsDecrypterTest.kt | 36 ++++++++++++------- .../internal/sync/RoomEventCreatorTest.kt | 5 ++- .../kotlin/fixture/RoomEventFixture.kt | 14 ++++---- 5 files changed, 32 insertions(+), 27 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt index 0dd9bbb..837066d 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/RoomState.kt @@ -23,12 +23,11 @@ sealed class RoomEvent { data class Encrypted( @SerialName("event_id") override val eventId: EventId, @SerialName("timestamp") override val utcTimestamp: Long, - @SerialName("content") val content: String, @SerialName("author") override val author: RoomMember, @SerialName("meta") override val meta: MessageMeta, - @SerialName("encrypted_content") val encryptedContent: MegOlmV1, @SerialName("edited") val edited: Boolean = false, @SerialName("redacted") override val redacted: Boolean = false, + @SerialName("encrypted_content") val encryptedContent: MegOlmV1, ) : RoomEvent() { @Serializable diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt index ea39048..2bb35af 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreator.kt @@ -29,7 +29,6 @@ internal class RoomEventCreator( author = roomMembersService.find(roomId, this.senderId)!!, utcTimestamp = this.utcTimestamp, meta = MessageMeta.FromServer, - content = "Encrypted message", encryptedContent = RoomEvent.Encrypted.MegOlmV1( this.encryptedContent.cipherText, this.encryptedContent.deviceId, diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt index 23a5623..4283146 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/room/RoomEventsDecrypterTest.kt @@ -15,12 +15,12 @@ import org.junit.Test private const val A_DECRYPTED_MESSAGE_CONTENT = "decrypted - content" private val AN_ENCRYPTED_ROOM_CONTENT = aMegolmV1() -private val AN_ENCRYPTED_ROOM_MESSAGE = aMatrixRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT) +private val AN_ENCRYPTED_ROOM_MESSAGE = anEncryptedRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT) private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent( message = AN_ENCRYPTED_ROOM_MESSAGE, replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event")) ) -private val A_DECRYPTED_CONTENT = DecryptedContent.TimelineText(aTimelineTextEventContent(body = A_DECRYPTED_MESSAGE_CONTENT)) +private val A_DECRYPTED_TEXT_CONTENT = DecryptedContent.TimelineText(aTimelineTextEventContent(body = A_DECRYPTED_MESSAGE_CONTENT)) private val A_USER_CREDENTIALS = aUserCredentials() private val json = Json { encodeDefaults = true } @@ -37,7 +37,7 @@ class RoomEventsDecrypterTest { @Test fun `given clear message event, when decrypting, then does nothing`() = runTest { - val aClearMessageEvent = aMatrixRoomMessageEvent(encryptedContent = null) + val aClearMessageEvent = aMatrixRoomMessageEvent() val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent)) result shouldBeEqualTo listOf(aClearMessageEvent) @@ -45,42 +45,52 @@ class RoomEventsDecrypterTest { @Test fun `given encrypted message event, when decrypting, then applies decrypted body and removes encrypted content`() = runTest { - givenEncryptedMessage(AN_ENCRYPTED_ROOM_MESSAGE, decryptsTo = A_DECRYPTED_CONTENT) + givenEncryptedMessage(AN_ENCRYPTED_ROOM_MESSAGE, decryptsTo = A_DECRYPTED_TEXT_CONTENT) val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(AN_ENCRYPTED_ROOM_MESSAGE)) - result shouldBeEqualTo listOf(AN_ENCRYPTED_ROOM_MESSAGE.copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null)) + result shouldBeEqualTo listOf(AN_ENCRYPTED_ROOM_MESSAGE.toText(A_DECRYPTED_MESSAGE_CONTENT)) } @Test fun `given encrypted reply event, when decrypting, then decrypts message and replyTo`() = runTest { - givenEncryptedReply(AN_ENCRYPTED_ROOM_REPLY, decryptsTo = A_DECRYPTED_CONTENT) + givenEncryptedReply(AN_ENCRYPTED_ROOM_REPLY, decryptsTo = A_DECRYPTED_TEXT_CONTENT) val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(AN_ENCRYPTED_ROOM_REPLY)) result shouldBeEqualTo listOf( AN_ENCRYPTED_ROOM_REPLY.copy( - message = (AN_ENCRYPTED_ROOM_REPLY.message as RoomEvent.Message).copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null), - replyingTo = (AN_ENCRYPTED_ROOM_REPLY.replyingTo as RoomEvent.Message).copy(content = A_DECRYPTED_MESSAGE_CONTENT, encryptedContent = null), + message = (AN_ENCRYPTED_ROOM_REPLY.message as RoomEvent.Encrypted).toText(A_DECRYPTED_MESSAGE_CONTENT), + replyingTo = (AN_ENCRYPTED_ROOM_REPLY.replyingTo as RoomEvent.Encrypted).toText(A_DECRYPTED_MESSAGE_CONTENT), ) ) } - private fun givenEncryptedMessage(roomMessage: RoomEvent.Message, decryptsTo: DecryptedContent) { - val model = roomMessage.encryptedContent!!.toModel() + private fun givenEncryptedMessage(roomMessage: RoomEvent.Encrypted, decryptsTo: DecryptedContent) { + val model = roomMessage.encryptedContent.toModel() fakeMessageDecrypter.givenDecrypt(model) .returns(aDecryptionSuccessResult(payload = JsonString(json.encodeToString(DecryptedContent.serializer(), decryptsTo)))) } private fun givenEncryptedReply(roomReply: RoomEvent.Reply, decryptsTo: DecryptedContent) { - givenEncryptedMessage(roomReply.message as RoomEvent.Message, decryptsTo) - givenEncryptedMessage(roomReply.replyingTo as RoomEvent.Message, decryptsTo) + givenEncryptedMessage(roomReply.message as RoomEvent.Encrypted, decryptsTo) + givenEncryptedMessage(roomReply.replyingTo as RoomEvent.Encrypted, decryptsTo) } } -private fun RoomEvent.Message.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1( +private fun RoomEvent.Encrypted.MegOlmV1.toModel() = EncryptedMessageContent.MegOlmV1( this.cipherText, this.deviceId, this.senderKey, this.sessionId, ) + +private fun RoomEvent.Encrypted.toText(text: String) = RoomEvent.Message( + this.eventId, + this.utcTimestamp, + content = text, + this.author, + this.meta, + this.edited, + this.redacted, +) \ No newline at end of file diff --git a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt index b265549..38f1ed1 100644 --- a/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt +++ b/matrix/services/sync/src/test/kotlin/app/dapk/st/matrix/sync/internal/sync/RoomEventCreatorTest.kt @@ -40,10 +40,9 @@ internal class RoomEventCreatorTest { val result = with(roomEventCreator) { megolmEvent.toRoomEvent(A_ROOM_ID) } - result shouldBeEqualTo aMatrixRoomMessageEvent( + result shouldBeEqualTo anEncryptedRoomMessageEvent( eventId = megolmEvent.eventId, utcTimestamp = megolmEvent.utcTimestamp, - content = "Encrypted message", author = A_SENDER, encryptedContent = megolmEvent.encryptedContent.toMegolm(), ) @@ -320,7 +319,7 @@ private fun RoomEvent.Message.toReplyEvent(messageContent: String) = anApiTimeli ) ) -private fun ApiEncryptedContent.toMegolm(): RoomEvent.Message.MegOlmV1 { +private fun ApiEncryptedContent.toMegolm(): RoomEvent.Encrypted.MegOlmV1 { require(this is ApiEncryptedContent.MegOlmV1) return aMegolmV1(this.cipherText, this.deviceId, this.senderKey, this.sessionId) } diff --git a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt index 9b4d641..36bdf0b 100644 --- a/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt +++ b/matrix/services/sync/src/testFixtures/kotlin/fixture/RoomEventFixture.kt @@ -10,9 +10,8 @@ fun aMatrixRoomMessageEvent( content: String = "message-content", author: RoomMember = aRoomMember(), meta: MessageMeta = MessageMeta.FromServer, - encryptedContent: RoomEvent.Message.MegOlmV1? = null, edited: Boolean = false, -) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, encryptedContent, edited) +) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited) fun aRoomImageMessageEvent( eventId: EventId = anEventId(), @@ -20,9 +19,8 @@ fun aRoomImageMessageEvent( content: RoomEvent.Image.ImageMeta = anImageMeta(), author: RoomMember = aRoomMember(), meta: MessageMeta = MessageMeta.FromServer, - encryptedContent: RoomEvent.Message.MegOlmV1? = null, edited: Boolean = false, -) = RoomEvent.Image(eventId, utcTimestamp, content, author, meta, encryptedContent, edited) +) = RoomEvent.Image(eventId, utcTimestamp, content, author, meta, edited) fun aRoomReplyMessageEvent( message: RoomEvent = aMatrixRoomMessageEvent(), @@ -32,19 +30,19 @@ fun aRoomReplyMessageEvent( fun anEncryptedRoomMessageEvent( eventId: EventId = anEventId(), utcTimestamp: Long = 0L, - content: String = "encrypted-content", author: RoomMember = aRoomMember(), meta: MessageMeta = MessageMeta.FromServer, - encryptedContent: RoomEvent.Message.MegOlmV1? = aMegolmV1(), + encryptedContent: RoomEvent.Encrypted.MegOlmV1 = aMegolmV1(), edited: Boolean = false, -) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, encryptedContent, edited) + redacted: Boolean = false, +) = RoomEvent.Encrypted(eventId, utcTimestamp, author, meta, edited, redacted, encryptedContent) fun aMegolmV1( cipherText: CipherText = CipherText("a-cipher"), deviceId: DeviceId = aDeviceId(), senderKey: String = "a-sender-key", sessionId: SessionId = aSessionId(), -) = RoomEvent.Message.MegOlmV1(cipherText, deviceId, senderKey, sessionId) +) = RoomEvent.Encrypted.MegOlmV1(cipherText, deviceId, senderKey, sessionId) fun anImageMeta( width: Int? = 100, From b8b1612f010017e6132ba187fa317d27b106f976 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 17 Oct 2022 22:26:52 +0100 Subject: [PATCH 41/43] fixing login sometimes never transitioning to next state --- features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt index 6de8f9a..a18e8e8 100644 --- a/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt +++ b/features/home/src/main/kotlin/app/dapk/st/home/HomeViewModel.kt @@ -122,6 +122,6 @@ class HomeViewModel( } fun stop() { - viewModelScope.cancel() + // do nothing } } From 23ba01509cea17d1526ee23ae2eb7e53eafba9e6 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 17 Oct 2022 23:05:13 +0100 Subject: [PATCH 42/43] fixing key imports failing the first time --- .../main/kotlin/app/dapk/st/domain/OlmPersistence.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/OlmPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/OlmPersistence.kt index c01d535..df3a63c 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/OlmPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/OlmPersistence.kt @@ -14,6 +14,8 @@ import com.squareup.sqldelight.TransactionWithoutReturn import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine class OlmPersistence( private val database: DapkDb, @@ -83,10 +85,12 @@ class OlmPersistence( } suspend fun startTransaction(action: suspend TransactionWithoutReturn.() -> Unit) { - val scope = CoroutineScope(dispatchers.io) - database.cryptoQueries.transaction { - scope.launch { action() } + val transaction = suspendCoroutine { continuation -> + database.cryptoQueries.transaction { + continuation.resume(this) + } } + action(transaction) } suspend fun persist(sessionId: SessionId, inboundGroupSession: SerializedObject) { From 856e18c1afb9d4479fa794dd6b7c1c1f7058211b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 17 Oct 2022 23:12:57 +0100 Subject: [PATCH 43/43] updating version for release --- version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.json b/version.json index a52f12b..06414af 100644 --- a/version.json +++ b/version.json @@ -1,4 +1,4 @@ { - "code": 21, - "name": "06/10/2022-V1" + "code": 22, + "name": "17/10/2022-V1" } \ No newline at end of file