From 8787137107984d0b1f79ce231774c89dea3478aa Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Mon, 3 Apr 2023 13:45:46 +0100 Subject: [PATCH 01/83] Fix using local rust aar The rustsdk library wasn't included in the gradle build because it still used a groovy build file (and settings.gradle includes projects based on `build.gradle.kts` existing). Also reword the onboarding docs slightly to clarify the deps that need changing --- docs/_developer_onboarding.md | 8 ++++---- libraries/rustsdk/build.gradle | 2 -- libraries/rustsdk/build.gradle.kts | 2 ++ 3 files changed, 6 insertions(+), 6 deletions(-) delete mode 100644 libraries/rustsdk/build.gradle create mode 100644 libraries/rustsdk/build.gradle.kts diff --git a/docs/_developer_onboarding.md b/docs/_developer_onboarding.md index 9b07f37daf..37bf8a6548 100644 --- a/docs/_developer_onboarding.md +++ b/docs/_developer_onboarding.md @@ -136,7 +136,7 @@ git clone git@github.com:matrix-org/matrix-rust-sdk.git git clone git@github.com:matrix-org/matrix-rust-components-kotlin.git ``` -Then you can launch the build script with the following params: +Then you can launch the build script from the matrix-rust-components-kotlin repository with the following params: - `-p` Local path to the rust-sdk repository - `-o` Optional output path with the expected name of the aar file. By default the aar will be located in the corresponding build/outputs/aar directory. @@ -150,12 +150,12 @@ So for example to build the sdk against aarch64-linux-android target and copy th ./scripts/build.sh -p [YOUR MATRIX RUST SDK PATH] -t aarch64-linux-android -o [YOUR element-x-android PATH]/libraries/rustsdk/matrix-rust-sdk.aar ``` -Finally let the `matrix/impl` module use this aar by switching those lines in the gradle file : +Finally let the `matrix/impl` module use this aar by changing the dependencies from `libs.matrix.sdk` to `projects.libraries.rustsdk`: ```groovy dependencies { - api(projects.libraries.rustsdk) // <- comment this line - // api(libs.matrix.sdk) // <- uncomment this line + api(projects.libraries.rustsdk) // <- use the local version of the sdk. Uncomment this line. + //implementation(libs.matrix.sdk) // <- use the released version. Comment this line. } ``` diff --git a/libraries/rustsdk/build.gradle b/libraries/rustsdk/build.gradle deleted file mode 100644 index bfafe67f28..0000000000 --- a/libraries/rustsdk/build.gradle +++ /dev/null @@ -1,2 +0,0 @@ -configurations.maybeCreate("default") -artifacts.add("default", file('matrix-rust-sdk.aar')) \ No newline at end of file diff --git a/libraries/rustsdk/build.gradle.kts b/libraries/rustsdk/build.gradle.kts new file mode 100644 index 0000000000..842825ee86 --- /dev/null +++ b/libraries/rustsdk/build.gradle.kts @@ -0,0 +1,2 @@ +configurations.maybeCreate("default") +artifacts.add("default", file("./matrix-rust-sdk.aar")) From bd3b73e32320128497f612c13424bfc8382ff5f6 Mon Sep 17 00:00:00 2001 From: Chris Smith Date: Tue, 4 Apr 2023 11:34:24 +0100 Subject: [PATCH 02/83] Remove explicit include for rustsdk --- settings.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 944b17ab52..e44a2cb8fa 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -41,7 +41,6 @@ include(":appnav") include(":tests:uitests") include(":anvilannotations") include(":anvilcodegen") -include(":libraries:rustsdk") include(":samples:minimal") From 2aa027de6c8d7c6eee436f54f6c462425141bf7a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 6 Apr 2023 12:06:27 +0000 Subject: [PATCH 03/83] Update dependency androidx.compose:compose-bom to v2023.04.00 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 667f4791a5..d7ff168500 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -20,7 +20,7 @@ activity = "1.7.0" startup = "1.1.1" # Compose -compose_bom = "2023.03.00" +compose_bom = "2023.04.00" composecompiler = "1.4.2" # Coroutines From 0550a32821aa056d68db77939a86aa302e5b1f0b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 6 Apr 2023 16:58:29 +0200 Subject: [PATCH 04/83] Add test for Push parsers. --- .../android/libraries/matrix/test/TestData.kt | 6 +- .../impl/unifiedpush/PushDataUnifiedPush.kt | 10 +- .../impl/unifiedpush/UnifiedPushParser.kt | 4 +- .../impl/firebase/FirebasePushParserTest.kt | 100 ++++++++++++++++++ .../impl/unifiedpush/UnifiedPushParserTest.kt | 95 +++++++++++++++++ 5 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt create mode 100644 libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt index ec749025dc..6b4d83e43b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/TestData.kt @@ -31,9 +31,9 @@ val A_USER_ID = UserId("@alice:server.org") val A_USER_ID_2 = UserId("@bob:server.org") val A_SESSION_ID = SessionId(A_USER_ID.value) val A_SESSION_ID_2 = SessionId(A_USER_ID_2.value) -val A_SPACE_ID = SpaceId("!aSpaceId") -val A_ROOM_ID = RoomId("!aRoomId") -val A_ROOM_ID_2 = RoomId("!aRoomId2") +val A_SPACE_ID = SpaceId("!aSpaceId:domain") +val A_ROOM_ID = RoomId("!aRoomId:domain") +val A_ROOM_ID_2 = RoomId("!aRoomId2:domain") val A_THREAD_ID = ThreadId("\$aThreadId") val AN_EVENT_ID = EventId("\$anEventId") val AN_EVENT_ID_2 = EventId("\$anEventId2") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt index 56513ab970..0bc6a6bce5 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt @@ -41,19 +41,19 @@ import kotlinx.serialization.Serializable */ @Serializable data class PushDataUnifiedPush( - val notification: PushDataUnifiedPushNotification? + val notification: PushDataUnifiedPushNotification? = null ) @Serializable data class PushDataUnifiedPushNotification( - @SerialName("event_id") val eventId: String?, - @SerialName("room_id") val roomId: String?, - @SerialName("counts") var counts: PushDataUnifiedPushCounts?, + @SerialName("event_id") val eventId: String? = null, + @SerialName("room_id") val roomId: String? = null, + @SerialName("counts") var counts: PushDataUnifiedPushCounts? = null, ) @Serializable data class PushDataUnifiedPushCounts( - @SerialName("unread") val unread: Int? + @SerialName("unread") val unread: Int? = null ) fun PushDataUnifiedPush.toPushData() = PushData( diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt index 9788ecf1a1..05cd8425b7 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt @@ -23,7 +23,9 @@ import kotlinx.serialization.json.Json import javax.inject.Inject class UnifiedPushParser @Inject constructor() { + private val json by lazy { Json { ignoreUnknownKeys = true } } + fun parse(message: ByteArray): PushData? { - return tryOrNull { Json.decodeFromString(String(message)) }?.toPushData() + return tryOrNull { json.decodeFromString(String(message)) }?.toPushData() } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt new file mode 100644 index 0000000000..9d02913cf8 --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.firebase + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.push.impl.push.PushData +import org.junit.Test + +class FirebasePushParserTest { + private val validData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 1, + clientSecret = "a-secret" + ) + + private val emptyData = PushData( + eventId = null, + roomId = null, + unread = null, + clientSecret = null + ) + + @Test + fun `test edge cases Firebase`() { + val pushParser = FirebasePushParser() + // Empty Json + assertThat(pushParser.parse(emptyMap())).isEqualTo(emptyData) + // Bad Json + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("unread", "str"))).isEqualTo(validData.copy(unread = null)) + // Extra data + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("extra", "5"))).isEqualTo(validData) + } + + @Test + fun `test Firebase format`() { + val pushParser = FirebasePushParser() + assertThat(pushParser.parse(FIREBASE_PUSH_DATA)).isEqualTo(validData) + } + + @Test + fun `test empty roomId`() { + val pushParser = FirebasePushParser() + val expected = validData.copy(roomId = null) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", ""))).isEqualTo(expected) + } + + @Test + fun `test invalid roomId`() { + val pushParser = FirebasePushParser() + val expected = validData.copy(roomId = null) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain"))).isEqualTo(expected) + } + + @Test + fun `test empty eventId`() { + val pushParser = FirebasePushParser() + val expected = validData.copy(eventId = null) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", ""))).isEqualTo(expected) + } + + @Test + fun `test invalid eventId`() { + val pushParser = FirebasePushParser() + val expected = validData.copy(eventId = null) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId"))).isEqualTo(expected) + } + + companion object { + private val FIREBASE_PUSH_DATA = mapOf( + "event_id" to AN_EVENT_ID.value, + "room_id" to A_ROOM_ID.value, + "unread" to "1", + "prio" to "high", + "cs" to "a-secret", + ) + } +} + +private fun Map.mutate(key: String, value: String?): Map { + return toMutableMap().apply { put(key, value) } +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt new file mode 100644 index 0000000000..00c92bb08a --- /dev/null +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.impl.unifiedpush + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_EVENT_ID +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.push.impl.push.PushData +import org.junit.Test + +class UnifiedPushParserTest { + private val validData = PushData( + eventId = AN_EVENT_ID, + roomId = A_ROOM_ID, + unread = 1, + // TODO handle client secret here. + clientSecret = null + ) + + private val emptyData = PushData( + eventId = null, + roomId = null, + unread = null, + clientSecret = null + ) + + @Test + fun `test edge cases UnifiedPush`() { + val pushParser = UnifiedPushParser() + // Empty string + assertThat(pushParser.parse("".toByteArray())).isNull() + // Empty Json + assertThat(pushParser.parse("{}".toByteArray())).isEqualTo(emptyData) + // Bad Json + assertThat(pushParser.parse("ABC".toByteArray())).isNull() + } + + @Test + fun `test UnifiedPush format`() { + val pushParser = UnifiedPushParser() + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray())).isEqualTo(validData) + } + + @Test + fun `test empty roomId`() { + val pushParser = UnifiedPushParser() + val expected = validData.copy(roomId = null) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isEqualTo(expected) + } + + @Test + fun `test invalid roomId`() { + val pushParser = UnifiedPushParser() + val expected = validData.copy(roomId = null) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isEqualTo(expected) + } + + @Test + fun `test empty eventId`() { + val pushParser = UnifiedPushParser() + val expected = validData.copy(eventId = null) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isEqualTo(expected) + } + + @Test + fun `test invalid eventId`() { + val pushParser = UnifiedPushParser() + val expected = validData.copy(eventId = null) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isEqualTo(expected) + } + + companion object { + private val UNIFIED_PUSH_DATA = + "{\"notification\":{\"event_id\":\"${AN_EVENT_ID.value}\",\"room_id\":\"${A_ROOM_ID.value}\",\"counts\":{\"unread\":1},\"prio\":\"high\"}}" + // TODO Check client secret format? + } +} + +private fun String.mutate(oldValue: String, newValue: String): ByteArray { + return replace(oldValue, newValue).toByteArray() +} From 2926b7443d983f078abe88c16b2558aab2fd5108 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:12:29 +0200 Subject: [PATCH 05/83] Cleanup rule, TemplatePresenter does not exist anymore --- build.gradle.kts | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d7e3a12fcc..66c842630c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -230,7 +230,6 @@ koverMerged { target = kotlinx.kover.api.VerificationTarget.CLASS overrideClassFilter { includes += "*Presenter" - excludes += "*TemplatePresenter" excludes += "*Fake*Presenter" excludes += "io.element.android.appnav.loggedin.LoggedInPresenter$*" } From 3b49ab0e7ab2dd0116f0d53902b942cef23b37f5 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:13:06 +0200 Subject: [PATCH 06/83] Add default value for `noActivityFoundMessage` --- .../element/android/appnav/loggedin/LoggedInView.kt | 2 +- .../features/roomdetails/impl/RoomDetailsNode.kt | 2 -- .../libraries/androidutils/system/SystemUtils.kt | 11 ++++++----- .../androidutils/src/main/res/values/localazy.xml | 4 ++++ .../src/main/res/values-es/translations.xml | 3 +-- .../src/main/res/values-it/translations.xml | 3 +-- .../src/main/res/values-ro/translations.xml | 3 +-- libraries/ui-strings/src/main/res/values/localazy.xml | 1 - tools/localazy/config.json | 6 ++++++ 9 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 libraries/androidutils/src/main/res/values/localazy.xml diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt index 5db19ccae7..3f415c3dc1 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInView.kt @@ -38,7 +38,7 @@ fun LoggedInView( state = state.permissionsState, modifier = modifier, openSystemSettings = { - activity?.let { openAppSettingsPage(it, "") } + activity?.let { openAppSettingsPage(it) } } ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt index 937d755df5..f0e1f28cf3 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsNode.kt @@ -31,7 +31,6 @@ import io.element.android.libraries.androidutils.system.startSharePlainTextInten import io.element.android.libraries.di.RoomScope import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder import io.element.android.libraries.matrix.api.room.MatrixRoom -import io.element.android.libraries.ui.strings.R as StringR @ContributesNode(RoomScope::class) class RoomDetailsNode @AssistedInject constructor( @@ -57,7 +56,6 @@ class RoomDetailsNode @AssistedInject constructor( activityResultLauncher = null, chooserTitle = context.getString(R.string.screen_room_details_share_room_title), text = permalink, - noActivityFoundMessage = context.getString(StringR.string.error_no_compatible_app_found) ) } } diff --git a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt index 5f18f46827..c5bccc97f5 100644 --- a/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt +++ b/libraries/androidutils/src/main/kotlin/io/element/android/libraries/androidutils/system/SystemUtils.kt @@ -32,6 +32,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.annotation.ChecksSdkIntAtLeast import androidx.annotation.RequiresApi import androidx.core.content.getSystemService +import io.element.android.libraries.androidutils.R import io.element.android.libraries.androidutils.compat.getApplicationInfoCompat /** @@ -125,7 +126,7 @@ fun startNotificationSettingsIntent(context: Context, activityResultLauncher: Ac fun openAppSettingsPage( activity: Activity, - noActivityFoundMessage: String, + noActivityFoundMessage: String = activity.getString(R.string.error_no_compatible_app_found), ) { try { activity.startActivity( @@ -156,7 +157,7 @@ fun startNotificationChannelSettingsIntent(activity: Activity, channelID: String fun startAddGoogleAccountIntent( context: Context, activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String, + noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Settings.ACTION_ADD_ACCOUNT) intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf("com.google")) @@ -171,7 +172,7 @@ fun startAddGoogleAccountIntent( fun startInstallFromSourceIntent( context: Context, activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String, + noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES) .setData(Uri.parse(String.format("package:%s", context.packageName))) @@ -189,7 +190,7 @@ fun startSharePlainTextIntent( text: String, subject: String? = null, extraTitle: String? = null, - noActivityFoundMessage: String, + noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), ) { val share = Intent(Intent.ACTION_SEND) share.type = "text/plain" @@ -217,7 +218,7 @@ fun startSharePlainTextIntent( fun startImportTextFromFileIntent( context: Context, activityResultLauncher: ActivityResultLauncher, - noActivityFoundMessage: String, + noActivityFoundMessage: String = context.getString(R.string.error_no_compatible_app_found), ) { val intent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "text/plain" diff --git a/libraries/androidutils/src/main/res/values/localazy.xml b/libraries/androidutils/src/main/res/values/localazy.xml new file mode 100644 index 0000000000..0599c8922b --- /dev/null +++ b/libraries/androidutils/src/main/res/values/localazy.xml @@ -0,0 +1,4 @@ + + + "No compatible app was found to handle this action." + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index 4b14f3a4a7..58b25eaf3c 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -107,7 +107,6 @@ "Símbolos" "No se pudo crear el enlace permanente" "Error al cargar mensajes" - "No se encontró ninguna aplicación compatible con esta acción." "Algunos mensajes no se han enviado" "Lo siento, se ha producido un error" "Hola, puedes hablar conmigo en %1$s: %2$s" @@ -145,4 +144,4 @@ "General" "Versión: %1$s (%2$s)" "es" - \ No newline at end of file + diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 96d0648d3b..2eb58f0d6e 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -107,7 +107,6 @@ "Simboli" "Impossibile creare il collegamento permanente" "Caricamento dei messaggi non riuscito" - "Non è stata trovata alcuna app compatibile per gestire questa azione." "Alcuni messaggi non sono stati inviati" "Siamo spiacenti, si è verificato un errore" "Ehi, parlami su %1$s: %2$s" @@ -145,4 +144,4 @@ "Generali" "Versione: %1$s (%2$s)" "it" - \ No newline at end of file + diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index 1872bb057f..ba066efae9 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -107,7 +107,6 @@ "Simboluri" "Crearea permalink-ului a eșuat" "Încărcarea mesajelor a eșuat" - "Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune." "Unele mesaje nu au fost trimise" "Ne pare rău, a apărut o eroare" "Hei, vorbește cu mine pe %1$s: %2$s" @@ -147,4 +146,4 @@ "General" "Versiunea: %1$s (%2$s)" "ro" - \ No newline at end of file + diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index 37665e7207..de11a74eac 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -114,7 +114,6 @@ "Symbols" "Failed creating the permalink" "Failed loading messages" - "No compatible app was found to handle this action." "Some messages have not been sent" "Sorry, an error occurred" "Hey, talk to me on %1$s: %2$s" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 59c6bd6911..37f8c895f1 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -43,6 +43,12 @@ "rich_text_editor_.*" ] }, + { + "name": ":libraries:androidutils", + "includeRegex": [ + "error_no_compatible_app_found" + ] + }, { "name": ":libraries:push:impl", "includeRegex": [ From 40660ca317f0ef0048f1e48d766a8dcfbb2f3877 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:27:34 +0200 Subject: [PATCH 07/83] Add some check on as*Id() methods, to fail in debug mode. --- .../libraries/matrix/api/core/EventId.kt | 7 +++++- .../matrix/api/core/MatrixPatterns.kt | 24 +++++++++++++++++++ .../libraries/matrix/api/core/RoomId.kt | 7 +++++- .../libraries/matrix/api/core/SessionId.kt | 8 ++++++- .../libraries/matrix/api/core/SpaceId.kt | 7 +++++- .../libraries/matrix/api/core/ThreadId.kt | 7 +++++- .../libraries/matrix/api/core/UserId.kt | 7 +++++- 7 files changed, 61 insertions(+), 6 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt index dc5e7ab16a..ffd5bb8ea2 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt @@ -16,9 +16,14 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline value class EventId(val value: String) : Serializable -fun String.asEventId() = EventId(this) +fun String.asEventId() = EventId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isEventId(this)) { + error("`$this` is not a valid event Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt index f295fe81b9..a30baadb55 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/MatrixPatterns.kt @@ -91,6 +91,14 @@ object MatrixPatterns { PATTERN_CONTAIN_MATRIX_GROUP_IDENTIFIER ) + /** + * Tells if a string is a valid session Id. This is an alias for [isUserId] + * + * @param str the string to test + * @return true if the string is a valid session id + */ + fun isSessionId(str: String?) = isUserId(str) + /** * Tells if a string is a valid user Id. * @@ -101,6 +109,14 @@ object MatrixPatterns { return str != null && str matches PATTERN_CONTAIN_MATRIX_USER_IDENTIFIER } + /** + * Tells if a string is a valid space id. This is an alias for [isRoomId] + * + * @param str the string to test + * @return true if the string is a valid space Id + */ + fun isSpaceId(str: String?) = isRoomId(str) + /** * Tells if a string is a valid room id. * @@ -134,6 +150,14 @@ object MatrixPatterns { str matches PATTERN_CONTAIN_MATRIX_EVENT_IDENTIFIER_V4) } + /** + * Tells if a string is a valid thread id. This is an alias for [isEventId]. + * + * @param str the string to test + * @return true if the string is a valid thread id. + */ + fun isThreadId(str: String?) = isEventId(str) + /** * Tells if a string is a valid group id. * diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index df10038b05..f711723c3f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -16,9 +16,14 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline value class RoomId(val value: String) : Serializable -fun String.asRoomId() = RoomId(this) +fun String.asRoomId() = RoomId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { + error("`$this` is not a valid room Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt index bea1f3c671..8591876b29 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt @@ -16,6 +16,12 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig + typealias SessionId = UserId -fun String.asSessionId() = SessionId(this) +fun String.asSessionId() = SessionId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isSessionId(this)) { + error("`$this` is not a valid session Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt index 849dd7d637..1b8b33426b 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt @@ -16,6 +16,7 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline @@ -26,4 +27,8 @@ value class SpaceId(val value: String) : Serializable */ val MAIN_SPACE = SpaceId("!mainSpace") -fun String.asSpaceId() = SpaceId(this) +fun String.asSpaceId() = SpaceId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isSpaceId(this)) { + error("`$this` is not a valid space Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt index 57fc187406..f95c33bad3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt @@ -16,9 +16,14 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline value class ThreadId(val value: String) : Serializable -fun String.asThreadId() = ThreadId(this) +fun String.asThreadId() = ThreadId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isThreadId(this)) { + error("`$this` is not a valid Thread Id") + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index 216faade45..91f9c6f11c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -16,9 +16,14 @@ package io.element.android.libraries.matrix.api.core +import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline value class UserId(val value: String) : Serializable -fun String.asUserId() = UserId(this) +fun String.asUserId() = UserId(this).also { + if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { + error("`$this` is not a valid user Id") + } +} From 68fb2bd2ebb4265a64d05d1a7dfdf28e95876cf0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:30:09 +0200 Subject: [PATCH 08/83] `if` -> `when` --- .../permissions/api/PermissionsView.kt | 86 ++++++++++--------- 1 file changed, 45 insertions(+), 41 deletions(-) diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt index 382d8e9653..30f19aa31c 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsView.kt @@ -32,48 +32,52 @@ fun PermissionsView( ) { if (state.showDialog.not()) return - if (state.permissionGranted) { - // Notification Granted, nothing to do - } else if (state.permissionAlreadyDenied) { - // In this case, tell the user to go to the settings - ConfirmationDialog( - modifier = modifier, - title = "System", - content = "In order to let the application display notification, please grant the permission to the system settings", - submitText = "Open settings", - onSubmitClicked = { - state.eventSink.invoke(PermissionsEvents.CloseDialog) - openSystemSettings() - }, - onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) }, - ) - } else { - val textToShow = if (state.shouldShowRationale) { - // TODO Move to state - // If the user has denied the permission but the rationale can be shown, - // then gently explain why the app requires this permission - // permissions_rationale_msg_notification - "To be able to receive notifications, please grant the permission. Else you will not be able to be alerted if you've got new messages." - } else { - // TODO Move to state - // If it's the first time the user lands on this feature, or the user - // doesn't want to be asked again for this permission, explain that the - // permission is required - "To be able to receive notifications, please grant the permission." + when { + state.permissionGranted -> { + // Notification Granted, nothing to do + } + state.permissionAlreadyDenied -> { + // In this case, tell the user to go to the settings + ConfirmationDialog( + modifier = modifier, + title = "System", + content = "In order to let the application display notification, please grant the permission to the system settings", + submitText = "Open settings", + onSubmitClicked = { + state.eventSink.invoke(PermissionsEvents.CloseDialog) + openSystemSettings() + }, + onDismiss = { state.eventSink.invoke(PermissionsEvents.CloseDialog) }, + ) + } + else -> { + val textToShow = if (state.shouldShowRationale) { + // TODO Move to state + // If the user has denied the permission but the rationale can be shown, + // then gently explain why the app requires this permission + // permissions_rationale_msg_notification + "To be able to receive notifications, please grant the permission. Else you will not be able to be alerted if you've got new messages." + } else { + // TODO Move to state + // If it's the first time the user lands on this feature, or the user + // doesn't want to be asked again for this permission, explain that the + // permission is required + "To be able to receive notifications, please grant the permission." + } + ConfirmationDialog( + modifier = modifier, + title = "Notifications", + content = textToShow, + submitText = "Request permission", + onSubmitClicked = { + state.eventSink.invoke(PermissionsEvents.OpenSystemDialog) + }, + onCancelClicked = { + state.eventSink.invoke(PermissionsEvents.CloseDialog) + }, + onDismiss = {} + ) } - ConfirmationDialog( - modifier = modifier, - title = "Notifications", - content = textToShow, - submitText = "Request permission", - onSubmitClicked = { - state.eventSink.invoke(PermissionsEvents.OpenSystemDialog) - }, - onCancelClicked = { - state.eventSink.invoke(PermissionsEvents.CloseDialog) - }, - onDismiss = {} - ) } } From 5fc7870e4746ef13a9cb6f6195af3300f4f3d952 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:34:51 +0200 Subject: [PATCH 09/83] Add more state for more previews. --- .../libraries/permissions/api/PermissionsViewStateProvider.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt index 5cf74aca90..e93b74d934 100644 --- a/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt +++ b/libraries/permissions/api/src/main/kotlin/io/element/android/libraries/permissions/api/PermissionsViewStateProvider.kt @@ -23,7 +23,8 @@ open class PermissionsViewStateProvider : PreviewParameterProvider get() = sequenceOf( aPermissionsState(), - // Add other state here + aPermissionsState().copy(shouldShowRationale = true), + aPermissionsState().copy(permissionAlreadyDenied = true), ) } From 7aca1d6bd54c9b9b4dec9ce6fdb85891e5ac8cb9 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:46:44 +0200 Subject: [PATCH 10/83] Let this module generate screenshot preview. --- libraries/permissions/api/build.gradle.kts | 3 +++ ...oup_PermissionsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...oup_PermissionsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...oup_PermissionsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ ...up_PermissionsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...up_PermissionsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...up_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 +++ 7 files changed, 21 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/libraries/permissions/api/build.gradle.kts b/libraries/permissions/api/build.gradle.kts index d86f790a44..99bc60a2eb 100644 --- a/libraries/permissions/api/build.gradle.kts +++ b/libraries/permissions/api/build.gradle.kts @@ -16,6 +16,7 @@ plugins { id("io.element.android-compose-library") + alias(libs.plugins.ksp) } android { @@ -27,4 +28,6 @@ dependencies { implementation(projects.libraries.designsystem) implementation(projects.libraries.uiStrings) + + ksp(libs.showkase.processor) } diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4abfbacbbc --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d91a9a9decf08f9bd9301d5282e889fb4e12d4270e8dc7c4b8b24de0b6059126 +size 24662 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..cc898176e8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:44aa08cd01010ca90fb9ca33cb724dd0ebc6d523eff25b40e65f73f3ca280c19 +size 34209 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3141cfc7aa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cca345dc82e59d0a87411f00e39e12128b4e61d2d7343c8d7af3d96e54038ca0 +size 28591 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..22ad0f0059 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6548a7cc39e0861de6af8e55bc00424d8835e5aa3d99d4e7c68db682e054d677 +size 24542 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..4a8614354a --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:817a15eb656b7dbcc23a0a62528804d346ca045d9c765579829ac5d3e8d16974 +size 34224 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..dbd6530f91 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.permissions.api_null_DefaultGroup_PermissionsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:775380d09f801b563ba5444e046088963d6830c9bf2fa98cf106fae28f94784a +size 28600 From 0cf2bfea0e8ffb82f4a25373fc2818e0603407d0 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:48:12 +0200 Subject: [PATCH 11/83] Fix bad log. --- .../libraries/permissions/impl/DefaultPermissionsPresenter.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index 9e91869549..e3fc4ed12b 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -79,7 +79,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( if (!result) { // Should show rational true -> denied. if (permissionState?.status?.shouldShowRationale == true) { - Timber.tag("PERMISSION").w("onPermissionResult: reset the store") + Timber.tag("PERMISSION").w("onPermissionResult: setPermissionDenied to true") localCoroutineScope.launch { permissionsStore.setPermissionDenied(permission, true) } From bf63db458ccfc641691488590cc7d52f516cba40 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:50:38 +0200 Subject: [PATCH 12/83] Log: create and use a `loggerTag` --- .../permissions/impl/DefaultPermissionsPresenter.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt index e3fc4ed12b..50012f01b7 100644 --- a/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt +++ b/libraries/permissions/impl/src/main/kotlin/io/element/android/libraries/permissions/impl/DefaultPermissionsPresenter.kt @@ -33,6 +33,7 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.permissions.api.PermissionsEvents import io.element.android.libraries.permissions.api.PermissionsPresenter @@ -40,6 +41,8 @@ import io.element.android.libraries.permissions.api.PermissionsState import kotlinx.coroutines.launch import timber.log.Timber +private val loggerTag = LoggerTag("DefaultPermissionsPresenter") + class DefaultPermissionsPresenter @AssistedInject constructor( @Assisted val permission: String, private val permissionsStore: PermissionsStore, @@ -71,7 +74,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( var permissionState: PermissionState? = null fun onPermissionResult(result: Boolean) { - Timber.tag("PERMISSION").w("onPermissionResult: $result") + Timber.tag(loggerTag.value).d("onPermissionResult: $result") localCoroutineScope.launch { permissionsStore.setPermissionAsked(permission, true) } @@ -79,7 +82,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( if (!result) { // Should show rational true -> denied. if (permissionState?.status?.shouldShowRationale == true) { - Timber.tag("PERMISSION").w("onPermissionResult: setPermissionDenied to true") + Timber.tag(loggerTag.value).d("onPermissionResult: setPermissionDenied to true") localCoroutineScope.launch { permissionsStore.setPermissionDenied(permission, true) } @@ -102,7 +105,6 @@ class DefaultPermissionsPresenter @AssistedInject constructor( val showDialog = rememberSaveable { mutableStateOf(permissionState.status !is PermissionStatus.Granted) } fun handleEvents(event: PermissionsEvents) { - Timber.tag("PERMISSION").w("New event: $event") when (event) { PermissionsEvents.CloseDialog -> { showDialog.value = false @@ -123,7 +125,7 @@ class DefaultPermissionsPresenter @AssistedInject constructor( permissionAlreadyDenied = isAlreadyDenied, eventSink = ::handleEvents ).also { - Timber.tag("PERMISSION").w("New state: $it") + Timber.tag(loggerTag.value).d("New state: $it") } } From bec41f1c6a166c6f1073552754e18e59019be95a Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:54:37 +0200 Subject: [PATCH 13/83] Move dependency declaration to the gradle catalog. --- gradle/libs.versions.toml | 2 ++ libraries/push/impl/build.gradle.kts | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f81ce0e835..16231eedc5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -133,6 +133,8 @@ sqldelight-driver-jvm = { module = "com.squareup.sqldelight:sqlite-driver", vers sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions", version.ref = "sqldelight" } sqlcipher = "net.zetetic:android-database-sqlcipher:4.5.3" sqlite = "androidx.sqlite:sqlite:2.3.1" +unifiedpush = "com.github.UnifiedPush:android-connector:2.1.1" +gujun_span = "me.gujun.android:span:1.7" # Di inject = "javax.inject:javax.inject:1" diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 0968ac4344..159500a515 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -49,7 +49,7 @@ dependencies { implementation(projects.services.appnavstate.api) implementation(projects.services.toolbox.api) - api("me.gujun.android:span:1.7") { + api(libs.gujun.span) { exclude(group = "com.android.support", module = "support-annotations") } @@ -57,7 +57,7 @@ dependencies { implementation("com.google.firebase:firebase-messaging-ktx") // UnifiedPush - api("com.github.UnifiedPush:android-connector:2.1.1") + api(libs.unifiedpush) testImplementation(libs.test.junit) testImplementation(libs.test.mockk) From 5bb504861ce0b78bf8bf2eec360f5ee97970b38c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 15:55:44 +0200 Subject: [PATCH 14/83] Use handy `toIntOrNull` --- .../android/libraries/push/impl/firebase/FirebasePushParser.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt index 906816eb56..1f18b20652 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.push.impl.firebase -import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.push.impl.push.PushData import javax.inject.Inject @@ -25,7 +24,7 @@ class FirebasePushParser @Inject constructor() { val pushDataFirebase = PushDataFirebase( eventId = message["event_id"], roomId = message["room_id"], - unread = message["unread"]?.let { tryOrNull { Integer.parseInt(it) } }, + unread = message["unread"]?.toIntOrNull(), clientSecret = message["cs"], ) return pushDataFirebase.toPushData() From 1134f50090ff812f9bac7bc2a8ead7912a79211e Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 16:09:32 +0200 Subject: [PATCH 15/83] PushData must have valid Event and Room ids. --- .../push/impl/firebase/FirebasePushParser.kt | 2 +- .../push/impl/firebase/PushDataFirebase.kt | 17 +++++++------ .../VectorFirebaseMessagingService.kt | 7 ++++-- .../libraries/push/impl/push/PushData.kt | 10 ++++---- .../impl/unifiedpush/PushDataUnifiedPush.kt | 17 +++++++------ .../VectorUnifiedPushMessagingReceiver.kt | 9 ++++--- .../impl/firebase/FirebasePushParserTest.kt | 25 ++++++------------- .../impl/unifiedpush/UnifiedPushParserTest.kt | 21 ++++------------ 8 files changed, 48 insertions(+), 60 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt index 1f18b20652..8659c299ae 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt @@ -20,7 +20,7 @@ import io.element.android.libraries.push.impl.push.PushData import javax.inject.Inject class FirebasePushParser @Inject constructor() { - fun parse(message: Map): PushData { + fun parse(message: Map): PushData? { val pushDataFirebase = PushDataFirebase( eventId = message["event_id"], roomId = message["room_id"], diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt index bcf48bab15..739c161e79 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.push.impl.firebase -import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asRoomId import io.element.android.libraries.push.impl.push.PushData @@ -41,9 +40,13 @@ data class PushDataFirebase( val clientSecret: String? ) -fun PushDataFirebase.toPushData() = PushData( - eventId = eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(), - roomId = roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(), - unread = unread, - clientSecret = clientSecret, -) +fun PushDataFirebase.toPushData(): PushData? { + val safeEventId = eventId?.asEventId() ?: return null + val safeRoomId = roomId?.asRoomId() ?: return null + return PushData( + eventId = safeEventId, + roomId = safeRoomId, + unread = unread, + clientSecret = clientSecret, + ) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt index 8769baa947..2ccf6f2505 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt @@ -53,8 +53,11 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onMessageReceived(message: RemoteMessage) { Timber.tag(loggerTag.value).d("New Firebase message") coroutineScope.launch { - pushParser.parse(message.data).let { - pushHandler.handle(it) + val pushData = pushParser.parse(message.data) + if (pushData == null) { + Timber.tag(loggerTag.value).w("Invalid data received from Firebase") + } else { + pushHandler.handle(pushData) } } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt index 864155e522..aaf6d65c08 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt @@ -22,14 +22,14 @@ import io.element.android.libraries.matrix.api.core.RoomId /** * Represent parsed data that the app has received from a Push content. * - * @property eventId The Event ID. If not null, it will not be empty, and will have a valid format. - * @property roomId The Room ID. If not null, it will not be empty, and will have a valid format. + * @property eventId The Event Id. + * @property roomId The Room Id. * @property unread Number of unread message. - * @property clientSecret A client secret, used to determine which user should receive the notification. + * @property clientSecret data used when the pusher was configured, to be able to determine the session. */ data class PushData( - val eventId: EventId?, - val roomId: RoomId?, + val eventId: EventId, + val roomId: RoomId, val unread: Int?, val clientSecret: String?, ) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt index 0bc6a6bce5..73d5f0286a 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt @@ -16,7 +16,6 @@ package io.element.android.libraries.push.impl.unifiedpush -import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asRoomId import io.element.android.libraries.push.impl.push.PushData @@ -56,9 +55,13 @@ data class PushDataUnifiedPushCounts( @SerialName("unread") val unread: Int? = null ) -fun PushDataUnifiedPush.toPushData() = PushData( - eventId = notification?.eventId?.takeIf { MatrixPatterns.isEventId(it) }?.asEventId(), - roomId = notification?.roomId?.takeIf { MatrixPatterns.isRoomId(it) }?.asRoomId(), - unread = notification?.counts?.unread, - clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush -) +fun PushDataUnifiedPush.toPushData(): PushData? { + val safeEventId = notification?.eventId?.asEventId() ?: return null + val safeRoomId = notification.roomId?.asRoomId() ?: return null + return PushData( + eventId = safeEventId, + roomId = safeRoomId, + unread = notification.counts?.unread, + clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush + ) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index 81dd389e78..acb438cc70 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -67,10 +67,11 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { override fun onMessage(context: Context, message: ByteArray, instance: String) { Timber.tag(loggerTag.value).d("New message") coroutineScope.launch { - pushParser.parse(message)?.let { - pushHandler.handle(it) - } ?: run { - Timber.tag(loggerTag.value).w("Invalid received data Json format") + val pushData = pushParser.parse(message) + if (pushData == null) { + Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush") + } else { + pushHandler.handle(pushData) } } } diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt index 9d02913cf8..b14e067dae 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt @@ -30,18 +30,11 @@ class FirebasePushParserTest { clientSecret = "a-secret" ) - private val emptyData = PushData( - eventId = null, - roomId = null, - unread = null, - clientSecret = null - ) - @Test fun `test edge cases Firebase`() { val pushParser = FirebasePushParser() // Empty Json - assertThat(pushParser.parse(emptyMap())).isEqualTo(emptyData) + assertThat(pushParser.parse(emptyMap())).isNull() // Bad Json assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("unread", "str"))).isEqualTo(validData.copy(unread = null)) // Extra data @@ -57,31 +50,27 @@ class FirebasePushParserTest { @Test fun `test empty roomId`() { val pushParser = FirebasePushParser() - val expected = validData.copy(roomId = null) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isEqualTo(expected) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", ""))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull() + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", ""))).isNull() } @Test fun `test invalid roomId`() { val pushParser = FirebasePushParser() - val expected = validData.copy(roomId = null) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain"))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain"))).isNull() } @Test fun `test empty eventId`() { val pushParser = FirebasePushParser() - val expected = validData.copy(eventId = null) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isEqualTo(expected) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", ""))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isNull() + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", ""))).isNull() } @Test fun `test invalid eventId`() { val pushParser = FirebasePushParser() - val expected = validData.copy(eventId = null) - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId"))).isEqualTo(expected) + assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId"))).isNull() } companion object { diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt index 00c92bb08a..f9275de4b2 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt @@ -31,20 +31,13 @@ class UnifiedPushParserTest { clientSecret = null ) - private val emptyData = PushData( - eventId = null, - roomId = null, - unread = null, - clientSecret = null - ) - @Test fun `test edge cases UnifiedPush`() { val pushParser = UnifiedPushParser() // Empty string assertThat(pushParser.parse("".toByteArray())).isNull() // Empty Json - assertThat(pushParser.parse("{}".toByteArray())).isEqualTo(emptyData) + assertThat(pushParser.parse("{}".toByteArray())).isNull() // Bad Json assertThat(pushParser.parse("ABC".toByteArray())).isNull() } @@ -58,29 +51,25 @@ class UnifiedPushParserTest { @Test fun `test empty roomId`() { val pushParser = UnifiedPushParser() - val expected = validData.copy(roomId = null) - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isEqualTo(expected) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isNull() } @Test fun `test invalid roomId`() { val pushParser = UnifiedPushParser() - val expected = validData.copy(roomId = null) - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isEqualTo(expected) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isNull() } @Test fun `test empty eventId`() { val pushParser = UnifiedPushParser() - val expected = validData.copy(eventId = null) - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isEqualTo(expected) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isNull() } @Test fun `test invalid eventId`() { val pushParser = UnifiedPushParser() - val expected = validData.copy(eventId = null) - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isEqualTo(expected) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isNull() } companion object { From 219b97eea7f7d78c4093302b1315a177a3ba0fd7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 20:42:59 +0200 Subject: [PATCH 16/83] Split push module into several modules: Firebase, UnifiedPush, store --- appnav/build.gradle.kts | 1 + .../appnav/loggedin/LoggedInPresenter.kt | 4 +- .../appnav/loggedin/LoggedInPresenterTest.kt | 7 ++- libraries/push/api/build.gradle.kts | 1 + .../android/libraries/push/api/PushService.kt | 13 ++++- libraries/push/impl/build.gradle.kts | 6 +-- .../push/impl/src/main/AndroidManifest.xml | 50 +----------------- .../libraries/push/impl/DefaultPushService.kt | 20 ++++---- .../android/libraries/push/impl/FcmHelper.kt | 49 ------------------ .../libraries/push/impl/PushersManager.kt | 36 ++++--------- .../libraries/push/impl/config/PushConfig.kt | 17 ------- .../{PushHandler.kt => DefaultPushHandler.kt} | 11 ++-- libraries/pushproviders/api/build.gradle.kts | 28 ++++++++++ .../libraries/push/providers/api}/PushData.kt | 10 ++-- .../push/providers/api/PushHandler.kt | 21 ++++++++ .../push/providers/api/PushProvider.kt | 27 ++++++++++ .../push/providers/api/PusherSubscriber.kt | 23 +++++++++ .../pushproviders/firebase/build.gradle.kts | 48 +++++++++++++++++ .../firebase/src/main/AndroidManifest.xml | 31 +++++++++++ .../EnsureFcmTokenIsRetrievedUseCase.kt | 20 ++++---- .../push/providers/firebase/FirebaseConfig.kt | 27 ++++++++++ .../providers}/firebase/FirebasePushParser.kt | 6 +-- .../firebase/FirebasePushProvider.kt | 44 ++++++++++++++++ .../providers/firebase/FirebaseSetPusher.kt | 51 +++++++++++++++++++ .../providers/firebase}/GoogleFcmHelper.kt | 20 +++----- .../providers}/firebase/PushDataFirebase.kt | 4 +- .../VectorFirebaseMessagingService.kt | 14 ++--- .../VectorFirebaseMessagingServiceBindings.kt | 2 +- .../providers/firebase/di/FirebaseModule.kt | 33 ++++++++++++ .../firebase/FirebasePushParserTest.kt | 4 +- .../unifiedpush/build.gradle.kts | 49 ++++++++++++++++++ .../unifiedpush/src/main/AndroidManifest.xml | 47 +++++++++++++++++ .../unifiedpush/GuardServiceStarter.kt | 2 +- .../unifiedpush/KeepInternalDistributor.kt | 4 +- .../unifiedpush/PushDataUnifiedPush.kt | 4 +- .../unifiedpush/RegisterUnifiedPushUseCase.kt | 8 +-- .../unifiedpush/UnifiedPushConfig.kt | 27 ++++++++++ .../unifiedpush}/UnifiedPushHelper.kt | 26 ++++++---- .../unifiedpush/UnifiedPushParser.kt | 6 +-- .../unifiedpush/UnifiedPushProvider.kt | 31 +++++++++++ .../unifiedpush}/UnifiedPushStore.kt | 4 +- .../UnregisterUnifiedPushUseCase.kt | 15 ++---- .../VectorUnifiedPushMessagingReceiver.kt | 32 ++++++------ ...torUnifiedPushMessagingReceiverBindings.kt | 2 +- .../unifiedpush/di/UnifiedPushModule.kt | 33 ++++++++++++ .../unifiedpush/UnifiedPushParserTest.kt | 4 +- libraries/pushstore/api/build.gradle.kts | 26 ++++++++++ .../libraries/pushstore/api}/UserPushStore.kt | 9 +--- .../pushstore/api/UserPushStoreFactory.kt | 24 +++++++++ libraries/pushstore/impl/build.gradle.kts | 38 ++++++++++++++ .../impl/DefaultUserPushStoreFactory.kt} | 12 +++-- .../pushstore/impl}/UserPushStoreDataStore.kt | 7 +-- .../kotlin/extension/DependencyHandleScope.kt | 6 +++ 53 files changed, 768 insertions(+), 276 deletions(-) delete mode 100644 libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt rename libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/{PushHandler.kt => DefaultPushHandler.kt} (93%) create mode 100644 libraries/pushproviders/api/build.gradle.kts rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push => pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api}/PushData.kt (85%) create mode 100644 libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt create mode 100644 libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt create mode 100644 libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt create mode 100644 libraries/pushproviders/firebase/build.gradle.kts create mode 100644 libraries/pushproviders/firebase/src/main/AndroidManifest.xml rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/EnsureFcmTokenIsRetrievedUseCase.kt (66%) create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/FirebasePushParser.kt (85%) create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase}/GoogleFcmHelper.kt (87%) mode change 100755 => 100644 rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/PushDataFirebase.kt (91%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/VectorFirebaseMessagingService.kt (82%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers}/firebase/VectorFirebaseMessagingServiceBindings.kt (93%) create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push/impl => pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers}/firebase/FirebasePushParserTest.kt (95%) create mode 100644 libraries/pushproviders/unifiedpush/build.gradle.kts create mode 100644 libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/GuardServiceStarter.kt (93%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/KeepInternalDistributor.kt (90%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/PushDataUnifiedPush.kt (93%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/RegisterUnifiedPushUseCase.kt (86%) create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush}/UnifiedPushHelper.kt (90%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/UnifiedPushParser.kt (85%) create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush}/UnifiedPushStore.kt (95%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/UnregisterUnifiedPushUseCase.kt (72%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/VectorUnifiedPushMessagingReceiver.kt (82%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers}/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt (93%) create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push/impl => pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers}/unifiedpush/UnifiedPushParserTest.kt (95%) create mode 100644 libraries/pushstore/api/build.gradle.kts rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore => pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api}/UserPushStore.kt (72%) create mode 100644 libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt create mode 100644 libraries/pushstore/impl/build.gradle.kts rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt} (78%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl}/UserPushStoreDataStore.kt (91%) diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 8ece3e5841..17efdc15fc 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -45,6 +45,7 @@ dependencies { implementation(projects.libraries.architecture) implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) + implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.designsystem) implementation(projects.libraries.matrixui) implementation(projects.libraries.uiStrings) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index b628f19bd1..552420abf8 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -46,7 +46,9 @@ class LoggedInPresenter @Inject constructor( override fun present(): LoggedInState { LaunchedEffect(Unit) { // Ensure pusher is registered - pushService.registerFirebasePusher(matrixClient) + // TODO Register with Firebase for now + val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect + pushService.registerWith(matrixClient, pushProvider, pushProvider.getDistributorNames().first()) } val permissionsState = postNotificationPermissionsPresenter.present() diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 64767eaafc..595f4850b2 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.providers.api.PushProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Test @@ -55,7 +56,11 @@ class LoggedInPresenterTest { override fun notificationStyleChanged() { } - override suspend fun registerFirebasePusher(matrixClient: MatrixClient) { + override fun getAvailablePushProviders(): List { + return emptyList() + } + + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { } override suspend fun testPush() { diff --git a/libraries/push/api/build.gradle.kts b/libraries/push/api/build.gradle.kts index be1bbc13ef..0c5df8fb25 100644 --- a/libraries/push/api/build.gradle.kts +++ b/libraries/push/api/build.gradle.kts @@ -26,4 +26,5 @@ dependencies { implementation(libs.androidx.corektx) implementation(libs.coroutines.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.pushproviders.api) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index cf5792be35..7eeca6e0a3 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -17,12 +17,21 @@ package io.element.android.libraries.push.api import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.PushProvider interface PushService { + // TODO Move away fun notificationStyleChanged() - // Ensure pusher is registered - suspend fun registerFirebasePusher(matrixClient: MatrixClient) + fun getAvailablePushProviders(): List + /** + * Will unregister any previous pusher and register a new one with the provided [PushProvider]. + * + * The method has effect only if the [PushProvider] is different than the current one. + */ + suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) + + // TODO Move away suspend fun testPush() } diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 159500a515..44fc21fb2b 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -43,6 +43,8 @@ dependencies { implementation(projects.libraries.androidutils) implementation(projects.libraries.network) implementation(projects.libraries.matrix.api) + api(projects.libraries.pushproviders.api) + api(projects.libraries.pushstore.api) api(projects.libraries.push.api) implementation(projects.services.analytics.api) @@ -53,12 +55,10 @@ dependencies { exclude(group = "com.android.support", module = "support-annotations") } + // TODO Remove implementation(platform(libs.google.firebase.bom)) implementation("com.google.firebase:firebase-messaging-ktx") - // UnifiedPush - api(libs.unifiedpush) - testImplementation(libs.test.junit) testImplementation(libs.test.mockk) testImplementation(libs.test.truth) diff --git a/libraries/push/impl/src/main/AndroidManifest.xml b/libraries/push/impl/src/main/AndroidManifest.xml index a386a89caf..6085ffe4a4 100644 --- a/libraries/push/impl/src/main/AndroidManifest.xml +++ b/libraries/push/impl/src/main/AndroidManifest.xml @@ -14,63 +14,15 @@ ~ limitations under the License. --> - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index 7924ddf996..9bf4afa7ea 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -20,27 +20,29 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.api.PushService -import io.element.android.libraries.push.impl.config.PushConfig -import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager -import timber.log.Timber +import io.element.android.libraries.push.providers.api.PushProvider import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPushService @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val pushersManager: PushersManager, - private val fcmHelper: FcmHelper, + private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, ) : PushService { override fun notificationStyleChanged() { notificationDrawerManager.notificationStyleChanged() } - override suspend fun registerFirebasePusher(matrixClient: MatrixClient) { - val pushKey = fcmHelper.getFcmToken() ?: return Unit.also { - Timber.tag(pushLoggerTag.value).w("Unable to register pusher, Firebase token is not known.") - } - pushersManager.registerPusher(matrixClient, pushKey, PushConfig.pusher_http_url) + override fun getAvailablePushProviders(): List { + // TODO Sort by priority? + return pushProviders.toList() + } + + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { + // TODO Get current push provider, compare with provided one, then unregister and register if different, and store change + + pushProvider.registerWith(matrixClient, distributorName) } override suspend fun testPush() { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt deleted file mode 100644 index 9b8b6c2281..0000000000 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/FcmHelper.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2022 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.push.impl - -interface FcmHelper { - fun isFirebaseAvailable(): Boolean - - /** - * Retrieves the FCM registration token. - * - * @return the FCM token or null if not received from FCM. - */ - fun getFcmToken(): String? - - /** - * Store FCM token to the SharedPrefs. - * - * @param token the token to store. - */ - fun storeFcmToken(token: String?) - - /** - * onNewToken may not be called on application upgrade, so ensure my shared pref is set. - * - * @param pushersManager the instance to register the pusher on. - * @param registerPusher whether the pusher should be registered. - */ - fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) - - /* - fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) - - fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) - */ -} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index f1ac346909..532a904c82 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -16,7 +16,9 @@ package io.element.android.libraries.push.impl +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.EventId @@ -26,10 +28,9 @@ import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.config.PushConfig import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest -import io.element.android.libraries.push.impl.userpushstore.UserPushStoreFactory -import io.element.android.libraries.push.impl.userpushstore.isFirebase +import io.element.android.libraries.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.api.toUserList import io.element.android.services.toolbox.api.appname.AppNameProvider import timber.log.Timber import javax.inject.Inject @@ -38,8 +39,9 @@ internal const val DEFAULT_PUSHER_FILE_TAG = "mobile" private val loggerTag = LoggerTag("PushersManager", pushLoggerTag) +@ContributesBinding(AppScope::class) class PushersManager @Inject constructor( - private val unifiedPushHelper: UnifiedPushHelper, + // private val unifiedPushHelper: UnifiedPushHelper, // private val localeProvider: LocaleProvider, private val appNameProvider: AppNameProvider, // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, @@ -48,14 +50,14 @@ class PushersManager @Inject constructor( private val sessionStore: SessionStore, private val matrixAuthenticationService: MatrixAuthenticationService, private val userPushStoreFactory: UserPushStoreFactory, - private val fcmHelper: FcmHelper, -) { +): PusherSubscriber { + // TODO Move this to the PushProvider API suspend fun testPush() { pushGatewayNotifyRequest.execute( PushGatewayNotifyRequest.Params( - url = unifiedPushHelper.getPushGateway() ?: return, + url = "TODO", // unifiedPushHelper.getPushGateway() ?: return, appId = PushConfig.pusher_app_id, - pushKey = unifiedPushHelper.getEndpointOrToken().orEmpty(), + pushKey = "TODO", // unifiedPushHelper.getEndpointOrToken().orEmpty(), eventId = TEST_EVENT_ID ) ) @@ -73,26 +75,10 @@ class PushersManager @Inject constructor( TODO() } - suspend fun onNewFirebaseToken(firebaseToken: String) { - fcmHelper.storeFcmToken(firebaseToken) - - // Register the pusher for all the sessions - sessionStore.getAllSessions().toUserList().forEach { userId -> - val userDataStore = userPushStoreFactory.create(userId) - if (userDataStore.isFirebase()) { - matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> - registerPusher(client, firebaseToken, PushConfig.pusher_http_url) - } - } else { - Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") - } - } - } - /** * Register a pusher to the server if not done yet. */ - suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { + override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { val userDataStore = userPushStoreFactory.create(matrixClient.sessionId.value) if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt index d2d1c96506..823dd7f693 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/config/PushConfig.kt @@ -17,25 +17,8 @@ package io.element.android.libraries.push.impl.config object PushConfig { - /** - * It is the push gateway for FCM embedded distributor. - * Note: pusher_http_url should have path '/_matrix/push/v1/notify' --> - */ - const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify" - - /** - * It is the push gateway for UnifiedPush. - * Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify' - */ - const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" - /** * Note: pusher_app_id cannot exceed 64 chars. */ const val pusher_app_id: String = "im.vector.app.android" - - /** - * Set to true to allow external push distributor such as Ntfy. - */ - const val allowExternalUnifiedPushDistributors: Boolean = false } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt rename to libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index b4c9716b62..30ee74f3de 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -21,9 +21,11 @@ import android.content.Intent import android.os.Handler import android.os.Looper import androidx.localbroadcastmanager.content.LocalBroadcastManager +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.androidutils.network.WifiDetector import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta +import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.push.api.store.PushDataStore @@ -34,6 +36,8 @@ import io.element.android.libraries.push.impl.notifications.NotifiableEventResol import io.element.android.libraries.push.impl.notifications.NotificationActionIds import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager import io.element.android.libraries.push.impl.store.DefaultPushDataStore +import io.element.android.libraries.push.providers.api.PushData +import io.element.android.libraries.push.providers.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -43,7 +47,8 @@ import javax.inject.Inject private val loggerTag = LoggerTag("PushHandler", pushLoggerTag) -class PushHandler @Inject constructor( +@ContributesBinding(AppScope::class) +class DefaultPushHandler @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val notifiableEventResolver: NotifiableEventResolver, private val pushDataStore: PushDataStore, @@ -53,7 +58,7 @@ class PushHandler @Inject constructor( @ApplicationContext private val context: Context, private val buildMeta: BuildMeta, private val matrixAuthenticationService: MatrixAuthenticationService, -) { +): PushHandler { private val coroutineScope = CoroutineScope(SupervisorJob()) private val wifiDetector: WifiDetector = WifiDetector(context) @@ -68,7 +73,7 @@ class PushHandler @Inject constructor( * * @param pushData the data received in the push. */ - suspend fun handle(pushData: PushData) { + override suspend fun handle(pushData: PushData) { Timber.tag(loggerTag.value).d("## handling pushData") if (buildMeta.lowPrivacyLoggingEnabled) { diff --git a/libraries/pushproviders/api/build.gradle.kts b/libraries/pushproviders/api/build.gradle.kts new file mode 100644 index 0000000000..08d397b383 --- /dev/null +++ b/libraries/pushproviders/api/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.push.providers.api" +} + +dependencies { + implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushData.kt similarity index 85% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt rename to libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushData.kt index aaf6d65c08..b304d10b34 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/PushData.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushData.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.push +package io.element.android.libraries.push.providers.api import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId @@ -28,8 +28,8 @@ import io.element.android.libraries.matrix.api.core.RoomId * @property clientSecret data used when the pusher was configured, to be able to determine the session. */ data class PushData( - val eventId: EventId, - val roomId: RoomId, - val unread: Int?, - val clientSecret: String?, + val eventId: EventId, + val roomId: RoomId, + val unread: Int?, + val clientSecret: String?, ) diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt new file mode 100644 index 0000000000..09ca420a1f --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushHandler.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.api + +interface PushHandler { + suspend fun handle(pushData: PushData) +} diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt new file mode 100644 index 0000000000..2b90c3d5b3 --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.api + +import io.element.android.libraries.matrix.api.MatrixClient + +/** + * This is the main API for this module + */ +interface PushProvider { + fun getDistributorNames(): List + suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) +} diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt new file mode 100644 index 0000000000..805244e0ed --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.api + +import io.element.android.libraries.matrix.api.MatrixClient + +interface PusherSubscriber { + suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) +} diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts new file mode 100644 index 0000000000..65360d5465 --- /dev/null +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) + // kotlin("plugin.serialization") version "1.8.10" +} + +android { + namespace = "io.element.android.libraries.push.providers.firebase" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(libs.androidx.corektx) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.di) + implementation(projects.libraries.matrix.api) + + implementation(projects.libraries.pushstore.api) + implementation(projects.libraries.pushproviders.api) + + implementation(platform(libs.google.firebase.bom)) + implementation("com.google.firebase:firebase-messaging-ktx") + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) +} diff --git a/libraries/pushproviders/firebase/src/main/AndroidManifest.xml b/libraries/pushproviders/firebase/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..40dc254644 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/EnsureFcmTokenIsRetrievedUseCase.kt similarity index 66% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/EnsureFcmTokenIsRetrievedUseCase.kt index 9e9b28ecb8..e859976789 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/EnsureFcmTokenIsRetrievedUseCase.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/EnsureFcmTokenIsRetrievedUseCase.kt @@ -14,24 +14,22 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase -import io.element.android.libraries.push.impl.FcmHelper -import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.push.impl.UnifiedPushHelper import javax.inject.Inject +// TODO class EnsureFcmTokenIsRetrievedUseCase @Inject constructor( - private val unifiedPushHelper: UnifiedPushHelper, - private val fcmHelper: FcmHelper, +// private val unifiedPushHelper: UnifiedPushHelper, +// private val fcmHelper: FcmHelper, // private val activeSessionHolder: ActiveSessionHolder, ) { - fun execute(pushersManager: PushersManager, registerPusher: Boolean) { - if (unifiedPushHelper.isEmbeddedDistributor()) { - fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher)) - } - } +// fun execute(pushersManager: PushersManager, registerPusher: Boolean) { +// if (unifiedPushHelper.isEmbeddedDistributor()) { +// fcmHelper.ensureFcmTokenIsRetrieved(pushersManager, shouldAddHttpPusher(registerPusher)) +// } +// } private fun shouldAddHttpPusher(registerPusher: Boolean) = if (registerPusher) { /* diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt new file mode 100644 index 0000000000..27463825d6 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.firebase + +object FirebaseConfig { + /** + * It is the push gateway for firebase. + * Note: pusher_http_url should have path '/_matrix/push/v1/notify' --> + */ + const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify" + + const val internalName = "NOTIFICATION_METHOD_FIREBASE" +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParser.kt similarity index 85% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParser.kt index 8659c299ae..d3af7d8448 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParser.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParser.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,9 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import javax.inject.Inject class FirebasePushParser @Inject constructor() { diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt new file mode 100644 index 0000000000..855762e5ab --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.firebase + +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.push.providers.api.PusherSubscriber +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("FirebasePushProvider") + +class FirebasePushProvider @Inject constructor( + private val googleFcmHelper: GoogleFcmHelper, + private val pusherSubscriber: PusherSubscriber, +) : PushProvider { + + override fun getDistributorNames(): List { + // Must return an non-empty list for now + return listOf("unused") + } + + override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { + val pushKey = googleFcmHelper.getFcmToken() ?: return Unit.also { + Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") + } + pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt new file mode 100644 index 0000000000..ea6937551a --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.firebase + +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.toUserList +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("FirebaseSetPusher") + +// TODO Rename +class FirebaseSetPusher @Inject constructor( + private val pusherSubscriber: PusherSubscriber, + private val sessionStore: SessionStore, + private val userPushStoreFactory: UserPushStoreFactory, + private val matrixAuthenticationService: MatrixAuthenticationService, +) { + suspend fun onNewFirebaseToken(firebaseToken: String) { + // Register the pusher for all the sessions + sessionStore.getAllSessions().toUserList().forEach { userId -> + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.getNotificationMethod() == FirebaseConfig.internalName) { + matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> + pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) + } + } else { + Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") + } + } + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt old mode 100755 new mode 100644 similarity index 87% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt index 6c73607196..0772ac0603 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/GoogleFcmHelper.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt @@ -14,44 +14,37 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.providers.firebase import android.content.Context import android.content.SharedPreferences -import android.widget.Toast import androidx.core.content.edit import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import com.google.firebase.messaging.FirebaseMessaging -import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.DefaultPreferences -import kotlinx.coroutines.runBlocking -import timber.log.Timber import javax.inject.Inject /** * This class store the FCM token in SharedPrefs and ensure this token is retrieved. * It has an alter ego in the fdroid variant. */ -@ContributesBinding(AppScope::class) +// TODO Rename to store? class GoogleFcmHelper @Inject constructor( @ApplicationContext private val context: Context, @DefaultPreferences private val sharedPrefs: SharedPreferences, -) : FcmHelper { - override fun isFirebaseAvailable(): Boolean = true - - override fun getFcmToken(): String? { +) { + fun getFcmToken(): String? { return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) } - override fun storeFcmToken(token: String?) { + fun storeFcmToken(token: String?) { sharedPrefs.edit { putString(PREFS_KEY_FCM_TOKEN, token) } } + /* override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) { // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' if (checkPlayServices(context)) { @@ -76,6 +69,7 @@ class GoogleFcmHelper @Inject constructor( Timber.e("No valid Google Play Services found. Cannot use FCM.") } } + */ /** * Check the device to make sure it has the Google Play Services APK. If diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/PushDataFirebase.kt similarity index 91% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/PushDataFirebase.kt index 739c161e79..5c336e7dc6 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/PushDataFirebase.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/PushDataFirebase.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asRoomId -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData /** * In this case, the format is: diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt similarity index 82% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt index 2ccf6f2505..02bb84c541 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt @@ -14,27 +14,26 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase import com.google.firebase.messaging.FirebaseMessagingService import com.google.firebase.messaging.RemoteMessage import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.push.impl.log.pushLoggerTag -import io.element.android.libraries.push.impl.push.PushHandler +import io.element.android.libraries.push.providers.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("Firebase", pushLoggerTag) +private val loggerTag = LoggerTag("Firebase") class VectorFirebaseMessagingService : FirebaseMessagingService() { - @Inject lateinit var pushersManager: PushersManager + @Inject lateinit var firebaseSetPusher: FirebaseSetPusher @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler + @Inject lateinit var googleFcmHelper: GoogleFcmHelper private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -45,8 +44,9 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") + googleFcmHelper.storeFcmToken(token) coroutineScope.launch { - pushersManager.onNewFirebaseToken(token) + firebaseSetPusher.onNewFirebaseToken(token) } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingServiceBindings.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingServiceBindings.kt index aef87e7df3..e17cc922ee 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/firebase/VectorFirebaseMessagingServiceBindings.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingServiceBindings.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase import com.squareup.anvil.annotations.ContributesTo import io.element.android.libraries.di.AppScope diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt new file mode 100644 index 0000000000..9e36754101 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/di/FirebaseModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.firebase.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.push.providers.firebase.FirebasePushProvider + +@Module +@ContributesTo(AppScope::class) +interface FirebaseModule { + @Binds + @IntoSet + fun bind(pushProvider: FirebasePushProvider): PushProvider +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt similarity index 95% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt rename to libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt index b14e067dae..a6525657c8 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/firebase/FirebasePushParserTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.firebase +package io.element.android.libraries.push.providers.firebase import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import org.junit.Test class FirebasePushParserTest { diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts new file mode 100644 index 0000000000..6817d0aad6 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) + kotlin("plugin.serialization") version "1.8.10" +} + +android { + namespace = "io.element.android.libraries.push.providers.unifiedpush" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) + + implementation(projects.libraries.pushproviders.api) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.services.toolbox.api) + + implementation(libs.serialization.json) + + // UnifiedPush library + api(libs.unifiedpush) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) +} diff --git a/libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml b/libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..719733ab3e --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/AndroidManifest.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/GuardServiceStarter.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/GuardServiceStarter.kt index 08bd4a8326..f92468d047 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/GuardServiceStarter.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/GuardServiceStarter.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/KeepInternalDistributor.kt similarity index 90% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/KeepInternalDistributor.kt index de66ed3914..d2e0713f74 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/KeepInternalDistributor.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/KeepInternalDistributor.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import android.content.BroadcastReceiver import android.content.Context diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt index 73d5f0286a..6d5ecb1db3 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/PushDataUnifiedPush.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt @@ -14,11 +14,11 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import io.element.android.libraries.matrix.api.core.asEventId import io.element.android.libraries.matrix.api.core.asRoomId -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt similarity index 86% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt index 50ca94f30d..a80d9ba865 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -14,11 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import android.content.Context import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.push.impl.config.PushConfig import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject @@ -37,11 +36,6 @@ class RegisterUnifiedPushUseCase @Inject constructor( return RegisterUnifiedPushResult.Success } - if (!PushConfig.allowExternalUnifiedPushDistributors) { - saveAndRegisterApp(context.packageName) - return RegisterUnifiedPushResult.Success - } - if (UnifiedPush.getDistributor(context).isNotEmpty()) { registerApp() return RegisterUnifiedPushResult.Success diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt new file mode 100644 index 0000000000..73c31f430c --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.unifiedpush + +object UnifiedPushConfig { + /** + * It is the push gateway for UnifiedPush. + * Note: default_push_gateway_http_url should have path '/_matrix/push/v1/notify' + */ + const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" + + const val internalName = "NOTIFICATION_METHOD_UNIFIEDPUSH" +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt similarity index 90% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt index 12ed3f1993..dce17015b7 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushHelper.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.providers.unifiedpush import android.content.Context -import io.element.android.libraries.androidutils.system.getApplicationLabel import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.push.impl.config.PushConfig import io.element.android.services.toolbox.api.strings.StringProvider import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -31,8 +29,6 @@ import javax.inject.Inject class UnifiedPushHelper @Inject constructor( @ApplicationContext private val context: Context, private val unifiedPushStore: UnifiedPushStore, - // private val matrix: Matrix, - private val fcmHelper: FcmHelper, private val stringProvider: StringProvider, ) { @@ -95,11 +91,14 @@ class UnifiedPushHelper @Inject constructor( // if we use the embedded distributor, // register app_id type upfcm on sygnal // the pushkey if FCM key + /* if (UnifiedPush.getDistributor(context) == context.packageName) { unifiedPushStore.storePushGateway(PushConfig.pusher_http_url) onDoneRunnable?.run() return } + + */ /* TODO EAx UnifiedPush // else, unifiedpush, and pushkey is an endpoint val gateway = PushConfig.default_push_gateway_http_url @@ -132,19 +131,25 @@ class UnifiedPushHelper @Inject constructor( } fun getCurrentDistributorName(): String { + TODO() + /* return when { isEmbeddedDistributor() -> stringProvider.getString(R.string.push_distributor_firebase_android) isBackgroundSync() -> stringProvider.getString(R.string.push_distributor_background_sync_android) else -> context.getApplicationLabel(UnifiedPush.getDistributor(context)) } + + */ } fun isEmbeddedDistributor(): Boolean { - return isInternalDistributor() && fcmHelper.isFirebaseAvailable() + TODO() + //return isInternalDistributor() && fcmHelper.isFirebaseAvailable() } fun isBackgroundSync(): Boolean { - return isInternalDistributor() && !fcmHelper.isFirebaseAvailable() + TODO() + //return isInternalDistributor() && !fcmHelper.isFirebaseAvailable() } private fun isInternalDistributor(): Boolean { @@ -168,12 +173,13 @@ class UnifiedPushHelper @Inject constructor( } fun getEndpointOrToken(): String? { - return if (isEmbeddedDistributor()) fcmHelper.getFcmToken() + // TODO + return if (isEmbeddedDistributor()) "" // fcmHelper.getFcmToken() else unifiedPushStore.getEndpoint() } fun getPushGateway(): String? { - return if (isEmbeddedDistributor()) PushConfig.pusher_http_url + return if (isEmbeddedDistributor()) "" // PushConfig.pusher_http_url else unifiedPushStore.getPushGateway() } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt similarity index 85% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt index 05cd8425b7..881862d473 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParser.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,10 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import javax.inject.Inject diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt new file mode 100644 index 0000000000..a37e9da5e2 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.unifiedpush + +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.PushProvider +import javax.inject.Inject + +class UnifiedPushProvider @Inject constructor(): PushProvider { + override fun getDistributorNames(): List { + TODO("Not yet implemented") + } + + override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { + TODO("Not yet implemented") + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt similarity index 95% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt index 226d0c5669..31d7bbd63e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/UnifiedPushStore.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 New Vector Ltd + * Copyright (c) 2023 New Vector Ltd * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl +package io.element.android.libraries.push.providers.unifiedpush import android.content.Context import android.content.SharedPreferences diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt similarity index 72% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt index 6cd1af1de3..b7cd592951 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnregisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -14,19 +14,9 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush - -import android.content.Context -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.push.api.model.BackgroundSyncMode -import io.element.android.libraries.push.api.store.PushDataStore -import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.push.impl.UnifiedPushHelper -import io.element.android.libraries.push.impl.UnifiedPushStore -import org.unifiedpush.android.connector.UnifiedPush -import timber.log.Timber -import javax.inject.Inject +package io.element.android.libraries.push.providers.unifiedpush +/* class UnregisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, private val pushDataStore: PushDataStore, @@ -50,3 +40,4 @@ class UnregisterUnifiedPushUseCase @Inject constructor( UnifiedPush.unregisterApp(context) } } + */ diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt similarity index 82% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index acb438cc70..d745df6e87 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -14,40 +14,29 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import android.content.Context import android.content.Intent -import android.widget.Toast import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag -import io.element.android.libraries.push.api.model.BackgroundSyncMode -import io.element.android.libraries.push.api.store.PushDataStore -import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.push.impl.UnifiedPushHelper -import io.element.android.libraries.push.impl.UnifiedPushStore -import io.element.android.libraries.push.impl.log.pushLoggerTag -import io.element.android.libraries.push.impl.push.PushHandler +import io.element.android.libraries.push.providers.api.PushHandler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import org.unifiedpush.android.connector.MessagingReceiver import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("Unified", pushLoggerTag) +private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver") class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { - @Inject lateinit var pushersManager: PushersManager @Inject lateinit var pushParser: UnifiedPushParser - - //@Inject lateinit var activeSessionHolder: ActiveSessionHolder - @Inject lateinit var pushDataStore: PushDataStore + // @Inject lateinit var pushDataStore: PushDataStore @Inject lateinit var pushHandler: PushHandler @Inject lateinit var guardServiceStarter: GuardServiceStarter - @Inject lateinit var unifiedPushStore: UnifiedPushStore - @Inject lateinit var unifiedPushHelper: UnifiedPushHelper +// @Inject lateinit var unifiedPushStore: UnifiedPushStore +// @Inject lateinit var unifiedPushHelper: UnifiedPushHelper private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -77,6 +66,8 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { } override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + TODO() + /* Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") if (pushDataStore.areNotificationEnabledForDevice() /* TODO EAx && activeSessionHolder.hasActiveSession() */) { // If the endpoint has changed @@ -99,16 +90,22 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.stop() + */ } override fun onRegistrationFailed(context: Context, instance: String) { + TODO() + /* Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.start() + */ } override fun onUnregistered(context: Context, instance: String) { + TODO() + /* Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME pushDataStore.setFdroidSyncBackgroundMode(mode) @@ -120,5 +117,6 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { Timber.tag(loggerTag.value).d("Probably unregistering a non existing pusher") } } + */ } } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt rename to libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt index 90857d990d..603e297c6b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiverBindings.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import com.squareup.anvil.annotations.ContributesTo import io.element.android.libraries.di.AppScope diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt new file mode 100644 index 0000000000..9e34b349e3 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/di/UnifiedPushModule.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.unifiedpush.di + +import com.squareup.anvil.annotations.ContributesTo +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet +import io.element.android.libraries.di.AppScope +import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.push.providers.unifiedpush.UnifiedPushProvider + +@Module +@ContributesTo(AppScope::class) +interface UnifiedPushModule { + @Binds + @IntoSet + fun bind(pushProvider: UnifiedPushProvider): PushProvider +} diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt similarity index 95% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt rename to libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt index f9275de4b2..6b5c0db62f 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt @@ -14,12 +14,12 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.unifiedpush +package io.element.android.libraries.push.providers.unifiedpush import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID -import io.element.android.libraries.push.impl.push.PushData +import io.element.android.libraries.push.providers.api.PushData import org.junit.Test class UnifiedPushParserTest { diff --git a/libraries/pushstore/api/build.gradle.kts b/libraries/pushstore/api/build.gradle.kts new file mode 100644 index 0000000000..de7a852ee0 --- /dev/null +++ b/libraries/pushstore/api/build.gradle.kts @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") +} + +android { + namespace = "io.element.android.libraries.pushstore.api" +} + +dependencies { +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt similarity index 72% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt rename to libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 82c4beaf20..35ec23f80d 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -14,10 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.userpushstore - -const val NOTIFICATION_METHOD_FIREBASE = "NOTIFICATION_METHOD_FIREBASE" -const val NOTIFICATION_METHOD_UNIFIEDPUSH = "NOTIFICATION_METHOD_UNIFIEDPUSH" +package io.element.android.libraries.pushstore.api /** * Store data related to push about a user. @@ -26,7 +23,7 @@ interface UserPushStore { /** * [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH]. */ - suspend fun getNotificationMethod(): String + suspend fun getNotificationMethod(): String? suspend fun setNotificationMethod(value: String) @@ -36,5 +33,3 @@ interface UserPushStore { suspend fun reset() } - -suspend fun UserPushStore.isFirebase(): Boolean = getNotificationMethod() == NOTIFICATION_METHOD_FIREBASE diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt new file mode 100644 index 0000000000..832180e850 --- /dev/null +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.pushstore.api + +/** + * Store data related to push about a user. + */ +interface UserPushStoreFactory { + fun create(userId: String): UserPushStore +} diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts new file mode 100644 index 0000000000..0a39730199 --- /dev/null +++ b/libraries/pushstore/impl/build.gradle.kts @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.push.pushstore.impl" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(libs.dagger) + implementation(projects.libraries.architecture) + implementation(projects.libraries.core) + implementation(projects.libraries.pushstore.api) + implementation(projects.libraries.sessionStorage.api) + implementation(libs.androidx.corektx) + implementation(libs.androidx.datastore.preferences) +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt similarity index 78% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index 0323713de7..159c1cb892 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -14,28 +14,32 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.userpushstore +package io.element.android.libraries.pushstore.impl import android.content.Context +import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.pushstore.api.UserPushStore +import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.sessionstorage.api.observer.SessionListener import io.element.android.libraries.sessionstorage.api.observer.SessionObserver import javax.inject.Inject @SingleIn(AppScope::class) -class UserPushStoreFactory @Inject constructor( +@ContributesBinding(AppScope::class, boundType = UserPushStoreFactory::class) +class DefaultUserPushStoreFactory @Inject constructor( @ApplicationContext private val context: Context, private val sessionObserver: SessionObserver, -) : SessionListener { +) : UserPushStoreFactory, SessionListener { init { observeSessions() } // We can have only one class accessing a single data store, so keep a cache of them. private val cache = mutableMapOf() - fun create(userId: String): UserPushStore { + override fun create(userId: String): UserPushStore { return cache.getOrPut(userId) { UserPushStoreDataStore( context = context, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt similarity index 91% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 6f25599e54..8b37056768 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/userpushstore/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.userpushstore +package io.element.android.libraries.pushstore.impl import android.content.Context import androidx.datastore.core.DataStore @@ -22,6 +22,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.first /** @@ -35,8 +36,8 @@ class UserPushStoreDataStore( private val notificationMethod = stringPreferencesKey("notificationMethod") private val currentPushKey = stringPreferencesKey("currentPushKey") - override suspend fun getNotificationMethod(): String { - return context.dataStore.data.first()[notificationMethod] ?: NOTIFICATION_METHOD_FIREBASE + override suspend fun getNotificationMethod(): String? { + return context.dataStore.data.first()[notificationMethod] } override suspend fun setNotificationMethod(value: String) { diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 9f0cf92099..368747961d 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -75,6 +75,12 @@ fun DependencyHandlerScope.allLibrariesImpl() { implementation(project(":libraries:core")) implementation(project(":libraries:permissions:impl")) implementation(project(":libraries:push:impl")) + implementation(project(":libraries:push:impl")) + // Comment to not include firebase in the project + implementation(project(":libraries:pushproviders:firebase")) + // Comment to not include unified push in the project + // implementation(project(":libraries:pushproviders:unifiedpush")) + implementation(project(":libraries:pushstore:impl")) implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) implementation(project(":libraries:di")) From 05a8ca0eec89ef1cf389c1e50ccf8546f7aa5c6d Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 21:57:55 +0200 Subject: [PATCH 17/83] Sort provider by index --- .../element/android/libraries/push/impl/DefaultPushService.kt | 3 +-- .../android/libraries/push/providers/api/PushProvider.kt | 4 ++++ .../libraries/push/providers/firebase/FirebasePushProvider.kt | 1 + .../push/providers/unifiedpush/UnifiedPushProvider.kt | 4 +++- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index 9bf4afa7ea..1d9397d9a9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -35,8 +35,7 @@ class DefaultPushService @Inject constructor( } override fun getAvailablePushProviders(): List { - // TODO Sort by priority? - return pushProviders.toList() + return pushProviders.sortedBy { it.index } } override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 2b90c3d5b3..6476e4f815 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -22,6 +22,10 @@ import io.element.android.libraries.matrix.api.MatrixClient * This is the main API for this module */ interface PushProvider { + /** + * Allow to sort provider, from lower index to higher index + */ + val index: Int fun getDistributorNames(): List suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index 855762e5ab..0e96fc4a26 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -29,6 +29,7 @@ class FirebasePushProvider @Inject constructor( private val googleFcmHelper: GoogleFcmHelper, private val pusherSubscriber: PusherSubscriber, ) : PushProvider { + override val index = 0 override fun getDistributorNames(): List { // Must return an non-empty list for now diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt index a37e9da5e2..dcf629fe98 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -20,7 +20,9 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.providers.api.PushProvider import javax.inject.Inject -class UnifiedPushProvider @Inject constructor(): PushProvider { +class UnifiedPushProvider @Inject constructor() : PushProvider { + override val index = 1 + override fun getDistributorNames(): List { TODO("Not yet implemented") } From 586d1a076c13d5f1e4e831629894a2be37b953a4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Fri, 7 Apr 2023 22:08:57 +0200 Subject: [PATCH 18/83] Split GoogleFcmHelper --- .../push/providers/api/PushProvider.kt | 5 + ...etPusher.kt => FirebaseNewTokenHandler.kt} | 12 ++- .../firebase/FirebasePushProvider.kt | 9 +- .../push/providers/firebase/FirebaseStore.kt | 43 ++++++++ .../firebase/FirebaseTroubleshooter.kt | 79 +++++++++++++++ .../providers/firebase/GoogleFcmHelper.kt | 98 ------------------- .../VectorFirebaseMessagingService.kt | 6 +- .../unifiedpush/UnifiedPushProvider.kt | 4 + 8 files changed, 148 insertions(+), 108 deletions(-) rename libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/{FirebaseSetPusher.kt => FirebaseNewTokenHandler.kt} (84%) create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt create mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt delete mode 100644 libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 6476e4f815..854ae43522 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -28,4 +28,9 @@ interface PushProvider { val index: Int fun getDistributorNames(): List suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) + + /** + * Attempt to troubleshoot the push provider + */ + suspend fun troubleshoot(): Result } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt similarity index 84% rename from libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt rename to libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt index ea6937551a..01b93f3d95 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseSetPusher.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -26,16 +26,20 @@ import io.element.android.libraries.sessionstorage.api.toUserList import timber.log.Timber import javax.inject.Inject -private val loggerTag = LoggerTag("FirebaseSetPusher") +private val loggerTag = LoggerTag("FirebaseNewTokenHandler") -// TODO Rename -class FirebaseSetPusher @Inject constructor( +/** + * Handle new token receive from Firebase. Will update all the sessions which are using Firebase as a push provider. + */ +class FirebaseNewTokenHandler @Inject constructor( private val pusherSubscriber: PusherSubscriber, private val sessionStore: SessionStore, private val userPushStoreFactory: UserPushStoreFactory, private val matrixAuthenticationService: MatrixAuthenticationService, + private val firebaseStore: FirebaseStore, ) { - suspend fun onNewFirebaseToken(firebaseToken: String) { + suspend fun handle(firebaseToken: String) { + firebaseStore.storeFcmToken(firebaseToken) // Register the pusher for all the sessions sessionStore.getAllSessions().toUserList().forEach { userId -> val userDataStore = userPushStoreFactory.create(userId) diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index 0e96fc4a26..86042ae4e3 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -26,7 +26,8 @@ import javax.inject.Inject private val loggerTag = LoggerTag("FirebasePushProvider") class FirebasePushProvider @Inject constructor( - private val googleFcmHelper: GoogleFcmHelper, + private val firebaseStore: FirebaseStore, + private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, ) : PushProvider { override val index = 0 @@ -37,9 +38,13 @@ class FirebasePushProvider @Inject constructor( } override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { - val pushKey = googleFcmHelper.getFcmToken() ?: return Unit.also { + val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") } pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) } + + override suspend fun troubleshoot(): Result { + return firebaseTroubleshooter.troubleshoot() + } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt new file mode 100644 index 0000000000..f25ce08bc7 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseStore.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.firebase + +import android.content.SharedPreferences +import androidx.core.content.edit +import io.element.android.libraries.di.DefaultPreferences +import javax.inject.Inject + +/** + * This class store the Firebase token in SharedPrefs. + */ +class FirebaseStore @Inject constructor( + @DefaultPreferences private val sharedPrefs: SharedPreferences, +) { + fun getFcmToken(): String? { + return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) + } + + fun storeFcmToken(token: String?) { + sharedPrefs.edit { + putString(PREFS_KEY_FCM_TOKEN, token) + } + } + + companion object { + private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt new file mode 100644 index 0000000000..9fb9b5708d --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseTroubleshooter.kt @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.firebase + +import android.content.Context +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.GoogleApiAvailability +import com.google.firebase.messaging.FirebaseMessaging +import io.element.android.libraries.di.ApplicationContext +import timber.log.Timber +import javax.inject.Inject +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine + +/** + * This class force retrieving and storage of the Firebase token. + */ +class FirebaseTroubleshooter @Inject constructor( + @ApplicationContext private val context: Context, + private val newTokenHandler: FirebaseNewTokenHandler, +) { + suspend fun troubleshoot(): Result { + return runCatching { + val token = retrievedFirebaseToken() + newTokenHandler.handle(token) + } + } + + private suspend fun retrievedFirebaseToken(): String { + return suspendCoroutine { continuation -> + // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' + if (checkPlayServices(context)) { + try { + FirebaseMessaging.getInstance().token + .addOnSuccessListener { token -> + continuation.resume(token) + } + .addOnFailureListener { e -> + Timber.e(e, "## retrievedFirebaseToken() : failed") + continuation.resumeWithException(e) + } + } catch (e: Throwable) { + Timber.e(e, "## retrievedFirebaseToken() : failed") + continuation.resumeWithException(e) + } + } else { + val e = Exception("No valid Google Play Services found. Cannot use FCM.") + Timber.e(e) + continuation.resumeWithException(e) + } + } + } + + /** + * Check the device to make sure it has the Google Play Services APK. If + * it doesn't, display a dialog that allows users to download the APK from + * the Google Play Store or enable it in the device's system settings. + */ + private fun checkPlayServices(context: Context): Boolean { + val apiAvailability = GoogleApiAvailability.getInstance() + val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) + return resultCode == ConnectionResult.SUCCESS + } +} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt deleted file mode 100644 index 0772ac0603..0000000000 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/GoogleFcmHelper.kt +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.push.providers.firebase - -import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit -import com.google.android.gms.common.ConnectionResult -import com.google.android.gms.common.GoogleApiAvailability -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.DefaultPreferences -import javax.inject.Inject - -/** - * This class store the FCM token in SharedPrefs and ensure this token is retrieved. - * It has an alter ego in the fdroid variant. - */ -// TODO Rename to store? -class GoogleFcmHelper @Inject constructor( - @ApplicationContext private val context: Context, - @DefaultPreferences private val sharedPrefs: SharedPreferences, -) { - fun getFcmToken(): String? { - return sharedPrefs.getString(PREFS_KEY_FCM_TOKEN, null) - } - - fun storeFcmToken(token: String?) { - sharedPrefs.edit { - putString(PREFS_KEY_FCM_TOKEN, token) - } - } - - /* - override fun ensureFcmTokenIsRetrieved(pushersManager: PushersManager, registerPusher: Boolean) { - // 'app should always check the device for a compatible Google Play services APK before accessing Google Play services features' - if (checkPlayServices(context)) { - try { - FirebaseMessaging.getInstance().token - .addOnSuccessListener { token -> - storeFcmToken(token) - if (registerPusher) { - runBlocking {// TODO - pushersManager.enqueueRegisterPusherWithFcmKey(token) - } - } - } - .addOnFailureListener { e -> - Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") - } - } catch (e: Throwable) { - Timber.e(e, "## ensureFcmTokenIsRetrieved() : failed") - } - } else { - Toast.makeText(context, R.string.push_no_valid_google_play_services_apk_android, Toast.LENGTH_SHORT).show() - Timber.e("No valid Google Play Services found. Cannot use FCM.") - } - } - */ - - /** - * Check the device to make sure it has the Google Play Services APK. If - * it doesn't, display a dialog that allows users to download the APK from - * the Google Play Store or enable it in the device's system settings. - */ - private fun checkPlayServices(context: Context): Boolean { - val apiAvailability = GoogleApiAvailability.getInstance() - val resultCode = apiAvailability.isGooglePlayServicesAvailable(context) - return resultCode == ConnectionResult.SUCCESS - } - - /* - override fun onEnterForeground(activeSessionHolder: ActiveSessionHolder) { - // No op - } - - override fun onEnterBackground(activeSessionHolder: ActiveSessionHolder) { - // No op - } - */ - - companion object { - private const val PREFS_KEY_FCM_TOKEN = "FCM_TOKEN" - } -} diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt index 02bb84c541..35434ceb2e 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/VectorFirebaseMessagingService.kt @@ -30,10 +30,9 @@ import javax.inject.Inject private val loggerTag = LoggerTag("Firebase") class VectorFirebaseMessagingService : FirebaseMessagingService() { - @Inject lateinit var firebaseSetPusher: FirebaseSetPusher + @Inject lateinit var firebaseNewTokenHandler: FirebaseNewTokenHandler @Inject lateinit var pushParser: FirebasePushParser @Inject lateinit var pushHandler: PushHandler - @Inject lateinit var googleFcmHelper: GoogleFcmHelper private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -44,9 +43,8 @@ class VectorFirebaseMessagingService : FirebaseMessagingService() { override fun onNewToken(token: String) { Timber.tag(loggerTag.value).d("New Firebase token") - googleFcmHelper.storeFcmToken(token) coroutineScope.launch { - firebaseSetPusher.onNewFirebaseToken(token) + firebaseNewTokenHandler.handle(token) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt index dcf629fe98..e6d402b8d2 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -30,4 +30,8 @@ class UnifiedPushProvider @Inject constructor() : PushProvider { override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { TODO("Not yet implemented") } + + override suspend fun troubleshoot(): Result { + TODO("Not yet implemented") + } } From 1f87e10376f18b4d7218cf9ede94a5d37bc7f2d4 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 8 Apr 2023 00:23:30 +0200 Subject: [PATCH 19/83] Cleanup, Firebase dep. is not necessary here. --- app/build.gradle.kts | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b8833efbb..bfd69aea3d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -225,9 +225,6 @@ dependencies { implementation(platform(libs.network.okhttp.bom)) implementation("com.squareup.okhttp3:logging-interceptor") - implementation(platform(libs.google.firebase.bom)) - implementation("com.google.firebase:firebase-messaging-ktx") - implementation(libs.dagger) kapt(libs.dagger.compiler) From adfcd61287dd58d0ec29cf0a07a3f2a51e4266ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Sat, 8 Apr 2023 00:28:13 +0200 Subject: [PATCH 20/83] Safer code --- .../io/element/android/appnav/loggedin/LoggedInPresenter.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index 552420abf8..f548771717 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -48,7 +48,8 @@ class LoggedInPresenter @Inject constructor( // Ensure pusher is registered // TODO Register with Firebase for now val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect - pushService.registerWith(matrixClient, pushProvider, pushProvider.getDistributorNames().first()) + val distributor = pushProvider.getDistributorNames().firstOrNull() ?: return@LaunchedEffect + pushService.registerWith(matrixClient, pushProvider, distributor) } val permissionsState = postNotificationPermissionsPresenter.present() From 95bafe4059c936fea6b48515de9f3e477f50878c Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 11:19:44 +0200 Subject: [PATCH 21/83] UnifiedPush WIP --- app/build.gradle.kts | 1 + .../appnav/loggedin/LoggedInPresenter.kt | 6 +- .../appnav/loggedin/LoggedInPresenterTest.kt | 3 +- .../matrix/api/pusher/PushersService.kt | 1 + .../matrix/impl/pushers/RustPushersService.kt | 5 + .../matrix/test/pushers/FakePushersService.kt | 1 + .../android/libraries/push/api/PushService.kt | 3 +- .../libraries/push/impl/DefaultPushService.kt | 23 ++- .../libraries/push/impl/PushersManager.kt | 5 +- .../push/providers/api/Distributor.kt | 22 +++ .../push/providers/api/PushProvider.kt | 19 ++- .../push/providers/api/PusherSubscriber.kt | 1 + .../pushproviders/firebase/build.gradle.kts | 2 +- .../push/providers/firebase/FirebaseConfig.kt | 3 +- .../firebase/FirebaseNewTokenHandler.kt | 2 +- .../firebase/FirebasePushProvider.kt | 18 ++- .../unifiedpush/build.gradle.kts | 7 + .../unifiedpush/PushDataUnifiedPush.kt | 4 +- .../unifiedpush/RegisterUnifiedPushUseCase.kt | 29 ++-- .../unifiedpush/UnifiedPushConfig.kt | 3 +- .../unifiedpush/UnifiedPushHelper.kt | 138 +++--------------- .../UnifiedPushNewGatewayHandler.kt | 53 +++++++ .../unifiedpush/UnifiedPushParser.kt | 4 +- .../unifiedpush/UnifiedPushProvider.kt | 34 ++++- .../UnregisterUnifiedPushUseCase.kt | 18 ++- .../VectorUnifiedPushMessagingReceiver.kt | 64 ++++---- .../unifiedpush/network/DiscoveryResponse.kt | 25 ++++ .../network/DiscoveryUnifiedPush.kt | 25 ++++ .../unifiedpush/network/UnifiedPushApi.kt | 24 +++ .../unifiedpush/UnifiedPushParserTest.kt | 29 ++-- .../libraries/pushstore/api/UserPushStore.kt | 7 +- .../pushstore/impl/UserPushStoreDataStore.kt | 10 +- .../kotlin/extension/DependencyHandleScope.kt | 2 +- 33 files changed, 376 insertions(+), 215 deletions(-) create mode 100644 libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bfd69aea3d..74ebfa89d4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,6 +33,7 @@ plugins { id("com.google.firebase.appdistribution") version "4.0.0" id("org.jetbrains.kotlinx.knit") version "0.4.0" id("kotlin-parcelize") + // TODO Move the plugin to the firebase module? id("com.google.gms.google-services") } diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index f548771717..a798f95bae 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -46,9 +46,9 @@ class LoggedInPresenter @Inject constructor( override fun present(): LoggedInState { LaunchedEffect(Unit) { // Ensure pusher is registered - // TODO Register with Firebase for now - val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect - val distributor = pushProvider.getDistributorNames().firstOrNull() ?: return@LaunchedEffect + // TODO Manually select push provider for now + val pushProvider = pushService.getAvailablePushProviders().find { it.name == "UnifiedPush" } ?: return@LaunchedEffect + val distributor = pushProvider.getDistributors().first() pushService.registerWith(matrixClient, pushProvider, distributor) } diff --git a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt index 595f4850b2..71d303150b 100644 --- a/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt +++ b/appnav/src/test/kotlin/io/element/android/appnav/loggedin/LoggedInPresenterTest.kt @@ -27,6 +27,7 @@ import io.element.android.libraries.matrix.test.FakeMatrixClient import io.element.android.libraries.permissions.api.PermissionsPresenter import io.element.android.libraries.permissions.noop.NoopPermissionsPresenter import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -60,7 +61,7 @@ class LoggedInPresenterTest { return emptyList() } - override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { } override suspend fun testPush() { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt index ef2291f8ce..71a642965f 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/pusher/PushersService.kt @@ -18,4 +18,5 @@ package io.element.android.libraries.matrix.api.pusher interface PushersService { suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData): Result + suspend fun unsetHttpPusher(): Result } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt index 4eaafef12d..60ca4df311 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/pushers/RustPushersService.kt @@ -53,4 +53,9 @@ class RustPushersService( } } } + + override suspend fun unsetHttpPusher(): Result { + // TODO Missing client API. We need to set the pusher with Kind == null, but we do not have access to this field from the SDK. + return Result.success(Unit) + } } diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt index 77087d132f..6ff7e4a20b 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/pushers/FakePushersService.kt @@ -21,4 +21,5 @@ import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData class FakePushersService : PushersService { override suspend fun setHttpPusher(setHttpPusherData: SetHttpPusherData) = Result.success(Unit) + override suspend fun unsetHttpPusher(): Result = Result.success(Unit) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt index 7eeca6e0a3..83504e7a8a 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/PushService.kt @@ -17,6 +17,7 @@ package io.element.android.libraries.push.api import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider interface PushService { @@ -30,7 +31,7 @@ interface PushService { * * The method has effect only if the [PushProvider] is different than the current one. */ - suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) + suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) // TODO Move away suspend fun testPush() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index 1d9397d9a9..d1e266357b 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -20,14 +20,19 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.api.PushService +import io.element.android.libraries.push.impl.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.pushstore.api.UserPushStoreFactory import javax.inject.Inject @ContributesBinding(AppScope::class) class DefaultPushService @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val pushersManager: PushersManager, + private val pushClientSecret: PushClientSecret, + private val userPushStoreFactory: UserPushStoreFactory, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, ) : PushService { override fun notificationStyleChanged() { @@ -38,10 +43,22 @@ class DefaultPushService @Inject constructor( return pushProviders.sortedBy { it.index } } - override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributorName: String) { - // TODO Get current push provider, compare with provided one, then unregister and register if different, and store change + /** + * Get current push provider, compare with provided one, then unregister and register if different, and store change + */ + override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { + val userPushStore = userPushStoreFactory.create(matrixClient.sessionId.value) + val currentPushProviderName = userPushStore.getPushProviderName() + if (currentPushProviderName != pushProvider.name) { + // Unregister previous one if any + pushProviders.find { it.name == currentPushProviderName }?.unregister(matrixClient) + } - pushProvider.registerWith(matrixClient, distributorName) + val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) + pushProvider.registerWith(matrixClient, distributor, clientSecret) + + // Store new value + userPushStore.setPushProviderName(pushProvider.name) } override suspend fun testPush() { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 532a904c82..eb3157adff 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -148,9 +148,8 @@ class PushersManager @Inject constructor( // currentSession.pushersService().removeEmailPusher(email) } - suspend fun unregisterPusher(pushKey: String) { - // val currentSession = activeSessionHolder.getSafeActiveSession() ?: return - // currentSession.pushersService().removeHttpPusher(pushKey, PushConfig.pusher_app_id) + override suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { + matrixClient.pushersService().unsetHttpPusher() } companion object { diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt new file mode 100644 index 0000000000..3d4d0add28 --- /dev/null +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/Distributor.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.api + +data class Distributor( + val value: String, + val name: String, +) diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 854ae43522..92246851ec 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -26,8 +26,23 @@ interface PushProvider { * Allow to sort provider, from lower index to higher index */ val index: Int - fun getDistributorNames(): List - suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) + + /** + * User friendly name. + */ + val name: String + + fun getDistributors(): List + + /** + * Register the pusher to the homeserver + */ + suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) + + /** + * Unregister the pusher + */ + suspend fun unregister(matrixClient: MatrixClient) /** * Attempt to troubleshoot the push provider diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt index 805244e0ed..0bf0f949f3 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PusherSubscriber.kt @@ -20,4 +20,5 @@ import io.element.android.libraries.matrix.api.MatrixClient interface PusherSubscriber { suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) + suspend fun unregisterPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) } diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index 65360d5465..eabc70305b 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -40,7 +40,7 @@ dependencies { implementation(projects.libraries.pushproviders.api) implementation(platform(libs.google.firebase.bom)) - implementation("com.google.firebase:firebase-messaging-ktx") + api("com.google.firebase:firebase-messaging-ktx") testImplementation(libs.test.junit) testImplementation(libs.test.truth) diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt index 27463825d6..bf35a1b18a 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseConfig.kt @@ -23,5 +23,6 @@ object FirebaseConfig { */ const val pusher_http_url: String = "https://matrix.org/_matrix/push/v1/notify" - const val internalName = "NOTIFICATION_METHOD_FIREBASE" + const val index = 0 + const val name = "Firebase" } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt index 01b93f3d95..2c987c75e1 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -43,7 +43,7 @@ class FirebaseNewTokenHandler @Inject constructor( // Register the pusher for all the sessions sessionStore.getAllSessions().toUserList().forEach { userId -> val userDataStore = userPushStoreFactory.create(userId) - if (userDataStore.getNotificationMethod() == FirebaseConfig.internalName) { + if (userDataStore.getPushProviderName() == FirebaseConfig.name) { matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index 86042ae4e3..f30e031bab 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.push.providers.firebase import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider import io.element.android.libraries.push.providers.api.PusherSubscriber import timber.log.Timber @@ -30,20 +31,27 @@ class FirebasePushProvider @Inject constructor( private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, ) : PushProvider { - override val index = 0 + override val index = FirebaseConfig.index + override val name = FirebaseConfig.name - override fun getDistributorNames(): List { - // Must return an non-empty list for now - return listOf("unused") + override fun getDistributors(): List { + return listOf(Distributor("Firebase", "Firebase")) } - override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) { val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") } pusherSubscriber.registerPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) } + override suspend fun unregister(matrixClient: MatrixClient) { + val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { + Timber.tag(loggerTag.value).w("Unable to unregister pusher, Firebase token is not known.") + } + pusherSubscriber.unregisterPusher(matrixClient, pushKey, FirebaseConfig.pusher_http_url) + } + override suspend fun troubleshoot(): Result { return firebaseTroubleshooter.troubleshoot() } diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index 6817d0aad6..7dcd773c25 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -30,14 +30,21 @@ anvil { dependencies { implementation(libs.dagger) + implementation(projects.libraries.androidutils) implementation(projects.libraries.core) implementation(projects.libraries.matrix.api) + implementation(projects.libraries.pushstore.api) implementation(projects.libraries.pushproviders.api) implementation(projects.libraries.architecture) implementation(projects.libraries.core) implementation(projects.services.toolbox.api) + implementation(projects.libraries.network) + implementation(platform(libs.network.okhttp.bom)) + implementation("com.squareup.okhttp3:okhttp") + implementation(libs.network.retrofit) + implementation(libs.serialization.json) // UnifiedPush library diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt index 6d5ecb1db3..618a3c989f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/PushDataUnifiedPush.kt @@ -55,13 +55,13 @@ data class PushDataUnifiedPushCounts( @SerialName("unread") val unread: Int? = null ) -fun PushDataUnifiedPush.toPushData(): PushData? { +fun PushDataUnifiedPush.toPushData(clientSecret: String): PushData? { val safeEventId = notification?.eventId?.asEventId() ?: return null val safeRoomId = notification.roomId?.asRoomId() ?: return null return PushData( eventId = safeEventId, roomId = safeRoomId, unread = notification.counts?.unread, - clientSecret = null // TODO EAx check how client secret will be sent through UnifiedPush + clientSecret = clientSecret ) } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt index a80d9ba865..813f8e97ca 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -18,45 +18,56 @@ package io.element.android.libraries.push.providers.unifiedpush import android.content.Context import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.Distributor +import io.element.android.libraries.push.providers.api.PusherSubscriber import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject class RegisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, + private val pusherSubscriber: PusherSubscriber, + private val unifiedPushStore: UnifiedPushStore, ) { sealed interface RegisterUnifiedPushResult { object Success : RegisterUnifiedPushResult object NeedToAskUserForDistributor : RegisterUnifiedPushResult + object Error : RegisterUnifiedPushResult } - fun execute(distributor: String = ""): RegisterUnifiedPushResult { - if (distributor.isNotEmpty()) { - saveAndRegisterApp(distributor) + suspend fun execute(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String): RegisterUnifiedPushResult { + val distributorValue = distributor.value + if (distributorValue.isNotEmpty()) { + saveAndRegisterApp(distributorValue, clientSecret) + val endpoint = unifiedPushStore.getEndpoint() ?: return RegisterUnifiedPushResult.Error + val gateway = unifiedPushStore.getPushGateway() ?: return RegisterUnifiedPushResult.Error + pusherSubscriber.registerPusher(matrixClient, endpoint, gateway) return RegisterUnifiedPushResult.Success } + // TODO Below should never happen? if (UnifiedPush.getDistributor(context).isNotEmpty()) { - registerApp() + registerApp(clientSecret) return RegisterUnifiedPushResult.Success } val distributors = UnifiedPush.getDistributors(context) return if (distributors.size == 1) { - saveAndRegisterApp(distributors.first()) + saveAndRegisterApp(distributors.first(), clientSecret) RegisterUnifiedPushResult.Success } else { RegisterUnifiedPushResult.NeedToAskUserForDistributor } } - private fun saveAndRegisterApp(distributor: String) { + private fun saveAndRegisterApp(distributor: String, clientSecret: String) { UnifiedPush.saveDistributor(context, distributor) - registerApp() + registerApp(clientSecret) } - private fun registerApp() { - UnifiedPush.registerApp(context) + private fun registerApp(clientSecret: String) { + UnifiedPush.registerApp(context = context, instance = clientSecret) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt index 73c31f430c..21b4ca9a76 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushConfig.kt @@ -23,5 +23,6 @@ object UnifiedPushConfig { */ const val default_push_gateway_http_url: String = "https://matrix.gateway.unifiedpush.org/_matrix/push/v1/notify" - const val internalName = "NOTIFICATION_METHOD_UNIFIEDPUSH" + const val index = 1 + const val name = "UnifiedPush" } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt index dce17015b7..6c17c16c67 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt @@ -17,11 +17,13 @@ package io.element.android.libraries.push.providers.unifiedpush import android.content.Context +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.ApplicationContext +import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi import io.element.android.services.toolbox.api.strings.StringProvider -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import org.unifiedpush.android.connector.UnifiedPush +import kotlinx.coroutines.withContext import timber.log.Timber import java.net.URL import javax.inject.Inject @@ -30,132 +32,34 @@ class UnifiedPushHelper @Inject constructor( @ApplicationContext private val context: Context, private val unifiedPushStore: UnifiedPushStore, private val stringProvider: StringProvider, + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: CoroutineDispatchers, ) { - - /* TODO EAx - @MainThread - fun showSelectDistributorDialog( - context: Context, - onDistributorSelected: (String) -> Unit, - ) { - val internalDistributorName = stringProvider.getString( - if (fcmHelper.isFirebaseAvailable()) { - R.string.push_distributor_firebase_android - } else { - R.string.push_distributor_background_sync_android - } - ) - - val distributors = UnifiedPush.getDistributors(context) - val distributorsName = distributors.map { - if (it == context.packageName) { - internalDistributorName - } else { - context.getApplicationLabel(it) - } - } - - MaterialAlertDialogBuilder(context) - .setTitle(stringProvider.getString(R.string.push_choose_distributor_dialog_title_android)) - .setItems(distributorsName.toTypedArray()) { _, which -> - val distributor = distributors[which] - onDistributorSelected(distributor) - } - .setOnCancelListener { - // we do not want to change the distributor on behalf of the user - if (UnifiedPush.getDistributor(context).isEmpty()) { - // By default, use internal solution (fcm/background sync) - onDistributorSelected(context.packageName) - } - } - .setCancelable(true) - .show() - } - - */ - - @Serializable - internal data class DiscoveryResponse( - @SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() - ) - - @Serializable - internal data class DiscoveryUnifiedPush( - @SerialName("gateway") val gateway: String = "" - ) - - suspend fun storeCustomOrDefaultGateway( - endpoint: String, - onDoneRunnable: Runnable? = null - ) { - // if we use the embedded distributor, - // register app_id type upfcm on sygnal - // the pushkey if FCM key - /* - if (UnifiedPush.getDistributor(context) == context.packageName) { - unifiedPushStore.storePushGateway(PushConfig.pusher_http_url) - onDoneRunnable?.run() - return - } - - */ - /* TODO EAx UnifiedPush - // else, unifiedpush, and pushkey is an endpoint - val gateway = PushConfig.default_push_gateway_http_url + suspend fun storeCustomOrDefaultGateway(endpoint: String) { + val gateway = UnifiedPushConfig.default_push_gateway_http_url val parsed = URL(endpoint) val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" Timber.i("Testing $custom") try { - val response = matrix.rawService().getUrl(custom, CacheStrategy.NoCache) - tryOrNull { Json.decodeFromString(response) } - ?.let { discoveryResponse -> - if (discoveryResponse.unifiedpush.gateway == "matrix") { - Timber.d("Using custom gateway") - unifiedPushStore.storePushGateway(custom) - onDoneRunnable?.run() - return + withContext(coroutineDispatchers.io) { + val api = retrofitFactory.create("${parsed.protocol}://${parsed.host}") + .create(UnifiedPushApi::class.java) + tryOrNull { api.discover() } + ?.let { discoveryResponse -> + if (discoveryResponse.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + unifiedPushStore.storePushGateway(custom) + } } - } + } + return } catch (e: Throwable) { Timber.d(e, "Cannot try custom gateway") } unifiedPushStore.storePushGateway(gateway) - onDoneRunnable?.run() - - */ } - fun getExternalDistributors(): List { - return UnifiedPush.getDistributors(context) - .filterNot { it == context.packageName } - } - - fun getCurrentDistributorName(): String { - TODO() - /* - return when { - isEmbeddedDistributor() -> stringProvider.getString(R.string.push_distributor_firebase_android) - isBackgroundSync() -> stringProvider.getString(R.string.push_distributor_background_sync_android) - else -> context.getApplicationLabel(UnifiedPush.getDistributor(context)) - } - - */ - } - - fun isEmbeddedDistributor(): Boolean { - TODO() - //return isInternalDistributor() && fcmHelper.isFirebaseAvailable() - } - - fun isBackgroundSync(): Boolean { - TODO() - //return isInternalDistributor() && !fcmHelper.isFirebaseAvailable() - } - - private fun isInternalDistributor(): Boolean { - return UnifiedPush.getDistributor(context).isEmpty() || - UnifiedPush.getDistributor(context) == context.packageName - } + private fun isEmbeddedDistributor() = false fun getPrivacyFriendlyUpEndpoint(): String? { val endpoint = getEndpointOrToken() diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt new file mode 100644 index 0000000000..b02fab04ea --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.unifiedpush + +import io.element.android.libraries.core.log.logger.LoggerTag +import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.sessionstorage.api.toUserList +import timber.log.Timber +import javax.inject.Inject + +private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler") + +/** + * Handle new endpoint received from UnifiedPush. Will update all the sessions which are using UnifiedPush as a push provider. + */ +class UnifiedPushNewGatewayHandler @Inject constructor( + private val pusherSubscriber: PusherSubscriber, + private val sessionStore: SessionStore, + private val userPushStoreFactory: UserPushStoreFactory, + private val matrixAuthenticationService: MatrixAuthenticationService, +) { + suspend fun handle(endpoint: String, pushGateway: String) { + // Register the pusher for all the sessions which are using UnifiedPush. + sessionStore.getAllSessions().toUserList().forEach { userId -> + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) { + matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> + pusherSubscriber.registerPusher(client, endpoint, pushGateway) + } + } else { + Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher") + } + } + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt index 881862d473..6169e1f8eb 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParser.kt @@ -25,7 +25,7 @@ import javax.inject.Inject class UnifiedPushParser @Inject constructor() { private val json by lazy { Json { ignoreUnknownKeys = true } } - fun parse(message: ByteArray): PushData? { - return tryOrNull { json.decodeFromString(String(message)) }?.toPushData() + fun parse(message: ByteArray, clientSecret: String): PushData? { + return tryOrNull { json.decodeFromString(String(message)) }?.toPushData(clientSecret) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt index e6d402b8d2..c12c54c0a3 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -16,19 +16,41 @@ package io.element.android.libraries.push.providers.unifiedpush +import android.content.Context +import io.element.android.libraries.androidutils.system.getApplicationLabel +import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient +import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider +import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject -class UnifiedPushProvider @Inject constructor() : PushProvider { - override val index = 1 +class UnifiedPushProvider @Inject constructor( + @ApplicationContext private val context: Context, + private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, + private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, +) : PushProvider { + override val index = UnifiedPushConfig.index + override val name = UnifiedPushConfig.name - override fun getDistributorNames(): List { - TODO("Not yet implemented") + override fun getDistributors(): List { + val distributors = UnifiedPush.getDistributors(context) + return distributors.mapNotNull { + if (it == context.packageName) { + // Exclude self + null + } else { + Distributor(it, context.getApplicationLabel(it)) + } + } } - override suspend fun registerWith(matrixClient: MatrixClient, distributorName: String) { - TODO("Not yet implemented") + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) { + registerUnifiedPushUseCase.execute(matrixClient, distributor, clientSecret) + } + + override suspend fun unregister(matrixClient: MatrixClient) { + unRegisterUnifiedPushUseCase.execute() } override suspend fun troubleshoot(): Result { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt index b7cd592951..750eb95f83 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -16,21 +16,26 @@ package io.element.android.libraries.push.providers.unifiedpush -/* +import android.content.Context +import io.element.android.libraries.di.ApplicationContext +import org.unifiedpush.android.connector.UnifiedPush +import timber.log.Timber +import javax.inject.Inject + class UnregisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, - private val pushDataStore: PushDataStore, + //private val pushDataStore: PushDataStore, private val unifiedPushStore: UnifiedPushStore, private val unifiedPushHelper: UnifiedPushHelper, ) { - suspend fun execute(pushersManager: PushersManager?) { - val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME - pushDataStore.setFdroidSyncBackgroundMode(mode) + suspend fun execute(/*pushersManager: PushersManager?*/) { + //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME + //pushDataStore.setFdroidSyncBackgroundMode(mode) try { unifiedPushHelper.getEndpointOrToken()?.let { Timber.d("Removing $it") - pushersManager?.unregisterPusher(it) + // TODO pushersManager?.unregisterPusher(it) } } catch (e: Exception) { Timber.d(e, "Probably unregistering a non existing pusher") @@ -40,4 +45,3 @@ class UnregisterUnifiedPushUseCase @Inject constructor( UnifiedPush.unregisterApp(context) } } - */ diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index d745df6e87..0736badbc2 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -21,6 +21,7 @@ import android.content.Intent import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.push.providers.api.PushHandler +import io.element.android.libraries.push.providers.api.PusherSubscriber import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -32,22 +33,24 @@ private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver") class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var pushParser: UnifiedPushParser - // @Inject lateinit var pushDataStore: PushDataStore + + // @Inject lateinit var pushDataStore: PushDataStore @Inject lateinit var pushHandler: PushHandler @Inject lateinit var guardServiceStarter: GuardServiceStarter -// @Inject lateinit var unifiedPushStore: UnifiedPushStore -// @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var unifiedPushStore: UnifiedPushStore + @Inject lateinit var unifiedPushHelper: UnifiedPushHelper + @Inject lateinit var pusherSubscriber: PusherSubscriber + @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler private val coroutineScope = CoroutineScope(SupervisorJob()) override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) - // Inject context.applicationContext.bindings().inject(this) + super.onReceive(context, intent) } /** - * Called when message is received. + * Called when message is received. The message contains the full POST body of the push message. * * @param context the Android context * @param message the message @@ -56,7 +59,7 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { override fun onMessage(context: Context, message: ByteArray, instance: String) { Timber.tag(loggerTag.value).d("New message") coroutineScope.launch { - val pushData = pushParser.parse(message) + val pushData = pushParser.parse(message, instance) if (pushData == null) { Timber.tag(loggerTag.value).w("Invalid data received from UnifiedPush") } else { @@ -65,36 +68,36 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { } } + /** + * Called when a new endpoint is to be used for sending push messages. + * You should send the endpoint to your application server and sync for missing notifications. + * TODO use [instance] for multi-account + */ override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { - TODO() - /* Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") - if (pushDataStore.areNotificationEnabledForDevice() /* TODO EAx && activeSessionHolder.hasActiveSession() */) { - // If the endpoint has changed - // or the gateway has changed - if (unifiedPushHelper.getEndpointOrToken() != endpoint) { - unifiedPushStore.storeUpEndpoint(endpoint) - coroutineScope.launch { - unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) { - unifiedPushHelper.getPushGateway()?.let { - coroutineScope.launch { - pushersManager.onNewUnifiedPushEndpoint(endpoint, it) - } - } - } + // If the endpoint has changed + // or the gateway has changed + if (unifiedPushHelper.getEndpointOrToken() != endpoint) { + unifiedPushStore.storeUpEndpoint(endpoint) + coroutineScope.launch { + unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) + unifiedPushHelper.getPushGateway()?.let { pushGateway -> + newGatewayHandler.handle(endpoint, pushGateway) } - } else { - Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } + } else { + Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } - val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED - pushDataStore.setFdroidSyncBackgroundMode(mode) + //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED + //pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.stop() - */ } + /** + * Called when the registration is not possible, eg. no network. + */ override fun onRegistrationFailed(context: Context, instance: String) { - TODO() + Timber.tag(loggerTag.value).e("onRegistrationFailed for $instance") /* Toast.makeText(context, "Push service registration failed", Toast.LENGTH_SHORT).show() val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME @@ -103,10 +106,13 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { */ } + /** + * Called when this application is unregistered from receiving push messages. + */ override fun onUnregistered(context: Context, instance: String) { + Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") TODO() /* - Timber.tag(loggerTag.value).d("Unifiedpush: Unregistered") val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.start() diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt new file mode 100644 index 0000000000..b961da1285 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryResponse.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.unifiedpush.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DiscoveryResponse( + @SerialName("unifiedpush") val unifiedpush: DiscoveryUnifiedPush = DiscoveryUnifiedPush() +) diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt new file mode 100644 index 0000000000..b4c7345fd7 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/DiscoveryUnifiedPush.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.unifiedpush.network + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class DiscoveryUnifiedPush( + @SerialName("gateway") val gateway: String = "" +) diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt new file mode 100644 index 0000000000..e384b8353b --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/network/UnifiedPushApi.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.unifiedpush.network + +import retrofit2.http.GET + +interface UnifiedPushApi { + @GET("_matrix/push/v1/notify") + suspend fun discover(): DiscoveryResponse +} diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt index 6b5c0db62f..b18be39e07 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt @@ -20,56 +20,65 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.push.providers.api.PushData +import org.junit.Assert.assertThrows import org.junit.Test class UnifiedPushParserTest { + private val aClientSecret = "a-client-secret" private val validData = PushData( eventId = AN_EVENT_ID, roomId = A_ROOM_ID, unread = 1, - // TODO handle client secret here. - clientSecret = null + clientSecret = aClientSecret ) @Test fun `test edge cases UnifiedPush`() { val pushParser = UnifiedPushParser() // Empty string - assertThat(pushParser.parse("".toByteArray())).isNull() + assertThat(pushParser.parse("".toByteArray(), aClientSecret)).isNull() // Empty Json - assertThat(pushParser.parse("{}".toByteArray())).isNull() + assertThat(pushParser.parse("{}".toByteArray(), aClientSecret)).isNull() // Bad Json - assertThat(pushParser.parse("ABC".toByteArray())).isNull() + assertThat(pushParser.parse("ABC".toByteArray(), aClientSecret)).isNull() } @Test fun `test UnifiedPush format`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray())).isEqualTo(validData) + assertThat(pushParser.parse(UNIFIED_PUSH_DATA.toByteArray(), aClientSecret)).isEqualTo(validData) } @Test fun `test empty roomId`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray())).isNull() + assertThrows(IllegalStateException::class.java) { + pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret) + } } @Test fun `test invalid roomId`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"))).isNull() + assertThrows(IllegalStateException::class.java) { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret) + } } @Test fun `test empty eventId`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""))).isNull() + assertThrows(IllegalStateException::class.java) { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret) + } } @Test fun `test invalid eventId`() { val pushParser = UnifiedPushParser() - assertThat(pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"))).isNull() + assertThrows(IllegalStateException::class.java) { + pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret) + } } companion object { diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 35ec23f80d..6817199e13 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -20,12 +20,9 @@ package io.element.android.libraries.pushstore.api * Store data related to push about a user. */ interface UserPushStore { - /** - * [NOTIFICATION_METHOD_FIREBASE] or [NOTIFICATION_METHOD_UNIFIEDPUSH]. - */ - suspend fun getNotificationMethod(): String? + suspend fun getPushProviderName(): String? - suspend fun setNotificationMethod(value: String) + suspend fun setPushProviderName(value: String) suspend fun getCurrentRegisteredPushKey(): String? diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 8b37056768..0e4e668eda 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -33,16 +33,16 @@ class UserPushStoreDataStore( userId: String, ) : UserPushStore { private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store_$userId") - private val notificationMethod = stringPreferencesKey("notificationMethod") + private val pushProviderName = stringPreferencesKey("pushProviderName") private val currentPushKey = stringPreferencesKey("currentPushKey") - override suspend fun getNotificationMethod(): String? { - return context.dataStore.data.first()[notificationMethod] + override suspend fun getPushProviderName(): String? { + return context.dataStore.data.first()[pushProviderName] } - override suspend fun setNotificationMethod(value: String) { + override suspend fun setPushProviderName(value: String) { context.dataStore.edit { - it[notificationMethod] = value + it[pushProviderName] = value } } diff --git a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt index 368747961d..1427269755 100644 --- a/plugins/src/main/kotlin/extension/DependencyHandleScope.kt +++ b/plugins/src/main/kotlin/extension/DependencyHandleScope.kt @@ -79,7 +79,7 @@ fun DependencyHandlerScope.allLibrariesImpl() { // Comment to not include firebase in the project implementation(project(":libraries:pushproviders:firebase")) // Comment to not include unified push in the project - // implementation(project(":libraries:pushproviders:unifiedpush")) + implementation(project(":libraries:pushproviders:unifiedpush")) implementation(project(":libraries:pushstore:impl")) implementation(project(":libraries:architecture")) implementation(project(":libraries:dateformatter:impl")) From 80268156b59b7a77efd201e738ef39b168fe54e1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 16:29:31 +0200 Subject: [PATCH 22/83] UnifiedPush WIP --- .../libraries/push/impl/DefaultPushService.kt | 10 +-- .../libraries/push/impl/PushersManager.kt | 4 +- .../push/impl/push/DefaultPushHandler.kt | 2 +- .../push/providers/api/PushProvider.kt | 2 +- .../firebase/FirebaseNewTokenHandler.kt | 20 +++-- .../firebase/FirebasePushProvider.kt | 4 +- .../unifiedpush/RegisterUnifiedPushUseCase.kt | 4 +- .../unifiedpush/UnifiedPushGatewayResolver.kt | 56 ++++++++++++ .../unifiedpush/UnifiedPushHelper.kt | 89 ------------------- .../UnifiedPushNewGatewayHandler.kt | 27 +++--- .../unifiedpush/UnifiedPushProvider.kt | 8 +- .../providers/unifiedpush/UnifiedPushStore.kt | 19 ++-- .../UnregisterUnifiedPushUseCase.kt | 10 +-- .../VectorUnifiedPushMessagingReceiver.kt | 20 ++--- libraries/pushstore/api/build.gradle.kts | 1 + .../pushstore/api/UserPushStoreFactory.kt | 4 +- .../api}/clientsecret/PushClientSecret.kt | 2 +- .../clientsecret/PushClientSecretFactory.kt | 2 +- .../clientsecret/PushClientSecretStore.kt | 2 +- libraries/pushstore/impl/build.gradle.kts | 9 ++ .../impl/DefaultUserPushStoreFactory.kt | 8 +- .../pushstore/impl/UserPushStoreDataStore.kt | 3 +- .../PushClientSecretFactoryImpl.kt | 3 +- .../impl/clientsecret/PushClientSecretImpl.kt | 5 +- .../PushClientSecretStoreDataStore.kt | 3 +- .../FakePushClientSecretFactory.kt | 4 +- .../InMemoryPushClientSecretStore.kt | 3 +- .../clientsecret/PushClientSecretImplTest.kt | 2 +- 28 files changed, 155 insertions(+), 171 deletions(-) create mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt delete mode 100644 libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api}/clientsecret/PushClientSecret.kt (94%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api}/clientsecret/PushClientSecretFactory.kt (91%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push/impl => pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api}/clientsecret/PushClientSecretStore.kt (93%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/PushClientSecretFactoryImpl.kt (86%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/PushClientSecretImpl.kt (84%) rename libraries/{push/impl/src/main/kotlin/io/element/android/libraries/push => pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/PushClientSecretStoreDataStore.kt (94%) rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push => pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/FakePushClientSecretFactory.kt (85%) rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push => pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/InMemoryPushClientSecretStore.kt (89%) rename libraries/{push/impl/src/test/kotlin/io/element/android/libraries/push => pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore}/impl/clientsecret/PushClientSecretImplTest.kt (97%) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index d1e266357b..99af1317f9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -20,7 +20,7 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.api.PushService -import io.element.android.libraries.push.impl.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.notifications.NotificationDrawerManager import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider @@ -31,7 +31,6 @@ import javax.inject.Inject class DefaultPushService @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val pushersManager: PushersManager, - private val pushClientSecret: PushClientSecret, private val userPushStoreFactory: UserPushStoreFactory, private val pushProviders: Set<@JvmSuppressWildcards PushProvider>, ) : PushService { @@ -47,16 +46,13 @@ class DefaultPushService @Inject constructor( * Get current push provider, compare with provided one, then unregister and register if different, and store change */ override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { - val userPushStore = userPushStoreFactory.create(matrixClient.sessionId.value) + val userPushStore = userPushStoreFactory.create(matrixClient.sessionId) val currentPushProviderName = userPushStore.getPushProviderName() if (currentPushProviderName != pushProvider.name) { // Unregister previous one if any pushProviders.find { it.name == currentPushProviderName }?.unregister(matrixClient) } - - val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) - pushProvider.registerWith(matrixClient, distributor, clientSecret) - + pushProvider.registerWith(matrixClient, distributor) // Store new value userPushStore.setPushProviderName(pushProvider.name) } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index eb3157adff..33b8ac3507 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -24,7 +24,7 @@ import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData -import io.element.android.libraries.push.impl.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.config.PushConfig import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest @@ -79,7 +79,7 @@ class PushersManager @Inject constructor( * Register a pusher to the server if not done yet. */ override suspend fun registerPusher(matrixClient: MatrixClient, pushKey: String, gateway: String) { - val userDataStore = userPushStoreFactory.create(matrixClient.sessionId.value) + val userDataStore = userPushStoreFactory.create(matrixClient.sessionId) if (userDataStore.getCurrentRegisteredPushKey() == pushKey) { Timber.tag(loggerTag.value).d("Unnecessary to register again the same pusher") } else { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 30ee74f3de..7eef95fc67 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -30,7 +30,7 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.push.impl.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.notifications.NotificationActionIds diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 92246851ec..ebcdd962d6 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -37,7 +37,7 @@ interface PushProvider { /** * Register the pusher to the homeserver */ - suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) + suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) /** * Unregister the pusher diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt index 2c987c75e1..f26fcfae25 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -18,7 +18,7 @@ package io.element.android.libraries.push.providers.firebase import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.asSessionId import io.element.android.libraries.push.providers.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.sessionstorage.api.SessionStore @@ -41,15 +41,17 @@ class FirebaseNewTokenHandler @Inject constructor( suspend fun handle(firebaseToken: String) { firebaseStore.storeFcmToken(firebaseToken) // Register the pusher for all the sessions - sessionStore.getAllSessions().toUserList().forEach { userId -> - val userDataStore = userPushStoreFactory.create(userId) - if (userDataStore.getPushProviderName() == FirebaseConfig.name) { - matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> - pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) + sessionStore.getAllSessions().toUserList() + .map { it.asSessionId() } + .forEach { userId -> + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.getPushProviderName() == FirebaseConfig.name) { + matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client -> + pusherSubscriber.registerPusher(client, firebaseToken, FirebaseConfig.pusher_http_url) + } + } else { + Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") } - } else { - Timber.tag(loggerTag.value).d("This session is not using Firebase pusher") } - } } } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index f30e031bab..cfddff0bdf 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -21,6 +21,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider import io.element.android.libraries.push.providers.api.PusherSubscriber +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber import javax.inject.Inject @@ -30,6 +31,7 @@ class FirebasePushProvider @Inject constructor( private val firebaseStore: FirebaseStore, private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, + private val pushClientSecret: PushClientSecret, ) : PushProvider { override val index = FirebaseConfig.index override val name = FirebaseConfig.name @@ -38,7 +40,7 @@ class FirebasePushProvider @Inject constructor( return listOf(Distributor("Firebase", "Firebase")) } - override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) { + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) { val pushKey = firebaseStore.getFcmToken() ?: return Unit.also { Timber.tag(loggerTag.value).w("Unable to register pusher, Firebase token is not known.") } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt index 813f8e97ca..bff6b06876 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/RegisterUnifiedPushUseCase.kt @@ -40,8 +40,8 @@ class RegisterUnifiedPushUseCase @Inject constructor( val distributorValue = distributor.value if (distributorValue.isNotEmpty()) { saveAndRegisterApp(distributorValue, clientSecret) - val endpoint = unifiedPushStore.getEndpoint() ?: return RegisterUnifiedPushResult.Error - val gateway = unifiedPushStore.getPushGateway() ?: return RegisterUnifiedPushResult.Error + val endpoint = unifiedPushStore.getEndpoint(clientSecret) ?: return RegisterUnifiedPushResult.Error + val gateway = unifiedPushStore.getPushGateway(clientSecret) ?: return RegisterUnifiedPushResult.Error pusherSubscriber.registerPusher(matrixClient, endpoint, gateway) return RegisterUnifiedPushResult.Success } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt new file mode 100644 index 0000000000..9a1e1785a4 --- /dev/null +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushGatewayResolver.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.push.providers.unifiedpush + +import io.element.android.libraries.core.coroutine.CoroutineDispatchers +import io.element.android.libraries.network.RetrofitFactory +import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi +import kotlinx.coroutines.withContext +import timber.log.Timber +import java.net.URL +import javax.inject.Inject + +class UnifiedPushGatewayResolver @Inject constructor( + private val retrofitFactory: RetrofitFactory, + private val coroutineDispatchers: CoroutineDispatchers, +) { + suspend fun getGateway(endpoint: String): String? { + val gateway = UnifiedPushConfig.default_push_gateway_http_url + val url = URL(endpoint) + val custom = "${url.protocol}://${url.host}/_matrix/push/v1/notify" + Timber.i("Testing $custom") + try { + return withContext(coroutineDispatchers.io) { + val api = retrofitFactory.create("${url.protocol}://${url.host}") + .create(UnifiedPushApi::class.java) + try { + val discoveryResponse = api.discover() + if (discoveryResponse.unifiedpush.gateway == "matrix") { + Timber.d("Using custom gateway") + return@withContext custom + } + } catch (throwable: Throwable) { + Timber.tag("UnifiedPushHelper").e(throwable) + } + return@withContext gateway + } + } catch (e: Throwable) { + Timber.d(e, "Cannot try custom gateway") + } + return gateway + } +} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt deleted file mode 100644 index 6c17c16c67..0000000000 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushHelper.kt +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.push.providers.unifiedpush - -import android.content.Context -import io.element.android.libraries.core.coroutine.CoroutineDispatchers -import io.element.android.libraries.core.data.tryOrNull -import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.network.RetrofitFactory -import io.element.android.libraries.push.providers.unifiedpush.network.UnifiedPushApi -import io.element.android.services.toolbox.api.strings.StringProvider -import kotlinx.coroutines.withContext -import timber.log.Timber -import java.net.URL -import javax.inject.Inject - -class UnifiedPushHelper @Inject constructor( - @ApplicationContext private val context: Context, - private val unifiedPushStore: UnifiedPushStore, - private val stringProvider: StringProvider, - private val retrofitFactory: RetrofitFactory, - private val coroutineDispatchers: CoroutineDispatchers, -) { - suspend fun storeCustomOrDefaultGateway(endpoint: String) { - val gateway = UnifiedPushConfig.default_push_gateway_http_url - val parsed = URL(endpoint) - val custom = "${parsed.protocol}://${parsed.host}/_matrix/push/v1/notify" - Timber.i("Testing $custom") - try { - withContext(coroutineDispatchers.io) { - val api = retrofitFactory.create("${parsed.protocol}://${parsed.host}") - .create(UnifiedPushApi::class.java) - tryOrNull { api.discover() } - ?.let { discoveryResponse -> - if (discoveryResponse.unifiedpush.gateway == "matrix") { - Timber.d("Using custom gateway") - unifiedPushStore.storePushGateway(custom) - } - } - } - return - } catch (e: Throwable) { - Timber.d(e, "Cannot try custom gateway") - } - unifiedPushStore.storePushGateway(gateway) - } - - private fun isEmbeddedDistributor() = false - - fun getPrivacyFriendlyUpEndpoint(): String? { - val endpoint = getEndpointOrToken() - if (endpoint.isNullOrEmpty()) return null - if (isEmbeddedDistributor()) { - return endpoint - } - return try { - val parsed = URL(endpoint) - "${parsed.protocol}://${parsed.host}/***" - } catch (e: Exception) { - Timber.e(e, "Error parsing unifiedpush endpoint") - null - } - } - - fun getEndpointOrToken(): String? { - // TODO - return if (isEmbeddedDistributor()) "" // fcmHelper.getFcmToken() - else unifiedPushStore.getEndpoint() - } - - fun getPushGateway(): String? { - return if (isEmbeddedDistributor()) "" // PushConfig.pusher_http_url - else unifiedPushStore.getPushGateway() - } -} diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt index b02fab04ea..3c9833010e 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushNewGatewayHandler.kt @@ -18,11 +18,9 @@ package io.element.android.libraries.push.providers.unifiedpush import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.push.providers.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory -import io.element.android.libraries.sessionstorage.api.SessionStore -import io.element.android.libraries.sessionstorage.api.toUserList +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber import javax.inject.Inject @@ -33,21 +31,22 @@ private val loggerTag = LoggerTag("UnifiedPushNewGatewayHandler") */ class UnifiedPushNewGatewayHandler @Inject constructor( private val pusherSubscriber: PusherSubscriber, - private val sessionStore: SessionStore, private val userPushStoreFactory: UserPushStoreFactory, + private val pushClientSecret: PushClientSecret, private val matrixAuthenticationService: MatrixAuthenticationService, ) { - suspend fun handle(endpoint: String, pushGateway: String) { - // Register the pusher for all the sessions which are using UnifiedPush. - sessionStore.getAllSessions().toUserList().forEach { userId -> - val userDataStore = userPushStoreFactory.create(userId) - if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) { - matrixAuthenticationService.restoreSession(SessionId(userId)).getOrNull()?.use { client -> - pusherSubscriber.registerPusher(client, endpoint, pushGateway) - } - } else { - Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher") + suspend fun handle(endpoint: String, pushGateway: String, clientSecret: String) { + // Register the pusher for the session with this client secret, if is it using UnifiedPush. + val userId = pushClientSecret.getUserIdFromSecret(clientSecret) ?: return Unit.also { + Timber.w("Unable to retrieve session") + } + val userDataStore = userPushStoreFactory.create(userId) + if (userDataStore.getPushProviderName() == UnifiedPushConfig.name) { + matrixAuthenticationService.restoreSession(userId).getOrNull()?.use { client -> + pusherSubscriber.registerPusher(client, endpoint, pushGateway) } + } else { + Timber.tag(loggerTag.value).d("This session is not using UnifiedPush pusher") } } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt index c12c54c0a3..854c070d7e 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushProvider.kt @@ -22,6 +22,7 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import org.unifiedpush.android.connector.UnifiedPush import javax.inject.Inject @@ -29,6 +30,7 @@ class UnifiedPushProvider @Inject constructor( @ApplicationContext private val context: Context, private val registerUnifiedPushUseCase: RegisterUnifiedPushUseCase, private val unRegisterUnifiedPushUseCase: UnregisterUnifiedPushUseCase, + private val pushClientSecret: PushClientSecret, ) : PushProvider { override val index = UnifiedPushConfig.index override val name = UnifiedPushConfig.name @@ -45,12 +47,14 @@ class UnifiedPushProvider @Inject constructor( } } - override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor, clientSecret: String) { + override suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) { + val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) registerUnifiedPushUseCase.execute(matrixClient, distributor, clientSecret) } override suspend fun unregister(matrixClient: MatrixClient) { - unRegisterUnifiedPushUseCase.execute() + val clientSecret = pushClientSecret.getSecretForUser(matrixClient.sessionId) + unRegisterUnifiedPushUseCase.execute(clientSecret) } override suspend fun troubleshoot(): Result { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt index 31d7bbd63e..fe260ed24a 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt @@ -23,9 +23,6 @@ import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.DefaultPreferences import javax.inject.Inject -/** - * TODO EAx Store in BDD (for multisession). - */ class UnifiedPushStore @Inject constructor( @ApplicationContext val context: Context, @DefaultPreferences private val defaultPrefs: SharedPreferences, @@ -35,8 +32,8 @@ class UnifiedPushStore @Inject constructor( * * @return the UnifiedPush Endpoint or null if not received */ - fun getEndpoint(): String? { - return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN, null) + fun getEndpoint(clientSecret: String): String? { + return defaultPrefs.getString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, null) } /** @@ -44,9 +41,9 @@ class UnifiedPushStore @Inject constructor( * * @param endpoint the endpoint to store */ - fun storeUpEndpoint(endpoint: String?) { + fun storeUpEndpoint(endpoint: String?, clientSecret: String) { defaultPrefs.edit { - putString(PREFS_ENDPOINT_OR_TOKEN, endpoint) + putString(PREFS_ENDPOINT_OR_TOKEN + clientSecret, endpoint) } } @@ -55,8 +52,8 @@ class UnifiedPushStore @Inject constructor( * * @return the Push Gateway or null if not defined */ - fun getPushGateway(): String? { - return defaultPrefs.getString(PREFS_PUSH_GATEWAY, null) + fun getPushGateway(clientSecret: String): String? { + return defaultPrefs.getString(PREFS_PUSH_GATEWAY + clientSecret, null) } /** @@ -64,9 +61,9 @@ class UnifiedPushStore @Inject constructor( * * @param gateway the push gateway to store */ - fun storePushGateway(gateway: String?) { + fun storePushGateway(gateway: String?, clientSecret: String) { defaultPrefs.edit { - putString(PREFS_PUSH_GATEWAY, gateway) + putString(PREFS_PUSH_GATEWAY + clientSecret, gateway) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt index 750eb95f83..e6eb778f7f 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnregisterUnifiedPushUseCase.kt @@ -26,22 +26,22 @@ class UnregisterUnifiedPushUseCase @Inject constructor( @ApplicationContext private val context: Context, //private val pushDataStore: PushDataStore, private val unifiedPushStore: UnifiedPushStore, - private val unifiedPushHelper: UnifiedPushHelper, + private val unifiedPushGatewayResolver: UnifiedPushGatewayResolver, ) { - suspend fun execute(/*pushersManager: PushersManager?*/) { + suspend fun execute(clientSecret: String /*pushersManager: PushersManager?*/) { //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME //pushDataStore.setFdroidSyncBackgroundMode(mode) try { - unifiedPushHelper.getEndpointOrToken()?.let { + unifiedPushStore.getEndpoint(clientSecret)?.let { Timber.d("Removing $it") // TODO pushersManager?.unregisterPusher(it) } } catch (e: Exception) { Timber.d(e, "Probably unregistering a non existing pusher") } - unifiedPushStore.storeUpEndpoint(null) - unifiedPushStore.storePushGateway(null) + unifiedPushStore.storeUpEndpoint(null, clientSecret) + unifiedPushStore.storePushGateway(null, clientSecret) UnifiedPush.unregisterApp(context) } } diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt index 0736badbc2..0f065acc52 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/VectorUnifiedPushMessagingReceiver.kt @@ -21,7 +21,6 @@ import android.content.Intent import io.element.android.libraries.architecture.bindings import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.push.providers.api.PushHandler -import io.element.android.libraries.push.providers.api.PusherSubscriber import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch @@ -33,13 +32,10 @@ private val loggerTag = LoggerTag("VectorUnifiedPushMessagingReceiver") class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { @Inject lateinit var pushParser: UnifiedPushParser - - // @Inject lateinit var pushDataStore: PushDataStore @Inject lateinit var pushHandler: PushHandler @Inject lateinit var guardServiceStarter: GuardServiceStarter @Inject lateinit var unifiedPushStore: UnifiedPushStore - @Inject lateinit var unifiedPushHelper: UnifiedPushHelper - @Inject lateinit var pusherSubscriber: PusherSubscriber + @Inject lateinit var unifiedPushGatewayResolver: UnifiedPushGatewayResolver @Inject lateinit var newGatewayHandler: UnifiedPushNewGatewayHandler private val coroutineScope = CoroutineScope(SupervisorJob()) @@ -71,25 +67,23 @@ class VectorUnifiedPushMessagingReceiver : MessagingReceiver() { /** * Called when a new endpoint is to be used for sending push messages. * You should send the endpoint to your application server and sync for missing notifications. - * TODO use [instance] for multi-account */ override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { Timber.tag(loggerTag.value).i("onNewEndpoint: adding $endpoint") // If the endpoint has changed // or the gateway has changed - if (unifiedPushHelper.getEndpointOrToken() != endpoint) { - unifiedPushStore.storeUpEndpoint(endpoint) + if (unifiedPushStore.getEndpoint(instance) != endpoint) { + unifiedPushStore.storeUpEndpoint(endpoint, instance) coroutineScope.launch { - unifiedPushHelper.storeCustomOrDefaultGateway(endpoint) - unifiedPushHelper.getPushGateway()?.let { pushGateway -> - newGatewayHandler.handle(endpoint, pushGateway) + val gateway = unifiedPushGatewayResolver.getGateway(endpoint) + unifiedPushStore.storePushGateway(gateway, instance) + gateway?.let { pushGateway -> + newGatewayHandler.handle(endpoint, pushGateway, instance) } } } else { Timber.tag(loggerTag.value).i("onNewEndpoint: skipped") } - //val mode = BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED - //pushDataStore.setFdroidSyncBackgroundMode(mode) guardServiceStarter.stop() } diff --git a/libraries/pushstore/api/build.gradle.kts b/libraries/pushstore/api/build.gradle.kts index de7a852ee0..9a97bf693f 100644 --- a/libraries/pushstore/api/build.gradle.kts +++ b/libraries/pushstore/api/build.gradle.kts @@ -23,4 +23,5 @@ android { } dependencies { + implementation(projects.libraries.matrix.api) } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt index 832180e850..52e4596ca0 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStoreFactory.kt @@ -16,9 +16,11 @@ package io.element.android.libraries.pushstore.api +import io.element.android.libraries.matrix.api.core.SessionId + /** * Store data related to push about a user. */ interface UserPushStoreFactory { - fun create(userId: String): UserPushStore + fun create(userId: SessionId): UserPushStore } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt similarity index 94% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt rename to libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt index 93f5f43ce4..dbdd22ce07 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecret.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecret.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.api.clientsecret import io.element.android.libraries.matrix.api.core.SessionId diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt similarity index 91% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt rename to libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt index 4ab6c775e3..128302d5c0 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactory.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretFactory.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.api.clientsecret interface PushClientSecretFactory { fun create(): String diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt similarity index 93% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt rename to libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt index c5f7358241..e2bd5a6084 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/clientsecret/PushClientSecretStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.api.clientsecret import io.element.android.libraries.matrix.api.core.SessionId diff --git a/libraries/pushstore/impl/build.gradle.kts b/libraries/pushstore/impl/build.gradle.kts index 0a39730199..4625f293cb 100644 --- a/libraries/pushstore/impl/build.gradle.kts +++ b/libraries/pushstore/impl/build.gradle.kts @@ -31,8 +31,17 @@ dependencies { implementation(libs.dagger) implementation(projects.libraries.architecture) implementation(projects.libraries.core) + implementation(projects.libraries.matrix.api) implementation(projects.libraries.pushstore.api) implementation(projects.libraries.sessionStorage.api) implementation(libs.androidx.corektx) implementation(libs.androidx.datastore.preferences) + + testImplementation(libs.test.junit) + testImplementation(libs.test.mockk) + testImplementation(libs.test.truth) + testImplementation(libs.test.turbine) + testImplementation(libs.coroutines.test) + testImplementation(projects.libraries.matrix.test) + testImplementation(projects.services.appnavstate.test) } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index 159c1cb892..e167f5294a 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -21,6 +21,8 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.di.SingleIn +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.asSessionId import io.element.android.libraries.pushstore.api.UserPushStore import io.element.android.libraries.pushstore.api.UserPushStoreFactory import io.element.android.libraries.sessionstorage.api.observer.SessionListener @@ -38,8 +40,8 @@ class DefaultUserPushStoreFactory @Inject constructor( } // We can have only one class accessing a single data store, so keep a cache of them. - private val cache = mutableMapOf() - override fun create(userId: String): UserPushStore { + private val cache = mutableMapOf() + override fun create(userId: SessionId): UserPushStore { return cache.getOrPut(userId) { UserPushStoreDataStore( context = context, @@ -58,6 +60,6 @@ class DefaultUserPushStoreFactory @Inject constructor( override suspend fun onSessionDeleted(userId: String) { // Delete the store - create(userId).reset() + create(userId.asSessionId()).reset() } } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index 0e4e668eda..c7a320f085 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -22,6 +22,7 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.first @@ -30,7 +31,7 @@ import kotlinx.coroutines.flow.first */ class UserPushStoreDataStore( private val context: Context, - userId: String, + userId: SessionId, ) : UserPushStore { private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store_$userId") private val pushProviderName = stringPreferencesKey("pushProviderName") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretFactoryImpl.kt similarity index 86% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretFactoryImpl.kt index 1d7a1e6247..4e6e718a60 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretFactoryImpl.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretFactoryImpl.kt @@ -14,10 +14,11 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory import java.util.UUID import javax.inject.Inject diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImpl.kt similarity index 84% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImpl.kt index b57b24d25e..ca0ed14e33 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImpl.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImpl.kt @@ -14,11 +14,14 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore import javax.inject.Inject @ContributesBinding(AppScope::class) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretStoreDataStore.kt similarity index 94% rename from libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt rename to libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretStoreDataStore.kt index 055de6fc47..2431120c9e 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretStoreDataStore.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import android.content.Context import androidx.datastore.core.DataStore @@ -27,6 +27,7 @@ import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.asSessionId +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore import kotlinx.coroutines.flow.first import javax.inject.Inject diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/FakePushClientSecretFactory.kt similarity index 85% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt rename to libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/FakePushClientSecretFactory.kt index 25823a57e8..b1cb93e49c 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/FakePushClientSecretFactory.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/FakePushClientSecretFactory.kt @@ -14,7 +14,9 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret + +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretFactory private const val A_SECRET_PREFIX = "A_SECRET_" diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt similarity index 89% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt rename to libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt index a2d2d9c83c..8c9b577967 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/InMemoryPushClientSecretStore.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/InMemoryPushClientSecretStore.kt @@ -14,9 +14,10 @@ * limitations under the License. */ -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecretStore class InMemoryPushClientSecretStore : PushClientSecretStore { private val secrets = mutableMapOf() diff --git a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt similarity index 97% rename from libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt rename to libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt index a9d740bf31..d7f8e2e337 100644 --- a/libraries/push/impl/src/test/kotlin/io/element/android/libraries/push/impl/clientsecret/PushClientSecretImplTest.kt +++ b/libraries/pushstore/impl/src/test/kotlin/io/element/android/libraries/pushstore/impl/clientsecret/PushClientSecretImplTest.kt @@ -16,7 +16,7 @@ @file:OptIn(ExperimentalCoroutinesApi::class) -package io.element.android.libraries.push.impl.clientsecret +package io.element.android.libraries.pushstore.impl.clientsecret import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.api.core.SessionId From f4a283567ea4c403d8f4665235e2ce359f31f938 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 16:48:51 +0200 Subject: [PATCH 23/83] Cleanup store. --- .../libraries/push/api/store/PushDataStore.kt | 18 ---- .../NotificationDrawerManager.kt | 11 +-- .../notifications/NotificationEventQueue.kt | 1 + .../push/impl/push/DefaultPushHandler.kt | 23 +++-- .../push/impl/store/DefaultPushDataStore.kt | 96 ------------------- .../libraries/pushstore/api/UserPushStore.kt | 11 ++- .../pushstore/impl/UserPushStoreDataStore.kt | 17 ++++ 7 files changed, 40 insertions(+), 137 deletions(-) diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt index d2a6bda0b0..f478034063 100644 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt +++ b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/store/PushDataStore.kt @@ -16,26 +16,8 @@ package io.element.android.libraries.push.api.store -import io.element.android.libraries.push.api.model.BackgroundSyncMode import kotlinx.coroutines.flow.Flow interface PushDataStore { val pushCounterFlow: Flow - - // TODO Move all those settings to the per user store... - fun areNotificationEnabledForDevice(): Boolean - fun setNotificationEnabledForDevice(enabled: Boolean) - - fun backgroundSyncTimeOut(): Int - fun setBackgroundSyncTimeout(timeInSecond: Int) - fun backgroundSyncDelay(): Int - fun setBackgroundSyncDelay(timeInSecond: Int) - fun isBackgroundSyncEnabled(): Boolean - fun setFdroidSyncBackgroundMode(mode: BackgroundSyncMode) - fun getFdroidSyncBackgroundMode(): BackgroundSyncMode - - /** - * Return true if Pin code is disabled, or if user set the settings to see full notification content. - */ - fun useCompleteNotificationFormat(): Boolean } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt index 8d3bfda4c3..cf0307fbd9 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationDrawerManager.kt @@ -70,7 +70,8 @@ class NotificationDrawerManager @Inject constructor( private var currentAppNavigationState: AppNavigationState? = null private val firstThrottler = FirstThrottler(200) - private var useCompleteNotificationFormat = pushDataStore.useCompleteNotificationFormat() + // TODO EAx add a setting per user for this + private var useCompleteNotificationFormat = true init { handlerThread.start() @@ -111,12 +112,6 @@ class NotificationDrawerManager @Inject constructor( } private fun NotificationEventQueue.onNotifiableEventReceived(notifiableEvent: NotifiableEvent) { - if (!pushDataStore.areNotificationEnabledForDevice()) { - Timber.i("Notification are disabled for this device") - return - } - // If we support multi session, event list should be per userId - // Currently only manage single session if (buildMeta.lowPrivacyLoggingEnabled) { Timber.d("onNotifiableEventReceived(): $notifiableEvent") } else { @@ -185,7 +180,7 @@ class NotificationDrawerManager @Inject constructor( // TODO EAx Must be per account fun notificationStyleChanged() { updateEvents { - val newSettings = pushDataStore.useCompleteNotificationFormat() + val newSettings = true // pushDataStore.useCompleteNotificationFormat() if (newSettings != useCompleteNotificationFormat) { // Settings has changed, remove all current notifications notificationRenderer.cancelAllNotifications() diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt index 60fb1baa05..862b4784ac 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationEventQueue.kt @@ -34,6 +34,7 @@ data class NotificationEventQueue constructor( * Acts as a notification debouncer to stop already dismissed push notifications from * displaying again when the /sync response is delayed. */ + // TODO Should be per session, so the key must be Pair. private val seenEventIds: CircularCache ) { diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt index 7eef95fc67..09afe0a861 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/push/DefaultPushHandler.kt @@ -22,15 +22,12 @@ import android.os.Handler import android.os.Looper import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.androidutils.network.WifiDetector import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.core.meta.BuildMeta import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService -import io.element.android.libraries.push.api.store.PushDataStore import io.element.android.libraries.push.impl.PushersManager -import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.notifications.NotifiableEventResolver import io.element.android.libraries.push.impl.notifications.NotificationActionIds @@ -38,6 +35,8 @@ import io.element.android.libraries.push.impl.notifications.NotificationDrawerMa import io.element.android.libraries.push.impl.store.DefaultPushDataStore import io.element.android.libraries.push.providers.api.PushData import io.element.android.libraries.push.providers.api.PushHandler +import io.element.android.libraries.pushstore.api.UserPushStoreFactory +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob @@ -51,17 +50,16 @@ private val loggerTag = LoggerTag("PushHandler", pushLoggerTag) class DefaultPushHandler @Inject constructor( private val notificationDrawerManager: NotificationDrawerManager, private val notifiableEventResolver: NotifiableEventResolver, - private val pushDataStore: PushDataStore, private val defaultPushDataStore: DefaultPushDataStore, + private val userPushStoreFactory: UserPushStoreFactory, private val pushClientSecret: PushClientSecret, private val actionIds: NotificationActionIds, @ApplicationContext private val context: Context, private val buildMeta: BuildMeta, private val matrixAuthenticationService: MatrixAuthenticationService, -): PushHandler { +) : PushHandler { private val coroutineScope = CoroutineScope(SupervisorJob()) - private val wifiDetector: WifiDetector = WifiDetector(context) // UI handler private val mUIHandler by lazy { @@ -89,12 +87,6 @@ class DefaultPushHandler @Inject constructor( return } - // TODO EAx Should be per user - if (!pushDataStore.areNotificationEnabledForDevice()) { - Timber.tag(loggerTag.value).i("Notification are disabled for this device") - return - } - mUIHandler.post { coroutineScope.launch(Dispatchers.IO) { handleInternal(pushData) } } @@ -139,6 +131,13 @@ class DefaultPushHandler @Inject constructor( return } + val userPushStore = userPushStoreFactory.create(userId) + if (!userPushStore.areNotificationEnabledForDevice()) { + // TODO We need to check if this is an incoming call + Timber.tag(loggerTag.value).i("Notification are disabled for this device, ignore push.") + return + } + notificationDrawerManager.onNotifiableEventReceived(notificationData) } catch (e: Exception) { Timber.tag(loggerTag.value).e(e, "## handleInternal() failed") diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt index ffbd575aa4..22faa91453 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/store/DefaultPushDataStore.kt @@ -17,20 +17,15 @@ package io.element.android.libraries.push.impl.store import android.content.Context -import android.content.SharedPreferences -import androidx.core.content.edit import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.squareup.anvil.annotations.ContributesBinding -import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext -import io.element.android.libraries.di.DefaultPreferences import io.element.android.libraries.di.SingleIn -import io.element.android.libraries.push.api.model.BackgroundSyncMode import io.element.android.libraries.push.api.store.PushDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -42,7 +37,6 @@ private val Context.dataStore: DataStore by preferencesDataStore(na @ContributesBinding(AppScope::class) class DefaultPushDataStore @Inject constructor( @ApplicationContext private val context: Context, - @DefaultPreferences private val defaultPrefs: SharedPreferences, ) : PushDataStore { private val pushCounter = intPreferencesKey("push_counter") @@ -56,94 +50,4 @@ class DefaultPushDataStore @Inject constructor( settings[pushCounter] = currentCounterValue + 1 } } - - override fun areNotificationEnabledForDevice(): Boolean { - return defaultPrefs.getBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, true) - } - - override fun setNotificationEnabledForDevice(enabled: Boolean) { - defaultPrefs.edit { - putBoolean(SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY, enabled) - } - } - - override fun backgroundSyncTimeOut(): Int { - return tryOrNull { - // The xml pref is saved as a string so use getString and parse - defaultPrefs.getString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, null)?.toInt() - } ?: BackgroundSyncMode.DEFAULT_SYNC_TIMEOUT_SECONDS - } - - override fun setBackgroundSyncTimeout(timeInSecond: Int) { - defaultPrefs - .edit() - .putString(SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY, timeInSecond.toString()) - .apply() - } - - override fun backgroundSyncDelay(): Int { - return tryOrNull { - // The xml pref is saved as a string so use getString and parse - defaultPrefs.getString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, null)?.toInt() - } ?: BackgroundSyncMode.DEFAULT_SYNC_DELAY_SECONDS - } - - override fun setBackgroundSyncDelay(timeInSecond: Int) { - defaultPrefs - .edit() - .putString(SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY, timeInSecond.toString()) - .apply() - } - - override fun isBackgroundSyncEnabled(): Boolean { - return getFdroidSyncBackgroundMode() != BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_DISABLED - } - - override fun setFdroidSyncBackgroundMode(mode: BackgroundSyncMode) { - defaultPrefs - .edit() - .putString(SETTINGS_FDROID_BACKGROUND_SYNC_MODE, mode.name) - .apply() - } - - override fun getFdroidSyncBackgroundMode(): BackgroundSyncMode { - return try { - val strPref = defaultPrefs - .getString(SETTINGS_FDROID_BACKGROUND_SYNC_MODE, BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY.name) - BackgroundSyncMode.values().firstOrNull { it.name == strPref } ?: BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY - } catch (e: Throwable) { - BackgroundSyncMode.FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY - } - } - - /** - * Return true if Pin code is disabled, or if user set the settings to see full notification content. - */ - override fun useCompleteNotificationFormat(): Boolean { - return true - /* - return !useFlagPinCode() || - defaultPrefs.getBoolean(SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG, true) - */ - } - - companion object { - // notifications - const val SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY = "SETTINGS_ENABLE_ALL_NOTIF_PREFERENCE_KEY" - const val SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY = "SETTINGS_ENABLE_THIS_DEVICE_PREFERENCE_KEY" - - // background sync - const val SETTINGS_START_ON_BOOT_PREFERENCE_KEY = "SETTINGS_START_ON_BOOT_PREFERENCE_KEY" - const val SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_ENABLE_BACKGROUND_SYNC_PREFERENCE_KEY" - const val SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY = "SETTINGS_SET_SYNC_TIMEOUT_PREFERENCE_KEY" - const val SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY = "SETTINGS_SET_SYNC_DELAY_PREFERENCE_KEY" - - const val SETTINGS_FDROID_BACKGROUND_SYNC_MODE = "SETTINGS_FDROID_BACKGROUND_SYNC_MODE" - const val SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY = "SETTINGS_BACKGROUND_SYNC_PREFERENCE_KEY" - - const val SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG = "SETTINGS_SECURITY_USE_COMPLETE_NOTIFICATIONS_FLAG" - - // notification method - const val SETTINGS_NOTIFICATION_METHOD_KEY = "SETTINGS_NOTIFICATION_METHOD_KEY" - } } diff --git a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt index 6817199e13..28577ba3f8 100644 --- a/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt +++ b/libraries/pushstore/api/src/main/kotlin/io/element/android/libraries/pushstore/api/UserPushStore.kt @@ -21,12 +21,17 @@ package io.element.android.libraries.pushstore.api */ interface UserPushStore { suspend fun getPushProviderName(): String? - suspend fun setPushProviderName(value: String) - suspend fun getCurrentRegisteredPushKey(): String? - suspend fun setCurrentRegisteredPushKey(value: String) + suspend fun areNotificationEnabledForDevice(): Boolean + suspend fun setNotificationEnabledForDevice(enabled: Boolean) + + /** + * Return true if Pin code is disabled, or if user set the settings to see full notification content. + */ + fun useCompleteNotificationFormat(): Boolean + suspend fun reset() } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt index c7a320f085..56867a6584 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/UserPushStoreDataStore.kt @@ -19,9 +19,11 @@ package io.element.android.libraries.pushstore.impl import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import io.element.android.libraries.core.bool.orTrue import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.pushstore.api.UserPushStore import kotlinx.coroutines.flow.first @@ -36,6 +38,7 @@ class UserPushStoreDataStore( private val Context.dataStore: DataStore by preferencesDataStore(name = "push_store_$userId") private val pushProviderName = stringPreferencesKey("pushProviderName") private val currentPushKey = stringPreferencesKey("currentPushKey") + private val notificationEnabled = booleanPreferencesKey("notificationEnabled") override suspend fun getPushProviderName(): String? { return context.dataStore.data.first()[pushProviderName] @@ -57,6 +60,20 @@ class UserPushStoreDataStore( } } + override suspend fun areNotificationEnabledForDevice(): Boolean { + return context.dataStore.data.first()[notificationEnabled].orTrue() + } + + override suspend fun setNotificationEnabledForDevice(enabled: Boolean) { + context.dataStore.edit { + it[notificationEnabled] = enabled + } + } + + override fun useCompleteNotificationFormat(): Boolean { + return true + } + override suspend fun reset() { context.dataStore.edit { it.clear() From 7f22c6b211b3a3dd1a7f47cd6d30318a66a582e2 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 17:02:14 +0200 Subject: [PATCH 24/83] Use Firebase by default and cleanup --- .../appnav/loggedin/LoggedInPresenter.kt | 4 +- .../push/api/model/BackgroundSyncMode.kt | 48 ------------------- .../push/providers/api/PushProvider.kt | 8 ++-- .../firebase/FirebasePushProvider.kt | 2 - 4 files changed, 6 insertions(+), 56 deletions(-) delete mode 100644 libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt diff --git a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt index a798f95bae..50bc09d0a0 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/loggedin/LoggedInPresenter.kt @@ -47,8 +47,8 @@ class LoggedInPresenter @Inject constructor( LaunchedEffect(Unit) { // Ensure pusher is registered // TODO Manually select push provider for now - val pushProvider = pushService.getAvailablePushProviders().find { it.name == "UnifiedPush" } ?: return@LaunchedEffect - val distributor = pushProvider.getDistributors().first() + val pushProvider = pushService.getAvailablePushProviders().firstOrNull() ?: return@LaunchedEffect + val distributor = pushProvider.getDistributors().firstOrNull() ?: return@LaunchedEffect pushService.registerWith(matrixClient, pushProvider, distributor) } diff --git a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt b/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt deleted file mode 100644 index 3fb4841aba..0000000000 --- a/libraries/push/api/src/main/kotlin/io/element/android/libraries/push/api/model/BackgroundSyncMode.kt +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.libraries.push.api.model - -/** - * Different strategies for Background sync, only applicable to F-Droid version of the app. - */ -enum class BackgroundSyncMode { - /** - * In this mode background syncs are scheduled via Workers, meaning that the system will have control on the periodicity - * of syncs when battery is low or when the phone is idle (sync will occur in allowed maintenance windows). After completion - * the sync work will schedule another one. - */ - FDROID_BACKGROUND_SYNC_MODE_FOR_BATTERY, - - /** - * This mode requires the app to be exempted from battery optimization. Alarms will be launched and will wake up the app - * in order to perform the background sync as a foreground service. After completion the service will schedule another alarm - */ - FDROID_BACKGROUND_SYNC_MODE_FOR_REALTIME, - - /** - * The app won't sync in background. - */ - FDROID_BACKGROUND_SYNC_MODE_DISABLED; - - companion object { - const val DEFAULT_SYNC_DELAY_SECONDS = 60 - const val DEFAULT_SYNC_TIMEOUT_SECONDS = 6 - - fun fromString(value: String?): BackgroundSyncMode = values().firstOrNull { it.name == value } - ?: FDROID_BACKGROUND_SYNC_MODE_DISABLED - } -} diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index ebcdd962d6..7e16121ed4 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -23,7 +23,7 @@ import io.element.android.libraries.matrix.api.MatrixClient */ interface PushProvider { /** - * Allow to sort provider, from lower index to higher index + * Allow to sort providers, from lower index to higher index. */ val index: Int @@ -35,17 +35,17 @@ interface PushProvider { fun getDistributors(): List /** - * Register the pusher to the homeserver + * Register the pusher to the homeserver. */ suspend fun registerWith(matrixClient: MatrixClient, distributor: Distributor) /** - * Unregister the pusher + * Unregister the pusher. */ suspend fun unregister(matrixClient: MatrixClient) /** - * Attempt to troubleshoot the push provider + * Attempt to troubleshoot the push provider. */ suspend fun troubleshoot(): Result } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt index cfddff0bdf..15530033d5 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushProvider.kt @@ -21,7 +21,6 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.push.providers.api.Distributor import io.element.android.libraries.push.providers.api.PushProvider import io.element.android.libraries.push.providers.api.PusherSubscriber -import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import timber.log.Timber import javax.inject.Inject @@ -31,7 +30,6 @@ class FirebasePushProvider @Inject constructor( private val firebaseStore: FirebaseStore, private val firebaseTroubleshooter: FirebaseTroubleshooter, private val pusherSubscriber: PusherSubscriber, - private val pushClientSecret: PushClientSecret, ) : PushProvider { override val index = FirebaseConfig.index override val name = FirebaseConfig.name From ab1b1ab1cb4f856196f7f50b9d8ee9f7f7cb2ef1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Tue, 11 Apr 2023 17:09:12 +0200 Subject: [PATCH 25/83] Fix test --- .../push/providers/firebase/FirebasePushParserTest.kt | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt index a6525657c8..466a2bfedf 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt @@ -20,6 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.push.providers.api.PushData +import org.junit.Assert.assertThrows import org.junit.Test class FirebasePushParserTest { @@ -51,26 +52,26 @@ class FirebasePushParserTest { fun `test empty roomId`() { val pushParser = FirebasePushParser() assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull() - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", ""))).isNull() + assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) } } @Test fun `test invalid roomId`() { val pushParser = FirebasePushParser() - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain"))).isNull() + assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain")) } } @Test fun `test empty eventId`() { val pushParser = FirebasePushParser() assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isNull() - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", ""))).isNull() + assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) } } @Test fun `test invalid eventId`() { val pushParser = FirebasePushParser() - assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId"))).isNull() + assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) } } companion object { From f4e4be7479aca85b76cf292c297d938636326e43 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Apr 2023 09:14:55 +0200 Subject: [PATCH 26/83] Improve asXId and make tests pass in release and debug mode. --- .../libraries/matrix/api/core/EventId.kt | 8 +++-- .../libraries/matrix/api/core/RoomId.kt | 8 +++-- .../libraries/matrix/api/core/SessionId.kt | 8 +++-- .../libraries/matrix/api/core/SpaceId.kt | 8 +++-- .../libraries/matrix/api/core/ThreadId.kt | 10 ++++-- .../libraries/matrix/api/core/UserId.kt | 8 +++-- .../pushproviders/firebase/build.gradle.kts | 1 + .../firebase/FirebaseNewTokenHandler.kt | 2 +- .../firebase/FirebasePushParserTest.kt | 10 +++--- .../unifiedpush/build.gradle.kts | 1 + .../unifiedpush/UnifiedPushParserTest.kt | 10 +++--- .../impl/DefaultUserPushStoreFactory.kt | 2 +- settings.gradle.kts | 1 + tests/testutils/build.gradle.kts | 36 +++++++++++++++++++ .../android/tests/testutils/NullOrThrow.kt | 33 +++++++++++++++++ 15 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 tests/testutils/build.gradle.kts create mode 100644 tests/testutils/src/main/kotlin/io/element/android/tests/testutils/NullOrThrow.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt index ffd5bb8ea2..b24d886ee8 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt @@ -22,8 +22,12 @@ import java.io.Serializable @JvmInline value class EventId(val value: String) : Serializable -fun String.asEventId() = EventId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isEventId(this)) { +fun String.asEventId() = if (MatrixPatterns.isEventId(this)) { + EventId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid event Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index f711723c3f..f71f4ba4f9 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -22,8 +22,12 @@ import java.io.Serializable @JvmInline value class RoomId(val value: String) : Serializable -fun String.asRoomId() = RoomId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { +fun String.asRoomId() = if (MatrixPatterns.isRoomId(this)) { + RoomId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid room Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt index 8591876b29..0f0edf2299 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt @@ -20,8 +20,12 @@ import io.element.android.libraries.matrix.api.BuildConfig typealias SessionId = UserId -fun String.asSessionId() = SessionId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isSessionId(this)) { +fun String.asSessionId() = if (MatrixPatterns.isSessionId(this)) { + SessionId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid session Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt index 1b8b33426b..d4f2e43be6 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt @@ -27,8 +27,12 @@ value class SpaceId(val value: String) : Serializable */ val MAIN_SPACE = SpaceId("!mainSpace") -fun String.asSpaceId() = SpaceId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isSpaceId(this)) { +fun String.asSpaceId() = if (MatrixPatterns.isSpaceId(this)) { + SpaceId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid space Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt index f95c33bad3..f57cb8fa23 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt @@ -22,8 +22,12 @@ import java.io.Serializable @JvmInline value class ThreadId(val value: String) : Serializable -fun String.asThreadId() = ThreadId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isThreadId(this)) { - error("`$this` is not a valid Thread Id") +fun String.asThreadId() = if (MatrixPatterns.isThreadId(this)) { + ThreadId(this) +} else { + if (BuildConfig.DEBUG) { + error("`$this` is not a valid thread Id") + } else { + null } } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index 91f9c6f11c..ba7028c926 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -22,8 +22,12 @@ import java.io.Serializable @JvmInline value class UserId(val value: String) : Serializable -fun String.asUserId() = UserId(this).also { - if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { +fun String.asUserId() = if (MatrixPatterns.isUserId(this)) { + UserId(this) +} else { + if (BuildConfig.DEBUG) { error("`$this` is not a valid user Id") + } else { + null } } diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index eabc70305b..a20398319b 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -45,4 +45,5 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt index f26fcfae25..58464b5af0 100644 --- a/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt +++ b/libraries/pushproviders/firebase/src/main/kotlin/io/element/android/libraries/push/providers/firebase/FirebaseNewTokenHandler.kt @@ -42,7 +42,7 @@ class FirebaseNewTokenHandler @Inject constructor( firebaseStore.storeFcmToken(firebaseToken) // Register the pusher for all the sessions sessionStore.getAllSessions().toUserList() - .map { it.asSessionId() } + .mapNotNull { it.asSessionId() } .forEach { userId -> val userDataStore = userPushStoreFactory.create(userId) if (userDataStore.getPushProviderName() == FirebaseConfig.name) { diff --git a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt index 466a2bfedf..562aecc790 100644 --- a/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt +++ b/libraries/pushproviders/firebase/src/test/kotlin/io/element/android/libraries/push/providers/firebase/FirebasePushParserTest.kt @@ -20,7 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.push.providers.api.PushData -import org.junit.Assert.assertThrows +import io.element.android.tests.testutils.assertNullOrThrow import org.junit.Test class FirebasePushParserTest { @@ -52,26 +52,26 @@ class FirebasePushParserTest { fun `test empty roomId`() { val pushParser = FirebasePushParser() assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", null))).isNull() - assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) } + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "")) } } @Test fun `test invalid roomId`() { val pushParser = FirebasePushParser() - assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain")) } + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("room_id", "aRoomId:domain")) } } @Test fun `test empty eventId`() { val pushParser = FirebasePushParser() assertThat(pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", null))).isNull() - assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) } + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "")) } } @Test fun `test invalid eventId`() { val pushParser = FirebasePushParser() - assertThrows(IllegalStateException::class.java) { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) } + assertNullOrThrow { pushParser.parse(FIREBASE_PUSH_DATA.mutate("event_id", "anEventId")) } } companion object { diff --git a/libraries/pushproviders/unifiedpush/build.gradle.kts b/libraries/pushproviders/unifiedpush/build.gradle.kts index 7dcd773c25..3546bb16e1 100644 --- a/libraries/pushproviders/unifiedpush/build.gradle.kts +++ b/libraries/pushproviders/unifiedpush/build.gradle.kts @@ -53,4 +53,5 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt index b18be39e07..19231505cc 100644 --- a/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt +++ b/libraries/pushproviders/unifiedpush/src/test/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushParserTest.kt @@ -20,7 +20,7 @@ import com.google.common.truth.Truth.assertThat import io.element.android.libraries.matrix.test.AN_EVENT_ID import io.element.android.libraries.matrix.test.A_ROOM_ID import io.element.android.libraries.push.providers.api.PushData -import org.junit.Assert.assertThrows +import io.element.android.tests.testutils.assertNullOrThrow import org.junit.Test class UnifiedPushParserTest { @@ -52,7 +52,7 @@ class UnifiedPushParserTest { @Test fun `test empty roomId`() { val pushParser = UnifiedPushParser() - assertThrows(IllegalStateException::class.java) { + assertNullOrThrow { pushParser.parse(UNIFIED_PUSH_DATA.replace(A_ROOM_ID.value, "").toByteArray(), aClientSecret) } } @@ -60,7 +60,7 @@ class UnifiedPushParserTest { @Test fun `test invalid roomId`() { val pushParser = UnifiedPushParser() - assertThrows(IllegalStateException::class.java) { + assertNullOrThrow { pushParser.parse(UNIFIED_PUSH_DATA.mutate(A_ROOM_ID.value, "aRoomId:domain"), aClientSecret) } } @@ -68,7 +68,7 @@ class UnifiedPushParserTest { @Test fun `test empty eventId`() { val pushParser = UnifiedPushParser() - assertThrows(IllegalStateException::class.java) { + assertNullOrThrow { pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, ""), aClientSecret) } } @@ -76,7 +76,7 @@ class UnifiedPushParserTest { @Test fun `test invalid eventId`() { val pushParser = UnifiedPushParser() - assertThrows(IllegalStateException::class.java) { + assertNullOrThrow { pushParser.parse(UNIFIED_PUSH_DATA.mutate(AN_EVENT_ID.value, "anEventId"), aClientSecret) } } diff --git a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt index e167f5294a..ed32dba472 100644 --- a/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt +++ b/libraries/pushstore/impl/src/main/kotlin/io/element/android/libraries/pushstore/impl/DefaultUserPushStoreFactory.kt @@ -60,6 +60,6 @@ class DefaultUserPushStoreFactory @Inject constructor( override suspend fun onSessionDeleted(userId: String) { // Delete the store - create(userId.asSessionId()).reset() + userId.asSessionId()?.let { create(it).reset() } } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 7429f80b43..1173288adb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -49,6 +49,7 @@ rootProject.name = "ElementX" include(":app") include(":appnav") include(":tests:uitests") +include(":tests:testutils") include(":anvilannotations") include(":anvilcodegen") diff --git a/tests/testutils/build.gradle.kts b/tests/testutils/build.gradle.kts new file mode 100644 index 0000000000..0c28da1f06 --- /dev/null +++ b/tests/testutils/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-library") + alias(libs.plugins.ksp) +} + +android { + namespace = "io.element.android.tests.testutils" +} + +dependencies { + implementation(libs.test.junit) + implementation(libs.test.mockk) + implementation(libs.test.truth) + implementation(libs.test.turbine) + implementation(libs.coroutines.test) + implementation(projects.libraries.matrix.test) + implementation(projects.services.appnavstate.test) +} diff --git a/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/NullOrThrow.kt b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/NullOrThrow.kt new file mode 100644 index 0000000000..adfd58da5f --- /dev/null +++ b/tests/testutils/src/main/kotlin/io/element/android/tests/testutils/NullOrThrow.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.tests.testutils + +import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertThrows + +/** + * Assert that the lambda throws on debug and returns null on release. + */ +fun assertNullOrThrow(lambda: () -> Any?) { + if (BuildConfig.DEBUG) { + assertThrows(IllegalStateException::class.java) { + lambda() + } + } else { + assertThat(lambda()).isNull() + } +} From 245c46c8b860db3a11eb11a4b1a7c7b274b057ab Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Apr 2023 10:06:11 +0200 Subject: [PATCH 27/83] Cleanup --- libraries/push/impl/build.gradle.kts | 5 ++--- .../libraries/push/impl/DefaultPushService.kt | 2 +- .../libraries/push/impl/PushersManager.kt | 21 ++----------------- .../push/providers/api/PushProvider.kt | 2 +- .../pushproviders/firebase/build.gradle.kts | 1 - .../providers/unifiedpush/UnifiedPushStore.kt | 4 ++++ 6 files changed, 10 insertions(+), 25 deletions(-) diff --git a/libraries/push/impl/build.gradle.kts b/libraries/push/impl/build.gradle.kts index 44fc21fb2b..81ba07fc63 100644 --- a/libraries/push/impl/build.gradle.kts +++ b/libraries/push/impl/build.gradle.kts @@ -55,9 +55,8 @@ dependencies { exclude(group = "com.android.support", module = "support-annotations") } - // TODO Remove - implementation(platform(libs.google.firebase.bom)) - implementation("com.google.firebase:firebase-messaging-ktx") + // TODO Temporary use the deprecated LocalBroadcastManager, to be changed later. + implementation("androidx.localbroadcastmanager:localbroadcastmanager:1.1.0") testImplementation(libs.test.junit) testImplementation(libs.test.mockk) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt index 99af1317f9..28a58d1058 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/DefaultPushService.kt @@ -43,7 +43,7 @@ class DefaultPushService @Inject constructor( } /** - * Get current push provider, compare with provided one, then unregister and register if different, and store change + * Get current push provider, compare with provided one, then unregister and register if different, and store change. */ override suspend fun registerWith(matrixClient: MatrixClient, pushProvider: PushProvider, distributor: Distributor) { val userPushStore = userPushStoreFactory.create(matrixClient.sessionId) diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt index 33b8ac3507..04d2875328 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/PushersManager.kt @@ -20,17 +20,15 @@ import com.squareup.anvil.annotations.ContributesBinding import io.element.android.libraries.core.log.logger.LoggerTag import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.MatrixClient -import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.pusher.SetHttpPusherData -import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.libraries.push.impl.config.PushConfig import io.element.android.libraries.push.impl.log.pushLoggerTag import io.element.android.libraries.push.impl.pushgateway.PushGatewayNotifyRequest import io.element.android.libraries.push.providers.api.PusherSubscriber import io.element.android.libraries.pushstore.api.UserPushStoreFactory -import io.element.android.libraries.sessionstorage.api.SessionStore +import io.element.android.libraries.pushstore.api.clientsecret.PushClientSecret import io.element.android.services.toolbox.api.appname.AppNameProvider import timber.log.Timber import javax.inject.Inject @@ -41,16 +39,13 @@ private val loggerTag = LoggerTag("PushersManager", pushLoggerTag) @ContributesBinding(AppScope::class) class PushersManager @Inject constructor( - // private val unifiedPushHelper: UnifiedPushHelper, // private val localeProvider: LocaleProvider, private val appNameProvider: AppNameProvider, // private val getDeviceInfoUseCase: GetDeviceInfoUseCase, private val pushGatewayNotifyRequest: PushGatewayNotifyRequest, private val pushClientSecret: PushClientSecret, - private val sessionStore: SessionStore, - private val matrixAuthenticationService: MatrixAuthenticationService, private val userPushStoreFactory: UserPushStoreFactory, -): PusherSubscriber { +) : PusherSubscriber { // TODO Move this to the PushProvider API suspend fun testPush() { pushGatewayNotifyRequest.execute( @@ -63,18 +58,6 @@ class PushersManager @Inject constructor( ) } - suspend fun enqueueRegisterPusherWithFcmKey(pushKey: String) { - // return onNewFirebaseToken(pushKey, PushConfig.pusher_http_url) - TODO() - } - - suspend fun onNewUnifiedPushEndpoint( - pushKey: String, - gateway: String - ) { - TODO() - } - /** * Register a pusher to the server if not done yet. */ diff --git a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt index 7e16121ed4..4ad0179403 100644 --- a/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt +++ b/libraries/pushproviders/api/src/main/kotlin/io/element/android/libraries/push/providers/api/PushProvider.kt @@ -19,7 +19,7 @@ package io.element.android.libraries.push.providers.api import io.element.android.libraries.matrix.api.MatrixClient /** - * This is the main API for this module + * This is the main API for this module. */ interface PushProvider { /** diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index a20398319b..b9b4f76c12 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -17,7 +17,6 @@ plugins { id("io.element.android-library") alias(libs.plugins.anvil) - // kotlin("plugin.serialization") version "1.8.10" } android { diff --git a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt index fe260ed24a..3883c3348c 100644 --- a/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt +++ b/libraries/pushproviders/unifiedpush/src/main/kotlin/io/element/android/libraries/push/providers/unifiedpush/UnifiedPushStore.kt @@ -30,6 +30,7 @@ class UnifiedPushStore @Inject constructor( /** * Retrieves the UnifiedPush Endpoint. * + * @param clientSecret the client secret, to identify the session * @return the UnifiedPush Endpoint or null if not received */ fun getEndpoint(clientSecret: String): String? { @@ -40,6 +41,7 @@ class UnifiedPushStore @Inject constructor( * Store UnifiedPush Endpoint to the SharedPrefs. * * @param endpoint the endpoint to store + * @param clientSecret the client secret, to identify the session */ fun storeUpEndpoint(endpoint: String?, clientSecret: String) { defaultPrefs.edit { @@ -50,6 +52,7 @@ class UnifiedPushStore @Inject constructor( /** * Retrieves the Push Gateway. * + * @param clientSecret the client secret, to identify the session * @return the Push Gateway or null if not defined */ fun getPushGateway(clientSecret: String): String? { @@ -60,6 +63,7 @@ class UnifiedPushStore @Inject constructor( * Store Push Gateway to the SharedPrefs. * * @param gateway the push gateway to store + * @param clientSecret the client secret, to identify the session */ fun storePushGateway(gateway: String?, clientSecret: String) { defaultPrefs.edit { From e9fa854143b127aa4e97108d2cc9be54ef601054 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Apr 2023 11:15:47 +0200 Subject: [PATCH 28/83] Fix issue with lint. --- libraries/pushproviders/firebase/build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/pushproviders/firebase/build.gradle.kts b/libraries/pushproviders/firebase/build.gradle.kts index b9b4f76c12..17f2071624 100644 --- a/libraries/pushproviders/firebase/build.gradle.kts +++ b/libraries/pushproviders/firebase/build.gradle.kts @@ -38,7 +38,7 @@ dependencies { implementation(projects.libraries.pushstore.api) implementation(projects.libraries.pushproviders.api) - implementation(platform(libs.google.firebase.bom)) + api(platform(libs.google.firebase.bom)) api("com.google.firebase:firebase-messaging-ktx") testImplementation(libs.test.junit) From 1f7b89721692ac12bdcceaac390f317b785e6cf1 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Wed, 12 Apr 2023 12:03:11 +0200 Subject: [PATCH 29/83] Add firebase resource file generated by the firebase plugin, to be able to remove the plugin. --- app/build.gradle.kts | 4 +- app/src/debug/google-services.json | 49 ------------------- app/src/nightly/google-services.json | 40 --------------- app/src/release/google-services.json | 40 --------------- libraries/pushproviders/firebase/README.md | 7 +++ .../src/debug/res/values/firebase.xml | 4 ++ .../firebase/src/main/res/values/firebase.xml | 10 ++++ .../src/nightly/res/values/firebase.xml | 4 ++ .../src/release/res/values/firebase.xml | 4 ++ 9 files changed, 31 insertions(+), 131 deletions(-) delete mode 100644 app/src/debug/google-services.json delete mode 100644 app/src/nightly/google-services.json delete mode 100644 app/src/release/google-services.json create mode 100644 libraries/pushproviders/firebase/README.md create mode 100644 libraries/pushproviders/firebase/src/debug/res/values/firebase.xml create mode 100644 libraries/pushproviders/firebase/src/main/res/values/firebase.xml create mode 100644 libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml create mode 100644 libraries/pushproviders/firebase/src/release/res/values/firebase.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 74ebfa89d4..772609f482 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,8 +33,8 @@ plugins { id("com.google.firebase.appdistribution") version "4.0.0" id("org.jetbrains.kotlinx.knit") version "0.4.0" id("kotlin-parcelize") - // TODO Move the plugin to the firebase module? - id("com.google.gms.google-services") + // To be able to update the firebase.xml files, uncomment and build the project + // id("com.google.gms.google-services") } android { diff --git a/app/src/debug/google-services.json b/app/src/debug/google-services.json deleted file mode 100644 index d9aa72f7ba..0000000000 --- a/app/src/debug/google-services.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "project_info": { - "project_number": "912726360885", - "firebase_url": "https://vector-alpha.firebaseio.com", - "project_id": "vector-alpha", - "storage_bucket": "vector-alpha.appspot.com" - }, - - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:912726360885:android:def0a4e454042e9b00427c", - "android_client_info": { - "package_name": "io.element.android.x.debug" - } - }, - "oauth_client": [ - { - "client_id": "912726360885-hvgoj23p6plt7hikhtdrakihojghaftv.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "io.element.android.x.debug", - "certificate_hash": "41bd63b3b612a15d9ba36a5245c393f2a9b992d1" - } - }, - { - "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} diff --git a/app/src/nightly/google-services.json b/app/src/nightly/google-services.json deleted file mode 100644 index 31b022b3f2..0000000000 --- a/app/src/nightly/google-services.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "project_info": { - "project_number": "912726360885", - "firebase_url": "https://vector-alpha.firebaseio.com", - "project_id": "vector-alpha", - "storage_bucket": "vector-alpha.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:912726360885:android:e17435e0beb0303000427c", - "android_client_info": { - "package_name": "io.element.android.x.nightly" - } - }, - "oauth_client": [ - { - "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} diff --git a/app/src/release/google-services.json b/app/src/release/google-services.json deleted file mode 100644 index 16fd1e855c..0000000000 --- a/app/src/release/google-services.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "project_info": { - "project_number": "912726360885", - "firebase_url": "https://vector-alpha.firebaseio.com", - "project_id": "vector-alpha", - "storage_bucket": "vector-alpha.appspot.com" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:912726360885:android:d097de99a4c23d2700427c", - "android_client_info": { - "package_name": "io.element.android.x" - } - }, - "oauth_client": [ - { - "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", - "client_type": 3 - } - ], - "api_key": [ - { - "current_key": "AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [ - { - "client_id": "912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com", - "client_type": 3 - } - ] - } - } - } - ], - "configuration_version": "1" -} diff --git a/libraries/pushproviders/firebase/README.md b/libraries/pushproviders/firebase/README.md new file mode 100644 index 0000000000..204ac6dd19 --- /dev/null +++ b/libraries/pushproviders/firebase/README.md @@ -0,0 +1,7 @@ +# Firebase + +## Configuration + +In order to make this module only know about Firebase, the plugin `com.google.gms.google-services` has been disabled from the `app` module. + +To be able to change the values in the file `firebase.xml` from this module, you should enable the plugin `com.google.gms.google-services` again, copy the file `google-services.json` to the folder `/app/src/main`, build the project, and check the generated file `app/build/generated/res/google-services//values/values.xml` to import the generated values into the `firebase.xml` files. diff --git a/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml b/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml new file mode 100644 index 0000000000..540f0e9bbe --- /dev/null +++ b/libraries/pushproviders/firebase/src/debug/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:def0a4e454042e9b00427c + diff --git a/libraries/pushproviders/firebase/src/main/res/values/firebase.xml b/libraries/pushproviders/firebase/src/main/res/values/firebase.xml new file mode 100644 index 0000000000..163717db91 --- /dev/null +++ b/libraries/pushproviders/firebase/src/main/res/values/firebase.xml @@ -0,0 +1,10 @@ + + + 912726360885-e87n3jva9uoj4vbidvijq78ebg02asv2.apps.googleusercontent.com + https://vector-alpha.firebaseio.com + 912726360885 + AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c + AIzaSyAFZX8IhIfgzdOZvxDP_ISO5WYoU7jmQ5c + vector-alpha.appspot.com + vector-alpha + diff --git a/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml b/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml new file mode 100644 index 0000000000..f793ba93f9 --- /dev/null +++ b/libraries/pushproviders/firebase/src/nightly/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:e17435e0beb0303000427c + diff --git a/libraries/pushproviders/firebase/src/release/res/values/firebase.xml b/libraries/pushproviders/firebase/src/release/res/values/firebase.xml new file mode 100644 index 0000000000..d563b43d05 --- /dev/null +++ b/libraries/pushproviders/firebase/src/release/res/values/firebase.xml @@ -0,0 +1,4 @@ + + + 1:912726360885:android:d097de99a4c23d2700427c + From 798629faf73a2b888c5403c1bfb7b72015df83bc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 13:00:16 +0200 Subject: [PATCH 30/83] Do check only on Debug --- .../android/libraries/matrix/api/core/EventId.kt | 10 +++------- .../android/libraries/matrix/api/core/RoomId.kt | 10 +++------- .../android/libraries/matrix/api/core/SessionId.kt | 10 +++------- .../android/libraries/matrix/api/core/SpaceId.kt | 10 +++------- .../android/libraries/matrix/api/core/ThreadId.kt | 10 +++------- .../android/libraries/matrix/api/core/UserId.kt | 10 +++------- 6 files changed, 18 insertions(+), 42 deletions(-) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt index b24d886ee8..a6e6d7edb3 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/EventId.kt @@ -22,12 +22,8 @@ import java.io.Serializable @JvmInline value class EventId(val value: String) : Serializable -fun String.asEventId() = if (MatrixPatterns.isEventId(this)) { - EventId(this) +fun String.asEventId() = if (BuildConfig.DEBUG && !MatrixPatterns.isEventId(this)) { + error("`$this` is not a valid event Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid event Id") - } else { - null - } + EventId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index f71f4ba4f9..e31b8063df 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -22,12 +22,8 @@ import java.io.Serializable @JvmInline value class RoomId(val value: String) : Serializable -fun String.asRoomId() = if (MatrixPatterns.isRoomId(this)) { - RoomId(this) +fun String.asRoomId() = if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { + error("`$this` is not a valid room Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid room Id") - } else { - null - } + RoomId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt index 0f0edf2299..f6d45dc6df 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SessionId.kt @@ -20,12 +20,8 @@ import io.element.android.libraries.matrix.api.BuildConfig typealias SessionId = UserId -fun String.asSessionId() = if (MatrixPatterns.isSessionId(this)) { - SessionId(this) +fun String.asSessionId() = if (BuildConfig.DEBUG && !MatrixPatterns.isSessionId(this)) { + error("`$this` is not a valid session Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid session Id") - } else { - null - } + SessionId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt index d4f2e43be6..342a13d693 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/SpaceId.kt @@ -27,12 +27,8 @@ value class SpaceId(val value: String) : Serializable */ val MAIN_SPACE = SpaceId("!mainSpace") -fun String.asSpaceId() = if (MatrixPatterns.isSpaceId(this)) { - SpaceId(this) +fun String.asSpaceId() = if (BuildConfig.DEBUG && !MatrixPatterns.isSpaceId(this)) { + error("`$this` is not a valid space Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid space Id") - } else { - null - } + SpaceId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt index f57cb8fa23..7599cd8a6a 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/ThreadId.kt @@ -22,12 +22,8 @@ import java.io.Serializable @JvmInline value class ThreadId(val value: String) : Serializable -fun String.asThreadId() = if (MatrixPatterns.isThreadId(this)) { - ThreadId(this) +fun String.asThreadId() = if (BuildConfig.DEBUG && !MatrixPatterns.isThreadId(this)) { + error("`$this` is not a valid thread Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid thread Id") - } else { - null - } + ThreadId(this) } diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index ba7028c926..46adcdd59c 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -22,12 +22,8 @@ import java.io.Serializable @JvmInline value class UserId(val value: String) : Serializable -fun String.asUserId() = if (MatrixPatterns.isUserId(this)) { - UserId(this) +fun String.asUserId() = if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { + error("`$this` is not a valid user Id") } else { - if (BuildConfig.DEBUG) { - error("`$this` is not a valid user Id") - } else { - null - } + UserId(this) } From 0874c076d6c001cd7f1142cdca52b9474110fffc Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 15:04:51 +0200 Subject: [PATCH 31/83] Deeplink: handle notification click to open a room. --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 8 ++++ .../io/element/android/x/MainActivity.kt | 22 ++++++++- .../kotlin/io/element/android/x/MainNode.kt | 20 +++++++- .../android/x/intent/IntentProviderImpl.kt | 20 ++++---- appnav/build.gradle.kts | 1 + .../android/appnav/LoggedInFlowNode.kt | 17 +++++-- .../io/element/android/appnav/RoomFlowNode.kt | 1 - .../io/element/android/appnav/RootFlowNode.kt | 30 ++++++++++++ libraries/deeplink/build.gradle.kts | 40 ++++++++++++++++ .../libraries/deeplink/DeepLinkCreator.kt | 39 +++++++++++++++ .../libraries/deeplink/DeeplinkData.kt | 27 +++++++++++ .../libraries/deeplink/DeeplinkParser.kt | 47 +++++++++++++++++++ .../push/impl/intent/IntentProvider.kt | 4 +- .../notifications/NotificationActionIds.kt | 1 - .../impl/notifications/NotificationUtils.kt | 19 ++------ tools/adb/deeplink.sh | 28 +++++++++++ 17 files changed, 292 insertions(+), 33 deletions(-) create mode 100644 libraries/deeplink/build.gradle.kts create mode 100644 libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt create mode 100644 libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt create mode 100644 libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt create mode 100755 tools/adb/deeplink.sh diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 772609f482..7a90fc62b8 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -206,6 +206,7 @@ dependencies { allLibrariesImpl() allServicesImpl() allFeaturesImpl(rootDir) + implementation(projects.libraries.deeplink) implementation(projects.tests.uitests) implementation(projects.anvilannotations) implementation(projects.appnav) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 828788ed80..342e05532c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -40,6 +40,14 @@ + + { + override fun init(node: MainNode) { + mainNode = node + mainNode.handleIntent(intent) + } + } + ) + ) } } } @@ -63,6 +79,8 @@ class MainActivity : NodeComponentActivity() { override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) Timber.w("onNewIntent") + intent ?: return + mainNode.handleIntent(intent) } override fun onSaveInstanceState(outState: Bundle) { diff --git a/app/src/main/kotlin/io/element/android/x/MainNode.kt b/app/src/main/kotlin/io/element/android/x/MainNode.kt index 6b7dee92b8..fb551f326d 100644 --- a/app/src/main/kotlin/io/element/android/x/MainNode.kt +++ b/app/src/main/kotlin/io/element/android/x/MainNode.kt @@ -16,14 +16,17 @@ package io.element.android.x +import android.content.Intent import android.os.Parcelable import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.lifecycle.lifecycleScope import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.navigation.model.permanent.PermanentNavModel import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.node.ParentNode +import com.bumble.appyx.core.plugin.Plugin import io.element.android.appnav.LoggedInFlowNode import io.element.android.appnav.RoomFlowNode import io.element.android.appnav.RootFlowNode @@ -35,11 +38,13 @@ import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.x.di.MainDaggerComponentsOwner import io.element.android.x.di.RoomComponent import io.element.android.x.di.SessionComponent +import kotlinx.coroutines.launch import kotlinx.parcelize.Parcelize class MainNode( buildContext: BuildContext, private val mainDaggerComponentOwner: MainDaggerComponentsOwner, + plugins: List, ) : ParentNode( navModel = PermanentNavModel( @@ -47,6 +52,7 @@ class MainNode( savedStateMap = buildContext.savedStateMap, ), buildContext = buildContext, + plugins = plugins, ), DaggerComponentOwner by mainDaggerComponentOwner { @@ -73,7 +79,13 @@ class MainNode( } override fun resolve(navTarget: RootNavTarget, buildContext: BuildContext): Node { - return createNode(buildContext, plugins = listOf(loggedInFlowNodeCallback, roomFlowNodeCallback)) + return createNode( + context = buildContext, + plugins = listOf( + loggedInFlowNodeCallback, + roomFlowNodeCallback, + ) + ) } @Composable @@ -81,6 +93,12 @@ class MainNode( Children(navModel = navModel) } + fun handleIntent(intent: Intent) { + lifecycleScope.launch { + waitForChildAttached().handleIntent(intent) + } + } + @Parcelize object RootNavTarget : Parcelable } diff --git a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt index b3c7aa98e0..e777b08906 100644 --- a/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt +++ b/app/src/main/kotlin/io/element/android/x/intent/IntentProviderImpl.kt @@ -18,7 +18,9 @@ package io.element.android.x.intent import android.content.Context import android.content.Intent +import androidx.core.net.toUri import com.squareup.anvil.annotations.ContributesBinding +import io.element.android.libraries.deeplink.DeepLinkCreator import io.element.android.libraries.di.AppScope import io.element.android.libraries.di.ApplicationContext import io.element.android.libraries.matrix.api.core.RoomId @@ -28,17 +30,19 @@ import io.element.android.libraries.push.impl.intent.IntentProvider import io.element.android.x.MainActivity import javax.inject.Inject -// TODO EAx change to deep-link. @ContributesBinding(AppScope::class) class IntentProviderImpl @Inject constructor( @ApplicationContext private val context: Context, + private val deepLinkCreator: DeepLinkCreator, ) : IntentProvider { - override fun getMainIntent(): Intent { - return Intent(context, MainActivity::class.java) - } - - override fun getIntent(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): Intent { - // TODO Handle deeplink or pass parameters - return Intent(context, MainActivity::class.java) + override fun getViewIntent( + sessionId: SessionId, + roomId: RoomId?, + threadId: ThreadId?, + ): Intent { + return Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_VIEW + data = deepLinkCreator.create(sessionId, roomId, threadId).toUri() + } } } diff --git a/appnav/build.gradle.kts b/appnav/build.gradle.kts index 17efdc15fc..b672f582f9 100644 --- a/appnav/build.gradle.kts +++ b/appnav/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { implementation(projects.libraries.core) implementation(projects.libraries.androidutils) implementation(projects.libraries.architecture) + implementation(projects.libraries.deeplink) implementation(projects.libraries.matrix.api) implementation(projects.libraries.push.api) implementation(projects.libraries.pushproviders.api) diff --git a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt index 6f9319f923..fe2d8aa0dc 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/LoggedInFlowNode.kt @@ -33,6 +33,7 @@ import com.bumble.appyx.core.plugin.plugins import com.bumble.appyx.navmodel.backstack.BackStack import com.bumble.appyx.navmodel.backstack.operation.push import com.bumble.appyx.navmodel.backstack.operation.replace +import com.bumble.appyx.navmodel.backstack.operation.singleTop import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode @@ -56,10 +57,7 @@ import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.di.MatrixUIBindings import io.element.android.services.appnavstate.api.AppNavigationStateService import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking import kotlinx.parcelize.Parcelize -import kotlin.coroutines.coroutineContext @ContributesNode(AppScope::class) class LoggedInFlowNode @AssistedInject constructor( @@ -217,6 +215,19 @@ class LoggedInFlowNode @AssistedInject constructor( } } + suspend fun attachRoot(): Node { + return attachChild { + backstack.singleTop(NavTarget.RoomList) + } + } + + suspend fun attachRoom(roomId: RoomId): RoomFlowNode { + return attachChild { + backstack.singleTop(NavTarget.RoomList) + backstack.push(NavTarget.Room(roomId)) + } + } + @Composable override fun View(modifier: Modifier) { Box(modifier = modifier) { diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt index 69c02d500e..3609dbf57e 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RoomFlowNode.kt @@ -18,7 +18,6 @@ package io.element.android.appnav import android.os.Parcelable import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Modifier import com.bumble.appyx.core.composable.Children import com.bumble.appyx.core.lifecycle.subscribe diff --git a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt index 8251584308..519c4e734b 100644 --- a/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt +++ b/appnav/src/main/kotlin/io/element/android/appnav/RootFlowNode.kt @@ -17,6 +17,7 @@ package io.element.android.appnav import android.app.Activity +import android.content.Intent import android.os.Parcelable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -45,6 +46,8 @@ import io.element.android.features.rageshake.api.bugreport.BugReportEntryPoint import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.deeplink.DeeplinkData +import io.element.android.libraries.deeplink.DeeplinkParser import io.element.android.libraries.designsystem.theme.components.CircularProgressIndicator import io.element.android.libraries.di.AppScope import io.element.android.libraries.matrix.api.auth.MatrixAuthenticationService @@ -65,6 +68,7 @@ class RootFlowNode @AssistedInject constructor( private val matrixClientsHolder: MatrixClientsHolder, private val presenter: RootPresenter, private val bugReportEntryPoint: BugReportEntryPoint, + private val deeplinkParser: DeeplinkParser, ) : BackstackNode( backstack = BackStack( @@ -207,4 +211,30 @@ class RootFlowNode @AssistedInject constructor( CircularProgressIndicator() } } + + suspend fun handleIntent(intent: Intent) { + deeplinkParser.getFromIntent(intent) + ?.let { navigateTo(it) } + } + + private suspend fun navigateTo(deeplinkData: DeeplinkData) { + Timber.d("Navigating to $deeplinkData") + attachSession(deeplinkData.sessionId) + .apply { + val roomId = deeplinkData.roomId + if (roomId == null) { + // In case room is not provided, ensure the app navigate back to the room list + attachRoot() + } else { + attachRoom(roomId) + // TODO .attachThread(deeplinkData.threadId) + } + } + } + + private suspend fun attachSession(sessionId: SessionId): LoggedInFlowNode { + return attachChild { + backstack.newRoot(NavTarget.LoggedInFlow(sessionId)) + } + } } diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts new file mode 100644 index 0000000000..5d28470cfc --- /dev/null +++ b/libraries/deeplink/build.gradle.kts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// TODO: Remove once https://youtrack.jetbrains.com/issue/KTIJ-19369 is fixed +@Suppress("DSL_SCOPE_VIOLATION") +plugins { + id("io.element.android-library") + alias(libs.plugins.anvil) +} + +android { + namespace = "io.element.android.libraries.deeplink" +} + +anvil { + generateDaggerFactories.set(true) +} + +dependencies { + implementation(projects.libraries.di) + implementation(libs.dagger) + implementation(libs.androidx.corektx) + implementation(projects.libraries.matrix.api) + + testImplementation(libs.test.junit) + testImplementation(libs.test.truth) +} diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt new file mode 100644 index 0000000000..a135988ca0 --- /dev/null +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.deeplink + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId +import javax.inject.Inject + +class DeepLinkCreator @Inject constructor() { + fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String { + return buildString { + append("elementx://open/") + append(sessionId.value) + if (roomId != null) { + append("/") + append(roomId.value) + if (threadId != null) { + append("/") + append(threadId.value) + } + } + } + } +} diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt new file mode 100644 index 0000000000..d393a37c16 --- /dev/null +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkData.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.deeplink + +import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.SessionId +import io.element.android.libraries.matrix.api.core.ThreadId + +data class DeeplinkData( + val sessionId: SessionId, + val roomId: RoomId? = null, + val threadId: ThreadId? = null, +) diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt new file mode 100644 index 0000000000..2affc50818 --- /dev/null +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.deeplink + +import android.content.Intent +import android.net.Uri +import io.element.android.libraries.matrix.api.core.asRoomId +import io.element.android.libraries.matrix.api.core.asSessionId +import io.element.android.libraries.matrix.api.core.asThreadId +import javax.inject.Inject + +class DeeplinkParser @Inject constructor() { + fun getFromIntent(intent: Intent): DeeplinkData? { + return intent + .takeIf { it.action == Intent.ACTION_VIEW } + ?.data + ?.toDeeplinkData() + } + + private fun Uri.toDeeplinkData(): DeeplinkData? { + if (scheme != "elementx") return null + if (host != "open") return null + val pathBits = path.orEmpty().split("/").drop(1) + val sessionId = pathBits.elementAtOrNull(0)?.asSessionId() ?: return null + val roomId = pathBits.elementAtOrNull(1)?.asRoomId() + val threadId = pathBits.elementAtOrNull(2)?.asThreadId() + return DeeplinkData( + sessionId = sessionId, + roomId = roomId, + threadId = threadId, + ) + } +} diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt index 52abb3f6a4..ce2b1d3fce 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/intent/IntentProvider.kt @@ -25,9 +25,7 @@ interface IntentProvider { /** * Provide an intent to start the application. */ - fun getMainIntent(): Intent - - fun getIntent( + fun getViewIntent( sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?, diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt index 56054bbef8..bc20d49917 100644 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationActionIds.kt @@ -34,7 +34,6 @@ data class NotificationActionIds @Inject constructor( val smartReply = "${buildMeta.applicationId}.NotificationActions.SMART_REPLY_ACTION" val dismissSummary = "${buildMeta.applicationId}.NotificationActions.DISMISS_SUMMARY_ACTION" val dismissRoom = "${buildMeta.applicationId}.NotificationActions.DISMISS_ROOM_NOTIF_ACTION" - val tapToView = "${buildMeta.applicationId}.NotificationActions.TAP_TO_VIEW_ACTION" val diagnostic = "${buildMeta.applicationId}.NotificationActions.DIAGNOSTIC" val push = "${buildMeta.applicationId}.PUSH" } diff --git a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt index add8fd74eb..8aeaa998ca 100755 --- a/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt +++ b/libraries/push/impl/src/main/kotlin/io/element/android/libraries/push/impl/notifications/NotificationUtils.kt @@ -482,15 +482,11 @@ class NotificationUtils @Inject constructor( } private fun buildOpenRoomIntent(sessionId: SessionId, roomId: RoomId): PendingIntent? { - val roomIntent = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = null) - roomIntent.action = actionIds.tapToView - // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that - roomIntent.data = createIgnoredUri("openRoom?$sessionId&$roomId") - + val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = null) return PendingIntent.getActivity( context, clock.epochMillis().toInt(), - roomIntent, + intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } @@ -498,22 +494,17 @@ class NotificationUtils @Inject constructor( private fun buildOpenThreadIntent(roomInfo: RoomEventGroupInfo, threadId: ThreadId?): PendingIntent? { val sessionId = roomInfo.sessionId val roomId = roomInfo.roomId - val threadIntentTap = intentProvider.getIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) - threadIntentTap.action = actionIds.tapToView - // pending intent get reused by system, this will mess up the extra params, so put unique info to avoid that - threadIntentTap.data = createIgnoredUri("openThread?$sessionId&$roomId&$threadId") - + val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = roomId, threadId = threadId) return PendingIntent.getActivity( context, clock.epochMillis().toInt(), - threadIntentTap, + intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) } private fun buildOpenHomePendingIntentForSummary(sessionId: SessionId): PendingIntent { - val intent = intentProvider.getIntent(sessionId = sessionId, roomId = null, threadId = null) - intent.data = createIgnoredUri("tapSummary?$sessionId") + val intent = intentProvider.getViewIntent(sessionId = sessionId, roomId = null, threadId = null) return PendingIntent.getActivity( context, clock.epochMillis().toInt(), diff --git a/tools/adb/deeplink.sh b/tools/adb/deeplink.sh new file mode 100755 index 0000000000..a88d1083b3 --- /dev/null +++ b/tools/adb/deeplink.sh @@ -0,0 +1,28 @@ +#! /bin/bash +# +# Copyright (c) 2023 New Vector Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Format is: +# elementx://open/{sessionId} to open a session +# elementx://open/{sessionId}/{roomId} to open a room +# elementx://open/{sessionId}/{roomId}/{eventId} to open an event + +# Open a session +# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org +# Open a room +adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org +# Open a thread +# adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org/!dehdDVSkabQLZFYrgo:matrix.org/\\\$threadId From da6a391cc6bdc4bef23062b66e6359515ca4ff1b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 15:22:03 +0200 Subject: [PATCH 32/83] Add test for DeepLinkCreator --- libraries/deeplink/build.gradle.kts | 1 + .../android/libraries/deeplink/Constants.kt | 20 ++++++++++ .../libraries/deeplink/DeepLinkCreator.kt | 2 +- .../libraries/deeplink/DeeplinkParser.kt | 4 +- .../libraries/deeplink/DeepLinkCreatorTest.kt | 37 +++++++++++++++++++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt create mode 100644 libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts index 5d28470cfc..a377d0c2a7 100644 --- a/libraries/deeplink/build.gradle.kts +++ b/libraries/deeplink/build.gradle.kts @@ -37,4 +37,5 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(projects.libraries.matrix.test) } diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt new file mode 100644 index 0000000000..df26ef2fa0 --- /dev/null +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/Constants.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.deeplink + +internal const val SCHEME = "elementx" +internal const val HOST = "open" diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt index a135988ca0..71aa7ebddd 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeepLinkCreator.kt @@ -24,7 +24,7 @@ import javax.inject.Inject class DeepLinkCreator @Inject constructor() { fun create(sessionId: SessionId, roomId: RoomId?, threadId: ThreadId?): String { return buildString { - append("elementx://open/") + append("$SCHEME://$HOST/") append(sessionId.value) if (roomId != null) { append("/") diff --git a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt index 2affc50818..9f217f497e 100644 --- a/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt +++ b/libraries/deeplink/src/main/kotlin/io/element/android/libraries/deeplink/DeeplinkParser.kt @@ -32,8 +32,8 @@ class DeeplinkParser @Inject constructor() { } private fun Uri.toDeeplinkData(): DeeplinkData? { - if (scheme != "elementx") return null - if (host != "open") return null + if (scheme != SCHEME) return null + if (host != HOST) return null val pathBits = path.orEmpty().split("/").drop(1) val sessionId = pathBits.elementAtOrNull(0)?.asSessionId() ?: return null val roomId = pathBits.elementAtOrNull(1)?.asRoomId() diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt new file mode 100644 index 0000000000..730bdde248 --- /dev/null +++ b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeepLinkCreatorTest.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.deeplink + +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID +import org.junit.Test + +class DeepLinkCreatorTest { + + @Test + fun create() { + val sut = DeepLinkCreator() + assertThat(sut.create(A_SESSION_ID, null, null)) + .isEqualTo("elementx://open/@alice:server.org") + assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, null)) + .isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain") + assertThat(sut.create(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID)) + .isEqualTo("elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId") + } +} From 54659083653c374e56aa57bf017b588866a36bc7 Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 15:38:54 +0200 Subject: [PATCH 33/83] Add test for DeeplinkParser --- libraries/deeplink/build.gradle.kts | 2 + .../libraries/deeplink/DeeplinkParserTest.kt | 78 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt diff --git a/libraries/deeplink/build.gradle.kts b/libraries/deeplink/build.gradle.kts index a377d0c2a7..3fe27bfd1c 100644 --- a/libraries/deeplink/build.gradle.kts +++ b/libraries/deeplink/build.gradle.kts @@ -37,5 +37,7 @@ dependencies { testImplementation(libs.test.junit) testImplementation(libs.test.truth) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) + testImplementation(projects.tests.testutils) } diff --git a/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt new file mode 100644 index 0000000000..259cd6fde1 --- /dev/null +++ b/libraries/deeplink/src/test/kotlin/io/element/android/libraries/deeplink/DeeplinkParserTest.kt @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.deeplink + +import android.content.Intent +import androidx.core.net.toUri +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.A_ROOM_ID +import io.element.android.libraries.matrix.test.A_SESSION_ID +import io.element.android.libraries.matrix.test.A_THREAD_ID +import io.element.android.tests.testutils.assertNullOrThrow +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class DeeplinkParserTest { + companion object { + const val A_URI = + "elementx://open/@alice:server.org" + const val A_URI_WITH_ROOM = + "elementx://open/@alice:server.org/!aRoomId:domain" + const val A_URI_WITH_ROOM_WITH_THREAD = + "elementx://open/@alice:server.org/!aRoomId:domain/\$aThreadId" + } + + private val sut = DeeplinkParser() + + @Test + fun `nominal cases`() { + assertThat(sut.getFromIntent(createIntent(A_URI))) + .isEqualTo(DeeplinkData(A_SESSION_ID, null, null)) + assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM))) + .isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, null)) + assertThat(sut.getFromIntent(createIntent(A_URI_WITH_ROOM_WITH_THREAD))) + .isEqualTo(DeeplinkData(A_SESSION_ID, A_ROOM_ID, A_THREAD_ID)) + } + + @Test + fun `error cases`() { + val sut = DeeplinkParser() + // Bad scheme + assertThat(sut.getFromIntent(createIntent("x://open/@alice:server.org"))).isNull() + // Bad host + assertThat(sut.getFromIntent(createIntent("elementx://close/@alice:server.org"))).isNull() + // No session Id + assertThat(sut.getFromIntent(createIntent("elementx://open"))).isNull() + // Invalid sessionId + assertNullOrThrow { + sut.getFromIntent(createIntent("elementx://open/alice:server.org")) + } + // Empty sessionId + assertNullOrThrow { + sut.getFromIntent(createIntent("elementx://open//")) + } + } + + private fun createIntent(uri: String): Intent { + return Intent().apply { + action = Intent.ACTION_VIEW + data = uri.toUri() + } + } +} From 0a926bd05ae0c470e5f21a6aa924128410e223df Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 09:28:24 +0200 Subject: [PATCH 34/83] Navigate from people view to configuration view --- .../createroom/impl/CreateRoomFlowNode.kt | 21 +++++-- .../impl/addpeople/AddPeopleNode.kt | 12 +++- .../impl/addpeople/AddPeopleView.kt | 5 +- .../impl/configureroom/ConfigureRoomEvents.kt | 22 +++++++ .../impl/configureroom/ConfigureRoomNode.kt | 56 ++++++++++++++++++ .../configureroom/ConfigureRoomPresenter.kt | 48 +++++++++++++++ .../ConfigureRoomPresenterArgs.kt | 23 ++++++++ .../impl/configureroom/ConfigureRoomState.kt | 24 ++++++++ .../ConfigureRoomStateProvider.kt | 32 ++++++++++ .../impl/configureroom/ConfigureRoomView.kt | 58 +++++++++++++++++++ libraries/designsystem/build.gradle.kts | 1 + .../components/avatar/AvatarData.kt | 7 ++- libraries/matrixui/build.gradle.kts | 1 + .../libraries/matrix/ui/model/MatrixUser.kt | 5 +- 14 files changed, 306 insertions(+), 9 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 22851904c4..137017a2ec 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -31,12 +31,14 @@ import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.impl.addpeople.AddPeopleNode +import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -58,12 +60,15 @@ class CreateRoomFlowNode @AssistedInject constructor( @Parcelize object NewRoom : NavTarget + + @Parcelize + data class ConfigureRoom(val users: List) : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { - val callback = object : CreateRoomRootNode.Callback { + createNode(context = buildContext, plugins = listOf(object : CreateRoomRootNode.Callback { override fun onCreateNewRoom() { backstack.push(NavTarget.NewRoom) } @@ -71,10 +76,18 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun onOpenRoom(roomId: RoomId) { plugins().forEach { it.onOpenRoom(roomId) } } - } - createNode(buildContext, plugins = listOf(callback)) + })) + } + NavTarget.NewRoom -> { + createNode(context = buildContext, plugins = listOf(object : AddPeopleNode.Callback { + override fun onContinue(selectedUsers: List) { + backstack.push(NavTarget.ConfigureRoom(selectedUsers)) + } + })) + } + is NavTarget.ConfigureRoom -> { + createNode(context = buildContext, plugins = listOf(ConfigureRoomNode.Inputs(navTarget.users))) } - NavTarget.NewRoom -> createNode(buildContext) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt index 5393075d18..91b1d5a721 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt @@ -21,10 +21,12 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.ui.model.MatrixUser @ContributesNode(SessionScope::class) class AddPeopleNode @AssistedInject constructor( @@ -33,6 +35,14 @@ class AddPeopleNode @AssistedInject constructor( private val presenter: AddPeoplePresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun onContinue(selectedUsers: List) + } + + private fun onContinue(selectedUsers: List) { + plugins().forEach { it.onContinue(selectedUsers) } + } + @Composable override fun View(modifier: Modifier) { val state = presenter.present() @@ -40,7 +50,7 @@ class AddPeopleNode @AssistedInject constructor( state = state, modifier = modifier, onBackPressed = { navigateUp() }, - onNextPressed = { }, + onNextPressed = this::onContinue, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index 56a16b24f9..e74a1477d2 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -38,6 +38,7 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton +import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @@ -46,7 +47,7 @@ fun AddPeopleView( state: AddPeopleState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onNextPressed: () -> Unit = {}, + onNextPressed: (List) -> Unit = {}, ) { val eventSink = state.eventSink @@ -56,7 +57,7 @@ fun AddPeopleView( AddPeopleViewTopBar( hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, - onNextPressed = onNextPressed, + onNextPressed = { onNextPressed(state.selectUsersState.selectedUsers) }, ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt new file mode 100644 index 0000000000..d1ba2c8ebe --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +// TODO Add your events or remove the file completely if no events +sealed interface ConfigureRoomEvents { + object MyEvent : ConfigureRoomEvents +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt new file mode 100644 index 0000000000..6e11e492b8 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.ui.model.MatrixUser + +@ContributesNode(SessionScope::class) +class ConfigureRoomNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + private val presenterFactory: ConfigureRoomPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val selectedUsers: List + ) : NodeInputs + + private val inputs: Inputs = inputs() + private val presenter by lazy { + presenterFactory.create(ConfigureRoomPresenterArgs(inputs.selectedUsers)) + } + + @Composable + override fun View(modifier: Modifier) { + val state = presenter.present() + ConfigureRoomView( + state = state, + modifier = modifier, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt new file mode 100644 index 0000000000..243769e90d --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +import androidx.compose.runtime.Composable +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter + +class ConfigureRoomPresenter @AssistedInject constructor( + @Assisted val args: ConfigureRoomPresenterArgs, +) : Presenter { + + @AssistedFactory + interface Factory { + fun create(args: ConfigureRoomPresenterArgs): ConfigureRoomPresenter + } + + @Composable + override fun present(): ConfigureRoomState { + + fun handleEvents(event: ConfigureRoomEvents) { + when (event) { + ConfigureRoomEvents.MyEvent -> Unit + } + } + + return ConfigureRoomState( + selectedUsers = args.selectedUsers, + eventSink = ::handleEvents, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt new file mode 100644 index 0000000000..8969a2a5fa --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterArgs.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +import io.element.android.libraries.matrix.ui.model.MatrixUser + +data class ConfigureRoomPresenterArgs( + val selectedUsers: List, +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt new file mode 100644 index 0000000000..b06f4e29b9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +import io.element.android.libraries.matrix.ui.model.MatrixUser + +data class ConfigureRoomState( + val selectedUsers: List, + val eventSink: (ConfigureRoomEvents) -> Unit +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt new file mode 100644 index 0000000000..f9701fdaa6 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class ConfigureRoomStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aConfigureRoomState(), + // Add other state here + ) +} + +fun aConfigureRoomState() = ConfigureRoomState( + selectedUsers = emptyList(), + eventSink = {} +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt new file mode 100644 index 0000000000..43c4a84f03 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun ConfigureRoomView( + state: ConfigureRoomState, + modifier: Modifier = Modifier, +) { + Box(modifier, contentAlignment = Alignment.Center) { + Text( + "ConfigureRoom feature view", + color = MaterialTheme.colorScheme.primary, + ) + } +} + +@Preview +@Composable +fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun ConfigureRoomViewDarkPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: ConfigureRoomState) { + ConfigureRoomView( + state = state, + ) +} diff --git a/libraries/designsystem/build.gradle.kts b/libraries/designsystem/build.gradle.kts index 45430e5d82..4533c950b1 100644 --- a/libraries/designsystem/build.gradle.kts +++ b/libraries/designsystem/build.gradle.kts @@ -19,6 +19,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt index 7f2cddea09..3bf4f7d0b4 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarData.kt @@ -16,15 +16,20 @@ package io.element.android.libraries.designsystem.components.avatar +import android.os.Parcelable import androidx.compose.runtime.Immutable +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize @Immutable +@Parcelize data class AvatarData( val id: String, val name: String?, val url: String? = null, + @IgnoredOnParcel val size: AvatarSize = AvatarSize.MEDIUM -) { +) : Parcelable { fun getInitial(): String { val firstChar = name?.firstOrNull() ?: id.getOrNull(1) ?: '?' return firstChar.uppercase() diff --git a/libraries/matrixui/build.gradle.kts b/libraries/matrixui/build.gradle.kts index c9dead5eb4..6d2109d3f8 100644 --- a/libraries/matrixui/build.gradle.kts +++ b/libraries/matrixui/build.gradle.kts @@ -20,6 +20,7 @@ plugins { id("io.element.android-compose-library") alias(libs.plugins.anvil) alias(libs.plugins.ksp) + id("kotlin-parcelize") } android { diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt index c524cbb733..f0ddb30a93 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/model/MatrixUser.kt @@ -16,16 +16,19 @@ package io.element.android.libraries.matrix.ui.model +import android.os.Parcelable import androidx.compose.runtime.Immutable import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.UserId +import kotlinx.parcelize.Parcelize +@Parcelize @Immutable data class MatrixUser( val id: UserId, val username: String? = null, val avatarData: AvatarData = AvatarData(id.value, username), -) +) : Parcelable fun MatrixUser.getBestName(): String { return username?.takeIf { it.isNotEmpty() } ?: id.value From ac5f50d264a8899c52043cb7b45dd7075103ea38 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 15:02:33 +0200 Subject: [PATCH 35/83] WIP create room screen --- features/createroom/impl/build.gradle.kts | 1 + .../configureroom/ConfigureRoomPresenter.kt | 4 +- .../impl/configureroom/ConfigureRoomState.kt | 5 +- .../ConfigureRoomStateProvider.kt | 5 +- .../impl/configureroom/ConfigureRoomView.kt | 195 +++++++++++++++++- .../features/userlist/api/UserListView.kt | 3 + 6 files changed, 207 insertions(+), 6 deletions(-) diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 6f2544822c..77a5ed26be 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -49,6 +49,7 @@ dependencies { implementation(projects.libraries.uiStrings) implementation(projects.features.userlist.api) api(projects.features.createroom.api) + implementation(libs.coil.compose) // FIXME temp testImplementation(libs.test.junit) testImplementation(libs.coroutines.test) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 243769e90d..25851e41af 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -21,6 +21,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.toImmutableList class ConfigureRoomPresenter @AssistedInject constructor( @Assisted val args: ConfigureRoomPresenterArgs, @@ -41,7 +42,8 @@ class ConfigureRoomPresenter @AssistedInject constructor( } return ConfigureRoomState( - selectedUsers = args.selectedUsers, + selectedUsers = args.selectedUsers.toImmutableList(), + avatarUri = null, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index b06f4e29b9..b8eee9e7bf 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -16,9 +16,12 @@ package io.element.android.features.createroom.impl.configureroom +import android.net.Uri import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList data class ConfigureRoomState( - val selectedUsers: List, + val selectedUsers: ImmutableList, + val avatarUri: Uri?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index f9701fdaa6..30100c720b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -17,6 +17,8 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.collections.immutable.persistentListOf open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -27,6 +29,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Unit = {}, + onCreatePressed: () -> Unit = {}, ) { - Box(modifier, contentAlignment = Alignment.Center) { + Scaffold( + modifier = modifier, + topBar = { + ConfigureRoomToolbar( + isNextActionEnabled = false, + onBackPressed = onBackPressed, + onNextPressed = onCreatePressed, + ) + } + ) { padding -> + Column( + modifier = Modifier.padding(padding), + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + RoomNameWithAvatar( + modifier = Modifier.padding(horizontal = 16.dp), + ) + RoomTopic( + modifier = Modifier.padding(horizontal = 16.dp), + ) + SelectedUsersList( + listState = LazyListState(), // FIXME + contentPadding = PaddingValues(horizontal = 24.dp), + selectedUsers = state.selectedUsers, + onUserRemoved = { + // TODO + }, + ) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ConfigureRoomToolbar( + isNextActionEnabled: Boolean, + modifier: Modifier = Modifier, + onBackPressed: () -> Unit = {}, + onNextPressed: () -> Unit = {}, +) { + CenterAlignedTopAppBar( + modifier = modifier, + title = { + Text( + text = "Create a room", + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + ) + }, + navigationIcon = { BackButton(onClick = onBackPressed) }, + actions = { + TextButton( + modifier = Modifier.padding(horizontal = 8.dp), + enabled = isNextActionEnabled, + onClick = onNextPressed, + ) { + Text( + text = "Create", + fontSize = 16.sp, + ) + } + } + ) +} + +@Composable +fun RoomNameWithAvatar( + modifier: Modifier = Modifier, + avatarUri: Uri? = null, + roomName: String = "", + onAvatarClick: () -> Unit = {}, + onRoomNameChanged: (String) -> Unit = {}, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Avatar( + avatarUri = avatarUri, + onClick = onAvatarClick, + ) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = "Room name" + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = roomName, + placeholder = { Text("e.g. Product Sprint") }, + onValueChange = onRoomNameChanged, + maxLines = 1, + ) + } + } +} + +@Composable +fun Avatar( + modifier: Modifier = Modifier, + avatarUri: Uri? = null, + onClick: () -> Unit = {}, +) { + val commonModifier = modifier + .size(70.dp) + .clip(CircleShape) + .clickable(onClick = onClick) + + if (avatarUri != null) { + val context = LocalContext.current + val model = ImageRequest.Builder(context) + .data(avatarUri) + .build() + AsyncImage( + model = model, + contentDescription = null, + modifier = commonModifier, + ) + } else { + Box( + modifier = commonModifier + .background(LocalColors.current.quinary) + ) { + Icon( + imageVector = Icons.Outlined.AddAPhoto, + contentDescription = "", + modifier = modifier + .align(Alignment.Center) + .size(40.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + } + } +} + +@Composable +fun RoomTopic( + modifier: Modifier = Modifier, + topic: String = "", + onTopicChanged: (String) -> Unit = {}, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { Text( - "ConfigureRoom feature view", - color = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(horizontal = 16.dp), + text = "Topic (optional)", + ) + TextField( + modifier = Modifier.fillMaxWidth(), + value = topic, + placeholder = { Text("What is this room about?") }, + onValueChange = onTopicChanged, + maxLines = 3, ) } } diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt index bc355a0a26..5605ed6028 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -244,11 +245,13 @@ fun SelectedUsersList( listState: LazyListState, selectedUsers: ImmutableList, modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), onUserRemoved: (MatrixUser) -> Unit = {}, ) { LazyRow( state = listState, modifier = modifier, + contentPadding = contentPadding, horizontalArrangement = Arrangement.spacedBy(24.dp), ) { items(selectedUsers.toList()) { matrixUser -> From 6c4cc71d3f6164ff70695fb9288c314942cc8e90 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Mon, 3 Apr 2023 15:11:22 +0200 Subject: [PATCH 36/83] Use content padding --- .../io/element/android/features/userlist/api/UserListView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt index 5605ed6028..5f587b471e 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt @@ -98,7 +98,7 @@ fun UserListView( if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) { SelectedUsersList( listState = state.selectedUsersListState, - modifier = Modifier.padding(16.dp), + contentPadding = PaddingValues(16.dp), selectedUsers = state.selectedUsers, onUserRemoved = { state.eventSink(UserListEvents.RemoveFromSelection(it)) @@ -175,7 +175,7 @@ fun SearchUserBar( if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { SelectedUsersList( listState = selectedUsersListState, - modifier = Modifier.padding(16.dp), + contentPadding = PaddingValues(16.dp), selectedUsers = selectedUsers, onUserRemoved = onUserDeselected, ) From 66b672e6554a339412fb57a94c1ea64f6010a109 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 16:42:06 +0200 Subject: [PATCH 37/83] Change wording of create a room button --- .../features/createroom/impl/root/CreateRoomRootView.kt | 2 +- ...CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...reateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index e488645b78..4e12dcaab0 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -156,7 +156,7 @@ fun CreateRoomActionButtonsList( Column(modifier = modifier) { CreateRoomActionButton( iconRes = DrawableR.drawable.ic_groups, - text = stringResource(id = R.string.screen_create_room_action_create_room), + text = stringResource(id = StringR.string.action_create_a_room), onClick = onNewRoomClicked, ) CreateRoomActionButton( diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 1c22798a9a..de4377ecc9 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:365161fefa9ea3c82bf3ed2527b4847df27860266e1d5f0e770962e95154b4a6 -size 19178 +oid sha256:6f36c2b4f4266048d5295df08f380e4128630d11cce7ac11cf3a0eaaa5594d61 +size 19924 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index d404485dee..0bdbed7968 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.root_null_DefaultGroup_CreateRoomRootViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3f4413c59f47c45c06433f53c3f1fb7e9095bb47e3d8c3e7fabefcb19cdd5146 -size 18346 +oid sha256:23b9e996b8f0cc2efcb3adb6294bfa7b4a53fefbb7b6ee07add4105da9b9d40e +size 18984 From 97ade693f54eae2881941ac933a2bbcbd126e50a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 17:26:00 +0200 Subject: [PATCH 38/83] Add fake list of matrix users --- .../configureroom/ConfigureRoomStateProvider.kt | 6 +++--- .../matrix/ui/components/MatrixUserProvider.kt | 13 +++++++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 30100c720b..2bc7f62ee8 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -17,8 +17,8 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.ui.components.aMatrixUser -import kotlinx.collections.immutable.persistentListOf +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.toImmutableList open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -29,7 +29,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( From 470afea802c9d1a4583213aff5a92053fe516fcb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 4 Apr 2023 17:41:31 +0200 Subject: [PATCH 39/83] Add topic and room name to the state --- .../impl/configureroom/ConfigureRoomEvents.kt | 7 +++++-- .../impl/configureroom/ConfigureRoomPresenter.kt | 16 ++++++++++++++-- .../impl/configureroom/ConfigureRoomState.kt | 2 ++ .../configureroom/ConfigureRoomStateProvider.kt | 2 ++ .../impl/configureroom/ConfigureRoomView.kt | 13 +++++++++---- 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index d1ba2c8ebe..db90c58834 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -16,7 +16,10 @@ package io.element.android.features.createroom.impl.configureroom -// TODO Add your events or remove the file completely if no events +import android.net.Uri + sealed interface ConfigureRoomEvents { - object MyEvent : ConfigureRoomEvents + data class RoomNameChanged(val name: String) : ConfigureRoomEvents + data class TopicChanged(val topic: String) : ConfigureRoomEvents + data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 25851e41af..56962647ad 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -16,7 +16,12 @@ package io.element.android.features.createroom.impl.configureroom +import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -34,16 +39,23 @@ class ConfigureRoomPresenter @AssistedInject constructor( @Composable override fun present(): ConfigureRoomState { + var roomName by rememberSaveable { mutableStateOf("") } + var topic by rememberSaveable { mutableStateOf("") } + var avatarUri by rememberSaveable { mutableStateOf(null) } fun handleEvents(event: ConfigureRoomEvents) { when (event) { - ConfigureRoomEvents.MyEvent -> Unit + is ConfigureRoomEvents.AvatarUriChanged -> avatarUri = event.uri + is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name + is ConfigureRoomEvents.TopicChanged -> topic = event.topic } } return ConfigureRoomState( selectedUsers = args.selectedUsers.toImmutableList(), - avatarUri = null, + roomName = roomName, + topic = topic, + avatarUri = avatarUri, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index b8eee9e7bf..d8a3635d35 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -22,6 +22,8 @@ import kotlinx.collections.immutable.ImmutableList data class ConfigureRoomState( val selectedUsers: ImmutableList, + val roomName: String, + val topic: String, val avatarUri: Uri?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 2bc7f62ee8..2838d77f23 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -30,6 +30,8 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Unit = {}, onRoomNameChanged: (String) -> Unit = {}, ) { @@ -170,7 +175,7 @@ fun RoomNameWithAvatar( @Composable fun Avatar( modifier: Modifier = Modifier, - avatarUri: Uri? = null, + avatarUri: Uri?, onClick: () -> Unit = {}, ) { val commonModifier = modifier @@ -208,7 +213,7 @@ fun Avatar( @Composable fun RoomTopic( modifier: Modifier = Modifier, - topic: String = "", + topic: String, onTopicChanged: (String) -> Unit = {}, ) { Column( From 5e88b2337280a3d55e6575644737742dcf67d5f0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 16:28:55 +0200 Subject: [PATCH 40/83] Fix build --- .../features/createroom/impl/addpeople/AddPeopleView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index e74a1477d2..eed59923a8 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -29,8 +29,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import io.element.android.features.userlist.api.UserListView import io.element.android.features.createroom.impl.R +import io.element.android.features.userlist.api.UserListView import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight @@ -57,7 +57,7 @@ fun AddPeopleView( AddPeopleViewTopBar( hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, - onNextPressed = { onNextPressed(state.selectUsersState.selectedUsers) }, + onNextPressed = { onNextPressed(state.userListState.selectedUsers) }, ) } } From bb48f5f378c1d24d172366e2b21332711dbbffec Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 5 Apr 2023 16:28:09 +0200 Subject: [PATCH 41/83] Room visibility --- .../impl/configureroom/ConfigureRoomEvents.kt | 1 + .../configureroom/ConfigureRoomPresenter.kt | 3 + .../impl/configureroom/ConfigureRoomState.kt | 1 + .../ConfigureRoomStateProvider.kt | 1 + .../impl/configureroom/ConfigureRoomView.kt | 125 +++++++++++++++++- .../impl/configureroom/RoomPrivacy.kt | 22 +++ .../theme/components/RadioButton.kt | 63 +++++++++ 7 files changed, 209 insertions(+), 7 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt create mode 100644 libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index db90c58834..7322dbd961 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -22,4 +22,5 @@ sealed interface ConfigureRoomEvents { data class RoomNameChanged(val name: String) : ConfigureRoomEvents data class TopicChanged(val topic: String) : ConfigureRoomEvents data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents + data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 56962647ad..97871fb99a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -42,12 +42,14 @@ class ConfigureRoomPresenter @AssistedInject constructor( var roomName by rememberSaveable { mutableStateOf("") } var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } + var privacy by rememberSaveable { mutableStateOf(null) } fun handleEvents(event: ConfigureRoomEvents) { when (event) { is ConfigureRoomEvents.AvatarUriChanged -> avatarUri = event.uri is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name is ConfigureRoomEvents.TopicChanged -> topic = event.topic + is ConfigureRoomEvents.RoomPrivacyChanged -> privacy = event.privacy } } @@ -56,6 +58,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( roomName = roomName, topic = topic, avatarUri = avatarUri, + privacy = privacy, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index d8a3635d35..643f01ef3e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -25,5 +25,6 @@ data class ConfigureRoomState( val roomName: String, val topic: String, val avatarUri: Uri?, + val privacy: RoomPrivacy?, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 2838d77f23..d097b0e599 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -33,5 +33,6 @@ fun aConfigureRoomState() = ConfigureRoomState( roomName = "", topic = "", avatarUri = null, + privacy = null, eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 360ad2e0e0..d492d5d81c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -24,20 +24,27 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.AddAPhoto +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter @@ -45,13 +52,14 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest -import io.element.android.features.selectusers.api.SelectedUsersList +import io.element.android.features.userlist.api.SelectedUsersList import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.RadioButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton @@ -98,6 +106,12 @@ fun ConfigureRoomView( // TODO }, ) + Spacer(Modifier.weight(1f)) + RoomPrivacyOptions( + modifier = Modifier.padding(bottom = 40.dp), + selected = state.privacy, + onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it)) }, + ) } } } @@ -189,15 +203,12 @@ fun Avatar( .data(avatarUri) .build() AsyncImage( + modifier = commonModifier, model = model, contentDescription = null, - modifier = commonModifier, ) } else { - Box( - modifier = commonModifier - .background(LocalColors.current.quinary) - ) { + Box(modifier = commonModifier.background(LocalColors.current.quinary)) { Icon( imageVector = Icons.Outlined.AddAPhoto, contentDescription = "", @@ -212,8 +223,8 @@ fun Avatar( @Composable fun RoomTopic( - modifier: Modifier = Modifier, topic: String, + modifier: Modifier = Modifier, onTopicChanged: (String) -> Unit = {}, ) { Column( @@ -234,6 +245,106 @@ fun RoomTopic( } } +@Composable +fun RoomPrivacyOptions( + selected: RoomPrivacy?, + modifier: Modifier = Modifier, + onOptionSelected: (RoomPrivacy) -> Unit = {}, +) { + + data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, + ) + + val items = RoomPrivacy.values().map { + when (it) { + RoomPrivacy.Public -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Lock, + title = "Private room (invite only)", + description = "Messages in this room are encrypted. Encryption can’t be disabled afterwards.", + ) + RoomPrivacy.Private -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Public, + title = "Public room (anyone)", + description = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date.", + ) + } + } + Column(modifier = modifier.selectableGroup()) { + items.forEach { item -> + RoomPrivacyOption( + privacy = RoomPrivacy.Private, + icon = item.icon, + title = item.title, + description = item.description, + isSelected = selected == item.privacy, + onOptionSelected = { onOptionSelected(item.privacy) } + ) + } + } +} + +@Composable +fun RoomPrivacyOption( + privacy: RoomPrivacy, + icon: ImageVector, + title: String, + description: String, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + onOptionSelected: (RoomPrivacy) -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onOptionSelected(privacy) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Icon( + modifier = Modifier.padding(horizontal = 8.dp), + imageVector = icon, + contentDescription = "", + tint = MaterialTheme.colorScheme.secondary, + ) + + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Text( + text = title, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.size(3.dp)) + Text( + text = description, + fontSize = 12.sp, + lineHeight = 17.sp, + color = MaterialTheme.colorScheme.tertiary, + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} + @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt new file mode 100644 index 0000000000..e0b7411680 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +enum class RoomPrivacy { + Public, + Private, +} diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt new file mode 100644 index 0000000000..3e703962a0 --- /dev/null +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/RadioButton.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.designsystem.theme.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.RadioButtonColors +import androidx.compose.material3.RadioButtonDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight + +@Composable +fun RadioButton( + selected: Boolean, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: RadioButtonColors = RadioButtonDefaults.colors(), + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + androidx.compose.material3.RadioButton( + selected = selected, + onClick = onClick, + modifier = modifier, + enabled = enabled, + colors = colors, + interactionSource = interactionSource, + ) +} + +@Preview +@Composable +internal fun RadioButtonLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun RadioButtonDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + RadioButton(selected = false, onClick = {}) + RadioButton(selected = true, onClick = {}) + } +} From 11994ec629386a288093beb130076db19d8b4255 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:29:36 +0200 Subject: [PATCH 42/83] Extract room name and topic to dedicated composable --- .../impl/configureroom/ConfigureRoomView.kt | 72 +++++++++++-------- 1 file changed, 41 insertions(+), 31 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index d492d5d81c..50514b663a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -167,22 +167,12 @@ fun RoomNameWithAvatar( onClick = onAvatarClick, ) - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = "Room name" - ) - - TextField( - modifier = Modifier.fillMaxWidth(), - value = roomName, - placeholder = { Text("e.g. Product Sprint") }, - onValueChange = onRoomNameChanged, - maxLines = 1, - ) - } + LabelledTextField( + label = "Room name", + value = roomName, + placeholder = "e.g. Product Sprint", + onValueChange = onRoomNameChanged + ) } } @@ -227,22 +217,14 @@ fun RoomTopic( modifier: Modifier = Modifier, onTopicChanged: (String) -> Unit = {}, ) { - Column( + LabelledTextField( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = "Topic (optional)", - ) - TextField( - modifier = Modifier.fillMaxWidth(), - value = topic, - placeholder = { Text("What is this room about?") }, - onValueChange = onTopicChanged, - maxLines = 3, - ) - } + label = "Topic (optional)", + value = topic, + placeholder = "What is this room about?", + onValueChange = onTopicChanged, + maxLines = 3, + ) } @Composable @@ -345,6 +327,34 @@ fun RoomPrivacyOption( } } +@Composable +fun LabelledTextField( + label: String, + value: String, + modifier: Modifier = Modifier, + placeholder: String = "", + maxLines: Int = 1, + onValueChange: (String) -> Unit, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = label + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = value, + placeholder = { Text(placeholder) }, + onValueChange = onValueChange, + maxLines = maxLines, + ) + } +} + @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = From f1b350c8d4e2e746f4cc9d9a88f2f3421e01aa8d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:34:24 +0200 Subject: [PATCH 43/83] Use rememberLazyListState --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 50514b663a..5b9b548ea3 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -28,7 +28,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape @@ -73,6 +73,7 @@ fun ConfigureRoomView( onBackPressed: () -> Unit = {}, onCreatePressed: () -> Unit = {}, ) { + val selectedUsersListState = rememberLazyListState() Scaffold( modifier = modifier, topBar = { @@ -99,12 +100,10 @@ fun ConfigureRoomView( onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) }, ) SelectedUsersList( - listState = LazyListState(), // FIXME + listState = selectedUsersListState, contentPadding = PaddingValues(horizontal = 24.dp), selectedUsers = state.selectedUsers, - onUserRemoved = { - // TODO - }, + onUserRemoved = { }, // TODO ) Spacer(Modifier.weight(1f)) RoomPrivacyOptions( From 5741be3689832b5911c04c6275536e37774b8e66 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:51:47 +0200 Subject: [PATCH 44/83] Update enable state of create room button --- .../createroom/impl/configureroom/ConfigureRoomEvents.kt | 1 + .../createroom/impl/configureroom/ConfigureRoomPresenter.kt | 6 ++++++ .../createroom/impl/configureroom/ConfigureRoomState.kt | 1 + .../impl/configureroom/ConfigureRoomStateProvider.kt | 2 +- .../createroom/impl/configureroom/ConfigureRoomView.kt | 6 +++--- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index 7322dbd961..644c3d1e03 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -23,4 +23,5 @@ sealed interface ConfigureRoomEvents { data class TopicChanged(val topic: String) : ConfigureRoomEvents data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents + object CreateRoom : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 97871fb99a..c8bad29f93 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -43,6 +43,10 @@ class ConfigureRoomPresenter @AssistedInject constructor( var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } var privacy by rememberSaveable { mutableStateOf(null) } + val isCreateButtonEnabled by rememberSaveable(roomName, privacy) { + val enabled = roomName.isNotEmpty() && privacy != null + mutableStateOf(enabled) + } fun handleEvents(event: ConfigureRoomEvents) { when (event) { @@ -50,6 +54,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name is ConfigureRoomEvents.TopicChanged -> topic = event.topic is ConfigureRoomEvents.RoomPrivacyChanged -> privacy = event.privacy + ConfigureRoomEvents.CreateRoom -> Unit // TODO } } @@ -59,6 +64,7 @@ class ConfigureRoomPresenter @AssistedInject constructor( topic = topic, avatarUri = avatarUri, privacy = privacy, + isCreateButtonEnabled = isCreateButtonEnabled, eventSink = ::handleEvents, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index 643f01ef3e..23f5691267 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -26,5 +26,6 @@ data class ConfigureRoomState( val topic: String, val avatarUri: Uri?, val privacy: RoomPrivacy?, + val isCreateButtonEnabled: Boolean, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index d097b0e599..3e4961f56a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -24,7 +24,6 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider get() = sequenceOf( aConfigureRoomState(), - // Add other state here ) } @@ -34,5 +33,6 @@ fun aConfigureRoomState() = ConfigureRoomState( topic = "", avatarUri = null, privacy = null, + isCreateButtonEnabled = false, eventSink = {} ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 5b9b548ea3..6f56ae06ef 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -71,16 +71,15 @@ fun ConfigureRoomView( state: ConfigureRoomState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onCreatePressed: () -> Unit = {}, ) { val selectedUsersListState = rememberLazyListState() Scaffold( modifier = modifier, topBar = { ConfigureRoomToolbar( - isNextActionEnabled = false, + isNextActionEnabled = state.isCreateButtonEnabled, onBackPressed = onBackPressed, - onNextPressed = onCreatePressed, + onNextPressed = { state.eventSink(ConfigureRoomEvents.CreateRoom) }, ) } ) { padding -> @@ -326,6 +325,7 @@ fun RoomPrivacyOption( } } +// Move this composable to design module if we want to reuse it in other screens @Composable fun LabelledTextField( label: String, From 74be5b121eec1b7de8e3616edf3e027614f2494d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 14:53:54 +0200 Subject: [PATCH 45/83] plug back button --- .../features/createroom/impl/configureroom/ConfigureRoomNode.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt index 6e11e492b8..ec5bf67787 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt @@ -51,6 +51,7 @@ class ConfigureRoomNode @AssistedInject constructor( ConfigureRoomView( state = state, modifier = modifier, + onBackPressed = { navigateUp() } // TODO we should keep in memory the current view state ) } } From 7cc0137b0bbc2e8a4775e5309c3983ca11f2004b Mon Sep 17 00:00:00 2001 From: Benoit Marty Date: Thu, 13 Apr 2023 15:47:23 +0200 Subject: [PATCH 46/83] Fix typo --- tools/adb/deeplink.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/adb/deeplink.sh b/tools/adb/deeplink.sh index a88d1083b3..5d50ec9409 100755 --- a/tools/adb/deeplink.sh +++ b/tools/adb/deeplink.sh @@ -18,7 +18,7 @@ # Format is: # elementx://open/{sessionId} to open a session # elementx://open/{sessionId}/{roomId} to open a room -# elementx://open/{sessionId}/{roomId}/{eventId} to open an event +# elementx://open/{sessionId}/{roomId}/{eventId} to open a thread # Open a session # adb shell am start -a android.intent.action.VIEW -d elementx://open/@benoit10518:matrix.org From 023c5f4a7edd8d7747436ca16a1af8b9d94af042 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 15:29:55 +0200 Subject: [PATCH 47/83] Use string resources --- .../impl/configureroom/ConfigureRoomView.kt | 23 +++++++++++-------- .../impl/root/CreateRoomRootView.kt | 2 +- .../src/main/res/values-es/translations.xml | 2 ++ .../src/main/res/values-it/translations.xml | 2 ++ .../src/main/res/values-ro/translations.xml | 2 ++ .../impl/src/main/res/values/localazy.xml | 2 ++ .../src/main/res/values-es/translations.xml | 4 ++++ .../src/main/res/values-it/translations.xml | 4 ++++ .../src/main/res/values-ro/translations.xml | 4 ++++ .../src/main/res/values-es/translations.xml | 4 +--- .../src/main/res/values-it/translations.xml | 4 +--- .../src/main/res/values-ro/translations.xml | 4 +--- .../src/main/res/values/localazy.xml | 3 +-- tools/localazy/config.json | 3 ++- 14 files changed, 40 insertions(+), 23 deletions(-) create mode 100644 libraries/androidutils/src/main/res/values-es/translations.xml create mode 100644 libraries/androidutils/src/main/res/values-it/translations.xml create mode 100644 libraries/androidutils/src/main/res/values-ro/translations.xml diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 6f56ae06ef..255e208532 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -44,6 +44,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview @@ -52,6 +53,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import coil.request.ImageRequest +import io.element.android.features.createroom.impl.R import io.element.android.features.userlist.api.SelectedUsersList import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -64,6 +66,7 @@ import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton import io.element.android.libraries.designsystem.theme.components.TextField +import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -126,7 +129,7 @@ fun ConfigureRoomToolbar( modifier = modifier, title = { Text( - text = "Create a room", + text = stringResource(R.string.screen_create_room_title), fontSize = 16.sp, fontWeight = FontWeight.SemiBold, ) @@ -139,7 +142,7 @@ fun ConfigureRoomToolbar( onClick = onNextPressed, ) { Text( - text = "Create", + text = stringResource(StringR.string.action_create), fontSize = 16.sp, ) } @@ -166,9 +169,9 @@ fun RoomNameWithAvatar( ) LabelledTextField( - label = "Room name", + label = stringResource(R.string.screen_create_room_room_name_label), value = roomName, - placeholder = "e.g. Product Sprint", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), onValueChange = onRoomNameChanged ) } @@ -217,9 +220,9 @@ fun RoomTopic( ) { LabelledTextField( modifier = modifier, - label = "Topic (optional)", + label = stringResource(R.string.screen_create_room_topic_label), value = topic, - placeholder = "What is this room about?", + placeholder = stringResource(R.string.screen_create_room_topic_placeholder), onValueChange = onTopicChanged, maxLines = 3, ) @@ -244,14 +247,14 @@ fun RoomPrivacyOptions( RoomPrivacy.Public -> RoomPrivacyItem( privacy = it, icon = Icons.Outlined.Lock, - title = "Private room (invite only)", - description = "Messages in this room are encrypted. Encryption can’t be disabled afterwards.", + title = stringResource(R.string.screen_create_room_private_option_title), + description = stringResource(R.string.screen_create_room_private_option_description), ) RoomPrivacy.Private -> RoomPrivacyItem( privacy = it, icon = Icons.Outlined.Public, - title = "Public room (anyone)", - description = "Messages are not encrypted and anyone can read them. You can enable encryption at a later date.", + title = stringResource(R.string.screen_create_room_public_option_title), + description = stringResource(R.string.screen_create_room_public_option_description), ) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index 4e12dcaab0..bcf58b7d2d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -110,7 +110,7 @@ fun CreateRoomRootView( } is Async.Failure -> { RetryDialog( - content = stringResource(id = StringR.string.screen_start_chat_error_starting_chat), + content = stringResource(id = R.string.screen_start_chat_error_starting_chat), onDismiss = { state.eventSink(CreateRoomRootEvents.CancelStartDM) }, onRetry = { state.userListState.selectedUsers.firstOrNull() diff --git a/features/createroom/impl/src/main/res/values-es/translations.xml b/features/createroom/impl/src/main/res/values-es/translations.xml index f6248df74e..bb3d6fa0b8 100644 --- a/features/createroom/impl/src/main/res/values-es/translations.xml +++ b/features/createroom/impl/src/main/res/values-es/translations.xml @@ -3,4 +3,6 @@ "Nueva sala" "Invitar gente" "Añadir personas" + "Se ha producido un error al intentar iniciar un chat" + "No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación." \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values-it/translations.xml b/features/createroom/impl/src/main/res/values-it/translations.xml index ea0c0b10e1..1d6ce99b5f 100644 --- a/features/createroom/impl/src/main/res/values-it/translations.xml +++ b/features/createroom/impl/src/main/res/values-it/translations.xml @@ -3,4 +3,6 @@ "Nuova stanza" "Invita persone" "Aggiungi persone" + "Si è verificato un errore durante il tentativo di avviare una chat" + "Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto." \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values-ro/translations.xml b/features/createroom/impl/src/main/res/values-ro/translations.xml index 98839a883e..af6e3db1fa 100644 --- a/features/createroom/impl/src/main/res/values-ro/translations.xml +++ b/features/createroom/impl/src/main/res/values-ro/translations.xml @@ -3,4 +3,6 @@ "Cameră nouă" "Invitați persoane" "Adaugați persoane" + "A apărut o eroare la încercarea începerii conversației" + "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită." \ No newline at end of file diff --git a/features/createroom/impl/src/main/res/values/localazy.xml b/features/createroom/impl/src/main/res/values/localazy.xml index 48f055e082..177d588f7e 100644 --- a/features/createroom/impl/src/main/res/values/localazy.xml +++ b/features/createroom/impl/src/main/res/values/localazy.xml @@ -12,4 +12,6 @@ "Create a room" "Topic (optional)" "What is this room about?" + "An error occurred when trying to start a chat" + "We can’t validate this user’s Matrix ID. The invite might not be received." \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-es/translations.xml b/libraries/androidutils/src/main/res/values-es/translations.xml new file mode 100644 index 0000000000..80b2b88347 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-es/translations.xml @@ -0,0 +1,4 @@ + + + "No se encontró ninguna aplicación compatible con esta acción." + \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-it/translations.xml b/libraries/androidutils/src/main/res/values-it/translations.xml new file mode 100644 index 0000000000..03aaf3ffd1 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-it/translations.xml @@ -0,0 +1,4 @@ + + + "Non è stata trovata alcuna app compatibile per gestire questa azione." + \ No newline at end of file diff --git a/libraries/androidutils/src/main/res/values-ro/translations.xml b/libraries/androidutils/src/main/res/values-ro/translations.xml new file mode 100644 index 0000000000..d2149227c5 --- /dev/null +++ b/libraries/androidutils/src/main/res/values-ro/translations.xml @@ -0,0 +1,4 @@ + + + "Nu a fost găsită nicio aplicație capabilă să gestioneze această acțiune." + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-es/translations.xml b/libraries/ui-strings/src/main/res/values-es/translations.xml index 58b25eaf3c..564ede34a8 100644 --- a/libraries/ui-strings/src/main/res/values-es/translations.xml +++ b/libraries/ui-strings/src/main/res/values-es/translations.xml @@ -137,11 +137,9 @@ "Desbloquear" "Al desbloquear al usuario, podrás volver a ver todos sus mensajes." "Desbloquear usuario" - "Se ha producido un error al intentar iniciar un chat" - "No podemos validar el ID de Matrix de este usuario. Es posible que no reciba la invitación." "Agitar con fuerza" "Umbral de detección" "General" "Versión: %1$s (%2$s)" "es" - + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-it/translations.xml b/libraries/ui-strings/src/main/res/values-it/translations.xml index 2eb58f0d6e..a8ec05115a 100644 --- a/libraries/ui-strings/src/main/res/values-it/translations.xml +++ b/libraries/ui-strings/src/main/res/values-it/translations.xml @@ -137,11 +137,9 @@ "Sblocca" "Dopo aver sbloccato l\'utente, potrai vedere nuovamente tutti i suoi messaggi." "Sblocca utente" - "Si è verificato un errore durante il tentativo di avviare una chat" - "Non possiamo convalidare l\'ID Matrix di questo utente. L\'invito potrebbe non essere ricevuto." "Rageshake" "Soglia di rilevamento" "Generali" "Versione: %1$s (%2$s)" "it" - + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values-ro/translations.xml b/libraries/ui-strings/src/main/res/values-ro/translations.xml index ba066efae9..091dd7f36d 100644 --- a/libraries/ui-strings/src/main/res/values-ro/translations.xml +++ b/libraries/ui-strings/src/main/res/values-ro/translations.xml @@ -139,11 +139,9 @@ "Deblocați" "La deblocarea utilizatorului, veți putea vedea din nou toate mesajele de la acesta." "Deblocați utilizatorul" - "A apărut o eroare la încercarea începerii conversației" - "Nu am putut valida ID-ul Matrix al acestui utilizator. Este posibil ca invitația să nu fi fost primită." "Rageshake" "Prag de detecție" "General" "Versiunea: %1$s (%2$s)" "ro" - + \ No newline at end of file diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index de11a74eac..379dd5508f 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -136,6 +136,7 @@ "This is the beginning of %1$s." "This is the beginning of this conversation." "New" + "No Invites" "%1$s invited you" "Block user" "Check if you want to hide all current and future messages from this user" @@ -145,8 +146,6 @@ "Unblock" "On unblocking the user, you will be able to see all messages by them again." "Unblock user" - "An error occurred when trying to start a chat" - "We can’t validate this user’s Matrix ID. The invite might not be received." "Rageshake" "Detection threshold" "General" diff --git a/tools/localazy/config.json b/tools/localazy/config.json index 37f8c895f1..9cbd6cd620 100644 --- a/tools/localazy/config.json +++ b/tools/localazy/config.json @@ -28,7 +28,8 @@ { "name": ":features:createroom:impl", "includeRegex": [ - "screen_create_room_.*" + "screen_create_room_.*", + "screen_start_chat_.*" ] }, { From dde2aad600e2ba5fc9bcec65adc5b68d65d495bf Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:12:31 +0200 Subject: [PATCH 48/83] screenshots --- ...p_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ..._ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 +++ ...aultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ ...ultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png | 3 +++ 4 files changed, 12 insertions(+) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..22e0b97b81 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2327de2be62afdacae32b297347fbcd23cd5e6986f513d018068aa981ecc3941 +size 89757 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..54b8ae3f9e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50129ad0c4d75ff5e7b8ffd43b0ffdc40d77a78f5699f9b7194a9c9ef026af69 +size 82189 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e2b920b8ed --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:247d303776a79f310144487784fb3841a430a6b0fa7fdee28485ed6157e42354 +size 7056 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b2c1d53b70 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.libraries.designsystem.theme.components_null_DefaultGroup_RadioButtonLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d93540b38b57540142d2e57e4ce2caf7424fcd2aa3017ab16449d60c1e156797 +size 6730 From 25fe88c59d601586f2866928f0d8469956b4cd25 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:45:35 +0200 Subject: [PATCH 49/83] Add tests for ConfigureRoomPresenter --- features/createroom/impl/build.gradle.kts | 1 + .../ConfigureRoomPresenterTests.kt | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt diff --git a/features/createroom/impl/build.gradle.kts b/features/createroom/impl/build.gradle.kts index 77a5ed26be..edb35428a0 100644 --- a/features/createroom/impl/build.gradle.kts +++ b/features/createroom/impl/build.gradle.kts @@ -56,6 +56,7 @@ dependencies { testImplementation(libs.molecule.runtime) testImplementation(libs.test.truth) testImplementation(libs.test.turbine) + testImplementation(libs.test.robolectric) testImplementation(projects.libraries.matrix.test) testImplementation(projects.features.userlist.impl) testImplementation(projects.features.userlist.test) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt new file mode 100644 index 0000000000..0934909460 --- /dev/null +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package io.element.android.features.createroom.impl.configureroom + +import android.net.Uri +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth.assertThat +import io.element.android.libraries.matrix.test.AN_AVATAR_URL +import io.element.android.libraries.matrix.test.A_MESSAGE +import io.element.android.libraries.matrix.test.A_ROOM_NAME +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ConfigureRoomPresenterTests { + + private lateinit var presenter: ConfigureRoomPresenter + + @Before + fun setup() { + presenter = ConfigureRoomPresenter(ConfigureRoomPresenterArgs(emptyList())) + } + + @Test + fun `present - initial state`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.roomName).isEmpty() + assertThat(initialState.topic).isEmpty() + assertThat(initialState.privacy).isNull() + } + } + + @Test + fun `present - create room button is enabled only if the required fields are completed`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + assertThat(initialState.isCreateButtonEnabled).isFalse() + + // Room name not empty + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) + var newState: ConfigureRoomState = awaitItem() + assertThat(newState.roomName).isEqualTo(A_ROOM_NAME) + assertThat(newState.isCreateButtonEnabled).isFalse() + + // Select privacy + initialState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Private)) + newState = awaitItem() + assertThat(newState.privacy).isEqualTo(RoomPrivacy.Private) + assertThat(newState.isCreateButtonEnabled).isTrue() + + // Clear room name + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) + newState = awaitItem() + assertThat(newState.roomName).isEqualTo("") + assertThat(newState.isCreateButtonEnabled).isFalse() + } + } + + @Test + fun `present - state is updated when fields are changed`() = runTest { + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + // Room name + initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) + val stateAfterRoomNameChanged = awaitItem() + assertThat(stateAfterRoomNameChanged.roomName).isEqualTo(A_ROOM_NAME) + + // Room topic + stateAfterRoomNameChanged.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) + val stateAfterTopicChanged = awaitItem() + assertThat(stateAfterTopicChanged.topic).isEqualTo(A_MESSAGE) + + // Room avatar + val anUri = Uri.parse(AN_AVATAR_URL) + stateAfterTopicChanged.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri)) + val stateAfterAvatarUriChanged = awaitItem() + assertThat(stateAfterAvatarUriChanged.avatarUri).isEqualTo(anUri) + + // Room privacy + stateAfterAvatarUriChanged.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public)) + val stateAfterPrivacyChanged = awaitItem() + assertThat(stateAfterPrivacyChanged.privacy).isEqualTo(RoomPrivacy.Public) + } + } +} + From c50e199be4ea1d060aaf06a217aba5504477df43 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 7 Apr 2023 16:49:01 +0200 Subject: [PATCH 50/83] Changelog --- changelog.d/110.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/110.feature diff --git a/changelog.d/110.feature b/changelog.d/110.feature new file mode 100644 index 0000000000..d28935b65f --- /dev/null +++ b/changelog.d/110.feature @@ -0,0 +1 @@ +[Create and join rooms] Create a room screen (UI) From 2e013c15d8aa0887100d0f9eb9ac86ab49536afa Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 11 Apr 2023 14:31:05 +0200 Subject: [PATCH 51/83] reorder params --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 255e208532..39b47eb60e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -152,9 +152,9 @@ fun ConfigureRoomToolbar( @Composable fun RoomNameWithAvatar( - modifier: Modifier = Modifier, avatarUri: Uri?, roomName: String, + modifier: Modifier = Modifier, onAvatarClick: () -> Unit = {}, onRoomNameChanged: (String) -> Unit = {}, ) { @@ -179,8 +179,8 @@ fun RoomNameWithAvatar( @Composable fun Avatar( - modifier: Modifier = Modifier, avatarUri: Uri?, + modifier: Modifier = Modifier, onClick: () -> Unit = {}, ) { val commonModifier = modifier From 2e34c8e8eb1439601c3547e03efadc7c7003455a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 15:54:48 +0200 Subject: [PATCH 52/83] declare node callback in local variable --- .../features/createroom/impl/CreateRoomFlowNode.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index 137017a2ec..a50c9ab1d0 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -68,7 +68,7 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.Root -> { - createNode(context = buildContext, plugins = listOf(object : CreateRoomRootNode.Callback { + val callback = object : CreateRoomRootNode.Callback { override fun onCreateNewRoom() { backstack.push(NavTarget.NewRoom) } @@ -76,14 +76,16 @@ class CreateRoomFlowNode @AssistedInject constructor( override fun onOpenRoom(roomId: RoomId) { plugins().forEach { it.onOpenRoom(roomId) } } - })) + } + createNode(context = buildContext, plugins = listOf(callback)) } NavTarget.NewRoom -> { - createNode(context = buildContext, plugins = listOf(object : AddPeopleNode.Callback { + val callback = object : AddPeopleNode.Callback { override fun onContinue(selectedUsers: List) { backstack.push(NavTarget.ConfigureRoom(selectedUsers)) } - })) + } + createNode(context = buildContext, plugins = listOf(callback)) } is NavTarget.ConfigureRoom -> { createNode(context = buildContext, plugins = listOf(ConfigureRoomNode.Inputs(navTarget.users))) From 5699dcf39e8b6d273806563400dc60905f9611e3 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 16:18:33 +0200 Subject: [PATCH 53/83] Fix modifier usage --- .../features/createroom/impl/configureroom/ConfigureRoomView.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 39b47eb60e..1ff41bc8af 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -203,7 +203,7 @@ fun Avatar( Icon( imageVector = Icons.Outlined.AddAPhoto, contentDescription = "", - modifier = modifier + modifier = Modifier .align(Alignment.Center) .size(40.dp), tint = MaterialTheme.colorScheme.secondary, From 335eb49b6c1f6eeacf24404f6fe06b7a3bc250e8 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Tue, 11 Apr 2023 22:29:16 +0200 Subject: [PATCH 54/83] Add create room API --- .../libraries/matrix/api/MatrixClient.kt | 4 +- .../api/createroom/CreateRoomParameters.kt | 30 +++++++++ .../matrix/api/createroom/RoomPreset.kt | 22 +++++++ .../matrix/api/createroom/RoomVisibility.kt | 21 +++++++ .../libraries/matrix/impl/RustMatrixClient.kt | 63 +++++++++++++------ .../libraries/matrix/test/FakeMatrixClient.kt | 14 +++-- 6 files changed, 130 insertions(+), 24 deletions(-) create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomPreset.kt create mode 100644 libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomVisibility.kt diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt index 5a9e2a4bb6..41e86f2701 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/MatrixClient.kt @@ -19,6 +19,7 @@ package io.element.android.libraries.matrix.api import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.pusher.PushersService @@ -32,8 +33,9 @@ interface MatrixClient : Closeable { val sessionId: SessionId val roomSummaryDataSource: RoomSummaryDataSource fun getRoom(roomId: RoomId): MatrixRoom? - suspend fun createDM(userId: UserId): Result fun findDM(userId: UserId): MatrixRoom? + suspend fun createRoom(createRoomParams: CreateRoomParameters): Result + suspend fun createDM(userId: UserId): Result fun startSync() fun stopSync() fun mediaResolver(): MediaResolver diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt new file mode 100644 index 0000000000..c65aae6156 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/CreateRoomParameters.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.libraries.matrix.api.createroom + +import io.element.android.libraries.matrix.api.core.UserId + +data class CreateRoomParameters( + val name: String?, + val topic: String? = null, + val isEncrypted: Boolean, + val isDirect: Boolean = false, + val visibility: RoomVisibility, + val preset: RoomPreset, + val invite: List? = null, + val avatar: String? = null, +) diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomPreset.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomPreset.kt new file mode 100644 index 0000000000..c2254e395f --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomPreset.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.libraries.matrix.api.createroom + +enum class RoomPreset { + PRIVATE_CHAT, + PUBLIC_CHAT, + TRUSTED_PRIVATE_CHAT, +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomVisibility.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomVisibility.kt new file mode 100644 index 0000000000..d2715363e8 --- /dev/null +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/createroom/RoomVisibility.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.element.android.libraries.matrix.api.createroom + +enum class RoomVisibility { + PUBLIC, + PRIVATE, +} diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt index d2408abdd7..bcac6cebd1 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/RustMatrixClient.kt @@ -20,6 +20,9 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters +import io.element.android.libraries.matrix.api.createroom.RoomPreset +import io.element.android.libraries.matrix.api.createroom.RoomVisibility import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.pusher.PushersService @@ -42,10 +45,7 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext import org.matrix.rustcomponents.sdk.Client import org.matrix.rustcomponents.sdk.ClientDelegate -import org.matrix.rustcomponents.sdk.CreateRoomParameters import org.matrix.rustcomponents.sdk.RequiredState -import org.matrix.rustcomponents.sdk.RoomPreset -import org.matrix.rustcomponents.sdk.RoomVisibility import org.matrix.rustcomponents.sdk.SlidingSyncListBuilder import org.matrix.rustcomponents.sdk.SlidingSyncMode import org.matrix.rustcomponents.sdk.SlidingSyncRequestListFilters @@ -55,6 +55,9 @@ import org.matrix.rustcomponents.sdk.use import timber.log.Timber import java.io.File import java.util.concurrent.atomic.AtomicBoolean +import org.matrix.rustcomponents.sdk.CreateRoomParameters as RustCreateRoomParameters +import org.matrix.rustcomponents.sdk.RoomPreset as RustRoomPreset +import org.matrix.rustcomponents.sdk.RoomVisibility as RustRoomVisibility class RustMatrixClient constructor( private val client: Client, @@ -175,24 +178,46 @@ class RustMatrixClient constructor( return roomId?.let { getRoom(it) } } - override suspend fun createDM(userId: UserId): Result = - withContext(dispatchers.io) { - runCatching { - val roomId = client.createRoom( - CreateRoomParameters( - name = null, - topic = null, - isEncrypted = true, - isDirect = true, - visibility = RoomVisibility.PRIVATE, - preset = RoomPreset.TRUSTED_PRIVATE_CHAT, - invite = listOf(userId.value), - avatar = null, - ) + override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result = withContext(dispatchers.io) { + runCatching { + val roomId = client.createRoom( + RustCreateRoomParameters( + name = createRoomParams.name, + topic = createRoomParams.topic, + isEncrypted = createRoomParams.isEncrypted, + isDirect = createRoomParams.isDirect, + visibility = when (createRoomParams.visibility) { + RoomVisibility.PUBLIC -> RustRoomVisibility.PUBLIC + RoomVisibility.PRIVATE -> RustRoomVisibility.PRIVATE + }, + preset = when (createRoomParams.preset) { + RoomPreset.PRIVATE_CHAT -> RustRoomPreset.PRIVATE_CHAT + RoomPreset.PUBLIC_CHAT -> RustRoomPreset.PUBLIC_CHAT + RoomPreset.TRUSTED_PRIVATE_CHAT -> RustRoomPreset.TRUSTED_PRIVATE_CHAT + }, + invite = createRoomParams.invite?.map { it.value }, + avatar = createRoomParams.avatar, ) - RoomId(roomId) - } + ) + RoomId(roomId) } + } + + override suspend fun createDM(userId: UserId): Result = withContext(dispatchers.io) { + runCatching { + val roomId = client.createRoom( + RustCreateRoomParameters( + name = null, + isEncrypted = true, + isDirect = true, + visibility = RustRoomVisibility.PRIVATE, + preset = RustRoomPreset.TRUSTED_PRIVATE_CHAT, + invite = listOf(userId.value), + ) + ) + RoomId(roomId) + } + } override fun mediaResolver(): MediaResolver = mediaResolver diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt index 7159eff417..a6741982ea 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/FakeMatrixClient.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.matrix.api.MatrixClient import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.api.core.SessionId import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.createroom.CreateRoomParameters import io.element.android.libraries.matrix.api.media.MediaResolver import io.element.android.libraries.matrix.api.notification.NotificationService import io.element.android.libraries.matrix.api.pusher.PushersService @@ -54,16 +55,21 @@ class FakeMatrixClient( return FakeMatrixRoom(roomId) } + override fun findDM(userId: UserId): MatrixRoom? { + return findDmResult + } + + override suspend fun createRoom(createRoomParams: CreateRoomParameters): Result { + delay(100) + return Result.success(A_ROOM_ID) + } + override suspend fun createDM(userId: UserId): Result { delay(100) createDmFailure?.let { throw it } return createDmResult } - override fun findDM(userId: UserId): MatrixRoom? { - return findDmResult - } - override fun startSync() = Unit override fun stopSync() = Unit From 5796789a681e2572ef73bbed2dc054a3b8cfbf8f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 12 Apr 2023 14:14:48 +0200 Subject: [PATCH 55/83] Add CreateRoomScope with data store --- .../createroom/impl/CreateRoomConfig.kt | 30 +++++++++++++ .../createroom/impl/CreateRoomDataStore.kt | 35 +++++++++++++++ .../createroom/impl/CreateRoomFlowNode.kt | 35 ++++++++++----- .../impl/addpeople/AddPeopleNode.kt | 4 +- .../impl/addpeople/AddPeoplePresenter.kt | 12 ++++- .../impl/configureroom/ConfigureRoomNode.kt | 18 ++------ .../configureroom/ConfigureRoomPresenter.kt | 44 +++++++------------ .../impl/configureroom/ConfigureRoomState.kt | 10 +---- .../ConfigureRoomStateProvider.kt | 9 +--- .../impl/configureroom/ConfigureRoomView.kt | 11 ++--- .../createroom/impl/di/CreateRoomComponent.kt | 39 ++++++++++++++++ .../createroom/impl/di/CreateRoomScope.kt | 19 ++++++++ 12 files changed, 186 insertions(+), 80 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomComponent.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomScope.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt new file mode 100644 index 0000000000..0aedf12d0f --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomConfig.kt @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl + +import io.element.android.features.createroom.impl.configureroom.RoomPrivacy +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +data class CreateRoomConfig( + val roomName: String? = null, + val topic: String? = null, + val avatarUrl: String? = null, + val invites: ImmutableList = persistentListOf(), + val privacy: RoomPrivacy? = null, +) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt new file mode 100644 index 0000000000..ed61431511 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl + +import io.element.android.features.createroom.impl.di.CreateRoomScope +import io.element.android.libraries.di.SingleIn +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +@SingleIn(CreateRoomScope::class) +class CreateRoomDataStore @Inject constructor() { + + private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig()) + + fun getCreateRoomConfig(): Flow = createRoomConfigFlow + + fun setCreateRoomConfig(createRoomConfig: CreateRoomConfig) { + createRoomConfigFlow.tryEmit(createRoomConfig) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index a50c9ab1d0..c663814c19 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -32,10 +32,13 @@ import io.element.android.anvilannotations.ContributesNode import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.impl.addpeople.AddPeopleNode import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode +import io.element.android.features.createroom.impl.di.CreateRoomComponent import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId import io.element.android.libraries.matrix.ui.model.MatrixUser @@ -45,14 +48,22 @@ import kotlinx.parcelize.Parcelize class CreateRoomFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, -) : BackstackNode( - backstack = BackStack( - initialElement = NavTarget.Root, - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins -) { +) : DaggerComponentOwner, + BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins + ) { + + private val component by lazy { + parent!!.bindings().createRoomComponentBuilder().build() + } + + override val daggerComponent: Any + get() = component sealed interface NavTarget : Parcelable { @Parcelize @@ -62,7 +73,7 @@ class CreateRoomFlowNode @AssistedInject constructor( object NewRoom : NavTarget @Parcelize - data class ConfigureRoom(val users: List) : NavTarget + object ConfigureRoom : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -82,13 +93,13 @@ class CreateRoomFlowNode @AssistedInject constructor( NavTarget.NewRoom -> { val callback = object : AddPeopleNode.Callback { override fun onContinue(selectedUsers: List) { - backstack.push(NavTarget.ConfigureRoom(selectedUsers)) + backstack.push(NavTarget.ConfigureRoom) } } createNode(context = buildContext, plugins = listOf(callback)) } - is NavTarget.ConfigureRoom -> { - createNode(context = buildContext, plugins = listOf(ConfigureRoomNode.Inputs(navTarget.users))) + NavTarget.ConfigureRoom -> { + createNode(context = buildContext) } } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt index 91b1d5a721..2d060a6644 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt @@ -25,10 +25,10 @@ import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.di.SessionScope +import io.element.android.features.createroom.impl.di.CreateRoomScope import io.element.android.libraries.matrix.ui.model.MatrixUser -@ContributesNode(SessionScope::class) +@ContributesNode(CreateRoomScope::class) class AddPeopleNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt index da51a36335..7090a642af 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt @@ -17,8 +17,12 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.runtime.Composable -import io.element.android.features.userlist.api.SelectionMode +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Presenter @@ -28,6 +32,7 @@ import javax.inject.Named class AddPeoplePresenter @Inject constructor( private val userListPresenterFactory: UserListPresenter.Factory, @Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource, + private val dataStore: CreateRoomDataStore, ) : Presenter { private val userListPresenter by lazy { @@ -40,7 +45,10 @@ class AddPeoplePresenter @Inject constructor( @Composable override fun present(): AddPeopleState { val userListState = userListPresenter.present() - + val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig()) + LaunchedEffect(userListState.selectedUsers) { + dataStore.setCreateRoomConfig(createRoomConfig.value.copy(invites = userListState.selectedUsers)) + } fun handleEvents(event: AddPeopleEvents) { // do nothing for now } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt index ec5bf67787..08a4f5f64b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomNode.kt @@ -24,27 +24,15 @@ import com.bumble.appyx.core.plugin.Plugin import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode -import io.element.android.libraries.architecture.NodeInputs -import io.element.android.libraries.architecture.inputs -import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.features.createroom.impl.di.CreateRoomScope -@ContributesNode(SessionScope::class) +@ContributesNode(CreateRoomScope::class) class ConfigureRoomNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, - private val presenterFactory: ConfigureRoomPresenter.Factory, + private val presenter: ConfigureRoomPresenter, ) : Node(buildContext, plugins = plugins) { - data class Inputs( - val selectedUsers: List - ) : NodeInputs - - private val inputs: Inputs = inputs() - private val presenter by lazy { - presenterFactory.create(ConfigureRoomPresenterArgs(inputs.selectedUsers)) - } - @Composable override fun View(modifier: Modifier) { val state = presenter.present() diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index c8bad29f93..7a877a872b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -16,54 +16,40 @@ package io.element.android.features.createroom.impl.configureroom -import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import dagger.assisted.Assisted -import dagger.assisted.AssistedFactory -import dagger.assisted.AssistedInject +import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.libraries.architecture.Presenter -import kotlinx.collections.immutable.toImmutableList +import javax.inject.Inject -class ConfigureRoomPresenter @AssistedInject constructor( - @Assisted val args: ConfigureRoomPresenterArgs, +class ConfigureRoomPresenter @Inject constructor( + private val dataStore: CreateRoomDataStore, ) : Presenter { - @AssistedFactory - interface Factory { - fun create(args: ConfigureRoomPresenterArgs): ConfigureRoomPresenter - } - @Composable override fun present(): ConfigureRoomState { - var roomName by rememberSaveable { mutableStateOf("") } - var topic by rememberSaveable { mutableStateOf("") } - var avatarUri by rememberSaveable { mutableStateOf(null) } - var privacy by rememberSaveable { mutableStateOf(null) } - val isCreateButtonEnabled by rememberSaveable(roomName, privacy) { - val enabled = roomName.isNotEmpty() && privacy != null + val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig()) + val isCreateButtonEnabled by rememberSaveable(createRoomConfig.value.roomName, createRoomConfig.value.privacy) { + val enabled = createRoomConfig.value.roomName.isNullOrEmpty().not() && createRoomConfig.value.privacy != null mutableStateOf(enabled) } fun handleEvents(event: ConfigureRoomEvents) { when (event) { - is ConfigureRoomEvents.AvatarUriChanged -> avatarUri = event.uri - is ConfigureRoomEvents.RoomNameChanged -> roomName = event.name - is ConfigureRoomEvents.TopicChanged -> topic = event.topic - is ConfigureRoomEvents.RoomPrivacyChanged -> privacy = event.privacy - ConfigureRoomEvents.CreateRoom -> Unit // TODO + is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(avatarUrl = event.uri?.toString())) + is ConfigureRoomEvents.RoomNameChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(roomName = event.name)) + is ConfigureRoomEvents.TopicChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(topic = event.topic.takeUnless { it.isEmpty() })) + is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(privacy = event.privacy)) + ConfigureRoomEvents.CreateRoom -> Unit } } return ConfigureRoomState( - selectedUsers = args.selectedUsers.toImmutableList(), - roomName = roomName, - topic = topic, - avatarUri = avatarUri, - privacy = privacy, + createRoomConfig.value, isCreateButtonEnabled = isCreateButtonEnabled, eventSink = ::handleEvents, ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt index 23f5691267..47be0b1f17 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomState.kt @@ -16,16 +16,10 @@ package io.element.android.features.createroom.impl.configureroom -import android.net.Uri -import io.element.android.libraries.matrix.ui.model.MatrixUser -import kotlinx.collections.immutable.ImmutableList +import io.element.android.features.createroom.impl.CreateRoomConfig data class ConfigureRoomState( - val selectedUsers: ImmutableList, - val roomName: String, - val topic: String, - val avatarUri: Uri?, - val privacy: RoomPrivacy?, + val config: CreateRoomConfig, val isCreateButtonEnabled: Boolean, val eventSink: (ConfigureRoomEvents) -> Unit ) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index 3e4961f56a..b1614381ae 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -17,8 +17,7 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.ui.components.aMatrixUserList -import kotlinx.collections.immutable.toImmutableList +import io.element.android.features.createroom.impl.CreateRoomConfig open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence @@ -28,11 +27,7 @@ open class ConfigureRoomStateProvider : PreviewParameterProvider Date: Wed, 12 Apr 2023 14:17:15 +0200 Subject: [PATCH 56/83] Add RemoveFromSelection event in room configuration screen --- .../createroom/impl/configureroom/ConfigureRoomEvents.kt | 2 ++ .../impl/configureroom/ConfigureRoomPresenter.kt | 8 ++++++++ .../createroom/impl/configureroom/ConfigureRoomView.kt | 2 +- 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt index 644c3d1e03..f10f673d78 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomEvents.kt @@ -17,11 +17,13 @@ package io.element.android.features.createroom.impl.configureroom import android.net.Uri +import io.element.android.libraries.matrix.ui.model.MatrixUser sealed interface ConfigureRoomEvents { data class RoomNameChanged(val name: String) : ConfigureRoomEvents data class TopicChanged(val topic: String) : ConfigureRoomEvents data class AvatarUriChanged(val uri: Uri?) : ConfigureRoomEvents data class RoomPrivacyChanged(val privacy: RoomPrivacy?) : ConfigureRoomEvents + data class RemoveFromSelection(val matrixUser: MatrixUser) : ConfigureRoomEvents object CreateRoom : ConfigureRoomEvents } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 7a877a872b..53523449f7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.libraries.architecture.Presenter +import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject class ConfigureRoomPresenter @Inject constructor( @@ -44,6 +45,13 @@ class ConfigureRoomPresenter @Inject constructor( is ConfigureRoomEvents.RoomNameChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(roomName = event.name)) is ConfigureRoomEvents.TopicChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(topic = event.topic.takeUnless { it.isEmpty() })) is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(privacy = event.privacy)) + is ConfigureRoomEvents.RemoveFromSelection -> dataStore.setCreateRoomConfig( + createRoomConfig.value.copy( + invites = createRoomConfig.value.invites.minus( + event.matrixUser + ).toImmutableList() + ) + ) ConfigureRoomEvents.CreateRoom -> Unit } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index d0cf2e310d..0e8167fea4 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -106,7 +106,7 @@ fun ConfigureRoomView( listState = selectedUsersListState, contentPadding = PaddingValues(horizontal = 24.dp), selectedUsers = state.config.invites, - onUserRemoved = { }, // TODO + onUserRemoved = { state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it)) }, ) Spacer(Modifier.weight(1f)) RoomPrivacyOptions( From cf8e91c3cf3ac444aa72ce20226a45e295bf4b24 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 12 Apr 2023 16:31:51 +0200 Subject: [PATCH 57/83] Split user list views into multiple files --- .../impl/addpeople/AddPeopleView.kt | 2 +- .../impl/configureroom/ConfigureRoomView.kt | 2 +- .../impl/root/CreateRoomRootView.kt | 2 +- .../impl/members/RoomMemberListView.kt | 4 +- .../userlist/api/UserListStateProvider.kt | 31 +- .../features/userlist/api/UserListView.kt | 314 ------------------ .../SearchMultipleUsersResultItem.kt | 61 ++++ .../components/SearchSingleUserResultItem.kt | 55 +++ .../userlist/api/components/SearchUserBar.kt | 146 ++++++++ .../userlist/api/components/SelectedUser.kt | 94 ++++++ .../api/components/SelectedUsersList.kt | 71 ++++ .../userlist/api/components/UserListView.kt | 91 +++++ 12 files changed, 528 insertions(+), 345 deletions(-) delete mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt create mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchMultipleUsersResultItem.kt create mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchSingleUserResultItem.kt create mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchUserBar.kt create mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUser.kt create mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt create mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/UserListView.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index eed59923a8..9de3e0fa20 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -30,7 +30,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.createroom.impl.R -import io.element.android.features.userlist.api.UserListView +import io.element.android.features.userlist.api.components.UserListView import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 0e8167fea4..ddb000d883 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -55,7 +55,7 @@ import androidx.core.net.toUri import coil.compose.AsyncImage import coil.request.ImageRequest import io.element.android.features.createroom.impl.R -import io.element.android.features.userlist.api.SelectedUsersList +import io.element.android.features.userlist.api.components.SelectedUsersList import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt index bcf58b7d2d..a94b3bf28b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootView.kt @@ -41,7 +41,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.createroom.impl.R -import io.element.android.features.userlist.api.UserListView +import io.element.android.features.userlist.api.components.UserListView import io.element.android.libraries.architecture.Async import io.element.android.libraries.designsystem.components.ProgressDialog import io.element.android.libraries.designsystem.components.dialogs.RetryDialog diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt index e2c41e34b3..f356e203f2 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListView.kt @@ -39,8 +39,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.roomdetails.impl.R -import io.element.android.features.userlist.api.SearchSingleUserResultItem -import io.element.android.features.userlist.api.UserListView +import io.element.android.features.userlist.api.components.SearchSingleUserResultItem +import io.element.android.features.userlist.api.components.UserListView import io.element.android.libraries.architecture.Async import io.element.android.libraries.architecture.isLoading import io.element.android.libraries.designsystem.ElementTextStyles diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt index d97a4537ed..80207fb4bc 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt @@ -18,9 +18,9 @@ package io.element.android.features.userlist.api import androidx.compose.foundation.lazy.LazyListState import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import io.element.android.libraries.matrix.api.core.UserId -import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.libraries.matrix.ui.components.aMatrixUserList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList open class UserListStateProvider : PreviewParameterProvider { override val values: Sequence @@ -38,14 +38,14 @@ open class UserListStateProvider : PreviewParameterProvider { isSearchActive = true, searchQuery = "@someone:matrix.org", selectedUsers = aListOfSelectedUsers(), - searchResults = aListOfResults(), + searchResults = aMatrixUserList().toImmutableList(), ), aUserListState().copy( isSearchActive = true, searchQuery = "@someone:matrix.org", selectionMode = SelectionMode.Multiple, selectedUsers = aListOfSelectedUsers(), - searchResults = aListOfResults(), + searchResults = aMatrixUserList().toImmutableList(), ) ) } @@ -63,25 +63,4 @@ fun aUserListState() = UserListState( eventSink = {} ) -fun aListOfSelectedUsers() = persistentListOf( - MatrixUser(id = UserId("@someone:matrix.org")), - MatrixUser(id = UserId("@other:matrix.org"), username = "other"), -) - -fun aListOfResults() = persistentListOf( - MatrixUser(id = UserId("@someone:matrix.org")), - MatrixUser(id = UserId("@other:matrix.org"), username = "other"), - MatrixUser( - id = UserId("@someone_with_a_very_long_matrix_identifier:a_very_long_domain.org"), - username = "hey, I am someone with a very long display name" - ), - MatrixUser(id = UserId("@someone_2:matrix.org"), username = "someone 2"), - MatrixUser(id = UserId("@someone_3:matrix.org"), username = "someone 3"), - MatrixUser(id = UserId("@someone_4:matrix.org"), username = "someone 4"), - MatrixUser(id = UserId("@someone_5:matrix.org"), username = "someone 5"), - MatrixUser(id = UserId("@someone_6:matrix.org"), username = "someone 6"), - MatrixUser(id = UserId("@someone_7:matrix.org"), username = "someone 7"), - MatrixUser(id = UserId("@someone_8:matrix.org"), username = "someone 8"), - MatrixUser(id = UserId("@someone_9:matrix.org"), username = "someone 9"), - MatrixUser(id = UserId("@someone_10:matrix.org"), username = "someone 10"), -) +fun aListOfSelectedUsers() = aMatrixUserList().take(4).toImmutableList() diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt deleted file mode 100644 index 5f587b471e..0000000000 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListView.kt +++ /dev/null @@ -1,314 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.userlist.api - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Search -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SearchBarDefaults -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.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.unit.dp -import io.element.android.libraries.designsystem.components.avatar.Avatar -import io.element.android.libraries.designsystem.components.avatar.AvatarSize -import io.element.android.libraries.designsystem.components.button.BackButton -import io.element.android.libraries.designsystem.preview.ElementPreviewDark -import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.IconButton -import io.element.android.libraries.designsystem.theme.components.SearchBar -import io.element.android.libraries.designsystem.theme.components.Text -import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow -import io.element.android.libraries.matrix.ui.components.MatrixUserRow -import io.element.android.libraries.matrix.ui.model.MatrixUser -import io.element.android.libraries.matrix.ui.model.getBestName -import kotlinx.collections.immutable.ImmutableList -import io.element.android.libraries.ui.strings.R as StringR - -@Composable -fun UserListView( - state: UserListState, - modifier: Modifier = Modifier, - onUserSelected: (MatrixUser) -> Unit = {}, - onUserDeselected: (MatrixUser) -> Unit = {}, -) { - Column( - modifier = modifier, - ) { - SearchUserBar( - modifier = Modifier.fillMaxWidth(), - query = state.searchQuery, - results = state.searchResults, - selectedUsers = state.selectedUsers, - selectedUsersListState = state.selectedUsersListState, - active = state.isSearchActive, - isMultiSelectionEnabled = state.isMultiSelectionEnabled, - onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, - onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) }, - onUserSelected = { - state.eventSink(UserListEvents.AddToSelection(it)) - onUserSelected(it) - }, - onUserDeselected = { - state.eventSink(UserListEvents.RemoveFromSelection(it)) - onUserDeselected(it) - }, - ) - - if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) { - SelectedUsersList( - listState = state.selectedUsersListState, - contentPadding = PaddingValues(16.dp), - selectedUsers = state.selectedUsers, - onUserRemoved = { - state.eventSink(UserListEvents.RemoveFromSelection(it)) - onUserDeselected(it) - }, - ) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun SearchUserBar( - query: String, - results: ImmutableList, - selectedUsers: ImmutableList, - selectedUsersListState: LazyListState, - active: Boolean, - isMultiSelectionEnabled: Boolean, - modifier: Modifier = Modifier, - placeHolderTitle: String = stringResource(StringR.string.common_search_for_someone), - onActiveChanged: (Boolean) -> Unit = {}, - onTextChanged: (String) -> Unit = {}, - onUserSelected: (MatrixUser) -> Unit = {}, - onUserDeselected: (MatrixUser) -> Unit = {}, -) { - val focusManager = LocalFocusManager.current - - if (!active) { - onTextChanged("") - focusManager.clearFocus() - } - - SearchBar( - query = query, - onQueryChange = onTextChanged, - onSearch = { focusManager.clearFocus() }, - active = active, - onActiveChange = onActiveChanged, - modifier = modifier - .padding(horizontal = if (!active) 16.dp else 0.dp), - placeholder = { - Text( - text = placeHolderTitle, - modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine) - ) - }, - leadingIcon = if (active) { - { BackButton(onClick = { onActiveChanged(false) }) } - } else { - null - }, - trailingIcon = when { - active && query.isNotEmpty() -> { - { - IconButton(onClick = { onTextChanged("") }) { - Icon(Icons.Default.Close, stringResource(StringR.string.action_clear)) - } - } - } - !active -> { - { - Icon( - imageVector = Icons.Default.Search, - contentDescription = stringResource(StringR.string.action_search), - modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine) - ) - } - } - else -> null - }, - colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent), - content = { - if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { - SelectedUsersList( - listState = selectedUsersListState, - contentPadding = PaddingValues(16.dp), - selectedUsers = selectedUsers, - onUserRemoved = onUserDeselected, - ) - } - - LazyColumn { - if (isMultiSelectionEnabled) { - items(results) { matrixUser -> - SearchMultipleUsersResultItem( - modifier = Modifier.fillMaxWidth(), - matrixUser = matrixUser, - isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null, - onCheckedChange = { checked -> - if (checked) { - onUserSelected(matrixUser) - } else { - onUserDeselected(matrixUser) - } - } - ) - } - } else { - items(results) { matrixUser -> - SearchSingleUserResultItem( - modifier = Modifier.fillMaxWidth(), - matrixUser = matrixUser, - onClick = { onUserSelected(matrixUser) } - ) - } - } - } - }, - ) -} - -@Composable -fun SearchMultipleUsersResultItem( - matrixUser: MatrixUser, - isUserSelected: Boolean, - modifier: Modifier = Modifier, - onCheckedChange: (Boolean) -> Unit, -) { - CheckableMatrixUserRow( - checked = isUserSelected, - modifier = modifier, - matrixUser = matrixUser, - avatarSize = AvatarSize.Custom(36.dp), - onCheckedChange = onCheckedChange, - ) -} - -@Composable -fun SearchSingleUserResultItem( - matrixUser: MatrixUser, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, -) { - MatrixUserRow( - modifier = modifier.clickable(onClick = onClick), - matrixUser = matrixUser, - avatarSize = AvatarSize.Custom(36.dp), - ) -} - -@Composable -fun SelectedUsersList( - listState: LazyListState, - selectedUsers: ImmutableList, - modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues(0.dp), - onUserRemoved: (MatrixUser) -> Unit = {}, -) { - LazyRow( - state = listState, - modifier = modifier, - contentPadding = contentPadding, - horizontalArrangement = Arrangement.spacedBy(24.dp), - ) { - items(selectedUsers.toList()) { matrixUser -> - SelectedUser( - matrixUser = matrixUser, - onUserRemoved = onUserRemoved, - ) - } - } -} - -@Composable -fun SelectedUser( - matrixUser: MatrixUser, - modifier: Modifier = Modifier, - onUserRemoved: (MatrixUser) -> Unit, -) { - Box(modifier = modifier.width(56.dp)) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Avatar(matrixUser.avatarData.copy(size = AvatarSize.Custom(56.dp))) - Text( - text = matrixUser.getBestName(), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = MaterialTheme.typography.bodyLarge, - ) - } - IconButton( - modifier = Modifier - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary) - .size(20.dp) - .align(Alignment.TopEnd), - onClick = { onUserRemoved(matrixUser) } - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(id = StringR.string.action_remove), - tint = MaterialTheme.colorScheme.onPrimary, - ) - } - } -} - -@Preview -@Composable -internal fun UserListViewLightPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = - ElementPreviewLight { ContentToPreview(state) } - -@Preview -@Composable -internal fun UserListViewDarkPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = - ElementPreviewDark { ContentToPreview(state) } - -@Composable -private fun ContentToPreview(state: UserListState) { - UserListView(state = state) -} diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchMultipleUsersResultItem.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchMultipleUsersResultItem.kt new file mode 100644 index 0000000000..7267af1f9b --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchMultipleUsersResultItem.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.api.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.matrix.ui.components.CheckableMatrixUserRow +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.model.MatrixUser + +@Composable +fun SearchMultipleUsersResultItem( + matrixUser: MatrixUser, + isUserSelected: Boolean, + modifier: Modifier = Modifier, + onCheckedChange: (Boolean) -> Unit = {}, +) { + CheckableMatrixUserRow( + checked = isUserSelected, + modifier = modifier, + matrixUser = matrixUser, + avatarSize = AvatarSize.Custom(36.dp), + onCheckedChange = onCheckedChange, + ) +} + +@Preview +@Composable +internal fun SearchMultipleUsersResultItemLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun SearchMultipleUsersResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = true) + SearchMultipleUsersResultItem(matrixUser = aMatrixUser(), isUserSelected = false) + } +} diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchSingleUserResultItem.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchSingleUserResultItem.kt new file mode 100644 index 0000000000..67af583473 --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchSingleUserResultItem.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.api.components + +import androidx.compose.foundation.clickable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.matrix.ui.components.MatrixUserRow +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.model.MatrixUser + +@Composable +fun SearchSingleUserResultItem( + matrixUser: MatrixUser, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + MatrixUserRow( + modifier = modifier.clickable(onClick = onClick), + matrixUser = matrixUser, + avatarSize = AvatarSize.Custom(36.dp), + ) +} + +@Preview +@Composable +internal fun SearchSingleUserResultItemLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun SearchSingleUserResultItemDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + SearchSingleUserResultItem(matrixUser = aMatrixUser()) +} diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchUserBar.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchUserBar.kt new file mode 100644 index 0000000000..83aad4151b --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchUserBar.kt @@ -0,0 +1,146 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.api.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Search +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SearchBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.SearchBar +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.libraries.ui.strings.R +import kotlinx.collections.immutable.ImmutableList + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SearchUserBar( + query: String, + results: ImmutableList, + selectedUsers: ImmutableList, + selectedUsersListState: LazyListState, + active: Boolean, + isMultiSelectionEnabled: Boolean, + modifier: Modifier = Modifier, + placeHolderTitle: String = stringResource(R.string.common_search_for_someone), + onActiveChanged: (Boolean) -> Unit = {}, + onTextChanged: (String) -> Unit = {}, + onUserSelected: (MatrixUser) -> Unit = {}, + onUserDeselected: (MatrixUser) -> Unit = {}, +) { + val focusManager = LocalFocusManager.current + + if (!active) { + onTextChanged("") + focusManager.clearFocus() + } + + SearchBar( + query = query, + onQueryChange = onTextChanged, + onSearch = { focusManager.clearFocus() }, + active = active, + onActiveChange = onActiveChanged, + modifier = modifier + .padding(horizontal = if (!active) 16.dp else 0.dp), + placeholder = { + Text( + text = placeHolderTitle, + modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine) + ) + }, + leadingIcon = if (active) { + { BackButton(onClick = { onActiveChanged(false) }) } + } else { + null + }, + trailingIcon = when { + active && query.isNotEmpty() -> { + { + IconButton(onClick = { onTextChanged("") }) { + Icon(Icons.Default.Close, stringResource(R.string.action_clear)) + } + } + } + !active -> { + { + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(R.string.action_search), + modifier = Modifier.alpha(0.4f), // FIXME align on Design system theme (removing alpha should be fine) + ) + } + } + else -> null + }, + colors = if (!active) SearchBarDefaults.colors() else SearchBarDefaults.colors(containerColor = Color.Transparent), + content = { + if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { + SelectedUsersList( + listState = selectedUsersListState, + contentPadding = PaddingValues(16.dp), + selectedUsers = selectedUsers, + onUserRemoved = onUserDeselected, + ) + } + + LazyColumn { + if (isMultiSelectionEnabled) { + items(results) { matrixUser -> + SearchMultipleUsersResultItem( + modifier = Modifier.fillMaxWidth(), + matrixUser = matrixUser, + isUserSelected = selectedUsers.find { it.id == matrixUser.id } != null, + onCheckedChange = { checked -> + if (checked) { + onUserSelected(matrixUser) + } else { + onUserDeselected(matrixUser) + } + } + ) + } + } else { + items(results) { matrixUser -> + SearchSingleUserResultItem( + modifier = Modifier.fillMaxWidth(), + matrixUser = matrixUser, + onClick = { onUserSelected(matrixUser) } + ) + } + } + } + }, + ) +} diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUser.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUser.kt new file mode 100644 index 0000000000..666a0c5265 --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUser.kt @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.api.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.IconButton +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import io.element.android.libraries.matrix.ui.model.MatrixUser +import io.element.android.libraries.matrix.ui.model.getBestName +import io.element.android.libraries.ui.strings.R + +@Composable +fun SelectedUser( + matrixUser: MatrixUser, + modifier: Modifier = Modifier, + onUserRemoved: (MatrixUser) -> Unit = {}, +) { + Box(modifier = modifier.width(56.dp)) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Avatar(matrixUser.avatarData.copy(size = AvatarSize.Custom(56.dp))) + Text( + text = matrixUser.getBestName(), + overflow = TextOverflow.Ellipsis, + maxLines = 1, + style = MaterialTheme.typography.bodyLarge, + ) + } + IconButton( + modifier = Modifier + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary) + .size(20.dp) + .align(Alignment.TopEnd), + onClick = { onUserRemoved(matrixUser) } + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(id = R.string.action_remove), + tint = MaterialTheme.colorScheme.onPrimary, + ) + } + } +} + +@Preview +@Composable +internal fun SelectedUserLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun SelectedUserDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + SelectedUser(aMatrixUser()) +} diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt new file mode 100644 index 0000000000..4c2a0b10d3 --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.api.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.features.userlist.api.aListOfSelectedUsers +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun SelectedUsersList( + listState: LazyListState, + selectedUsers: ImmutableList, + modifier: Modifier = Modifier, + contentPadding: PaddingValues = PaddingValues(0.dp), + onUserRemoved: (MatrixUser) -> Unit = {}, +) { + LazyRow( + state = listState, + modifier = modifier, + contentPadding = contentPadding, + horizontalArrangement = Arrangement.spacedBy(24.dp), + ) { + items(selectedUsers.toList()) { matrixUser -> + SelectedUser( + matrixUser = matrixUser, + onUserRemoved = onUserRemoved, + ) + } + } +} + +@Preview +@Composable +internal fun SelectedUsersListLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +internal fun SelectedUsersListDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + SelectedUsersList( + listState = LazyListState(), + selectedUsers = aListOfSelectedUsers(), + ) +} diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/UserListView.kt new file mode 100644 index 0000000000..42b96ab3a2 --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/UserListView.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.api.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.userlist.api.UserListEvents +import io.element.android.features.userlist.api.UserListState +import io.element.android.features.userlist.api.UserListStateProvider +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.matrix.ui.model.MatrixUser + +@Composable +fun UserListView( + state: UserListState, + modifier: Modifier = Modifier, + onUserSelected: (MatrixUser) -> Unit = {}, + onUserDeselected: (MatrixUser) -> Unit = {}, +) { + Column( + modifier = modifier, + ) { + SearchUserBar( + modifier = Modifier.fillMaxWidth(), + query = state.searchQuery, + results = state.searchResults, + selectedUsers = state.selectedUsers, + selectedUsersListState = state.selectedUsersListState, + active = state.isSearchActive, + isMultiSelectionEnabled = state.isMultiSelectionEnabled, + onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, + onTextChanged = { state.eventSink(UserListEvents.UpdateSearchQuery(it)) }, + onUserSelected = { + state.eventSink(UserListEvents.AddToSelection(it)) + onUserSelected(it) + }, + onUserDeselected = { + state.eventSink(UserListEvents.RemoveFromSelection(it)) + onUserDeselected(it) + }, + ) + + if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) { + SelectedUsersList( + listState = state.selectedUsersListState, + contentPadding = PaddingValues(16.dp), + selectedUsers = state.selectedUsers, + onUserRemoved = { + state.eventSink(UserListEvents.RemoveFromSelection(it)) + onUserDeselected(it) + }, + ) + } + } +} + +@Preview +@Composable +internal fun UserListViewLightPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +internal fun UserListViewDarkPreview(@PreviewParameter(UserListStateProvider::class) state: UserListState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: UserListState) { + UserListView(state = state) +} From 5364bbd2cbe38b6214924fd5b035b4a952f53e66 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 12 Apr 2023 16:48:00 +0200 Subject: [PATCH 58/83] Update screenshots --- .../impl/configureroom/ConfigureRoomStateProvider.kt | 10 ++++++++++ .../features/userlist/api/UserListStateProvider.kt | 2 +- .../components/avatar/AvatarDataProvider.kt | 6 +++--- .../matrix/ui/components/MatrixUserProvider.kt | 2 +- ...dPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...dPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...PeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...PeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...ureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...ureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ ...reRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png | 4 ++-- ...reRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 +++ 12 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt index b1614381ae..32744178dd 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomStateProvider.kt @@ -18,11 +18,21 @@ package io.element.android.features.createroom.impl.configureroom import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.createroom.impl.CreateRoomConfig +import io.element.android.features.userlist.api.aListOfSelectedUsers open class ConfigureRoomStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aConfigureRoomState(), + aConfigureRoomState().copy( + config = CreateRoomConfig( + roomName = "Room 101", + topic = "Room topic for this room when the text goes onto multiple lines and is really long, there shouldn’t be more than 3 lines", + invites = aListOfSelectedUsers(), + privacy = RoomPrivacy.Private, + ), + isCreateButtonEnabled = true, + ), ) } diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt index 80207fb4bc..8d3bf68f0d 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt @@ -63,4 +63,4 @@ fun aUserListState() = UserListState( eventSink = {} ) -fun aListOfSelectedUsers() = aMatrixUserList().take(4).toImmutableList() +fun aListOfSelectedUsers() = aMatrixUserList().take(6).toImmutableList() diff --git a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt index 5e4bad531c..61c76d80c3 100644 --- a/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt +++ b/libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/components/avatar/AvatarDataProvider.kt @@ -27,8 +27,8 @@ open class AvatarDataProvider : PreviewParameterProvider { ) } -fun anAvatarData() = AvatarData( +fun anAvatarData(id: String = "@id_of_alice:server.org", name: String = "Alice") = AvatarData( // Let's the id not start with a 'a'. - id = "@id_of_alice:server.org", - name = "Alice", + id = id, + name = name, ) diff --git a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt index 89a8b4d5fd..e79e78802a 100644 --- a/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt +++ b/libraries/matrixui/src/main/kotlin/io/element/android/libraries/matrix/ui/components/MatrixUserProvider.kt @@ -35,7 +35,7 @@ open class MatrixUserProvider : PreviewParameterProvider { fun aMatrixUser(id: String = "@id_of_alice:server.org", userName: String = "Alice") = MatrixUser( id = UserId(id), username = userName, - avatarData = anAvatarData() + avatarData = anAvatarData(id, userName) ) fun aMatrixUserList() = listOf( diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index ad94119f4c..596f01f6a6 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:489aa166bf6d0f283da497b2752e457f81620959d404b5b9c3d96c7e18e8b953 -size 28100 +oid sha256:b96cf938377ee0f02e0cf2d5896eb83935d9416a5b87849ed81caed4db5e90f2 +size 41264 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 85c608618b..3fa12e378f 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0a8969c9a82f41bbdbdfb013412d039f3be4fcf450a0dd55f3217c64d51c8489 -size 23495 +oid sha256:d5fa485b200f71d2809efb3495b33f9cf000145d07cb5a37953d5f44057044c3 +size 35361 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 2074516963..339b2adf16 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5f29443193cd21eeb721a4c86f9063466439dd5ad4830708e6fa587f95e5abd -size 27456 +oid sha256:92fe30f0927b8be4ffc384548e6731e7d5e347dc5caa1c84251f2a932ee1873c +size 38848 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index 2139d8a6c5..ef68b1d3d5 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:fa79c54d2431dc0c8399a2bcb8e03d73749e9d54f505ac893e2376fded0e7dfe -size 22599 +oid sha256:0520170acaa265e514120f7552a551f660e6c56e6c5af7155c8d639a94eb2673 +size 33046 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png index 22e0b97b81..699b8973c1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2327de2be62afdacae32b297347fbcd23cd5e6986f513d018068aa981ecc3941 -size 89757 +oid sha256:756d835de7820c96019995ab47d34c2663330c4ddc9ca124eac266ea9eeef0ab +size 64397 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..c273abf7fb --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32e702b0c3671c6ef9ae38acadccf8c3da48b7093ac81ede95f8ea6f9b28e405 +size 103375 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png index 54b8ae3f9e..a9a3720ece 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:50129ad0c4d75ff5e7b8ffd43b0ffdc40d77a78f5699f9b7194a9c9ef026af69 -size 82189 +oid sha256:a03aa11c787804adc5b2947359421b90e6abcdc7e840983a0276a6e02da59a2f +size 58526 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8693cd2e06 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad0d51590e0c66e5711d1a1809a234328717548dc7fac837dc4bce4870caf70a +size 96969 From 4aad2d5ce2762d7c270244a171a500427f6256a1 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 12 Apr 2023 17:19:39 +0200 Subject: [PATCH 59/83] Rename MatrixUserDataSource to UserListDataSource --- .../createroom/impl/AllMatrixUsersDataSource.kt | 4 ++-- .../createroom/impl/addpeople/AddPeoplePresenter.kt | 6 +++--- .../features/createroom/impl/di/CreateRoomModule.kt | 4 ++-- .../createroom/impl/root/CreateRoomRootPresenter.kt | 6 +++--- .../impl/addpeople/AddPeoplePresenterTests.kt | 4 ++-- .../impl/root/CreateRoomRootPresenterTests.kt | 6 +++--- .../features/roomdetails/impl/di/RoomMemberModule.kt | 6 +++--- .../impl/members/RoomMemberListPresenter.kt | 8 ++++---- ...trixUserDataSource.kt => RoomUserListDataSource.kt} | 6 +++--- .../members/RoomMemberListPresenterTests.kt | 8 ++++---- .../{MatrixUserDataSource.kt => UserListDataSource.kt} | 2 +- .../android/features/userlist/api/UserListPresenter.kt | 2 +- .../features/userlist/impl/DefaultUserListPresenter.kt | 10 +++++----- .../userlist/impl/DefaultUserListPresenterTests.kt | 6 +++--- ...trixUserDataSource.kt => FakeUserListDataSource.kt} | 4 ++-- .../userlist/test/FakeUserListPresenterFactory.kt | 4 ++-- 16 files changed, 43 insertions(+), 43 deletions(-) rename features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/{RoomMatrixUserDataSource.kt => RoomUserListDataSource.kt} (92%) rename features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/{MatrixUserDataSource.kt => UserListDataSource.kt} (96%) rename features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/{FakeMatrixUserDataSource.kt => FakeUserListDataSource.kt} (90%) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt index 6bbdfb5e93..87f465ecbb 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/AllMatrixUsersDataSource.kt @@ -16,13 +16,13 @@ package io.element.android.features.createroom.impl -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser import javax.inject.Inject // TODO this is empty as we currently don't have an endpoint to perform user search -class AllMatrixUsersDataSource @Inject constructor() : MatrixUserDataSource { +class AllMatrixUsersDataSource @Inject constructor() : UserListDataSource { override suspend fun search(query: String): List { return emptyList() } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt index 7090a642af..6602e4de14 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt @@ -21,8 +21,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore -import io.element.android.features.userlist.api.MatrixUserDataSource import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Presenter @@ -31,14 +31,14 @@ import javax.inject.Named class AddPeoplePresenter @Inject constructor( private val userListPresenterFactory: UserListPresenter.Factory, - @Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource, + @Named("AllUsers") private val userListDataSource: UserListDataSource, private val dataStore: CreateRoomDataStore, ) : Presenter { private val userListPresenter by lazy { userListPresenterFactory.create( UserListPresenterArgs(selectionMode = SelectionMode.Multiple), - matrixUserDataSource, + userListDataSource, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt index c5f2d0ca06..46c471bf1b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/di/CreateRoomModule.kt @@ -20,7 +20,7 @@ import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module import io.element.android.features.createroom.impl.AllMatrixUsersDataSource -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.libraries.di.AppScope import javax.inject.Named @@ -30,6 +30,6 @@ interface CreateRoomModule { @Binds @Named("AllUsers") - fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): MatrixUserDataSource + fun bindAllUserListDataSource(dataSource: AllMatrixUsersDataSource): UserListDataSource } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 89932b0cdb..19ad6fdeb9 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -21,8 +21,8 @@ import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import io.element.android.features.userlist.api.MatrixUserDataSource import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Async @@ -38,14 +38,14 @@ import javax.inject.Named class CreateRoomRootPresenter @Inject constructor( private val presenterFactory: UserListPresenter.Factory, - @Named("AllUsers") private val matrixUserDataSource: MatrixUserDataSource, + @Named("AllUsers") private val userListDataSource: UserListDataSource, private val matrixClient: MatrixClient, ) : Presenter { private val presenter by lazy { presenterFactory.create( UserListPresenterArgs(selectionMode = SelectionMode.Single), - matrixUserDataSource, + userListDataSource, ) } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt index 086b5edf30..14dd92abce 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt @@ -22,7 +22,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat -import io.element.android.features.userlist.test.FakeMatrixUserDataSource +import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.features.userlist.test.FakeUserListPresenterFactory import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -35,7 +35,7 @@ class AddPeoplePresenterTests { @Before fun setup() { - presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeMatrixUserDataSource()) + presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource()) } @Test diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index cf399fdbd3..ea349f7128 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -23,7 +23,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.userlist.api.aUserListState -import io.element.android.features.userlist.test.FakeMatrixUserDataSource +import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.features.userlist.test.FakeUserListPresenter import io.element.android.features.userlist.test.FakeUserListPresenterFactory import io.element.android.libraries.architecture.Async @@ -41,7 +41,7 @@ import org.junit.Test class CreateRoomRootPresenterTests { - private lateinit var userListDataSource: FakeMatrixUserDataSource + private lateinit var userListDataSource: FakeUserListDataSource private lateinit var presenter: CreateRoomRootPresenter private lateinit var fakeUserListPresenter: FakeUserListPresenter private lateinit var fakeMatrixClient: FakeMatrixClient @@ -50,7 +50,7 @@ class CreateRoomRootPresenterTests { fun setup() { fakeUserListPresenter = FakeUserListPresenter() fakeMatrixClient = FakeMatrixClient() - userListDataSource = FakeMatrixUserDataSource() + userListDataSource = FakeUserListDataSource() presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, fakeMatrixClient) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt index 49c98374f2..88edd75f3e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt @@ -19,8 +19,8 @@ package io.element.android.features.roomdetails.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module -import io.element.android.features.roomdetails.impl.members.RoomMatrixUserDataSource -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.roomdetails.impl.members.RoomUserListDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.libraries.di.RoomScope import javax.inject.Named @@ -30,6 +30,6 @@ interface RoomMemberModule { @Binds @Named("RoomMembers") - fun bindRoomMemberUserListDataSource(dataSource: RoomMatrixUserDataSource): MatrixUserDataSource + fun bindRoomMemberUserListDataSource(dataSource: RoomUserListDataSource): UserListDataSource } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 8c3a873ade..24fb25991b 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -21,7 +21,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.element.android.features.userlist.api.SelectionMode -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Async @@ -36,13 +36,13 @@ import javax.inject.Named class RoomMemberListPresenter @Inject constructor( private val userListPresenterFactory: UserListPresenter.Factory, - @Named("RoomMembers") private val matrixUserDataSource: MatrixUserDataSource, + @Named("RoomMembers") private val userListDataSource: UserListDataSource, ) : Presenter { private val userListPresenter by lazy { userListPresenterFactory.create( UserListPresenterArgs(selectionMode = SelectionMode.Single), - matrixUserDataSource, + userListDataSource, ) } @@ -52,7 +52,7 @@ class RoomMemberListPresenter @Inject constructor( val allUsers = remember { mutableStateOf>>(Async.Loading()) } LaunchedEffect(Unit) { withContext(Dispatchers.IO) { - allUsers.value = Async.Success(matrixUserDataSource.search("").toImmutableList()) + allUsers.value = Async.Success(userListDataSource.search("").toImmutableList()) } } return RoomMemberListState( diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt similarity index 92% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt index b97dcd62e5..9f22c41666 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMatrixUserDataSource.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomUserListDataSource.kt @@ -16,7 +16,7 @@ package io.element.android.features.roomdetails.impl.members -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.libraries.core.bool.orFalse import io.element.android.libraries.designsystem.components.avatar.AvatarData import io.element.android.libraries.matrix.api.core.UserId @@ -25,9 +25,9 @@ import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser import javax.inject.Inject -class RoomMatrixUserDataSource @Inject constructor( +class RoomUserListDataSource @Inject constructor( private val room: MatrixRoom -) : MatrixUserDataSource { +) : UserListDataSource { override suspend fun search(query: String): List { return room.members().filter { member -> diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 3564daa5f1..853d2eb613 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -22,11 +22,11 @@ import app.cash.turbine.test import com.google.common.truth.Truth import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter import io.element.android.features.userlist.api.SelectionMode -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.features.userlist.impl.DefaultUserListPresenter -import io.element.android.features.userlist.test.FakeMatrixUserDataSource +import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.ui.components.aMatrixUser import kotlinx.coroutines.test.runTest @@ -38,11 +38,11 @@ class RoomMemberListPresenterTests { @Test fun `present - search is done automatically on start, but is async`() = runTest { val searchResult = listOf(aMatrixUser()) - val userListDataSource = FakeMatrixUserDataSource().apply { + val userListDataSource = FakeUserListDataSource().apply { givenSearchResult(searchResult) } val userListFactory = object : UserListPresenter.Factory { - override fun create(args: UserListPresenterArgs, dataSource: MatrixUserDataSource) = DefaultUserListPresenter(args, dataSource) + override fun create(args: UserListPresenterArgs, dataSource: UserListDataSource) = DefaultUserListPresenter(args, dataSource) } val presenter = RoomMemberListPresenter(userListFactory, userListDataSource) moleculeFlow(RecompositionClock.Immediate) { diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt similarity index 96% rename from features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt rename to features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt index 08eddfd7e9..afe2d1ab3d 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/MatrixUserDataSource.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataSource.kt @@ -19,7 +19,7 @@ package io.element.android.features.userlist.api import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser -interface MatrixUserDataSource { +interface UserListDataSource { suspend fun search(query: String): List suspend fun getProfile(userId: UserId): MatrixUser? } diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt index c328efd44e..2fa416bfb5 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt @@ -21,6 +21,6 @@ import io.element.android.libraries.architecture.Presenter interface UserListPresenter : Presenter { interface Factory { - fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter + fun create(args: UserListPresenterArgs, userListDataSource: UserListDataSource): UserListPresenter } } diff --git a/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt index 567d183e15..5aaa106eb7 100644 --- a/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt +++ b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt @@ -31,7 +31,7 @@ import com.squareup.anvil.annotations.ContributesBinding import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListEvents import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.features.userlist.api.UserListState @@ -48,13 +48,13 @@ import kotlinx.coroutines.launch class DefaultUserListPresenter @AssistedInject constructor( @Assisted val args: UserListPresenterArgs, - @Assisted val matrixUserDataSource: MatrixUserDataSource, + @Assisted val userListDataSource: UserListDataSource, ) : UserListPresenter { @AssistedFactory @ContributesBinding(SessionScope::class) interface DefaultUserListFactory : UserListPresenter.Factory { - override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): DefaultUserListPresenter + override fun create(args: UserListPresenterArgs, userListDataSource: UserListDataSource): DefaultUserListPresenter } @Composable @@ -110,9 +110,9 @@ class DefaultUserListPresenter @AssistedInject constructor( private suspend fun performSearch(query: String): ImmutableList { val isMatrixId = MatrixPatterns.isUserId(query) - val results = matrixUserDataSource.search(query).toMutableList() + val results = userListDataSource.search(query).toMutableList() if (isMatrixId && results.none { it.id.value == query }) { - val getProfileResult: MatrixUser? = matrixUserDataSource.getProfile(UserId(query)) + val getProfileResult: MatrixUser? = userListDataSource.getProfile(UserId(query)) val profile = getProfileResult ?: MatrixUser(UserId(query)) results.add(0, profile) } diff --git a/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt b/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt index 1cae186d56..15c26fb8d3 100644 --- a/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt +++ b/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt @@ -21,10 +21,10 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListEvents import io.element.android.features.userlist.api.UserListPresenterArgs -import io.element.android.features.userlist.api.SelectionMode -import io.element.android.features.userlist.test.FakeMatrixUserDataSource +import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.components.aMatrixUser import io.element.android.libraries.matrix.ui.model.MatrixUser @@ -37,7 +37,7 @@ import org.junit.Test @OptIn(ExperimentalCoroutinesApi::class) class DefaultUserListPresenterTests { - private val userListDataSource = FakeMatrixUserDataSource() + private val userListDataSource = FakeUserListDataSource() @Test fun `present - initial state for single selection`() = runTest { diff --git a/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListDataSource.kt similarity index 90% rename from features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt rename to features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListDataSource.kt index db6297ec05..ba0ccd2c89 100644 --- a/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeMatrixUserDataSource.kt +++ b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListDataSource.kt @@ -16,11 +16,11 @@ package io.element.android.features.userlist.test -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.ui.model.MatrixUser -class FakeMatrixUserDataSource : MatrixUserDataSource { +class FakeUserListDataSource : UserListDataSource { private var searchResult: List = emptyList() private var profile: MatrixUser? = null diff --git a/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListPresenterFactory.kt b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListPresenterFactory.kt index 37d50c303c..2255512490 100644 --- a/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListPresenterFactory.kt +++ b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListPresenterFactory.kt @@ -16,7 +16,7 @@ package io.element.android.features.userlist.test -import io.element.android.features.userlist.api.MatrixUserDataSource +import io.element.android.features.userlist.api.UserListDataSource import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs @@ -24,5 +24,5 @@ class FakeUserListPresenterFactory( private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter() ) : UserListPresenter.Factory { - override fun create(args: UserListPresenterArgs, matrixUserDataSource: MatrixUserDataSource): UserListPresenter = fakeUserListPresenter + override fun create(args: UserListPresenterArgs, userListDataSource: UserListDataSource): UserListPresenter = fakeUserListPresenter } From ca7e8bca0c14e0f5c12ca6d6f05d5c67e974818d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Wed, 12 Apr 2023 17:25:44 +0200 Subject: [PATCH 60/83] Fix tests --- .../configureroom/ConfigureRoomPresenter.kt | 21 +++++++++-------- .../impl/addpeople/AddPeoplePresenterTests.kt | 3 ++- .../ConfigureRoomPresenterTests.kt | 23 ++++++++++--------- 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 53523449f7..7cb5401511 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -41,17 +41,18 @@ class ConfigureRoomPresenter @Inject constructor( fun handleEvents(event: ConfigureRoomEvents) { when (event) { - is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(avatarUrl = event.uri?.toString())) - is ConfigureRoomEvents.RoomNameChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(roomName = event.name)) - is ConfigureRoomEvents.TopicChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(topic = event.topic.takeUnless { it.isEmpty() })) - is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setCreateRoomConfig(createRoomConfig.value.copy(privacy = event.privacy)) - is ConfigureRoomEvents.RemoveFromSelection -> dataStore.setCreateRoomConfig( - createRoomConfig.value.copy( - invites = createRoomConfig.value.invites.minus( - event.matrixUser - ).toImmutableList() + is ConfigureRoomEvents.AvatarUriChanged -> + dataStore.setCreateRoomConfig(createRoomConfig.value.copy(avatarUrl = event.uri?.toString())) + is ConfigureRoomEvents.RoomNameChanged -> + dataStore.setCreateRoomConfig(createRoomConfig.value.copy(roomName = event.name.takeUnless { it.isEmpty() })) + is ConfigureRoomEvents.TopicChanged -> + dataStore.setCreateRoomConfig(createRoomConfig.value.copy(topic = event.topic.takeUnless { it.isEmpty() })) + is ConfigureRoomEvents.RoomPrivacyChanged -> + dataStore.setCreateRoomConfig(createRoomConfig.value.copy(privacy = event.privacy)) + is ConfigureRoomEvents.RemoveFromSelection -> + dataStore.setCreateRoomConfig( + createRoomConfig.value.copy(invites = createRoomConfig.value.invites.minus(event.matrixUser).toImmutableList()) ) - ) ConfigureRoomEvents.CreateRoom -> Unit } } diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt index 14dd92abce..4b89eb7cbc 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt @@ -22,6 +22,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.features.userlist.test.FakeUserListPresenterFactory import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -35,7 +36,7 @@ class AddPeoplePresenterTests { @Before fun setup() { - presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource()) + presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource(), CreateRoomDataStore()) } @Test diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 0934909460..c77416bacb 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -23,6 +23,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_NAME @@ -40,7 +41,7 @@ class ConfigureRoomPresenterTests { @Before fun setup() { - presenter = ConfigureRoomPresenter(ConfigureRoomPresenterArgs(emptyList())) + presenter = ConfigureRoomPresenter(CreateRoomDataStore()) } @Test @@ -49,9 +50,9 @@ class ConfigureRoomPresenterTests { presenter.present() }.test { val initialState = awaitItem() - assertThat(initialState.roomName).isEmpty() - assertThat(initialState.topic).isEmpty() - assertThat(initialState.privacy).isNull() + assertThat(initialState.config.roomName).isNull() + assertThat(initialState.config.topic).isNull() + assertThat(initialState.config.privacy).isNull() } } @@ -66,19 +67,19 @@ class ConfigureRoomPresenterTests { // Room name not empty initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) var newState: ConfigureRoomState = awaitItem() - assertThat(newState.roomName).isEqualTo(A_ROOM_NAME) + assertThat(newState.config.roomName).isEqualTo(A_ROOM_NAME) assertThat(newState.isCreateButtonEnabled).isFalse() // Select privacy initialState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Private)) newState = awaitItem() - assertThat(newState.privacy).isEqualTo(RoomPrivacy.Private) + assertThat(newState.config.privacy).isEqualTo(RoomPrivacy.Private) assertThat(newState.isCreateButtonEnabled).isTrue() // Clear room name initialState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) newState = awaitItem() - assertThat(newState.roomName).isEqualTo("") + assertThat(newState.config.roomName).isNull() assertThat(newState.isCreateButtonEnabled).isFalse() } } @@ -92,23 +93,23 @@ class ConfigureRoomPresenterTests { // Room name initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) val stateAfterRoomNameChanged = awaitItem() - assertThat(stateAfterRoomNameChanged.roomName).isEqualTo(A_ROOM_NAME) + assertThat(stateAfterRoomNameChanged.config.roomName).isEqualTo(A_ROOM_NAME) // Room topic stateAfterRoomNameChanged.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) val stateAfterTopicChanged = awaitItem() - assertThat(stateAfterTopicChanged.topic).isEqualTo(A_MESSAGE) + assertThat(stateAfterTopicChanged.config.topic).isEqualTo(A_MESSAGE) // Room avatar val anUri = Uri.parse(AN_AVATAR_URL) stateAfterTopicChanged.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri)) val stateAfterAvatarUriChanged = awaitItem() - assertThat(stateAfterAvatarUriChanged.avatarUri).isEqualTo(anUri) + assertThat(stateAfterAvatarUriChanged.config.avatarUrl).isEqualTo(anUri.toString()) // Room privacy stateAfterAvatarUriChanged.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public)) val stateAfterPrivacyChanged = awaitItem() - assertThat(stateAfterPrivacyChanged.privacy).isEqualTo(RoomPrivacy.Public) + assertThat(stateAfterPrivacyChanged.config.privacy).isEqualTo(RoomPrivacy.Public) } } } From 8950428cd3e2edc5fed1b0dc69aeb10645ce02d1 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 10:06:05 +0200 Subject: [PATCH 61/83] Persist selected users in data store --- .../createroom/impl/CreateRoomDataStore.kt | 31 ++++++++++++++-- .../impl/addpeople/AddPeoplePresenter.kt | 8 +--- .../configureroom/ConfigureRoomPresenter.kt | 18 +++------ .../impl/root/CreateRoomRootPresenter.kt | 3 ++ .../impl/members/RoomMemberListPresenter.kt | 3 ++ .../userlist/api/UserListDataStore.kt | 37 +++++++++++++++++++ .../userlist/api/UserListPresenter.kt | 6 ++- .../userlist/impl/DefaultUserListPresenter.kt | 19 ++++++---- 8 files changed, 93 insertions(+), 32 deletions(-) create mode 100644 features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataStore.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt index ed61431511..e55fca755d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomDataStore.kt @@ -16,20 +16,43 @@ package io.element.android.features.createroom.impl +import io.element.android.features.createroom.impl.configureroom.RoomPrivacy import io.element.android.features.createroom.impl.di.CreateRoomScope +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.libraries.di.SingleIn +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine import javax.inject.Inject @SingleIn(CreateRoomScope::class) -class CreateRoomDataStore @Inject constructor() { +class CreateRoomDataStore @Inject constructor( + val selectedUserListDataStore: UserListDataStore, +) { private val createRoomConfigFlow: MutableStateFlow = MutableStateFlow(CreateRoomConfig()) - fun getCreateRoomConfig(): Flow = createRoomConfigFlow + fun getCreateRoomConfig(): Flow = combine( + selectedUserListDataStore.selectedUsers(), + createRoomConfigFlow, + ) { selectedUsers, config -> + config.copy(invites = selectedUsers.toImmutableList()) + } - fun setCreateRoomConfig(createRoomConfig: CreateRoomConfig) { - createRoomConfigFlow.tryEmit(createRoomConfig) + fun setRoomName(roomName: String?) { + createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(roomName = roomName?.takeIf { it.isNotEmpty() })) + } + + fun setTopic(topic: String?) { + createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(topic = topic?.takeIf { it.isNotEmpty() })) + } + + fun setAvatarUrl(avatarUrl: String?) { + createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(avatarUrl = avatarUrl)) + } + + fun setPrivacy(privacy: RoomPrivacy?) { + createRoomConfigFlow.tryEmit(createRoomConfigFlow.value.copy(privacy = privacy)) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt index 6602e4de14..88e24f58b7 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt @@ -17,9 +17,6 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListDataSource @@ -39,16 +36,13 @@ class AddPeoplePresenter @Inject constructor( userListPresenterFactory.create( UserListPresenterArgs(selectionMode = SelectionMode.Multiple), userListDataSource, + dataStore.selectedUserListDataStore, ) } @Composable override fun present(): AddPeopleState { val userListState = userListPresenter.present() - val createRoomConfig = dataStore.getCreateRoomConfig().collectAsState(CreateRoomConfig()) - LaunchedEffect(userListState.selectedUsers) { - dataStore.setCreateRoomConfig(createRoomConfig.value.copy(invites = userListState.selectedUsers)) - } fun handleEvents(event: AddPeopleEvents) { // do nothing for now } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index 7cb5401511..e9211976af 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.libraries.architecture.Presenter -import kotlinx.collections.immutable.toImmutableList import javax.inject.Inject class ConfigureRoomPresenter @Inject constructor( @@ -41,18 +40,11 @@ class ConfigureRoomPresenter @Inject constructor( fun handleEvents(event: ConfigureRoomEvents) { when (event) { - is ConfigureRoomEvents.AvatarUriChanged -> - dataStore.setCreateRoomConfig(createRoomConfig.value.copy(avatarUrl = event.uri?.toString())) - is ConfigureRoomEvents.RoomNameChanged -> - dataStore.setCreateRoomConfig(createRoomConfig.value.copy(roomName = event.name.takeUnless { it.isEmpty() })) - is ConfigureRoomEvents.TopicChanged -> - dataStore.setCreateRoomConfig(createRoomConfig.value.copy(topic = event.topic.takeUnless { it.isEmpty() })) - is ConfigureRoomEvents.RoomPrivacyChanged -> - dataStore.setCreateRoomConfig(createRoomConfig.value.copy(privacy = event.privacy)) - is ConfigureRoomEvents.RemoveFromSelection -> - dataStore.setCreateRoomConfig( - createRoomConfig.value.copy(invites = createRoomConfig.value.invites.minus(event.matrixUser).toImmutableList()) - ) + is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString()) + is ConfigureRoomEvents.RoomNameChanged -> dataStore.setRoomName(event.name) + is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic) + is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy) + is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) ConfigureRoomEvents.CreateRoom -> Unit } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt index 19ad6fdeb9..2ef30908e1 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenter.kt @@ -23,6 +23,7 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListDataSource +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Async @@ -39,6 +40,7 @@ import javax.inject.Named class CreateRoomRootPresenter @Inject constructor( private val presenterFactory: UserListPresenter.Factory, @Named("AllUsers") private val userListDataSource: UserListDataSource, + private val userListDataStore: UserListDataStore, private val matrixClient: MatrixClient, ) : Presenter { @@ -46,6 +48,7 @@ class CreateRoomRootPresenter @Inject constructor( presenterFactory.create( UserListPresenterArgs(selectionMode = SelectionMode.Single), userListDataSource, + userListDataStore, ) } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt index 24fb25991b..8841bd9d5e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListPresenter.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListDataSource +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.libraries.architecture.Async @@ -37,12 +38,14 @@ import javax.inject.Named class RoomMemberListPresenter @Inject constructor( private val userListPresenterFactory: UserListPresenter.Factory, @Named("RoomMembers") private val userListDataSource: UserListDataSource, + private val userListDataStore: UserListDataStore, ) : Presenter { private val userListPresenter by lazy { userListPresenterFactory.create( UserListPresenterArgs(selectionMode = SelectionMode.Single), userListDataSource, + userListDataStore, ) } diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataStore.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataStore.kt new file mode 100644 index 0000000000..c1e982dd59 --- /dev/null +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataStore.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.userlist.api + +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import javax.inject.Inject + +class UserListDataStore @Inject constructor() { + + private val selectedUsers: MutableStateFlow> = MutableStateFlow(emptyList()) + + fun selectUser(user: MatrixUser) { + selectedUsers.tryEmit(selectedUsers.value.plus(user)) + } + + fun removeUserFromSelection(user: MatrixUser) { + selectedUsers.tryEmit(selectedUsers.value.minus(user)) + } + + fun selectedUsers(): Flow> = selectedUsers +} diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt index 2fa416bfb5..90205eab2a 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListPresenter.kt @@ -21,6 +21,10 @@ import io.element.android.libraries.architecture.Presenter interface UserListPresenter : Presenter { interface Factory { - fun create(args: UserListPresenterArgs, userListDataSource: UserListDataSource): UserListPresenter + fun create( + args: UserListPresenterArgs, + userListDataSource: UserListDataSource, + userListDataStore: UserListDataStore, + ): UserListPresenter } } diff --git a/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt index 5aaa106eb7..8e1829f4a7 100644 --- a/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt +++ b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt @@ -21,6 +21,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -32,10 +33,11 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import io.element.android.features.userlist.api.UserListDataSource +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.api.UserListEvents +import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.features.userlist.api.UserListState -import io.element.android.features.userlist.api.UserListPresenter import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.UserId @@ -49,21 +51,24 @@ import kotlinx.coroutines.launch class DefaultUserListPresenter @AssistedInject constructor( @Assisted val args: UserListPresenterArgs, @Assisted val userListDataSource: UserListDataSource, + @Assisted val userListDataStore: UserListDataStore, ) : UserListPresenter { @AssistedFactory @ContributesBinding(SessionScope::class) interface DefaultUserListFactory : UserListPresenter.Factory { - override fun create(args: UserListPresenterArgs, userListDataSource: UserListDataSource): DefaultUserListPresenter + override fun create( + args: UserListPresenterArgs, + userListDataSource: UserListDataSource, + userListDataStore: UserListDataStore, + ): DefaultUserListPresenter } @Composable override fun present(): UserListState { val localCoroutineScope = rememberCoroutineScope() var isSearchActive by rememberSaveable { mutableStateOf(false) } - val selectedUsers: MutableState> = remember { - mutableStateOf(persistentListOf()) - } + val selectedUsers = userListDataStore.selectedUsers().collectAsState(emptyList()) val selectedUsersListState = rememberLazyListState() var searchQuery by rememberSaveable { mutableStateOf("") } val searchResults: MutableState> = remember { @@ -76,11 +81,11 @@ class DefaultUserListPresenter @AssistedInject constructor( is UserListEvents.UpdateSearchQuery -> searchQuery = event.query is UserListEvents.AddToSelection -> { if (event.matrixUser !in selectedUsers.value) { - selectedUsers.value = selectedUsers.value.plus(event.matrixUser).toImmutableList() + userListDataStore.selectUser(event.matrixUser) } localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState) } - is UserListEvents.RemoveFromSelection -> selectedUsers.value = selectedUsers.value.minus(event.matrixUser).toImmutableList() + is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser) } } From 8de8dca65340c9b829311db95c23d0bcdcd20f3e Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 10:26:21 +0200 Subject: [PATCH 62/83] Improve AddPeople node --- .../impl/addpeople/AddPeopleEvents.kt | 19 ---------- .../impl/addpeople/AddPeoplePresenter.kt | 19 +++------- .../impl/addpeople/AddPeopleState.kt | 24 ------------- .../impl/addpeople/AddPeopleStateProvider.kt | 35 +++++++++---------- .../impl/addpeople/AddPeopleView.kt | 19 +++++----- 5 files changed, 29 insertions(+), 87 deletions(-) delete mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleEvents.kt delete mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleEvents.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleEvents.kt deleted file mode 100644 index 5d246be501..0000000000 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleEvents.kt +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.createroom.impl.addpeople - -sealed interface AddPeopleEvents diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt index 88e24f58b7..21ab9fafbd 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt @@ -18,10 +18,7 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.runtime.Composable import io.element.android.features.createroom.impl.CreateRoomDataStore -import io.element.android.features.userlist.api.SelectionMode -import io.element.android.features.userlist.api.UserListDataSource -import io.element.android.features.userlist.api.UserListPresenter -import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.features.userlist.api.* import io.element.android.libraries.architecture.Presenter import javax.inject.Inject import javax.inject.Named @@ -30,7 +27,7 @@ class AddPeoplePresenter @Inject constructor( private val userListPresenterFactory: UserListPresenter.Factory, @Named("AllUsers") private val userListDataSource: UserListDataSource, private val dataStore: CreateRoomDataStore, -) : Presenter { +) : Presenter { private val userListPresenter by lazy { userListPresenterFactory.create( @@ -41,16 +38,8 @@ class AddPeoplePresenter @Inject constructor( } @Composable - override fun present(): AddPeopleState { - val userListState = userListPresenter.present() - fun handleEvents(event: AddPeopleEvents) { - // do nothing for now - } - - return AddPeopleState( - userListState = userListState, - eventSink = ::handleEvents, - ) + override fun present(): UserListState { + return userListPresenter.present() } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt deleted file mode 100644 index 8605e1aba6..0000000000 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleState.kt +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright (c) 2023 New Vector Ltd - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.element.android.features.createroom.impl.addpeople - -import io.element.android.features.userlist.api.UserListState - -data class AddPeopleState( - val userListState: UserListState, - val eventSink: (AddPeopleEvents) -> Unit, -) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt index cfbf7941ce..3c9073d0d0 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt @@ -18,30 +18,27 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.UserListState import io.element.android.features.userlist.api.aListOfSelectedUsers import io.element.android.features.userlist.api.aUserListState +import io.element.android.libraries.matrix.ui.components.aMatrixUserList +import kotlinx.collections.immutable.toImmutableList -open class AddPeopleStateProvider : PreviewParameterProvider { - override val values: Sequence +open class AddPeopleStateProvider : PreviewParameterProvider { + override val values: Sequence get() = sequenceOf( - aAddPeopleState(), - aAddPeopleState().copy( - userListState = aUserListState().copy( - selectedUsers = aListOfSelectedUsers(), - selectionMode = SelectionMode.Multiple, - ) + aUserListState(), + aUserListState().copy( + searchResults = aMatrixUserList().toImmutableList(), + selectedUsers = aListOfSelectedUsers(), + isSearchActive = false, + selectionMode = SelectionMode.Multiple, ), - aAddPeopleState().copy( - userListState = aUserListState().copy( - selectedUsers = aListOfSelectedUsers(), - isSearchActive = true, - selectionMode = SelectionMode.Multiple, - ) + aUserListState().copy( + searchResults = aMatrixUserList().toImmutableList(), + selectedUsers = aListOfSelectedUsers(), + isSearchActive = true, + selectionMode = SelectionMode.Multiple, ) ) } - -fun aAddPeopleState() = AddPeopleState( - userListState = aUserListState(), - eventSink = {} -) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index 9de3e0fa20..e3e7911a20 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import io.element.android.features.createroom.impl.R +import io.element.android.features.userlist.api.UserListState import io.element.android.features.userlist.api.components.UserListView import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark @@ -44,20 +45,18 @@ import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @Composable fun AddPeopleView( - state: AddPeopleState, + state: UserListState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, onNextPressed: (List) -> Unit = {}, ) { - val eventSink = state.eventSink - Scaffold( topBar = { - if (!state.userListState.isSearchActive) { + if (!state.isSearchActive) { AddPeopleViewTopBar( - hasSelectedUsers = state.userListState.selectedUsers.isNotEmpty(), + hasSelectedUsers = state.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, - onNextPressed = { onNextPressed(state.userListState.selectedUsers) }, + onNextPressed = { onNextPressed(state.selectedUsers) }, ) } } @@ -69,7 +68,7 @@ fun AddPeopleView( ) { UserListView( modifier = Modifier.fillMaxWidth(), - state = state.userListState, + state = state, ) } } @@ -110,15 +109,15 @@ fun AddPeopleViewTopBar( @Preview @Composable -internal fun AddPeopleViewLightPreview(@PreviewParameter(AddPeopleStateProvider::class) state: AddPeopleState) = +internal fun AddPeopleViewLightPreview(@PreviewParameter(AddPeopleStateProvider::class) state: UserListState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun AddPeopleViewDarkPreview(@PreviewParameter(AddPeopleStateProvider::class) state: AddPeopleState) = +internal fun AddPeopleViewDarkPreview(@PreviewParameter(AddPeopleStateProvider::class) state: UserListState) = ElementPreviewDark { ContentToPreview(state) } @Composable -private fun ContentToPreview(state: AddPeopleState) { +private fun ContentToPreview(state: UserListState) { AddPeopleView(state = state) } From 93b06116a3239474e379aab022762ab2856c3b81 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 11:34:19 +0200 Subject: [PATCH 63/83] Introduce ConfigureRoomFlowNode and bind CreateRoomScope to this flow --- .../createroom/impl/ConfigureRoomFlowNode.kt | 96 +++++++++++++++++++ .../createroom/impl/CreateRoomFlowNode.kt | 43 ++------- 2 files changed, 105 insertions(+), 34 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt new file mode 100644 index 0000000000..55c6c693c5 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl + +import android.os.Parcelable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.bumble.appyx.core.composable.Children +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.navmodel.backstack.BackStack +import com.bumble.appyx.navmodel.backstack.operation.push +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.createroom.impl.addpeople.AddPeopleNode +import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode +import io.element.android.features.createroom.impl.di.CreateRoomComponent +import io.element.android.libraries.architecture.BackstackNode +import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler +import io.element.android.libraries.architecture.bindings +import io.element.android.libraries.architecture.createNode +import io.element.android.libraries.di.DaggerComponentOwner +import io.element.android.libraries.di.SessionScope +import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.parcelize.Parcelize + +@ContributesNode(SessionScope::class) +class ConfigureRoomFlowNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, +) : DaggerComponentOwner, + BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins + ) { + + private val component by lazy { + parent!!.bindings().createRoomComponentBuilder().build() + } + + override val daggerComponent: Any + get() = component + + sealed interface NavTarget : Parcelable { + @Parcelize + object Root : NavTarget + + @Parcelize + object ConfigureRoom : NavTarget + } + + override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { + return when (navTarget) { + NavTarget.Root -> { + val callback = object : AddPeopleNode.Callback { + override fun onContinue(selectedUsers: List) { + backstack.push(NavTarget.ConfigureRoom) + } + } + createNode(context = buildContext, plugins = listOf(callback)) + } + NavTarget.ConfigureRoom -> { + createNode(context = buildContext) + } + } + } + + @Composable + override fun View(modifier: Modifier) { + Children( + navModel = backstack, + modifier = modifier, + transitionHandler = rememberDefaultTransitionHandler() + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt index c663814c19..6b9edc68d2 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/CreateRoomFlowNode.kt @@ -30,40 +30,26 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.createroom.api.CreateRoomEntryPoint -import io.element.android.features.createroom.impl.addpeople.AddPeopleNode -import io.element.android.features.createroom.impl.configureroom.ConfigureRoomNode -import io.element.android.features.createroom.impl.di.CreateRoomComponent import io.element.android.features.createroom.impl.root.CreateRoomRootNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler -import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode -import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.SessionScope import io.element.android.libraries.matrix.api.core.RoomId -import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) class CreateRoomFlowNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, -) : DaggerComponentOwner, - BackstackNode( - backstack = BackStack( - initialElement = NavTarget.Root, - savedStateMap = buildContext.savedStateMap, - ), - buildContext = buildContext, - plugins = plugins - ) { - - private val component by lazy { - parent!!.bindings().createRoomComponentBuilder().build() - } - - override val daggerComponent: Any - get() = component +) : BackstackNode( + backstack = BackStack( + initialElement = NavTarget.Root, + savedStateMap = buildContext.savedStateMap, + ), + buildContext = buildContext, + plugins = plugins +) { sealed interface NavTarget : Parcelable { @Parcelize @@ -71,9 +57,6 @@ class CreateRoomFlowNode @AssistedInject constructor( @Parcelize object NewRoom : NavTarget - - @Parcelize - object ConfigureRoom : NavTarget } override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { @@ -91,15 +74,7 @@ class CreateRoomFlowNode @AssistedInject constructor( createNode(context = buildContext, plugins = listOf(callback)) } NavTarget.NewRoom -> { - val callback = object : AddPeopleNode.Callback { - override fun onContinue(selectedUsers: List) { - backstack.push(NavTarget.ConfigureRoom) - } - } - createNode(context = buildContext, plugins = listOf(callback)) - } - NavTarget.ConfigureRoom -> { - createNode(context = buildContext) + createNode(context = buildContext, plugins = emptyList()) } } } From 9cbfa4096a612586af93db1c76e5e1801a8acf14 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 11:37:17 +0200 Subject: [PATCH 64/83] Remove useless selectedUsers parameter --- .../features/createroom/impl/ConfigureRoomFlowNode.kt | 3 +-- .../features/createroom/impl/addpeople/AddPeopleNode.kt | 9 ++++----- .../features/createroom/impl/addpeople/AddPeopleView.kt | 5 ++--- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt index 55c6c693c5..a01f0747f5 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/ConfigureRoomFlowNode.kt @@ -37,7 +37,6 @@ import io.element.android.libraries.architecture.bindings import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.DaggerComponentOwner import io.element.android.libraries.di.SessionScope -import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.parcelize.Parcelize @ContributesNode(SessionScope::class) @@ -73,7 +72,7 @@ class ConfigureRoomFlowNode @AssistedInject constructor( return when (navTarget) { NavTarget.Root -> { val callback = object : AddPeopleNode.Callback { - override fun onContinue(selectedUsers: List) { + override fun onContinue() { backstack.push(NavTarget.ConfigureRoom) } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt index 2d060a6644..1b4bd9ac8d 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleNode.kt @@ -26,7 +26,6 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.createroom.impl.di.CreateRoomScope -import io.element.android.libraries.matrix.ui.model.MatrixUser @ContributesNode(CreateRoomScope::class) class AddPeopleNode @AssistedInject constructor( @@ -36,11 +35,11 @@ class AddPeopleNode @AssistedInject constructor( ) : Node(buildContext, plugins = plugins) { interface Callback : Plugin { - fun onContinue(selectedUsers: List) + fun onContinue() } - private fun onContinue(selectedUsers: List) { - plugins().forEach { it.onContinue(selectedUsers) } + private fun onContinue() { + plugins().forEach { it.onContinue() } } @Composable @@ -49,7 +48,7 @@ class AddPeopleNode @AssistedInject constructor( AddPeopleView( state = state, modifier = modifier, - onBackPressed = { navigateUp() }, + onBackPressed = this::navigateUp, onNextPressed = this::onContinue, ) } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index e3e7911a20..e81a47a04a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -39,7 +39,6 @@ import io.element.android.libraries.designsystem.theme.components.CenterAlignedT import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton -import io.element.android.libraries.matrix.ui.model.MatrixUser import io.element.android.libraries.ui.strings.R as StringR @OptIn(ExperimentalMaterial3Api::class) @@ -48,7 +47,7 @@ fun AddPeopleView( state: UserListState, modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, - onNextPressed: (List) -> Unit = {}, + onNextPressed: () -> Unit = {}, ) { Scaffold( topBar = { @@ -56,7 +55,7 @@ fun AddPeopleView( AddPeopleViewTopBar( hasSelectedUsers = state.selectedUsers.isNotEmpty(), onBackPressed = onBackPressed, - onNextPressed = { onNextPressed(state.selectedUsers) }, + onNextPressed = onNextPressed, ) } } From d4bc00aae47e80cb3cab704de8e329e1208383bd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 14:12:46 +0200 Subject: [PATCH 65/83] Show toast for not implemented actions --- .../createroom/impl/configureroom/ConfigureRoomPresenter.kt | 5 ++++- .../createroom/impl/configureroom/ConfigureRoomView.kt | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index e9211976af..b504edae96 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -16,11 +16,13 @@ package io.element.android.features.createroom.impl.configureroom +import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.platform.LocalContext import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.libraries.architecture.Presenter @@ -38,6 +40,7 @@ class ConfigureRoomPresenter @Inject constructor( mutableStateOf(enabled) } + val context = LocalContext.current fun handleEvents(event: ConfigureRoomEvents) { when (event) { is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString()) @@ -45,7 +48,7 @@ class ConfigureRoomPresenter @Inject constructor( is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic) is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy) is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) - ConfigureRoomEvents.CreateRoom -> Unit + ConfigureRoomEvents.CreateRoom -> Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index ddb000d883..b85901fc2b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -17,6 +17,7 @@ package io.element.android.features.createroom.impl.configureroom import android.net.Uri +import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -77,6 +78,7 @@ fun ConfigureRoomView( onBackPressed: () -> Unit = {}, ) { val selectedUsersListState = rememberLazyListState() + val context = LocalContext.current Scaffold( modifier = modifier, topBar = { @@ -95,6 +97,7 @@ fun ConfigureRoomView( modifier = Modifier.padding(horizontal = 16.dp), avatarUri = state.config.avatarUrl?.toUri(), roomName = state.config.roomName.orEmpty(), + onAvatarClick = { Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() }, onRoomNameChanged = { state.eventSink(ConfigureRoomEvents.RoomNameChanged(it)) }, ) RoomTopic( From a7d273432d092b93b7d94d784e77906bc223f7d2 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 14:24:33 +0200 Subject: [PATCH 66/83] Update screenshots --- ...roup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- ...oup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png index 3fa12e378f..8b87a5a644 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d5fa485b200f71d2809efb3495b33f9cf000145d07cb5a37953d5f44057044c3 -size 35361 +oid sha256:56148beb26b35c8309190271b43fc225e11b90b8b849a8e60abf98b6ab663c1b +size 101010 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png index ef68b1d3d5..ba7682c859 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.addpeople_null_DefaultGroup_AddPeopleViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:0520170acaa265e514120f7552a551f660e6c56e6c5af7155c8d639a94eb2673 -size 33046 +oid sha256:9fa60afaf7ab23f66622d7d77e168db8b9d8bc15d178e8adb63f944de3cc29df +size 96242 From f3b64e0ca58fe0aad69daf68f264debbfe238b29 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 15:44:00 +0200 Subject: [PATCH 67/83] do not reverse selected user list ordering & add autoscroll when selecting user --- .../impl/configureroom/ConfigureRoomView.kt | 3 --- .../features/userlist/api/UserListState.kt | 2 -- .../userlist/api/UserListStateProvider.kt | 5 ---- .../userlist/api/components/SearchUserBar.kt | 4 +-- .../api/components/SelectedUsersList.kt | 27 ++++++++++++++----- .../userlist/api/components/UserListView.kt | 3 +-- .../userlist/impl/DefaultUserListPresenter.kt | 15 +---------- 7 files changed, 24 insertions(+), 35 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index b85901fc2b..34d0332c0f 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -29,7 +29,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape @@ -77,7 +76,6 @@ fun ConfigureRoomView( modifier: Modifier = Modifier, onBackPressed: () -> Unit = {}, ) { - val selectedUsersListState = rememberLazyListState() val context = LocalContext.current Scaffold( modifier = modifier, @@ -106,7 +104,6 @@ fun ConfigureRoomView( onTopicChanged = { state.eventSink(ConfigureRoomEvents.TopicChanged(it)) }, ) SelectedUsersList( - listState = selectedUsersListState, contentPadding = PaddingValues(horizontal = 24.dp), selectedUsers = state.config.invites, onUserRemoved = { state.eventSink(ConfigureRoomEvents.RemoveFromSelection(it)) }, diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt index 80de1e991f..dfbfddbcf5 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListState.kt @@ -16,7 +16,6 @@ package io.element.android.features.userlist.api -import androidx.compose.foundation.lazy.LazyListState import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList @@ -24,7 +23,6 @@ data class UserListState( val searchQuery: String, val searchResults: ImmutableList, val selectedUsers: ImmutableList, - val selectedUsersListState: LazyListState, val isSearchActive: Boolean, val selectionMode: SelectionMode, val eventSink: (UserListEvents) -> Unit, diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt index 8d3bf68f0d..7ea0ba0827 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListStateProvider.kt @@ -16,7 +16,6 @@ package io.element.android.features.userlist.api -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.ui.tooling.preview.PreviewParameterProvider import io.element.android.libraries.matrix.ui.components.aMatrixUserList import kotlinx.collections.immutable.persistentListOf @@ -55,10 +54,6 @@ fun aUserListState() = UserListState( searchQuery = "", searchResults = persistentListOf(), selectedUsers = persistentListOf(), - selectedUsersListState = LazyListState( - firstVisibleItemIndex = 0, - firstVisibleItemScrollOffset = 0, - ), selectionMode = SelectionMode.Single, eventSink = {} ) diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchUserBar.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchUserBar.kt index 83aad4151b..baee8c5b2a 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchUserBar.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SearchUserBar.kt @@ -20,7 +20,6 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close @@ -49,7 +48,6 @@ fun SearchUserBar( query: String, results: ImmutableList, selectedUsers: ImmutableList, - selectedUsersListState: LazyListState, active: Boolean, isMultiSelectionEnabled: Boolean, modifier: Modifier = Modifier, @@ -108,9 +106,9 @@ fun SearchUserBar( content = { if (isMultiSelectionEnabled && active && selectedUsers.isNotEmpty()) { SelectedUsersList( - listState = selectedUsersListState, contentPadding = PaddingValues(16.dp), selectedUsers = selectedUsers, + autoScroll = true, onUserRemoved = onUserDeselected, ) } diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt index 4c2a0b10d3..491fb7827f 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt @@ -18,10 +18,15 @@ package io.element.android.features.userlist.api.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -29,18 +34,29 @@ import io.element.android.features.userlist.api.aListOfSelectedUsers import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.matrix.ui.model.MatrixUser -import kotlinx.collections.immutable.ImmutableList @Composable fun SelectedUsersList( - listState: LazyListState, - selectedUsers: ImmutableList, + selectedUsers: List, modifier: Modifier = Modifier, + autoScroll: Boolean = false, contentPadding: PaddingValues = PaddingValues(0.dp), onUserRemoved: (MatrixUser) -> Unit = {}, ) { + val lazyListState = rememberLazyListState() + if (autoScroll) { + var currentSize by rememberSaveable { mutableStateOf(selectedUsers.size) } + LaunchedEffect(selectedUsers.size) { + val isItemAdded = selectedUsers.size > currentSize + if (isItemAdded) { + lazyListState.animateScrollToItem(selectedUsers.lastIndex) + } + currentSize = selectedUsers.size + } + } + LazyRow( - state = listState, + state = lazyListState, modifier = modifier, contentPadding = contentPadding, horizontalArrangement = Arrangement.spacedBy(24.dp), @@ -65,7 +81,6 @@ internal fun SelectedUsersListDarkPreview() = ElementPreviewDark { ContentToPrev @Composable private fun ContentToPreview() { SelectedUsersList( - listState = LazyListState(), selectedUsers = aListOfSelectedUsers(), ) } diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/UserListView.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/UserListView.kt index 42b96ab3a2..6532dea2b6 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/UserListView.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/UserListView.kt @@ -46,7 +46,6 @@ fun UserListView( query = state.searchQuery, results = state.searchResults, selectedUsers = state.selectedUsers, - selectedUsersListState = state.selectedUsersListState, active = state.isSearchActive, isMultiSelectionEnabled = state.isMultiSelectionEnabled, onActiveChanged = { state.eventSink(UserListEvents.OnSearchActiveChanged(it)) }, @@ -63,9 +62,9 @@ fun UserListView( if (state.isMultiSelectionEnabled && !state.isSearchActive && state.selectedUsers.isNotEmpty()) { SelectedUsersList( - listState = state.selectedUsersListState, contentPadding = PaddingValues(16.dp), selectedUsers = state.selectedUsers, + autoScroll = true, onUserRemoved = { state.eventSink(UserListEvents.RemoveFromSelection(it)) onUserDeselected(it) diff --git a/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt index 8e1829f4a7..965bf40983 100644 --- a/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt +++ b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt @@ -16,8 +16,6 @@ package io.element.android.features.userlist.impl -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -25,7 +23,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import com.squareup.anvil.annotations.ContributesBinding @@ -45,8 +42,6 @@ import io.element.android.libraries.matrix.ui.model.MatrixUser import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.toImmutableList -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch class DefaultUserListPresenter @AssistedInject constructor( @Assisted val args: UserListPresenterArgs, @@ -66,10 +61,8 @@ class DefaultUserListPresenter @AssistedInject constructor( @Composable override fun present(): UserListState { - val localCoroutineScope = rememberCoroutineScope() var isSearchActive by rememberSaveable { mutableStateOf(false) } val selectedUsers = userListDataStore.selectedUsers().collectAsState(emptyList()) - val selectedUsersListState = rememberLazyListState() var searchQuery by rememberSaveable { mutableStateOf("") } val searchResults: MutableState> = remember { mutableStateOf(persistentListOf()) @@ -83,7 +76,6 @@ class DefaultUserListPresenter @AssistedInject constructor( if (event.matrixUser !in selectedUsers.value) { userListDataStore.selectUser(event.matrixUser) } - localCoroutineScope.scrollToFirstSelectedUser(selectedUsersListState) } is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser) } @@ -105,8 +97,7 @@ class DefaultUserListPresenter @AssistedInject constructor( return UserListState( searchQuery = searchQuery, searchResults = searchResults.value, - selectedUsers = selectedUsers.value.reversed().toImmutableList(), - selectedUsersListState = selectedUsersListState, + selectedUsers = selectedUsers.value.toImmutableList(), isSearchActive = isSearchActive, selectionMode = args.selectionMode, eventSink = ::handleEvents, @@ -123,8 +114,4 @@ class DefaultUserListPresenter @AssistedInject constructor( } return results.toImmutableList() } - - private fun CoroutineScope.scrollToFirstSelectedUser(listState: LazyListState) = launch { - listState.scrollToItem(index = 0) - } } From ea3c5618afff913fee58e80362e4c17b012fd14b Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 16:06:21 +0200 Subject: [PATCH 68/83] Fix unit tests --- .../impl/configureroom/ConfigureRoomPresenter.kt | 5 +---- .../impl/configureroom/ConfigureRoomView.kt | 5 ++++- .../impl/addpeople/AddPeoplePresenterTests.kt | 3 ++- .../impl/configureroom/ConfigureRoomPresenterTests.kt | 3 ++- .../impl/root/CreateRoomRootPresenterTests.kt | 3 ++- .../members/RoomMemberListPresenterTests.kt | 11 ++++++++--- .../userlist/test/FakeUserListPresenterFactory.kt | 7 ++++++- 7 files changed, 25 insertions(+), 12 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index b504edae96..ffee2e3fd9 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -16,13 +16,11 @@ package io.element.android.features.createroom.impl.configureroom -import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.platform.LocalContext import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.libraries.architecture.Presenter @@ -40,7 +38,6 @@ class ConfigureRoomPresenter @Inject constructor( mutableStateOf(enabled) } - val context = LocalContext.current fun handleEvents(event: ConfigureRoomEvents) { when (event) { is ConfigureRoomEvents.AvatarUriChanged -> dataStore.setAvatarUrl(event.uri?.toString()) @@ -48,7 +45,7 @@ class ConfigureRoomPresenter @Inject constructor( is ConfigureRoomEvents.TopicChanged -> dataStore.setTopic(event.topic) is ConfigureRoomEvents.RoomPrivacyChanged -> dataStore.setPrivacy(event.privacy) is ConfigureRoomEvents.RemoveFromSelection -> dataStore.selectedUserListDataStore.removeUserFromSelection(event.matrixUser) - ConfigureRoomEvents.CreateRoom -> Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() + ConfigureRoomEvents.CreateRoom -> Unit // TODO } } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 34d0332c0f..425143af7c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -83,7 +83,10 @@ fun ConfigureRoomView( ConfigureRoomToolbar( isNextActionEnabled = state.isCreateButtonEnabled, onBackPressed = onBackPressed, - onNextPressed = { state.eventSink(ConfigureRoomEvents.CreateRoom) }, + onNextPressed = { + // state.eventSink(ConfigureRoomEvents.CreateRoom) + Toast.makeText(context, "not implemented yet", Toast.LENGTH_SHORT).show() + }, ) } ) { padding -> diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt index 4b89eb7cbc..7ca2f0147f 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenterTests.kt @@ -23,6 +23,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.createroom.impl.CreateRoomDataStore +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.features.userlist.test.FakeUserListPresenterFactory import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -36,7 +37,7 @@ class AddPeoplePresenterTests { @Before fun setup() { - presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource(), CreateRoomDataStore()) + presenter = AddPeoplePresenter(FakeUserListPresenterFactory(), FakeUserListDataSource(), CreateRoomDataStore(UserListDataStore())) } @Test diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index c77416bacb..4828b7e636 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -24,6 +24,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.createroom.impl.CreateRoomDataStore +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_NAME @@ -41,7 +42,7 @@ class ConfigureRoomPresenterTests { @Before fun setup() { - presenter = ConfigureRoomPresenter(CreateRoomDataStore()) + presenter = ConfigureRoomPresenter(CreateRoomDataStore(UserListDataStore())) } @Test diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt index ea349f7128..4d26e68bcb 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/root/CreateRoomRootPresenterTests.kt @@ -22,6 +22,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.api.aUserListState import io.element.android.features.userlist.test.FakeUserListDataSource import io.element.android.features.userlist.test.FakeUserListPresenter @@ -51,7 +52,7 @@ class CreateRoomRootPresenterTests { fakeUserListPresenter = FakeUserListPresenter() fakeMatrixClient = FakeMatrixClient() userListDataSource = FakeUserListDataSource() - presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, fakeMatrixClient) + presenter = CreateRoomRootPresenter(FakeUserListPresenterFactory(fakeUserListPresenter), userListDataSource, UserListDataStore(), fakeMatrixClient) } @Test diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 853d2eb613..097eb46b30 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -23,6 +23,7 @@ import com.google.common.truth.Truth import io.element.android.features.roomdetails.impl.members.RoomMemberListPresenter import io.element.android.features.userlist.api.SelectionMode import io.element.android.features.userlist.api.UserListDataSource +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.features.userlist.impl.DefaultUserListPresenter @@ -41,10 +42,15 @@ class RoomMemberListPresenterTests { val userListDataSource = FakeUserListDataSource().apply { givenSearchResult(searchResult) } + val userListDataStore = UserListDataStore() val userListFactory = object : UserListPresenter.Factory { - override fun create(args: UserListPresenterArgs, dataSource: UserListDataSource) = DefaultUserListPresenter(args, dataSource) + override fun create( + args: UserListPresenterArgs, + userListDataSource: UserListDataSource, + userListDataStore: UserListDataStore, + ) = DefaultUserListPresenter(args, userListDataSource, userListDataStore) } - val presenter = RoomMemberListPresenter(userListFactory, userListDataSource) + val presenter = RoomMemberListPresenter(userListFactory, userListDataSource, userListDataStore) moleculeFlow(RecompositionClock.Immediate) { presenter.present() }.test { @@ -58,5 +64,4 @@ class RoomMemberListPresenterTests { Truth.assertThat((loadedState.allUsers as? Async.Success)?.state).isEqualTo(searchResult.toImmutableList()) } } - } diff --git a/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListPresenterFactory.kt b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListPresenterFactory.kt index 2255512490..00966a5082 100644 --- a/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListPresenterFactory.kt +++ b/features/userlist/test/src/main/kotlin/io/element/android/features/userlist/test/FakeUserListPresenterFactory.kt @@ -17,6 +17,7 @@ package io.element.android.features.userlist.test import io.element.android.features.userlist.api.UserListDataSource +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.api.UserListPresenter import io.element.android.features.userlist.api.UserListPresenterArgs @@ -24,5 +25,9 @@ class FakeUserListPresenterFactory( private val fakeUserListPresenter: FakeUserListPresenter = FakeUserListPresenter() ) : UserListPresenter.Factory { - override fun create(args: UserListPresenterArgs, userListDataSource: UserListDataSource): UserListPresenter = fakeUserListPresenter + override fun create( + args: UserListPresenterArgs, + userListDataSource: UserListDataSource, + userListDataStore: UserListDataStore, + ): UserListPresenter = fakeUserListPresenter } From e2e641c75165d8a1fe09f2c910b4aa7769bb51cb Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 16:53:38 +0200 Subject: [PATCH 69/83] Remove wildcard import --- .../createroom/impl/addpeople/AddPeoplePresenter.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt index 21ab9fafbd..6b2774a36e 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeoplePresenter.kt @@ -18,7 +18,11 @@ package io.element.android.features.createroom.impl.addpeople import androidx.compose.runtime.Composable import io.element.android.features.createroom.impl.CreateRoomDataStore -import io.element.android.features.userlist.api.* +import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.UserListDataSource +import io.element.android.features.userlist.api.UserListPresenter +import io.element.android.features.userlist.api.UserListPresenterArgs +import io.element.android.features.userlist.api.UserListState import io.element.android.libraries.architecture.Presenter import javax.inject.Inject import javax.inject.Named From 4337a95a39470a96060fb8d975a5b5472227af68 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 17:49:14 +0200 Subject: [PATCH 70/83] use derived state --- .../impl/configureroom/ConfigureRoomPresenter.kt | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt index c8bad29f93..572d5aa2fb 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenter.kt @@ -18,8 +18,10 @@ package io.element.android.features.createroom.impl.configureroom import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import dagger.assisted.Assisted @@ -43,9 +45,10 @@ class ConfigureRoomPresenter @AssistedInject constructor( var topic by rememberSaveable { mutableStateOf("") } var avatarUri by rememberSaveable { mutableStateOf(null) } var privacy by rememberSaveable { mutableStateOf(null) } - val isCreateButtonEnabled by rememberSaveable(roomName, privacy) { - val enabled = roomName.isNotEmpty() && privacy != null - mutableStateOf(enabled) + val isCreateButtonEnabled by remember { + derivedStateOf { + roomName.isNotEmpty() && privacy != null + } } fun handleEvents(event: ConfigureRoomEvents) { From 878b9ccf331edec448d352dae1e615926fa65459 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 17:56:18 +0200 Subject: [PATCH 71/83] Fix hardcoding privacy option --- .../createroom/impl/configureroom/ConfigureRoomView.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index 1ff41bc8af..e61dc1524c 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -261,12 +261,12 @@ fun RoomPrivacyOptions( Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( - privacy = RoomPrivacy.Private, + privacy = item.privacy, icon = item.icon, title = item.title, description = item.description, isSelected = selected == item.privacy, - onOptionSelected = { onOptionSelected(item.privacy) } + onOptionSelected = onOptionSelected, ) } } From 54b76078a8dc21a55b9f9a830a28dbd706780dc0 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 18:06:17 +0200 Subject: [PATCH 72/83] Pass item to RoomPrivacyOption --- .../impl/configureroom/ConfigureRoomView.kt | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index e61dc1524c..ee718b555a 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -111,7 +111,7 @@ fun ConfigureRoomView( RoomPrivacyOptions( modifier = Modifier.padding(bottom = 40.dp), selected = state.privacy, - onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it)) }, + onOptionSelected = { state.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(it.privacy)) }, ) } } @@ -228,20 +228,19 @@ fun RoomTopic( ) } +data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, +) + @Composable fun RoomPrivacyOptions( selected: RoomPrivacy?, modifier: Modifier = Modifier, - onOptionSelected: (RoomPrivacy) -> Unit = {}, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { - - data class RoomPrivacyItem( - val privacy: RoomPrivacy, - val icon: ImageVector, - val title: String, - val description: String, - ) - val items = RoomPrivacy.values().map { when (it) { RoomPrivacy.Public -> RoomPrivacyItem( @@ -261,10 +260,7 @@ fun RoomPrivacyOptions( Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( - privacy = item.privacy, - icon = item.icon, - title = item.title, - description = item.description, + roomPrivacyItem = item, isSelected = selected == item.privacy, onOptionSelected = onOptionSelected, ) @@ -274,27 +270,24 @@ fun RoomPrivacyOptions( @Composable fun RoomPrivacyOption( - privacy: RoomPrivacy, - icon: ImageVector, - title: String, - description: String, + roomPrivacyItem: RoomPrivacyItem, modifier: Modifier = Modifier, isSelected: Boolean = false, - onOptionSelected: (RoomPrivacy) -> Unit = {}, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { Row( modifier .fillMaxWidth() .selectable( selected = isSelected, - onClick = { onOptionSelected(privacy) }, + onClick = { onOptionSelected(roomPrivacyItem) }, role = Role.RadioButton, ) .padding(8.dp), ) { Icon( modifier = Modifier.padding(horizontal = 8.dp), - imageVector = icon, + imageVector = roomPrivacyItem.icon, contentDescription = "", tint = MaterialTheme.colorScheme.secondary, ) @@ -305,13 +298,13 @@ fun RoomPrivacyOption( .padding(horizontal = 8.dp) ) { Text( - text = title, + text = roomPrivacyItem.title, fontSize = 16.sp, color = MaterialTheme.colorScheme.primary, ) Spacer(Modifier.size(3.dp)) Text( - text = description, + text = roomPrivacyItem.description, fontSize = 12.sp, lineHeight = 17.sp, color = MaterialTheme.colorScheme.tertiary, From 97b5fa7ea8bd78882292215c59f262c5ac183fbe Mon Sep 17 00:00:00 2001 From: Jorge Martin Espinosa Date: Thu, 13 Apr 2023 18:41:57 +0200 Subject: [PATCH 73/83] [Room Details] Implement member details screen (#302) --- changelog.d/300.feature | 1 + .../roomdetails/impl/RoomDetailsFlowNode.kt | 18 +- .../roomdetails/impl/RoomDetailsView.kt | 7 +- ...mberModule.kt => RoomMemberBindsModule.kt} | 22 ++- .../impl/members/RoomMemberListNode.kt | 18 +- .../details/RoomMemberDetailsEvents.kt | 22 +++ .../members/details/RoomMemberDetailsNode.kt | 80 ++++++++ .../details/RoomMemberDetailsPresenter.kt | 51 +++++ .../members/details/RoomMemberDetailsState.kt | 25 +++ .../details/RoomMemberDetailsStateProvider.kt | 37 ++++ .../members/details/RoomMemberDetailsView.kt | 177 ++++++++++++++++++ .../roomdetails/RoomDetailsPresenterTests.kt | 4 +- .../members/RoomMemberListPresenterTests.kt | 2 + .../RoomMemberDetailsPresenterTests.kt | 48 +++++ .../libraries/matrix/api/core/RoomId.kt | 6 +- .../libraries/matrix/api/core/UserId.kt | 6 +- .../matrix/api/permalink/PermalinkBuilder.kt | 31 ++- .../libraries/matrix/api/room/MatrixRoom.kt | 3 + .../libraries/matrix/api/room/RoomMember.kt | 12 +- .../matrix/impl/room/RoomMemberMapper.kt | 1 + .../matrix/impl/room/RustMatrixRoom.kt | 5 + .../matrix/test/room/FakeMatrixRoom.kt | 5 + .../src/main/res/values/localazy.xml | 45 +++++ ...ewDarkPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...ewDarkPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_0,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_1,NEXUS_5,1.0,en].png | 3 + ...wLightPreview_0_null_2,NEXUS_5,1.0,en].png | 3 + 29 files changed, 629 insertions(+), 15 deletions(-) create mode 100644 changelog.d/300.feature rename features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/{RoomMemberModule.kt => RoomMemberBindsModule.kt} (61%) create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt create mode 100644 features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt create mode 100644 features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png diff --git a/changelog.d/300.feature b/changelog.d/300.feature new file mode 100644 index 0000000000..fba672e002 --- /dev/null +++ b/changelog.d/300.feature @@ -0,0 +1 @@ +Implement room member details screen diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt index 4a46f3370c..5778eae96e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsFlowNode.kt @@ -29,10 +29,13 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode import io.element.android.features.roomdetails.impl.members.RoomMemberListNode +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsNode import io.element.android.libraries.architecture.BackstackNode import io.element.android.libraries.architecture.animation.rememberDefaultTransitionHandler import io.element.android.libraries.architecture.createNode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.room.RoomMember import kotlinx.parcelize.Parcelize @ContributesNode(RoomScope::class) @@ -54,6 +57,9 @@ class RoomDetailsFlowNode @AssistedInject constructor( @Parcelize object RoomMemberList : NavTarget + + @Parcelize + data class RoomMemberDetails(val roomMember: RoomMember) : NavTarget } interface Callback : Plugin { @@ -69,7 +75,17 @@ class RoomDetailsFlowNode @AssistedInject constructor( override fun resolve(navTarget: NavTarget, buildContext: BuildContext): Node { return when (navTarget) { NavTarget.RoomDetails -> createNode(buildContext, listOf(callback)) - NavTarget.RoomMemberList -> createNode(buildContext) + NavTarget.RoomMemberList -> { + val callback = object : RoomMemberListNode.Callback { + override fun openRoomMemberDetails(roomMember: RoomMember) { + backstack.push(NavTarget.RoomMemberDetails(roomMember)) + } + } + createNode(buildContext, listOf(callback)) + } + is NavTarget.RoomMemberDetails -> { + createNode(buildContext, listOf(RoomMemberDetailsNode.Inputs(navTarget.roomMember))) + } } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt index d5b2a4a9e4..a4e7c8f77e 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/RoomDetailsView.kt @@ -87,7 +87,7 @@ fun RoomDetailsView( roomAlias = state.roomAlias ) - ShareSection(onShareRoom = onShareRoom) + ShareSection(onShareUser = onShareRoom) if (state.roomTopic != null) { TopicSection(roomTopic = state.roomTopic) @@ -127,12 +127,12 @@ fun RoomDetailsView( } @Composable -internal fun ShareSection(onShareRoom: () -> Unit, modifier: Modifier = Modifier) { +internal fun ShareSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) { PreferenceCategory(modifier = modifier) { PreferenceText( title = stringResource(R.string.screen_room_details_share_room_title), icon = Icons.Outlined.Share, - onClick = onShareRoom, + onClick = onShareUser, ) } } @@ -172,7 +172,6 @@ internal fun TopicSection(roomTopic: String, modifier: Modifier = Modifier) { color = MaterialTheme.colorScheme.tertiary ) } - } @Composable diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindsModule.kt similarity index 61% rename from features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt rename to features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindsModule.kt index 49c98374f2..e539e9070a 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberModule.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/di/RoomMemberBindsModule.kt @@ -19,17 +19,35 @@ package io.element.android.features.roomdetails.impl.di import com.squareup.anvil.annotations.ContributesTo import dagger.Binds import dagger.Module +import dagger.Provides import io.element.android.features.roomdetails.impl.members.RoomMatrixUserDataSource +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter import io.element.android.features.userlist.api.MatrixUserDataSource import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember import javax.inject.Named @Module @ContributesTo(RoomScope::class) -interface RoomMemberModule { +interface RoomMemberBindsModule { @Binds @Named("RoomMembers") fun bindRoomMemberUserListDataSource(dataSource: RoomMatrixUserDataSource): MatrixUserDataSource - +} + +@Module +@ContributesTo(RoomScope::class) +object RoomMemberProvidesModule { + @Provides + fun provideRoomMemberDetailsPresenterFactory( + room: MatrixRoom, + ): RoomMemberDetailsPresenter.Factory { + return object : RoomMemberDetailsPresenter.Factory { + override fun create(roomMember: RoomMember): RoomMemberDetailsPresenter { + return RoomMemberDetailsPresenter(room, roomMember) + } + } + } } diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt index 0f65b4657b..59f13115f0 100644 --- a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/RoomMemberListNode.kt @@ -21,10 +21,14 @@ import androidx.compose.ui.Modifier import com.bumble.appyx.core.modality.BuildContext import com.bumble.appyx.core.node.Node import com.bumble.appyx.core.plugin.Plugin +import com.bumble.appyx.core.plugin.plugins import dagger.assisted.Assisted import dagger.assisted.AssistedInject import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.impl.RoomDetailsFlowNode import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.ui.model.MatrixUser import timber.log.Timber @@ -32,11 +36,23 @@ import timber.log.Timber class RoomMemberListNode @AssistedInject constructor( @Assisted buildContext: BuildContext, @Assisted plugins: List, + private val room: MatrixRoom, private val presenter: RoomMemberListPresenter, ) : Node(buildContext, plugins = plugins) { + interface Callback : Plugin { + fun openRoomMemberDetails(roomMember: RoomMember) + } + + private val callback = plugins().first() + private fun onUserSelected(matrixUser: MatrixUser) { - Timber.d("TODO: implement user selection. User: $matrixUser") + val member = room.getMember(matrixUser.id) + if (member != null) { + callback.openRoomMemberDetails(member) + } else { + Timber.e("Could find room member ${matrixUser.id} in room ${room.roomId}") + } } @Composable diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt new file mode 100644 index 0000000000..2ce6688925 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsEvents.kt @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members.details + +// TODO Add your events or remove the file completely if no events +sealed interface RoomMemberDetailsEvents { + object MyEvent : RoomMemberDetailsEvents +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt new file mode 100644 index 0000000000..fe7f816a5a --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsNode.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members.details + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.bumble.appyx.core.modality.BuildContext +import com.bumble.appyx.core.node.Node +import com.bumble.appyx.core.plugin.Plugin +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.anvilannotations.ContributesNode +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.androidutils.system.startSharePlainTextIntent +import io.element.android.libraries.architecture.NodeInputs +import io.element.android.libraries.architecture.inputs +import io.element.android.libraries.di.RoomScope +import io.element.android.libraries.matrix.api.core.UserId +import io.element.android.libraries.matrix.api.permalink.PermalinkBuilder +import io.element.android.libraries.matrix.api.room.RoomMember +import timber.log.Timber +import io.element.android.libraries.androidutils.R as AndroidUtilsR + +@ContributesNode(RoomScope::class) +class RoomMemberDetailsNode @AssistedInject constructor( + @Assisted buildContext: BuildContext, + @Assisted plugins: List, + presenterFactory: RoomMemberDetailsPresenter.Factory, +) : Node(buildContext, plugins = plugins) { + + data class Inputs( + val member: RoomMember, + ) : NodeInputs + + private val inputs = inputs() + private val presenter = presenterFactory.create(inputs.member) + + private fun onShareUser(context: Context) { + val permalinkResult = PermalinkBuilder.permalinkForUser(UserId(inputs.member.userId)) + permalinkResult.onSuccess { permalink -> + startSharePlainTextIntent( + context = context, + activityResultLauncher = null, + chooserTitle = context.getString(R.string.screen_room_details_share_room_title), + text = permalink, + noActivityFoundMessage = context.getString(AndroidUtilsR.string.error_no_compatible_app_found) + ) + }.onFailure { + Timber.e(it) + } + } + + @Composable + override fun View(modifier: Modifier) { + val context = LocalContext.current + val state = presenter.present() + RoomMemberDetailsView( + state = state, + modifier = modifier, + goBack = { navigateUp() }, + onShareUser = { onShareUser(context) } + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt new file mode 100644 index 0000000000..0e997c4499 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsPresenter.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members.details + +import androidx.compose.runtime.Composable +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import io.element.android.libraries.architecture.Presenter +import io.element.android.libraries.matrix.api.room.MatrixRoom +import io.element.android.libraries.matrix.api.room.RoomMember + +class RoomMemberDetailsPresenter @AssistedInject constructor( + private val room: MatrixRoom, + @Assisted private val roomMember: RoomMember, +) : Presenter { + + interface Factory { + fun create(roomMember: RoomMember): RoomMemberDetailsPresenter + } + + @Composable + override fun present(): RoomMemberDetailsState { + +// fun handleEvents(event: RoomMemberDetailsEvents) { +// when (event) { +// } +// } + + return RoomMemberDetailsState( + userId = roomMember.userId, + userName = roomMember.displayName, + avatarUrl = roomMember.avatarUrl, + isBlocked = roomMember.isIgnored, +// eventSink = ::handleEvents + ) + } +} diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt new file mode 100644 index 0000000000..d9e3f949e7 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsState.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members.details + +data class RoomMemberDetailsState( + val userId: String, + val userName: String?, + val avatarUrl: String?, + val isBlocked: Boolean, +// val eventSink: (RoomMemberDetailsEvents) -> Unit +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt new file mode 100644 index 0000000000..c719ab7a26 --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsStateProvider.kt @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members.details + +import androidx.compose.ui.tooling.preview.PreviewParameterProvider + +open class RoomMemberDetailsStateProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + aRoomMemberDetailsState(), + aRoomMemberDetailsState().copy(userName = null), + aRoomMemberDetailsState().copy(isBlocked = true), + // Add other states here + ) +} + +fun aRoomMemberDetailsState() = RoomMemberDetailsState( + userId = "@daniel:domain.com", + userName = "Daniel", + avatarUrl = null, + isBlocked = false, +// eventSink = {}, +) diff --git a/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt new file mode 100644 index 0000000000..d7a97e5fdc --- /dev/null +++ b/features/roomdetails/impl/src/main/kotlin/io/element/android/features/roomdetails/impl/members/details/RoomMemberDetailsView.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.impl.members.details + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Block +import androidx.compose.material.icons.outlined.ChatBubbleOutline +import androidx.compose.material.icons.outlined.Share +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.unit.dp +import io.element.android.features.roomdetails.impl.R +import io.element.android.libraries.designsystem.ElementTextStyles +import io.element.android.libraries.designsystem.components.avatar.Avatar +import io.element.android.libraries.designsystem.components.avatar.AvatarData +import io.element.android.libraries.designsystem.components.avatar.AvatarSize +import io.element.android.libraries.designsystem.components.button.BackButton +import io.element.android.libraries.designsystem.components.preferences.PreferenceCategory +import io.element.android.libraries.designsystem.components.preferences.PreferenceText +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.LocalColors +import io.element.android.libraries.designsystem.theme.components.Scaffold +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TopAppBar +import io.element.android.libraries.ui.strings.R as StringR + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun RoomMemberDetailsView( + state: RoomMemberDetailsState, + onShareUser: () -> Unit, + goBack: () -> Unit, + modifier: Modifier = Modifier, +) { + Scaffold( + modifier = modifier, + topBar = { + TopAppBar(title = { }, navigationIcon = { BackButton(onClick = goBack) }) + }, + ) { padding -> + Column( + modifier = Modifier + .padding(padding) + .verticalScroll(rememberScrollState()) + ) { + HeaderSection( + avatarUrl = state.avatarUrl, + userId = state.userId, + userName = state.userName, + ) + + ShareSection(onShareUser = onShareUser) + + SendMessageSection(onSendMessage = { + // TODO implement send DM + }) + + BlockSection(isBlocked = state.isBlocked, onToggleBlock = { + // TODO implement block & unblock + }) + } + } +} + +@Composable +internal fun HeaderSection( + avatarUrl: String?, + userId: String, + userName: String?, + modifier: Modifier = Modifier +) { + Column(modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { + Box(modifier = Modifier.size(70.dp)) { + Avatar( + avatarData = AvatarData(userId, userName, avatarUrl, AvatarSize.HUGE), + modifier = Modifier.fillMaxSize() + ) + } + Spacer(modifier = Modifier.height(30.dp)) + if (userName != null) { + Text(userName, style = ElementTextStyles.Bold.title1) + Spacer(modifier = Modifier.height(8.dp)) + } + Text(userId, style = ElementTextStyles.Regular.body, color = MaterialTheme.colorScheme.secondary) + Spacer(Modifier.height(32.dp)) + } +} + +@Composable +internal fun ShareSection(onShareUser: () -> Unit, modifier: Modifier = Modifier) { + PreferenceCategory(modifier = modifier) { + PreferenceText( + title = stringResource(StringR.string.action_share), + icon = Icons.Outlined.Share, + onClick = onShareUser, + ) + } +} + +@Composable +internal fun SendMessageSection(onSendMessage: () -> Unit, modifier: Modifier = Modifier) { + PreferenceCategory(modifier = modifier) { + PreferenceText( + title = stringResource(StringR.string.action_send_message), + icon = Icons.Outlined.ChatBubbleOutline, + onClick = onSendMessage, + ) + } +} + +@Composable +internal fun BlockSection(isBlocked: Boolean, onToggleBlock: () -> Unit, modifier: Modifier = Modifier) { + PreferenceCategory(showDivider = false, modifier = modifier) { + if (isBlocked) { + PreferenceText( + title = stringResource(R.string.screen_dm_details_unblock_user), + icon = Icons.Outlined.Block, + ) + } else { + PreferenceText( + title = stringResource(R.string.screen_dm_details_block_user), + icon = Icons.Outlined.Block, + tintColor = LocalColors.current.textActionCritical, + ) + } + } +} + +@Preview +@Composable +fun RoomMemberDetailsViewLightPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = + ElementPreviewLight { ContentToPreview(state) } + +@Preview +@Composable +fun RoomMemberDetailsViewDarkPreview(@PreviewParameter(RoomMemberDetailsStateProvider::class) state: RoomMemberDetailsState) = + ElementPreviewDark { ContentToPreview(state) } + +@Composable +private fun ContentToPreview(state: RoomMemberDetailsState) { + RoomMemberDetailsView( + state = state, + onShareUser = {}, + goBack = {}, + ) +} diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt index 41404b7354..6292f877d7 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/RoomDetailsPresenterTests.kt @@ -233,7 +233,8 @@ fun aRoomMember( membership: RoomMembershipState = RoomMembershipState.JOIN, isNameAmbiguous: Boolean = false, powerLevel: Long = 0L, - normalizedPowerLevel: Long = 0L + normalizedPowerLevel: Long = 0L, + isIgnored: Boolean = false, ) = RoomMember( userId = userId.value, displayName = displayName, @@ -242,4 +243,5 @@ fun aRoomMember( isNameAmbiguous = isNameAmbiguous, powerLevel = powerLevel, normalizedPowerLevel = normalizedPowerLevel, + isIgnored = isIgnored, ) diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt index 3564daa5f1..1263ecff5d 100644 --- a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/RoomMemberListPresenterTests.kt @@ -29,10 +29,12 @@ import io.element.android.features.userlist.impl.DefaultUserListPresenter import io.element.android.features.userlist.test.FakeMatrixUserDataSource import io.element.android.libraries.architecture.Async import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import okhttp3.internal.toImmutableList import org.junit.Test +@ExperimentalCoroutinesApi class RoomMemberListPresenterTests { @Test diff --git a/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt new file mode 100644 index 0000000000..de4904fb7f --- /dev/null +++ b/features/roomdetails/impl/src/test/kotlin/io/element/android/features/roomdetails/members/details/RoomMemberDetailsPresenterTests.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.roomdetails.members.details + +import app.cash.molecule.RecompositionClock +import app.cash.molecule.moleculeFlow +import app.cash.turbine.test +import com.google.common.truth.Truth +import io.element.android.features.roomdetails.aMatrixRoom +import io.element.android.features.roomdetails.aRoomMember +import io.element.android.features.roomdetails.impl.members.details.RoomMemberDetailsPresenter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@ExperimentalCoroutinesApi +class RoomMemberDetailsPresenterTests { + + @Test + fun `present - returns the room member's data`() = runTest { + val room = aMatrixRoom() + val roomMember = aRoomMember(displayName = "Alice") + val presenter = RoomMemberDetailsPresenter(room, roomMember) + moleculeFlow(RecompositionClock.Immediate) { + presenter.present() + }.test { + val initialState = awaitItem() + Truth.assertThat(initialState.userId).isEqualTo(roomMember.userId) + Truth.assertThat(initialState.userName).isEqualTo(roomMember.displayName) + Truth.assertThat(initialState.avatarUrl).isEqualTo(roomMember.avatarUrl) + Truth.assertThat(initialState.isBlocked).isEqualTo(roomMember.isIgnored) + } + } +} diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt index e31b8063df..9ff8c1d76d 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/RoomId.kt @@ -20,7 +20,11 @@ import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline -value class RoomId(val value: String) : Serializable +value class RoomId(val value: String) : Serializable { + override fun toString(): String { + return value + } +} fun String.asRoomId() = if (BuildConfig.DEBUG && !MatrixPatterns.isRoomId(this)) { error("`$this` is not a valid room Id") diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt index 46adcdd59c..93810f8815 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/core/UserId.kt @@ -20,7 +20,11 @@ import io.element.android.libraries.matrix.api.BuildConfig import java.io.Serializable @JvmInline -value class UserId(val value: String) : Serializable +value class UserId(val value: String) : Serializable { + override fun toString(): String { + return value + } +} fun String.asUserId() = if (BuildConfig.DEBUG && !MatrixPatterns.isUserId(this)) { error("`$this` is not a valid user Id") diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt index 92e2bcef1d..2d8456be38 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/permalink/PermalinkBuilder.kt @@ -19,8 +19,14 @@ package io.element.android.libraries.matrix.api.permalink import io.element.android.libraries.matrix.api.config.MatrixConfiguration import io.element.android.libraries.matrix.api.core.MatrixPatterns import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId object PermalinkBuilder { + + private const val ROOM_PATH = "room/" + private const val USER_PATH = "user/" + private const val GROUP_PATH = "group/" + private val permalinkBaseUrl get() = (MatrixConfiguration.clientPermalinkBaseUrl ?: MatrixConfiguration.matrixToPermalinkBaseUrl).also { var baseUrl = it if (!baseUrl.endsWith("/")) { @@ -31,6 +37,21 @@ object PermalinkBuilder { } } + fun permalinkForUser(userId: UserId): Result { + return if (MatrixPatterns.isUserId(userId.value)) { + val url = buildString { + append(permalinkBaseUrl) + if (!isMatrixTo()) { + append(USER_PATH) + } + append(userId.value) + } + Result.success(url) + } else { + Result.failure(PermalinkBuilderError.InvalidRoomAlias) + } + } + fun permalinkForRoomAlias(roomAlias: String): Result { return if (MatrixPatterns.isRoomAlias(roomAlias)) { Result.success(permalinkForRoomAliasOrId(roomAlias)) @@ -49,10 +70,18 @@ object PermalinkBuilder { private fun permalinkForRoomAliasOrId(value: String): String { val id = escapeId(value) - return permalinkBaseUrl + id + return buildString { + append(permalinkBaseUrl) + if (!isMatrixTo()) { + append(ROOM_PATH) + } + append(id) + } } private fun escapeId(value: String) = value.replace("/", "%2F") + + private fun isMatrixTo(): Boolean = permalinkBaseUrl.startsWith(MatrixConfiguration.matrixToPermalinkBaseUrl) } sealed class PermalinkBuilderError : Throwable() { diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt index 60ff09a15b..9a97fcc9cf 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.api.room import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.timeline.MatrixTimeline import kotlinx.coroutines.flow.Flow import java.io.Closeable @@ -38,6 +39,8 @@ interface MatrixRoom: Closeable { suspend fun memberCount(): Int + fun getMember(userId: UserId): RoomMember? + fun syncUpdateFlow(): Flow fun timeline(): MatrixTimeline diff --git a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt index 66de9bd627..fcb83553c5 100644 --- a/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt +++ b/libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/RoomMember.kt @@ -16,6 +16,10 @@ package io.element.android.libraries.matrix.api.room +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize data class RoomMember( val userId: String, val displayName: String?, @@ -23,9 +27,11 @@ data class RoomMember( val membership: RoomMembershipState, val isNameAmbiguous: Boolean, val powerLevel: Long, - val normalizedPowerLevel: Long -) + val normalizedPowerLevel: Long, + val isIgnored: Boolean, +) : Parcelable -enum class RoomMembershipState { +@Parcelize +enum class RoomMembershipState : Parcelable { BAN, INVITE, JOIN, KNOCK, LEAVE } diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomMemberMapper.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomMemberMapper.kt index ebc93780ee..1fe0de6080 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomMemberMapper.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RoomMemberMapper.kt @@ -32,6 +32,7 @@ object RoomMemberMapper { roomMember.isNameAmbiguous(), roomMember.powerLevel(), roomMember.normalizedPowerLevel(), + roomMember.isIgnored(), ) fun mapMembership(membershipState: RustMembershipState): RoomMembershipState = diff --git a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt index fd5a63cb3b..67cddeee00 100644 --- a/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt +++ b/libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt @@ -20,6 +20,7 @@ import io.element.android.libraries.core.coroutine.CoroutineDispatchers import io.element.android.libraries.core.data.tryOrNull import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.api.timeline.MatrixTimeline @@ -68,6 +69,10 @@ class RustMatrixRoom( return members().size } + override fun getMember(userId: UserId): RoomMember? { + return cachedMembers.firstOrNull { it.userId == userId.value } + } + override fun syncUpdateFlow(): Flow { return slidingSyncUpdateFlow .filter { diff --git a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt index 1c6d580a3c..f213da2358 100644 --- a/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt +++ b/libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt @@ -18,6 +18,7 @@ package io.element.android.libraries.matrix.test.room import io.element.android.libraries.matrix.api.core.EventId import io.element.android.libraries.matrix.api.core.RoomId +import io.element.android.libraries.matrix.api.core.UserId import io.element.android.libraries.matrix.api.room.MatrixRoom import io.element.android.libraries.matrix.api.room.RoomMember import io.element.android.libraries.matrix.test.A_ROOM_ID @@ -85,6 +86,10 @@ class FakeMatrixRoom( } } + override fun getMember(userId: UserId): RoomMember? { + return members.firstOrNull { it.userId == userId.value } + } + override suspend fun sendMessage(message: String): Result { delay(100) return Result.success(Unit) diff --git a/libraries/ui-strings/src/main/res/values/localazy.xml b/libraries/ui-strings/src/main/res/values/localazy.xml index de11a74eac..fcade4a62b 100644 --- a/libraries/ui-strings/src/main/res/values/localazy.xml +++ b/libraries/ui-strings/src/main/res/values/localazy.xml @@ -121,15 +121,60 @@ "Are you sure that you want to leave this room? This room is not public and you will not be able to rejoin without an invite." "Are you sure that you want to leave the room?" "%1$s Android" + "Call" + "Listening for events" + "Noisy notifications" + "Silent notifications" + "** Failed to send - please open room" + "Join" + "Reject" + "New Messages" + "Mark as read" + "Quick reply" + "Me" + "You are viewing the notification! Click me!" + "%1$s: %2$s" + "%1$s: %2$s %3$s" + "%1$s and %2$s" + "%1$s in %2$s" + "%1$s in %2$s and %3$s" "%1$d member" "%1$d members" + + "%1$s: %2$d message" + "%1$s: %2$d messages" + + + "%d notification" + "%d notifications" + + + "%d invitation" + "%d invitations" + + + "%d new message" + "%d new messages" + + + "%d unread notified message" + "%d unread notified messages" + + + "%d room" + "%d rooms" + "%1$d room change" "%1$d room changes" "Rageshake to report bug" + "Choose how to receive notifications" + "Background synchronization" + "Google Services" + "No valid Google Play Services found. Notifications may not work properly." "You seem to be shaking the phone in frustration. Would you like to open the bug report screen?" "This message will be reported to your homeserver’s administrator. They will not be able to read any encrypted messages." "Reason for reporting this content" diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..9af1f155a8 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6b4eaf1009581383dc6b95f1e27c3f4f94f63ef9e6b1875521ba5249f449ce9 +size 28705 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..75a3ccc4ee --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32b9585ac785804c27eba9a630d5634c79b08b276a11607ea80c83eacccb59a0 +size 26252 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..479406f955 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewDarkPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:312a9c09176df3772386c5bb6f1d6945ae2638101e790a991e6becaec2440b8a +size 29618 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..ea22da115e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_0,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf5bef51ccb02f801be411b46859f86d5ff0a571978763b589135eb12f1ec60e +size 28265 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..a11a39bb0e --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:625cad77761037dbf600a1ce7635257051d66138ffbde2663f94550a96d3007c +size 25872 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..b982a61f9c --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.roomdetails.impl.members.details_null_DefaultGroup_RoomMemberDetailsViewLightPreview_0_null_2,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6c210546e81ac92d6a3f2583dd3443e0314f2259aa455d1ecdf6da77048fd861 +size 28733 From d5e62dfbf17499a66f8b0f5d1557340f1231842d Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 23:02:56 +0200 Subject: [PATCH 74/83] Split ConfigureRoomView into multiple files --- .../createroom/impl/components/Avatar.kt | 97 ++++++++++ .../impl/components/LabelledTextField.kt | 84 +++++++++ .../impl/components/RoomPrivacyOption.kt | 116 ++++++++++++ .../impl/configureroom/ConfigureRoomView.kt | 170 +----------------- .../impl/configureroom/RoomPrivacyItem.kt | 56 ++++++ ...atarDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...tarLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ieldDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...eldLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...tionDarkPreview_0_null,NEXUS_5,1.0,en].png | 3 + ...ionLightPreview_0_null,NEXUS_5,1.0,en].png | 3 + 11 files changed, 377 insertions(+), 164 deletions(-) create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt create mode 100644 features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png create mode 100644 tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt new file mode 100644 index 0000000000..bbaf5c46e5 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/Avatar.kt @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.components + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.AddAPhoto +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.preview.debugPlaceholderBackground +import io.element.android.libraries.designsystem.theme.LocalColors +import io.element.android.libraries.designsystem.theme.components.Icon + +@Composable +fun Avatar( + avatarUri: Uri?, + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + val commonModifier = modifier + .size(70.dp) + .clip(CircleShape) + .clickable(onClick = onClick) + + if (avatarUri != null) { + val context = LocalContext.current + val model = ImageRequest.Builder(context) + .data(avatarUri) + .build() + AsyncImage( + modifier = commonModifier, + model = model, + placeholder = debugPlaceholderBackground(ColorPainter(MaterialTheme.colorScheme.surfaceVariant)), + contentScale = ContentScale.Crop, + contentDescription = null, + ) + } else { + Box(modifier = commonModifier.background(LocalColors.current.quinary)) { + Icon( + imageVector = Icons.Outlined.AddAPhoto, + contentDescription = "", + modifier = Modifier + .align(Alignment.Center) + .size(40.dp), + tint = MaterialTheme.colorScheme.secondary, + ) + } + } +} + +@Preview +@Composable +fun AvatarLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun AvatarDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Row { + Avatar(null) + Avatar(Uri.EMPTY) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt new file mode 100644 index 0000000000..382e4b8de2 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/LabelledTextField.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.element.android.features.createroom.impl.R +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Text +import io.element.android.libraries.designsystem.theme.components.TextField + +@Composable +fun LabelledTextField( + label: String, + value: String, + modifier: Modifier = Modifier, + placeholder: String = "", + maxLines: Int = 1, + onValueChange: (String) -> Unit = {}, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = label + ) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = value, + placeholder = { Text(placeholder) }, + onValueChange = onValueChange, + maxLines = maxLines, + ) + } +} + +@Preview +@Composable +fun LabelledTextFieldLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun LabelledTextFieldDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + Column { + LabelledTextField( + label = stringResource(R.string.screen_create_room_room_name_label), + value = "", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), + ) + LabelledTextField( + label = stringResource(R.string.screen_create_room_room_name_label), + value = "a room name", + placeholder = stringResource(R.string.screen_create_room_room_name_placeholder), + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt new file mode 100644 index 0000000000..8da6d43fcc --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/components/RoomPrivacyOption.kt @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.selection.selectable +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import io.element.android.features.createroom.impl.configureroom.RoomPrivacyItem +import io.element.android.features.createroom.impl.configureroom.roomPrivacyItems +import io.element.android.libraries.designsystem.preview.ElementPreviewDark +import io.element.android.libraries.designsystem.preview.ElementPreviewLight +import io.element.android.libraries.designsystem.theme.components.Icon +import io.element.android.libraries.designsystem.theme.components.RadioButton +import io.element.android.libraries.designsystem.theme.components.Text + +@Composable +fun RoomPrivacyOption( + roomPrivacyItem: RoomPrivacyItem, + modifier: Modifier = Modifier, + isSelected: Boolean = false, + onOptionSelected: (RoomPrivacyItem) -> Unit = {}, +) { + Row( + modifier + .fillMaxWidth() + .selectable( + selected = isSelected, + onClick = { onOptionSelected(roomPrivacyItem) }, + role = Role.RadioButton, + ) + .padding(8.dp), + ) { + Icon( + modifier = Modifier.padding(horizontal = 8.dp), + imageVector = roomPrivacyItem.icon, + contentDescription = "", + tint = MaterialTheme.colorScheme.secondary, + ) + + Column( + Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + Text( + text = roomPrivacyItem.title, + fontSize = 16.sp, + color = MaterialTheme.colorScheme.primary, + ) + Spacer(Modifier.size(3.dp)) + Text( + text = roomPrivacyItem.description, + fontSize = 12.sp, + lineHeight = 17.sp, + color = MaterialTheme.colorScheme.tertiary, + ) + } + + RadioButton( + modifier = Modifier + .align(Alignment.CenterVertically) + .size(48.dp), + selected = isSelected, + onClick = null // null recommended for accessibility with screenreaders + ) + } +} + +@Preview +@Composable +fun RoomPrivacyOptionLightPreview() = ElementPreviewLight { ContentToPreview() } + +@Preview +@Composable +fun RoomPrivacyOptionDarkPreview() = ElementPreviewDark { ContentToPreview() } + +@Composable +private fun ContentToPreview() { + val aRoomPrivacyItem = roomPrivacyItems().first() + Column { + RoomPrivacyOption( + roomPrivacyItem = aRoomPrivacyItem, + isSelected = true, + ) + RoomPrivacyOption( + roomPrivacyItem = aRoomPrivacyItem, + isSelected = false, + ) + } +} diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt index ee718b555a..ad0b9b0b79 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomView.kt @@ -14,61 +14,43 @@ * limitations under the License. */ +@file:OptIn(ExperimentalMaterial3Api::class) + package io.element.android.features.createroom.impl.configureroom import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AddAPhoto -import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material.icons.outlined.Public import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage -import coil.request.ImageRequest import io.element.android.features.createroom.impl.R +import io.element.android.features.createroom.impl.components.Avatar +import io.element.android.features.createroom.impl.components.LabelledTextField +import io.element.android.features.createroom.impl.components.RoomPrivacyOption import io.element.android.features.userlist.api.SelectedUsersList import io.element.android.libraries.designsystem.components.button.BackButton import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight -import io.element.android.libraries.designsystem.theme.LocalColors import io.element.android.libraries.designsystem.theme.components.CenterAlignedTopAppBar -import io.element.android.libraries.designsystem.theme.components.Icon -import io.element.android.libraries.designsystem.theme.components.RadioButton import io.element.android.libraries.designsystem.theme.components.Scaffold import io.element.android.libraries.designsystem.theme.components.Text import io.element.android.libraries.designsystem.theme.components.TextButton -import io.element.android.libraries.designsystem.theme.components.TextField import io.element.android.libraries.ui.strings.R as StringR -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfigureRoomView( state: ConfigureRoomState, @@ -117,7 +99,6 @@ fun ConfigureRoomView( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ConfigureRoomToolbar( isNextActionEnabled: Boolean, @@ -177,41 +158,6 @@ fun RoomNameWithAvatar( } } -@Composable -fun Avatar( - avatarUri: Uri?, - modifier: Modifier = Modifier, - onClick: () -> Unit = {}, -) { - val commonModifier = modifier - .size(70.dp) - .clip(CircleShape) - .clickable(onClick = onClick) - - if (avatarUri != null) { - val context = LocalContext.current - val model = ImageRequest.Builder(context) - .data(avatarUri) - .build() - AsyncImage( - modifier = commonModifier, - model = model, - contentDescription = null, - ) - } else { - Box(modifier = commonModifier.background(LocalColors.current.quinary)) { - Icon( - imageVector = Icons.Outlined.AddAPhoto, - contentDescription = "", - modifier = Modifier - .align(Alignment.Center) - .size(40.dp), - tint = MaterialTheme.colorScheme.secondary, - ) - } - } -} - @Composable fun RoomTopic( topic: String, @@ -228,35 +174,13 @@ fun RoomTopic( ) } -data class RoomPrivacyItem( - val privacy: RoomPrivacy, - val icon: ImageVector, - val title: String, - val description: String, -) - @Composable fun RoomPrivacyOptions( selected: RoomPrivacy?, modifier: Modifier = Modifier, onOptionSelected: (RoomPrivacyItem) -> Unit = {}, ) { - val items = RoomPrivacy.values().map { - when (it) { - RoomPrivacy.Public -> RoomPrivacyItem( - privacy = it, - icon = Icons.Outlined.Lock, - title = stringResource(R.string.screen_create_room_private_option_title), - description = stringResource(R.string.screen_create_room_private_option_description), - ) - RoomPrivacy.Private -> RoomPrivacyItem( - privacy = it, - icon = Icons.Outlined.Public, - title = stringResource(R.string.screen_create_room_public_option_title), - description = stringResource(R.string.screen_create_room_public_option_description), - ) - } - } + val items = roomPrivacyItems() Column(modifier = modifier.selectableGroup()) { items.forEach { item -> RoomPrivacyOption( @@ -268,88 +192,6 @@ fun RoomPrivacyOptions( } } -@Composable -fun RoomPrivacyOption( - roomPrivacyItem: RoomPrivacyItem, - modifier: Modifier = Modifier, - isSelected: Boolean = false, - onOptionSelected: (RoomPrivacyItem) -> Unit = {}, -) { - Row( - modifier - .fillMaxWidth() - .selectable( - selected = isSelected, - onClick = { onOptionSelected(roomPrivacyItem) }, - role = Role.RadioButton, - ) - .padding(8.dp), - ) { - Icon( - modifier = Modifier.padding(horizontal = 8.dp), - imageVector = roomPrivacyItem.icon, - contentDescription = "", - tint = MaterialTheme.colorScheme.secondary, - ) - - Column( - Modifier - .weight(1f) - .padding(horizontal = 8.dp) - ) { - Text( - text = roomPrivacyItem.title, - fontSize = 16.sp, - color = MaterialTheme.colorScheme.primary, - ) - Spacer(Modifier.size(3.dp)) - Text( - text = roomPrivacyItem.description, - fontSize = 12.sp, - lineHeight = 17.sp, - color = MaterialTheme.colorScheme.tertiary, - ) - } - - RadioButton( - modifier = Modifier - .align(Alignment.CenterVertically) - .size(48.dp), - selected = isSelected, - onClick = null // null recommended for accessibility with screenreaders - ) - } -} - -// Move this composable to design module if we want to reuse it in other screens -@Composable -fun LabelledTextField( - label: String, - value: String, - modifier: Modifier = Modifier, - placeholder: String = "", - maxLines: Int = 1, - onValueChange: (String) -> Unit, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = label - ) - - TextField( - modifier = Modifier.fillMaxWidth(), - value = value, - placeholder = { Text(placeholder) }, - onValueChange = onValueChange, - maxLines = maxLines, - ) - } -} - @Preview @Composable fun ConfigureRoomViewLightPreview(@PreviewParameter(ConfigureRoomStateProvider::class) state: ConfigureRoomState) = diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt new file mode 100644 index 0000000000..0d1f6011c9 --- /dev/null +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2023 New Vector Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package io.element.android.features.createroom.impl.configureroom + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Lock +import androidx.compose.material.icons.outlined.Public +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import io.element.android.features.createroom.impl.R +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +data class RoomPrivacyItem( + val privacy: RoomPrivacy, + val icon: ImageVector, + val title: String, + val description: String, +) + +@Composable +fun roomPrivacyItems(): ImmutableList { + return RoomPrivacy.values() + .map { + when (it) { + RoomPrivacy.Public -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Lock, + title = stringResource(R.string.screen_create_room_private_option_title), + description = stringResource(R.string.screen_create_room_private_option_description), + ) + RoomPrivacy.Private -> RoomPrivacyItem( + privacy = it, + icon = Icons.Outlined.Public, + title = stringResource(R.string.screen_create_room_public_option_title), + description = stringResource(R.string.screen_create_room_public_option_description), + ) + } + } + .toImmutableList() +} diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..e5f8881070 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:aaeb60c55803711952413d99191209f467b4f77e1b03ad46601111691c2fc7fe +size 38188 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..8197e52f75 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_AvatarLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4c8ccd79709924e19793977ebe5ab4f6ded7f20507d9534dc24f46513fdd7a69 +size 37844 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..6f85c2f397 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b1243c92ee9f3735e81c2ab77910a7323b692a72c3c81c4b2ea776c1f0e5c84 +size 16267 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..3229cb3561 --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_LabelledTextFieldLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7db235009f0e0a50eda1196f2cd24eb490af10ebe42e26cc73fdf8ea2fdb0bf8 +size 15906 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..582b3621ea --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionDarkPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5abeb4bb717e33b34ff0a61d657a01b4e7ad965b5d7f8e4252c7e956c4caada3 +size 38719 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png new file mode 100644 index 0000000000..81a08165fa --- /dev/null +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.components_null_DefaultGroup_RoomPrivacyOptionLightPreview_0_null,NEXUS_5,1.0,en].png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:74c34325693f03c620a64493ea9bead27d8db1719d849ef4e1f589940efc73c9 +size 34559 From c691e2e3d3a003c30637421ffe1b827aa2ad8188 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 23:30:33 +0200 Subject: [PATCH 75/83] Use immutableList --- .../features/userlist/api/components/SelectedUsersList.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt index 491fb7827f..6909345a51 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/components/SelectedUsersList.kt @@ -34,10 +34,11 @@ import io.element.android.features.userlist.api.aListOfSelectedUsers import io.element.android.libraries.designsystem.preview.ElementPreviewDark import io.element.android.libraries.designsystem.preview.ElementPreviewLight import io.element.android.libraries.matrix.ui.model.MatrixUser +import kotlinx.collections.immutable.ImmutableList @Composable fun SelectedUsersList( - selectedUsers: List, + selectedUsers: ImmutableList, modifier: Modifier = Modifier, autoScroll: Boolean = false, contentPadding: PaddingValues = PaddingValues(0.dp), From 5979421f48d43d9549f85bf685bfd4d38fb8cd38 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 23:32:27 +0200 Subject: [PATCH 76/83] rename state provider --- ...opleStateProvider.kt => AddPeopleUserListStateProvider.kt} | 2 +- .../features/createroom/impl/addpeople/AddPeopleView.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/{AddPeopleStateProvider.kt => AddPeopleUserListStateProvider.kt} (95%) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt similarity index 95% rename from features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt rename to features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt index 3c9073d0d0..4a4581539b 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleStateProvider.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleUserListStateProvider.kt @@ -24,7 +24,7 @@ import io.element.android.features.userlist.api.aUserListState import io.element.android.libraries.matrix.ui.components.aMatrixUserList import kotlinx.collections.immutable.toImmutableList -open class AddPeopleStateProvider : PreviewParameterProvider { +open class AddPeopleUserListStateProvider : PreviewParameterProvider { override val values: Sequence get() = sequenceOf( aUserListState(), diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt index e81a47a04a..ebf3e9d508 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/addpeople/AddPeopleView.kt @@ -108,12 +108,12 @@ fun AddPeopleViewTopBar( @Preview @Composable -internal fun AddPeopleViewLightPreview(@PreviewParameter(AddPeopleStateProvider::class) state: UserListState) = +internal fun AddPeopleViewLightPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreviewLight { ContentToPreview(state) } @Preview @Composable -internal fun AddPeopleViewDarkPreview(@PreviewParameter(AddPeopleStateProvider::class) state: UserListState) = +internal fun AddPeopleViewDarkPreview(@PreviewParameter(AddPeopleUserListStateProvider::class) state: UserListState) = ElementPreviewDark { ContentToPreview(state) } @Composable From a3bb817d1f6b212a181a5e8d1f5e1d12842fc10f Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Thu, 13 Apr 2023 23:33:54 +0200 Subject: [PATCH 77/83] Check if user is not already selected --- .../android/features/userlist/api/UserListDataStore.kt | 4 +++- .../features/userlist/impl/DefaultUserListPresenter.kt | 6 +----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataStore.kt b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataStore.kt index c1e982dd59..f9c73950f1 100644 --- a/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataStore.kt +++ b/features/userlist/api/src/main/kotlin/io/element/android/features/userlist/api/UserListDataStore.kt @@ -26,7 +26,9 @@ class UserListDataStore @Inject constructor() { private val selectedUsers: MutableStateFlow> = MutableStateFlow(emptyList()) fun selectUser(user: MatrixUser) { - selectedUsers.tryEmit(selectedUsers.value.plus(user)) + if (user !in selectedUsers.value) { + selectedUsers.tryEmit(selectedUsers.value.plus(user)) + } } fun removeUserFromSelection(user: MatrixUser) { diff --git a/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt index 965bf40983..06f4caed86 100644 --- a/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt +++ b/features/userlist/impl/src/main/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenter.kt @@ -72,11 +72,7 @@ class DefaultUserListPresenter @AssistedInject constructor( when (event) { is UserListEvents.OnSearchActiveChanged -> isSearchActive = event.active is UserListEvents.UpdateSearchQuery -> searchQuery = event.query - is UserListEvents.AddToSelection -> { - if (event.matrixUser !in selectedUsers.value) { - userListDataStore.selectUser(event.matrixUser) - } - } + is UserListEvents.AddToSelection -> userListDataStore.selectUser(event.matrixUser) is UserListEvents.RemoveFromSelection -> userListDataStore.removeUserFromSelection(event.matrixUser) } } From 490ed7ef9867736218060fe6600859779f15e804 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 14 Apr 2023 00:22:41 +0200 Subject: [PATCH 78/83] Fix unit test --- .../userlist/impl/DefaultUserListPresenterTests.kt | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt b/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt index 15c26fb8d3..29432c2ff1 100644 --- a/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt +++ b/features/userlist/impl/src/test/kotlin/io/element/android/features/userlist/impl/DefaultUserListPresenterTests.kt @@ -22,6 +22,7 @@ import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat import io.element.android.features.userlist.api.SelectionMode +import io.element.android.features.userlist.api.UserListDataStore import io.element.android.features.userlist.api.UserListEvents import io.element.android.features.userlist.api.UserListPresenterArgs import io.element.android.features.userlist.test.FakeUserListDataSource @@ -43,7 +44,8 @@ class DefaultUserListPresenterTests { fun `present - initial state for single selection`() = runTest { val presenter = DefaultUserListPresenter( UserListPresenterArgs(selectionMode = SelectionMode.Single), - userListDataSource + userListDataSource, + UserListDataStore(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -61,7 +63,8 @@ class DefaultUserListPresenterTests { fun `present - initial state for multiple selection`() = runTest { val presenter = DefaultUserListPresenter( UserListPresenterArgs(selectionMode = SelectionMode.Multiple), - userListDataSource + userListDataSource, + UserListDataStore(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -79,7 +82,8 @@ class DefaultUserListPresenterTests { fun `present - update search query`() = runTest { val presenter = DefaultUserListPresenter( UserListPresenterArgs(selectionMode = SelectionMode.Single), - userListDataSource + userListDataSource, + UserListDataStore(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() @@ -111,7 +115,8 @@ class DefaultUserListPresenterTests { val presenter = DefaultUserListPresenter( UserListPresenterArgs(selectionMode = SelectionMode.Single), - userListDataSource + userListDataSource, + UserListDataStore(), ) moleculeFlow(RecompositionClock.Immediate) { presenter.present() From b88c1f35a753e91d23d6dad3e40fcb64e15e2e9a Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 14 Apr 2023 09:17:44 +0200 Subject: [PATCH 79/83] update ConfigureRoomPresenter unit tests --- .../ConfigureRoomPresenterTests.kt | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 4828b7e636..47fe698d05 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -23,6 +23,7 @@ import app.cash.molecule.RecompositionClock import app.cash.molecule.moleculeFlow import app.cash.turbine.test import com.google.common.truth.Truth.assertThat +import io.element.android.features.createroom.impl.CreateRoomConfig import io.element.android.features.createroom.impl.CreateRoomDataStore import io.element.android.features.userlist.api.UserListDataStore import io.element.android.libraries.matrix.test.AN_AVATAR_URL @@ -51,8 +52,11 @@ class ConfigureRoomPresenterTests { presenter.present() }.test { val initialState = awaitItem() + assertThat(initialState.config).isEqualTo(CreateRoomConfig()) assertThat(initialState.config.roomName).isNull() assertThat(initialState.config.topic).isNull() + assertThat(initialState.config.invites).isEmpty() + assertThat(initialState.config.avatarUrl).isNull() assertThat(initialState.config.privacy).isNull() } } @@ -63,24 +67,28 @@ class ConfigureRoomPresenterTests { presenter.present() }.test { val initialState = awaitItem() + var config = initialState.config assertThat(initialState.isCreateButtonEnabled).isFalse() // Room name not empty initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) var newState: ConfigureRoomState = awaitItem() - assertThat(newState.config.roomName).isEqualTo(A_ROOM_NAME) + config = config.copy(roomName = A_ROOM_NAME) + assertThat(newState.config).isEqualTo(config) assertThat(newState.isCreateButtonEnabled).isFalse() // Select privacy - initialState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Private)) + newState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Private)) newState = awaitItem() - assertThat(newState.config.privacy).isEqualTo(RoomPrivacy.Private) + config = config.copy(privacy = RoomPrivacy.Private) + assertThat(newState.config).isEqualTo(config) assertThat(newState.isCreateButtonEnabled).isTrue() // Clear room name - initialState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) + newState.eventSink(ConfigureRoomEvents.RoomNameChanged("")) newState = awaitItem() - assertThat(newState.config.roomName).isNull() + config = config.copy(roomName = null) + assertThat(newState.config).isEqualTo(config) assertThat(newState.isCreateButtonEnabled).isFalse() } } @@ -91,26 +99,32 @@ class ConfigureRoomPresenterTests { presenter.present() }.test { val initialState = awaitItem() + var config = initialState.config + // Room name initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) - val stateAfterRoomNameChanged = awaitItem() - assertThat(stateAfterRoomNameChanged.config.roomName).isEqualTo(A_ROOM_NAME) + var newState = awaitItem() + config = config.copy(roomName = A_ROOM_NAME) + assertThat(newState.config).isEqualTo(config) // Room topic - stateAfterRoomNameChanged.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) - val stateAfterTopicChanged = awaitItem() - assertThat(stateAfterTopicChanged.config.topic).isEqualTo(A_MESSAGE) + newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) + newState = awaitItem() + config = config.copy(topic = A_MESSAGE) + assertThat(newState.config).isEqualTo(config) // Room avatar val anUri = Uri.parse(AN_AVATAR_URL) - stateAfterTopicChanged.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri)) - val stateAfterAvatarUriChanged = awaitItem() - assertThat(stateAfterAvatarUriChanged.config.avatarUrl).isEqualTo(anUri.toString()) + newState.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri)) + newState = awaitItem() + config = config.copy(avatarUrl = anUri.toString()) + assertThat(newState.config).isEqualTo(config) // Room privacy - stateAfterAvatarUriChanged.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public)) - val stateAfterPrivacyChanged = awaitItem() - assertThat(stateAfterPrivacyChanged.config.privacy).isEqualTo(RoomPrivacy.Public) + newState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public)) + newState = awaitItem() + config = config.copy(privacy = RoomPrivacy.Public) + assertThat(newState.config).isEqualTo(config) } } } From a53ac0d5d5bd282818ba5d5768c117ddc700dc24 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 09:09:13 +0000 Subject: [PATCH 80/83] Update dependency app.cash.molecule:molecule-runtime to v0.9.0 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9c736f4939..34fd4d36da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ android_gradle_plugin = "7.4.2" kotlin = "1.8.10" ksp = "1.8.10-1.0.9" -molecule = "0.8.0" +molecule = "0.9.0" # AndroidX material = "1.8.0" From d8fd19a32464d5ce0dd92b32c0fcaeb98d0bcefd Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 14 Apr 2023 10:37:20 +0200 Subject: [PATCH 81/83] Fix privacy item binding --- .../features/createroom/impl/configureroom/RoomPrivacy.kt | 2 +- .../features/createroom/impl/configureroom/RoomPrivacyItem.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt index e0b7411680..5cb0cf25b4 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacy.kt @@ -17,6 +17,6 @@ package io.element.android.features.createroom.impl.configureroom enum class RoomPrivacy { - Public, Private, + Public, } diff --git a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt index 0d1f6011c9..462dedba00 100644 --- a/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt +++ b/features/createroom/impl/src/main/kotlin/io/element/android/features/createroom/impl/configureroom/RoomPrivacyItem.kt @@ -38,13 +38,13 @@ fun roomPrivacyItems(): ImmutableList { return RoomPrivacy.values() .map { when (it) { - RoomPrivacy.Public -> RoomPrivacyItem( + RoomPrivacy.Private -> RoomPrivacyItem( privacy = it, icon = Icons.Outlined.Lock, title = stringResource(R.string.screen_create_room_private_option_title), description = stringResource(R.string.screen_create_room_private_option_description), ) - RoomPrivacy.Private -> RoomPrivacyItem( + RoomPrivacy.Public -> RoomPrivacyItem( privacy = it, icon = Icons.Outlined.Public, title = stringResource(R.string.screen_create_room_public_option_title), From 16aefb203919c8a0f360826696421bd1fadc65f1 Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 14 Apr 2023 14:29:12 +0200 Subject: [PATCH 82/83] update screenshots --- ..._ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- ...ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png index c273abf7fb..62e855dab1 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewDarkPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:32e702b0c3671c6ef9ae38acadccf8c3da48b7093ac81ede95f8ea6f9b28e405 -size 103375 +oid sha256:df7631ad85dc6b3fbdabfc820b11b4e12a6128202e3faa2d105ba22793fe8c22 +size 103428 diff --git a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png index 8693cd2e06..4045dae649 100644 --- a/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png +++ b/tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.createroom.impl.configureroom_null_DefaultGroup_ConfigureRoomViewLightPreview_0_null_1,NEXUS_5,1.0,en].png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ad0d51590e0c66e5711d1a1809a234328717548dc7fac837dc4bce4870caf70a -size 96969 +oid sha256:3c2731ea21ca5b001aadc29ecbb15a2b263b0c042b7344cb10149174ff88ec0a +size 96835 From d10e8b8f523f684d53e46236058cef3587fbb5ed Mon Sep 17 00:00:00 2001 From: Florian Renaud Date: Fri, 14 Apr 2023 14:54:38 +0200 Subject: [PATCH 83/83] Add missing test --- .../ConfigureRoomPresenterTests.kt | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt index 47fe698d05..1f27baa511 100644 --- a/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt +++ b/features/createroom/impl/src/test/kotlin/io/element/android/features/createroom/impl/configureroom/ConfigureRoomPresenterTests.kt @@ -29,6 +29,9 @@ import io.element.android.features.userlist.api.UserListDataStore import io.element.android.libraries.matrix.test.AN_AVATAR_URL import io.element.android.libraries.matrix.test.A_MESSAGE import io.element.android.libraries.matrix.test.A_ROOM_NAME +import io.element.android.libraries.matrix.ui.components.aMatrixUser +import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before @@ -40,10 +43,12 @@ import org.robolectric.RobolectricTestRunner class ConfigureRoomPresenterTests { private lateinit var presenter: ConfigureRoomPresenter + private lateinit var userListDataStore: UserListDataStore @Before fun setup() { - presenter = ConfigureRoomPresenter(CreateRoomDataStore(UserListDataStore())) + userListDataStore = UserListDataStore() + presenter = ConfigureRoomPresenter(CreateRoomDataStore(userListDataStore)) } @Test @@ -99,32 +104,49 @@ class ConfigureRoomPresenterTests { presenter.present() }.test { val initialState = awaitItem() - var config = initialState.config + var expectedConfig = CreateRoomConfig() + assertThat(initialState.config).isEqualTo(expectedConfig) + + // Select User + val selectedUser1 = aMatrixUser() + val selectedUser2 = aMatrixUser("@id_of_bob:server.org", "Bob") + userListDataStore.selectUser(selectedUser1) + skipItems(1) + userListDataStore.selectUser(selectedUser2) + var newState = awaitItem() + expectedConfig = expectedConfig.copy(invites = persistentListOf(selectedUser1, selectedUser2)) + assertThat(newState.config).isEqualTo(expectedConfig) // Room name initialState.eventSink(ConfigureRoomEvents.RoomNameChanged(A_ROOM_NAME)) - var newState = awaitItem() - config = config.copy(roomName = A_ROOM_NAME) - assertThat(newState.config).isEqualTo(config) + newState = awaitItem() + expectedConfig = expectedConfig.copy(roomName = A_ROOM_NAME) + assertThat(newState.config).isEqualTo(expectedConfig) // Room topic newState.eventSink(ConfigureRoomEvents.TopicChanged(A_MESSAGE)) newState = awaitItem() - config = config.copy(topic = A_MESSAGE) - assertThat(newState.config).isEqualTo(config) + expectedConfig = expectedConfig.copy(topic = A_MESSAGE) + assertThat(newState.config).isEqualTo(expectedConfig) // Room avatar val anUri = Uri.parse(AN_AVATAR_URL) newState.eventSink(ConfigureRoomEvents.AvatarUriChanged(anUri)) newState = awaitItem() - config = config.copy(avatarUrl = anUri.toString()) - assertThat(newState.config).isEqualTo(config) + expectedConfig = expectedConfig.copy(avatarUrl = anUri.toString()) + assertThat(newState.config).isEqualTo(expectedConfig) // Room privacy newState.eventSink(ConfigureRoomEvents.RoomPrivacyChanged(RoomPrivacy.Public)) newState = awaitItem() - config = config.copy(privacy = RoomPrivacy.Public) - assertThat(newState.config).isEqualTo(config) + expectedConfig = expectedConfig.copy(privacy = RoomPrivacy.Public) + assertThat(newState.config).isEqualTo(expectedConfig) + + // Remove user + newState.eventSink(ConfigureRoomEvents.RemoveFromSelection(selectedUser1)) + newState = awaitItem() + expectedConfig = expectedConfig.copy(invites = expectedConfig.invites.minus(selectedUser1).toImmutableList()) + assertThat(newState.config).isEqualTo(expectedConfig) } } }