├── .github └── workflows │ ├── build-pull-request.yaml │ ├── publish-docs.yaml │ ├── publish-release.yaml │ └── publish-snapshot.yaml ├── .gitignore ├── .idea └── codeStyles.xml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASING.md ├── build-logic ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ └── kotlin │ ├── accessors.kt │ ├── main.kt │ └── plugins.kt ├── build.gradle.kts ├── ci-build.sh ├── docs └── screenshot.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── librarian.root.properties ├── openfeedback-m3 ├── api │ ├── openfeedback-m3.api │ └── openfeedback-m3.klib.api ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── io │ │ └── openfeedback │ │ └── m3 │ │ ├── CommentInputPreview.kt │ │ ├── CommentItemsPreview.kt │ │ ├── CommentPreview.kt │ │ ├── LoadingPreview.kt │ │ ├── OpenFeedbackLayoutPreview.kt │ │ ├── VoteCardPreview.kt │ │ └── VoteItemsPreview.kt │ └── commonMain │ └── kotlin │ └── io │ └── openfeedback │ └── m3 │ ├── Comment.kt │ ├── CommentInput.kt │ ├── CommentItems.kt │ ├── DotModifier.kt │ ├── FeedbackNotReady.kt │ ├── Loading.kt │ ├── OpenFeedbackLayout.kt │ ├── PoweredBy.kt │ ├── VoteCard.kt │ └── VoteItems.kt ├── openfeedback-resources ├── api │ ├── openfeedback-resources.api │ └── openfeedback-resources.klib.api ├── build.gradle.kts └── src │ └── commonMain │ ├── .DS_Store │ ├── composeResources │ └── drawable │ │ ├── openfeedback_dark.png │ │ └── openfeedback_light.png │ └── kotlin │ ├── .DS_Store │ └── io │ └── openfeedback │ └── resources │ ├── EnStrings.kt │ ├── FrStrings.kt │ ├── LocalStrings.kt │ └── Strings.kt ├── openfeedback-ui-models ├── api │ ├── openfeedback-ui-models.api │ └── openfeedback-ui-models.klib.api ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── io │ └── openfeedback │ └── ui │ └── models │ ├── UIComment.kt │ ├── UIDot.kt │ ├── UISessionFeedback.kt │ └── UIVoteItem.kt ├── openfeedback-viewmodel ├── api │ ├── openfeedback-viewmodel.api │ └── openfeedback-viewmodel.klib.api ├── build.gradle.kts └── src │ └── commonMain │ └── kotlin │ └── io │ ├── .DS_Store │ └── openfeedback │ ├── .DS_Store │ ├── OpenFeedback.kt │ └── viewmodels │ ├── OpenFeedbackFirebaseConfig.kt │ ├── OpenFeedbackViewModel.kt │ ├── extensions │ └── Flow.ext.kt │ └── mappers │ └── SessionDataToUiModels.kt ├── openfeedback ├── api │ ├── openfeedback.api │ └── openfeedback.klib.api ├── build.gradle.kts ├── openfeedback-proguard-rules.pro └── src │ ├── androidMain │ └── kotlin │ │ └── io │ │ └── openfeedback │ │ └── mappers │ │ └── FirestoreToModelMappers.android.kt │ ├── commonMain │ └── kotlin │ │ └── io │ │ ├── .DS_Store │ │ └── openfeedback │ │ ├── OpenFeedbackRepository.kt │ │ ├── extensions │ │ ├── CommentMap.ext.kt │ │ ├── Flow.ext.kt │ │ ├── Project.ext.kt │ │ └── SessionData.ext.kt │ │ ├── mappers │ │ └── FirestoreToModelMappers.kt │ │ ├── model │ │ ├── EntityModels.kt │ │ └── FirestoreModels.kt │ │ └── sources │ │ ├── OpenFeedbackAuth.kt │ │ └── OpenFeedbackFirestore.kt │ └── iosMain │ └── kotlin │ └── io │ └── openfeedback │ └── mappers │ └── FirestoreToModelMappers.ios.kt ├── sample-app-android ├── .gitignore ├── api │ └── sample-app-android.api ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── openfeedback │ │ └── android │ │ ├── MainActivity.kt │ │ └── MainApplication.kt │ └── res │ ├── drawable-v24 │ └── ic_launcher_foreground.xml │ ├── drawable │ └── ic_launcher_background.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ └── ic_launcher_round.png │ └── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml ├── sample-app-ios ├── io-openfeedback-ios-Info.plist ├── io.openfeedback.ios.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ ├── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── xcuserdata │ │ │ └── mbonnin.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ ├── xcshareddata │ │ └── xcschemes │ │ │ └── io.openfeedback.ios.xcscheme │ └── xcuserdata │ │ └── mbonnin.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist └── io.openfeedback.ios │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── ContentView.swift │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── io_openfeedback_iosApp.swift ├── sample-app-shared ├── api │ ├── sample-app-shared.api │ └── sample-app-shared.klib.api ├── build.gradle.kts ├── librarian.module.properties └── src │ ├── commonMain │ └── kotlin │ │ └── io │ │ └── openfeedback │ │ └── shared │ │ └── main.kt │ └── iosMain │ └── kotlin │ └── io │ └── openfeedback │ └── shared │ └── MainViewController.kt ├── scripts └── release.main.kts └── settings.gradle.kts /.github/workflows/build-pull-request.yaml: -------------------------------------------------------------------------------- 1 | name: Build pull request 2 | 3 | on: pull_request 4 | 5 | jobs: 6 | build-pull-request: 7 | runs-on: macos-latest 8 | 9 | steps: 10 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 11 | - run: | 12 | ./gradlew build 13 | -------------------------------------------------------------------------------- /.github/workflows/publish-docs.yaml: -------------------------------------------------------------------------------- 1 | name: Publish documentation 2 | 3 | on: 4 | push: 5 | branches: ["main"] 6 | workflow_dispatch: 7 | 8 | env: 9 | INSTANCE: 'Writerside/doc' 10 | ARTIFACT: 'webHelpDOC2-all.zip' 11 | DOCKER_VERSION: '241.16003' 12 | 13 | jobs: 14 | build-docs: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 19 | with: 20 | fetch-depth: 0 21 | 22 | - name: Prepare static content 23 | run: | 24 | export JAVA_HOME=$JAVA_HOME_21_X64 # Remove when ubuntu-latest updates to Java 21 25 | ./gradlew dokkatooGeneratePublicationHtml 26 | mkdir -p build/static 27 | cp -rf build/dokka/html build/static/kdoc 28 | 29 | - name: Deploy Kdoc to github pages 30 | uses: JamesIves/github-pages-deploy-action@5c6e9e9f3672ce8fd37b9856193d2a537941e66c #v4.6.1 31 | with: 32 | branch: gh-pages # The branch the action should deploy to. 33 | folder: build/static # The folder the action should deploy. 34 | 35 | - name: Save artifact with build results 36 | uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 #v4.3.3 37 | with: 38 | name: docs 39 | path: | 40 | artifacts/${{ env.ARTIFACT }} 41 | retention-days: 7 -------------------------------------------------------------------------------- /.github/workflows/publish-release.yaml: -------------------------------------------------------------------------------- 1 | name: Publish release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - '*' 8 | 9 | jobs: 10 | publish-release: 11 | runs-on: macos-latest 12 | 13 | steps: 14 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 15 | - run: | 16 | ./gradlew librarianPublishToMavenCentral 17 | gh release create $GITHUB_REF_NAME --title $GITHUB_REF_NAME --verify-tag --notes-from-tag 18 | env: 19 | LIBRARIAN_SONATYPE_USERNAME: ${{ secrets.LIBRARIAN_SONATYPE_USERNAME }} 20 | LIBRARIAN_SONATYPE_PASSWORD: ${{ secrets.LIBRARIAN_SONATYPE_PASSWORD }} 21 | LIBRARIAN_SIGNING_PRIVATE_KEY: ${{ secrets.LIBRARIAN_SIGNING_PRIVATE_KEY }} 22 | LIBRARIAN_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.LIBRARIAN_SIGNING_PRIVATE_KEY_PASSWORD }} 23 | GH_TOKEN: ${{ github.token }} -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yaml: -------------------------------------------------------------------------------- 1 | name: Publish snapshot 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | jobs: 7 | publish-snapshot: 8 | runs-on: macos-latest 9 | 10 | steps: 11 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 #v4.1.7 12 | - run: | 13 | ./gradlew librarianPublishToSnapshots 14 | env: 15 | LIBRARIAN_SONATYPE_USERNAME: ${{ secrets.LIBRARIAN_SONATYPE_USERNAME }} 16 | LIBRARIAN_SONATYPE_PASSWORD: ${{ secrets.LIBRARIAN_SONATYPE_PASSWORD }} 17 | LIBRARIAN_SIGNING_PRIVATE_KEY: ${{ secrets.LIBRARIAN_SIGNING_PRIVATE_KEY }} 18 | LIBRARIAN_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.LIBRARIAN_SIGNING_PRIVATE_KEY_PASSWORD }} 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.idea/* 2 | build 3 | !/.idea/codeStyles.xml 4 | .gradle 5 | local.properties 6 | .kotlin 7 | xcuserdata -------------------------------------------------------------------------------- /.idea/codeStyles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 120 | 121 | 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Next version (unreleased) 2 | 3 | - Bump Kotlin to 2.1.10 4 | - Bump jvmTarget to 17 (because of the gitlive-firebase update) 5 | 6 | # Version 1.0.0-alpha.3 7 | _2024-07-16_ 8 | 9 | - [#41] ensure compose and models are stable for Compose Compiler. 10 | - [#41] be able to hide comments. 11 | - [#41] display a component if the feedback is not ready for review. 12 | 13 | # Version 1.0.0-alpha.2 14 | _2024-07-15_ 15 | 16 | - [#38] Publish Android artifacts to maven central. 17 | 18 | # Version 1.0.0-alpha.1 19 | _2024-07-13_ 20 | 21 | # Version 1.0.0-alpha.0 22 | _2024-07-13_ 23 | 24 | - [#29] Clean Gradle config, move to Lyricist and AndroidX ViewModel, clean ViewModel code, add api text files 25 | 26 | 27 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Thanks for contributing to openfeedback-android-sdk 2 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Martin Bonnin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![BuildStatus](https://github.com/paug/openfeedback-android-sdk/actions/workflows/ci.yaml/badge.svg)](https://github.com/paug/openfeedback-android-sdk/actions/workflows/ci.yaml/badge.svg) 2 | 3 | # Open-Feedback Kotlin SDK 4 | 5 | A Kotlin multiplatform client for Open-Feeedback https://github.com/HugoGresse/open-feedback: 6 | 7 | ![screenshot](docs/screenshot.png) 8 | 9 | ## Usage 10 | 11 | The Composable `OpenFeedback` is the entry point to vote on a session. It'll make calls 12 | between the Firebase which host your OpenFeedback instance and your mobile application. It is 13 | mandatory to initialize the `OpenFeedbackFirebaseConfig` class to be able to get the Firebase 14 | instance which is common for all sessions of your event. 15 | 16 | Note that it is mandatory to keep this instance unique in your application because it creates the 17 | `FirebaseApp` instance which is the active connection between your mobile application and the 18 | OpenFeedback Firebase host. Consider to init this configuration in your custom `Application` class. 19 | 20 | ```kotlin 21 | // In your Application class 22 | initializeOpenFeedback(OpenFeedbackFirebaseConfig( 23 | context = applicationContext, 24 | projectId = "", 25 | applicationId = "", 26 | apiKey = "", 27 | databaseUrl = "https://.firebaseio.com" 28 | )) 29 | 30 | // In your Compose screen 31 | OpenFeedback( 32 | projectId = "", 33 | sessionId = "" 34 | ) 35 | ``` 36 | 37 | That's all! 38 | 39 | See the [sample-app-android](sample-app/src/main/java/io/openfeedback/android/sample/MainActivity.kt) 40 | app module if you want to see this implementation in action. 41 | 42 | If you are interested to create your own UI, you can use the component `OpenFeedbackLayout`. This 43 | `Composable` takes OpenFeedback Model UI in input and you can use `OpenFeedbackViewModel` in the 44 | viewmodel artifact to get the data from the Firebase host. 45 | 46 | ## Metrics 47 | 48 | If you change Compose contracts or model ui, you can run the following command to check if 49 | Composable or models are still stable: 50 | 51 | ```shell 52 | ./gradlew assembleRelease -PcomposeCompilerReports=true -PcomposeCompilerMetrics=true 53 | ``` 54 | 55 | Then, you can check the `build/compose_compiler` folder where we are using Compose UI to check 56 | metrics. 57 | 58 | ## Installation 59 | 60 | The SDK is available on mavenCentral: 61 | 62 | ```kotlin 63 | repositories { 64 | mavenCentral() 65 | } 66 | 67 | val openfeedbackVersion = "1.0.0-alpha.3" 68 | dependencies { 69 | // Material 3 70 | implementation("io.openfeedback:openfeedback-m3:$openfeedbackVersion") 71 | } 72 | ``` 73 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | ## Releases 2 | 3 | Releases are made automatically from CI every time a tag is pushed. 4 | 5 | Run `./scripts/release.main.kts` to start a new release 6 | -------------------------------------------------------------------------------- /build-logic/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `embedded-kotlin` 3 | } 4 | 5 | group = "build-logic" 6 | 7 | repositories { 8 | mavenCentral() 9 | google() 10 | gradlePluginPortal() 11 | } 12 | 13 | dependencies { 14 | implementation(gradleApi()) 15 | implementation(libs.librarian) 16 | implementation(libs.jetbrains.kotlinx.coroutines) 17 | implementation(libs.android.gradle.plugin) 18 | implementation(libs.jetbrains.kotlin.gradle.plugin) 19 | implementation(libs.jetbrains.compose.compiler.gradle.plugin) 20 | implementation(libs.jetbrains.kotlin.serialization.plugin) 21 | implementation(libs.jetbrains.compose.gradle.plugin) 22 | implementation(libs.jetbrains.kotlinx.binary.compatibility.validator) 23 | implementation(libs.ben.manes.versions) 24 | implementation(libs.version.catalog.update) 25 | } 26 | -------------------------------------------------------------------------------- /build-logic/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "build-logic" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | dependencyResolutionManagement { 5 | versionCatalogs { 6 | create("libs") { 7 | from(files("../gradle/libs.versions.toml")) 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/accessors.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | import org.gradle.api.plugins.ExtensionAware 3 | import org.jetbrains.compose.ComposePlugin 4 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 5 | 6 | 7 | inline fun Project.extensionOrNull(): T? { 8 | return extensions.findByType(T::class.java) 9 | } 10 | 11 | inline fun Project.extension(): T { 12 | return extensionOrNull() ?: error("No extension of type '${T::class.java.name}") 13 | } 14 | 15 | val KotlinMultiplatformExtension.compose: ComposePlugin.Dependencies 16 | get() { 17 | return (this as ExtensionAware).extensions.getByName("compose") as ComposePlugin.Dependencies 18 | } 19 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.CommonExtension 2 | import com.gradleup.librarian.gradle.Librarian 3 | import com.gradleup.librarian.gradle.configureAndroidCompatibility 4 | import com.gradleup.librarian.gradle.configureJavaCompatibility 5 | import com.gradleup.librarian.gradle.configureKotlinCompatibility 6 | import org.gradle.api.Project 7 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 8 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask 9 | 10 | private fun Project.configureAndroid(namespace: String) { 11 | configureAndroidCompatibility(23, 35, 35) 12 | 13 | configureJavaCompatibility(17) 14 | //configureKotlinCompatibility(librarianProperties().kotlinCompatibility() ?: error("no kotlin compatibility found")) 15 | configureKotlinCompatibility("2.0.0") 16 | 17 | extensions.getByType(CommonExtension::class.java).apply { 18 | this.namespace = namespace 19 | } 20 | } 21 | 22 | private fun Project.configureKotlin(composeMetrics: Boolean) { 23 | tasks.withType(KotlinCompilationTask::class.java) { 24 | val freeCompilerArgs = it.compilerOptions.freeCompilerArgs 25 | freeCompilerArgs.add("-Xexpect-actual-classes") 26 | if (composeMetrics) { 27 | if (project.findProperty("composeCompilerReports") == "true") { 28 | freeCompilerArgs.add("-P") 29 | freeCompilerArgs.add("plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler") 30 | } 31 | if (project.findProperty("composeCompilerMetrics") == "true") { 32 | freeCompilerArgs.add("-P") 33 | freeCompilerArgs.add("plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=${project.layout.buildDirectory.asFile.get().absolutePath}/compose_compiler") 34 | } 35 | } 36 | } 37 | } 38 | 39 | private fun Project.configureKMP() { 40 | (extensions.getByName("kotlin") as KotlinMultiplatformExtension).apply { 41 | applyDefaultHierarchyTemplate() 42 | androidTarget { 43 | publishLibraryVariants("release") 44 | } 45 | iosX64() 46 | iosArm64() 47 | iosSimulatorArm64() 48 | } 49 | } 50 | 51 | fun Project.library( 52 | namespace: String, 53 | compose: Boolean = false, 54 | kotlin: (KotlinMultiplatformExtension) -> Unit 55 | ) { 56 | val kotlinMultiplatformExtension = applyKotlinMultiplatformPlugin() 57 | if (compose) { 58 | applyJetbrainsComposePlugin() 59 | } 60 | configureAndroid(namespace = namespace) 61 | configureKMP() 62 | 63 | configureKotlin(compose) 64 | 65 | kotlin(kotlinMultiplatformExtension) 66 | 67 | Librarian.module(project) 68 | } 69 | 70 | fun Project.androidApp( 71 | namespace: String, 72 | ) { 73 | configureAndroid(namespace = namespace) 74 | configureKotlin(composeMetrics = true) 75 | } 76 | -------------------------------------------------------------------------------- /build-logic/src/main/kotlin/plugins.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Project 2 | import org.jetbrains.compose.ComposeExtension 3 | import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension 4 | 5 | 6 | fun Project.applyKotlinMultiplatformPlugin(): KotlinMultiplatformExtension { 7 | pluginManager.apply("org.jetbrains.kotlin.multiplatform") 8 | return extension() 9 | } 10 | 11 | fun Project.applyJetbrainsComposePlugin(): ComposeExtension { 12 | pluginManager.apply("org.jetbrains.compose") 13 | pluginManager.apply("org.jetbrains.kotlin.plugin.compose") 14 | return extension() 15 | } 16 | 17 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.gradleup.librarian.gradle.Librarian 2 | 3 | buildscript { 4 | repositories { 5 | mavenCentral() 6 | google() 7 | gradlePluginPortal() 8 | } 9 | dependencies { 10 | //noinspection UseTomlInstead 11 | classpath("build-logic:build-logic") 12 | } 13 | configurations.all { 14 | resolutionStrategy.dependencySubstitution.all { 15 | requested.let { 16 | if (it is ModuleComponentSelector && it.module == "bcprov-jdk15on") { 17 | useTarget("${it.group}:bcprov-jdk18on:1.77") 18 | } 19 | if (it is ModuleComponentSelector && it.module == "bcpg-jdk15on") { 20 | useTarget("${it.group}:bcpg-jdk18on:1.77") 21 | } 22 | if (it is ModuleComponentSelector && it.module == "bcpkix-jdk15on") { 23 | useTarget("${it.group}:bcpkix-jdk18on:1.77") 24 | } 25 | } 26 | } 27 | } 28 | } 29 | Librarian.root(project) 30 | apply(plugin = "com.github.ben-manes.versions") 31 | apply(plugin = "nl.littlerobots.version-catalog-update") 32 | 33 | -------------------------------------------------------------------------------- /ci-build.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | 3 | ./gradlew assemble 4 | 5 | -------------------------------------------------------------------------------- /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/docs/screenshot.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx8g 3 | 4 | #Kotlin 5 | kotlin.code.style=official 6 | 7 | #Android 8 | android.useAndroidX=true 9 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | androidx-activity = "1.10.1" 3 | androidx-appcompat = "1.7.0" 4 | androidx-core = "1.15.0" 5 | gitlive-firebase = "2.1.0" 6 | google-firebase-auth = "23.2.0" 7 | google-firebase-common = "21.0.0" 8 | google-firebase-firestore = "25.1.2" 9 | jetbrains-compose = "1.7.3" 10 | jetbrains-kotlin = "2.1.10" 11 | jetbrains-kotlin-coroutines = "1.10.1" 12 | bcv = "0.17.0" 13 | jetbrains-kotlinx-collections-immutable = "0.3.8" 14 | jetbrains-kotlinx-datetime = "0.6.2" 15 | jetbrains-kotlinx-serialization = "1.8.0" 16 | lyricist = "1.7.0" 17 | multiplatform-locale = "0.9.0" 18 | touchlab-kermit = "2.0.5" 19 | 20 | [libraries] 21 | android-gradle-plugin = "com.android.tools.build:gradle:8.9.0" 22 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } 23 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } 24 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } 25 | androidx-lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version = "2.8.4" } 26 | ben-manes-versions = "com.github.ben-manes.versions:com.github.ben-manes.versions.gradle.plugin:0.52.0" 27 | gitlive-firebase-app = { module = "dev.gitlive:firebase-app", version.ref = "gitlive-firebase" } 28 | gitlive-firebase-auth = { module = "dev.gitlive:firebase-auth", version.ref = "gitlive-firebase" } 29 | gitlive-firebase-common = { module = "dev.gitlive:firebase-common", version.ref = "gitlive-firebase" } 30 | gitlive-firebase-firestore = { module = "dev.gitlive:firebase-firestore", version.ref = "gitlive-firebase" } 31 | google-firebase-auth = { module = "com.google.firebase:firebase-auth", version.ref = "google-firebase-auth" } 32 | google-firebase-common = { module = "com.google.firebase:firebase-common", version.ref = "google-firebase-common" } 33 | google-firebase-firestore = { module = "com.google.firebase:firebase-firestore-ktx", version.ref = "google-firebase-firestore" } 34 | jetbrains-compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "jetbrains-kotlin" } 35 | jetbrains-compose-gradle-plugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "jetbrains-compose" } 36 | jetbrains-kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "jetbrains-kotlin" } 37 | jetbrains-kotlin-serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "jetbrains-kotlin" } 38 | jetbrains-kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "jetbrains-kotlin" } 39 | jetbrains-kotlinx-binary-compatibility-validator = { module = "org.jetbrains.kotlinx:binary-compatibility-validator", version.ref = "bcv" } 40 | jetbrains-kotlinx-collections-immutable = { module = "org.jetbrains.kotlinx:kotlinx-collections-immutable", version.ref = "jetbrains-kotlinx-collections-immutable" } 41 | jetbrains-kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "jetbrains-kotlin-coroutines" } 42 | jetbrains-kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "jetbrains-kotlinx-datetime" } 43 | jetbrains-kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "jetbrains-kotlinx-serialization" } 44 | librarian = "com.gradleup.librarian:librarian-gradle-plugin:0.0.7" 45 | lyricist = { module = "cafe.adriel.lyricist:lyricist", version.ref = "lyricist" } 46 | touchlab-kermit = { module = "co.touchlab:kermit", version.ref = "touchlab-kermit" } 47 | vanniktech-multiplatform-locale = { module = "com.vanniktech:multiplatform-locale", version.ref = "multiplatform-locale" } 48 | version-catalog-update = "nl.littlerobots.version-catalog-update:nl.littlerobots.version-catalog-update.gradle.plugin:0.8.5" 49 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC2039,SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC2039,SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /librarian.root.properties: -------------------------------------------------------------------------------- 1 | java.compatibility=17 2 | kotlin.compatibility=2.0.0 3 | 4 | kdoc.olderVersions= 5 | kdoc.artifactId=kdoc 6 | 7 | sonatype.backend=S01 8 | sonatype.release=Manual 9 | 10 | pom.groupId=io.openfeedback 11 | pom.version=1.0.0-alpha.5-SNAPSHOT 12 | pom.description=openfeedback-sdk-kotlin 13 | pom.vcsUrl=https://github.com/paug/openfeedback-sdk-kotlin 14 | pom.developer=openfeedback-sdk-kotlin authors 15 | pom.license=MIT License 16 | -------------------------------------------------------------------------------- /openfeedback-m3/api/openfeedback-m3.api: -------------------------------------------------------------------------------- 1 | public final class io/openfeedback/m3/CommentInputKt { 2 | public static final fun CommentInput (Ljava/lang/String;Lkotlin/jvm/functions/Function1;Lkotlin/jvm/functions/Function0;Landroidx/compose/ui/Modifier;ZLandroidx/compose/runtime/Composer;II)V 3 | } 4 | 5 | public final class io/openfeedback/m3/CommentKt { 6 | public static final fun Comment-njYn8yo (Lio/openfeedback/ui/models/UIComment;Landroidx/compose/ui/Modifier;JJLandroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/text/TextStyle;Landroidx/compose/ui/graphics/Shape;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V 7 | } 8 | 9 | public final class io/openfeedback/m3/ComposableSingletons$CommentInputKt { 10 | public static final field INSTANCE Lio/openfeedback/m3/ComposableSingletons$CommentInputKt; 11 | public fun ()V 12 | public final fun getLambda-1$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 13 | public final fun getLambda-2$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 14 | } 15 | 16 | public final class io/openfeedback/m3/ComposableSingletons$CommentInputPreviewKt { 17 | public static final field INSTANCE Lio/openfeedback/m3/ComposableSingletons$CommentInputPreviewKt; 18 | public fun ()V 19 | public final fun getLambda-1$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 20 | } 21 | 22 | public final class io/openfeedback/m3/ComposableSingletons$CommentItemsPreviewKt { 23 | public static final field INSTANCE Lio/openfeedback/m3/ComposableSingletons$CommentItemsPreviewKt; 24 | public fun ()V 25 | public final fun getLambda-1$openfeedback_m3_release ()Lkotlin/jvm/functions/Function3; 26 | public final fun getLambda-2$openfeedback_m3_release ()Lkotlin/jvm/functions/Function4; 27 | public final fun getLambda-3$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 28 | } 29 | 30 | public final class io/openfeedback/m3/ComposableSingletons$CommentPreviewKt { 31 | public static final field INSTANCE Lio/openfeedback/m3/ComposableSingletons$CommentPreviewKt; 32 | public fun ()V 33 | public final fun getLambda-1$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 34 | } 35 | 36 | public final class io/openfeedback/m3/ComposableSingletons$FeedbackNotReadyKt { 37 | public static final field INSTANCE Lio/openfeedback/m3/ComposableSingletons$FeedbackNotReadyKt; 38 | public fun ()V 39 | public final fun getLambda-1$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 40 | } 41 | 42 | public final class io/openfeedback/m3/ComposableSingletons$OpenFeedbackLayoutPreviewKt { 43 | public static final field INSTANCE Lio/openfeedback/m3/ComposableSingletons$OpenFeedbackLayoutPreviewKt; 44 | public fun ()V 45 | public final fun getLambda-1$openfeedback_m3_release ()Lkotlin/jvm/functions/Function4; 46 | public final fun getLambda-2$openfeedback_m3_release ()Lkotlin/jvm/functions/Function3; 47 | public final fun getLambda-3$openfeedback_m3_release ()Lkotlin/jvm/functions/Function4; 48 | public final fun getLambda-4$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 49 | } 50 | 51 | public final class io/openfeedback/m3/ComposableSingletons$VoteCardPreviewKt { 52 | public static final field INSTANCE Lio/openfeedback/m3/ComposableSingletons$VoteCardPreviewKt; 53 | public fun ()V 54 | public final fun getLambda-1$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 55 | } 56 | 57 | public final class io/openfeedback/m3/ComposableSingletons$VoteItemsPreviewKt { 58 | public static final field INSTANCE Lio/openfeedback/m3/ComposableSingletons$VoteItemsPreviewKt; 59 | public fun ()V 60 | public final fun getLambda-1$openfeedback_m3_release ()Lkotlin/jvm/functions/Function4; 61 | public final fun getLambda-2$openfeedback_m3_release ()Lkotlin/jvm/functions/Function2; 62 | } 63 | 64 | public final class io/openfeedback/m3/FeedbackNotReadyKt { 65 | public static final fun FeedbackNotReady-eopBjH0 (Landroidx/compose/ui/Modifier;JJLandroidx/compose/foundation/layout/PaddingValues;Landroidx/compose/runtime/Composer;II)V 66 | } 67 | 68 | public final class io/openfeedback/m3/LoadingKt { 69 | public static final fun Loading (Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;II)V 70 | } 71 | 72 | public final class io/openfeedback/m3/OpenFeedbackLayoutKt { 73 | public static final fun OpenFeedbackLayout (Lio/openfeedback/ui/models/UISessionFeedback;Landroidx/compose/ui/Modifier;IZLandroidx/compose/foundation/layout/Arrangement$Horizontal;Landroidx/compose/foundation/layout/Arrangement$Vertical;Lkotlin/jvm/functions/Function4;Lkotlin/jvm/functions/Function3;Lkotlin/jvm/functions/Function4;Landroidx/compose/runtime/Composer;II)V 74 | } 75 | 76 | public final class io/openfeedback/m3/VoteCardKt { 77 | public static final fun VoteCard-vRFhKjU (Lio/openfeedback/ui/models/UIVoteItem;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/text/TextStyle;JJLandroidx/compose/ui/graphics/Shape;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V 78 | } 79 | 80 | -------------------------------------------------------------------------------- /openfeedback-m3/api/openfeedback-m3.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [iosArm64, iosSimulatorArm64, iosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | final fun io.openfeedback.m3/Comment(io.openfeedback.ui.models/UIComment, androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.text/TextStyle?, androidx.compose.ui.text/TextStyle?, androidx.compose.ui.graphics/Shape?, kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.openfeedback.m3/Comment|Comment(io.openfeedback.ui.models.UIComment;androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.text.TextStyle?;androidx.compose.ui.text.TextStyle?;androidx.compose.ui.graphics.Shape?;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] 10 | final fun io.openfeedback.m3/CommentInput(kotlin/String, kotlin/Function1, kotlin/Function0, androidx.compose.ui/Modifier?, kotlin/Boolean, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.openfeedback.m3/CommentInput|CommentInput(kotlin.String;kotlin.Function1;kotlin.Function0;androidx.compose.ui.Modifier?;kotlin.Boolean;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] 11 | final fun io.openfeedback.m3/FeedbackNotReady(androidx.compose.ui/Modifier?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.foundation.layout/PaddingValues?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.openfeedback.m3/FeedbackNotReady|FeedbackNotReady(androidx.compose.ui.Modifier?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.foundation.layout.PaddingValues?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] 12 | final fun io.openfeedback.m3/Loading(androidx.compose.ui/Modifier?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.openfeedback.m3/Loading|Loading(androidx.compose.ui.Modifier?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] 13 | final fun io.openfeedback.m3/OpenFeedbackLayout(io.openfeedback.ui.models/UISessionFeedback, androidx.compose.ui/Modifier?, kotlin/Int, kotlin/Boolean, androidx.compose.foundation.layout/Arrangement.Horizontal?, androidx.compose.foundation.layout/Arrangement.Vertical?, kotlin/Function4, kotlin/Function3, kotlin/Function4, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.openfeedback.m3/OpenFeedbackLayout|OpenFeedbackLayout(io.openfeedback.ui.models.UISessionFeedback;androidx.compose.ui.Modifier?;kotlin.Int;kotlin.Boolean;androidx.compose.foundation.layout.Arrangement.Horizontal?;androidx.compose.foundation.layout.Arrangement.Vertical?;kotlin.Function4;kotlin.Function3;kotlin.Function4;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] 14 | final fun io.openfeedback.m3/VoteCard(io.openfeedback.ui.models/UIVoteItem, androidx.compose.ui/Modifier?, androidx.compose.ui.text/TextStyle?, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Shape?, kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.openfeedback.m3/VoteCard|VoteCard(io.openfeedback.ui.models.UIVoteItem;androidx.compose.ui.Modifier?;androidx.compose.ui.text.TextStyle?;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Shape?;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] 15 | -------------------------------------------------------------------------------- /openfeedback-m3/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.plugin.serialization") 4 | } 5 | 6 | library( 7 | namespace = "io.openfeedback.m3", 8 | compose = true, 9 | ) { kotlinMultiplatformExtension -> 10 | kotlinMultiplatformExtension.sourceSets { 11 | findByName("commonMain")!!.apply { 12 | dependencies { 13 | api(projects.openfeedbackResources) 14 | api(projects.openfeedbackUiModels) 15 | 16 | implementation(kotlinMultiplatformExtension.compose.material3) 17 | implementation(kotlinMultiplatformExtension.compose.materialIconsExtended) 18 | } 19 | } 20 | val androidMain by getting { 21 | dependencies { 22 | with (kotlinMultiplatformExtension) { 23 | implementation(compose.uiTooling) 24 | } 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentInputPreview.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.tooling.preview.Preview 6 | 7 | @Preview 8 | @Composable 9 | private fun CommentInputPreview() { 10 | MaterialTheme { 11 | CommentInput( 12 | value = "My comment", 13 | onValueChange = {}, 14 | onSubmit = {} 15 | ) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentItemsPreview.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.tooling.preview.Preview 6 | import io.openfeedback.ui.models.UIComment 7 | import io.openfeedback.ui.models.UIDot 8 | import kotlinx.collections.immutable.persistentListOf 9 | 10 | @Preview 11 | @Composable 12 | private fun CommentItemsPreview() { 13 | MaterialTheme { 14 | CommentItems( 15 | comments = persistentListOf( 16 | UIComment( 17 | id = "", 18 | message = "Nice comment", 19 | createdAt = "08 August 2023", 20 | upVotes = 8, 21 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 22 | votedByUser = true, 23 | fromUser = false 24 | ), 25 | UIComment( 26 | id = "", 27 | message = "Another comment", 28 | createdAt = "08 August 2023", 29 | upVotes = 0, 30 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 31 | votedByUser = true, 32 | fromUser = false 33 | ) 34 | ), 35 | commentInput = { 36 | CommentInput(value = "", onValueChange = {}, onSubmit = {}) 37 | }, 38 | comment = { 39 | Comment(comment = it, onClick = {}) 40 | } 41 | ) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentPreview.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.tooling.preview.Preview 6 | import io.openfeedback.ui.models.UIComment 7 | import io.openfeedback.ui.models.UIDot 8 | import kotlinx.collections.immutable.persistentListOf 9 | 10 | @Preview 11 | @Composable 12 | private fun CommentPreview() { 13 | MaterialTheme { 14 | Comment( 15 | comment = UIComment( 16 | id = "", 17 | message = "Super talk and great speakers!", 18 | createdAt = "08 August 2023", 19 | upVotes = 8, 20 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 21 | votedByUser = true, 22 | fromUser = false 23 | ), 24 | onClick = {} 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/LoadingPreview.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.tooling.preview.Preview 5 | 6 | @Preview 7 | @Composable 8 | internal fun LoadingPreview() { 9 | Loading() 10 | } 11 | -------------------------------------------------------------------------------- /openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/OpenFeedbackLayoutPreview.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | import io.openfeedback.ui.models.UIComment 13 | import io.openfeedback.ui.models.UIDot 14 | import io.openfeedback.ui.models.UISessionFeedback 15 | import io.openfeedback.ui.models.UIVoteItem 16 | import kotlinx.collections.immutable.persistentListOf 17 | 18 | @OptIn(ExperimentalMaterial3Api::class) 19 | @Preview 20 | @Composable 21 | private fun OpenFeedbackLayoutPreview() { 22 | MaterialTheme { 23 | OpenFeedbackLayout( 24 | sessionFeedback = UISessionFeedback( 25 | comments = persistentListOf( 26 | UIComment( 27 | id = "", 28 | message = "Nice comment", 29 | createdAt = "08 August 2023", 30 | upVotes = 8, 31 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 32 | votedByUser = true, 33 | fromUser = false 34 | ), 35 | UIComment( 36 | id = "", 37 | message = "Another one", 38 | createdAt = "08 August 2023", 39 | upVotes = 0, 40 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 41 | votedByUser = true, 42 | fromUser = false 43 | ) 44 | ), 45 | voteItems = persistentListOf( 46 | UIVoteItem( 47 | id = "", 48 | text = "Fun", 49 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 50 | votedByUser = true 51 | ), 52 | UIVoteItem( 53 | id = "", 54 | text = "Fun", 55 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 56 | votedByUser = true 57 | ) 58 | ), 59 | colors = persistentListOf() 60 | ), 61 | horizontalArrangement = Arrangement.spacedBy(8.dp), 62 | verticalArrangement = Arrangement.spacedBy(8.dp), 63 | commentInput = { 64 | CommentInput(value = "", onValueChange = {}, onSubmit = {}) 65 | }, 66 | comment = { Comment(comment = it, onClick = {}) }, 67 | ) { 68 | VoteCard( 69 | voteModel = it, 70 | onClick = {}, 71 | modifier = Modifier 72 | .height(100.dp) 73 | .fillMaxWidth() 74 | ) 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/VoteCardPreview.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.tooling.preview.Preview 9 | import androidx.compose.ui.unit.dp 10 | import io.openfeedback.ui.models.UIDot 11 | import io.openfeedback.ui.models.UIVoteItem 12 | import kotlinx.collections.immutable.persistentListOf 13 | 14 | @OptIn(ExperimentalMaterial3Api::class) 15 | @Preview 16 | @Composable 17 | private fun VoteCardPreview() { 18 | MaterialTheme { 19 | VoteCard( 20 | voteModel = UIVoteItem( 21 | id = "", 22 | text = "Fun", 23 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 24 | votedByUser = true 25 | ), 26 | onClick = {}, 27 | modifier = Modifier.size(height = 100.dp, width = 200.dp) 28 | ) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/VoteItemsPreview.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.tooling.preview.Preview 11 | import androidx.compose.ui.unit.dp 12 | import io.openfeedback.ui.models.UIDot 13 | import io.openfeedback.ui.models.UIVoteItem 14 | import kotlinx.collections.immutable.persistentListOf 15 | 16 | @OptIn(ExperimentalMaterial3Api::class) 17 | @Preview 18 | @Composable 19 | private fun VoteItemsPreview() { 20 | MaterialTheme { 21 | VoteItems( 22 | voteItems = persistentListOf( 23 | UIVoteItem( 24 | id = "", 25 | text = "Fun", 26 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 27 | votedByUser = true 28 | ), 29 | UIVoteItem( 30 | id = "", 31 | text = "Fun", 32 | dots = persistentListOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), 33 | votedByUser = true 34 | ) 35 | ), 36 | horizontalArrangement = Arrangement.spacedBy(8.dp), 37 | verticalArrangement = Arrangement.spacedBy(8.dp), 38 | content = { 39 | VoteCard( 40 | voteModel = it, 41 | onClick = {}, 42 | modifier = Modifier 43 | .height(100.dp) 44 | .fillMaxWidth() 45 | ) 46 | } 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/Comment.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxWidth 7 | import androidx.compose.foundation.layout.padding 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.material3.Text 11 | import androidx.compose.material3.contentColorFor 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.graphics.Shape 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.compose.ui.unit.dp 18 | import io.openfeedback.resources.LocalStrings 19 | import io.openfeedback.ui.models.UIComment 20 | 21 | @Composable 22 | fun Comment( 23 | comment: UIComment, 24 | modifier: Modifier = Modifier, 25 | containerColor: Color = MaterialTheme.colorScheme.surfaceVariant, 26 | contentColor: Color = contentColorFor(backgroundColor = containerColor), 27 | style: TextStyle = MaterialTheme.typography.bodyMedium, 28 | subStyle: TextStyle = MaterialTheme.typography.labelMedium, 29 | shape: Shape = MaterialTheme.shapes.medium, 30 | onClick: (UIComment) -> Unit 31 | ) { 32 | Surface( 33 | color = containerColor, 34 | contentColor = contentColor, 35 | shape = shape, 36 | modifier = modifier, 37 | onClick = { onClick(comment) } 38 | ) { 39 | Column( 40 | modifier = Modifier 41 | .fillMaxWidth() 42 | .drawDots(comment.dots) 43 | .padding(16.dp), 44 | verticalArrangement = Arrangement.spacedBy(16.dp) 45 | ) { 46 | Text(text = comment.message, style = style) 47 | Row( 48 | horizontalArrangement = Arrangement.SpaceBetween, 49 | modifier = Modifier.fillMaxWidth() 50 | ) { 51 | Text( 52 | text = LocalStrings.current.strings.comments.nbVotes(comment.upVotes), 53 | color = contentColor.copy(alpha = .7f), 54 | style = subStyle 55 | ) 56 | Text( 57 | text = comment.createdAt + (if (comment.fromUser) LocalStrings.current.strings.fromYou else ""), 58 | color = contentColor.copy(alpha = .7f), 59 | style = subStyle 60 | ) 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/CommentInput.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.text.KeyboardActions 4 | import androidx.compose.foundation.text.KeyboardOptions 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.outlined.Send 7 | import androidx.compose.material3.Icon 8 | import androidx.compose.material3.IconButton 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.TextField 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.text.input.ImeAction 14 | import io.openfeedback.resources.LocalStrings 15 | 16 | @Composable 17 | fun CommentInput( 18 | value: String, 19 | onValueChange: (String) -> Unit, 20 | onSubmit: () -> Unit, 21 | modifier: Modifier = Modifier, 22 | enabled: Boolean = true 23 | ) { 24 | TextField( 25 | value = value, 26 | onValueChange = onValueChange, 27 | modifier = modifier, 28 | label = { Text(text = LocalStrings.current.strings.comments.titleInput) }, 29 | trailingIcon = { 30 | IconButton(onClick = onSubmit) { 31 | Icon( 32 | imageVector = Icons.Outlined.Send, 33 | contentDescription = LocalStrings.current.strings.comments.actionSend 34 | ) 35 | } 36 | }, 37 | enabled = enabled, 38 | keyboardActions = KeyboardActions(onDone = { onSubmit() }), 39 | keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), 40 | maxLines = 5 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/CommentItems.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Text 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import io.openfeedback.resources.LocalStrings 12 | import io.openfeedback.ui.models.UIComment 13 | import kotlinx.collections.immutable.ImmutableList 14 | 15 | @Composable 16 | internal fun CommentItems( 17 | comments: ImmutableList, 18 | modifier: Modifier = Modifier, 19 | verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), 20 | commentInput: @Composable ColumnScope.() -> Unit, 21 | comment: @Composable ColumnScope.(UIComment) -> Unit 22 | ) { 23 | Column( 24 | modifier = modifier, 25 | verticalArrangement = verticalArrangement 26 | ) { 27 | Text( 28 | text = LocalStrings.current.strings.comments.titleSection, 29 | style = MaterialTheme.typography.titleMedium 30 | ) 31 | commentInput() 32 | comments.forEach { uiComment -> 33 | comment(uiComment) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/DotModifier.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.draw.drawBehind 5 | import androidx.compose.ui.geometry.Offset 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.graphics.drawscope.Fill 8 | import androidx.compose.ui.unit.dp 9 | import io.openfeedback.ui.models.UIDot 10 | 11 | internal fun Modifier.drawDots(dots: List): Modifier = drawBehind { 12 | dots.forEach { dot -> 13 | val offset = Offset(x = this.size.width * dot.x, y = this.size.height * dot.y) 14 | drawCircle( 15 | color = Color( 16 | red = dot.color.substring(0, 2).toInt(16), 17 | green = dot.color.substring(2, 4).toInt(16), 18 | blue = dot.color.substring(4, 6).toInt(16), 19 | alpha = 255 / 3 20 | ), 21 | radius = 30.dp.value, 22 | center = offset, 23 | style = Fill 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/FeedbackNotReady.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.SentimentDissatisfied 15 | import androidx.compose.material.icons.filled.SentimentNeutral 16 | import androidx.compose.material.icons.filled.SentimentSatisfied 17 | import androidx.compose.material.icons.filled.SentimentVeryDissatisfied 18 | import androidx.compose.material.icons.filled.SentimentVerySatisfied 19 | import androidx.compose.material3.Icon 20 | import androidx.compose.material3.LocalContentColor 21 | import androidx.compose.material3.MaterialTheme 22 | import androidx.compose.material3.Surface 23 | import androidx.compose.material3.Text 24 | import androidx.compose.material3.contentColorFor 25 | import androidx.compose.runtime.Composable 26 | import androidx.compose.runtime.CompositionLocalProvider 27 | import androidx.compose.ui.Alignment 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.Color 30 | import androidx.compose.ui.unit.dp 31 | import io.openfeedback.resources.LocalStrings 32 | 33 | @Composable 34 | fun FeedbackNotReady( 35 | modifier: Modifier = Modifier, 36 | containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, 37 | contentColor: Color = contentColorFor(containerColor), 38 | contentPadding: PaddingValues = PaddingValues(32.dp) 39 | ) { 40 | Surface( 41 | modifier = modifier, 42 | color = containerColor, 43 | contentColor = contentColor 44 | ) { 45 | Column( 46 | modifier = Modifier 47 | .fillMaxWidth() 48 | .padding(contentPadding), 49 | ) { 50 | Box( 51 | modifier = Modifier.fillMaxWidth(), 52 | contentAlignment = Alignment.Center 53 | ) { 54 | Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { 55 | CompositionLocalProvider(LocalContentColor provides contentColor.copy(alpha = 0.38f)) { 56 | Icon( 57 | imageVector = Icons.Default.SentimentVerySatisfied, 58 | contentDescription = null, 59 | modifier = Modifier.size(32.dp) 60 | ) 61 | Icon( 62 | imageVector = Icons.Default.SentimentSatisfied, 63 | contentDescription = null, 64 | modifier = Modifier.size(32.dp) 65 | ) 66 | Icon( 67 | imageVector = Icons.Default.SentimentNeutral, 68 | contentDescription = null, 69 | modifier = Modifier.size(32.dp) 70 | ) 71 | Icon( 72 | imageVector = Icons.Default.SentimentDissatisfied, 73 | contentDescription = null, 74 | modifier = Modifier.size(32.dp) 75 | ) 76 | Icon( 77 | imageVector = Icons.Default.SentimentVeryDissatisfied, 78 | contentDescription = null, 79 | modifier = Modifier.size(32.dp) 80 | ) 81 | } 82 | } 83 | } 84 | Spacer(modifier = Modifier.height(16.dp)) 85 | Text( 86 | text = LocalStrings.current.strings.notReady.title, 87 | style = MaterialTheme.typography.titleMedium 88 | ) 89 | Text(text = LocalStrings.current.strings.notReady.description) 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/Loading.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | 10 | @Composable 11 | fun Loading(modifier: Modifier = Modifier) { 12 | Box( 13 | modifier = modifier.fillMaxSize(), 14 | contentAlignment = Alignment.Center 15 | ) { 16 | CircularProgressIndicator() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/OpenFeedbackLayout.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.ColumnScope 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.material3.ExperimentalMaterial3Api 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | import io.openfeedback.ui.models.UIComment 14 | import io.openfeedback.ui.models.UISessionFeedback 15 | import io.openfeedback.ui.models.UIVoteItem 16 | 17 | /** 18 | * Stateless OpenFeedback component to display vote items, text field to enter a new comment 19 | * and display comments of a session. 20 | * 21 | * @param sessionFeedback Ui model for vote items, new comment value and comments. 22 | * @param modifier The modifier to be applied to the component. 23 | * @param columnCount Number of column to display for vote items. 24 | * @param displayComments Flag to display comments or not. 25 | * @param horizontalArrangement The horizontal arrangement of the vote items layout. 26 | * @param verticalArrangement The vertical arrangement to display between every column. 27 | * @param comment API slot for the list of comments. 28 | * @param commentInput API slot for the text field to create new comment. 29 | * @param voteItem API slot for vote items. 30 | */ 31 | @OptIn(ExperimentalMaterial3Api::class) 32 | @Composable 33 | fun OpenFeedbackLayout( 34 | sessionFeedback: UISessionFeedback, 35 | modifier: Modifier = Modifier, 36 | columnCount: Int = 2, 37 | displayComments: Boolean = true, 38 | horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(8.dp), 39 | verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(16.dp), 40 | comment: @Composable ColumnScope.(UIComment) -> Unit, 41 | commentInput: @Composable ColumnScope.() -> Unit, 42 | voteItem: @Composable ColumnScope.(UIVoteItem) -> Unit 43 | ) { 44 | Column( 45 | modifier = modifier, 46 | verticalArrangement = verticalArrangement 47 | ) { 48 | VoteItems( 49 | voteItems = sessionFeedback.voteItems, 50 | columnCount = columnCount, 51 | horizontalArrangement = horizontalArrangement, 52 | verticalArrangement = verticalArrangement, 53 | content = voteItem 54 | ) 55 | if (displayComments) { 56 | CommentItems( 57 | comments = sessionFeedback.comments, 58 | commentInput = commentInput, 59 | comment = comment 60 | ) 61 | } 62 | Box( 63 | modifier = Modifier.fillMaxWidth(), 64 | contentAlignment = Alignment.Center 65 | ) { 66 | PoweredBy() 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/PoweredBy.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.height 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.graphics.luminance 14 | import androidx.compose.ui.semantics.contentDescription 15 | import androidx.compose.ui.semantics.semantics 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.compose.ui.unit.dp 18 | import io.openfeedback.resources.LocalStrings 19 | import io.openfeedback.resources.Res 20 | import io.openfeedback.resources.openfeedback_dark 21 | import io.openfeedback.resources.openfeedback_light 22 | import org.jetbrains.compose.resources.painterResource 23 | 24 | @Composable 25 | internal fun PoweredBy( 26 | modifier: Modifier = Modifier, 27 | style: TextStyle = MaterialTheme.typography.bodyMedium, 28 | color: Color = MaterialTheme.colorScheme.onBackground 29 | ) { 30 | val logo = 31 | if (MaterialTheme.colorScheme.background.luminance() > 0.5) Res.drawable.openfeedback_light 32 | else Res.drawable.openfeedback_dark 33 | val poweredBy = LocalStrings.current.strings.poweredBy 34 | Row( 35 | modifier = modifier.semantics(mergeDescendants = true) { 36 | contentDescription = "$poweredBy Openfeedback" 37 | }, 38 | horizontalArrangement = Arrangement.spacedBy(4.dp), 39 | verticalAlignment = Alignment.Top 40 | ) { 41 | Text(text = poweredBy, style = style, color = color) 42 | Image( 43 | painter = painterResource(logo), 44 | contentDescription = null, 45 | modifier = Modifier.height(style.fontSize.value.dp + 13.dp) 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/VoteCard.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.ExperimentalMaterial3Api 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Surface 9 | import androidx.compose.material3.Text 10 | import androidx.compose.material3.contentColorFor 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.Shape 15 | import androidx.compose.ui.text.TextStyle 16 | import androidx.compose.ui.unit.dp 17 | import io.openfeedback.ui.models.UIVoteItem 18 | 19 | @ExperimentalMaterial3Api 20 | @Composable 21 | fun VoteCard( 22 | voteModel: UIVoteItem, 23 | modifier: Modifier = Modifier, 24 | style: TextStyle = MaterialTheme.typography.bodyMedium, 25 | backgroundColor: Color = MaterialTheme.colorScheme.surface, 26 | contentColor: Color = contentColorFor(backgroundColor = backgroundColor), 27 | shape: Shape = MaterialTheme.shapes.medium, 28 | onClick: (voteItem: UIVoteItem) -> Unit 29 | ) { 30 | val border = if (voteModel.votedByUser) 4.dp else 1.dp 31 | Surface( 32 | shape = shape, 33 | border = BorderStroke(border, contentColor.copy(alpha = .2f)), 34 | color = backgroundColor, 35 | modifier = modifier, 36 | onClick = { onClick(voteModel) } 37 | ) { 38 | Box( 39 | modifier = Modifier.drawDots(voteModel.dots) 40 | ) { 41 | Text( 42 | text = voteModel.text, 43 | style = style, 44 | color = contentColor, 45 | modifier = Modifier.padding(10.dp), 46 | ) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/VoteItems.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.m3 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import io.openfeedback.ui.models.UIVoteItem 12 | import kotlinx.collections.immutable.ImmutableList 13 | 14 | @ExperimentalMaterial3Api 15 | @Composable 16 | internal fun VoteItems( 17 | voteItems: ImmutableList, 18 | modifier: Modifier = Modifier, 19 | columnCount: Int = 2, 20 | horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(8.dp), 21 | verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), 22 | content: @Composable ColumnScope.(UIVoteItem) -> Unit 23 | ) { 24 | Row( 25 | modifier = modifier, 26 | horizontalArrangement = horizontalArrangement 27 | ) { 28 | 0.until(columnCount).forEach { column -> 29 | Column( 30 | verticalArrangement = verticalArrangement, 31 | modifier = Modifier.weight(1f) 32 | ) { 33 | voteItems 34 | .filterIndexed { index, _ -> 35 | index % columnCount == column 36 | } 37 | .forEach { voteItem -> 38 | content(voteItem) 39 | } 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /openfeedback-resources/api/openfeedback-resources.api: -------------------------------------------------------------------------------- 1 | public final class io/openfeedback/resources/ActualResourceCollectorsKt { 2 | public static final fun getAllDrawableResources (Lio/openfeedback/resources/Res;)Ljava/util/Map; 3 | public static final fun getAllFontResources (Lio/openfeedback/resources/Res;)Ljava/util/Map; 4 | public static final fun getAllPluralStringResources (Lio/openfeedback/resources/Res;)Ljava/util/Map; 5 | public static final fun getAllStringArrayResources (Lio/openfeedback/resources/Res;)Ljava/util/Map; 6 | public static final fun getAllStringResources (Lio/openfeedback/resources/Res;)Ljava/util/Map; 7 | } 8 | 9 | public final class io/openfeedback/resources/CommentStrings { 10 | public static final field $stable I 11 | public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)V 12 | public final fun component1 ()Ljava/lang/String; 13 | public final fun component2 ()Ljava/lang/String; 14 | public final fun component3 ()Ljava/lang/String; 15 | public final fun component4 ()Lkotlin/jvm/functions/Function1; 16 | public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;)Lio/openfeedback/resources/CommentStrings; 17 | public static synthetic fun copy$default (Lio/openfeedback/resources/CommentStrings;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/openfeedback/resources/CommentStrings; 18 | public fun equals (Ljava/lang/Object;)Z 19 | public final fun getActionSend ()Ljava/lang/String; 20 | public final fun getNbVotes ()Lkotlin/jvm/functions/Function1; 21 | public final fun getTitleInput ()Ljava/lang/String; 22 | public final fun getTitleSection ()Ljava/lang/String; 23 | public fun hashCode ()I 24 | public fun toString ()Ljava/lang/String; 25 | } 26 | 27 | public final class io/openfeedback/resources/Drawable0_commonMainKt { 28 | public static final fun getOpenfeedback_dark (Lio/openfeedback/resources/Res$drawable;)Lorg/jetbrains/compose/resources/DrawableResource; 29 | public static final fun getOpenfeedback_light (Lio/openfeedback/resources/Res$drawable;)Lorg/jetbrains/compose/resources/DrawableResource; 30 | } 31 | 32 | public final class io/openfeedback/resources/LocalStringsKt { 33 | public static final fun getLocalStrings ()Landroidx/compose/runtime/ProvidableCompositionLocal; 34 | } 35 | 36 | public final class io/openfeedback/resources/NotReadyStrings { 37 | public static final field $stable I 38 | public fun (Ljava/lang/String;Ljava/lang/String;)V 39 | public final fun component1 ()Ljava/lang/String; 40 | public final fun component2 ()Ljava/lang/String; 41 | public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/openfeedback/resources/NotReadyStrings; 42 | public static synthetic fun copy$default (Lio/openfeedback/resources/NotReadyStrings;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/openfeedback/resources/NotReadyStrings; 43 | public fun equals (Ljava/lang/Object;)Z 44 | public final fun getDescription ()Ljava/lang/String; 45 | public final fun getTitle ()Ljava/lang/String; 46 | public fun hashCode ()I 47 | public fun toString ()Ljava/lang/String; 48 | } 49 | 50 | public final class io/openfeedback/resources/Res { 51 | public static final field $stable I 52 | public static final field INSTANCE Lio/openfeedback/resources/Res; 53 | public final fun getUri (Ljava/lang/String;)Ljava/lang/String; 54 | public final fun readBytes (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 55 | } 56 | 57 | public final class io/openfeedback/resources/Res$array { 58 | public static final field $stable I 59 | public static final field INSTANCE Lio/openfeedback/resources/Res$array; 60 | } 61 | 62 | public final class io/openfeedback/resources/Res$drawable { 63 | public static final field $stable I 64 | public static final field INSTANCE Lio/openfeedback/resources/Res$drawable; 65 | } 66 | 67 | public final class io/openfeedback/resources/Res$font { 68 | public static final field $stable I 69 | public static final field INSTANCE Lio/openfeedback/resources/Res$font; 70 | } 71 | 72 | public final class io/openfeedback/resources/Res$plurals { 73 | public static final field $stable I 74 | public static final field INSTANCE Lio/openfeedback/resources/Res$plurals; 75 | } 76 | 77 | public final class io/openfeedback/resources/Res$string { 78 | public static final field $stable I 79 | public static final field INSTANCE Lio/openfeedback/resources/Res$string; 80 | } 81 | 82 | public final class io/openfeedback/resources/Strings { 83 | public static final field $stable I 84 | public fun (Lio/openfeedback/resources/NotReadyStrings;Lio/openfeedback/resources/CommentStrings;Ljava/lang/String;Ljava/lang/String;)V 85 | public final fun component1 ()Lio/openfeedback/resources/NotReadyStrings; 86 | public final fun component2 ()Lio/openfeedback/resources/CommentStrings; 87 | public final fun component3 ()Ljava/lang/String; 88 | public final fun component4 ()Ljava/lang/String; 89 | public final fun copy (Lio/openfeedback/resources/NotReadyStrings;Lio/openfeedback/resources/CommentStrings;Ljava/lang/String;Ljava/lang/String;)Lio/openfeedback/resources/Strings; 90 | public static synthetic fun copy$default (Lio/openfeedback/resources/Strings;Lio/openfeedback/resources/NotReadyStrings;Lio/openfeedback/resources/CommentStrings;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/openfeedback/resources/Strings; 91 | public fun equals (Ljava/lang/Object;)Z 92 | public final fun getComments ()Lio/openfeedback/resources/CommentStrings; 93 | public final fun getFromYou ()Ljava/lang/String; 94 | public final fun getNotReady ()Lio/openfeedback/resources/NotReadyStrings; 95 | public final fun getPoweredBy ()Ljava/lang/String; 96 | public fun hashCode ()I 97 | public fun toString ()Ljava/lang/String; 98 | } 99 | 100 | -------------------------------------------------------------------------------- /openfeedback-resources/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.plugin.serialization") 4 | id("org.jetbrains.compose") 5 | id("org.jetbrains.kotlin.plugin.compose") 6 | } 7 | 8 | library( 9 | namespace = "io.openfeedback.resources", 10 | ) { 11 | it.sourceSets { 12 | findByName("commonMain")!!.apply { 13 | dependencies { 14 | implementation(it.compose.ui) 15 | api(it.compose.components.resources) 16 | 17 | api(libs.lyricist) 18 | } 19 | } 20 | } 21 | } 22 | 23 | compose.resources { 24 | publicResClass = true 25 | packageOfResClass = "io.openfeedback.resources" 26 | generateResClass = always 27 | } 28 | -------------------------------------------------------------------------------- /openfeedback-resources/src/commonMain/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/openfeedback-resources/src/commonMain/.DS_Store -------------------------------------------------------------------------------- /openfeedback-resources/src/commonMain/composeResources/drawable/openfeedback_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/openfeedback-resources/src/commonMain/composeResources/drawable/openfeedback_dark.png -------------------------------------------------------------------------------- /openfeedback-resources/src/commonMain/composeResources/drawable/openfeedback_light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/openfeedback-resources/src/commonMain/composeResources/drawable/openfeedback_light.png -------------------------------------------------------------------------------- /openfeedback-resources/src/commonMain/kotlin/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/openfeedback-resources/src/commonMain/kotlin/.DS_Store -------------------------------------------------------------------------------- /openfeedback-resources/src/commonMain/kotlin/io/openfeedback/resources/EnStrings.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.resources 2 | 3 | import cafe.adriel.lyricist.LyricistStrings 4 | 5 | @LyricistStrings(languageTag = "en", default = true) 6 | internal val EnStrings = Strings( 7 | notReady = NotReadyStrings( 8 | title = "React online!", 9 | description = "A little more patience, and you'll be able to share your feedback when the session starts." 10 | ), 11 | comments = CommentStrings( 12 | titleSection = "Comments", 13 | titleInput = "Your comment", 14 | actionSend = "Submit comment", 15 | nbVotes = { nbVotes: Int -> "$nbVotes votes" } 16 | ), 17 | poweredBy = "Powered by", 18 | fromYou = ", from you" 19 | ) 20 | -------------------------------------------------------------------------------- /openfeedback-resources/src/commonMain/kotlin/io/openfeedback/resources/FrStrings.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.resources 2 | 3 | import cafe.adriel.lyricist.LyricistStrings 4 | 5 | @LyricistStrings(languageTag = "fr") 6 | internal val FrStrings = Strings( 7 | notReady = NotReadyStrings( 8 | title = "Réagissez en live !", 9 | description = "Encore un peu de patience, vous pourrez partagez vos feedbacks lorsque la session démarrera." 10 | ), 11 | comments = CommentStrings( 12 | titleSection = "Commentaires", 13 | titleInput = "Votre commentaire", 14 | actionSend = "Soumettre votre commentaire", 15 | nbVotes = { nbVotes: Int -> "$nbVotes votes" } 16 | ), 17 | poweredBy = "Proposé par", 18 | fromYou = ", de vous" 19 | ) 20 | -------------------------------------------------------------------------------- /openfeedback-resources/src/commonMain/kotlin/io/openfeedback/resources/LocalStrings.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.resources 2 | 3 | import androidx.compose.runtime.staticCompositionLocalOf 4 | import cafe.adriel.lyricist.Lyricist 5 | 6 | val LocalStrings = staticCompositionLocalOf { 7 | Lyricist( 8 | defaultLanguageTag = "en", 9 | translations = mapOf("en" to EnStrings, "fr" to FrStrings) 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /openfeedback-resources/src/commonMain/kotlin/io/openfeedback/resources/Strings.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.resources 2 | 3 | data class Strings( 4 | val notReady: NotReadyStrings, 5 | val comments: CommentStrings, 6 | val poweredBy: String, 7 | val fromYou: String 8 | ) 9 | 10 | data class NotReadyStrings( 11 | val title: String, 12 | val description: String 13 | ) 14 | 15 | data class CommentStrings( 16 | val titleSection: String, 17 | val titleInput: String, 18 | val actionSend: String, 19 | val nbVotes: (nbVotes: Int) -> String, 20 | ) 21 | -------------------------------------------------------------------------------- /openfeedback-ui-models/api/openfeedback-ui-models.api: -------------------------------------------------------------------------------- 1 | public final class io/openfeedback/ui/models/UIComment { 2 | public static final field $stable I 3 | public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlinx/collections/immutable/ImmutableList;ZZ)V 4 | public final fun component1 ()Ljava/lang/String; 5 | public final fun component2 ()Ljava/lang/String; 6 | public final fun component3 ()Ljava/lang/String; 7 | public final fun component4 ()I 8 | public final fun component5 ()Lkotlinx/collections/immutable/ImmutableList; 9 | public final fun component6 ()Z 10 | public final fun component7 ()Z 11 | public final fun copy (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlinx/collections/immutable/ImmutableList;ZZ)Lio/openfeedback/ui/models/UIComment; 12 | public static synthetic fun copy$default (Lio/openfeedback/ui/models/UIComment;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlinx/collections/immutable/ImmutableList;ZZILjava/lang/Object;)Lio/openfeedback/ui/models/UIComment; 13 | public fun equals (Ljava/lang/Object;)Z 14 | public final fun getCreatedAt ()Ljava/lang/String; 15 | public final fun getDots ()Lkotlinx/collections/immutable/ImmutableList; 16 | public final fun getFromUser ()Z 17 | public final fun getId ()Ljava/lang/String; 18 | public final fun getMessage ()Ljava/lang/String; 19 | public final fun getUpVotes ()I 20 | public final fun getVotedByUser ()Z 21 | public fun hashCode ()I 22 | public fun toString ()Ljava/lang/String; 23 | } 24 | 25 | public final class io/openfeedback/ui/models/UIDot { 26 | public static final field $stable I 27 | public fun (FFLjava/lang/String;)V 28 | public final fun component1 ()F 29 | public final fun component2 ()F 30 | public final fun component3 ()Ljava/lang/String; 31 | public final fun copy (FFLjava/lang/String;)Lio/openfeedback/ui/models/UIDot; 32 | public static synthetic fun copy$default (Lio/openfeedback/ui/models/UIDot;FFLjava/lang/String;ILjava/lang/Object;)Lio/openfeedback/ui/models/UIDot; 33 | public fun equals (Ljava/lang/Object;)Z 34 | public final fun getColor ()Ljava/lang/String; 35 | public final fun getX ()F 36 | public final fun getY ()F 37 | public fun hashCode ()I 38 | public fun toString ()Ljava/lang/String; 39 | } 40 | 41 | public final class io/openfeedback/ui/models/UISessionFeedback { 42 | public static final field $stable I 43 | public fun (Lkotlinx/collections/immutable/ImmutableList;Lkotlinx/collections/immutable/ImmutableList;Lkotlinx/collections/immutable/ImmutableList;)V 44 | public final fun component1 ()Lkotlinx/collections/immutable/ImmutableList; 45 | public final fun component2 ()Lkotlinx/collections/immutable/ImmutableList; 46 | public final fun component3 ()Lkotlinx/collections/immutable/ImmutableList; 47 | public final fun copy (Lkotlinx/collections/immutable/ImmutableList;Lkotlinx/collections/immutable/ImmutableList;Lkotlinx/collections/immutable/ImmutableList;)Lio/openfeedback/ui/models/UISessionFeedback; 48 | public static synthetic fun copy$default (Lio/openfeedback/ui/models/UISessionFeedback;Lkotlinx/collections/immutable/ImmutableList;Lkotlinx/collections/immutable/ImmutableList;Lkotlinx/collections/immutable/ImmutableList;ILjava/lang/Object;)Lio/openfeedback/ui/models/UISessionFeedback; 49 | public fun equals (Ljava/lang/Object;)Z 50 | public final fun getColors ()Lkotlinx/collections/immutable/ImmutableList; 51 | public final fun getComments ()Lkotlinx/collections/immutable/ImmutableList; 52 | public final fun getVoteItems ()Lkotlinx/collections/immutable/ImmutableList; 53 | public fun hashCode ()I 54 | public fun toString ()Ljava/lang/String; 55 | } 56 | 57 | public final class io/openfeedback/ui/models/UIVoteItem { 58 | public static final field $stable I 59 | public fun (Ljava/lang/String;Ljava/lang/String;Lkotlinx/collections/immutable/ImmutableList;Z)V 60 | public final fun component1 ()Ljava/lang/String; 61 | public final fun component2 ()Ljava/lang/String; 62 | public final fun component3 ()Lkotlinx/collections/immutable/ImmutableList; 63 | public final fun component4 ()Z 64 | public final fun copy (Ljava/lang/String;Ljava/lang/String;Lkotlinx/collections/immutable/ImmutableList;Z)Lio/openfeedback/ui/models/UIVoteItem; 65 | public static synthetic fun copy$default (Lio/openfeedback/ui/models/UIVoteItem;Ljava/lang/String;Ljava/lang/String;Lkotlinx/collections/immutable/ImmutableList;ZILjava/lang/Object;)Lio/openfeedback/ui/models/UIVoteItem; 66 | public fun equals (Ljava/lang/Object;)Z 67 | public final fun getDots ()Lkotlinx/collections/immutable/ImmutableList; 68 | public final fun getId ()Ljava/lang/String; 69 | public final fun getText ()Ljava/lang/String; 70 | public final fun getVotedByUser ()Z 71 | public fun hashCode ()I 72 | public fun toString ()Ljava/lang/String; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /openfeedback-ui-models/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.multiplatform") 4 | } 5 | 6 | library( 7 | namespace = "io.openfeedback.ui.models", 8 | compose = true, 9 | ) { kotlinMultiplatformExtension -> 10 | kotlinMultiplatformExtension.sourceSets { 11 | getByName("commonMain") { 12 | dependencies { 13 | implementation(kotlinMultiplatformExtension.compose.runtime) 14 | api(libs.vanniktech.multiplatform.locale) 15 | api(libs.jetbrains.kotlinx.collections.immutable) 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /openfeedback-ui-models/src/commonMain/kotlin/io/openfeedback/ui/models/UIComment.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.ui.models 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlinx.collections.immutable.ImmutableList 5 | 6 | @Immutable 7 | data class UIComment( 8 | val id: String, 9 | val message: String, 10 | val createdAt: String, 11 | val upVotes: Int, 12 | val dots: ImmutableList, 13 | val votedByUser: Boolean, 14 | val fromUser: Boolean 15 | ) 16 | -------------------------------------------------------------------------------- /openfeedback-ui-models/src/commonMain/kotlin/io/openfeedback/ui/models/UIDot.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.ui.models 2 | 3 | import androidx.compose.runtime.Immutable 4 | 5 | /** 6 | * @param x: the x coordinate between 0f and 1f 7 | * @param y: the y coordinate between 0f and 1f 8 | * @param color: the color as "rrggbb" 9 | */ 10 | @Immutable 11 | data class UIDot( 12 | val x: Float, 13 | val y: Float, 14 | val color: String 15 | ) 16 | -------------------------------------------------------------------------------- /openfeedback-ui-models/src/commonMain/kotlin/io/openfeedback/ui/models/UISessionFeedback.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.ui.models 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlinx.collections.immutable.ImmutableList 5 | 6 | @Immutable 7 | data class UISessionFeedback( 8 | val comments: ImmutableList, 9 | val voteItems: ImmutableList, 10 | val colors: ImmutableList, 11 | ) 12 | -------------------------------------------------------------------------------- /openfeedback-ui-models/src/commonMain/kotlin/io/openfeedback/ui/models/UIVoteItem.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.ui.models 2 | 3 | import androidx.compose.runtime.Immutable 4 | import kotlinx.collections.immutable.ImmutableList 5 | 6 | @Immutable 7 | data class UIVoteItem( 8 | val id: String, 9 | val text: String, 10 | val dots: ImmutableList, 11 | val votedByUser: Boolean 12 | ) 13 | -------------------------------------------------------------------------------- /openfeedback-viewmodel/api/openfeedback-viewmodel.api: -------------------------------------------------------------------------------- 1 | public final class io/openfeedback/OpenFeedbackKt { 2 | public static final fun OpenFeedback (Ljava/lang/String;Ljava/lang/String;Landroidx/compose/ui/Modifier;IZZLjava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function2;Lkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;II)V 3 | } 4 | 5 | public final class io/openfeedback/viewmodels/OpenFeedbackFirebaseConfig { 6 | public static final field $stable I 7 | public static final field Companion Lio/openfeedback/viewmodels/OpenFeedbackFirebaseConfig$Companion; 8 | public fun (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V 9 | public synthetic fun (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 10 | public final fun component1 ()Ljava/lang/Object; 11 | public final fun component2 ()Ljava/lang/String; 12 | public final fun component3 ()Ljava/lang/String; 13 | public final fun component4 ()Ljava/lang/String; 14 | public final fun component5 ()Ljava/lang/String; 15 | public final fun component6 ()Ljava/lang/String; 16 | public final fun copy (Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Lio/openfeedback/viewmodels/OpenFeedbackFirebaseConfig; 17 | public static synthetic fun copy$default (Lio/openfeedback/viewmodels/OpenFeedbackFirebaseConfig;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/openfeedback/viewmodels/OpenFeedbackFirebaseConfig; 18 | public fun equals (Ljava/lang/Object;)Z 19 | public final fun getApiKey ()Ljava/lang/String; 20 | public final fun getAppName ()Ljava/lang/String; 21 | public final fun getApplicationId ()Ljava/lang/String; 22 | public final fun getContext ()Ljava/lang/Object; 23 | public final fun getDatabaseUrl ()Ljava/lang/String; 24 | public final fun getProjectId ()Ljava/lang/String; 25 | public fun hashCode ()I 26 | public fun toString ()Ljava/lang/String; 27 | } 28 | 29 | public final class io/openfeedback/viewmodels/OpenFeedbackFirebaseConfig$Companion { 30 | public final fun default (Ljava/lang/Object;)Lio/openfeedback/viewmodels/OpenFeedbackFirebaseConfig; 31 | } 32 | 33 | public final class io/openfeedback/viewmodels/OpenFeedbackFirebaseConfigKt { 34 | public static final fun getFirebaseApp (Ljava/lang/String;)Ldev/gitlive/firebase/FirebaseApp; 35 | public static final fun initializeOpenFeedback (Lio/openfeedback/viewmodels/OpenFeedbackFirebaseConfig;)V 36 | } 37 | 38 | public abstract class io/openfeedback/viewmodels/OpenFeedbackUiState { 39 | public static final field $stable I 40 | } 41 | 42 | public final class io/openfeedback/viewmodels/OpenFeedbackUiState$Loading : io/openfeedback/viewmodels/OpenFeedbackUiState { 43 | public static final field $stable I 44 | public static final field INSTANCE Lio/openfeedback/viewmodels/OpenFeedbackUiState$Loading; 45 | public fun equals (Ljava/lang/Object;)Z 46 | public fun hashCode ()I 47 | public fun toString ()Ljava/lang/String; 48 | } 49 | 50 | public final class io/openfeedback/viewmodels/OpenFeedbackUiState$Success : io/openfeedback/viewmodels/OpenFeedbackUiState { 51 | public static final field $stable I 52 | public fun (Lio/openfeedback/ui/models/UISessionFeedback;)V 53 | public final fun getSession ()Lio/openfeedback/ui/models/UISessionFeedback; 54 | } 55 | 56 | public final class io/openfeedback/viewmodels/OpenFeedbackViewModel : androidx/lifecycle/ViewModel { 57 | public static final field $stable I 58 | public static final field Companion Lio/openfeedback/viewmodels/OpenFeedbackViewModel$Companion; 59 | public synthetic fun (Ldev/gitlive/firebase/FirebaseApp;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/internal/DefaultConstructorMarker;)V 60 | public final fun getUiState ()Lkotlinx/coroutines/flow/StateFlow; 61 | public final fun submitComment (Ljava/lang/String;)Lkotlinx/coroutines/Job; 62 | public final fun upVote (Lio/openfeedback/ui/models/UIComment;)Lkotlinx/coroutines/Job; 63 | public final fun vote (Lio/openfeedback/ui/models/UIVoteItem;)Lkotlinx/coroutines/Job; 64 | } 65 | 66 | public final class io/openfeedback/viewmodels/OpenFeedbackViewModel$Companion { 67 | public final fun provideFactory (Ldev/gitlive/firebase/FirebaseApp;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroidx/lifecycle/ViewModelProvider$Factory; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /openfeedback-viewmodel/api/openfeedback-viewmodel.klib.api: -------------------------------------------------------------------------------- 1 | // Klib ABI Dump 2 | // Targets: [iosArm64, iosSimulatorArm64, iosX64] 3 | // Rendering settings: 4 | // - Signature version: 2 5 | // - Show manifest properties: true 6 | // - Show declarations: true 7 | 8 | // Library unique name: 9 | final class io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig { // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig|null[0] 10 | constructor (kotlin/Any?, kotlin/String, kotlin/String, kotlin/String, kotlin/String, kotlin/String = ...) // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.|(kotlin.Any?;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String){}[0] 11 | 12 | final val apiKey // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.apiKey|{}apiKey[0] 13 | final fun (): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.apiKey.|(){}[0] 14 | final val appName // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.appName|{}appName[0] 15 | final fun (): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.appName.|(){}[0] 16 | final val applicationId // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.applicationId|{}applicationId[0] 17 | final fun (): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.applicationId.|(){}[0] 18 | final val context // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.context|{}context[0] 19 | final fun (): kotlin/Any? // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.context.|(){}[0] 20 | final val databaseUrl // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.databaseUrl|{}databaseUrl[0] 21 | final fun (): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.databaseUrl.|(){}[0] 22 | final val projectId // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.projectId|{}projectId[0] 23 | final fun (): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.projectId.|(){}[0] 24 | 25 | final fun component1(): kotlin/Any? // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.component1|component1(){}[0] 26 | final fun component2(): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.component2|component2(){}[0] 27 | final fun component3(): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.component3|component3(){}[0] 28 | final fun component4(): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.component4|component4(){}[0] 29 | final fun component5(): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.component5|component5(){}[0] 30 | final fun component6(): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.component6|component6(){}[0] 31 | final fun copy(kotlin/Any? = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ..., kotlin/String = ...): io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.copy|copy(kotlin.Any?;kotlin.String;kotlin.String;kotlin.String;kotlin.String;kotlin.String){}[0] 32 | final fun equals(kotlin/Any?): kotlin/Boolean // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.equals|equals(kotlin.Any?){}[0] 33 | final fun hashCode(): kotlin/Int // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.hashCode|hashCode(){}[0] 34 | final fun toString(): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.toString|toString(){}[0] 35 | 36 | final object Companion { // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.Companion|null[0] 37 | final fun default(kotlin/Any?): io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig // io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig.Companion.default|default(kotlin.Any?){}[0] 38 | } 39 | } 40 | 41 | final class io.openfeedback.viewmodels/OpenFeedbackViewModel : androidx.lifecycle/ViewModel { // io.openfeedback.viewmodels/OpenFeedbackViewModel|null[0] 42 | final val uiState // io.openfeedback.viewmodels/OpenFeedbackViewModel.uiState|{}uiState[0] 43 | final fun (): kotlinx.coroutines.flow/StateFlow // io.openfeedback.viewmodels/OpenFeedbackViewModel.uiState.|(){}[0] 44 | 45 | final fun submitComment(kotlin/String): kotlinx.coroutines/Job // io.openfeedback.viewmodels/OpenFeedbackViewModel.submitComment|submitComment(kotlin.String){}[0] 46 | final fun upVote(io.openfeedback.ui.models/UIComment): kotlinx.coroutines/Job // io.openfeedback.viewmodels/OpenFeedbackViewModel.upVote|upVote(io.openfeedback.ui.models.UIComment){}[0] 47 | final fun vote(io.openfeedback.ui.models/UIVoteItem): kotlinx.coroutines/Job // io.openfeedback.viewmodels/OpenFeedbackViewModel.vote|vote(io.openfeedback.ui.models.UIVoteItem){}[0] 48 | 49 | final object Companion { // io.openfeedback.viewmodels/OpenFeedbackViewModel.Companion|null[0] 50 | final fun provideFactory(dev.gitlive.firebase/FirebaseApp, kotlin/String, kotlin/String, kotlin/String): androidx.lifecycle/ViewModelProvider.Factory // io.openfeedback.viewmodels/OpenFeedbackViewModel.Companion.provideFactory|provideFactory(dev.gitlive.firebase.FirebaseApp;kotlin.String;kotlin.String;kotlin.String){}[0] 51 | } 52 | } 53 | 54 | sealed class io.openfeedback.viewmodels/OpenFeedbackUiState { // io.openfeedback.viewmodels/OpenFeedbackUiState|null[0] 55 | final class Success : io.openfeedback.viewmodels/OpenFeedbackUiState { // io.openfeedback.viewmodels/OpenFeedbackUiState.Success|null[0] 56 | constructor (io.openfeedback.ui.models/UISessionFeedback) // io.openfeedback.viewmodels/OpenFeedbackUiState.Success.|(io.openfeedback.ui.models.UISessionFeedback){}[0] 57 | 58 | final val session // io.openfeedback.viewmodels/OpenFeedbackUiState.Success.session|{}session[0] 59 | final fun (): io.openfeedback.ui.models/UISessionFeedback // io.openfeedback.viewmodels/OpenFeedbackUiState.Success.session.|(){}[0] 60 | } 61 | 62 | final object Loading : io.openfeedback.viewmodels/OpenFeedbackUiState { // io.openfeedback.viewmodels/OpenFeedbackUiState.Loading|null[0] 63 | final fun equals(kotlin/Any?): kotlin/Boolean // io.openfeedback.viewmodels/OpenFeedbackUiState.Loading.equals|equals(kotlin.Any?){}[0] 64 | final fun hashCode(): kotlin/Int // io.openfeedback.viewmodels/OpenFeedbackUiState.Loading.hashCode|hashCode(){}[0] 65 | final fun toString(): kotlin/String // io.openfeedback.viewmodels/OpenFeedbackUiState.Loading.toString|toString(){}[0] 66 | } 67 | } 68 | 69 | final val io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackFirebaseConfig$stableprop // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackFirebaseConfig$stableprop|#static{}io_openfeedback_viewmodels_OpenFeedbackFirebaseConfig$stableprop[0] 70 | final val io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState$stableprop // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState$stableprop|#static{}io_openfeedback_viewmodels_OpenFeedbackUiState$stableprop[0] 71 | final val io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState_Loading$stableprop // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState_Loading$stableprop|#static{}io_openfeedback_viewmodels_OpenFeedbackUiState_Loading$stableprop[0] 72 | final val io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState_Success$stableprop // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState_Success$stableprop|#static{}io_openfeedback_viewmodels_OpenFeedbackUiState_Success$stableprop[0] 73 | final val io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackViewModel$stableprop // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackViewModel$stableprop|#static{}io_openfeedback_viewmodels_OpenFeedbackViewModel$stableprop[0] 74 | 75 | final fun io.openfeedback.viewmodels/getFirebaseApp(kotlin/String?): dev.gitlive.firebase/FirebaseApp // io.openfeedback.viewmodels/getFirebaseApp|getFirebaseApp(kotlin.String?){}[0] 76 | final fun io.openfeedback.viewmodels/initializeOpenFeedback(io.openfeedback.viewmodels/OpenFeedbackFirebaseConfig) // io.openfeedback.viewmodels/initializeOpenFeedback|initializeOpenFeedback(io.openfeedback.viewmodels.OpenFeedbackFirebaseConfig){}[0] 77 | final fun io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackFirebaseConfig$stableprop_getter(): kotlin/Int // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackFirebaseConfig$stableprop_getter|io_openfeedback_viewmodels_OpenFeedbackFirebaseConfig$stableprop_getter(){}[0] 78 | final fun io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState$stableprop_getter(): kotlin/Int // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState$stableprop_getter|io_openfeedback_viewmodels_OpenFeedbackUiState$stableprop_getter(){}[0] 79 | final fun io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState_Loading$stableprop_getter(): kotlin/Int // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState_Loading$stableprop_getter|io_openfeedback_viewmodels_OpenFeedbackUiState_Loading$stableprop_getter(){}[0] 80 | final fun io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState_Success$stableprop_getter(): kotlin/Int // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackUiState_Success$stableprop_getter|io_openfeedback_viewmodels_OpenFeedbackUiState_Success$stableprop_getter(){}[0] 81 | final fun io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackViewModel$stableprop_getter(): kotlin/Int // io.openfeedback.viewmodels/io_openfeedback_viewmodels_OpenFeedbackViewModel$stableprop_getter|io_openfeedback_viewmodels_OpenFeedbackViewModel$stableprop_getter(){}[0] 82 | final fun io.openfeedback/OpenFeedback(kotlin/String, kotlin/String, androidx.compose.ui/Modifier?, kotlin/Int, kotlin/Boolean, kotlin/Boolean, kotlin/String?, kotlin/String?, kotlin/Function2?, kotlin/Function2?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.openfeedback/OpenFeedback|OpenFeedback(kotlin.String;kotlin.String;androidx.compose.ui.Modifier?;kotlin.Int;kotlin.Boolean;kotlin.Boolean;kotlin.String?;kotlin.String?;kotlin.Function2?;kotlin.Function2?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] 83 | -------------------------------------------------------------------------------- /openfeedback-viewmodel/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.multiplatform") 4 | id("org.jetbrains.kotlin.plugin.serialization") 5 | } 6 | 7 | library( 8 | namespace = "io.openfeedback.viewmodels", 9 | compose = true, 10 | ) { kotlinMultiplatformExtension -> 11 | kotlinMultiplatformExtension.sourceSets { 12 | getByName("commonMain") { 13 | dependencies { 14 | implementation(projects.openfeedback) 15 | api(projects.openfeedbackM3) 16 | api(projects.openfeedbackUiModels) 17 | 18 | implementation(kotlinMultiplatformExtension.compose.material3) 19 | implementation(kotlinMultiplatformExtension.compose.runtime) 20 | // Not sure why this is needed 🤷 21 | implementation(libs.jetbrains.kotlin.stdlib) 22 | 23 | api(libs.androidx.lifecycle.viewmodel.compose) 24 | api(libs.vanniktech.multiplatform.locale) 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /openfeedback-viewmodel/src/commonMain/kotlin/io/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/openfeedback-viewmodel/src/commonMain/kotlin/io/.DS_Store -------------------------------------------------------------------------------- /openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/.DS_Store -------------------------------------------------------------------------------- /openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/OpenFeedback.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.material3.ExperimentalMaterial3Api 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.collectAsState 8 | import androidx.compose.runtime.getValue 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.lifecycle.viewmodel.compose.viewModel 15 | import com.vanniktech.locale.Locale 16 | import com.vanniktech.locale.Locales 17 | import io.openfeedback.m3.Comment 18 | import io.openfeedback.m3.CommentInput 19 | import io.openfeedback.m3.FeedbackNotReady 20 | import io.openfeedback.m3.Loading 21 | import io.openfeedback.m3.OpenFeedbackLayout 22 | import io.openfeedback.m3.VoteCard 23 | import io.openfeedback.viewmodels.OpenFeedbackUiState 24 | import io.openfeedback.viewmodels.OpenFeedbackViewModel 25 | import io.openfeedback.viewmodels.getFirebaseApp 26 | 27 | /** 28 | * Stateful component that will observe remote OpenFeedback Firestore project to 29 | * display feedback of a session. 30 | * 31 | * @param projectId Firestore project id 32 | * @param sessionId Firestore session id 33 | * @param modifier The modifier to be applied to the component. 34 | * @param columnCount Number of column to display for vote items. 35 | * @param isReady Flag to display the component or not. 36 | * @param displayComments Flag to display comments or not. 37 | * @param languageCode Language code of the user. 38 | * @param appName Locale openfeedback name, used to restore openfeedback configuration. 39 | * @param loading Component to display when the view model fetch vote items. 40 | */ 41 | @OptIn(ExperimentalMaterial3Api::class) 42 | @Composable 43 | fun OpenFeedback( 44 | projectId: String, 45 | sessionId: String, 46 | modifier: Modifier = Modifier, 47 | columnCount: Int = 2, 48 | isReady: Boolean = true, 49 | displayComments: Boolean = true, 50 | languageCode: String = Locale.from(Locales.currentLocaleString()).language.code, 51 | appName: String? = null, 52 | loading: @Composable () -> Unit = { Loading(modifier = modifier) }, 53 | notReady: @Composable () -> Unit = { FeedbackNotReady(modifier = modifier) }, 54 | ) { 55 | if (isReady.not()) { 56 | notReady() 57 | return 58 | } 59 | // Putting the ViewModel initialization here allows us to display this component 60 | // in a Composable Preview with isNotReady flag set to true. 61 | val viewModel = viewModel( 62 | key = sessionId, 63 | factory = OpenFeedbackViewModel.provideFactory( 64 | firebaseApp = getFirebaseApp(appName), 65 | projectId = projectId, 66 | sessionId = sessionId, 67 | languageCode = languageCode 68 | ) 69 | ) 70 | val uiState = viewModel.uiState.collectAsState() 71 | when (uiState.value) { 72 | is OpenFeedbackUiState.Loading -> loading() 73 | is OpenFeedbackUiState.Success -> { 74 | val session = (uiState.value as OpenFeedbackUiState.Success).session 75 | var text by remember { mutableStateOf("") } 76 | 77 | OpenFeedbackLayout( 78 | sessionFeedback = session, 79 | modifier = modifier, 80 | columnCount = columnCount, 81 | displayComments = displayComments, 82 | comment = { 83 | Comment( 84 | comment = it, 85 | onClick = viewModel::upVote 86 | ) 87 | }, 88 | commentInput = { 89 | CommentInput( 90 | value = text, 91 | onValueChange = { text = it }, 92 | onSubmit = { viewModel.submitComment(text) }, 93 | modifier = Modifier.fillMaxWidth() 94 | ) 95 | }, 96 | voteItem = { 97 | VoteCard( 98 | voteModel = it, 99 | onClick = viewModel::vote, 100 | modifier = Modifier 101 | .height(100.dp) 102 | .fillMaxWidth() 103 | ) 104 | } 105 | ) 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/OpenFeedbackFirebaseConfig.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.viewmodels 2 | 3 | import androidx.compose.runtime.Immutable 4 | import dev.gitlive.firebase.Firebase 5 | import dev.gitlive.firebase.FirebaseApp 6 | import dev.gitlive.firebase.initialize 7 | 8 | @Immutable 9 | data class OpenFeedbackFirebaseConfig( 10 | val context: Any?, 11 | val projectId: String, 12 | val applicationId: String, 13 | val apiKey: String, 14 | val databaseUrl: String, 15 | val appName: String = "openfeedback" 16 | ) { 17 | companion object { 18 | /** 19 | * Returns a [OpenFeedbackFirebaseConfig] configured for the default openfeedback instance at openfeedback.io 20 | * 21 | * @param context the context on Android or null on iOS 22 | */ 23 | fun default(context: Any?): OpenFeedbackFirebaseConfig { 24 | /** 25 | * The firebase parameters are from the openfeedback.io project so we can 26 | * access firestore directly 27 | */ 28 | return OpenFeedbackFirebaseConfig( 29 | context = context, 30 | projectId = "open-feedback-42", 31 | // Hack: I replaced :web: by :ios: for the iOS SDK to behave 32 | applicationId = "1:635903227116:ios:31de912f8bf29befb1e1c9", 33 | apiKey = "AIzaSyB3ELJsaiItrln0uDGSuuHE1CfOJO67Hb4", 34 | databaseUrl = "https://open-feedback-42.firebaseio.com/" 35 | ) 36 | } 37 | } 38 | } 39 | 40 | private val appCache = mutableMapOf() 41 | 42 | /** 43 | * Initialize in cache OpenFeedback configuration. 44 | * 45 | * @param config Firebase configuration. 46 | */ 47 | fun initializeOpenFeedback( 48 | config: OpenFeedbackFirebaseConfig 49 | ) { 50 | require(!appCache.containsKey(config.appName)) { 51 | "Openfeedback '${config.apiKey}' is already initialized" 52 | } 53 | 54 | with(config) { 55 | appCache.put( 56 | appName, 57 | Firebase.initialize( 58 | context = context, 59 | options = dev.gitlive.firebase.FirebaseOptions( 60 | projectId = projectId, 61 | applicationId = applicationId, 62 | apiKey = apiKey, 63 | databaseUrl = databaseUrl 64 | ), 65 | name = appName 66 | ) 67 | ) 68 | } 69 | } 70 | 71 | /** 72 | * Get OpenFeedback Firebase instance by app name. 73 | * 74 | * @param appName Local OpenFeedback name 75 | * @return [FirebaseApp] instance. 76 | */ 77 | fun getFirebaseApp(appName: String?): FirebaseApp { 78 | if (appName != null) { 79 | return appCache.get(appName) ?: error("OpenFeedback was not initialized for app '$appName'") 80 | } 81 | 82 | return when { 83 | appCache.isEmpty() -> error("You need to call initializeOpenFeedback() before OpenFeedback()") 84 | appCache.size == 1 -> appCache.values.single() 85 | else -> error("Multiple OpenFeedback apps initialized, pass 'appName'") 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/OpenFeedbackViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.viewmodels 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.ViewModelProvider 5 | import androidx.lifecycle.viewModelScope 6 | import androidx.lifecycle.viewmodel.CreationExtras 7 | import dev.gitlive.firebase.FirebaseApp 8 | import io.openfeedback.OpenFeedbackRepository 9 | import io.openfeedback.model.SessionData 10 | import io.openfeedback.ui.models.UIComment 11 | import io.openfeedback.ui.models.UISessionFeedback 12 | import io.openfeedback.ui.models.UIVoteItem 13 | import io.openfeedback.viewmodels.extensions.mapWithPreviousValue 14 | import io.openfeedback.viewmodels.mappers.toUISessionFeedback 15 | import kotlinx.coroutines.flow.MutableStateFlow 16 | import kotlinx.coroutines.flow.StateFlow 17 | import kotlinx.coroutines.launch 18 | import kotlin.reflect.KClass 19 | 20 | sealed class OpenFeedbackUiState { 21 | data object Loading : OpenFeedbackUiState() 22 | class Success(val session: UISessionFeedback) : OpenFeedbackUiState() 23 | } 24 | 25 | class OpenFeedbackViewModel private constructor( 26 | firebaseApp: FirebaseApp, 27 | projectId: String, 28 | sessionId: String, 29 | languageCode: String 30 | ) : ViewModel() { 31 | private val repository = OpenFeedbackRepository(firebaseApp, projectId, sessionId) 32 | 33 | private val _uiState = MutableStateFlow(OpenFeedbackUiState.Loading) 34 | val uiState: StateFlow = _uiState 35 | 36 | init { 37 | /** 38 | * Warning: This screen is not 100% reactive because there are 2 sources of truth for votes: 39 | * - userVotes are written by the app 40 | * - sessionVotes is written by the backend which computes the aggregates 41 | * 42 | * We used to be reactive but this creates a blinking effect because there's a long delay until the cloud 43 | * function updates sessionVotes. 44 | * 45 | * Instead, just retrieve the data from the network once and use local votes. 46 | * There is no feedback if a given vote fails. 47 | * 48 | * See also https://stackoverflow.com/questions/58840642/set-update-collection-or-document-but-only-locally 49 | */ 50 | viewModelScope.launch { 51 | repository 52 | .fetchSessionData() 53 | .mapWithPreviousValue { prev, cur -> 54 | if (prev == null) { 55 | cur.toUISessionFeedback( 56 | languageCode = languageCode, 57 | oldVoteItems = null, 58 | oldComments = null 59 | ) 60 | } else { 61 | cur.toUISessionFeedback( 62 | languageCode = languageCode, 63 | oldVoteItems = prev.voteItems, 64 | oldComments = prev.comments 65 | ) 66 | } 67 | } 68 | .collect { 69 | _uiState.value = OpenFeedbackUiState.Success(it) 70 | } 71 | } 72 | } 73 | 74 | fun submitComment(text: String) = viewModelScope.launch { 75 | repository.submitComment(text) 76 | } 77 | 78 | fun vote(voteItem: UIVoteItem) = viewModelScope.launch { 79 | repository.vote(voteItem.id, voteItem.votedByUser) 80 | } 81 | 82 | fun upVote(comment: UIComment) = viewModelScope.launch { 83 | repository.upVote(comment.id, comment.votedByUser) 84 | } 85 | 86 | companion object { 87 | fun provideFactory( 88 | firebaseApp: FirebaseApp, 89 | projectId: String, 90 | sessionId: String, 91 | languageCode: String 92 | ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { 93 | override fun create(modelClass: KClass, extras: CreationExtras): T = 94 | OpenFeedbackViewModel(firebaseApp, projectId, sessionId, languageCode) as T 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/extensions/Flow.ext.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.viewmodels.extensions 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.flow 5 | 6 | /** 7 | * Allows access to the previous emitted value 8 | * We use that to have stable dots coordinates 9 | */ 10 | internal fun Flow.mapWithPreviousValue(block: (previous: R?, current: T) -> R): Flow { 11 | var prev: R? = null 12 | return flow { 13 | this@mapWithPreviousValue.collect { 14 | block(prev, it).also { 15 | emit(it) 16 | prev = it 17 | } 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/mappers/SessionDataToUiModels.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.viewmodels.mappers 2 | 3 | import com.vanniktech.locale.Locale 4 | import io.openfeedback.model.SessionData 5 | import io.openfeedback.ui.models.UIComment 6 | import io.openfeedback.ui.models.UIDot 7 | import io.openfeedback.ui.models.UISessionFeedback 8 | import io.openfeedback.ui.models.UIVoteItem 9 | import kotlinx.collections.immutable.toImmutableList 10 | import kotlinx.datetime.LocalDateTime 11 | import kotlinx.datetime.TimeZone 12 | import kotlinx.datetime.format 13 | import kotlinx.datetime.format.MonthNames 14 | import kotlinx.datetime.format.char 15 | import kotlinx.datetime.toLocalDateTime 16 | import kotlin.math.absoluteValue 17 | import kotlin.random.Random 18 | 19 | /** 20 | * Map [SessionData] instance to [UISessionFeedback], stable model for Compose UI. 21 | * 22 | * @param languageCode User language code. 23 | * @param oldVoteItems Old version of vote items. 24 | * @param oldComments old version of comments. 25 | * @return [UISessionFeedback] model. 26 | */ 27 | internal fun SessionData.toUISessionFeedback( 28 | languageCode: String, 29 | oldVoteItems: List?, 30 | oldComments: List?, 31 | ): UISessionFeedback { 32 | val sessionData = this 33 | val votedItemIds = sessionData.votedItemIds 34 | return UISessionFeedback( 35 | voteItems = sessionData.project.voteItems 36 | .filter { it.type == "boolean" } 37 | .map { voteItem -> 38 | val oldVoteItem = oldVoteItems?.firstOrNull { it.id == voteItem.id } 39 | val count = sessionData.voteItemAggregates[voteItem.id]?.toInt() ?: 0 40 | val oldDots = oldVoteItem?.dots.orEmpty() 41 | val diff = count - oldDots.size 42 | val dots = if (diff > 0) { 43 | oldDots + newDots(diff, sessionData.project.chipColors) 44 | } else { 45 | oldDots.dropLast(diff.absoluteValue) 46 | } 47 | UIVoteItem( 48 | id = voteItem.id, 49 | text = voteItem.localizedName(languageCode), 50 | dots = dots.toImmutableList(), 51 | votedByUser = votedItemIds.contains(voteItem.id) 52 | ) 53 | } 54 | .toImmutableList(), 55 | comments = sessionData.comments.map { commentItem -> 56 | val localDateTime = 57 | commentItem.createdAt.toLocalDateTime(TimeZone.currentSystemDefault()) 58 | val oldComment = oldComments?.firstOrNull { it.id == commentItem.id } 59 | val oldDots = oldComment?.dots.orEmpty() 60 | val diff = commentItem.plus.toInt() - oldDots.size 61 | val dots = if (diff > 0) { 62 | oldDots + newDots(diff, sessionData.project.chipColors) 63 | } else { 64 | oldDots.dropLast(diff.absoluteValue) 65 | } 66 | UIComment( 67 | id = commentItem.id, 68 | message = commentItem.text, 69 | createdAt = localDateTime.format(dateFormat), 70 | upVotes = commentItem.plus.toInt(), 71 | dots = dots.toImmutableList(), 72 | votedByUser = sessionData.votedCommentIds.contains(commentItem.id), 73 | fromUser = commentItem.userId == sessionData.userId 74 | ) 75 | }.toImmutableList(), 76 | colors = sessionData.project.chipColors.toImmutableList() 77 | ) 78 | } 79 | 80 | /** 81 | * Compute new version of dots. 82 | * 83 | * @param count Number of dots to generate. 84 | * @param possibleColors Possible colors to display. 85 | * @return List of [UIDot] model. 86 | */ 87 | private fun newDots(count: Int, possibleColors: List): List = 0.until(count).map { 88 | UIDot( 89 | Random.nextFloat(), 90 | Random.nextFloat().coerceIn(0.1f, 0.9f), 91 | possibleColors[Random.nextInt().absoluteValue % possibleColors.size] 92 | ) 93 | } 94 | 95 | private val dateFormat = LocalDateTime.Format { 96 | dayOfMonth() 97 | char(' ') 98 | monthName(MonthNames.ENGLISH_ABBREVIATED) 99 | chars(", ") 100 | hour() 101 | char(':') 102 | minute() 103 | } 104 | -------------------------------------------------------------------------------- /openfeedback/api/openfeedback.api: -------------------------------------------------------------------------------- 1 | public final class io/openfeedback/OpenFeedbackRepository { 2 | public fun (Ldev/gitlive/firebase/FirebaseApp;Ljava/lang/String;Ljava/lang/String;)V 3 | public final fun fetchSessionData (Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 4 | public final fun getProjectId ()Ljava/lang/String; 5 | public final fun getSessionId ()Ljava/lang/String; 6 | public final fun submitComment (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 7 | public final fun upVote (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; 8 | public final fun vote (Ljava/lang/String;ZLkotlin/coroutines/Continuation;)Ljava/lang/Object; 9 | } 10 | 11 | public final class io/openfeedback/model/Comment : io/openfeedback/model/SessionThing { 12 | public fun (Ljava/lang/String;Ljava/lang/String;JLkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Ljava/lang/String;)V 13 | public synthetic fun (Ljava/lang/String;Ljava/lang/String;JLkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 14 | public final fun component1 ()Ljava/lang/String; 15 | public final fun component2 ()Ljava/lang/String; 16 | public final fun component3 ()J 17 | public final fun component4 ()Lkotlinx/datetime/Instant; 18 | public final fun component5 ()Lkotlinx/datetime/Instant; 19 | public final fun component6 ()Ljava/lang/String; 20 | public final fun copy (Ljava/lang/String;Ljava/lang/String;JLkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Ljava/lang/String;)Lio/openfeedback/model/Comment; 21 | public static synthetic fun copy$default (Lio/openfeedback/model/Comment;Ljava/lang/String;Ljava/lang/String;JLkotlinx/datetime/Instant;Lkotlinx/datetime/Instant;Ljava/lang/String;ILjava/lang/Object;)Lio/openfeedback/model/Comment; 22 | public fun equals (Ljava/lang/Object;)Z 23 | public final fun getCreatedAt ()Lkotlinx/datetime/Instant; 24 | public final fun getId ()Ljava/lang/String; 25 | public final fun getPlus ()J 26 | public final fun getText ()Ljava/lang/String; 27 | public final fun getUpdatedAt ()Lkotlinx/datetime/Instant; 28 | public final fun getUserId ()Ljava/lang/String; 29 | public fun hashCode ()I 30 | public fun toString ()Ljava/lang/String; 31 | } 32 | 33 | public final class io/openfeedback/model/Project { 34 | public static final field Companion Lio/openfeedback/model/Project$Companion; 35 | public fun ()V 36 | public fun (Ljava/util/List;Ljava/util/List;)V 37 | public synthetic fun (Ljava/util/List;Ljava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 38 | public final fun component1 ()Ljava/util/List; 39 | public final fun component2 ()Ljava/util/List; 40 | public final fun copy (Ljava/util/List;Ljava/util/List;)Lio/openfeedback/model/Project; 41 | public static synthetic fun copy$default (Lio/openfeedback/model/Project;Ljava/util/List;Ljava/util/List;ILjava/lang/Object;)Lio/openfeedback/model/Project; 42 | public fun equals (Ljava/lang/Object;)Z 43 | public final fun getChipColors ()Ljava/util/List; 44 | public final fun getVoteItems ()Ljava/util/List; 45 | public fun hashCode ()I 46 | public fun toString ()Ljava/lang/String; 47 | } 48 | 49 | public synthetic class io/openfeedback/model/Project$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 50 | public static final field INSTANCE Lio/openfeedback/model/Project$$serializer; 51 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 52 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/openfeedback/model/Project; 53 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 54 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 55 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/openfeedback/model/Project;)V 56 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 57 | } 58 | 59 | public final class io/openfeedback/model/Project$Companion { 60 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 61 | } 62 | 63 | public final class io/openfeedback/model/SessionData { 64 | public fun (Lio/openfeedback/model/Project;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Map;Ljava/util/List;)V 65 | public final fun component1 ()Lio/openfeedback/model/Project; 66 | public final fun component2 ()Ljava/lang/String; 67 | public final fun component3 ()Ljava/util/Set; 68 | public final fun component4 ()Ljava/util/Set; 69 | public final fun component5 ()Ljava/util/Map; 70 | public final fun component6 ()Ljava/util/List; 71 | public final fun copy (Lio/openfeedback/model/Project;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Map;Ljava/util/List;)Lio/openfeedback/model/SessionData; 72 | public static synthetic fun copy$default (Lio/openfeedback/model/SessionData;Lio/openfeedback/model/Project;Ljava/lang/String;Ljava/util/Set;Ljava/util/Set;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lio/openfeedback/model/SessionData; 73 | public fun equals (Ljava/lang/Object;)Z 74 | public final fun getComments ()Ljava/util/List; 75 | public final fun getProject ()Lio/openfeedback/model/Project; 76 | public final fun getUserId ()Ljava/lang/String; 77 | public final fun getVoteItemAggregates ()Ljava/util/Map; 78 | public final fun getVotedCommentIds ()Ljava/util/Set; 79 | public final fun getVotedItemIds ()Ljava/util/Set; 80 | public fun hashCode ()I 81 | public fun toString ()Ljava/lang/String; 82 | } 83 | 84 | public final class io/openfeedback/model/VoteItem { 85 | public static final field Companion Lio/openfeedback/model/VoteItem$Companion; 86 | public fun ()V 87 | public fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/String;)V 88 | public synthetic fun (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 89 | public final fun component1 ()Ljava/lang/String; 90 | public final fun component2 ()Ljava/util/Map; 91 | public final fun component3 ()Ljava/lang/String; 92 | public final fun component4 ()I 93 | public final fun component5 ()Ljava/lang/String; 94 | public final fun copy (Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/String;)Lio/openfeedback/model/VoteItem; 95 | public static synthetic fun copy$default (Lio/openfeedback/model/VoteItem;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;ILjava/lang/String;ILjava/lang/Object;)Lio/openfeedback/model/VoteItem; 96 | public fun equals (Ljava/lang/Object;)Z 97 | public final fun getId ()Ljava/lang/String; 98 | public final fun getLanguages ()Ljava/util/Map; 99 | public final fun getName ()Ljava/lang/String; 100 | public final fun getPosition ()I 101 | public final fun getType ()Ljava/lang/String; 102 | public fun hashCode ()I 103 | public final fun localizedName (Ljava/lang/String;)Ljava/lang/String; 104 | public fun toString ()Ljava/lang/String; 105 | } 106 | 107 | public synthetic class io/openfeedback/model/VoteItem$$serializer : kotlinx/serialization/internal/GeneratedSerializer { 108 | public static final field INSTANCE Lio/openfeedback/model/VoteItem$$serializer; 109 | public final fun childSerializers ()[Lkotlinx/serialization/KSerializer; 110 | public final fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Lio/openfeedback/model/VoteItem; 111 | public synthetic fun deserialize (Lkotlinx/serialization/encoding/Decoder;)Ljava/lang/Object; 112 | public final fun getDescriptor ()Lkotlinx/serialization/descriptors/SerialDescriptor; 113 | public final fun serialize (Lkotlinx/serialization/encoding/Encoder;Lio/openfeedback/model/VoteItem;)V 114 | public synthetic fun serialize (Lkotlinx/serialization/encoding/Encoder;Ljava/lang/Object;)V 115 | } 116 | 117 | public final class io/openfeedback/model/VoteItem$Companion { 118 | public final fun serializer ()Lkotlinx/serialization/KSerializer; 119 | } 120 | 121 | -------------------------------------------------------------------------------- /openfeedback/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.library") 3 | id("org.jetbrains.kotlin.plugin.serialization") 4 | } 5 | 6 | library( 7 | namespace = "io.openfeedback", 8 | ) { 9 | it.sourceSets { 10 | getByName("commonMain").apply { 11 | dependencies { 12 | api(libs.jetbrains.kotlinx.coroutines) 13 | api(libs.jetbrains.kotlinx.datetime) 14 | api(libs.jetbrains.kotlinx.serialization.json) 15 | 16 | api(libs.gitlive.firebase.app) 17 | api(libs.gitlive.firebase.firestore) 18 | implementation(libs.gitlive.firebase.auth) 19 | implementation(libs.gitlive.firebase.common) 20 | 21 | implementation(libs.touchlab.kermit) 22 | } 23 | } 24 | getByName("androidMain"){ 25 | dependencies { 26 | api(libs.google.firebase.common) 27 | api(libs.google.firebase.firestore) 28 | implementation(libs.google.firebase.auth) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /openfeedback/openfeedback-proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keepattributes Signature 2 | 3 | -keep class io.openfeedback.android.model.** { *; } 4 | -------------------------------------------------------------------------------- /openfeedback/src/androidMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.android.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.mappers 2 | 3 | import com.google.firebase.Timestamp 4 | import kotlinx.datetime.Instant 5 | 6 | internal actual fun timestampToInstant(nativeTimestamp: Any): Instant { 7 | (nativeTimestamp as Timestamp) 8 | return Instant.fromEpochSeconds(nativeTimestamp.seconds, nativeTimestamp.nanoseconds) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/openfeedback/src/commonMain/kotlin/io/.DS_Store -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/OpenFeedbackRepository.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback 2 | 3 | import dev.gitlive.firebase.FirebaseApp 4 | import io.openfeedback.extensions.commentVoteItemId 5 | import io.openfeedback.extensions.commitComment 6 | import io.openfeedback.extensions.filterFirst 7 | import io.openfeedback.extensions.voteComment 8 | import io.openfeedback.extensions.voteItem 9 | import io.openfeedback.mappers.mapToSessionData 10 | import io.openfeedback.model.CommitComment 11 | import io.openfeedback.model.Event 12 | import io.openfeedback.model.SessionData 13 | import io.openfeedback.model.VoteCommentEvent 14 | import io.openfeedback.model.VoteItemEvent 15 | import io.openfeedback.model.VoteStatus 16 | import io.openfeedback.sources.OpenFeedbackAuth 17 | import io.openfeedback.sources.OpenFeedbackFirestore 18 | import kotlinx.coroutines.ExperimentalCoroutinesApi 19 | import kotlinx.coroutines.coroutineScope 20 | import kotlinx.coroutines.flow.Flow 21 | import kotlinx.coroutines.flow.MutableSharedFlow 22 | import kotlinx.coroutines.flow.combine 23 | import kotlinx.coroutines.flow.filterNotNull 24 | import kotlinx.coroutines.flow.flatMapLatest 25 | import kotlinx.coroutines.flow.scan 26 | 27 | class OpenFeedbackRepository( 28 | firebaseApp: FirebaseApp, 29 | val projectId: String, 30 | val sessionId: String, 31 | ) { 32 | private val auth = OpenFeedbackAuth(firebaseApp) 33 | private val firestore = OpenFeedbackFirestore.create(firebaseApp) 34 | private val voteEvents = MutableSharedFlow() 35 | private var commentVoteItemId: String? = null 36 | 37 | /** 38 | * Observe remote firestore database to merge the project, user votes and sessions 39 | * to cache vote events and vote item id of the current user. 40 | * 41 | * @return Flow for the [SessionData]. 42 | */ 43 | @OptIn(ExperimentalCoroutinesApi::class) 44 | suspend fun fetchSessionData(): Flow = coroutineScope { 45 | return@coroutineScope combine( 46 | firestore.project(projectId), 47 | firestore.userVotes( 48 | projectId = projectId, 49 | userId = auth.userId(), 50 | sessionId = sessionId, 51 | ), 52 | firestore.sessionThings(projectId = projectId, sessionId = sessionId), 53 | ) { project, userVotesResult, sessionThingsResult -> 54 | mapToSessionData( 55 | auth.userId(), 56 | project, 57 | userVotesResult.data, 58 | sessionThingsResult.data, 59 | ) 60 | }.filterNotNull() 61 | /* 62 | * Take only the first (maybe cached) item. Meaning we might be a bit stale sometimes but this prevents 63 | * the network result to kick in with completely different results after the fact, which can be surprising 64 | */ 65 | .filterFirst() 66 | .flatMapLatest { sessionData -> 67 | // Remember the commentVoteItemId 68 | commentVoteItemId = sessionData.project.commentVoteItemId() 69 | 70 | voteEvents.scan(sessionData) { acc, value -> 71 | when (value) { 72 | is VoteItemEvent -> { 73 | acc.voteItem(value.voteItemId, value.votedByUser) 74 | } 75 | 76 | is VoteCommentEvent -> { 77 | acc.voteComment(value.commentId, value.votedByUser) 78 | } 79 | 80 | is CommitComment -> { 81 | acc.commitComment(value.text) 82 | } 83 | } 84 | } 85 | } 86 | } 87 | 88 | /** 89 | * Submit a new comment for a session. 90 | * 91 | * @param text Content of the comment. 92 | */ 93 | suspend fun submitComment(text: String) = coroutineScope { 94 | if (text == "") { 95 | println("Can't submit an empty comment") 96 | return@coroutineScope 97 | } 98 | if (commentVoteItemId == null) { 99 | println("No commentVoteItemId") 100 | return@coroutineScope 101 | } 102 | voteEvents.emit(CommitComment(text)) 103 | firestore.setComment( 104 | projectId = projectId, 105 | talkId = sessionId, 106 | voteItemId = commentVoteItemId!!, 107 | status = VoteStatus.Active, 108 | text = text, 109 | userId = auth.userId() 110 | ) 111 | } 112 | 113 | /** 114 | * Update a vote on a vote item. 115 | * 116 | * @param voteItemId Identifier of a vote item. 117 | * @param votedByUser Notify if we need to active or delete the up vote. 118 | */ 119 | suspend fun vote(voteItemId: String, votedByUser: Boolean) = coroutineScope { 120 | voteEvents.emit(VoteItemEvent(voteItemId = voteItemId, votedByUser = !votedByUser)) 121 | firestore.setVote( 122 | projectId = projectId, 123 | talkId = sessionId, 124 | voteItemId = voteItemId, 125 | status = if (!votedByUser) VoteStatus.Active else VoteStatus.Deleted, 126 | userId = auth.userId() 127 | ) 128 | } 129 | 130 | /** 131 | * Up vote an existing comment. 132 | * 133 | * @param commentId Identifier of the existing comment. 134 | * @param votedByUser Notify if we need to active or delete the up vote. 135 | */ 136 | suspend fun upVote(commentId: String, votedByUser: Boolean) = coroutineScope { 137 | if (commentVoteItemId == null) { 138 | println("No commentVoteItemId yet") 139 | return@coroutineScope 140 | } 141 | voteEvents.emit(VoteCommentEvent(commentId = commentId, votedByUser = !votedByUser)) 142 | firestore.upVote( 143 | projectId = projectId, 144 | talkId = sessionId, 145 | voteItemId = commentVoteItemId!!, 146 | voteId = commentId, 147 | status = if (!votedByUser) VoteStatus.Active else VoteStatus.Deleted, 148 | userId = auth.userId() 149 | ) 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/extensions/CommentMap.ext.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.extensions 2 | 3 | import io.openfeedback.model.CommentsMap 4 | 5 | /** 6 | * Define 1 up vote for every comments specified in [votedCommentIds] parameter. Then, 7 | * ensures that we are using the greater up vote value between the [CommentsMap] and the 8 | * up vote defined by the parameter. 9 | * 10 | * @param votedCommentIds List of comment ids. 11 | * @return New [CommentsMap] instance with new up vote values. 12 | */ 13 | internal fun CommentsMap.coerceAggregations(votedCommentIds: Set): CommentsMap { 14 | return CommentsMap(all.mapValues { 15 | val minValue = if (votedCommentIds.contains(it.key)) { 16 | 1L 17 | } else { 18 | 0L 19 | } 20 | it.value.copy(plus = it.value.plus.coerceAtLeast(minValue)) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/extensions/Flow.ext.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.extensions 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.mapNotNull 5 | 6 | /** 7 | * A variation of take(1) that does not cancel the flow so that the query continues running and 8 | * network results get written 9 | */ 10 | internal fun Flow.filterFirst(): Flow { 11 | var first = true 12 | return mapNotNull { 13 | if (first) { 14 | first = false 15 | it 16 | } else { 17 | null 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/extensions/Project.ext.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.extensions 2 | 3 | import io.openfeedback.model.Project 4 | 5 | internal fun Project.commentVoteItemId(): String? = voteItems.find { it.type == "text" }?.id 6 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/extensions/SessionData.ext.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.extensions 2 | 3 | import io.openfeedback.model.Comment 4 | import io.openfeedback.model.SessionData 5 | import kotlinx.datetime.Clock 6 | 7 | /** 8 | * Update the [SessionData] with the new up vote on the comment. 9 | * 10 | * @param commentId Identifier of the comment. 11 | * @param voted true if it is an up vote, otherwise false. 12 | * @return copy of the [SessionData] with the up vote update. 13 | */ 14 | internal fun SessionData.voteComment(commentId: String, voted: Boolean): SessionData { 15 | val newVotedCommentIds = if (voted) { 16 | votedCommentIds + commentId 17 | } else { 18 | votedCommentIds - commentId 19 | } 20 | 21 | val newComments = comments.map { 22 | if (it.id == commentId) { 23 | if (voted) { 24 | it.copy(plus = it.plus + 1) 25 | } else { 26 | it.copy(plus = it.plus - 1) 27 | } 28 | } else { 29 | it 30 | } 31 | } 32 | return copy( 33 | votedCommentIds = newVotedCommentIds, 34 | comments = newComments 35 | ) 36 | } 37 | 38 | /** 39 | * Update the [SessionData] with the new comment. 40 | * 41 | * @param text Content of the comment. 42 | * @return copy of the [SessionData] with the new comment. 43 | */ 44 | internal fun SessionData.commitComment(text: String): SessionData { 45 | var found = false 46 | var newComments = comments.map { 47 | if (it.userId == userId) { 48 | found = true 49 | it.copy(updatedAt = Clock.System.now(), text = text) 50 | } else { 51 | it 52 | } 53 | } 54 | if (!found) { 55 | newComments = newComments + Comment( 56 | id = "placeholderId", 57 | userId = userId, 58 | createdAt = Clock.System.now(), 59 | updatedAt = Clock.System.now(), 60 | text = text, 61 | plus = 0 62 | ) 63 | } 64 | return copy( 65 | comments = newComments.sortedByDescending { it.updatedAt } 66 | ) 67 | } 68 | 69 | /** 70 | * Update the [SessionData] with the new vote on a vote item. 71 | * 72 | * @param voteItemId Identifier of the vote item. 73 | * @param voted true if it is an up vote, otherwise false. 74 | * @return copy of the [SessionData] with the vote on a vote item. 75 | */ 76 | internal fun SessionData.voteItem(voteItemId: String, voted: Boolean): SessionData { 77 | val newVotedItemsIds = if (voted) { 78 | votedItemIds + voteItemId 79 | } else { 80 | votedItemIds - voteItemId 81 | } 82 | 83 | val newAggregates = voteItemAggregates.mapValues { 84 | if (it.key == voteItemId) { 85 | if (voted) { 86 | it.value + 1 87 | } else { 88 | it.value - 1 89 | } 90 | } else { 91 | it.value 92 | } 93 | } 94 | return copy( 95 | votedItemIds = newVotedItemsIds, 96 | voteItemAggregates = newAggregates 97 | ) 98 | } 99 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.mappers 2 | 3 | import io.openfeedback.extensions.coerceAggregations 4 | import io.openfeedback.extensions.commentVoteItemId 5 | import io.openfeedback.model.CommentsMap 6 | import io.openfeedback.model.Project 7 | import io.openfeedback.model.SessionData 8 | import io.openfeedback.model.SessionThing 9 | import io.openfeedback.model.UserVote 10 | import io.openfeedback.model.VoteItemCount 11 | import kotlinx.datetime.Instant 12 | 13 | internal expect fun timestampToInstant(nativeTimestamp: Any): Instant 14 | 15 | /** 16 | * Turns the openfeedback model into something that is a bit more palatable 17 | */ 18 | internal fun mapToSessionData( 19 | userId: String, 20 | project: Project, 21 | userVotes: List, 22 | sessionThings: Map, 23 | ): SessionData { 24 | val votedItemIds = userVotes.mapNotNull { 25 | if (it.text != null) { 26 | // This is a comment 27 | return@mapNotNull null 28 | } 29 | if (it.voteItemId == project.commentVoteItemId()) { 30 | // In theory one cannot vote on the "text" vote item but 🤷 31 | return@mapNotNull null 32 | } 33 | it.voteItemId 34 | }.toSet() 35 | val votedCommentIds = userVotes.mapNotNull { 36 | if (it.text != null) { 37 | // This is a comment 38 | return@mapNotNull null 39 | } 40 | /** 41 | * Do we need to check voteItemId? 42 | */ 43 | // if (it.voteItemId != project.commentVoteItemId()) { 44 | // return@mapNotNull null 45 | // } 46 | // If it.id is not null, it's the upvote for a comment 47 | it.id 48 | }.toSet() 49 | 50 | val voteItemAggregates = project.voteItems.mapNotNull { voteItem -> 51 | if (voteItem.type == "text") { 52 | return@mapNotNull null 53 | } 54 | 55 | val existing = sessionThings.get(voteItem.id) 56 | if (existing != null && existing is VoteItemCount) { 57 | /** 58 | * Be robust to negative votes 59 | */ 60 | val minValue = if (votedCommentIds.contains(voteItem.id)) { 61 | 1L 62 | } else { 63 | 0L 64 | } 65 | voteItem.id to existing.count.coerceAtLeast(minValue) 66 | } else { 67 | /** 68 | * No document yet, return 0 69 | */ 70 | voteItem.id to 0L 71 | } 72 | }.toMap() 73 | 74 | val commentsMaps = sessionThings.values.filterIsInstance() 75 | if (commentsMaps.size > 1) { 76 | val keys = sessionThings.filter { it.value is CommentsMap }.keys 77 | println("Several comment maps for voteItemIds = '$keys'.") 78 | } 79 | val commentMap = 80 | (commentsMaps.firstOrNull() ?: CommentsMap(emptyMap())).coerceAggregations(votedCommentIds) 81 | 82 | val comments = commentMap.all.values.sortedByDescending { it.updatedAt } 83 | return SessionData( 84 | project = project, 85 | userId = userId, 86 | votedItemIds = votedItemIds, 87 | votedCommentIds = votedCommentIds, 88 | voteItemAggregates = voteItemAggregates, 89 | comments = comments 90 | ) 91 | } 92 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/model/EntityModels.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.model 2 | 3 | data class SessionData( 4 | val project: Project, 5 | val userId: String, 6 | val votedItemIds: Set, 7 | val votedCommentIds: Set, 8 | /** 9 | * The aggregate counter for voteItems. 10 | * The counter for comments is in [comments] 11 | * 12 | * key is a voteItemId 13 | */ 14 | val voteItemAggregates: Map, 15 | val comments: List, 16 | ) 17 | 18 | internal sealed interface Event 19 | internal class CommitComment( 20 | val text: String 21 | ) : Event 22 | 23 | internal class VoteItemEvent( 24 | val voteItemId: String, 25 | val votedByUser: Boolean 26 | ) : Event 27 | 28 | internal class VoteCommentEvent( 29 | val commentId: String, 30 | val votedByUser: Boolean 31 | ) : Event 32 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/model/FirestoreModels.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.model 2 | 3 | import kotlinx.datetime.Instant 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class Project( 8 | val chipColors: List = emptyList(), 9 | val voteItems: List = emptyList() 10 | ) 11 | 12 | @Serializable 13 | data class VoteItem( 14 | val id: String = "", 15 | val languages: Map = emptyMap(), 16 | val name: String = "", 17 | val position: Int = 0, 18 | val type: String = "" 19 | ) { 20 | fun localizedName(language: String): String { 21 | return languages.getOrElse(language) { name } 22 | } 23 | } 24 | 25 | @Serializable 26 | internal enum class VoteStatus(val value: String) { 27 | Active("active"), 28 | Deleted("deleted") 29 | } 30 | 31 | /** 32 | * An user vote. This is a document in firebase. 33 | * [UserVote] may represent: 34 | * - a vote on a voteItem 35 | * - a plus on a comment 36 | * - a comment 37 | * 38 | * Note that this can not represent the absence of a vote. 39 | * 40 | * @param voteItemId the voteItemId 41 | * @param id only if this is an upvote for a comment 42 | * @param text only if this is a comment 43 | */ 44 | @Serializable 45 | internal data class UserVote( 46 | val projectId: String, 47 | val talkId: String, 48 | val id: String?, 49 | val voteItemId: String, 50 | val text: String?, 51 | val userId: String?, 52 | val status: String 53 | ) 54 | 55 | /** 56 | * Not serializable using kotlinx-serialization because there is no type discriminator 57 | * See https://github.com/Kotlin/kotlinx.serialization/issues/2223 58 | */ 59 | //@Serializable 60 | internal sealed interface SessionThing 61 | 62 | internal class VoteItemCount(val count: Long): SessionThing 63 | 64 | /** 65 | * A SessionThing representing all the comments for that session 66 | */ 67 | internal class CommentsMap( 68 | /** 69 | * The key is the comment.id 70 | */ 71 | val all: Map 72 | ): SessionThing 73 | 74 | data class Comment( 75 | val id: String, 76 | val text: String, 77 | val plus: Long = 0L, 78 | val createdAt: Instant, 79 | val updatedAt: Instant, 80 | val userId: String?, 81 | ): SessionThing 82 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/sources/OpenFeedbackAuth.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.sources 2 | 3 | import co.touchlab.kermit.Logger 4 | import dev.gitlive.firebase.Firebase 5 | import dev.gitlive.firebase.FirebaseApp 6 | import dev.gitlive.firebase.auth.auth 7 | import kotlinx.coroutines.sync.Mutex 8 | import kotlinx.coroutines.sync.withLock 9 | 10 | internal class OpenFeedbackAuth(app: FirebaseApp) { 11 | private val auth = Firebase.auth(app) 12 | private val mutex = Mutex() 13 | 14 | suspend fun userId(): String { 15 | mutex.withLock { 16 | // TODO: move this to a one-time initialization at startup 17 | if (auth.currentUser == null) { 18 | auth.signInAnonymously() 19 | val result = auth.signInAnonymously() 20 | if (result.user == null) { 21 | Logger.e("OpenFeedbackAuth") { "Cannot signInAnonymously" } 22 | } 23 | } 24 | } 25 | return auth.currentUser?.uid ?: "woopsie" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /openfeedback/src/commonMain/kotlin/io/openfeedback/sources/OpenFeedbackFirestore.kt: -------------------------------------------------------------------------------- 1 | // See https://github.com/GitLiveApp/firebase-kotlin-sdk/issues/710 2 | @file:Suppress( 3 | "CANNOT_OVERRIDE_INVISIBLE_MEMBER", 4 | "INVISIBLE_MEMBER", 5 | "INVISIBLE_REFERENCE", 6 | ) 7 | package io.openfeedback.sources 8 | 9 | import dev.gitlive.firebase.Firebase 10 | import dev.gitlive.firebase.FirebaseApp 11 | import dev.gitlive.firebase.firestore.FieldValue 12 | import dev.gitlive.firebase.firestore.FirebaseFirestore 13 | import dev.gitlive.firebase.firestore.firestore 14 | import io.openfeedback.mappers.timestampToInstant 15 | import io.openfeedback.model.Comment 16 | import io.openfeedback.model.CommentsMap 17 | import io.openfeedback.model.Project 18 | import io.openfeedback.model.SessionThing 19 | import io.openfeedback.model.UserVote 20 | import io.openfeedback.model.VoteItemCount 21 | import io.openfeedback.model.VoteStatus 22 | import kotlinx.coroutines.flow.Flow 23 | import kotlinx.coroutines.flow.map 24 | import kotlinx.coroutines.flow.mapNotNull 25 | 26 | internal class UserVotesResult(val data: List, val isFromCache: Boolean) 27 | internal class SessionThingsResult(val data: Map, val isFromCache: Boolean) 28 | 29 | @Suppress("UNCHECKED_CAST") 30 | internal class OpenFeedbackFirestore(private val firestore: FirebaseFirestore) { 31 | fun project(projectId: String): Flow = 32 | firestore.collection("projects") 33 | .document(projectId) 34 | .snapshots 35 | .map { querySnapshot -> querySnapshot.data() } 36 | 37 | fun userVotes(projectId: String, userId: String, sessionId: String): Flow = 38 | firestore.collection("projects/$projectId/userVotes") 39 | .where { "userId" equalTo userId } 40 | .where { "status" equalTo VoteStatus.Active.value } 41 | .where { "talkId" equalTo sessionId } 42 | .snapshots 43 | .map { querySnapshot -> 44 | var isFromCache = true 45 | val userVotes = querySnapshot.documents.map { 46 | if (!it.metadata.isFromCache) { 47 | isFromCache = false 48 | } 49 | it.data() 50 | } 51 | UserVotesResult( 52 | userVotes, 53 | isFromCache 54 | ) 55 | } 56 | 57 | 58 | private fun Map<*, *>.toComment(id: String): Comment = Comment( 59 | id = id, 60 | text = this["text"] as String, 61 | plus = (this["plus"] as Long).coerceAtLeast(0), 62 | createdAt = timestampToInstant(this["createdAt"]!!), 63 | updatedAt = timestampToInstant(this["updatedAt"]!!), 64 | userId = this["userId"] as String 65 | ) 66 | 67 | /** 68 | * For some weird reasons, OpenFeedback can return empty map for some comments. 69 | * To avoid a crash when the comment is parsed in [toComment] function, we check 70 | * that the map is not empty. 71 | */ 72 | private fun Map.filterMapNotEmpty(): Map = 73 | filter { it.value is Map<*, *> && (it.value as Map).isNotEmpty() } 74 | 75 | private fun Map.toCommentsMap(): CommentsMap { 76 | val comments = this 77 | .filterMapNotEmpty() 78 | .mapValues { (it.value as Map<*, *>).toComment(it.key) } 79 | return CommentsMap(comments) 80 | } 81 | 82 | /** 83 | * Return all things related to this session, vote counts and comments 84 | */ 85 | fun sessionThings(projectId: String, sessionId: String): Flow = 86 | firestore.collection("projects/$projectId/sessionVotes") 87 | .document(sessionId) 88 | .snapshots 89 | .mapNotNull { documentSnapshot -> 90 | if (documentSnapshot.exists.not()) { 91 | return@mapNotNull SessionThingsResult( 92 | emptyMap(), 93 | documentSnapshot.metadata.isFromCache 94 | ) 95 | } 96 | // See https://github.com/GitLiveApp/firebase-kotlin-sdk/issues/710 97 | val sessionData = documentSnapshot.encodedData() 98 | sessionData as Map 99 | val sessionThings = sessionData.mapValues { 100 | if (it.value is Long) { 101 | VoteItemCount(it.value as Long) 102 | } else if (it.value is Map<*, *>) { 103 | (it.value as Map).toCommentsMap() 104 | } else { 105 | error("expected a long or a map of comments, got '$this'") 106 | } 107 | } 108 | 109 | SessionThingsResult(sessionThings, documentSnapshot.metadata.isFromCache) 110 | } 111 | 112 | suspend fun setComment( 113 | projectId: String, 114 | userId: String, 115 | talkId: String, 116 | voteItemId: String, 117 | status: VoteStatus, 118 | text: String 119 | ) { 120 | if (text.trim() == "") return 121 | val collectionReference = firestore.collection("projects/$projectId/userVotes") 122 | val querySnapshot = collectionReference 123 | .where { "userId" equalTo userId } 124 | .where { "talkId" equalTo talkId } 125 | .where { "voteItemId" equalTo voteItemId } 126 | .get() 127 | /** 128 | * XXX: There may be a race here where we create 2 documents, not really sure under which circumstances 129 | */ 130 | if (querySnapshot.documents.isEmpty()) { 131 | val documentReference = collectionReference.document 132 | documentReference.set( 133 | mapOf( 134 | "id" to documentReference.id, 135 | "createdAt" to FieldValue.serverTimestamp, 136 | "projectId" to projectId, 137 | "status" to status.value, 138 | "talkId" to talkId, 139 | "updatedAt" to FieldValue.serverTimestamp, 140 | "userId" to userId, 141 | "voteItemId" to voteItemId, 142 | "text" to text.trim(), 143 | ) 144 | ) 145 | } else { 146 | querySnapshot.documents[0] 147 | collectionReference 148 | .document(querySnapshot.documents[0].id) 149 | .update( 150 | mapOf( 151 | "updatedAt" to FieldValue.serverTimestamp, 152 | "status" to status.value, 153 | "text" to text.trim() 154 | ) 155 | ) 156 | } 157 | } 158 | 159 | suspend fun setVote( 160 | projectId: String, 161 | userId: String, 162 | talkId: String, 163 | voteItemId: String, 164 | status: VoteStatus 165 | ) { 166 | val collectionReference = firestore.collection("projects/$projectId/userVotes") 167 | val querySnapshot = collectionReference 168 | .where { "userId" equalTo userId } 169 | .where { "talkId" equalTo talkId } 170 | .where { "voteItemId" equalTo voteItemId } 171 | .get() 172 | if (querySnapshot.documents.isEmpty()) { 173 | val documentReference = collectionReference.document 174 | documentReference.set( 175 | mapOf( 176 | "id" to documentReference.id, 177 | "createdAt" to FieldValue.serverTimestamp, 178 | "projectId" to projectId, 179 | "status" to status.value, 180 | "talkId" to talkId, 181 | "updatedAt" to FieldValue.serverTimestamp, 182 | "userId" to userId, 183 | "voteItemId" to voteItemId 184 | ) 185 | ) 186 | } else { 187 | collectionReference 188 | .document(querySnapshot.documents[0].id) 189 | .update( 190 | mapOf( 191 | "updatedAt" to FieldValue.serverTimestamp, 192 | "status" to status.value 193 | ) 194 | ) 195 | } 196 | } 197 | 198 | suspend fun upVote( 199 | projectId: String, 200 | userId: String, 201 | talkId: String, 202 | voteItemId: String, 203 | voteId: String, 204 | status: VoteStatus 205 | ) { 206 | val collectionReference = firestore.collection("projects/$projectId/userVotes") 207 | val querySnapshot = collectionReference 208 | .where { "userId" equalTo userId } 209 | .where { "talkId" equalTo talkId } 210 | .where { "voteItemId" equalTo voteItemId } 211 | .where { "voteId" equalTo voteId } 212 | .get() 213 | if (querySnapshot.documents.isEmpty()) { 214 | val documentReference = collectionReference.document 215 | documentReference.set( 216 | mapOf( 217 | "projectId" to projectId, 218 | "talkId" to talkId, 219 | "voteItemId" to voteItemId, 220 | "id" to documentReference.id, 221 | "voteId" to voteId, 222 | "createdAt" to FieldValue.serverTimestamp, 223 | "updatedAt" to FieldValue.serverTimestamp, 224 | "voteType" to "textPlus", 225 | "userId" to userId, 226 | "status" to status.value 227 | ) 228 | ) 229 | } else { 230 | collectionReference 231 | .document(querySnapshot.documents[0].id) 232 | .update( 233 | mapOf( 234 | "updatedAt" to FieldValue.serverTimestamp, 235 | "status" to status.value 236 | ) 237 | ) 238 | } 239 | } 240 | 241 | companion object Factory { 242 | fun create(app: FirebaseApp): OpenFeedbackFirestore { 243 | val firestore = Firebase.firestore(app) 244 | firestore.setSettings(persistenceEnabled = true) 245 | return OpenFeedbackFirestore(firestore) 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /openfeedback/src/iosMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.ios.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress( 2 | "CANNOT_OVERRIDE_INVISIBLE_MEMBER", 3 | "INVISIBLE_MEMBER", 4 | "INVISIBLE_REFERENCE", 5 | ) 6 | package io.openfeedback.mappers 7 | 8 | import dev.gitlive.firebase.firestore.Timestamp 9 | import kotlinx.datetime.Instant 10 | 11 | internal actual fun timestampToInstant(nativeTimestamp: Any): Instant { 12 | val ts = Timestamp(nativeTimestamp) 13 | return Instant.fromEpochSeconds(ts.seconds, ts.nanoseconds) 14 | } 15 | -------------------------------------------------------------------------------- /sample-app-android/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /sample-app-android/api/sample-app-android.api: -------------------------------------------------------------------------------- 1 | public final class io/openfeedback/android/MainActivity : androidx/appcompat/app/AppCompatActivity { 2 | public static final field $stable I 3 | public fun ()V 4 | } 5 | 6 | public final class io/openfeedback/android/MainApplication : android/app/Application { 7 | public static final field $stable I 8 | public field context Landroid/content/Context; 9 | public fun ()V 10 | public final fun getContext ()Landroid/content/Context; 11 | public fun onCreate ()V 12 | public final fun setContext (Landroid/content/Context;)V 13 | } 14 | 15 | -------------------------------------------------------------------------------- /sample-app-android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.gradleup.librarian.gradle.configureAndroidCompatibility 2 | 3 | plugins { 4 | id("com.android.application") 5 | id("org.jetbrains.kotlin.android") 6 | id("org.jetbrains.compose") 7 | id("org.jetbrains.kotlin.plugin.compose") 8 | } 9 | 10 | androidApp("io.openfeedback.android") 11 | 12 | android { 13 | defaultConfig { 14 | versionCode = 1 15 | versionName = "1" 16 | } 17 | } 18 | 19 | dependencies { 20 | implementation(projects.openfeedbackViewmodel) 21 | implementation(projects.sampleAppShared) 22 | 23 | implementation(libs.androidx.core.ktx) 24 | implementation(libs.androidx.appcompat) 25 | implementation(libs.androidx.activity.compose) 26 | 27 | implementation(compose.material3) 28 | } 29 | -------------------------------------------------------------------------------- /sample-app-android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /sample-app-android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /sample-app-android/src/main/java/io/openfeedback/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.android 2 | 3 | import SampleApp 4 | import android.annotation.SuppressLint 5 | import android.os.Bundle 6 | import androidx.activity.compose.setContent 7 | import androidx.appcompat.app.AppCompatActivity 8 | import androidx.compose.foundation.isSystemInDarkTheme 9 | 10 | class MainActivity : AppCompatActivity() { 11 | @SuppressLint("UnusedMaterialScaffoldPaddingParameter") 12 | override fun onCreate(savedInstanceState: Bundle?) { 13 | super.onCreate(savedInstanceState) 14 | 15 | setContent { 16 | val isDark = isSystemInDarkTheme() 17 | SampleApp( 18 | isSystemLight = !isDark, 19 | context = (application as MainApplication).context, 20 | ) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /sample-app-android/src/main/java/io/openfeedback/android/MainApplication.kt: -------------------------------------------------------------------------------- 1 | package io.openfeedback.android 2 | 3 | import android.app.Application 4 | import android.content.Context 5 | import io.openfeedback.viewmodels.OpenFeedbackFirebaseConfig 6 | import io.openfeedback.viewmodels.initializeOpenFeedback 7 | 8 | class MainApplication: Application() { 9 | lateinit var context: Context 10 | 11 | override fun onCreate() { 12 | super.onCreate() 13 | context = this 14 | } 15 | } -------------------------------------------------------------------------------- /sample-app-android/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /sample-app-android/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /sample-app-android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #6200EE 4 | #3700B3 5 | #03DAC5 6 | -------------------------------------------------------------------------------- /sample-app-android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | OpenFeedback Sample 3 | -------------------------------------------------------------------------------- /sample-app-android/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 14 | 15 |