├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | xmlns:android
19 |
20 | ^$
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | xmlns:.*
30 |
31 | ^$
32 |
33 |
34 | BY_NAME
35 |
36 |
37 |
38 |
39 |
40 |
41 | .*:id
42 |
43 | http://schemas.android.com/apk/res/android
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 | .*:name
53 |
54 | http://schemas.android.com/apk/res/android
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | name
64 |
65 | ^$
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 | style
75 |
76 | ^$
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 | .*
86 |
87 | ^$
88 |
89 |
90 | BY_NAME
91 |
92 |
93 |
94 |
95 |
96 |
97 | .*
98 |
99 | http://schemas.android.com/apk/res/android
100 |
101 |
102 | ANDROID_ATTRIBUTE_ORDER
103 |
104 |
105 |
106 |
107 |
108 |
109 | .*
110 |
111 | .*
112 |
113 |
114 | BY_NAME
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
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 | [](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 | 
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 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/sample-app-ios/io-openfeedback-ios-Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved:
--------------------------------------------------------------------------------
1 | {
2 | "originHash" : "a1569f9895aa2be8e24832f98525d5da4eb90b5d158a82691c15b47eb72a13d7",
3 | "pins" : [
4 | {
5 | "identity" : "abseil-cpp-binary",
6 | "kind" : "remoteSourceControl",
7 | "location" : "https://github.com/google/abseil-cpp-binary.git",
8 | "state" : {
9 | "revision" : "7ce7be095bc3ed3c98b009532fe2d7698c132614",
10 | "version" : "1.2024011601.0"
11 | }
12 | },
13 | {
14 | "identity" : "app-check",
15 | "kind" : "remoteSourceControl",
16 | "location" : "https://github.com/google/app-check.git",
17 | "state" : {
18 | "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2",
19 | "version" : "10.18.1"
20 | }
21 | },
22 | {
23 | "identity" : "firebase-ios-sdk",
24 | "kind" : "remoteSourceControl",
25 | "location" : "https://github.com/firebase/firebase-ios-sdk.git",
26 | "state" : {
27 | "revision" : "fcf5ced6dae2d43fced2581e673cc3b59bdb8ffa",
28 | "version" : "10.23.0"
29 | }
30 | },
31 | {
32 | "identity" : "googleappmeasurement",
33 | "kind" : "remoteSourceControl",
34 | "location" : "https://github.com/google/GoogleAppMeasurement.git",
35 | "state" : {
36 | "revision" : "6ec4ca62b00a665fa09b594fab897753a8c635fa",
37 | "version" : "10.23.0"
38 | }
39 | },
40 | {
41 | "identity" : "googledatatransport",
42 | "kind" : "remoteSourceControl",
43 | "location" : "https://github.com/google/GoogleDataTransport.git",
44 | "state" : {
45 | "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565",
46 | "version" : "9.4.0"
47 | }
48 | },
49 | {
50 | "identity" : "googleutilities",
51 | "kind" : "remoteSourceControl",
52 | "location" : "https://github.com/google/GoogleUtilities.git",
53 | "state" : {
54 | "revision" : "26c898aed8bed13b8a63057ee26500abbbcb8d55",
55 | "version" : "7.13.1"
56 | }
57 | },
58 | {
59 | "identity" : "grpc-binary",
60 | "kind" : "remoteSourceControl",
61 | "location" : "https://github.com/google/grpc-binary.git",
62 | "state" : {
63 | "revision" : "67043f6389d0e28b38fa02d1c6952afeb04d807f",
64 | "version" : "1.62.1"
65 | }
66 | },
67 | {
68 | "identity" : "gtm-session-fetcher",
69 | "kind" : "remoteSourceControl",
70 | "location" : "https://github.com/google/gtm-session-fetcher.git",
71 | "state" : {
72 | "revision" : "9534039303015a84837090d20fa21cae6e5eadb6",
73 | "version" : "3.3.2"
74 | }
75 | },
76 | {
77 | "identity" : "interop-ios-for-google-sdks",
78 | "kind" : "remoteSourceControl",
79 | "location" : "https://github.com/google/interop-ios-for-google-sdks.git",
80 | "state" : {
81 | "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648",
82 | "version" : "100.0.0"
83 | }
84 | },
85 | {
86 | "identity" : "leveldb",
87 | "kind" : "remoteSourceControl",
88 | "location" : "https://github.com/firebase/leveldb.git",
89 | "state" : {
90 | "revision" : "43aaef65e0c665daadf848761d560e446d350d3d",
91 | "version" : "1.22.4"
92 | }
93 | },
94 | {
95 | "identity" : "nanopb",
96 | "kind" : "remoteSourceControl",
97 | "location" : "https://github.com/firebase/nanopb.git",
98 | "state" : {
99 | "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1",
100 | "version" : "2.30910.0"
101 | }
102 | },
103 | {
104 | "identity" : "promises",
105 | "kind" : "remoteSourceControl",
106 | "location" : "https://github.com/google/promises.git",
107 | "state" : {
108 | "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac",
109 | "version" : "2.4.0"
110 | }
111 | },
112 | {
113 | "identity" : "swift-protobuf",
114 | "kind" : "remoteSourceControl",
115 | "location" : "https://github.com/apple/swift-protobuf.git",
116 | "state" : {
117 | "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8",
118 | "version" : "1.25.2"
119 | }
120 | }
121 | ],
122 | "version" : 3
123 | }
124 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcuserdata/mbonnin.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/paug/openfeedback-sdk-kotlin/340fda6d3266703e398730f22e968bcd52bea136/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcuserdata/mbonnin.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios.xcodeproj/xcshareddata/xcschemes/io.openfeedback.ios.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
10 |
16 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
43 |
45 |
51 |
52 |
53 |
54 |
58 |
59 |
60 |
61 |
67 |
69 |
75 |
76 |
77 |
78 |
80 |
81 |
84 |
85 |
86 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios.xcodeproj/xcuserdata/mbonnin.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | Promises (Playground) 1.xcscheme
8 |
9 | isShown
10 |
11 | orderHint
12 | 2
13 |
14 | Promises (Playground) 2.xcscheme
15 |
16 | isShown
17 |
18 | orderHint
19 | 3
20 |
21 | Promises (Playground).xcscheme
22 |
23 | isShown
24 |
25 | orderHint
26 | 0
27 |
28 | io.openfeedback.ios.xcscheme_^#shared#^_
29 |
30 | orderHint
31 | 1
32 |
33 |
34 | SuppressBuildableAutocreation
35 |
36 | 1167ADDB2BB0CFED001541D8
37 |
38 | primary
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "platform" : "ios",
6 | "size" : "1024x1024"
7 | }
8 | ],
9 | "info" : {
10 | "author" : "xcode",
11 | "version" : 1
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios/ContentView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ContentView.swift
3 | // io.openfeedback.ios
4 | //
5 | // Created by Martin on 24/03/2024.
6 | //
7 |
8 | import SwiftUI
9 | import UIKit
10 | import SwiftUI
11 | import SampleApp
12 | import FirebaseCore
13 |
14 | struct ComposeView: UIViewControllerRepresentable {
15 | func makeUIViewController(context: Context) -> UIViewController {
16 |
17 | // let options = FirebaseOptions(googleAppID: "1:635903227116:web:31de912f8bf29befb1e1c9", gcmSenderID: "lknlkn")
18 | // FirebaseApp.configure(options: options)
19 | //
20 | return MainViewControllerKt.MainViewController()
21 | }
22 |
23 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
24 | }
25 |
26 | struct ContentView: View {
27 | var body: some View {
28 | ComposeView()
29 | .ignoresSafeArea(.keyboard) // Compose has own keyboard handler
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/sample-app-ios/io.openfeedback.ios/io_openfeedback_iosApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // io_openfeedback_iosApp.swift
3 | // io.openfeedback.ios
4 | //
5 | // Created by Martin on 24/03/2024.
6 | //
7 |
8 | import SwiftUI
9 | import FirebaseCore
10 |
11 | class AppDelegate: NSObject, UIApplicationDelegate {
12 |
13 | func application(_ application: UIApplication,
14 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
15 | return true
16 | }
17 | }
18 |
19 | @main
20 | struct io_openfeedback_iosApp: App {
21 |
22 | @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate
23 |
24 | var body: some Scene {
25 | WindowGroup {
26 | ContentView()
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/sample-app-shared/api/sample-app-shared.api:
--------------------------------------------------------------------------------
1 | public final class ComposableSingletons$MainKt {
2 | public static final field INSTANCE LComposableSingletons$MainKt;
3 | public fun ()V
4 | public final fun getLambda-1$sample_app_shared_release ()Lkotlin/jvm/functions/Function3;
5 | }
6 |
7 | public final class MainKt {
8 | public static final fun OpenFeedbackTheme (ZLkotlin/jvm/functions/Function2;Landroidx/compose/runtime/Composer;I)V
9 | public static final fun SampleApp (ZLjava/lang/Object;Landroidx/compose/runtime/Composer;I)V
10 | public static final fun ThemeSwitcher (ZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;I)V
11 | }
12 |
13 |
--------------------------------------------------------------------------------
/sample-app-shared/api/sample-app-shared.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 /OpenFeedbackTheme(kotlin/Boolean, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int) // /OpenFeedbackTheme|OpenFeedbackTheme(kotlin.Boolean;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
10 | final fun /SampleApp(kotlin/Boolean, kotlin/Any?, androidx.compose.runtime/Composer?, kotlin/Int) // /SampleApp|SampleApp(kotlin.Boolean;kotlin.Any?;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
11 | final fun /ThemeSwitcher(kotlin/Boolean, kotlin/Function1, androidx.compose.runtime/Composer?, kotlin/Int) // /ThemeSwitcher|ThemeSwitcher(kotlin.Boolean;kotlin.Function1;androidx.compose.runtime.Composer?;kotlin.Int){}[0]
12 | final fun io.openfeedback.shared/MainViewController(): platform.UIKit/UIViewController // io.openfeedback.shared/MainViewController|MainViewController(){}[0]
13 |
--------------------------------------------------------------------------------
/sample-app-shared/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
2 |
3 | plugins {
4 | id("com.android.library")
5 | }
6 |
7 | library(
8 | namespace = "io.openfeedback.shared",
9 | compose = true
10 | ) { kotlinMultiplatformExtension ->
11 | with(kotlinMultiplatformExtension) {
12 | targets.forEach {
13 | if (it is KotlinNativeTarget) {
14 | it.binaries {
15 | this.framework {
16 | baseName = "SampleApp"
17 | isStatic = true
18 | }
19 | }
20 | }
21 | }
22 |
23 | kotlinMultiplatformExtension.sourceSets {
24 | getByName("commonMain") {
25 | dependencies {
26 | implementation(compose.ui)
27 | implementation(compose.foundation)
28 | implementation(compose.runtime)
29 | implementation(projects.openfeedbackViewmodel)
30 | implementation(compose.material3)
31 | }
32 | }
33 | }
34 | }
35 | }
36 |
37 |
--------------------------------------------------------------------------------
/sample-app-shared/librarian.module.properties:
--------------------------------------------------------------------------------
1 | publish=false
--------------------------------------------------------------------------------
/sample-app-shared/src/commonMain/kotlin/io/openfeedback/shared/main.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.foundation.layout.Arrangement
2 | import androidx.compose.foundation.layout.Column
3 | import androidx.compose.foundation.layout.Row
4 | import androidx.compose.foundation.layout.fillMaxWidth
5 | import androidx.compose.foundation.layout.padding
6 | import androidx.compose.foundation.lazy.LazyColumn
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Scaffold
9 | import androidx.compose.material3.Switch
10 | import androidx.compose.material3.Text
11 | import androidx.compose.material3.darkColorScheme
12 | import androidx.compose.material3.lightColorScheme
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.runtime.getValue
15 | import androidx.compose.runtime.mutableStateOf
16 | import androidx.compose.runtime.saveable.rememberSaveable
17 | import androidx.compose.runtime.setValue
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.unit.dp
21 | import io.openfeedback.OpenFeedback
22 | import io.openfeedback.viewmodels.OpenFeedbackFirebaseConfig
23 | import io.openfeedback.viewmodels.initializeOpenFeedback
24 |
25 | private var _initialized = false
26 | @Composable
27 | fun SampleApp(
28 | isSystemLight: Boolean,
29 | context: Any?,
30 | ) {
31 | if (!_initialized) {
32 | initializeOpenFeedback(OpenFeedbackFirebaseConfig.default(context))
33 | }
34 | var isLight by rememberSaveable(isSystemLight) { mutableStateOf(isSystemLight) }
35 | OpenFeedbackTheme(
36 | isLight = isLight
37 | ) {
38 | Scaffold {
39 | LazyColumn(contentPadding = it) {
40 | item {
41 | ThemeSwitcher(isLight = isLight) { isLight = it }
42 | }
43 | item {
44 | /**
45 | * The project and session Ids are taken from openfeedback.io demo conference:
46 | * https://openfeedback.io/eaJnyMXD3oNfhrrnBYDT/
47 | */
48 | OpenFeedback(
49 | projectId = "eaJnyMXD3oNfhrrnBYDT",
50 | sessionId = "100",
51 | modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
52 | )
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
59 | @Composable
60 | fun OpenFeedbackTheme(
61 | isLight: Boolean,
62 | content: @Composable () -> Unit
63 | ) {
64 | val colorScheme = when {
65 | else -> if (isLight) lightColorScheme() else darkColorScheme()
66 | }
67 | MaterialTheme(
68 | colorScheme = colorScheme,
69 | content = content
70 | )
71 | }
72 |
73 | @Composable
74 | fun ThemeSwitcher(
75 | isLight: Boolean,
76 | onLightDarkChanged: (Boolean) -> Unit
77 | ) {
78 | Column(
79 | horizontalAlignment = Alignment.CenterHorizontally,
80 | verticalArrangement = Arrangement.spacedBy(4.dp),
81 | modifier = Modifier.fillMaxWidth()
82 | ) {
83 | Row(
84 | horizontalArrangement = Arrangement.spacedBy(8.dp),
85 | verticalAlignment = Alignment.CenterVertically
86 | ) {
87 | Text(text = "Dark")
88 | Switch(checked = isLight, onCheckedChange = {
89 | onLightDarkChanged(!isLight)
90 | })
91 | Text(text = "Light")
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/sample-app-shared/src/iosMain/kotlin/io/openfeedback/shared/MainViewController.kt:
--------------------------------------------------------------------------------
1 | package io.openfeedback.shared
2 |
3 | import SampleApp
4 | import androidx.compose.ui.window.ComposeUIViewController
5 |
6 | fun MainViewController() = ComposeUIViewController {
7 | SampleApp(
8 | true,
9 | null
10 | )
11 | }
--------------------------------------------------------------------------------
/scripts/release.main.kts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env kotlin
2 | import java.io.File
3 |
4 | /**
5 | * A script to run locally in order to make a release.
6 | *
7 | * You need kotlin 1.3.70+ installed on your machine
8 | */
9 |
10 | fun runCommand(vararg args: String): String {
11 | val builder = ProcessBuilder(*args)
12 | .redirectError(ProcessBuilder.Redirect.INHERIT)
13 |
14 | val process = builder.start()
15 | val ret = process.waitFor()
16 |
17 | val output = process.inputStream.bufferedReader().readText()
18 | if (ret != 0) {
19 | throw java.lang.Exception("command ${args.joinToString(" ")} failed:\n$output")
20 | }
21 |
22 | return output
23 | }
24 |
25 | fun setCurrentVersion(version: String) {
26 | val gradleProperties = File("librarian.root.properties")
27 | val newContent = gradleProperties.readLines().map {
28 | it.replace(Regex("pom.version=.*"), "pom.version=$version")
29 | }.joinToString(separator = "\n", postfix = "\n")
30 | gradleProperties.writeText(newContent)
31 | }
32 |
33 | fun getCurrentVersion(): String {
34 | val versionLines = File("librarian.root.properties").readLines().filter { it.startsWith("pom.version=") }
35 |
36 | require(versionLines.isNotEmpty()) {
37 | "cannot find the version in ./gradle.properties"
38 | }
39 |
40 | require(versionLines.size == 1) {
41 | "multiple versions found in ./gradle.properties"
42 | }
43 |
44 | val regex = Regex("pom.version=(.*)-SNAPSHOT")
45 | val matchResult = regex.matchEntire(versionLines.first())
46 |
47 | require(matchResult != null) {
48 | "'${versionLines.first()}' doesn't match ${regex.pattern}"
49 | }
50 |
51 | return matchResult.groupValues[1]
52 | }
53 |
54 | val versionRegex = Regex("(?[0-9]+)\\.(?[0-9]+)\\.(?[0-9]+)(-(?alpha|beta)\\.(?[0-9]+))?(-SNAPSHOT)?")
55 | fun getNext(version: String, position: Int): String {
56 | val groupName = when (position) {
57 | 0 -> "major"
58 | 1 -> "minor"
59 | 2 -> "patch"
60 | 3 -> "prerelease"
61 | else -> throw IllegalArgumentException("position must be 0, 1, 2 or 3")
62 | }
63 | return version.replace(versionRegex) {
64 | when (groupName) {
65 | "major" -> "${it.groups["major"]!!.value.toInt() + 1}.0.0"
66 | "minor" -> "${it.groups["major"]!!.value}.${it.groups["minor"]!!.value.toInt() + 1}.0"
67 | "patch" -> "${it.groups["major"]!!.value}.${it.groups["minor"]!!.value}.${it.groups["patch"]!!.value.toInt() + 1}"
68 | "prerelease" -> {
69 | val prereleaseName = it.groups["prereleaseName"]?.value ?: "alpha"
70 | val newPrerelease = (it.groups["prerelease"]?.value?.toInt() ?: 0) + 1
71 | "${it.groups["major"]!!.value}.${it.groups["minor"]!!.value}.${it.groups["patch"]!!.value}-$prereleaseName.$newPrerelease"
72 | }
73 | else -> throw IllegalArgumentException("unknown group $groupName")
74 | }
75 | }
76 | }
77 |
78 | fun getNextPreRelease(version: String) = getNext(version, 3)
79 | fun getNextPatch(version: String) = getNext(version, 2)
80 | fun getNextMinor(version: String) = getNext(version, 1)
81 | fun getNextMajor(version: String) = getNext(version, 0)
82 |
83 | if (runCommand("git", "status", "--porcelain").isNotEmpty()) {
84 | println("Your git repo is not clean. Make sur to stash or commit your changes before making a release")
85 | System.exit(1)
86 | }
87 |
88 | val version = getCurrentVersion()
89 | val nextPreRelease = getNextPreRelease(version)
90 | val nextPatch = getNextPatch(version)
91 | val nextMinor = getNextMinor(version)
92 | val nextMinorAfterMinor = getNextMinor(nextMinor)
93 | val nextMajor = getNextMajor(version)
94 | val nextMinorAfterMajor = getNextMinor(nextMajor)
95 |
96 | var tagVersion: String = ""
97 | var nextSnapshot: String = ""
98 |
99 | while (tagVersion.isEmpty()) {
100 | println("Current version is '$version-SNAPSHOT'.")
101 | println("1. current: tag $version and bump to $nextMinor-SNAPSHOT")
102 | println("2. prerelease: tag $version and bump to $nextPreRelease-SNAPSHOT")
103 | println("3. patch: tag $nextPatch and bump to $nextMinor-SNAPSHOT")
104 | println("4. minor: tag $nextMinor and bump to $nextMinorAfterMinor-SNAPSHOT")
105 | println("5. major: tag $nextMajor and bump to $nextMinorAfterMajor-SNAPSHOT")
106 | println("What do you want to do [1/2/3/4/5]?")
107 |
108 | val answer = readLine()!!.trim()
109 | when (answer) {
110 | "1" -> {
111 | tagVersion = version
112 | nextSnapshot = "$nextMinor-SNAPSHOT"
113 | }
114 | "2" -> {
115 | tagVersion = version
116 | nextSnapshot = "$nextPreRelease-SNAPSHOT"
117 | }
118 | "3" -> {
119 | tagVersion = nextPatch
120 | nextSnapshot = "$nextMinor-SNAPSHOT"
121 | }
122 | "4" -> {
123 | tagVersion = nextMinor
124 | nextSnapshot = "$nextMinorAfterMinor-SNAPSHOT"
125 | }
126 | "5" -> {
127 | tagVersion = nextMajor
128 | nextSnapshot = "$nextMinorAfterMajor-SNAPSHOT"
129 | }
130 | }
131 | }
132 |
133 | setCurrentVersion(tagVersion)
134 |
135 | runCommand("git", "commit", "-a", "-m", "release $tagVersion")
136 | runCommand("git", "tag", "v$tagVersion")
137 |
138 | setCurrentVersion(nextSnapshot)
139 | runCommand("git", "commit", "-a", "-m", "version is now $nextSnapshot")
140 |
141 | println("Everything is done. Verify everything is ok and type `git push origin master` to trigger the new version.")
142 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "openfeedback-sdk-kotlin"
2 |
3 | pluginManagement {
4 | listOf(repositories, dependencyResolutionManagement.repositories).forEach {
5 | it.apply {
6 | mavenCentral()
7 | google()
8 | gradlePluginPortal()
9 | }
10 | }
11 | }
12 |
13 | includeBuild("build-logic")
14 |
15 | include(
16 | ":openfeedback",
17 | ":openfeedback-viewmodel",
18 | ":openfeedback-ui-models",
19 | ":openfeedback-m3",
20 | ":openfeedback-resources",
21 | ":sample-app-android",
22 | ":sample-app-shared",
23 | )
24 |
--------------------------------------------------------------------------------