From 184955d70e3fcbdd69c235f68482db7e30ed78cb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 05:38:54 +0000 Subject: [PATCH 01/22] Bump kotlinx-coroutines-core from 1.6.2 to 1.6.3 Bumps [kotlinx-coroutines-core](https://github.com/Kotlin/kotlinx.coroutines) from 1.6.2 to 1.6.3. - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.6.2...1.6.3) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-core 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 e3a2ffb..8ebb9aa 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -110,7 +110,7 @@ ext.Dependencies.with { kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}" kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}" kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" - kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2" + kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3" kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}" From 8f6452915401c7c3b926005bec7854af36fc1d20 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jun 2022 05:24:24 +0000 Subject: [PATCH 02/22] Bump kotlinx-coroutines-test from 1.6.2 to 1.6.3 Bumps [kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines) from 1.6.2 to 1.6.3. - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.6.2...1.6.3) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test 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 252aaa0..434d190 100644 --- a/build.gradle +++ b/build.gradle @@ -133,7 +133,7 @@ ext.kotlinTest = { dependencies -> dependencies.testImplementation Dependencies.mavenCentral.kotlinTest dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10" dependencies.testImplementation 'io.mockk:mockk:1.12.4' - dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' + dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.3' dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' diff --git a/dependencies.gradle b/dependencies.gradle index 8ebb9aa..101c16d 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -111,7 +111,7 @@ ext.Dependencies.with { kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}" kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3" - kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' + kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.3' kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}" sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${sqldelightVer}" From 27e8e7593c8d9b809e760b00594dcc915c3c273e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Jun 2022 18:44:18 +0000 Subject: [PATCH 03/22] Bump ktorVer from 2.0.2 to 2.0.3 Bumps `ktorVer` from 2.0.2 to 2.0.3. Updates `ktor-client-android` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.0.2...2.0.3) Updates `ktor-client-core` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.0.2...2.0.3) Updates `ktor-client-serialization` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.0.2...2.0.3) Updates `ktor-serialization-kotlinx-json` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.0.2...2.0.3) Updates `ktor-client-logging-jvm` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.0.2...2.0.3) Updates `ktor-client-java` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.0.2...2.0.3) Updates `ktor-client-content-negotiation` from 2.0.2 to 2.0.3 - [Release notes](https://github.com/ktorio/ktor/releases) - [Changelog](https://github.com/ktorio/ktor/blob/main/CHANGELOG.md) - [Commits](https://github.com/ktorio/ktor/compare/2.0.2...2.0.3) --- updated-dependencies: - dependency-name: io.ktor:ktor-client-android dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-core dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-serialization dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-serialization-kotlinx-json dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-logging-jvm dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-java dependency-type: direct:production update-type: version-update:semver-patch - dependency-name: io.ktor:ktor-client-content-negotiation dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- dependencies.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 8ebb9aa..69ba916 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -91,7 +91,7 @@ ext.Dependencies.with { def kotlinVer = "1.6.10" def sqldelightVer = "1.5.3" def composeVer = "1.1.1" - def ktorVer = "2.0.2" + def ktorVer = "2.0.3" google = new DependenciesContainer() google.with { @@ -111,7 +111,7 @@ ext.Dependencies.with { kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}" kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3" - kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2' + kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.3' kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}" sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${sqldelightVer}" From eb8fddd4cd49fd5391448b78bdded5a51bf9b87d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 30 Jun 2022 05:42:15 +0000 Subject: [PATCH 04/22] Bump accompanist-systemuicontroller from 0.24.10-beta to 0.24.13-rc Bumps [accompanist-systemuicontroller](https://github.com/google/accompanist) from 0.24.10-beta to 0.24.13-rc. - [Release notes](https://github.com/google/accompanist/releases) - [Commits](https://github.com/google/accompanist/compare/v0.24.10-beta...v0.24.13-rc) --- updated-dependencies: - dependency-name: com.google.accompanist:accompanist-systemuicontroller 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 69ba916..274f862 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -129,7 +129,7 @@ ext.Dependencies.with { ktorContentNegotiation = "io.ktor:ktor-client-content-negotiation:${ktorVer}" coil = "io.coil-kt:coil-compose:2.1.0" - accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.24.10-beta" + accompanistSystemuicontroller = "com.google.accompanist:accompanist-systemuicontroller:0.24.13-rc" junit = "junit:junit:4.13.2" kluent = "org.amshove.kluent:kluent:1.68" From d8277c3f24567809c09a2fcd1c425c77b75e58e8 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Jul 2022 19:05:29 +0100 Subject: [PATCH 05/22] updating to kotlin 1.7.0 with separating compose compiler --- dependencies.gradle | 4 ++-- .../main/kotlin/app/dapk/st/core/ComposeExtensions.kt | 3 +++ .../src/testFixtures/kotlin/ViewModelTest.kt | 1 + .../kotlin/app/dapk/st/messenger/MessengerScreen.kt | 11 +++++++++-- .../kotlin/app/dapk/st/settings/SettingsScreen.kt | 7 +++++++ .../dapk/st/settings/eventlogger/EventLogScreen.kt | 7 +++++++ 6 files changed, 29 insertions(+), 4 deletions(-) diff --git a/dependencies.gradle b/dependencies.gradle index 69ba916..71ba55b 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -88,7 +88,7 @@ ext.Dependencies.with { } } - def kotlinVer = "1.6.10" + def kotlinVer = "1.7.0" def sqldelightVer = "1.5.3" def composeVer = "1.1.1" def ktorVer = "2.0.3" @@ -102,7 +102,7 @@ ext.Dependencies.with { androidxComposeMaterial = "androidx.compose.material:material:${composeVer}" androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}" androidxActivityCompose = "androidx.activity:activity-compose:1.4.0" - kotlinCompilerExtensionVersion = "${composeVer}" + kotlinCompilerExtensionVersion = "1.2.0" } mavenCentral = new DependenciesContainer() diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt index 4166d6b..a06b4b7 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/ComposeExtensions.kt @@ -41,6 +41,9 @@ fun LifecycleEffect(onStart: () -> Unit = {}, onStop: () -> Unit = {}) { when (event) { Lifecycle.Event.ON_START -> onStart() Lifecycle.Event.ON_STOP -> onStop() + else -> { + // ignored + } } } diff --git a/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTest.kt b/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTest.kt index 9cdadde..018c6cd 100644 --- a/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTest.kt +++ b/domains/android/viewmodel/src/testFixtures/kotlin/ViewModelTest.kt @@ -6,6 +6,7 @@ import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import test.ExpectTest +@Suppress("UNCHECKED_CAST") class ViewModelTest { var instance: TestMutableState? = null 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 f0f2a9a..6454056 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 @@ -84,8 +84,7 @@ internal fun MessengerScreen(roomId: RoomId, viewModel: MessengerViewModel, navi private fun MessengerViewModel.ObserveEvents() { StartObserving { this@ObserveEvents.events.launch { - when (it) { - } + // TODO() } } } @@ -429,6 +428,10 @@ private fun ReplyBubbleContent(content: BubbleContent) { ) Spacer(modifier = Modifier.height(4.dp)) } + + is RoomEvent.Reply -> { + // TODO - a reply to a reply + } } } @@ -466,6 +469,10 @@ private fun ReplyBubbleContent(content: BubbleContent) { ) Spacer(modifier = Modifier.height(4.dp)) } + + is RoomEvent.Reply -> { + // TODO - a reply to a reply + } } Spacer(modifier = Modifier.height(2.dp)) 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 3832fd3..8338af8 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 @@ -199,6 +199,13 @@ private fun RootSettings(page: Page.Root, onClick: (SettingItem) -> Unit) { item { Spacer(Modifier.height(12.dp)) } } } + + is Lce.Error -> { + // TODO + } + is Lce.Loading -> { + // TODO + } } } 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 885c985..43a24dd 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 @@ -42,6 +42,13 @@ fun EventLogScreen(viewModel: EventLoggerViewModel) { } } } + + is Lce.Error -> { + // TODO + } + is Lce.Loading -> { + // TODO + } } } From 880a4fd78a93207907ecf57bfed18790e9691088 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Jul 2022 19:13:55 +0100 Subject: [PATCH 06/22] updating crashlytics plugin --- build.gradle | 2 +- core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt | 2 ++ dependencies.gradle | 2 ++ .../src/main/kotlin/androidx/compose/runtime/MutableState.kt | 2 +- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 434d190..207b7e3 100644 --- a/build.gradle +++ b/build.gradle @@ -9,7 +9,7 @@ buildscript { classpath Dependencies.mavenCentral.kotlinGradlePlugin classpath Dependencies.mavenCentral.sqldelightGradlePlugin classpath Dependencies.mavenCentral.kotlinSerializationGradlePlugin - classpath 'com.google.firebase:firebase-crashlytics-gradle:2.8.1' + classpath Dependencies.google.firebaseCrashlyticsPlugin } } diff --git a/core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt b/core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt index 1d09273..64f66b8 100644 --- a/core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt +++ b/core/src/main/kotlin/app/dapk/st/core/SingletonFlows.kt @@ -31,10 +31,12 @@ class SingletonFlows( } } + @Suppress("UNCHECKED_CAST") fun get(key: String): Flow { return cache[key]!! as Flow } + @Suppress("UNCHECKED_CAST") suspend fun update(key: String, value: T) { (cache[key] as? MutableSharedFlow)?.emit(value) } diff --git a/dependencies.gradle b/dependencies.gradle index 71ba55b..5ae5c0f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -103,6 +103,8 @@ ext.Dependencies.with { androidxComposeIconsExtended = "androidx.compose.material:material-icons-extended:${composeVer}" androidxActivityCompose = "androidx.activity:activity-compose:1.4.0" kotlinCompilerExtensionVersion = "1.2.0" + + firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1" } mavenCentral = new DependenciesContainer() diff --git a/domains/android/viewmodel-stub/src/main/kotlin/androidx/compose/runtime/MutableState.kt b/domains/android/viewmodel-stub/src/main/kotlin/androidx/compose/runtime/MutableState.kt index 543df16..c1bfc7a 100644 --- a/domains/android/viewmodel-stub/src/main/kotlin/androidx/compose/runtime/MutableState.kt +++ b/domains/android/viewmodel-stub/src/main/kotlin/androidx/compose/runtime/MutableState.kt @@ -1,5 +1,5 @@ @file:JvmName("SnapshotStateKt") - +@file:Suppress("UNUSED") package androidx.compose.runtime import kotlin.reflect.KProperty From 6c3311284c80009a45c69654aeb069396c3571eb Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Jul 2022 19:37:53 +0100 Subject: [PATCH 07/22] lifting desugar'd dep to the deps file --- app/build.gradle | 2 +- dependencies.gradle | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/build.gradle b/app/build.gradle index 36dfe6f..3642d4d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -61,7 +61,7 @@ android { } dependencies { - coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' + coreLibraryDesugaring Dependencies.google.jdkLibs implementation project(":features:home") implementation project(":features:directory") diff --git a/dependencies.gradle b/dependencies.gradle index 5ae5c0f..c687110 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -105,6 +105,7 @@ ext.Dependencies.with { kotlinCompilerExtensionVersion = "1.2.0" firebaseCrashlyticsPlugin = "com.google.firebase:firebase-crashlytics-gradle:2.9.1" + jdkLibs = "com.android.tools:desugar_jdk_libs:1.1.5" } mavenCentral = new DependenciesContainer() From c7da6c1663714101a416959e7faf022e65687978 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Jul 2022 21:07:10 +0100 Subject: [PATCH 08/22] allowing h/w information to be null --- .../dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt index a81db3d..80756bf 100644 --- a/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt +++ b/matrix/services/sync/src/main/kotlin/app/dapk/st/matrix/sync/internal/request/ApiSyncResponse.kt @@ -484,8 +484,8 @@ internal sealed class ApiTimelineEvent { @Serializable internal data class Info( - @SerialName("h") val height: Int, - @SerialName("w") val width: Int, + @SerialName("h") val height: Int? = null, + @SerialName("w") val width: Int? = null, ) } From 671913fd696451fee581373eadf86025d04341a4 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Jul 2022 21:08:24 +0100 Subject: [PATCH 09/22] using the same group alert behaviour for the summary --- .../kotlin/app/dapk/st/notifications/NotificationFactory.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 b21d56a..84d54d4 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 @@ -70,6 +70,10 @@ class NotificationFactory( smallIcon = R.drawable.ic_notification_small_icon, contentIntent = openAppIntent, groupId = GROUP_ID, + groupAlertBehavior = deviceMeta.whenPOrHigher( + block = { Notification.GROUP_ALERT_SUMMARY }, + fallback = { null } + ), isGroupSummary = true, ) } From ca0889603a6b51c5260d337d472db39a3f7c1d96 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 11 Jul 2022 22:43:11 +0100 Subject: [PATCH 10/22] forcing synapse to last known working version --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fd29ec1..d84ef48 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -25,7 +25,7 @@ jobs: - name: Create pip requirements run: | - echo "matrix-synapse" > requirements.txt + echo "matrix-synapse==v1.60.0" > requirements.txt - name: Set up Python 3.8 uses: actions/setup-python@v2 From 6f18d4890503a5a278c01199c30de49986ff65c7 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 13 Jul 2022 18:24:17 +0100 Subject: [PATCH 11/22] adding missing messaging style config --- .../dapk/st/notifications/AndroidNotificationStyleBuilder.kt | 2 ++ .../kotlin/app/dapk/st/notifications/NotificationChannels.kt | 2 +- .../kotlin/app/dapk/st/notifications/NotificationFactory.kt | 1 - 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyleBuilder.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyleBuilder.kt index 5da92f3..ec91fba 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyleBuilder.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/AndroidNotificationStyleBuilder.kt @@ -34,6 +34,8 @@ class AndroidNotificationStyleBuilder( .setKey(person.key) .build() ).also { style -> + style.conversationTitle = title + style.isGroupConversation = isGroup content.forEach { val sender = personBuilderFactory() .setName(it.sender.name) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt index 676d968..6533b10 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt @@ -4,7 +4,7 @@ import android.app.NotificationChannel import android.app.NotificationManager import android.os.Build -private const val channelId = "message" +const val channelId = "message" class NotificationChannels( private val notificationManager: NotificationManager 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 84d54d4..7585d5b 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 @@ -10,7 +10,6 @@ import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.navigator.IntentFactory private const val GROUP_ID = "st" -private const val channelId = "message" class NotificationFactory( private val context: Context, From 63618276b7a8e0d31b5f5237ad62acfac20d6b7b Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 13 Jul 2022 18:32:10 +0100 Subject: [PATCH 12/22] initialisating channels as part of the first notification emission rather than blocking application init --- .../dapk/st/notifications/RenderNotificationsUseCase.kt | 8 +++----- .../st/notifications/RenderNotificationsUseCaseTest.kt | 7 ++++++- 2 files changed, 9 insertions(+), 6 deletions(-) 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 efb0d21..35a58c6 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 @@ -6,19 +6,17 @@ import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomOverview import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.onStart class RenderNotificationsUseCase( private val notificationRenderer: NotificationRenderer, private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase, - notificationChannels: NotificationChannels, + private val notificationChannels: NotificationChannels, ) { - init { - notificationChannels.initChannels() - } - suspend fun listenForNotificationChanges() { observeRenderableUnreadEventsUseCase() + .onStart { notificationChannels.initChannels() } .onEach { (each, diff) -> renderUnreadChange(each, diff) } .collect() } 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 be968b5..6cc7836 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 @@ -25,7 +25,12 @@ class RenderNotificationsUseCaseTest { ) @Test - fun `when creating use case instance, then initiates channels`() { + fun `given events, when listening for changes then initiates channels once`() = runTest { + fakeNotificationRenderer.instance.expect { it.render(any()) } + fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) + + renderNotificationsUseCase.listenForNotificationChanges() + fakeNotificationChannels.verifyInitiated() } From b0a438ee98d7b31d84141db025b0cf5a05ca0671 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 13 Jul 2022 23:03:58 +0100 Subject: [PATCH 13/22] diffing historical event timestamps to avoid acting on them as new events - fixes an issue where the database window would pull in legacy events after marking new events as read --- .../ObserveUnreadNotificationsUseCaseImpl.kt | 39 +++++++++++++++---- ...rveUnreadRenderNotificationsUseCaseTest.kt | 33 +++++++++++++--- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt index dd7f7dc..8a3860d 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/ObserveUnreadNotificationsUseCaseImpl.kt @@ -34,10 +34,12 @@ private fun Flow.onlyRenderableChanges(): Flow { log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read") false } + else -> true } } @@ -45,29 +47,48 @@ private fun Flow.onlyRenderableChanges(): Flow>>.mapWithDiff(): Flow>, NotificationDiff>> { - val previousUnreadEvents = mutableMapOf>() + val previousUnreadEvents = mutableMapOf>() return this.map { each -> - val allUnreadIds = each.toIds() + val allUnreadIds = each.toTimestampedIds() val notificationDiff = calculateDiff(allUnreadIds, previousUnreadEvents) previousUnreadEvents.clearAndPutAll(allUnreadIds) each to notificationDiff } } -private fun calculateDiff(allUnread: Map>, previousUnread: Map>?): NotificationDiff { +private fun calculateDiff(allUnread: Map>, previousUnread: Map>?): NotificationDiff { + val previousLatestEventTimestamps = previousUnread.toLatestTimestamps() val newRooms = allUnread.filter { !previousUnread.containsKey(it.key) }.keys - val unchanged = previousUnread?.filter { allUnread.containsKey(it.key) && it.value == allUnread[it.key] } ?: emptyMap() - val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) } + + val unchanged = previousUnread?.filter { + allUnread.containsKey(it.key) && (it.value == allUnread[it.key]) + } ?: emptyMap() + val changedOrNew = allUnread.filterNot { unchanged.containsKey(it.key) }.mapValues { (key, value) -> + val isChangedRoom = !newRooms.contains(key) + if (isChangedRoom) { + val latest = previousLatestEventTimestamps[key] ?: 0L + value.filter { + val isExistingEvent = (previousUnread?.get(key)?.contains(it) ?: false) + !isExistingEvent && it.second > latest + } + } else { + value + } + }.filter { it.value.isNotEmpty() } val removed = previousUnread?.filter { !allUnread.containsKey(it.key) } ?: emptyMap() - return NotificationDiff(unchanged, changedOrNew, removed, newRooms) + return NotificationDiff(unchanged.toEventIds(), changedOrNew.toEventIds(), removed.toEventIds(), newRooms) } -private fun List.toEventIds() = this.map { it.eventId } +private fun Map>?.toLatestTimestamps() = this?.mapValues { it.value.maxOf { it.second } } ?: emptyMap() -private fun Map>.toIds() = this +private fun Map>.toEventIds() = this.mapValues { it.value.map { it.first } } + +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 Flow.avoidShowingPreviousNotificationsOnLaunch() = drop(1) data class NotificationDiff( @@ -76,3 +97,5 @@ data class NotificationDiff( val removed: Map>, val newRooms: Set ) + +typealias TimestampedEventId = Pair \ No newline at end of file diff --git a/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt b/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt index fdce470..1a932fe 100644 --- a/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt +++ b/features/notifications/src/test/kotlin/app/dapk/st/notifications/ObserveUnreadRenderNotificationsUseCaseTest.kt @@ -3,8 +3,11 @@ package app.dapk.st.notifications import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomOverview import fake.FakeRoomStore -import fixture.* import fixture.NotificationDiffFixtures.aNotificationDiff +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 @@ -12,8 +15,8 @@ import org.amshove.kluent.shouldBeEqualTo import org.junit.Test private val NO_UNREADS = emptyMap>() -private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello") -private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world") +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")) @@ -48,7 +51,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest { 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, A_MESSAGE_2)) + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) ) } @@ -61,7 +64,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, A_MESSAGE_2)) + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) ) } @@ -76,6 +79,26 @@ class ObserveUnreadRenderNotificationsUseCaseTest { result shouldBeEqualTo emptyList() } + @Test + fun `given new and then historical message, when reading a message, then only emits the latest`() = runTest { + fakeRoomStore.givenUnreadEvents( + flowOf( + NO_UNREADS, + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE), + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE.copy(eventId = anEventId("old"), utcTimestamp = -1)) + ) + ) + + val result = useCase.invoke().toList() + + result shouldBeEqualTo listOf( + A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( + changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), + newRooms = setOf(A_ROOM_OVERVIEW.roomId) + ), + ) + } + @Test fun `given initial unreads, when reading a duplicate unread, then emits nothing`() = runTest { fakeRoomStore.givenUnreadEvents( From 13700444b9226869165ca0f2a431d50fe0a65ecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 14 Jul 2022 05:31:19 +0000 Subject: [PATCH 14/22] Bump kotlinx-coroutines-test from 1.6.3 to 1.6.4 Bumps [kotlinx-coroutines-test](https://github.com/Kotlin/kotlinx.coroutines) from 1.6.3 to 1.6.4. - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.6.3...1.6.4) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-test 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 207b7e3..2bdc19a 100644 --- a/build.gradle +++ b/build.gradle @@ -133,7 +133,7 @@ ext.kotlinTest = { dependencies -> dependencies.testImplementation Dependencies.mavenCentral.kotlinTest dependencies.testImplementation "org.jetbrains.kotlin:kotlin-test-junit:1.6.10" dependencies.testImplementation 'io.mockk:mockk:1.12.4' - dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.3' + dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' diff --git a/dependencies.gradle b/dependencies.gradle index c687110..91c3138 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -114,7 +114,7 @@ ext.Dependencies.with { kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}" kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3" - kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.3' + kotlinCoroutinesTest = 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' kotlinTest = "org.jetbrains.kotlin:kotlin-test-junit:${kotlinVer}" sqldelightGradlePlugin = "com.squareup.sqldelight:gradle-plugin:${sqldelightVer}" From be00219d064b5e74c45a699f437e1c8ffdf0f85e Mon Sep 17 00:00:00 2001 From: gradle-update-robot Date: Fri, 15 Jul 2022 00:24:46 +0000 Subject: [PATCH 15/22] Update Gradle Wrapper from 7.4.2 to 7.5. Signed-off-by: gradle-update-robot --- gradle/wrapper/gradle-wrapper.jar | Bin 59821 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 6 +++--- gradlew | 6 ++++++ gradlew.bat | 14 ++++++++------ 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 41d9927a4d4fb3f96a785543079b8df6723c946b..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 10197 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3a8202c..012d6d9 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sat Apr 02 22:45:08 BST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip distributionPath=wrapper/dists -zipStorePath=wrapper/dists +distributionSha256Sum=cb87f222c5585bd46838ad4db78463a5c5f3d336e5e2b98dc7c0c586527351c2 +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 1b6c787..a69d9cb 100755 --- a/gradlew +++ b/gradlew @@ -205,6 +205,12 @@ set -- \ org.gradle.wrapper.GradleWrapperMain \ "$@" +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + # Use "xargs" to parse quoted args. # # With -n1 it outputs one arg per line, with the quotes and backslashes removed. diff --git a/gradlew.bat b/gradlew.bat index ac1b06f..53a6b23 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 8e61a30af030a733c46ccac967d3f887b5d8329d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 16 Jul 2022 10:07:36 +0000 Subject: [PATCH 16/22] Bump kotlinx-coroutines-core from 1.6.3 to 1.6.4 Bumps [kotlinx-coroutines-core](https://github.com/Kotlin/kotlinx.coroutines) from 1.6.3 to 1.6.4. - [Release notes](https://github.com/Kotlin/kotlinx.coroutines/releases) - [Changelog](https://github.com/Kotlin/kotlinx.coroutines/blob/master/CHANGES.md) - [Commits](https://github.com/Kotlin/kotlinx.coroutines/compare/1.6.3...1.6.4) --- updated-dependencies: - dependency-name: org.jetbrains.kotlinx:kotlinx-coroutines-core 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 91c3138..43c256f 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -113,7 +113,7 @@ ext.Dependencies.with { kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVer}" kotlinSerializationGradlePlugin = "org.jetbrains.kotlin:kotlin-serialization:${kotlinVer}" kotlinSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.3" - kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.3" + 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 2a0f28d3b395031470cee92b0ec7163729b70d9e Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Tue, 26 Jul 2022 22:48:24 +0100 Subject: [PATCH 17/22] splitting dm and group notifications into separate channels - adding notification system group - only notifying the summary to avoid overlapping notifications --- .../st/notifications/NotificationChannels.kt | 32 ++++++++++++++++--- .../st/notifications/NotificationFactory.kt | 15 ++++++--- .../notifications/NotificationStateMapper.kt | 9 +++++- .../RenderNotificationsUseCase.kt | 3 -- .../notifications/NotificationFactoryTest.kt | 24 +++++++++----- .../kotlin/fixture/NotificationFixtures.kt | 3 +- 6 files changed, 63 insertions(+), 23 deletions(-) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt index 6533b10..0ca041f 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt @@ -1,10 +1,14 @@ package app.dapk.st.notifications import android.app.NotificationChannel +import android.app.NotificationChannelGroup import android.app.NotificationManager import android.os.Build -const val channelId = "message" +const val DIRECT_CHANNEL_ID = "direct_channel_id" +const val GROUP_CHANNEL_ID = "group_channel_id" + +private const val CHATS_NOTIFICATION_GROUP_ID = "chats_notification_group" class NotificationChannels( private val notificationManager: NotificationManager @@ -12,13 +16,31 @@ class NotificationChannels( fun initChannels() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (notificationManager.getNotificationChannel(channelId) == null) { + notificationManager.createNotificationChannelGroup(NotificationChannelGroup(CHATS_NOTIFICATION_GROUP_ID, "Chats")) + + if (notificationManager.getNotificationChannel(DIRECT_CHANNEL_ID) == null) { notificationManager.createNotificationChannel( NotificationChannel( - channelId, - "messages", + DIRECT_CHANNEL_ID, + "Direct notifications", NotificationManager.IMPORTANCE_HIGH, - ) + ).also { + it.enableVibration(true) + it.enableLights(true) + it.group = CHATS_NOTIFICATION_GROUP_ID + } + ) + } + + if (notificationManager.getNotificationChannel(GROUP_CHANNEL_ID) == null) { + notificationManager.createNotificationChannel( + NotificationChannel( + GROUP_CHANNEL_ID, + "Group notifications", + NotificationManager.IMPORTANCE_HIGH, + ).also { + it.group = CHATS_NOTIFICATION_GROUP_ID + } ) } } 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 7585d5b..87068cb 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 @@ -34,17 +34,21 @@ class NotificationFactory( else -> newRooms.contains(roomOverview.roomId) } + val last = sortedEvents.last() return NotificationTypes.Room( AndroidNotification( - channelId = channelId, - whenTimestamp = sortedEvents.last().utcTimestamp, + channelId = when { + roomOverview.isDm() -> DIRECT_CHANNEL_ID + else -> GROUP_CHANNEL_ID + }, + whenTimestamp = last.utcTimestamp, groupId = GROUP_ID, groupAlertBehavior = deviceMeta.whenPOrHigher( block = { Notification.GROUP_ALERT_SUMMARY }, fallback = { null } ), shortcutId = roomOverview.roomId.value, - alertMoreThanOnce = shouldAlertMoreThanOnce, + alertMoreThanOnce = false, contentIntent = openRoomIntent, messageStyle = messageStyle, category = Notification.CATEGORY_MESSAGE, @@ -53,7 +57,7 @@ class NotificationFactory( autoCancel = true ), roomId = roomOverview.roomId, - summary = sortedEvents.last().content, + summary = last.content, messageCount = sortedEvents.size, isAlerting = shouldAlertMoreThanOnce ) @@ -63,7 +67,7 @@ class NotificationFactory( val summaryInboxStyle = notificationStyleFactory.summary(notifications) val openAppIntent = intentFactory.notificationOpenApp(context) return AndroidNotification( - channelId = channelId, + channelId = notifications.firstOrNull { it.isAlerting }?.notification?.channelId ?: notifications.first().notification.channelId, messageStyle = summaryInboxStyle, alertMoreThanOnce = notifications.any { it.isAlerting }, smallIcon = R.drawable.ic_notification_small_icon, @@ -74,6 +78,7 @@ class NotificationFactory( fallback = { null } ), isGroupSummary = true, + category = Notification.CATEGORY_MESSAGE, ) } } diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStateMapper.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStateMapper.kt index c9d4c17..4fe1496 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStateMapper.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationStateMapper.kt @@ -16,7 +16,14 @@ class NotificationStateMapper( val messageEvents = roomEventsToNotifiableMapper.map(events) when (messageEvents.isEmpty()) { true -> NotificationTypes.DismissRoom(roomOverview.roomId) - false -> notificationFactory.createMessageNotification(messageEvents, roomOverview, state.roomsWithNewEvents, state.newRooms) + false -> { + notificationFactory.createMessageNotification( + events = messageEvents, + roomOverview = roomOverview, + roomsWithNewEvents = state.roomsWithNewEvents, + newRooms = state.newRooms + ) + } } } 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 35a58c6..59128eb 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,5 @@ package app.dapk.st.notifications -import app.dapk.st.core.AppLogTag.NOTIFICATION -import app.dapk.st.core.log import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomOverview import kotlinx.coroutines.flow.collect @@ -22,7 +20,6 @@ class RenderNotificationsUseCase( } private suspend fun renderUnreadChange(allUnread: Map>, diff: NotificationDiff) { - log(NOTIFICATION, "unread changed - render notifications") notificationRenderer.render( NotificationState( allUnread = allUnread, 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 350f60b..af98498 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 @@ -7,6 +7,7 @@ import app.dapk.st.core.DeviceMeta 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 import fixture.NotificationFixtures.aRoomNotification import fixture.aRoomId @@ -16,6 +17,7 @@ import kotlinx.coroutines.test.runTest import org.amshove.kluent.shouldBeEqualTo import org.junit.Test +private const val A_CHANNEL_ID = "a channel id" private val AN_OPEN_APP_INTENT = aPendingIntent() private val AN_OPEN_ROOM_INTENT = aPendingIntent() private val A_NOTIFICATION_STYLE = anInboxStyle() @@ -48,24 +50,24 @@ class NotificationFactoryTest { @Test fun `given alerting room notification, when creating summary, then is alerting`() { - val notifications = listOf(aRoomNotification(isAlerting = true)) + val notifications = listOf(aRoomNotification(notification = anAndroidNotification(channelId = A_CHANNEL_ID), isAlerting = true)) fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) fakeNotificationStyleFactory.givenSummary(notifications).returns(anInboxStyle()) val result = notificationFactory.createSummary(notifications) - result shouldBeEqualTo expectedSummary(shouldAlertMoreThanOnce = true) + result shouldBeEqualTo expectedSummary(channelId = A_CHANNEL_ID, shouldAlertMoreThanOnce = true) } @Test fun `given non alerting room notification, when creating summary, then is alerting`() { - val notifications = listOf(aRoomNotification(isAlerting = false)) + val notifications = listOf(aRoomNotification(notification = anAndroidNotification(channelId = A_CHANNEL_ID), isAlerting = false)) fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) fakeNotificationStyleFactory.givenSummary(notifications).returns(anInboxStyle()) val result = notificationFactory.createSummary(notifications) - result shouldBeEqualTo expectedSummary(shouldAlertMoreThanOnce = false) + result shouldBeEqualTo expectedSummary(channelId = A_CHANNEL_ID, shouldAlertMoreThanOnce = false) } @Test @@ -75,6 +77,7 @@ class NotificationFactoryTest { val result = notificationFactory.createMessageNotification(EVENTS, A_GROUP_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = setOf(A_ROOM_ID)) result shouldBeEqualTo expectedMessage( + channel = GROUP_CHANNEL_ID, shouldAlertMoreThanOnce = true, ) } @@ -86,6 +89,7 @@ class NotificationFactoryTest { val result = notificationFactory.createMessageNotification(EVENTS, A_GROUP_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = emptySet()) result shouldBeEqualTo expectedMessage( + channel = GROUP_CHANNEL_ID, shouldAlertMoreThanOnce = false, ) } @@ -97,6 +101,7 @@ class NotificationFactoryTest { val result = notificationFactory.createMessageNotification(EVENTS, A_DM_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = setOf(A_ROOM_ID)) result shouldBeEqualTo expectedMessage( + channel = DIRECT_CHANNEL_ID, shouldAlertMoreThanOnce = true, ) } @@ -108,6 +113,7 @@ class NotificationFactoryTest { val result = notificationFactory.createMessageNotification(EVENTS, A_DM_ROOM_OVERVIEW, setOf(A_ROOM_ID), newRooms = emptySet()) result shouldBeEqualTo expectedMessage( + channel = DIRECT_CHANNEL_ID, shouldAlertMoreThanOnce = true, ) } @@ -119,15 +125,16 @@ class NotificationFactoryTest { } private fun expectedMessage( + channel: String, shouldAlertMoreThanOnce: Boolean, ) = NotificationTypes.Room( AndroidNotification( - channelId = "message", + channelId = channel, whenTimestamp = LATEST_EVENT.utcTimestamp, groupId = "st", groupAlertBehavior = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.GROUP_ALERT_SUMMARY else null, shortcutId = A_ROOM_ID.value, - alertMoreThanOnce = shouldAlertMoreThanOnce, + alertMoreThanOnce = false, contentIntent = AN_OPEN_ROOM_INTENT, messageStyle = A_NOTIFICATION_STYLE, category = Notification.CATEGORY_MESSAGE, @@ -141,13 +148,14 @@ class NotificationFactoryTest { isAlerting = shouldAlertMoreThanOnce, ) - private fun expectedSummary(shouldAlertMoreThanOnce: Boolean) = AndroidNotification( - channelId = "message", + private fun expectedSummary(channelId: String, shouldAlertMoreThanOnce: Boolean) = AndroidNotification( + channelId = channelId, messageStyle = A_NOTIFICATION_STYLE, alertMoreThanOnce = shouldAlertMoreThanOnce, smallIcon = R.drawable.ic_notification_small_icon, contentIntent = AN_OPEN_APP_INTENT, groupId = "st", + category = Notification.CATEGORY_MESSAGE, isGroupSummary = true, autoCancel = true ) diff --git a/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt b/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt index d476f2f..caa8a7a 100644 --- a/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt +++ b/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt @@ -14,11 +14,12 @@ object NotificationFixtures { ) = Notifications(summaryNotification, delegates) fun aRoomNotification( + notification: AndroidNotification = anAndroidNotification(), summary: String = "a summary line", messageCount: Int = 1, isAlerting: Boolean = false, ) = NotificationTypes.Room( - anAndroidNotification(), + notification, aRoomId(), summary = summary, messageCount = messageCount, From 3ac9df765df98138243e51af1e6feb1baa41d8f3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 27 Jul 2022 05:34:28 +0000 Subject: [PATCH 18/22] Bump junit-jupiter-engine from 5.8.2 to 5.9.0 Bumps [junit-jupiter-engine](https://github.com/junit-team/junit5) from 5.8.2 to 5.9.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.8.2...r5.9.0) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-engine dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 2bdc19a..d3b79c0 100644 --- a/build.gradle +++ b/build.gradle @@ -136,7 +136,7 @@ ext.kotlinTest = { dependencies -> dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' - dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } ext.kotlinFixtures = { dependencies -> From 8c1265c957c57aa6d6dd8a536e15d1212a85f3ae Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 28 Jul 2022 19:43:27 +0100 Subject: [PATCH 19/22] avoiding notification overlap by only using alerting summary group instead of all notifications --- .../st/notifications/NotificationChannels.kt | 13 +++++++++++++ .../st/notifications/NotificationFactory.kt | 15 +++++++++------ .../st/notifications/NotificationRenderer.kt | 3 ++- .../notifications/NotificationFactoryTest.kt | 18 ++++++++++++++---- .../kotlin/fixture/NotificationFixtures.kt | 4 +++- 5 files changed, 41 insertions(+), 12 deletions(-) diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt index 0ca041f..12dbd8d 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationChannels.kt @@ -7,6 +7,7 @@ import android.os.Build const val DIRECT_CHANNEL_ID = "direct_channel_id" const val GROUP_CHANNEL_ID = "group_channel_id" +const val SUMMARY_CHANNEL_ID = "summary_channel_id" private const val CHATS_NOTIFICATION_GROUP_ID = "chats_notification_group" @@ -43,6 +44,18 @@ class NotificationChannels( } ) } + + if (notificationManager.getNotificationChannel(SUMMARY_CHANNEL_ID) == null) { + notificationManager.createNotificationChannel( + NotificationChannel( + SUMMARY_CHANNEL_ID, + "Other notifications", + NotificationManager.IMPORTANCE_DEFAULT, + ).also { + it.group = CHATS_NOTIFICATION_GROUP_ID + } + ) + } } } 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 87068cb..50e8193 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 @@ -37,10 +37,7 @@ class NotificationFactory( val last = sortedEvents.last() return NotificationTypes.Room( AndroidNotification( - channelId = when { - roomOverview.isDm() -> DIRECT_CHANNEL_ID - else -> GROUP_CHANNEL_ID - }, + channelId = SUMMARY_CHANNEL_ID, whenTimestamp = last.utcTimestamp, groupId = GROUP_ID, groupAlertBehavior = deviceMeta.whenPOrHigher( @@ -59,7 +56,11 @@ class NotificationFactory( roomId = roomOverview.roomId, summary = last.content, messageCount = sortedEvents.size, - isAlerting = shouldAlertMoreThanOnce + isAlerting = shouldAlertMoreThanOnce, + summaryChannelId = when { + roomOverview.isDm() -> DIRECT_CHANNEL_ID + else -> GROUP_CHANNEL_ID + } ) } @@ -67,7 +68,7 @@ class NotificationFactory( val summaryInboxStyle = notificationStyleFactory.summary(notifications) val openAppIntent = intentFactory.notificationOpenApp(context) return AndroidNotification( - channelId = notifications.firstOrNull { it.isAlerting }?.notification?.channelId ?: notifications.first().notification.channelId, + channelId = notifications.mostRecent().summaryChannelId, messageStyle = summaryInboxStyle, alertMoreThanOnce = notifications.any { it.isAlerting }, smallIcon = R.drawable.ic_notification_small_icon, @@ -83,4 +84,6 @@ class NotificationFactory( } } +private fun List.mostRecent() = this.sortedBy { it.notification.whenTimestamp }.first() + private fun RoomOverview.isDm() = !this.isGroup diff --git a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt index 5d4e177..5a2cc7c 100644 --- a/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt +++ b/features/notifications/src/main/kotlin/app/dapk/st/notifications/NotificationRenderer.kt @@ -68,7 +68,8 @@ sealed interface NotificationTypes { val roomId: RoomId, val summary: String, val messageCount: Int, - val isAlerting: Boolean + val isAlerting: Boolean, + val summaryChannelId: String, ) : NotificationTypes data class DismissRoom(val roomId: RoomId) : NotificationTypes 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 af98498..0c466b6 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 @@ -32,7 +32,6 @@ private val EVENTS = listOf( aNotifiable("message two", utcTimestamp = 2), ) - class NotificationFactoryTest { private val fakeContext = FakeContext() @@ -50,7 +49,12 @@ class NotificationFactoryTest { @Test fun `given alerting room notification, when creating summary, then is alerting`() { - val notifications = listOf(aRoomNotification(notification = anAndroidNotification(channelId = A_CHANNEL_ID), isAlerting = true)) + val notifications = listOf( + aRoomNotification( + summaryChannelId = A_CHANNEL_ID, + notification = anAndroidNotification(channelId = A_CHANNEL_ID), isAlerting = true + ) + ) fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) fakeNotificationStyleFactory.givenSummary(notifications).returns(anInboxStyle()) @@ -61,7 +65,12 @@ class NotificationFactoryTest { @Test fun `given non alerting room notification, when creating summary, then is alerting`() { - val notifications = listOf(aRoomNotification(notification = anAndroidNotification(channelId = A_CHANNEL_ID), isAlerting = false)) + val notifications = listOf( + aRoomNotification( + summaryChannelId = A_CHANNEL_ID, + notification = anAndroidNotification(channelId = A_CHANNEL_ID), isAlerting = false + ) + ) fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) fakeNotificationStyleFactory.givenSummary(notifications).returns(anInboxStyle()) @@ -129,7 +138,7 @@ class NotificationFactoryTest { shouldAlertMoreThanOnce: Boolean, ) = NotificationTypes.Room( AndroidNotification( - channelId = channel, + channelId = SUMMARY_CHANNEL_ID, whenTimestamp = LATEST_EVENT.utcTimestamp, groupId = "st", groupAlertBehavior = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) Notification.GROUP_ALERT_SUMMARY else null, @@ -146,6 +155,7 @@ class NotificationFactoryTest { summary = LATEST_EVENT.content, messageCount = EVENTS.size, isAlerting = shouldAlertMoreThanOnce, + summaryChannelId = channel, ) private fun expectedSummary(channelId: String, shouldAlertMoreThanOnce: Boolean) = AndroidNotification( diff --git a/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt b/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt index caa8a7a..46d824a 100644 --- a/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt +++ b/features/notifications/src/test/kotlin/fixture/NotificationFixtures.kt @@ -18,12 +18,14 @@ object NotificationFixtures { summary: String = "a summary line", messageCount: Int = 1, isAlerting: Boolean = false, + summaryChannelId: String = "a-summary-channel-id", ) = NotificationTypes.Room( notification, aRoomId(), summary = summary, messageCount = messageCount, - isAlerting = isAlerting + isAlerting = isAlerting, + summaryChannelId = summaryChannelId ) fun aDismissRoomNotification( From 36b740e0d3d02024e34c484d3c00afadabbe3d8f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Jul 2022 18:51:08 +0000 Subject: [PATCH 20/22] Bump mockk from 1.12.4 to 1.12.5 Bumps [mockk](https://github.com/mockk/mockk) from 1.12.4 to 1.12.5. - [Release notes](https://github.com/mockk/mockk/releases) - [Commits](https://github.com/mockk/mockk/compare/1.12.4...1.12.5) --- updated-dependencies: - dependency-name: io.mockk:mockk dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- build.gradle | 4 ++-- dependencies.gradle | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 2bdc19a..765c037 100644 --- a/build.gradle +++ b/build.gradle @@ -132,11 +132,11 @@ 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 'io.mockk:mockk:1.12.4' + dependencies.testImplementation 'io.mockk:mockk:1.12.5' dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' - dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } ext.kotlinFixtures = { dependencies -> diff --git a/dependencies.gradle b/dependencies.gradle index 43c256f..bd26975 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -136,7 +136,7 @@ ext.Dependencies.with { junit = "junit:junit:4.13.2" kluent = "org.amshove.kluent:kluent:1.68" - mockk = 'io.mockk:mockk:1.12.4' + mockk = 'io.mockk:mockk:1.12.5' matrixOlm = "org.matrix.android:olm-sdk:3.2.12" } From 0387c0917e403a5dc2619aa44259ec2b240f567f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 28 Jul 2022 18:51:17 +0000 Subject: [PATCH 21/22] Bump junit-jupiter-api from 5.8.2 to 5.9.0 Bumps [junit-jupiter-api](https://github.com/junit-team/junit5) from 5.8.2 to 5.9.0. - [Release notes](https://github.com/junit-team/junit5/releases) - [Commits](https://github.com/junit-team/junit5/compare/r5.8.2...r5.9.0) --- updated-dependencies: - dependency-name: org.junit.jupiter:junit-jupiter-api dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d3b79c0..6ed8dd8 100644 --- a/build.gradle +++ b/build.gradle @@ -135,7 +135,7 @@ ext.kotlinTest = { dependencies -> dependencies.testImplementation 'io.mockk:mockk:1.12.4' dependencies.testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' - dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + dependencies.testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' dependencies.testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' } From 3bbfd05fcec9072761420e631f705fc629827450 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 28 Jul 2022 22:06:04 +0100 Subject: [PATCH 22/22] applying when timestamp to the summary notification --- .../app/dapk/st/notifications/NotificationFactory.kt | 4 +++- .../app/dapk/st/notifications/NotificationFactoryTest.kt | 9 +++++---- 2 files changed, 8 insertions(+), 5 deletions(-) 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 50e8193..13d26a1 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 @@ -67,9 +67,11 @@ class NotificationFactory( fun createSummary(notifications: List): AndroidNotification { val summaryInboxStyle = notificationStyleFactory.summary(notifications) val openAppIntent = intentFactory.notificationOpenApp(context) + val mostRecent = notifications.mostRecent() return AndroidNotification( - channelId = notifications.mostRecent().summaryChannelId, + channelId = mostRecent.summaryChannelId, messageStyle = summaryInboxStyle, + whenTimestamp = mostRecent.notification.whenTimestamp, alertMoreThanOnce = notifications.any { it.isAlerting }, smallIcon = R.drawable.ic_notification_small_icon, contentIntent = openAppIntent, 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 0c466b6..60d5e0e 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 @@ -60,7 +60,7 @@ class NotificationFactoryTest { val result = notificationFactory.createSummary(notifications) - result shouldBeEqualTo expectedSummary(channelId = A_CHANNEL_ID, shouldAlertMoreThanOnce = true) + result shouldBeEqualTo expectedSummary(notifications.first().notification, shouldAlertMoreThanOnce = true) } @Test @@ -76,7 +76,7 @@ class NotificationFactoryTest { val result = notificationFactory.createSummary(notifications) - result shouldBeEqualTo expectedSummary(channelId = A_CHANNEL_ID, shouldAlertMoreThanOnce = false) + result shouldBeEqualTo expectedSummary(notifications.first().notification, shouldAlertMoreThanOnce = false) } @Test @@ -158,8 +158,9 @@ class NotificationFactoryTest { summaryChannelId = channel, ) - private fun expectedSummary(channelId: String, shouldAlertMoreThanOnce: Boolean) = AndroidNotification( - channelId = channelId, + private fun expectedSummary(notification: AndroidNotification, shouldAlertMoreThanOnce: Boolean) = AndroidNotification( + channelId = notification.channelId, + whenTimestamp = notification.whenTimestamp, messageStyle = A_NOTIFICATION_STYLE, alertMoreThanOnce = shouldAlertMoreThanOnce, smallIcon = R.drawable.ic_notification_small_icon,