From 782837195836b70a9ff7c8c6bf902aef788d0b05 Mon Sep 17 00:00:00 2001 From: Gerard Paligot Date: Tue, 31 Oct 2023 12:48:20 +0100 Subject: [PATCH 1/3] chore: upgrade Gradle, Kotlin, Java and AndroidX libs. --- build-logic/settings.gradle.kts | 1 - .../extensions/LibraryExtensionExt.kt | 15 +++++++-------- .../openfeedback/extensions/TaskContainerExt.kt | 2 +- .../openfeedback/plugins/ComposeLibraryPlugin.kt | 2 +- .../io/openfeedback/plugins/LibraryPlugin.kt | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- libs.versions.toml | 16 ++++++++-------- openfeedback-m2/build.gradle.kts | 4 ++++ openfeedback-m2/src/main/AndroidManifest.xml | 2 -- openfeedback-m3/build.gradle.kts | 4 ++++ openfeedback-m3/src/main/AndroidManifest.xml | 2 -- openfeedback-ui/build.gradle.kts | 4 ++++ openfeedback-ui/src/main/AndroidManifest.xml | 7 ------- openfeedback-viewmodel/build.gradle.kts | 4 ++++ .../src/main/AndroidManifest.xml | 2 -- openfeedback/build.gradle.kts | 4 ++++ openfeedback/src/main/AndroidManifest.xml | 7 ------- sample-app/build.gradle.kts | 1 + sample-app/src/main/AndroidManifest.xml | 3 +-- settings.gradle.kts | 2 -- 20 files changed, 41 insertions(+), 45 deletions(-) delete mode 100644 openfeedback-m2/src/main/AndroidManifest.xml delete mode 100644 openfeedback-m3/src/main/AndroidManifest.xml delete mode 100644 openfeedback-ui/src/main/AndroidManifest.xml delete mode 100644 openfeedback-viewmodel/src/main/AndroidManifest.xml delete mode 100644 openfeedback/src/main/AndroidManifest.xml diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index e277e5b..5a07434 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1,4 +1,3 @@ -enableFeaturePreview("VERSION_CATALOGS") enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { diff --git a/build-logic/src/main/kotlin/io/openfeedback/extensions/LibraryExtensionExt.kt b/build-logic/src/main/kotlin/io/openfeedback/extensions/LibraryExtensionExt.kt index e5ec058..bedda6a 100644 --- a/build-logic/src/main/kotlin/io/openfeedback/extensions/LibraryExtensionExt.kt +++ b/build-logic/src/main/kotlin/io/openfeedback/extensions/LibraryExtensionExt.kt @@ -3,7 +3,6 @@ package io.openfeedback.extensions import com.android.build.api.dsl.CommonExtension -import com.android.build.gradle.LibraryExtension import org.gradle.api.JavaVersion import org.gradle.api.Project import org.gradle.api.artifacts.VersionCatalogsExtension @@ -12,24 +11,24 @@ import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.getByType import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions -internal fun CommonExtension<*, *, *, *>.configureKotlinAndroid() { - compileSdk = 33 +internal fun CommonExtension<*, *, *, *, *>.configureKotlinAndroid() { + compileSdk = 34 defaultConfig { minSdk = 21 } compileOptions { - sourceCompatibility = JavaVersion.VERSION_11 - targetCompatibility = JavaVersion.VERSION_11 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } } -internal fun CommonExtension<*, *, *, *>.configureAndroidCompose(project: Project) { +internal fun CommonExtension<*, *, *, *, *>.configureAndroidCompose(project: Project) { val libs = project.extensions.getByType().named("libs") buildFeatures { @@ -46,6 +45,6 @@ internal fun CommonExtension<*, *, *, *>.configureAndroidCompose(project: Projec } } -private fun CommonExtension<*, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { +private fun CommonExtension<*, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { (this as ExtensionAware).extensions.configure("kotlinOptions", block) } diff --git a/build-logic/src/main/kotlin/io/openfeedback/extensions/TaskContainerExt.kt b/build-logic/src/main/kotlin/io/openfeedback/extensions/TaskContainerExt.kt index 9796003..5bf5fb3 100644 --- a/build-logic/src/main/kotlin/io/openfeedback/extensions/TaskContainerExt.kt +++ b/build-logic/src/main/kotlin/io/openfeedback/extensions/TaskContainerExt.kt @@ -7,6 +7,6 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile fun TaskContainer.configureKotlinCompiler() = withType { kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() } } diff --git a/build-logic/src/main/kotlin/io/openfeedback/plugins/ComposeLibraryPlugin.kt b/build-logic/src/main/kotlin/io/openfeedback/plugins/ComposeLibraryPlugin.kt index 7b2a7ab..fd673c3 100644 --- a/build-logic/src/main/kotlin/io/openfeedback/plugins/ComposeLibraryPlugin.kt +++ b/build-logic/src/main/kotlin/io/openfeedback/plugins/ComposeLibraryPlugin.kt @@ -22,7 +22,7 @@ class ComposeLibraryPlugin : Plugin { target.extensions.configure { configureKotlinAndroid() configureAndroidCompose(target) - defaultConfig.targetSdk = 33 + defaultConfig.targetSdk = 34 } target.tasks.configureKotlinCompiler() } diff --git a/build-logic/src/main/kotlin/io/openfeedback/plugins/LibraryPlugin.kt b/build-logic/src/main/kotlin/io/openfeedback/plugins/LibraryPlugin.kt index 950d1cc..a8a0e26 100644 --- a/build-logic/src/main/kotlin/io/openfeedback/plugins/LibraryPlugin.kt +++ b/build-logic/src/main/kotlin/io/openfeedback/plugins/LibraryPlugin.kt @@ -20,7 +20,7 @@ class LibraryPlugin : Plugin { target.extensions.create("openfeedback", OpenFeedback::class.java, target) target.extensions.configure { configureKotlinAndroid() - defaultConfig.targetSdk = 33 + defaultConfig.targetSdk = 34 } target.tasks.configureKotlinCompiler() } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 39e4717..a7b16b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/libs.versions.toml b/libs.versions.toml index 68d951b..89ecd65 100644 --- a/libs.versions.toml +++ b/libs.versions.toml @@ -1,16 +1,16 @@ [versions] -kotlin_lang = "1.7.20" -kotlin_coroutines = "1.6.4" -androidx_core = "1.7.0" -androidx_compose_bom = "2022.10.00" -androidx_compose_compiler = "1.3.2" -androidx_lifecycle = "2.4.1" -androidx_savedstate = "1.1.0" +kotlin_lang = "1.9.10" +kotlin_coroutines = "1.7.3" +androidx_core = "1.12.0" +androidx_compose_bom = "2023.10.01" +androidx_compose_compiler = "1.5.3" +androidx_lifecycle = "2.6.2" +androidx_savedstate = "1.2.1" firebase_firestore = "24.0.1" firebase_auth = "21.0.1" [libraries] -android-gradle-plugin = "com.android.tools.build:gradle:7.3.1" +android-gradle-plugin = "com.android.tools.build:gradle:8.1.2" vespene = "net.mbonnin.vespene:vespene-lib:0.5" kotlin_gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_lang" } diff --git a/openfeedback-m2/build.gradle.kts b/openfeedback-m2/build.gradle.kts index 86a5250..cffe39d 100644 --- a/openfeedback-m2/build.gradle.kts +++ b/openfeedback-m2/build.gradle.kts @@ -3,6 +3,10 @@ plugins { id("io.openfeedback.plugins.compose.lib") } +android { + namespace = "io.openfeedback.android.m2" +} + openfeedback { configurePublishing("feedback-android-sdk-m2") } diff --git a/openfeedback-m2/src/main/AndroidManifest.xml b/openfeedback-m2/src/main/AndroidManifest.xml deleted file mode 100644 index 2f05a1e..0000000 --- a/openfeedback-m2/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/openfeedback-m3/build.gradle.kts b/openfeedback-m3/build.gradle.kts index 8a9a2fd..cfadbc2 100644 --- a/openfeedback-m3/build.gradle.kts +++ b/openfeedback-m3/build.gradle.kts @@ -3,6 +3,10 @@ plugins { id("io.openfeedback.plugins.compose.lib") } +android { + namespace = "io.openfeedback.android.m3" +} + openfeedback { configurePublishing("feedback-android-sdk-m3") } diff --git a/openfeedback-m3/src/main/AndroidManifest.xml b/openfeedback-m3/src/main/AndroidManifest.xml deleted file mode 100644 index 3831c3b..0000000 --- a/openfeedback-m3/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/openfeedback-ui/build.gradle.kts b/openfeedback-ui/build.gradle.kts index 0b5177f..9bc802c 100644 --- a/openfeedback-ui/build.gradle.kts +++ b/openfeedback-ui/build.gradle.kts @@ -3,6 +3,10 @@ plugins { id("io.openfeedback.plugins.compose.lib") } +android { + namespace = "io.openfeedback.android.ui" +} + openfeedback { configurePublishing("feedback-android-sdk-ui") } diff --git a/openfeedback-ui/src/main/AndroidManifest.xml b/openfeedback-ui/src/main/AndroidManifest.xml deleted file mode 100644 index 76cc108..0000000 --- a/openfeedback-ui/src/main/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/openfeedback-viewmodel/build.gradle.kts b/openfeedback-viewmodel/build.gradle.kts index 5473c76..9eacfde 100644 --- a/openfeedback-viewmodel/build.gradle.kts +++ b/openfeedback-viewmodel/build.gradle.kts @@ -3,6 +3,10 @@ plugins { id("io.openfeedback.plugins.compose.lib") } +android { + namespace = "io.openfeedback.android.viewmodel" +} + openfeedback { configurePublishing("feedback-android-sdk-viewmodel") } diff --git a/openfeedback-viewmodel/src/main/AndroidManifest.xml b/openfeedback-viewmodel/src/main/AndroidManifest.xml deleted file mode 100644 index e8ccd57..0000000 --- a/openfeedback-viewmodel/src/main/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file diff --git a/openfeedback/build.gradle.kts b/openfeedback/build.gradle.kts index 3804dcb..2de0e5b 100644 --- a/openfeedback/build.gradle.kts +++ b/openfeedback/build.gradle.kts @@ -3,6 +3,10 @@ plugins { id("io.openfeedback.plugins.lib") } +android { + namespace = "io.openfeedback.android" +} + openfeedback { configurePublishing("feedback-android-sdk") } diff --git a/openfeedback/src/main/AndroidManifest.xml b/openfeedback/src/main/AndroidManifest.xml deleted file mode 100644 index d74c25d..0000000 --- a/openfeedback/src/main/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ No newline at end of file diff --git a/sample-app/build.gradle.kts b/sample-app/build.gradle.kts index 8ef198f..fa9ec33 100644 --- a/sample-app/build.gradle.kts +++ b/sample-app/build.gradle.kts @@ -3,6 +3,7 @@ plugins { } android { + namespace = "io.openfeedback.android.sample" defaultConfig { versionCode = 1 versionName = "1" diff --git a/sample-app/src/main/AndroidManifest.xml b/sample-app/src/main/AndroidManifest.xml index 2de75e6..c325d1e 100644 --- a/sample-app/src/main/AndroidManifest.xml +++ b/sample-app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + Date: Tue, 31 Oct 2023 14:35:07 +0100 Subject: [PATCH 2/3] refactor(openfeedback): migrate to repo architecture in data layer. --- README.md | 17 +- .../android/m2/OpenFeedbackLayout.kt | 28 +- .../io/openfeedback/android/m2/PoweredBy.kt | 7 +- .../android/m3/OpenFeedbackLayout.kt | 28 +- .../io/openfeedback/android/m3/PoweredBy.kt | 8 +- openfeedback-ui/build.gradle.kts | 24 -- .../android/components/OpenFeedbackLayout.kt | 82 ------ .../android/components/VoteItems.kt | 40 --- .../viewmodels/OpenFeedbackModelHelper.kt | 77 ------ .../viewmodels/OpenFeedbackUIExtensions.kt | 46 ---- .../viewmodels/OpenFeedbackViewModel.kt | 75 +++--- .../android/viewmodels/mappers/UiMappers.kt | 66 +++++ .../models/UISessionFeedbackWithColors.kt | 6 + openfeedback/build.gradle.kts | 4 +- .../io/openfeedback/android/FirebaseConfig.kt | 8 + .../android/OpenFeedbackConfig.kt | 246 ------------------ .../android/OpenFeedbackRepository.kt | 47 ++++ .../android/caches/OptimisticVoteCaching.kt | 29 +++ .../android/sources/FirebaseFactory.kt | 22 ++ .../android/sources/OpenFeedbackAuth.kt | 30 +++ .../android/sources/OpenFeedbackFirestore.kt | 94 +++++++ .../main/res/drawable/openfeedback_dark.png | Bin .../main/res/drawable/openfeedback_light.png | Bin .../android/sample/MainActivity.kt | 21 +- settings.gradle.kts | 1 - 25 files changed, 380 insertions(+), 626 deletions(-) delete mode 100644 openfeedback-ui/build.gradle.kts delete mode 100644 openfeedback-ui/src/main/java/io/openfeedback/android/components/OpenFeedbackLayout.kt delete mode 100644 openfeedback-ui/src/main/java/io/openfeedback/android/components/VoteItems.kt delete mode 100644 openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackModelHelper.kt delete mode 100644 openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackUIExtensions.kt create mode 100644 openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/mappers/UiMappers.kt create mode 100644 openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedbackWithColors.kt create mode 100644 openfeedback/src/main/java/io/openfeedback/android/FirebaseConfig.kt delete mode 100644 openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackConfig.kt create mode 100644 openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackRepository.kt create mode 100644 openfeedback/src/main/java/io/openfeedback/android/caches/OptimisticVoteCaching.kt create mode 100644 openfeedback/src/main/java/io/openfeedback/android/sources/FirebaseFactory.kt create mode 100644 openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackAuth.kt create mode 100644 openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackFirestore.kt rename {openfeedback-viewmodel => openfeedback}/src/main/res/drawable/openfeedback_dark.png (100%) rename {openfeedback-viewmodel => openfeedback}/src/main/res/drawable/openfeedback_light.png (100%) diff --git a/README.md b/README.md index c54b8a4..7c8ac80 100644 --- a/README.md +++ b/README.md @@ -14,20 +14,17 @@ configuration which is common for all sessions of your event. ```kotlin // In your Application class -val config = OpenFeedbackConfig( - context = context, - projectId = "", - firebaseConfig = OpenFeedback.FirebaseConfig( - projectId = "", - applicationId = "", - apiKey = "", - databaseUrl = "https://.firebaseio.com" - ) +val firebaseConfig = FirebaseConfig( + projectId = "", + applicationId = "", + apiKey = "", + databaseUrl = "https://.firebaseio.com" ) // In your Compose screen OpenFeedback( - openFeedbackState = MyApp.config, + config = MyApp.firebaseConfig, + projectId = "", sessionId = "", language = "" ) diff --git a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/OpenFeedbackLayout.kt b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/OpenFeedbackLayout.kt index 07822cd..99ace12 100644 --- a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/OpenFeedbackLayout.kt +++ b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/OpenFeedbackLayout.kt @@ -8,45 +8,29 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import io.openfeedback.android.OpenFeedbackConfig +import io.openfeedback.android.FirebaseConfig import io.openfeedback.android.viewmodels.OpenFeedbackUiState import io.openfeedback.android.viewmodels.OpenFeedbackViewModel import io.openfeedback.android.viewmodels.models.UISessionFeedback import io.openfeedback.android.viewmodels.models.UIVoteItem -@Deprecated( - message = "Use OpenFeedback component with projectId parameter." -) @Composable fun OpenFeedback( - openFeedbackState: OpenFeedbackConfig, - sessionId: String, - language: String, - modifier: Modifier = Modifier, - loading: @Composable () -> Unit = { Loading(modifier = modifier) } -) = OpenFeedback( - config = openFeedbackState, - projectId = openFeedbackState.openFeedbackProjectId, - sessionId = sessionId, - language = language, - modifier = modifier, - loading = loading -) - -@Composable -fun OpenFeedback( - config: OpenFeedbackConfig, + config: FirebaseConfig, projectId: String, sessionId: String, language: String, modifier: Modifier = Modifier, loading: @Composable () -> Unit = { Loading(modifier = modifier) } ) { + val context = LocalContext.current val viewModel: OpenFeedbackViewModel = viewModel( factory = OpenFeedbackViewModel.Factory.create( - openFeedbackConfig = config, + context = context, + firebaseConfig = config, projectId = projectId, sessionId = sessionId, language = language diff --git a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/PoweredBy.kt b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/PoweredBy.kt index e9c3279..a8a4496 100644 --- a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/PoweredBy.kt +++ b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/PoweredBy.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import io.openfeedback.android.R as ROF @Composable fun PoweredBy( @@ -27,9 +28,9 @@ fun PoweredBy( color: Color = MaterialTheme.colors.onBackground ) { val logo = - if (MaterialTheme.colors.isLight) R.drawable.openfeedback_light - else R.drawable.openfeedback_dark - val poweredBy = stringResource(id = R.string.powered_by) + if (MaterialTheme.colors.isLight) ROF.drawable.openfeedback_light + else ROF.drawable.openfeedback_dark + val poweredBy = stringResource(id = ROF.string.powered_by) Row( modifier = modifier.semantics(mergeDescendants = true) { contentDescription = "$poweredBy Openfeedback" diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/OpenFeedbackLayout.kt b/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/OpenFeedbackLayout.kt index b649417..2a7ada9 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/OpenFeedbackLayout.kt +++ b/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/OpenFeedbackLayout.kt @@ -9,45 +9,29 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel -import io.openfeedback.android.OpenFeedbackConfig +import io.openfeedback.android.FirebaseConfig import io.openfeedback.android.viewmodels.OpenFeedbackUiState import io.openfeedback.android.viewmodels.OpenFeedbackViewModel import io.openfeedback.android.viewmodels.models.UISessionFeedback import io.openfeedback.android.viewmodels.models.UIVoteItem -@Deprecated( - message = "Use OpenFeedback component with projectId parameter." -) @Composable fun OpenFeedback( - openFeedbackState: OpenFeedbackConfig, - sessionId: String, - language: String, - modifier: Modifier = Modifier, - loading: @Composable () -> Unit = { Loading(modifier = modifier) } -) = OpenFeedback( - config = openFeedbackState, - projectId = openFeedbackState.openFeedbackProjectId, - sessionId = sessionId, - language = language, - modifier = modifier, - loading = loading -) - -@Composable -fun OpenFeedback( - config: OpenFeedbackConfig, + config: FirebaseConfig, projectId: String, sessionId: String, language: String, modifier: Modifier = Modifier, loading: @Composable () -> Unit = { Loading(modifier = modifier) } ) { + val context = LocalContext.current val viewModel: OpenFeedbackViewModel = viewModel( factory = OpenFeedbackViewModel.Factory.create( - openFeedbackConfig = config, + context = context, + firebaseConfig = config, projectId = projectId, sessionId = sessionId, language = language diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/PoweredBy.kt b/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/PoweredBy.kt index 3624bf6..c65f415 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/PoweredBy.kt +++ b/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/PoweredBy.kt @@ -1,7 +1,6 @@ package io.openfeedback.android.m3 import androidx.compose.foundation.Image -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height @@ -18,6 +17,7 @@ import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp +import io.openfeedback.android.R as ROF @Composable internal fun PoweredBy( @@ -27,9 +27,9 @@ internal fun PoweredBy( ) { val logo = - if (MaterialTheme.colorScheme.background.luminance() > 0.5) R.drawable.openfeedback_light - else R.drawable.openfeedback_dark - val poweredBy = stringResource(id = R.string.powered_by) + if (MaterialTheme.colorScheme.background.luminance() > 0.5) ROF.drawable.openfeedback_light + else ROF.drawable.openfeedback_dark + val poweredBy = stringResource(id = ROF.string.powered_by) Row( modifier = modifier.semantics(mergeDescendants = true) { contentDescription = "$poweredBy Openfeedback" diff --git a/openfeedback-ui/build.gradle.kts b/openfeedback-ui/build.gradle.kts deleted file mode 100644 index 9bc802c..0000000 --- a/openfeedback-ui/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ - -plugins { - id("io.openfeedback.plugins.compose.lib") -} - -android { - namespace = "io.openfeedback.android.ui" -} - -openfeedback { - configurePublishing("feedback-android-sdk-ui") -} - -dependencies { - implementation(libs.androidx.lifecycle.viewmodel.compose) - - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.foundation) - - api(projects.openfeedback) - api(projects.openfeedbackM2) - api(projects.openfeedbackViewmodel) -} diff --git a/openfeedback-ui/src/main/java/io/openfeedback/android/components/OpenFeedbackLayout.kt b/openfeedback-ui/src/main/java/io/openfeedback/android/components/OpenFeedbackLayout.kt deleted file mode 100644 index 067fb70..0000000 --- a/openfeedback-ui/src/main/java/io/openfeedback/android/components/OpenFeedbackLayout.kt +++ /dev/null @@ -1,82 +0,0 @@ -package io.openfeedback.android.components - -import android.content.Context -import androidx.compose.foundation.layout.Box -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.runtime.collectAsState -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import io.openfeedback.android.OpenFeedbackConfig -import io.openfeedback.android.viewmodels.OpenFeedbackUiState -import io.openfeedback.android.viewmodels.OpenFeedbackViewModel -import io.openfeedback.android.m2.Loading -import io.openfeedback.android.m2.PoweredBy -import io.openfeedback.android.viewmodels.models.UISessionFeedback -import io.openfeedback.android.viewmodels.models.UIVoteItem - -@Deprecated(message = "Use OpenFeedback component in m2 artifact") -@Composable -fun OpenFeedback( - openFeedbackState: OpenFeedbackConfig, - sessionId: String, - language: String, - modifier: Modifier = Modifier, - loading: @Composable () -> Unit = { Loading(modifier = modifier) } -) { - val viewModel: OpenFeedbackViewModel = viewModel( - factory = OpenFeedbackViewModel.Factory.create(openFeedbackState, sessionId, language) - ) - val uiState = viewModel.uiState.collectAsState() - when (uiState.value) { - is OpenFeedbackUiState.Loading -> loading() - is OpenFeedbackUiState.Success -> { - val session = (uiState.value as OpenFeedbackUiState.Success).session - OpenFeedbackLayout( - sessionFeedback = session, - modifier = modifier, - onClick = { voteItem -> viewModel.vote(voteItem = voteItem) } - ) - } - } -} - -@Deprecated(message = "Use OpenFeedbackLayout component in m2 artifact") -@Composable -fun OpenFeedbackLayout( - sessionFeedback: UISessionFeedback, - modifier: Modifier = Modifier, - onClick: (voteItem: UIVoteItem) -> Unit -) { - Column(modifier = modifier) { - VoteItems(voteItems = sessionFeedback.voteItem, onClick = onClick) - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 5.dp), - contentAlignment = Alignment.Center - ) { - PoweredBy() - } - } -} - -@Deprecated(message = "Configure state in your Application class") -@Composable -fun rememberOpenFeedbackState( - context: Context = LocalContext.current, - projectId: String, - firebaseConfig: OpenFeedbackConfig.FirebaseConfig -) = remember { - OpenFeedbackConfig( - context = context, - openFeedbackProjectId = projectId, - firebaseConfig = firebaseConfig - ) -} diff --git a/openfeedback-ui/src/main/java/io/openfeedback/android/components/VoteItems.kt b/openfeedback-ui/src/main/java/io/openfeedback/android/components/VoteItems.kt deleted file mode 100644 index 2566a52..0000000 --- a/openfeedback-ui/src/main/java/io/openfeedback/android/components/VoteItems.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.openfeedback.android.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.openfeedback.android.m2.VoteCard -import io.openfeedback.android.viewmodels.models.UIVoteItem - -@Composable -internal fun VoteItems( - voteItems: List, - modifier: Modifier = Modifier, - columnCount: Int = 2, - onClick: (voteItem: UIVoteItem) -> Unit -) { - val spaceGrid = 8.dp - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(spaceGrid) - ) { - 0.until(columnCount).forEach { column -> - Box(modifier = Modifier.weight(1f)) { - Column(verticalArrangement = Arrangement.spacedBy(spaceGrid)) { - voteItems.filterIndexed { index, _ -> - index % columnCount == column - }.forEach { voteItem -> - VoteCard( - voteModel = voteItem, - onClick = onClick - ) - } - } - } - } - } -} diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackModelHelper.kt b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackModelHelper.kt deleted file mode 100644 index 586c341..0000000 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackModelHelper.kt +++ /dev/null @@ -1,77 +0,0 @@ -package io.openfeedback.android.viewmodels - -import io.openfeedback.android.model.Project -import io.openfeedback.android.viewmodels.models.UIDot -import io.openfeedback.android.viewmodels.models.UISessionFeedback -import io.openfeedback.android.viewmodels.models.UIVoteItem -import kotlin.math.absoluteValue -import kotlin.random.Random - -internal object OpenFeedbackModelHelper { - fun toUISessionFeedback( - project: Project, - userVotes: List, - totalVotes: Map, - language: String - ): UISessionFeedback { - val voteItems = project.voteItems - .filter { it.type == "boolean" } - .map { voteItem -> - val count = totalVotes.entries - .find { e -> voteItem.id == e.key } - ?.value - ?.toInt() - ?: 0 - UIVoteItem( - id = voteItem.id, - text = voteItem.localizedName(language), - dots = dots(count, project.chipColors), - votedByUser = userVotes.contains(voteItem.id) - ) - } - return UISessionFeedback( - comments = emptyList(), - voteItem = voteItems - ) - } - - private fun dots(count: Int, possibleColors: List): List { - return 0.until(count).map { - UIDot( - Random.nextFloat(), - Random.nextFloat().coerceIn(0.1f, 0.9f), - possibleColors[Random.nextInt().absoluteValue % possibleColors.size] - ) - } - } - - fun keepDotsPosition( - oldSessionFeedback: UISessionFeedback?, - newSessionFeedback: UISessionFeedback, - colors: List - ): UISessionFeedback { - return UISessionFeedback( - comments = newSessionFeedback.comments, - voteItem = newSessionFeedback.voteItem.map { newVoteItem -> - val oldVoteItem = oldSessionFeedback?.voteItem?.find { it.id == newVoteItem.id } - val newDots = if (oldVoteItem != null) { - val diff = newVoteItem.dots.size - oldVoteItem.dots.size - if (diff > 0) { - oldVoteItem.dots + dots(diff, colors) - } else { - oldVoteItem.dots.dropLast(diff.absoluteValue) - } - } else { - newVoteItem.dots - } - - UIVoteItem( - id = newVoteItem.id, - text = newVoteItem.text, - dots = newDots, - votedByUser = newVoteItem.votedByUser - ) - } - ) - } -} diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackUIExtensions.kt b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackUIExtensions.kt deleted file mode 100644 index 8af1cf7..0000000 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackUIExtensions.kt +++ /dev/null @@ -1,46 +0,0 @@ -package io.openfeedback.android.viewmodels - -import io.openfeedback.android.OpenFeedbackConfig -import io.openfeedback.android.viewmodels.models.UISessionFeedback -import kotlinx.coroutines.flow.combine - -@Deprecated( - message = "Use getUISessionFeedback(projectId: String, sessionId: String, language: String) instead of this one.", - replaceWith = ReplaceWith("getUISessionFeedback(openFeedbackProjectId, sessionId, language)") -) -internal suspend fun OpenFeedbackConfig.getUISessionFeedback( - sessionId: String, - language: String -) = combine( - getProject(), - getUserVotes(sessionId), - getTotalVotes(sessionId) -) { project, userVotes, totalVotes -> - return@combine UISessionFeedbackWithColors( - OpenFeedbackModelHelper.toUISessionFeedback(project, userVotes, totalVotes, language), - project.chipColors - ) -} - -/** - * A bunch of extensions to get UI models from a openFeedback object - */ -internal suspend fun OpenFeedbackConfig.getUISessionFeedback( - projectId: String, - sessionId: String, - language: String -) = combine( - getProject(projectId), - getUserVotes(projectId, sessionId), - getTotalVotes(projectId, sessionId) -) { project, userVotes, totalVotes -> - return@combine UISessionFeedbackWithColors( - OpenFeedbackModelHelper.toUISessionFeedback(project, userVotes, totalVotes, language), - project.chipColors - ) -} - -internal data class UISessionFeedbackWithColors( - val session: UISessionFeedback, - val colors: List -) diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackViewModel.kt b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackViewModel.kt index 0b4f0cd..fa3d99f 100644 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackViewModel.kt +++ b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackViewModel.kt @@ -1,49 +1,71 @@ package io.openfeedback.android.viewmodels +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope -import io.openfeedback.android.OpenFeedbackConfig +import com.google.firebase.FirebaseApp +import io.openfeedback.android.FirebaseConfig +import io.openfeedback.android.OpenFeedbackRepository +import io.openfeedback.android.caches.OptimisticVoteCaching +import io.openfeedback.android.model.VoteStatus +import io.openfeedback.android.sources.FirebaseFactory +import io.openfeedback.android.sources.OpenFeedbackAuth +import io.openfeedback.android.sources.OpenFeedbackFirestore +import io.openfeedback.android.viewmodels.mappers.convertToUiSessionFeedback import io.openfeedback.android.viewmodels.models.UISessionFeedback +import io.openfeedback.android.viewmodels.models.UISessionFeedbackWithColors import io.openfeedback.android.viewmodels.models.UIVoteItem -import io.openfeedback.android.model.VoteStatus import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch sealed class OpenFeedbackUiState { - object Loading : OpenFeedbackUiState() + data object Loading : OpenFeedbackUiState() class Success(val session: UISessionFeedback) : OpenFeedbackUiState() } class OpenFeedbackViewModel( - private val openFeedbackConfig: OpenFeedbackConfig, + private val firebase: FirebaseApp, private val projectId: String, private val sessionId: String, private val language: String ) : ViewModel() { + private val repository = OpenFeedbackRepository( + auth = OpenFeedbackAuth.Factory.create(firebase), + firestore = OpenFeedbackFirestore.Factory.create(firebase), + optimisticVoteCaching = OptimisticVoteCaching() + ) + private val _uiState = MutableStateFlow(OpenFeedbackUiState.Loading) val uiState: StateFlow = _uiState init { viewModelScope.launch { - openFeedbackConfig.getUISessionFeedback(projectId, sessionId, language).collect { + combine( + flow = repository.project(projectId), + flow2 = repository.userVotes(projectId, sessionId), + flow3 = repository.totalVotes(projectId, sessionId), + transform = { project, votes, totals -> + UISessionFeedbackWithColors( + convertToUiSessionFeedback(project, votes, totals, language), + project.chipColors + ) + } + ).collect { val oldSession = if (uiState.value is OpenFeedbackUiState.Success) (uiState.value as OpenFeedbackUiState.Success).session else null _uiState.value = OpenFeedbackUiState.Success( - OpenFeedbackModelHelper.keepDotsPosition( - oldSessionFeedback = oldSession, - newSessionFeedback = it.session, - colors = it.colors - ) + it.convertToUiSessionFeedback(oldSession) ) } } } fun vote(voteItem: UIVoteItem) = viewModelScope.launch { - openFeedbackConfig.setVote( + repository.setVote( projectId = projectId, talkId = sessionId, voteItemId = voteItem.id, @@ -52,34 +74,19 @@ class OpenFeedbackViewModel( } object Factory { - @Deprecated( - message = "Use create(openFeedbackConfig: OpenFeedbackConfig, projectId: String, sessionId: String, language: String) instead of this one.", - replaceWith = ReplaceWith("create(config, openFeedbackProjectId, sessionId, language)") - ) - fun create(openFeedbackConfig: OpenFeedbackConfig, sessionId: String, language: String) = - object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T = - OpenFeedbackViewModel( - openFeedbackConfig = openFeedbackConfig, - projectId = openFeedbackConfig.openFeedbackProjectId, - sessionId = sessionId, - language = language - ) as T - } - fun create( - openFeedbackConfig: OpenFeedbackConfig, + context: Context, + firebaseConfig: FirebaseConfig, projectId: String, sessionId: String, language: String ) = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T = - OpenFeedbackViewModel( - openFeedbackConfig = openFeedbackConfig, - projectId = projectId, - sessionId = sessionId, - language = language - ) as T + override fun create(modelClass: Class): T = OpenFeedbackViewModel( + firebase = FirebaseFactory.create(context, firebaseConfig), + projectId = projectId, + sessionId = sessionId, + language = language + ) as T } } } diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/mappers/UiMappers.kt b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/mappers/UiMappers.kt new file mode 100644 index 0000000..1cee0d7 --- /dev/null +++ b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/mappers/UiMappers.kt @@ -0,0 +1,66 @@ +package io.openfeedback.android.viewmodels.mappers + +import io.openfeedback.android.model.Project +import io.openfeedback.android.viewmodels.models.UIDot +import io.openfeedback.android.viewmodels.models.UISessionFeedback +import io.openfeedback.android.viewmodels.models.UISessionFeedbackWithColors +import io.openfeedback.android.viewmodels.models.UIVoteItem +import kotlin.math.absoluteValue +import kotlin.random.Random + +fun convertToUiSessionFeedback( + project: Project, + userVotes: List, + totalVotes: Map, + language: String +): UISessionFeedback = UISessionFeedback( + comments = emptyList(), + voteItem = project.voteItems + .filter { it.type == "boolean" } + .map { voteItem -> + val count = totalVotes.entries + .find { e -> voteItem.id == e.key } + ?.value + ?.toInt() + ?: 0 + UIVoteItem( + id = voteItem.id, + text = voteItem.localizedName(language), + dots = dots(count, project.chipColors), + votedByUser = userVotes.contains(voteItem.id) + ) + } +) + +fun UISessionFeedbackWithColors.convertToUiSessionFeedback( + oldSessionFeedback: UISessionFeedback? +): UISessionFeedback = UISessionFeedback( + comments = this.session.comments, + voteItem = this.session.voteItem.map { newVoteItem -> + val oldVoteItem = oldSessionFeedback?.voteItem?.find { it.id == newVoteItem.id } + val newDots = if (oldVoteItem != null) { + val diff = newVoteItem.dots.size - oldVoteItem.dots.size + if (diff > 0) { + oldVoteItem.dots + dots(diff, this.colors) + } else { + oldVoteItem.dots.dropLast(diff.absoluteValue) + } + } else { + newVoteItem.dots + } + UIVoteItem( + id = newVoteItem.id, + text = newVoteItem.text, + dots = newDots, + votedByUser = newVoteItem.votedByUser + ) + } +) + +private fun dots(count: Int, possibleColors: List): List = 0.until(count).map { + UIDot( + Random.nextFloat(), + Random.nextFloat().coerceIn(0.1f, 0.9f), + possibleColors[Random.nextInt().absoluteValue % possibleColors.size] + ) +} diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedbackWithColors.kt b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedbackWithColors.kt new file mode 100644 index 0000000..3f80bf1 --- /dev/null +++ b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedbackWithColors.kt @@ -0,0 +1,6 @@ +package io.openfeedback.android.viewmodels.models + +data class UISessionFeedbackWithColors( + val session: UISessionFeedback, + val colors: List +) diff --git a/openfeedback/build.gradle.kts b/openfeedback/build.gradle.kts index 2de0e5b..1dfc072 100644 --- a/openfeedback/build.gradle.kts +++ b/openfeedback/build.gradle.kts @@ -17,6 +17,6 @@ dependencies { api(libs.kotlin.coroutines.play.services) // Firestore - implementation(libs.firebase.firestore) - implementation(libs.firebase.auth) + api(libs.firebase.firestore) + api(libs.firebase.auth) } diff --git a/openfeedback/src/main/java/io/openfeedback/android/FirebaseConfig.kt b/openfeedback/src/main/java/io/openfeedback/android/FirebaseConfig.kt new file mode 100644 index 0000000..e9412af --- /dev/null +++ b/openfeedback/src/main/java/io/openfeedback/android/FirebaseConfig.kt @@ -0,0 +1,8 @@ +package io.openfeedback.android + +class FirebaseConfig( + val projectId: String, + val applicationId: String, + val apiKey: String, + val databaseUrl: String +) \ No newline at end of file diff --git a/openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackConfig.kt b/openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackConfig.kt deleted file mode 100644 index ead389c..0000000 --- a/openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackConfig.kt +++ /dev/null @@ -1,246 +0,0 @@ -package io.openfeedback.android - -import android.content.Context -import android.util.Log -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.FirebaseUser -import com.google.firebase.firestore.FirebaseFirestore -import com.google.firebase.firestore.FirebaseFirestoreSettings -import io.openfeedback.android.model.Project -import io.openfeedback.android.model.VoteStatus -import kotlinx.coroutines.channels.BroadcastChannel -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.consumeEach -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.asFlow -import kotlinx.coroutines.flow.flattenMerge -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.tasks.await -import java.util.Date - -class OpenFeedbackConfig( - context: Context, - firebaseConfig: FirebaseConfig, - appName: String = "openfeedback" -) { - val firestore: FirebaseFirestore - val auth: FirebaseAuth - var openFeedbackProjectId: String = "" - - class OptimisticVotes( - var lastValue: Map?, - val channel: BroadcastChannel> - ) - - /** - * TODO: check if this leaks - */ - val optimisticVotes = mutableMapOf() - - class FirebaseConfig( - val projectId: String, - val applicationId: String, - val apiKey: String, - val databaseUrl: String - ) - - @Deprecated( - message = "Openfeedback project id should be passed in parameter in functions" - ) - constructor( - context: Context, - firebaseConfig: FirebaseConfig, - openFeedbackProjectId: String, - appName: String = "openfeedback" - ) : this(context, firebaseConfig, appName) { - this.openFeedbackProjectId = openFeedbackProjectId - } - - init { - val options = FirebaseOptions.Builder() - .setProjectId(firebaseConfig.projectId) - .setApplicationId(firebaseConfig.applicationId) - .setApiKey(firebaseConfig.apiKey) - .setDatabaseUrl(firebaseConfig.databaseUrl) - .build() - - val app = FirebaseApp.initializeApp(context, options, appName) - - firestore = FirebaseFirestore.getInstance(app) - - firestore.firestoreSettings = FirebaseFirestoreSettings.Builder() - .setPersistenceEnabled(true) - .build() - - auth = FirebaseAuth.getInstance(app) - } - - private suspend fun withFirebaseUser(block: suspend (FirebaseUser) -> R?): R? { - return getFirebaseUser()?.let { block.invoke(it) } - } - - private suspend fun getFirebaseUser() = Mutex().withLock { - if (auth.currentUser == null) { - val result = auth.signInAnonymously().await() - if (result.user == null) { - Log.e(OpenFeedbackConfig::class.java.name, "Cannot signInAnonymously") - } - } - auth.currentUser - } - - @Deprecated( - message = "Use getProject(projectId: String) instead of this one.", - replaceWith = ReplaceWith("getProject(openFeedbackProjectId)") - ) - suspend fun getProject(): Flow = getProject(openFeedbackProjectId) - - suspend fun getProject(projectId: String): Flow = flow { - firestore.collection("projects") - .document(projectId) - .toFlow() - .collect { documentSnapshot -> - documentSnapshot.toObject(Project::class.java)?.let { emit(it) } - } - } - - @Deprecated( - message = "Use getUserVotes(projectId: String, sessionId: String) instead of this one.", - replaceWith = ReplaceWith("getUserVotes(openFeedbackProjectId, sessionId)") - ) - fun getUserVotes(sessionId: String) = getUserVotes(openFeedbackProjectId, sessionId) - - fun getUserVotes(projectId: String, sessionId: String) = flow { - val user = getFirebaseUser() - if (user != null) { - firestore.collection("projects/$projectId/userVotes") - .whereEqualTo("userId", user.uid) - .toFlow() - .collect { querySnapshot -> - val votes = querySnapshot - .filter { it.data["status"] == VoteStatus.Active.value && it.data["talkId"] == sessionId } - .map { it.data["voteItemId"] as String } - emit(votes) - } - } - } - - @Deprecated( - message = "Use getTotalVotes(projectId: String, sessionId: String) instead of this one.", - replaceWith = ReplaceWith("getTotalVotes(openFeedbackProjectId, sessionId)") - ) - fun getTotalVotes(sessionId: String): Flow> = - getTotalVotes(openFeedbackProjectId, sessionId) - - fun getTotalVotes(projectId: String, sessionId: String): Flow> { - val optimisticVotes = optimisticVotes.getOrPut(sessionId) { - OptimisticVotes(null, BroadcastChannel(Channel.CONFLATED)) - } - - val channel = Channel>(Channel.CONFLATED) - val registration = firestore.collection("projects/$projectId/sessionVotes") - .document(sessionId) - .addSnapshotListener { documentSnapshot, firebaseFirestoreException -> - val totalVotes = documentSnapshot!!.data as? Map - ?: emptyMap() // If there's no vote yet, default to an empty map - - optimisticVotes.lastValue = totalVotes - //val source = if (documentSnapshot.metadata.isFromCache) "cache" else "netwo" - //Log.e("TotalVotes", "Firebase vote ($source): ${totalVotes.prettyString()}") - channel.trySend(totalVotes) - } - - channel.invokeOnClose { - registration.remove() - } - - val flow1 = flow { - channel.consumeEach { - emit(it) - } - } - val flow2 = optimisticVotes.channel.asFlow() - - return flowOf(flow1, flow2).flattenMerge() - } - - @Deprecated( - message = "Use setVote(projectId: String, talkId: String, voteItemId: String, status: VoteStatus) instead of this one.", - replaceWith = ReplaceWith("setVote(openFeedbackProjectId, talkId, voteItemId, status)") - ) - suspend fun setVote(talkId: String, voteItemId: String, status: VoteStatus) = - setVote(openFeedbackProjectId, talkId, voteItemId, status) - - suspend fun setVote(projectId: String, talkId: String, voteItemId: String, status: VoteStatus) = - withFirebaseUser { firebaseUser -> - val collectionReference = - firestore.collection("projects/$projectId/userVotes") - - val optimisticVotes = optimisticVotes.getOrPut(talkId) { - OptimisticVotes(null, BroadcastChannel(Channel.CONFLATED)) - } - - val lastValue = optimisticVotes.lastValue - if (lastValue != null) { - - optimisticVotes.lastValue = lastValue.toMutableMap().apply { - var count = lastValue.getOrElse(voteItemId, { 0L }) - count += if (status == VoteStatus.Deleted) -1 else 1 - if (count < 0) { - count = 0L - } - put(voteItemId, count) - } - - //Log.e("TotalVotes", "Optimistic vote: ${optimisticVotes.lastValue?.prettyString()}") - - optimisticVotes.channel.trySend(optimisticVotes.lastValue!!) - } - - val querySnapshot = collectionReference - .whereEqualTo("userId", firebaseUser.uid) - .whereEqualTo("talkId", talkId) - .whereEqualTo("voteItemId", voteItemId) - .get() - .await() - - if (querySnapshot.isEmpty) { - val documentReference = collectionReference.document() - documentReference.set( - mapOf( - "id" to documentReference.id, - "createdAt" to Date(), - "projectId" to projectId, - "status" to status.value, - "talkId" to talkId, - "updatedAt" to Date(), - "userId" to firebaseUser.uid, - "voteItemId" to voteItemId - ) - ) - } else { - if (querySnapshot.size() != 1) { - Log.e( - OpenFeedbackConfig::class.java.name, - "Too many votes registered for ${firebaseUser.uid}" - ) - } - - val documentID = querySnapshot.documents.get(0).id - collectionReference.document(documentID).update( - mapOf( - "updatedAt" to Date(), - "status" to status.value - ) - ) - } - } -} - -fun Map<*, *>.prettyString() = - entries.map { "${it.key}: ${it.value}" }.joinToString(separator = "\n", prefix = "\n") diff --git a/openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackRepository.kt b/openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackRepository.kt new file mode 100644 index 0000000..b1fa871 --- /dev/null +++ b/openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackRepository.kt @@ -0,0 +1,47 @@ +package io.openfeedback.android + +import io.openfeedback.android.caches.OptimisticVoteCaching +import io.openfeedback.android.model.Project +import io.openfeedback.android.model.VoteStatus +import io.openfeedback.android.sources.OpenFeedbackAuth +import io.openfeedback.android.sources.OpenFeedbackFirestore +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onEach + +class OpenFeedbackRepository( + private val auth: OpenFeedbackAuth, + private val firestore: OpenFeedbackFirestore, + private val optimisticVoteCaching: OptimisticVoteCaching +) { + fun project(projectId: String): Flow = firestore.project(projectId) + + @OptIn(ExperimentalCoroutinesApi::class) + fun userVotes(projectId: String, sessionId: String): Flow> = + flow { emit(auth.firebaseUser()) } + .flatMapConcat { + if (it != null) { + firestore.userVotes(projectId, it.uid, sessionId) + } else { + emptyFlow() + } + } + + fun totalVotes(projectId: String, sessionId: String): Flow> = + merge( + firestore.sessionVotes(projectId, sessionId) + .onEach { optimisticVoteCaching.setVotes(it) }, + optimisticVoteCaching.votes + ) + + suspend fun setVote(projectId: String, talkId: String, voteItemId: String, status: VoteStatus) { + auth.withFirebaseUser { + optimisticVoteCaching.updateVotes(voteItemId, status) + firestore.setVote(projectId, it.uid, talkId, voteItemId, status) + } + } +} diff --git a/openfeedback/src/main/java/io/openfeedback/android/caches/OptimisticVoteCaching.kt b/openfeedback/src/main/java/io/openfeedback/android/caches/OptimisticVoteCaching.kt new file mode 100644 index 0000000..ad0a54e --- /dev/null +++ b/openfeedback/src/main/java/io/openfeedback/android/caches/OptimisticVoteCaching.kt @@ -0,0 +1,29 @@ +package io.openfeedback.android.caches + +import io.openfeedback.android.model.VoteStatus +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.getAndUpdate +import kotlinx.coroutines.flow.update + +class OptimisticVoteCaching { + private val _votes = MutableStateFlow>(mutableMapOf()) + val votes: StateFlow> = _votes + + fun setVotes(votes: Map) { + _votes.update { votes } + } + + fun updateVotes(voteItemId: String, status: VoteStatus) { + _votes.update { + val map = it.toMutableMap() + var count = it.getOrElse(voteItemId) { 0L } + count += if (status == VoteStatus.Deleted) -1 else 1 + if (count < 0) { + count = 0L + } + map[voteItemId] = count + map + } + } +} diff --git a/openfeedback/src/main/java/io/openfeedback/android/sources/FirebaseFactory.kt b/openfeedback/src/main/java/io/openfeedback/android/sources/FirebaseFactory.kt new file mode 100644 index 0000000..41f5c70 --- /dev/null +++ b/openfeedback/src/main/java/io/openfeedback/android/sources/FirebaseFactory.kt @@ -0,0 +1,22 @@ +package io.openfeedback.android.sources + +import android.content.Context +import com.google.firebase.FirebaseApp +import com.google.firebase.FirebaseOptions +import io.openfeedback.android.FirebaseConfig + +object FirebaseFactory { + fun create( + context: Context, + config: FirebaseConfig, + appName: String = "openfeedback" + ): FirebaseApp { + val options = FirebaseOptions.Builder() + .setProjectId(config.projectId) + .setApplicationId(config.applicationId) + .setApiKey(config.apiKey) + .setDatabaseUrl(config.databaseUrl) + .build() + return FirebaseApp.initializeApp(context, options, appName) + } +} diff --git a/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackAuth.kt b/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackAuth.kt new file mode 100644 index 0000000..2be51eb --- /dev/null +++ b/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackAuth.kt @@ -0,0 +1,30 @@ +package io.openfeedback.android.sources + +import android.util.Log +import com.google.firebase.FirebaseApp +import com.google.firebase.auth.FirebaseAuth +import com.google.firebase.auth.FirebaseUser +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.tasks.await + +class OpenFeedbackAuth(private val auth: FirebaseAuth) { + suspend fun firebaseUser(): FirebaseUser? = Mutex().withLock { + if (auth.currentUser == null) { + val result = auth.signInAnonymously().await() + if (result.user == null) { + Log.e("OpenFeedbackAuth", "Cannot signInAnonymously") + } + } + auth.currentUser + } + + suspend fun withFirebaseUser(block: suspend (FirebaseUser) -> R?): R? { + return firebaseUser()?.let { block.invoke(it) } + } + + companion object Factory { + fun create(app: FirebaseApp): OpenFeedbackAuth = + OpenFeedbackAuth(FirebaseAuth.getInstance(app)) + } +} diff --git a/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackFirestore.kt b/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackFirestore.kt new file mode 100644 index 0000000..893338e --- /dev/null +++ b/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackFirestore.kt @@ -0,0 +1,94 @@ +package io.openfeedback.android.sources + +import com.google.firebase.FirebaseApp +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.FirebaseFirestoreSettings +import io.openfeedback.android.model.Project +import io.openfeedback.android.model.VoteStatus +import io.openfeedback.android.toFlow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.tasks.await +import java.util.Date + +class OpenFeedbackFirestore( + private val firestore: FirebaseFirestore +) { + fun project(projectId: String): Flow = + firestore.collection("projects") + .document(projectId) + .toFlow() + .map { querySnapshot -> + querySnapshot.toObject(Project::class.java)!! + } + + fun userVotes(projectId: String, userId: String, sessionId: String): Flow> = + firestore.collection("projects/$projectId/userVotes") + .whereEqualTo("userId", userId) + .toFlow() + .map { querySnapshot -> + querySnapshot + .filter { it.data["status"] == VoteStatus.Active.value && it.data["talkId"] == sessionId } + .map { it.data["voteItemId"] as String } + } + + fun sessionVotes(projectId: String, sessionId: String): Flow> = + firestore.collection("projects/$projectId/sessionVotes") + .document(sessionId) + .toFlow() + .map { querySnapshot -> + querySnapshot.data as? Map + ?: emptyMap() // If there's no vote yet, default to an empty map } + } + + + suspend fun setVote( + projectId: String, + userId: String, + talkId: String, + voteItemId: String, + status: VoteStatus + ) { + val collectionReference = firestore.collection("projects/$projectId/userVotes") + val querySnapshot = collectionReference + .whereEqualTo("userId", userId) + .whereEqualTo("talkId", talkId) + .whereEqualTo("voteItemId", voteItemId) + .get() + .await() + if (querySnapshot.isEmpty) { + val documentReference = collectionReference.document() + documentReference.set( + mapOf( + "id" to documentReference.id, + "createdAt" to Date(), + "projectId" to projectId, + "status" to status.value, + "talkId" to talkId, + "updatedAt" to Date(), + "userId" to userId, + "voteItemId" to voteItemId + ) + ) + } else { + collectionReference + .document(querySnapshot.documents[0].id) + .update( + mapOf( + "updatedAt" to Date(), + "status" to status.value + ) + ) + } + } + + companion object Factory { + fun create(app: FirebaseApp): OpenFeedbackFirestore { + val firestore = FirebaseFirestore.getInstance(app) + firestore.firestoreSettings = FirebaseFirestoreSettings.Builder() + .setPersistenceEnabled(true) + .build() + return OpenFeedbackFirestore(firestore) + } + } +} diff --git a/openfeedback-viewmodel/src/main/res/drawable/openfeedback_dark.png b/openfeedback/src/main/res/drawable/openfeedback_dark.png similarity index 100% rename from openfeedback-viewmodel/src/main/res/drawable/openfeedback_dark.png rename to openfeedback/src/main/res/drawable/openfeedback_dark.png diff --git a/openfeedback-viewmodel/src/main/res/drawable/openfeedback_light.png b/openfeedback/src/main/res/drawable/openfeedback_light.png similarity index 100% rename from openfeedback-viewmodel/src/main/res/drawable/openfeedback_light.png rename to openfeedback/src/main/res/drawable/openfeedback_light.png diff --git a/sample-app/src/main/java/io/openfeedback/android/sample/MainActivity.kt b/sample-app/src/main/java/io/openfeedback/android/sample/MainActivity.kt index f2b4d45..1412b47 100644 --- a/sample-app/src/main/java/io/openfeedback/android/sample/MainActivity.kt +++ b/sample-app/src/main/java/io/openfeedback/android/sample/MainActivity.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.Scaffold -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -22,25 +21,21 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import io.openfeedback.android.OpenFeedbackConfig +import io.openfeedback.android.FirebaseConfig import io.openfeedback.android.m2.OpenFeedback import io.openfeedback.android.sample.theme.DesignSystem import io.openfeedback.android.sample.theme.OpenFeedbackTheme class MainActivity : AppCompatActivity() { @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val projectId = "mMHR63ARZQpPidFQISyc" - val config = OpenFeedbackConfig( - context = this, - firebaseConfig = OpenFeedbackConfig.FirebaseConfig( - projectId = "openfeedback-b7ab9", - applicationId = "1:765209934800:android:a6bb09f3deabc2277297d5", - apiKey = "AIzaSyC_cfbh8xKwF8UPxCeasGcsHyK4s5yZFeA", - databaseUrl = "https://openfeedback-b7ab9.firebaseio.com" - ) + val firebaseConfig = FirebaseConfig( + projectId = "openfeedback-b7ab9", + applicationId = "1:765209934800:android:a6bb09f3deabc2277297d5", + apiKey = "AIzaSyC_cfbh8xKwF8UPxCeasGcsHyK4s5yZFeA", + databaseUrl = "https://openfeedback-b7ab9.firebaseio.com" ) setContent { var designSystem by rememberSaveable { mutableStateOf(DesignSystem.M2) } @@ -60,7 +55,7 @@ class MainActivity : AppCompatActivity() { when (designSystem) { DesignSystem.M2 -> Scaffold { OpenFeedback( - config = config, + config = firebaseConfig, projectId = projectId, sessionId = "173222", language = "en", @@ -71,7 +66,7 @@ class MainActivity : AppCompatActivity() { } DesignSystem.M3 -> androidx.compose.material3.Scaffold { io.openfeedback.android.m3.OpenFeedback( - config = config, + config = firebaseConfig, projectId = projectId, sessionId = "173222", language = "en", diff --git a/settings.gradle.kts b/settings.gradle.kts index 52181f2..a670f6a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -17,7 +17,6 @@ includeBuild("build-logic") include( ":openfeedback", - ":openfeedback-ui", ":openfeedback-m2", ":openfeedback-m3", ":openfeedback-viewmodel", From 4fb416c76be855f3353931601a472f38147af561 Mon Sep 17 00:00:00 2001 From: Gerard Paligot Date: Tue, 31 Oct 2023 15:55:18 +0100 Subject: [PATCH 3/3] chore(github): use Java 17 to build with GA. --- .github/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 15c8cee..f2a8f95 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,10 +11,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: set up JDK 11 + - name: set up JDK 17 uses: actions/setup-java@v1 with: - java-version: 11 + java-version: 17 - name: Build with Gradle run: | ./gradlew build -Dorg.gradle.jvmargs=-Xmx1g