diff --git a/README.md b/README.md index 5d4d650..cec83e5 100644 --- a/README.md +++ b/README.md @@ -57,8 +57,8 @@ repositories { val openfeedbackVersion = "0.1.2" dependencies { // Material 3 - implementation("io.openfeedback:feedback-android-sdk-m3:$openfeedbackVersion") + implementation("io.openfeedback:feedback-sdk-m3:$openfeedbackVersion") // ViewModel - implementation("io.openfeedback:feedback-android-sdk-viewmodel:$openfeedbackVersion") + implementation("io.openfeedback:feedback-sdk-viewmodel:$openfeedbackVersion") } ``` diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index f377ceb..cfec29f 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -1,34 +1,22 @@ plugins { `embedded-kotlin` - `kotlin-dsl` - `java-gradle-plugin` } +group = "build-logic" + repositories { mavenCentral() google() + gradlePluginPortal() } dependencies { + implementation(gradleApi()) implementation(libs.vespene) implementation(libs.kotlin.coroutines.core) implementation(libs.android.gradle.plugin) implementation(libs.kotlin.gradle.plugin) -} - -gradlePlugin { - plugins { - register("io.openfeedback.plugins.lib") { - id = "io.openfeedback.plugins.lib" - implementationClass = "io.openfeedback.plugins.LibraryPlugin" - } - register("io.openfeedback.plugins.compose.lib") { - id = "io.openfeedback.plugins.compose.lib" - implementationClass = "io.openfeedback.plugins.ComposeLibraryPlugin" - } - register("io.openfeedback.plugins.app") { - id = "io.openfeedback.plugins.app" - implementationClass = "io.openfeedback.plugins.AppPlugin" - } - } + implementation(libs.kotlin.serialization.plugin) + implementation(libs.moko.gradle.plugin) + implementation(libs.jetbrains.compose) } diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts index 5a07434..8de2683 100644 --- a/build-logic/settings.gradle.kts +++ b/build-logic/settings.gradle.kts @@ -1,9 +1,10 @@ +rootProject.name = "build-logic" enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") dependencyResolutionManagement { versionCatalogs { create("libs") { - from(files("../libs.versions.toml")) + from(files("../gradle/libs.versions.toml")) } } } diff --git a/build-logic/src/main/kotlin/io/openfeedback/EnvVarKeys.kt b/build-logic/src/main/kotlin/EnvVarKeys.kt similarity index 94% rename from build-logic/src/main/kotlin/io/openfeedback/EnvVarKeys.kt rename to build-logic/src/main/kotlin/EnvVarKeys.kt index 64f0bcf..2858425 100644 --- a/build-logic/src/main/kotlin/io/openfeedback/EnvVarKeys.kt +++ b/build-logic/src/main/kotlin/EnvVarKeys.kt @@ -1,5 +1,3 @@ -package io.openfeedback - object EnvVarKeys { object Nexus { const val username = "SONATYPE_NEXUS_USERNAME" @@ -16,4 +14,4 @@ object EnvVarKeys { const val event = "GITHUB_EVENT_NAME" const val ref = "GITHUB_REF" } -} +} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/accessors.kt b/build-logic/src/main/kotlin/accessors.kt new file mode 100644 index 0000000..26ea74a --- /dev/null +++ b/build-logic/src/main/kotlin/accessors.kt @@ -0,0 +1,23 @@ +import org.gradle.api.Project +import org.gradle.api.plugins.ExtensionAware +import org.jetbrains.compose.ComposePlugin +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + + +inline fun Project.extensionOrNull(): T? { + return extensions.findByType(T::class.java) +} + +inline fun Project.extension(): T { + return extensionOrNull() ?: error("No extension of type '${T::class.java.name}") +} + +inline fun Project.extension(block: T.() -> Unit) { + extension().apply(block) +} + + +val KotlinMultiplatformExtension.compose: ComposePlugin.Dependencies + get() { + return (this as ExtensionAware).extensions.getByName("compose") as ComposePlugin.Dependencies + } diff --git a/build-logic/src/main/kotlin/internal/Publishing.kt b/build-logic/src/main/kotlin/internal/Publishing.kt new file mode 100644 index 0000000..6fa76f0 --- /dev/null +++ b/build-logic/src/main/kotlin/internal/Publishing.kt @@ -0,0 +1,221 @@ +package internal + +import EnvVarKeys +import applyPublishingPlugin +import applySigningPlugin +import extension +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withTimeout +import net.mbonnin.vespene.lib.NexusStagingClient +import org.gradle.api.Project +import org.gradle.api.Task +import org.gradle.api.UnknownDomainObjectException +import org.gradle.api.artifacts.dsl.RepositoryHandler +import org.gradle.api.logging.LogLevel +import org.gradle.api.plugins.ExtensionContainer +import org.gradle.api.provider.Provider +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication +import org.gradle.api.tasks.TaskProvider +import org.gradle.plugins.signing.Sign +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget +import kotlin.time.Duration.Companion.minutes + +internal fun Project.configurePublications( + publishing: PublishingExtension, + artifactName: String +) { + val project = this + + publishing.publications.configureEach { + (it as MavenPublication).apply { + groupId = "io.openfeedback" + version = project.rootProject.version.toString() + artifactId = artifactName + + pom { + it.name.set(artifactName) + + it.description.set(artifactId) + it.url.set("https://github.com/paug/openfeedback-android-sdk") + + it.scm { + it.url.set("https://github.com/paug/openfeedback-android-sdk") + it.connection.set("https://github.com/paug/openfeedback-android-sdk") + it.developerConnection.set("https://github.com/paug/openfeedback-android-sdk") + } + + it.licenses { + it.license { + it.name.set("MIT License") + it.url.set("https://github.com/paug/openfeedback-android-sdk/blob/master/LICENSE") + } + } + + it.developers { + it.developer { + it.id.set("openfeedback team") + it.name.set("openfeedback team") + } + } + } + } + } +} + + +internal fun Project.configurePublishingInternal( + androidTarget: KotlinAndroidTarget +) { + val publishing = applyPublishingPlugin() + + /** + * Signing + */ + val privateKey = System.getenv(EnvVarKeys.GPG.privateKey) + val password = System.getenv(EnvVarKeys.GPG.password) + applySigningPlugin().apply { + // GPG_PRIVATE_KEY should contain the armoured private key that starts with -----BEGIN PGP PRIVATE KEY BLOCK----- + // It can be obtained with gpg --armour --export-secret-keys KEY_ID + useInMemoryPgpKeys( + privateKey, + password + ) + sign(publishing.publications) + } + + tasks.withType(Sign::class.java).configureEach { + it.isEnabled = !privateKey.isNullOrBlank() + } + + /** + * Android publication + */ + androidTarget.apply { + publishLibraryVariants("release") + } + + /** + * Pom + */ + configurePublications( + publishing = publishing, + artifactName = name + ) + + /** + * Repositories + */ + publishing.repositories { + it.mavenSonatypeSnapshot(project = project) + it.mavenSonatypeStaging(project = project) + } + + rootProject.tasks.named("ossStagingRelease").configure { + it.dependsOn(this@configurePublishingInternal.tasks.named("publishAllPublicationsToOssStagingRepository")) + } +} + +private fun Project.getOrCreateRepoIdTask(): TaskProvider { + return try { + rootProject.tasks.named("createStagingRepo") + } catch (e: UnknownDomainObjectException) { + rootProject.tasks.register("createStagingRepo") { + it.outputs.file(rootProject.layout.buildDirectory.file("stagingRepoId")) + + it.doLast { + val repoId = runBlocking { + nexusStagingClient.createRepository( + profileId = System.getenv(EnvVarKeys.Nexus.profileId), + description = "io.openfeedback ${rootProject.version}" + ) + } + logger.log(LogLevel.LIFECYCLE, "repo created: $repoId") + it.outputs.files.singleFile.writeText(repoId) + } + } + } +} + +fun Project.publishIfNeededTaskProvider(): TaskProvider { + return try { + tasks.named("publishIfNeeded") + } catch (ignored: Exception) { + tasks.register("publishIfNeeded") + } +} + +private val baseUrl = "https://s01.oss.sonatype.org/service/local/" + +private val nexusStagingClient by lazy { + NexusStagingClient( + baseUrl = baseUrl, + username = System.getenv(EnvVarKeys.Nexus.username) + ?: error("please set the ${EnvVarKeys.Nexus.username} environment variable"), + password = System.getenv(EnvVarKeys.Nexus.password) + ?: error("please set the ${EnvVarKeys.Nexus.password} environment variable"), + ) +} + +fun Project.getOrCreateRepoId(): Provider { + return getOrCreateRepoIdTask().map { + it.outputs.files.singleFile.readText() + } +} + +fun Project.getOrCreateRepoUrl(): Provider { + return getOrCreateRepoId().map { "${baseUrl}staging/deployByRepositoryId/$it/" } +} + +fun Task.closeAndReleaseStagingRepository(repoId: String) { + runBlocking { + logger.log(LogLevel.LIFECYCLE, "Closing repository $repoId") + nexusStagingClient.closeRepositories(listOf(repoId)) + withTimeout(5.minutes) { + nexusStagingClient.waitForClose(repoId, 1000) { + logger.log(LogLevel.LIFECYCLE, ".") + } + } + nexusStagingClient.releaseRepositories(listOf(repoId), true) + } +} + +internal fun Project.registerReleaseTask(name: String): TaskProvider { + val task = try { + rootProject.tasks.named(name) + } catch (e: UnknownDomainObjectException) { + val repoId = getOrCreateRepoId() + rootProject.tasks.register(name) { + it.inputs.property( + "repoId", + repoId + ) + it.doLast { + it.closeAndReleaseStagingRepository(it.inputs.properties.get("repoId") as String) + } + } + } + + return task +} + +fun RepositoryHandler.mavenSonatypeSnapshot(project: Project) = maven { + it.name = "ossSnapshots" + it.url = project.uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") + it.credentials { + it.username = System.getenv(EnvVarKeys.Nexus.username) + it.password = System.getenv(EnvVarKeys.Nexus.password) + } +} + +fun RepositoryHandler.mavenSonatypeStaging(project: Project) = maven { + it.name = "ossStaging" + it.setUrl { + project.uri(project.getOrCreateRepoUrl()) + } + it.credentials { + it.username = System.getenv(EnvVarKeys.Nexus.username) + it.password = System.getenv(EnvVarKeys.Nexus.password) + } +} + diff --git a/build-logic/src/main/kotlin/io/openfeedback/OpenFeedback.kt b/build-logic/src/main/kotlin/io/openfeedback/OpenFeedback.kt deleted file mode 100644 index 565c943..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/OpenFeedback.kt +++ /dev/null @@ -1,10 +0,0 @@ -package io.openfeedback - -import io.openfeedback.extensions.configurePublishingInternal -import org.gradle.api.Project - -open class OpenFeedback(val project: Project) { - fun Project.configurePublishing(artifactName: String) { - project.configurePublishingInternal(artifactName) - } -} diff --git a/build-logic/src/main/kotlin/io/openfeedback/extensions/ExtensionContainerExt.kt b/build-logic/src/main/kotlin/io/openfeedback/extensions/ExtensionContainerExt.kt deleted file mode 100644 index 0feb1d2..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/extensions/ExtensionContainerExt.kt +++ /dev/null @@ -1,34 +0,0 @@ -package io.openfeedback.extensions - -import io.openfeedback.EnvVarKeys -import org.gradle.api.Project -import org.gradle.api.plugins.ExtensionContainer -import org.gradle.api.publish.PublishingExtension -import org.gradle.plugins.signing.SigningExtension - -fun ExtensionContainer.configurePublishing( - project: Project, - artifactName: String -) = getByType(PublishingExtension::class.java).apply { - publications { - createReleasePublication( - project = project, - artifactName = artifactName - ) - } - - repositories { - mavenSonatypeSnapshot(project = project) - mavenSonatypeStaging(project = project) - } -} - -fun ExtensionContainer.configureSigning() = configure(SigningExtension::class.java) { - // GPG_PRIVATE_KEY should contain the armoured private key that starts with -----BEGIN PGP PRIVATE KEY BLOCK----- - // It can be obtained with gpg --armour --export-secret-keys KEY_ID - useInMemoryPgpKeys( - System.getenv(EnvVarKeys.GPG.privateKey), - System.getenv(EnvVarKeys.GPG.password) - ) - sign((getByName("publishing") as PublishingExtension).publications) -} diff --git a/build-logic/src/main/kotlin/io/openfeedback/extensions/LibraryExtensionExt.kt b/build-logic/src/main/kotlin/io/openfeedback/extensions/LibraryExtensionExt.kt deleted file mode 100644 index bedda6a..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/extensions/LibraryExtensionExt.kt +++ /dev/null @@ -1,50 +0,0 @@ -@file:Suppress("UnstableApiUsage") - -package io.openfeedback.extensions - -import com.android.build.api.dsl.CommonExtension -import org.gradle.api.JavaVersion -import org.gradle.api.Project -import org.gradle.api.artifacts.VersionCatalogsExtension -import org.gradle.api.plugins.ExtensionAware -import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.getByType -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions - -internal fun CommonExtension<*, *, *, *, *>.configureKotlinAndroid() { - compileSdk = 34 - - defaultConfig { - minSdk = 21 - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } -} - -internal fun CommonExtension<*, *, *, *, *>.configureAndroidCompose(project: Project) { - val libs = project.extensions.getByType().named("libs") - - buildFeatures { - compose = true - } - - composeOptions { - kotlinCompilerExtensionVersion = libs.findVersion("androidx_compose_compiler").get().toString() - } - - project.dependencies { - add("debugImplementation", "androidx.customview:customview-poolingcontainer:1.0.0") - add("debugImplementation", "androidx.savedstate:savedstate-ktx:1.2.0") - } -} - -private fun CommonExtension<*, *, *, *, *>.kotlinOptions(block: KotlinJvmOptions.() -> Unit) { - (this as ExtensionAware).extensions.configure("kotlinOptions", block) -} diff --git a/build-logic/src/main/kotlin/io/openfeedback/extensions/ProjectExt.kt b/build-logic/src/main/kotlin/io/openfeedback/extensions/ProjectExt.kt deleted file mode 100644 index 9d6bbcd..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/extensions/ProjectExt.kt +++ /dev/null @@ -1,148 +0,0 @@ -package io.openfeedback.extensions - -import com.android.build.gradle.internal.tasks.factory.dependsOn -import io.openfeedback.EnvVarKeys -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withTimeout -import net.mbonnin.vespene.lib.NexusStagingClient -import org.gradle.api.Project -import org.gradle.api.Task -import org.gradle.api.UnknownDomainObjectException -import org.gradle.api.logging.LogLevel -import org.gradle.api.provider.Provider -import org.gradle.api.tasks.TaskProvider -import org.gradle.plugins.signing.Sign -import kotlin.time.Duration.Companion.minutes -import kotlin.time.ExperimentalTime - -internal fun Project.configurePublishingInternal(artifactName: String) { - println("configurePublishingInternal for $name") - val android = extensions.findByType(com.android.build.gradle.LibraryExtension::class.java)!! - - android.publishing { - singleVariant("release") { - withJavadocJar() - withSourcesJar() - } - } - - extensions.configurePublishing( - project = this@configurePublishingInternal, - artifactName = artifactName - ) - - extensions.configureSigning() - - tasks.withType(Sign::class.java).configureEach { - isEnabled = !System.getenv(EnvVarKeys.GPG.privateKey).isNullOrBlank() - } - - rootProject.tasks.named("ossStagingRelease").configure { - dependsOn(this@configurePublishingInternal.tasks.named("publishAllPublicationsToOssStagingRepository")) - } -} - -private fun Project.getOrCreateRepoIdTask(): TaskProvider { - return try { - rootProject.tasks.named("createStagingRepo") - } catch (e: UnknownDomainObjectException) { - rootProject.tasks.register("createStagingRepo") { - outputs.file(rootProject.layout.buildDirectory.file("stagingRepoId")) - - doLast { - val repoId = runBlocking { - nexusStagingClient.createRepository( - profileId = System.getenv(EnvVarKeys.Nexus.profileId), - description = "io.openfeedback ${rootProject.version}" - ) - } - logger.log(LogLevel.LIFECYCLE, "repo created: $repoId") - this.outputs.files.singleFile.writeText(repoId) - } - } - } -} - -fun Project.publishIfNeededTaskProvider(): TaskProvider { - return try { - tasks.named("publishIfNeeded") - } catch (ignored: Exception) { - tasks.register("publishIfNeeded") - } -} - -private val baseUrl = "https://s01.oss.sonatype.org/service/local/" - -private val nexusStagingClient by lazy { - NexusStagingClient( - baseUrl = baseUrl, - username = System.getenv(EnvVarKeys.Nexus.username) - ?: error("please set the ${EnvVarKeys.Nexus.username} environment variable"), - password = System.getenv(EnvVarKeys.Nexus.password) - ?: error("please set the ${EnvVarKeys.Nexus.password} environment variable"), - ) -} - -fun Project.getOrCreateRepoId(): Provider { - return getOrCreateRepoIdTask().map { - it.outputs.files.singleFile.readText() - } -} - -fun Project.getOrCreateRepoUrl(): Provider { - return getOrCreateRepoId().map { "${baseUrl}staging/deployByRepositoryId/$it/" } -} - -fun Task.closeAndReleaseStagingRepository(repoId: String) { - runBlocking { - logger.log(LogLevel.LIFECYCLE, "Closing repository $repoId") - nexusStagingClient.closeRepositories(listOf(repoId)) - withTimeout(5.minutes) { - nexusStagingClient.waitForClose(repoId, 1000) { - logger.log(LogLevel.LIFECYCLE, ".") - } - } - nexusStagingClient.releaseRepositories(listOf(repoId), true) - } -} - -private fun Project.registerReleaseTask(name: String): TaskProvider { - val task = try { - rootProject.tasks.named(name) - } catch (e: UnknownDomainObjectException) { - val repoId = getOrCreateRepoId() - rootProject.tasks.register(name) { - inputs.property( - "repoId", - repoId - ) - doLast { - closeAndReleaseStagingRepository(inputs.properties.get("repoId") as String) - } - } - } - - return task -} - -fun Project.configureRoot() { - check(this == rootProject) { - "configureRoot must be called from the root project" - } - - val publishIfNeeded = project.publishIfNeededTaskProvider() - val ossStagingReleaseTask = project.registerReleaseTask("ossStagingRelease") - - val eventName = System.getenv(EnvVarKeys.GitHub.event) - val ref = System.getenv(EnvVarKeys.GitHub.ref) - - if (eventName == "push" && ref == "refs/heads/main" && project.version.toString().endsWith("SNAPSHOT")) { - project.logger.log(LogLevel.LIFECYCLE, "Deploying snapshot to OssSnapshot...") - publishIfNeeded.dependsOn(project.tasks.named("publishAllPublicationsToOssSnapshotsRepository")) - } - - if (ref?.startsWith("refs/tags/") == true) { - project.logger.log(LogLevel.LIFECYCLE, "Deploying release to OssStaging...") - publishIfNeeded.dependsOn(ossStagingReleaseTask) - } -} \ No newline at end of file diff --git a/build-logic/src/main/kotlin/io/openfeedback/extensions/PublicationContainerExt.kt b/build-logic/src/main/kotlin/io/openfeedback/extensions/PublicationContainerExt.kt deleted file mode 100644 index 7f3ae1f..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/extensions/PublicationContainerExt.kt +++ /dev/null @@ -1,47 +0,0 @@ -package io.openfeedback.extensions - -import org.gradle.api.Project -import org.gradle.api.publish.PublicationContainer -import org.gradle.api.publish.maven.MavenPublication - -fun PublicationContainer.createReleasePublication( - project: Project, - artifactName: String -) = create("default", MavenPublication::class.java) { - apply { - project.afterEvaluate { - from(components.findByName("release")) - } - - pom { - groupId = "io.openfeedback" - artifactId = artifactName - version = project.rootProject.version.toString() - - name.set(artifactId) - packaging = "aar" - description.set(artifactId) - url.set("https://github.com/paug/openfeedback-android-sdk") - - scm { - url.set("https://github.com/paug/openfeedback-android-sdk") - connection.set("https://github.com/paug/openfeedback-android-sdk") - developerConnection.set("https://github.com/paug/openfeedback-android-sdk") - } - - licenses { - license { - name.set("MIT License") - url.set("https://github.com/paug/openfeedback-android-sdk/blob/master/LICENSE") - } - } - - developers { - developer { - id.set("openfeedback team") - name.set("openfeedback team") - } - } - } - } -} diff --git a/build-logic/src/main/kotlin/io/openfeedback/extensions/RepositoryHandlerExt.kt b/build-logic/src/main/kotlin/io/openfeedback/extensions/RepositoryHandlerExt.kt deleted file mode 100644 index 61353cf..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/extensions/RepositoryHandlerExt.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.openfeedback.extensions - -import io.openfeedback.EnvVarKeys -import org.gradle.api.Project -import org.gradle.api.artifacts.dsl.RepositoryHandler - -fun RepositoryHandler.mavenSonatypeSnapshot(project: Project) = maven { - name = "ossSnapshots" - url = project.uri("https://s01.oss.sonatype.org/content/repositories/snapshots/") - credentials { - username = System.getenv(EnvVarKeys.Nexus.username) - password = System.getenv(EnvVarKeys.Nexus.password) - } -} - -fun RepositoryHandler.mavenSonatypeStaging(project: Project) = maven { - name = "ossStaging" - setUrl { - project.uri(project.getOrCreateRepoUrl()) - } - credentials { - username = System.getenv(EnvVarKeys.Nexus.username) - password = System.getenv(EnvVarKeys.Nexus.password) - } -} diff --git a/build-logic/src/main/kotlin/io/openfeedback/extensions/TaskContainerExt.kt b/build-logic/src/main/kotlin/io/openfeedback/extensions/TaskContainerExt.kt deleted file mode 100644 index 5bf5fb3..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/extensions/TaskContainerExt.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.openfeedback.extensions - -import org.gradle.api.JavaVersion -import org.gradle.api.tasks.TaskContainer -import org.gradle.kotlin.dsl.withType -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - -fun TaskContainer.configureKotlinCompiler() = withType { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } -} diff --git a/build-logic/src/main/kotlin/io/openfeedback/plugins/AppPlugin.kt b/build-logic/src/main/kotlin/io/openfeedback/plugins/AppPlugin.kt deleted file mode 100644 index 29a01f6..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/plugins/AppPlugin.kt +++ /dev/null @@ -1,24 +0,0 @@ -package io.openfeedback.plugins - -import com.android.build.gradle.internal.dsl.BaseAppModuleExtension -import io.openfeedback.extensions.configureAndroidCompose -import io.openfeedback.extensions.configureKotlinAndroid -import io.openfeedback.extensions.configureKotlinCompiler -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.configure - -class AppPlugin : Plugin { - override fun apply(target: Project) { - with(target.pluginManager) { - apply("com.android.application") - apply("org.jetbrains.kotlin.android") - } - - target.extensions.configure { - configureKotlinAndroid() - configureAndroidCompose(target) - } - target.tasks.configureKotlinCompiler() - } -} diff --git a/build-logic/src/main/kotlin/io/openfeedback/plugins/ComposeLibraryPlugin.kt b/build-logic/src/main/kotlin/io/openfeedback/plugins/ComposeLibraryPlugin.kt deleted file mode 100644 index fd673c3..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/plugins/ComposeLibraryPlugin.kt +++ /dev/null @@ -1,29 +0,0 @@ -package io.openfeedback.plugins - -import com.android.build.gradle.LibraryExtension -import io.openfeedback.OpenFeedback -import io.openfeedback.extensions.configureAndroidCompose -import io.openfeedback.extensions.configureKotlinAndroid -import io.openfeedback.extensions.configureKotlinCompiler -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.configure - -class ComposeLibraryPlugin : Plugin { - override fun apply(target: Project) { - with(target.pluginManager) { - apply("com.android.library") - apply("org.jetbrains.kotlin.android") - apply("maven-publish") - apply("signing") - } - - target.extensions.create("openfeedback", OpenFeedback::class.java, target) - target.extensions.configure { - configureKotlinAndroid() - configureAndroidCompose(target) - defaultConfig.targetSdk = 34 - } - target.tasks.configureKotlinCompiler() - } -} diff --git a/build-logic/src/main/kotlin/io/openfeedback/plugins/LibraryPlugin.kt b/build-logic/src/main/kotlin/io/openfeedback/plugins/LibraryPlugin.kt deleted file mode 100644 index a8a0e26..0000000 --- a/build-logic/src/main/kotlin/io/openfeedback/plugins/LibraryPlugin.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.openfeedback.plugins - -import com.android.build.gradle.LibraryExtension -import io.openfeedback.OpenFeedback -import io.openfeedback.extensions.configureKotlinAndroid -import io.openfeedback.extensions.configureKotlinCompiler -import org.gradle.api.Plugin -import org.gradle.api.Project -import org.gradle.kotlin.dsl.configure - -class LibraryPlugin : Plugin { - override fun apply(target: Project) { - with(target.pluginManager) { - apply("com.android.library") - apply("org.jetbrains.kotlin.android") - apply("maven-publish") - apply("signing") - } - - target.extensions.create("openfeedback", OpenFeedback::class.java, target) - target.extensions.configure { - configureKotlinAndroid() - defaultConfig.targetSdk = 34 - } - target.tasks.configureKotlinCompiler() - } -} diff --git a/build-logic/src/main/kotlin/main.kt b/build-logic/src/main/kotlin/main.kt new file mode 100644 index 0000000..8340d0a --- /dev/null +++ b/build-logic/src/main/kotlin/main.kt @@ -0,0 +1,112 @@ +import com.android.build.api.dsl.CommonExtension +import com.android.build.gradle.internal.tasks.factory.dependsOn +import dev.icerock.gradle.MultiplatformResourcesPluginExtension +import internal.configurePublishingInternal +import internal.publishIfNeededTaskProvider +import internal.registerReleaseTask +import org.gradle.api.JavaVersion +import org.gradle.api.Project +import org.gradle.api.logging.LogLevel +import org.jetbrains.kotlin.gradle.dsl.KotlinCompile +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +private fun Project.configureAndroid(namespace: String) { + extensions.getByName("android").apply { + this as CommonExtension<*,*,*,*,*> + compileSdk = 34 + this.namespace = namespace + + defaultConfig { + minSdk = 23 + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + } +} + +private fun Project.configureKotlin() { + tasks.withType(KotlinCompile::class.java) { + it.kotlinOptions { + if (this is KotlinJvmOptions) { + jvmTarget = JavaVersion.VERSION_17.toString() + } + } + it.kotlinOptions.freeCompilerArgs += "-Xexpect-actual-classes" + } +} + +private fun Project.configureKMP() { + (extensions.getByName("kotlin") as KotlinMultiplatformExtension).apply { + applyDefaultHierarchyTemplate() + androidTarget() + iosX64() + iosArm64() + iosSimulatorArm64() + } +} + +fun Project.configureMoko(namespace: String) { + pluginManager.apply("dev.icerock.mobile.multiplatform-resources") + extensions.getByType(MultiplatformResourcesPluginExtension::class.java).apply { + resourcesPackage.set(namespace) + } +} + +fun Project.library( + namespace: String, + moko: Boolean = false, + compose: Boolean = false, + publish: Boolean = false, + kotlin: (KotlinMultiplatformExtension) -> Unit +) { + val kotlinMultiplatformExtension = applyKotlinMultiplatformPlugin() + if (compose) { + applyJetbrainsComposePlugin() + } + if (publish) { + configurePublishingInternal(kotlinMultiplatformExtension.androidTarget()) + } + configureAndroid(namespace = namespace) + configureKMP() + configureKotlin() + + kotlin(kotlinMultiplatformExtension) + + if (moko) { + configureMoko(namespace) + } +} + +fun Project.androidApp( + namespace: String, +) { + configureAndroid(namespace = namespace) + configureKotlin() +} + + +fun Project.configureRoot() { + check(this == rootProject) { + "configureRoot must be called from the root project" + } + + val publishIfNeeded = project.publishIfNeededTaskProvider() + val ossStagingReleaseTask = project.registerReleaseTask("ossStagingRelease") + + val eventName = System.getenv(EnvVarKeys.GitHub.event) + val ref = System.getenv(EnvVarKeys.GitHub.ref) + + if (eventName == "push" && ref == "refs/heads/main" && project.version.toString().endsWith("SNAPSHOT")) { + project.logger.log(LogLevel.LIFECYCLE, "Deploying snapshot to OssSnapshot...") + publishIfNeeded.dependsOn(project.tasks.named("publishAllPublicationsToOssSnapshotsRepository")) + } + + if (ref?.startsWith("refs/tags/") == true) { + project.logger.log(LogLevel.LIFECYCLE, "Deploying release to OssStaging...") + publishIfNeeded.dependsOn(ossStagingReleaseTask) + } +} diff --git a/build-logic/src/main/kotlin/plugins.kt b/build-logic/src/main/kotlin/plugins.kt new file mode 100644 index 0000000..ea39a37 --- /dev/null +++ b/build-logic/src/main/kotlin/plugins.kt @@ -0,0 +1,25 @@ +import org.gradle.api.Project +import org.gradle.api.publish.PublishingExtension +import org.gradle.plugins.signing.SigningExtension +import org.jetbrains.compose.ComposeExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension + +fun Project.applyPublishingPlugin(): PublishingExtension { + pluginManager.apply("maven-publish") + return extension() +} + +fun Project.applySigningPlugin(): SigningExtension { + pluginManager.apply("signing") + return extension() +} + +fun Project.applyKotlinMultiplatformPlugin(): KotlinMultiplatformExtension { + pluginManager.apply("org.jetbrains.kotlin.multiplatform") + return extension() +} + +fun Project.applyJetbrainsComposePlugin(): ComposeExtension { + pluginManager.apply("org.jetbrains.compose") + return extension() +} diff --git a/build.gradle.kts b/build.gradle.kts index 5032350..cc78df7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,14 +1,20 @@ -import io.openfeedback.extensions.configureRoot - -plugins { - id("io.openfeedback.plugins.compose.lib") apply false +buildscript { + repositories { + mavenCentral() + google() + gradlePluginPortal() + } + dependencies { + //noinspection UseTomlInstead + classpath("build-logic:build-logic") + } } version = "0.1.2" -subprojects { +allprojects { repositories { - google() mavenCentral() + google() } } diff --git a/gradle.properties b/gradle.properties index e184d04..8ef201b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,3 +8,4 @@ kotlin.code.style=official android.useAndroidX=true systemProp.org.gradle.internal.publish.checksums.insecure=true +moko.resources.disableStaticFrameworkWarning=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..1d749e6 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,51 @@ +[versions] +activityCompose = "1.8.2" +appcompat = "1.6.1" +kotlin_lang = "1.9.22" +kotlin_coroutines = "1.8.0" +kotlinx_datetime = "0.5.0" +kotlinx_serialization = "1.6.3" +androidx_core = "1.12.0" +firebase_common = "20.4.3" +firebase_firestore = "24.11.0" +firebase_auth = "22.3.1" +gitlive_firebase = "1.11.1" +moko_mvvm = "0.16.1" +moko_resources = "0.24.0-alpha-5" +kmm_locale = "0.5.0" +kermit = "2.0.2" + +[libraries] +activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } +android-gradle-plugin = "com.android.tools.build:gradle:8.1.2" +appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" } +vespene = "net.mbonnin.vespene:vespene-lib:0.5" + +kotlin_gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_lang" } +kotlin_serialization-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_lang" } +kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin_coroutines" } + +kotlinx_datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx_datetime" } +kotlinx_serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx_serialization" } + +firebase_common = { module = "com.google.firebase:firebase-common", version.ref = "firebase_common" } +firebase_firestore = { module = "com.google.firebase:firebase-firestore-ktx", version.ref = "firebase_firestore" } +firebase_auth = { module = "com.google.firebase:firebase-auth", version.ref = "firebase_auth" } +gitlive_app = { module = "dev.gitlive:firebase-app", version.ref = "gitlive_firebase" } +gitlive_firestore = { module = "dev.gitlive:firebase-firestore", version.ref = "gitlive_firebase" } +gitlive_auth = { module = "dev.gitlive:firebase-auth", version.ref = "gitlive_firebase" } +gitlive_common = { module = "dev.gitlive:firebase-common", version.ref = "gitlive_firebase" } + +androidx_core = { module = "androidx.core:core-ktx", version.ref = "androidx_core" } + +moko_resources = { module = "dev.icerock.moko:resources", version.ref = "moko_resources"} +moko_resources_compose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko_resources" } +moko_gradle_plugin = { module = "dev.icerock.moko:resources-generator", version.ref = "moko_resources" } +moko_mvvm_core = { module = "dev.icerock.moko:mvvm-core", version.ref = "moko_mvvm" } +moko_mvvm_compose = { module = "dev.icerock.moko:mvvm-compose", version.ref = "moko_mvvm" } + +kmm_locale = { module = "com.vanniktech:multiplatform-locale", version.ref = "kmm_locale" } + +kermit = { module = "co.touchlab:kermit", version.ref = "kermit" } + +jetbrains_compose = "org.jetbrains.compose:compose-gradle-plugin:1.6.1" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ebb793f..b82aa23 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-rc-2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/libs.versions.toml b/libs.versions.toml deleted file mode 100644 index 89ecd65..0000000 --- a/libs.versions.toml +++ /dev/null @@ -1,35 +0,0 @@ -[versions] -kotlin_lang = "1.9.10" -kotlin_coroutines = "1.7.3" -androidx_core = "1.12.0" -androidx_compose_bom = "2023.10.01" -androidx_compose_compiler = "1.5.3" -androidx_lifecycle = "2.6.2" -androidx_savedstate = "1.2.1" -firebase_firestore = "24.0.1" -firebase_auth = "21.0.1" - -[libraries] -android-gradle-plugin = "com.android.tools.build:gradle:8.1.2" -vespene = "net.mbonnin.vespene:vespene-lib:0.5" - -kotlin_gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin_lang" } -kotlin-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin_coroutines" } -kotlin-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlin_coroutines" } -kotlin-coroutines-play-services = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-play-services", version.ref = "kotlin_coroutines" } - -firebase_firestore = { module = "com.google.firebase:firebase-firestore-ktx", version.ref = "firebase_firestore" } -firebase_auth = { module = "com.google.firebase:firebase-auth", version.ref = "firebase_auth" } - -androidx_core = { module = "androidx.core:core-ktx", version.ref = "androidx_core" } -androidx_savedstate = { module = "androidx.savedstate:savedstate-ktx", version.ref = "androidx_savedstate" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx_compose_bom" } -androidx_compose_material = { module = "androidx.compose.material:material" } -androidx_compose_material3 = { module = "androidx.compose.material3:material3" } -androidx_compose_ui = { module = "androidx.compose.ui:ui" } -androidx_compose_runtime = { module = "androidx.compose.runtime:runtime" } -androidx_compose_foundation = { module = "androidx.compose.foundation:foundation" } -androidx_compose_uitooling = { module = "androidx.compose.ui:ui-tooling" } -androidx_lifecycle_viewmodel_compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx_lifecycle" } -androidx_lifecycle_runtime = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidx_lifecycle" } -androidx_lifecycle_viewmodel_ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx_lifecycle" } diff --git a/openfeedback-m2/build.gradle.kts b/openfeedback-m2/build.gradle.kts deleted file mode 100644 index cffe39d..0000000 --- a/openfeedback-m2/build.gradle.kts +++ /dev/null @@ -1,22 +0,0 @@ - -plugins { - id("io.openfeedback.plugins.compose.lib") -} - -android { - namespace = "io.openfeedback.android.m2" -} - -openfeedback { - configurePublishing("feedback-android-sdk-m2") -} - -dependencies { - api(projects.openfeedback) - api(projects.openfeedbackViewmodel) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.material) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.uitooling) -} diff --git a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/Loading.kt b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/Loading.kt deleted file mode 100644 index f6b7d33..0000000 --- a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/Loading.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.openfeedback.android.m2 - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview - -@Composable -fun Loading(modifier: Modifier = Modifier) { - Box( - modifier = modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } -} - -@Preview -@Composable -internal fun LoadingPreview() { - Loading() -} diff --git a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/OpenFeedbackLayout.kt b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/OpenFeedbackLayout.kt deleted file mode 100644 index 9c9693e..0000000 --- a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/OpenFeedbackLayout.kt +++ /dev/null @@ -1,70 +0,0 @@ -package io.openfeedback.android.m2 - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import io.openfeedback.android.viewmodels.OpenFeedbackFirebaseConfig -import io.openfeedback.android.viewmodels.OpenFeedbackUiState -import io.openfeedback.android.viewmodels.OpenFeedbackViewModel -import io.openfeedback.android.viewmodels.models.UISessionFeedback -import io.openfeedback.android.viewmodels.models.UIVoteItem - -@Deprecated("Please use m3 artifact to display OpenFeedback") -@Composable -fun OpenFeedback( - config: OpenFeedbackFirebaseConfig, - projectId: String, - sessionId: String, - modifier: Modifier = Modifier, - loading: @Composable () -> Unit = { Loading(modifier = modifier) } -) { - val systemConfig = LocalConfiguration.current - val viewModel: OpenFeedbackViewModel = viewModel( - factory = OpenFeedbackViewModel.Factory.create( - firebaseApp = config.firebaseApp.value, - projectId = projectId, - sessionId = sessionId, - locale = systemConfig.locale - ) - ) - val uiState = viewModel.uiState.collectAsState() - when (uiState.value) { - is OpenFeedbackUiState.Loading -> loading() - is OpenFeedbackUiState.Success -> { - val session = (uiState.value as OpenFeedbackUiState.Success).session - OpenFeedbackLayout( - sessionFeedback = session, - modifier = modifier, - onClick = { voteItem -> viewModel.vote(voteItem = voteItem) } - ) - } - } -} - -@Deprecated("Please use m3 artifact to display OpenFeedback") -@Composable -fun OpenFeedbackLayout( - sessionFeedback: UISessionFeedback, - modifier: Modifier = Modifier, - onClick: (voteItem: UIVoteItem) -> Unit -) { - Column(modifier = modifier) { - VoteItems(voteItems = sessionFeedback.voteItem, onClick = onClick) - Box( - modifier = Modifier - .fillMaxWidth() - .padding(top = 5.dp), - contentAlignment = Alignment.Center - ) { - PoweredBy() - } - } -} diff --git a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/PoweredBy.kt b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/PoweredBy.kt deleted file mode 100644 index a8a4496..0000000 --- a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/PoweredBy.kt +++ /dev/null @@ -1,61 +0,0 @@ -package io.openfeedback.android.m2 - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.height -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.darkColors -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.openfeedback.android.R as ROF - -@Composable -fun PoweredBy( - modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.body2, - color: Color = MaterialTheme.colors.onBackground -) { - val logo = - if (MaterialTheme.colors.isLight) ROF.drawable.openfeedback_light - else ROF.drawable.openfeedback_dark - val poweredBy = stringResource(id = ROF.string.powered_by) - Row( - modifier = modifier.semantics(mergeDescendants = true) { - contentDescription = "$poweredBy Openfeedback" - }, - horizontalArrangement = Arrangement.spacedBy(4.dp), - verticalAlignment = Alignment.Top - ) { - Text(text = poweredBy, style = style, color = color) - Image( - painter = painterResource(id = logo), - contentDescription = null, - modifier = Modifier.height(style.fontSize.value.dp + 13.dp) - ) - } -} - -@Preview -@Composable -internal fun PoweredByPreview() { - Column { - MaterialTheme { - PoweredBy() - } - MaterialTheme(colors = darkColors()) { - PoweredBy() - } - } -} diff --git a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/VoteCard.kt b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/VoteCard.kt deleted file mode 100644 index 0715ebf..0000000 --- a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/VoteCard.kt +++ /dev/null @@ -1,75 +0,0 @@ -package io.openfeedback.android.m2 - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.drawscope.Fill -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.openfeedback.android.m2.data.fakeVotes -import io.openfeedback.android.viewmodels.models.UIVoteItem - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun VoteCard( - voteModel: UIVoteItem, - modifier: Modifier = Modifier, - style: TextStyle = MaterialTheme.typography.body2, - backgroundColor: Color = MaterialTheme.colors.surface, - contentColor: Color = MaterialTheme.colors.onSurface, - shape: Shape = MaterialTheme.shapes.medium, - onClick: (voteItem: UIVoteItem) -> Unit -) { - val border = if (voteModel.votedByUser) 4.dp else 1.dp - Surface( - shape = shape, - border = BorderStroke(border, contentColor.copy(alpha = .2f)), - color = backgroundColor, - modifier = modifier, - onClick = { onClick(voteModel) } - ) { - Box(modifier = Modifier.height(100.dp)) { - Canvas(modifier = Modifier.fillMaxSize()) { - voteModel.dots.forEach { dot -> - val offset = Offset(x = this.size.width * dot.x, y = this.size.height * dot.y) - drawCircle( - color = Color( - dot.color.substring(0, 2).toInt(16), - dot.color.substring(2, 4).toInt(16), - dot.color.substring(4, 6).toInt(16), - 255 / 3 - ), - radius = 30.dp.value, - center = offset, - style = Fill - ) - } - } - Text( - text = voteModel.text, - style = style, - color = contentColor, - modifier = Modifier.padding(10.dp), - ) - } - } -} - -@Preview -@Composable -internal fun VoteCardPreview() { - VoteCard(voteModel = fakeVotes[0], onClick = {}) -} diff --git a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/VoteItems.kt b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/VoteItems.kt deleted file mode 100644 index 40b17af..0000000 --- a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/VoteItems.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.openfeedback.android.m2 - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.openfeedback.android.m2.VoteCard -import io.openfeedback.android.viewmodels.models.UIVoteItem - -@Composable -internal fun VoteItems( - voteItems: List, - modifier: Modifier = Modifier, - columnCount: Int = 2, - onClick: (voteItem: UIVoteItem) -> Unit -) { - val spaceGrid = 8.dp - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(spaceGrid) - ) { - 0.until(columnCount).forEach { column -> - Box(modifier = Modifier.weight(1f)) { - Column(verticalArrangement = Arrangement.spacedBy(spaceGrid)) { - voteItems.filterIndexed { index, _ -> - index % columnCount == column - }.forEach { voteItem -> - VoteCard( - voteModel = voteItem, - onClick = onClick - ) - } - } - } - } - } -} diff --git a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/data/FakeVotes.kt b/openfeedback-m2/src/main/java/io/openfeedback/android/m2/data/FakeVotes.kt deleted file mode 100644 index e908e97..0000000 --- a/openfeedback-m2/src/main/java/io/openfeedback/android/m2/data/FakeVotes.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.openfeedback.android.m2.data - -import io.openfeedback.android.viewmodels.models.UIDot -import io.openfeedback.android.viewmodels.models.UIVoteItem -import kotlin.math.absoluteValue -import kotlin.random.Random - -internal val fakeVotes = listOf( - fakeVoteItem("Drôle/original \uD83D\uDE03", 3), - fakeVoteItem("Trèsenrichissant enrichissant enrichissant \uD83E\uDD13", 1), - fakeVoteItem("Super intéressant \uD83D\uDC4D", 8), - fakeVoteItem("Très bon orateur \uD83D\uDC4F", 21), - fakeVoteItem("Pas clair \uD83E\uDDD0", 2) -) - -internal fun dots(count: Int, possibleColors: List): List { - return 0.until(count).map { - UIDot( - Random.nextFloat(), - Random.nextFloat(), - possibleColors[Random.nextInt().absoluteValue % possibleColors.size] - ) - } -} - -internal fun fakeVoteItem(text: String, count: Int): UIVoteItem { - val color = listOf( - "7cebcd", - "0822cb", - "d5219d", - "8eec1a" - ) - return UIVoteItem( - id = Random.nextInt().toString(), - text = text, - dots = dots(count, color), - votedByUser = count % 2 == 0 - ) -} diff --git a/openfeedback-m3/build.gradle.kts b/openfeedback-m3/build.gradle.kts index cfadbc2..80ead62 100644 --- a/openfeedback-m3/build.gradle.kts +++ b/openfeedback-m3/build.gradle.kts @@ -1,22 +1,32 @@ - plugins { - id("io.openfeedback.plugins.compose.lib") + id("com.android.library") + id("org.jetbrains.kotlin.plugin.serialization") + id("org.jetbrains.compose") } -android { - namespace = "io.openfeedback.android.m3" -} +library( + namespace = "io.openfeedback.m3", + publish = true, +) { kotlinMultiplatformExtension -> + kotlinMultiplatformExtension.sourceSets { + findByName("commonMain")!!.apply { + dependencies { + api(projects.openfeedback) + api(projects.openfeedbackViewmodel) -openfeedback { - configurePublishing("feedback-android-sdk-m3") -} + implementation(libs.moko.resources.compose) + implementation(libs.moko.mvvm.compose) -dependencies { - api(projects.openfeedback) - api(projects.openfeedbackViewmodel) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.ui) - implementation(libs.androidx.compose.uitooling) + implementation(kotlinMultiplatformExtension.compose.material3) + implementation(kotlinMultiplatformExtension.compose.ui) + } + } + val androidMain by getting { + dependencies { + with (kotlinMultiplatformExtension) { + implementation(compose.uiTooling) + } + } + } + } } diff --git a/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentInputPreview.kt b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentInputPreview.kt new file mode 100644 index 0000000..706a085 --- /dev/null +++ b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentInputPreview.kt @@ -0,0 +1,17 @@ +package io.openfeedback.m3 + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@Preview +@Composable +private fun CommentInputPreview() { + MaterialTheme { + CommentInput( + value = "My comment", + onValueChange = {}, + onSubmit = {} + ) + } +} diff --git a/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentItemsPreview.kt b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentItemsPreview.kt new file mode 100644 index 0000000..45f71d1 --- /dev/null +++ b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentItemsPreview.kt @@ -0,0 +1,42 @@ +package io.openfeedback.m3 + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import io.openfeedback.viewmodels.models.UIComment +import io.openfeedback.viewmodels.models.UIDot + +@Preview +@Composable +private fun CommentItemsPreview() { + MaterialTheme { + CommentItems( + comments = listOf( + UIComment( + id = "", + voteItemId = "", + message = "Nice comment", + createdAt = "08 August 2023", + upVotes = 8, + dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), + votedByUser = true + ), + UIComment( + id = "", + voteItemId = "", + message = "Another comment", + createdAt = "08 August 2023", + upVotes = 0, + dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), + votedByUser = true + ) + ), + commentInput = { + CommentInput(value = "", onValueChange = {}, onSubmit = {}) + }, + comment = { + Comment(comment = it, onClick = {}) + } + ) + } +} diff --git a/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentPreview.kt b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentPreview.kt new file mode 100644 index 0000000..816618c --- /dev/null +++ b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/CommentPreview.kt @@ -0,0 +1,26 @@ +package io.openfeedback.m3 + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import io.openfeedback.viewmodels.models.UIComment +import io.openfeedback.viewmodels.models.UIDot + +@Preview +@Composable +private fun CommentPreview() { + MaterialTheme { + Comment( + comment = UIComment( + id = "", + voteItemId = "", + message = "Super talk and great speakers!", + createdAt = "08 August 2023", + upVotes = 8, + dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), + votedByUser = true + ), + onClick = {} + ) + } +} diff --git a/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/LoadingPreview.kt b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/LoadingPreview.kt new file mode 100644 index 0000000..0e3210f --- /dev/null +++ b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/LoadingPreview.kt @@ -0,0 +1,10 @@ +package io.openfeedback.m3 + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +@Preview +@Composable +internal fun LoadingPreview() { + Loading() +} diff --git a/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/OpenFeedbackLayoutPreview.kt b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/OpenFeedbackLayoutPreview.kt new file mode 100644 index 0000000..efd4c82 --- /dev/null +++ b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/OpenFeedbackLayoutPreview.kt @@ -0,0 +1,77 @@ +package io.openfeedback.m3 + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.openfeedback.viewmodels.models.UIComment +import io.openfeedback.viewmodels.models.UIDot +import io.openfeedback.viewmodels.models.UISessionFeedback +import io.openfeedback.viewmodels.models.UIVoteItem + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun OpenFeedbackLayoutPreview() { + MaterialTheme { + OpenFeedbackLayout( + sessionFeedback = UISessionFeedback( + commentValue = "", + commentVoteItemId = "", + comments = listOf( + UIComment( + id = "", + voteItemId = "", + message = "Nice comment", + createdAt = "08 August 2023", + upVotes = 8, + dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), + votedByUser = true + ), + UIComment( + id = "", + voteItemId = "", + message = "Another one", + createdAt = "08 August 2023", + upVotes = 0, + dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), + votedByUser = true + ) + ), + voteItem = listOf( + UIVoteItem( + id = "", + text = "Fun", + dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), + votedByUser = true + ), + UIVoteItem( + id = "", + text = "Fun", + dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), + votedByUser = true + ) + ) + ), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + commentInput = { + CommentInput(value = "", onValueChange = {}, onSubmit = {}) + }, + comment = { Comment(comment = it, onClick = {}) } + ) { + VoteCard( + voteModel = it, + onClick = {}, + modifier = Modifier + .height(100.dp) + .fillMaxWidth() + ) + } + } +} diff --git a/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/VoteCardPreview.kt b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/VoteCardPreview.kt new file mode 100644 index 0000000..596e0e0 --- /dev/null +++ b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/VoteCardPreview.kt @@ -0,0 +1,29 @@ +package io.openfeedback.m3 + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import io.openfeedback.viewmodels.models.UIDot +import io.openfeedback.viewmodels.models.UIVoteItem + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun VoteCardPreview() { + MaterialTheme { + VoteCard( + voteModel = UIVoteItem( + id = "", + text = "Fun", + dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), + votedByUser = true + ), + onClick = {}, + modifier = Modifier.size(height = 100.dp, width = 200.dp) + ) + } +} diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/VoteItems.kt b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/VoteItemsPreview.kt similarity index 54% rename from openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/VoteItems.kt rename to openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/VoteItemsPreview.kt index 4327426..d98ba52 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/VoteItems.kt +++ b/openfeedback-m3/src/androidMain/kotlin/io/openfeedback/m3/VoteItemsPreview.kt @@ -1,9 +1,6 @@ -package io.openfeedback.android.m3 +package io.openfeedback.m3 import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.ExperimentalMaterial3Api @@ -12,39 +9,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.openfeedback.android.viewmodels.models.UIDot -import io.openfeedback.android.viewmodels.models.UIVoteItem - -@ExperimentalMaterial3Api -@Composable -internal fun VoteItems( - voteItems: List, - modifier: Modifier = Modifier, - columnCount: Int = 2, - horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(8.dp), - verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), - content: @Composable ColumnScope.(UIVoteItem) -> Unit -) { - Row( - modifier = modifier, - horizontalArrangement = horizontalArrangement - ) { - 0.until(columnCount).forEach { column -> - Column( - verticalArrangement = verticalArrangement, - modifier = Modifier.weight(1f) - ) { - voteItems - .filterIndexed { index, _ -> - index % columnCount == column - } - .forEach { voteItem -> - content(voteItem) - } - } - } - } -} +import io.openfeedback.viewmodels.models.UIDot +import io.openfeedback.viewmodels.models.UIVoteItem @OptIn(ExperimentalMaterial3Api::class) @Preview diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/Comment.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/Comment.kt similarity index 72% rename from openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/Comment.kt rename to openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/Comment.kt index 1bb23a4..bc0138a 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/Comment.kt +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/Comment.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.m3 +package io.openfeedback.m3 import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -13,13 +13,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.openfeedback.android.R -import io.openfeedback.android.viewmodels.models.UIComment -import io.openfeedback.android.viewmodels.models.UIDot +import dev.icerock.moko.resources.compose.stringResource +import io.openfeedback.MR +import io.openfeedback.viewmodels.models.UIComment @Composable fun Comment( @@ -51,9 +49,10 @@ fun Comment( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth() ) { + Text( text = stringResource( - id = R.string.openfeedback_comments_upvotes, + MR.strings.openfeedback_comments_upvotes, comment.upVotes ), color = contentColor.copy(alpha = .7f), @@ -68,22 +67,3 @@ fun Comment( } } } - -@Preview -@Composable -private fun CommentPreview() { - MaterialTheme { - Comment( - comment = UIComment( - id = "", - voteItemId = "", - message = "Super talk and great speakers!", - createdAt = "08 August 2023", - upVotes = 8, - dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), - votedByUser = true - ), - onClick = {} - ) - } -} diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/CommentInput.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/CommentInput.kt similarity index 65% rename from openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/CommentInput.kt rename to openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/CommentInput.kt index 4bf6d47..9b75c34 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/CommentInput.kt +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/CommentInput.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.m3 +package io.openfeedback.m3 import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions @@ -6,15 +6,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Send import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Preview -import io.openfeedback.android.R +import dev.icerock.moko.resources.compose.stringResource +import io.openfeedback.MR @Composable fun CommentInput( @@ -28,12 +26,12 @@ fun CommentInput( value = value, onValueChange = onValueChange, modifier = modifier, - label = { Text(text = stringResource(id = R.string.openfeedback_comments_title_input)) }, + label = { Text(text = stringResource(MR.strings.openfeedback_comments_title_input)) }, trailingIcon = { IconButton(onClick = onSubmit) { Icon( imageVector = Icons.Outlined.Send, - contentDescription = stringResource(id = R.string.openfeedback_comments_send) + contentDescription = stringResource(MR.strings.openfeedback_comments_send) ) } }, @@ -43,15 +41,3 @@ fun CommentInput( maxLines = 5 ) } - -@Preview -@Composable -private fun CommentInputPreview() { - MaterialTheme { - CommentInput( - value = "My comment", - onValueChange = {}, - onSubmit = {} - ) - } -} diff --git a/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/CommentItems.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/CommentItems.kt new file mode 100644 index 0000000..6f64938 --- /dev/null +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/CommentItems.kt @@ -0,0 +1,36 @@ +package io.openfeedback.m3 + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import dev.icerock.moko.resources.compose.stringResource +import io.openfeedback.MR +import io.openfeedback.viewmodels.models.UIComment + +@Composable +internal fun CommentItems( + comments: List, + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), + commentInput: @Composable ColumnScope.() -> Unit, + comment: @Composable ColumnScope.(UIComment) -> Unit +) { + Column( + modifier = modifier, + verticalArrangement = verticalArrangement + ) { + Text( + text = stringResource(MR.strings.openfeedback_comments_title), + style = MaterialTheme.typography.titleMedium + ) + commentInput() + comments.forEach { uiComment -> + comment(uiComment) + } + } +} diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/DotModifier.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/DotModifier.kt similarity index 90% rename from openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/DotModifier.kt rename to openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/DotModifier.kt index 8b56ff7..644f88a 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/DotModifier.kt +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/DotModifier.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.m3 +package io.openfeedback.m3 import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind @@ -6,7 +6,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.unit.dp -import io.openfeedback.android.viewmodels.models.UIDot +import io.openfeedback.viewmodels.models.UIDot fun Modifier.drawDots(dots: List): Modifier = drawBehind { dots.forEach { dot -> diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/Loading.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/Loading.kt similarity index 75% rename from openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/Loading.kt rename to openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/Loading.kt index 585c406..5422a79 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/Loading.kt +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/Loading.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.m3 +package io.openfeedback.m3 import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize @@ -6,7 +6,6 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview @Composable fun Loading(modifier: Modifier = Modifier) { @@ -17,9 +16,3 @@ fun Loading(modifier: Modifier = Modifier) { CircularProgressIndicator() } } - -@Preview -@Composable -internal fun LoadingPreview() { - Loading() -} diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/OpenFeedbackLayout.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/OpenFeedbackLayout.kt similarity index 53% rename from openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/OpenFeedbackLayout.kt rename to openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/OpenFeedbackLayout.kt index 75d9525..ba70a28 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/OpenFeedbackLayout.kt +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/OpenFeedbackLayout.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.m3 +package io.openfeedback.m3 import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -8,22 +8,22 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import io.openfeedback.android.viewmodels.OpenFeedbackFirebaseConfig -import io.openfeedback.android.viewmodels.OpenFeedbackUiState -import io.openfeedback.android.viewmodels.OpenFeedbackViewModel -import io.openfeedback.android.viewmodels.models.UIComment -import io.openfeedback.android.viewmodels.models.UIDot -import io.openfeedback.android.viewmodels.models.UISessionFeedback -import io.openfeedback.android.viewmodels.models.UIVoteItem +import com.vanniktech.locale.Locale +import com.vanniktech.locale.Locales +import dev.icerock.moko.mvvm.compose.getViewModel +import dev.icerock.moko.mvvm.compose.viewModelFactory +import io.openfeedback.viewmodels.OpenFeedbackFirebaseConfig +import io.openfeedback.viewmodels.OpenFeedbackUiState +import io.openfeedback.viewmodels.OpenFeedbackViewModel +import io.openfeedback.viewmodels.models.UIComment +import io.openfeedback.viewmodels.models.UISessionFeedback +import io.openfeedback.viewmodels.models.UIVoteItem +import io.openfeedback.viewmodels.toFirebaseApp @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -33,16 +33,19 @@ fun OpenFeedback( sessionId: String, modifier: Modifier = Modifier, columnCount: Int = 2, + locale: Locale = Locale.from(Locales.currentLocaleString()), loading: @Composable () -> Unit = { Loading(modifier = modifier) } ) { - val systemConfig = LocalConfiguration.current - val viewModel: OpenFeedbackViewModel = viewModel( - factory = OpenFeedbackViewModel.Factory.create( - firebaseApp = config.firebaseApp.value, - projectId = projectId, - sessionId = sessionId, - locale = systemConfig.locale - ) + val viewModel: OpenFeedbackViewModel = getViewModel( + key = sessionId, + factory = viewModelFactory { + OpenFeedbackViewModel( + firebaseApp = config.toFirebaseApp(), + projectId = projectId, + sessionId = sessionId, + locale = locale + ) + } ) val uiState = viewModel.uiState.collectAsState() when (uiState.value) { @@ -122,65 +125,3 @@ fun OpenFeedbackLayout( } } } - -@OptIn(ExperimentalMaterial3Api::class) -@Preview -@Composable -private fun OpenFeedbackLayoutPreview() { - MaterialTheme { - OpenFeedbackLayout( - sessionFeedback = UISessionFeedback( - commentValue = "", - commentVoteItemId = "", - comments = listOf( - UIComment( - id = "", - voteItemId = "", - message = "Nice comment", - createdAt = "08 August 2023", - upVotes = 8, - dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), - votedByUser = true - ), - UIComment( - id = "", - voteItemId = "", - message = "Another one", - createdAt = "08 August 2023", - upVotes = 0, - dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), - votedByUser = true - ) - ), - voteItem = listOf( - UIVoteItem( - id = "", - text = "Fun", - dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), - votedByUser = true - ), - UIVoteItem( - id = "", - text = "Fun", - dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), - votedByUser = true - ) - ) - ), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - commentInput = { - CommentInput(value = "", onValueChange = {}, onSubmit = {}) - }, - comment = { Comment(comment = it, onClick = {}) } - ) { - VoteCard( - voteModel = it, - onClick = {}, - modifier = Modifier - .height(100.dp) - .fillMaxWidth() - ) - } - } -} diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/PoweredBy.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/PoweredBy.kt similarity index 79% rename from openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/PoweredBy.kt rename to openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/PoweredBy.kt index c65f415..951e6c3 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/PoweredBy.kt +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/PoweredBy.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.m3 +package io.openfeedback.m3 import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement @@ -11,13 +11,13 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import io.openfeedback.android.R as ROF +import dev.icerock.moko.resources.compose.painterResource +import dev.icerock.moko.resources.compose.stringResource +import io.openfeedback.MR @Composable internal fun PoweredBy( @@ -25,11 +25,10 @@ internal fun PoweredBy( style: TextStyle = MaterialTheme.typography.bodyMedium, color: Color = MaterialTheme.colorScheme.onBackground ) { - val logo = - if (MaterialTheme.colorScheme.background.luminance() > 0.5) ROF.drawable.openfeedback_light - else ROF.drawable.openfeedback_dark - val poweredBy = stringResource(id = ROF.string.powered_by) + if (MaterialTheme.colorScheme.background.luminance() > 0.5) MR.images.openfeedback_light + else MR.images.openfeedback_dark + val poweredBy = stringResource(MR.strings.powered_by) Row( modifier = modifier.semantics(mergeDescendants = true) { contentDescription = "$poweredBy Openfeedback" @@ -39,7 +38,7 @@ internal fun PoweredBy( ) { Text(text = poweredBy, style = style, color = color) Image( - painter = painterResource(id = logo), + painter = painterResource(logo), contentDescription = null, modifier = Modifier.height(style.fontSize.value.dp + 13.dp) ) diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/VoteCard.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/VoteCard.kt similarity index 69% rename from openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/VoteCard.kt rename to openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/VoteCard.kt index e17e1eb..14aeb5e 100644 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/VoteCard.kt +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/VoteCard.kt @@ -1,9 +1,8 @@ -package io.openfeedback.android.m3 +package io.openfeedback.m3 import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -14,10 +13,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import io.openfeedback.android.viewmodels.models.UIDot -import io.openfeedback.android.viewmodels.models.UIVoteItem +import io.openfeedback.viewmodels.models.UIVoteItem @ExperimentalMaterial3Api @Composable @@ -50,21 +47,3 @@ fun VoteCard( } } } - -@OptIn(ExperimentalMaterial3Api::class) -@Preview -@Composable -private fun VoteCardPreview() { - MaterialTheme { - VoteCard( - voteModel = UIVoteItem( - id = "", - text = "Fun", - dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), - votedByUser = true - ), - onClick = {}, - modifier = Modifier.size(height = 100.dp, width = 200.dp) - ) - } -} diff --git a/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/VoteItems.kt b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/VoteItems.kt new file mode 100644 index 0000000..f4773ce --- /dev/null +++ b/openfeedback-m3/src/commonMain/kotlin/io/openfeedback/m3/VoteItems.kt @@ -0,0 +1,42 @@ +package io.openfeedback.m3 + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.openfeedback.viewmodels.models.UIVoteItem + +@ExperimentalMaterial3Api +@Composable +internal fun VoteItems( + voteItems: List, + modifier: Modifier = Modifier, + columnCount: Int = 2, + horizontalArrangement: Arrangement.Horizontal = Arrangement.spacedBy(8.dp), + verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), + content: @Composable ColumnScope.(UIVoteItem) -> Unit +) { + Row( + modifier = modifier, + horizontalArrangement = horizontalArrangement + ) { + 0.until(columnCount).forEach { column -> + Column( + verticalArrangement = verticalArrangement, + modifier = Modifier.weight(1f) + ) { + voteItems + .filterIndexed { index, _ -> + index % columnCount == column + } + .forEach { voteItem -> + content(voteItem) + } + } + } + } +} diff --git a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/CommentItems.kt b/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/CommentItems.kt deleted file mode 100644 index b7ce32e..0000000 --- a/openfeedback-m3/src/main/kotlin/io/openfeedback/android/m3/CommentItems.kt +++ /dev/null @@ -1,73 +0,0 @@ -package io.openfeedback.android.m3 - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import io.openfeedback.android.R -import io.openfeedback.android.viewmodels.models.UIComment -import io.openfeedback.android.viewmodels.models.UIDot - -@Composable -internal fun CommentItems( - comments: List, - modifier: Modifier = Modifier, - verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), - commentInput: @Composable ColumnScope.() -> Unit, - comment: @Composable ColumnScope.(UIComment) -> Unit -) { - Column( - modifier = modifier, - verticalArrangement = verticalArrangement - ) { - Text( - text = stringResource(id = R.string.openfeedback_comments_title), - style = MaterialTheme.typography.titleMedium - ) - commentInput() - comments.forEachIndexed { index, uiComment -> - comment(uiComment) - } - } -} - -@Preview -@Composable -private fun VoteItemsPreview() { - MaterialTheme { - CommentItems( - comments = listOf( - UIComment( - id = "", - voteItemId = "", - message = "Nice comment", - createdAt = "08 August 2023", - upVotes = 8, - dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), - votedByUser = true - ), - UIComment( - id = "", - voteItemId = "", - message = "Another comment", - createdAt = "08 August 2023", - upVotes = 0, - dots = listOf(UIDot(x = .5f, y = .5f, color = "FF00CC")), - votedByUser = true - ) - ), - commentInput = { - CommentInput(value = "", onValueChange = {}, onSubmit = {}) - }, - comment = { - Comment(comment = it, onClick = {}) - } - ) - } -} diff --git a/openfeedback-viewmodel/build.gradle.kts b/openfeedback-viewmodel/build.gradle.kts index 9eacfde..81e5d00 100644 --- a/openfeedback-viewmodel/build.gradle.kts +++ b/openfeedback-viewmodel/build.gradle.kts @@ -1,19 +1,24 @@ - plugins { - id("io.openfeedback.plugins.compose.lib") -} - -android { - namespace = "io.openfeedback.android.viewmodel" -} - -openfeedback { - configurePublishing("feedback-android-sdk-viewmodel") + id("com.android.library") + id("org.jetbrains.kotlin.multiplatform") + id("org.jetbrains.kotlin.plugin.serialization") } -dependencies { - implementation(projects.openfeedback) - implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.runtime) +library( + namespace = "io.openfeedback.viewmodels", + publish = true, + compose = true, +) { kotlinMultiplatformExtension -> + kotlinMultiplatformExtension.sourceSets { + getByName("commonMain") { + dependencies { + implementation(projects.openfeedback) + implementation(kotlinMultiplatformExtension.compose.runtime) + // Not sure why this is needed 🤷 + implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.23") + api(libs.moko.mvvm.core) + api(libs.kmm.locale) + } + } + } } \ No newline at end of file diff --git a/openfeedback-viewmodel/src/androidMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.kt b/openfeedback-viewmodel/src/androidMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.kt new file mode 100644 index 0000000..efe694f --- /dev/null +++ b/openfeedback-viewmodel/src/androidMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.kt @@ -0,0 +1,21 @@ +package io.openfeedback.viewmodels.extensions + +import com.vanniktech.locale.Locale +import com.vanniktech.locale.toJavaLocale +import kotlinx.datetime.LocalDateTime +import java.text.SimpleDateFormat +import java.util.Date + +actual fun LocalDateTime.format(pattern: String, locale: Locale): String { + val formatter = SimpleDateFormat("dd MMM, hh:mm", locale.toJavaLocale()) + return formatter.format( + Date( + date.year, + date.monthNumber, + date.dayOfMonth, + time.hour, + time.minute, + time.second + ) + ) +} \ No newline at end of file diff --git a/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/OpenFeedbackFirebaseConfig.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/OpenFeedbackFirebaseConfig.kt new file mode 100644 index 0000000..a1a24f6 --- /dev/null +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/OpenFeedbackFirebaseConfig.kt @@ -0,0 +1,51 @@ +package io.openfeedback.viewmodels + +import androidx.compose.runtime.Immutable +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.initialize + +@Immutable +data class OpenFeedbackFirebaseConfig( + val context: Any?, + val projectId: String, + val applicationId: String, + val apiKey: String, + val databaseUrl: String, + val appName: String = "openfeedback" +) { + companion object { + /** + * Returns a [OpenFeedbackFirebaseConfig] configured for the default openfeedback instance at openfeedback.io + * + * @param context the context on Android or null on iOS + */ + fun default( + context: Any?, + ): OpenFeedbackFirebaseConfig { + /** + * The firebase parameters are from the openfeedback.io project so we can + * access firestore directly + */ + return OpenFeedbackFirebaseConfig( + context = context, + projectId = "open-feedback-42", + // Hack: I replaced :web: by :ios: for the iOS SDK to behave + applicationId = "1:635903227116:ios:31de912f8bf29befb1e1c9", + apiKey = "AIzaSyB3ELJsaiItrln0uDGSuuHE1CfOJO67Hb4", + databaseUrl = "https://open-feedback-42.firebaseio.com/" + ) + } + } +} + +fun OpenFeedbackFirebaseConfig.toFirebaseApp(): FirebaseApp = Firebase.initialize( + context = context, + options = dev.gitlive.firebase.FirebaseOptions( + projectId = projectId, + applicationId = applicationId, + apiKey = apiKey, + databaseUrl = databaseUrl + ), + name = appName +) \ No newline at end of file diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackViewModel.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/OpenFeedbackViewModel.kt similarity index 67% rename from openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackViewModel.kt rename to openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/OpenFeedbackViewModel.kt index 178619f..eb8cb79 100644 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackViewModel.kt +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/OpenFeedbackViewModel.kt @@ -1,24 +1,22 @@ -package io.openfeedback.android.viewmodels +package io.openfeedback.viewmodels -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewModelScope -import com.google.firebase.FirebaseApp -import io.openfeedback.android.OpenFeedbackRepository -import io.openfeedback.android.caches.OptimisticVoteCaching -import io.openfeedback.android.model.VoteStatus -import io.openfeedback.android.sources.OpenFeedbackAuth -import io.openfeedback.android.sources.OpenFeedbackFirestore -import io.openfeedback.android.viewmodels.mappers.convertToUiSessionFeedback -import io.openfeedback.android.viewmodels.models.UIComment -import io.openfeedback.android.viewmodels.models.UISessionFeedback -import io.openfeedback.android.viewmodels.models.UISessionFeedbackWithColors -import io.openfeedback.android.viewmodels.models.UIVoteItem +import com.vanniktech.locale.Locale +import dev.gitlive.firebase.FirebaseApp +import dev.icerock.moko.mvvm.viewmodel.ViewModel +import io.openfeedback.OpenFeedbackRepository +import io.openfeedback.viewmodels.mappers.convertToUiSessionFeedback +import io.openfeedback.viewmodels.models.UIComment +import io.openfeedback.viewmodels.models.UISessionFeedback +import io.openfeedback.viewmodels.models.UISessionFeedbackWithColors +import io.openfeedback.viewmodels.models.UIVoteItem +import io.openfeedback.caches.OptimisticVoteCaching +import io.openfeedback.model.VoteStatus +import io.openfeedback.sources.OpenFeedbackAuth +import io.openfeedback.sources.OpenFeedbackFirestore import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch -import java.util.Locale sealed class OpenFeedbackUiState { data object Loading : OpenFeedbackUiState() @@ -32,8 +30,8 @@ class OpenFeedbackViewModel( private val locale: Locale ) : ViewModel() { private val repository = OpenFeedbackRepository( - auth = OpenFeedbackAuth.Factory.create(firebaseApp), - firestore = OpenFeedbackFirestore.Factory.create(firebaseApp), + auth = OpenFeedbackAuth.create(firebaseApp), + firestore = OpenFeedbackFirestore.create(firebaseApp), optimisticVoteCaching = OptimisticVoteCaching() ) @@ -100,20 +98,4 @@ class OpenFeedbackViewModel( status = if (!comment.votedByUser) VoteStatus.Active else VoteStatus.Deleted ) } - - object Factory { - fun create( - firebaseApp: FirebaseApp, - projectId: String, - sessionId: String, - locale: Locale - ) = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T = OpenFeedbackViewModel( - firebaseApp = firebaseApp, - projectId = projectId, - sessionId = sessionId, - locale = locale - ) as T - } - } } diff --git a/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.kt new file mode 100644 index 0000000..c6bdc41 --- /dev/null +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.kt @@ -0,0 +1,6 @@ +package io.openfeedback.viewmodels.extensions + +import com.vanniktech.locale.Locale +import kotlinx.datetime.LocalDateTime + +expect fun LocalDateTime.format(pattern: String, locale: Locale): String \ No newline at end of file diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/mappers/UiMappers.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/mappers/UiMappers.kt similarity index 81% rename from openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/mappers/UiMappers.kt rename to openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/mappers/UiMappers.kt index 8252711..20dffc2 100644 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/mappers/UiMappers.kt +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/mappers/UiMappers.kt @@ -1,15 +1,17 @@ -package io.openfeedback.android.viewmodels.mappers +package io.openfeedback.viewmodels.mappers -import io.openfeedback.android.model.Project -import io.openfeedback.android.model.SessionVotes -import io.openfeedback.android.model.UserVote -import io.openfeedback.android.viewmodels.models.UIComment -import io.openfeedback.android.viewmodels.models.UIDot -import io.openfeedback.android.viewmodels.models.UISessionFeedback -import io.openfeedback.android.viewmodels.models.UISessionFeedbackWithColors -import io.openfeedback.android.viewmodels.models.UIVoteItem -import java.text.SimpleDateFormat -import java.util.Locale +import com.vanniktech.locale.Locale +import io.openfeedback.model.Project +import io.openfeedback.model.SessionVotes +import io.openfeedback.model.UserVote +import io.openfeedback.viewmodels.extensions.format +import io.openfeedback.viewmodels.models.UIComment +import io.openfeedback.viewmodels.models.UIDot +import io.openfeedback.viewmodels.models.UISessionFeedback +import io.openfeedback.viewmodels.models.UISessionFeedbackWithColors +import io.openfeedback.viewmodels.models.UIVoteItem +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime import kotlin.math.absoluteValue import kotlin.random.Random @@ -19,18 +21,18 @@ fun convertToUiSessionFeedback( totalVotes: SessionVotes, locale: Locale ): UISessionFeedback { - val formatter = SimpleDateFormat("dd MMMM yyyy, hh:mm", locale) val userUpVoteIds = userVotes.filter { it.voteId != null }.map { it.voteId!! } val userVoteIds = userVotes.map { it.voteItemId } return UISessionFeedback( commentValue = "", commentVoteItemId = project.voteItems.find { it.type == "text" }?.id, comments = totalVotes.comments.map { commentItem -> + val localDateTime = commentItem.value.createdAt.toLocalDateTime(TimeZone.currentSystemDefault()) UIComment( id = commentItem.value.id, voteItemId = commentItem.value.voteItemId, message = commentItem.value.text, - createdAt = formatter.format(commentItem.value.createdAt.toDate()), + createdAt = localDateTime.format(pattern = "dd MMM, hh:mm", locale = locale), upVotes = commentItem.value.plus.toInt(), dots = dots(commentItem.value.plus.toInt(), project.chipColors), votedByUser = userUpVoteIds.contains(commentItem.value.id) @@ -42,7 +44,7 @@ fun convertToUiSessionFeedback( val count = totalVotes.votes[voteItem.id]?.toInt() ?: 0 UIVoteItem( id = voteItem.id, - text = voteItem.localizedName(locale.language), + text = voteItem.localizedName(locale.language.code), dots = dots(count, project.chipColors), votedByUser = userVoteIds.contains(voteItem.id) ) diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIComment.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIComment.kt similarity index 78% rename from openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIComment.kt rename to openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIComment.kt index 4666c1e..2bc71be 100644 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIComment.kt +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIComment.kt @@ -1,7 +1,6 @@ -package io.openfeedback.android.viewmodels.models +package io.openfeedback.viewmodels.models import androidx.compose.runtime.Immutable -import java.util.Date @Immutable data class UIComment( diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIDot.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIDot.kt similarity index 84% rename from openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIDot.kt rename to openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIDot.kt index 14403ac..97a7fce 100644 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIDot.kt +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIDot.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.viewmodels.models +package io.openfeedback.viewmodels.models import androidx.compose.runtime.Immutable diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedback.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UISessionFeedback.kt similarity index 81% rename from openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedback.kt rename to openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UISessionFeedback.kt index 36fde14..e6c64b3 100644 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedback.kt +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UISessionFeedback.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.viewmodels.models +package io.openfeedback.viewmodels.models import androidx.compose.runtime.Immutable diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedbackWithColors.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UISessionFeedbackWithColors.kt similarity index 68% rename from openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedbackWithColors.kt rename to openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UISessionFeedbackWithColors.kt index 3f80bf1..3807800 100644 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UISessionFeedbackWithColors.kt +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UISessionFeedbackWithColors.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.viewmodels.models +package io.openfeedback.viewmodels.models data class UISessionFeedbackWithColors( val session: UISessionFeedback, diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIVoteItem.kt b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIVoteItem.kt similarity index 78% rename from openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIVoteItem.kt rename to openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIVoteItem.kt index 75eb408..e2afdac 100644 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/models/UIVoteItem.kt +++ b/openfeedback-viewmodel/src/commonMain/kotlin/io/openfeedback/viewmodels/models/UIVoteItem.kt @@ -1,4 +1,4 @@ -package io.openfeedback.android.viewmodels.models +package io.openfeedback.viewmodels.models import androidx.compose.runtime.Immutable diff --git a/openfeedback-viewmodel/src/iosMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.ios.kt b/openfeedback-viewmodel/src/iosMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.ios.kt new file mode 100644 index 0000000..8b75c5d --- /dev/null +++ b/openfeedback-viewmodel/src/iosMain/kotlin/io/openfeedback/viewmodels/extensions/LocalDateTimeExtensions.ios.kt @@ -0,0 +1,17 @@ +package io.openfeedback.viewmodels.extensions + +import com.vanniktech.locale.Locale +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.toNSDateComponents +import platform.Foundation.NSDateFormatter +import platform.Foundation.NSLocale + +actual fun LocalDateTime.format(pattern: String, locale: Locale): String { + val dateFormatter = NSDateFormatter() + dateFormatter.dateFormat = pattern + dateFormatter.locale = NSLocale(locale.toString()) + return dateFormatter.stringFromDate( + date = toNSDateComponents().date() + ?: throw IllegalStateException("Could not convert kotlin date to NSDate $this") + ) +} diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/FirebaseFactory.kt b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/FirebaseFactory.kt deleted file mode 100644 index dd75797..0000000 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/FirebaseFactory.kt +++ /dev/null @@ -1,21 +0,0 @@ -package io.openfeedback.android.viewmodels - -import android.content.Context -import com.google.firebase.FirebaseApp -import com.google.firebase.FirebaseOptions - -internal object FirebaseFactory { - fun create( - context: Context, - config: OpenFeedbackFirebaseConfig, - appName: String = "openfeedback" - ): FirebaseApp { - val options = FirebaseOptions.Builder() - .setProjectId(config.projectId) - .setApplicationId(config.applicationId) - .setApiKey(config.apiKey) - .setDatabaseUrl(config.databaseUrl) - .build() - return FirebaseApp.initializeApp(context, options, appName) - } -} diff --git a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackFirebaseConfig.kt b/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackFirebaseConfig.kt deleted file mode 100644 index 6d7e8ca..0000000 --- a/openfeedback-viewmodel/src/main/java/io/openfeedback/android/viewmodels/OpenFeedbackFirebaseConfig.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.openfeedback.android.viewmodels - -import android.content.Context -import androidx.compose.runtime.Immutable - -@Immutable -data class OpenFeedbackFirebaseConfig( - val context: Context, - val projectId: String, - val applicationId: String, - val apiKey: String, - val databaseUrl: String, - val appName: String = "openfeedback" -) { - val firebaseApp = lazy { FirebaseFactory.create(context, this, appName) } -} diff --git a/openfeedback/build.gradle.kts b/openfeedback/build.gradle.kts index 1dfc072..74e7d3a 100644 --- a/openfeedback/build.gradle.kts +++ b/openfeedback/build.gradle.kts @@ -1,22 +1,37 @@ - plugins { - id("io.openfeedback.plugins.lib") + id("com.android.library") + id("org.jetbrains.kotlin.plugin.serialization") } -android { - namespace = "io.openfeedback.android" -} +library( + namespace = "io.openfeedback", + moko = true, + publish = true +) { + it.sourceSets { + getByName("commonMain").apply { + dependencies { + api(libs.kotlin.coroutines.core) + api(libs.kotlinx.datetime) + api(libs.kotlinx.serialization.json) -openfeedback { - configurePublishing("feedback-android-sdk") -} + api(libs.gitlive.app) + api(libs.gitlive.firestore) + implementation(libs.gitlive.auth) + implementation(libs.gitlive.common) -dependencies { - api(libs.kotlin.coroutines.core) - api(libs.kotlin.coroutines.android) - api(libs.kotlin.coroutines.play.services) + api(libs.moko.resources) - // Firestore - api(libs.firebase.firestore) - api(libs.firebase.auth) + implementation(libs.kermit) + } + } + getByName("androidMain"){ + dependencies { + api(libs.firebase.common) + api(libs.firebase.firestore) + implementation(libs.firebase.auth) + } + } + } } + diff --git a/openfeedback/src/androidMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.kt b/openfeedback/src/androidMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.kt new file mode 100644 index 0000000..b43d33b --- /dev/null +++ b/openfeedback/src/androidMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.kt @@ -0,0 +1,21 @@ +package io.openfeedback.mappers + +import com.google.firebase.Timestamp +import io.openfeedback.model.Comment +import kotlinx.datetime.Instant + +actual fun Map.convertToModel( + id: String, + voteItemId: String +): Comment { + val createdAt = this["createdAt"] as Timestamp + val updatedAt = this["updatedAt"] as Timestamp + return Comment( + id = id, + voteItemId = voteItemId, + text = this["text"] as String, + plus = this["plus"] as Long, + createdAt = Instant.fromEpochSeconds(createdAt.seconds, createdAt.nanoseconds), + updatedAt = Instant.fromEpochSeconds(updatedAt.seconds, updatedAt.nanoseconds) + ) +} diff --git a/openfeedback/src/commonMain/kotlin/io/.DS_Store b/openfeedback/src/commonMain/kotlin/io/.DS_Store new file mode 100644 index 0000000..c442ffc Binary files /dev/null and b/openfeedback/src/commonMain/kotlin/io/.DS_Store differ diff --git a/openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackRepository.kt b/openfeedback/src/commonMain/kotlin/io/openfeedback/OpenFeedbackRepository.kt similarity index 84% rename from openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackRepository.kt rename to openfeedback/src/commonMain/kotlin/io/openfeedback/OpenFeedbackRepository.kt index cac83ce..0f3251b 100644 --- a/openfeedback/src/main/java/io/openfeedback/android/OpenFeedbackRepository.kt +++ b/openfeedback/src/commonMain/kotlin/io/openfeedback/OpenFeedbackRepository.kt @@ -1,12 +1,12 @@ -package io.openfeedback.android +package io.openfeedback -import io.openfeedback.android.caches.OptimisticVoteCaching -import io.openfeedback.android.model.Project -import io.openfeedback.android.model.SessionVotes -import io.openfeedback.android.model.UserVote -import io.openfeedback.android.model.VoteStatus -import io.openfeedback.android.sources.OpenFeedbackAuth -import io.openfeedback.android.sources.OpenFeedbackFirestore +import io.openfeedback.caches.OptimisticVoteCaching +import io.openfeedback.model.Project +import io.openfeedback.model.SessionVotes +import io.openfeedback.model.UserVote +import io.openfeedback.model.VoteStatus +import io.openfeedback.sources.OpenFeedbackAuth +import io.openfeedback.sources.OpenFeedbackFirestore import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.emptyFlow diff --git a/openfeedback/src/main/java/io/openfeedback/android/caches/OptimisticVoteCaching.kt b/openfeedback/src/commonMain/kotlin/io/openfeedback/caches/OptimisticVoteCaching.kt similarity index 90% rename from openfeedback/src/main/java/io/openfeedback/android/caches/OptimisticVoteCaching.kt rename to openfeedback/src/commonMain/kotlin/io/openfeedback/caches/OptimisticVoteCaching.kt index 8cf89f7..03953df 100644 --- a/openfeedback/src/main/java/io/openfeedback/android/caches/OptimisticVoteCaching.kt +++ b/openfeedback/src/commonMain/kotlin/io/openfeedback/caches/OptimisticVoteCaching.kt @@ -1,7 +1,7 @@ -package io.openfeedback.android.caches +package io.openfeedback.caches -import io.openfeedback.android.model.SessionVotes -import io.openfeedback.android.model.VoteStatus +import io.openfeedback.model.SessionVotes +import io.openfeedback.model.VoteStatus import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update diff --git a/openfeedback/src/commonMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.kt b/openfeedback/src/commonMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.kt new file mode 100644 index 0000000..327157c --- /dev/null +++ b/openfeedback/src/commonMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.kt @@ -0,0 +1,5 @@ +package io.openfeedback.mappers + +import io.openfeedback.model.Comment + +expect fun Map.convertToModel(id: String, voteItemId: String): Comment diff --git a/openfeedback/src/main/java/io/openfeedback/android/model/Model.kt b/openfeedback/src/commonMain/kotlin/io/openfeedback/model/Model.kt similarity index 77% rename from openfeedback/src/main/java/io/openfeedback/android/model/Model.kt rename to openfeedback/src/commonMain/kotlin/io/openfeedback/model/Model.kt index 85f0586..c502c6c 100644 --- a/openfeedback/src/main/java/io/openfeedback/android/model/Model.kt +++ b/openfeedback/src/commonMain/kotlin/io/openfeedback/model/Model.kt @@ -1,12 +1,15 @@ -package io.openfeedback.android.model +package io.openfeedback.model -import com.google.firebase.Timestamp +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +@Serializable data class Project( val chipColors: List = emptyList(), val voteItems: List = emptyList() ) +@Serializable data class VoteItem( val id: String = "", val languages: Map = emptyMap(), @@ -19,26 +22,30 @@ data class VoteItem( } } +@Serializable enum class VoteStatus(val value: String) { Active("active"), Deleted("deleted") } +@Serializable data class UserVote( val voteItemId: String, val voteId: String? ) +@Serializable data class SessionVotes( val votes: Map, val comments: Map ) +@Serializable data class Comment( val id: String = "", val voteItemId: String = "", val text: String = "", val plus: Long = 0L, - val createdAt: Timestamp, - val updatedAt: Timestamp + val createdAt: Instant, + val updatedAt: Instant ) diff --git a/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackAuth.kt b/openfeedback/src/commonMain/kotlin/io/openfeedback/sources/OpenFeedbackAuth.kt similarity index 54% rename from openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackAuth.kt rename to openfeedback/src/commonMain/kotlin/io/openfeedback/sources/OpenFeedbackAuth.kt index 2be51eb..a1a8347 100644 --- a/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackAuth.kt +++ b/openfeedback/src/commonMain/kotlin/io/openfeedback/sources/OpenFeedbackAuth.kt @@ -1,19 +1,21 @@ -package io.openfeedback.android.sources +package io.openfeedback.sources -import android.util.Log -import com.google.firebase.FirebaseApp -import com.google.firebase.auth.FirebaseAuth -import com.google.firebase.auth.FirebaseUser +import co.touchlab.kermit.Logger +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.auth.FirebaseAuth +import dev.gitlive.firebase.auth.FirebaseUser +import dev.gitlive.firebase.auth.auth import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.tasks.await class OpenFeedbackAuth(private val auth: FirebaseAuth) { suspend fun firebaseUser(): FirebaseUser? = Mutex().withLock { if (auth.currentUser == null) { - val result = auth.signInAnonymously().await() + auth.signInAnonymously() + val result = auth.signInAnonymously() if (result.user == null) { - Log.e("OpenFeedbackAuth", "Cannot signInAnonymously") + Logger.e("OpenFeedbackAuth") { "Cannot signInAnonymously" } } } auth.currentUser @@ -25,6 +27,6 @@ class OpenFeedbackAuth(private val auth: FirebaseAuth) { companion object Factory { fun create(app: FirebaseApp): OpenFeedbackAuth = - OpenFeedbackAuth(FirebaseAuth.getInstance(app)) + OpenFeedbackAuth(Firebase.auth(app)) } } diff --git a/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackFirestore.kt b/openfeedback/src/commonMain/kotlin/io/openfeedback/sources/OpenFeedbackFirestore.kt similarity index 53% rename from openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackFirestore.kt rename to openfeedback/src/commonMain/kotlin/io/openfeedback/sources/OpenFeedbackFirestore.kt index 881d9f5..c0c76f7 100644 --- a/openfeedback/src/main/java/io/openfeedback/android/sources/OpenFeedbackFirestore.kt +++ b/openfeedback/src/commonMain/kotlin/io/openfeedback/sources/OpenFeedbackFirestore.kt @@ -1,73 +1,75 @@ -package io.openfeedback.android.sources +package io.openfeedback.sources -import com.google.firebase.FirebaseApp -import com.google.firebase.firestore.FieldValue -import com.google.firebase.firestore.FirebaseFirestore -import com.google.firebase.firestore.FirebaseFirestoreSettings -import io.openfeedback.android.mappers.convertToModel -import io.openfeedback.android.model.Project -import io.openfeedback.android.model.SessionVotes -import io.openfeedback.android.model.UserVote -import io.openfeedback.android.model.VoteStatus -import io.openfeedback.android.toFlow +import dev.gitlive.firebase.Firebase +import dev.gitlive.firebase.FirebaseApp +import dev.gitlive.firebase.SpecialValueSerializer +import dev.gitlive.firebase.firestore.FieldValue +import dev.gitlive.firebase.firestore.FirebaseFirestore +import dev.gitlive.firebase.firestore.firestore +import dev.gitlive.firebase.firestore.where +import io.openfeedback.mappers.convertToModel +import io.openfeedback.model.Project +import io.openfeedback.model.SessionVotes +import io.openfeedback.model.UserVote +import io.openfeedback.model.VoteStatus import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import kotlinx.coroutines.tasks.await +import kotlinx.coroutines.flow.mapNotNull -class OpenFeedbackFirestore( - private val firestore: FirebaseFirestore -) { +@Suppress("UNCHECKED_CAST") +class OpenFeedbackFirestore(private val firestore: FirebaseFirestore) { fun project(projectId: String): Flow = firestore.collection("projects") .document(projectId) - .toFlow() - .map { querySnapshot -> - querySnapshot.toObject(Project::class.java)!! - } + .snapshots + .map { querySnapshot -> querySnapshot.data() } fun userVotes(projectId: String, userId: String, sessionId: String): Flow> = firestore.collection("projects/$projectId/userVotes") - .whereEqualTo("userId", userId) - .whereEqualTo("status", VoteStatus.Active.value) - .whereEqualTo("talkId", sessionId) - .toFlow() + .where { "userId" equalTo userId } + .where { "status" equalTo VoteStatus.Active.value } + .where { "talkId" equalTo sessionId } + .snapshots .map { querySnapshot -> - querySnapshot.map { - UserVote( - voteItemId = it.data["voteItemId"] as String, - voteId = it.data["voteId"] as String? - ) + querySnapshot.documents.map { + it.data() } } fun sessionVotes(projectId: String, sessionId: String): Flow = firestore.collection("projects/$projectId/sessionVotes") .document(sessionId) - .toFlow() - .map { querySnapshot -> - SessionVotes( - votes = querySnapshot.data - ?.filter { it.value is Long } as? Map - ?: emptyMap(), // If there's no vote yet, default to an empty map } - comments = querySnapshot.data - ?.filter { it.value is HashMap<*, *> } - ?.map { - val voteItemId = it.key - (it.value as HashMap<*, *>).entries - .filter { (it.value as Map).isNotEmpty() } - .map { entry -> - entry.key as String to (entry.value as Map) - .convertToModel( - id = entry.key as String, - voteItemId = voteItemId - ) + .snapshots + .mapNotNull { documentSnapshot -> + if (documentSnapshot.exists.not()) { + return@mapNotNull null + } + documentSnapshot.data(strategy = SpecialValueSerializer( + serialName = "SessionVotes", + toNativeValue = {}, + fromNativeValue = { + val data = it as HashMap + SessionVotes( + votes = data.filter { it.value is Long } as Map, + comments = data + .filter { it.value is HashMap<*, *> } + .map { + val voteItemId = it.key + (it.value as HashMap<*, *>).entries + .filter { (it.value as Map).isNotEmpty() } + .map { entry -> + entry.key as String to (entry.value as Map) + .convertToModel( + id = entry.key as String, + voteItemId = voteItemId + ) + } } - } - ?.flatten() - ?.sortedBy { it.second.createdAt } - ?.associate { it.first to it.second } - ?: emptyMap() - ) + .flatten() + .associate { it.first to it.second } + ) + } + )) } suspend fun newComment( @@ -81,21 +83,20 @@ class OpenFeedbackFirestore( if (text.trim() == "") return val collectionReference = firestore.collection("projects/$projectId/userVotes") val querySnapshot = collectionReference - .whereEqualTo("userId", userId) - .whereEqualTo("talkId", talkId) - .whereEqualTo("voteItemId", voteItemId) + .where { "userId" equalTo userId } + .where { "talkId" equalTo talkId } + .where { "voteItemId" equalTo voteItemId } .get() - .await() - if (querySnapshot.isEmpty) { - val documentReference = collectionReference.document() + if (querySnapshot.documents.isEmpty()) { + val documentReference = collectionReference.document documentReference.set( mapOf( "id" to documentReference.id, - "createdAt" to FieldValue.serverTimestamp(), + "createdAt" to FieldValue.serverTimestamp, "projectId" to projectId, "status" to status.value, "talkId" to talkId, - "updatedAt" to FieldValue.serverTimestamp(), + "updatedAt" to FieldValue.serverTimestamp, "userId" to userId, "voteItemId" to voteItemId, "text" to text.trim() @@ -106,7 +107,7 @@ class OpenFeedbackFirestore( .document(querySnapshot.documents[0].id) .update( mapOf( - "updatedAt" to FieldValue.serverTimestamp(), + "updatedAt" to FieldValue.serverTimestamp, "status" to status.value, "text" to text.trim() ) @@ -123,21 +124,20 @@ class OpenFeedbackFirestore( ) { val collectionReference = firestore.collection("projects/$projectId/userVotes") val querySnapshot = collectionReference - .whereEqualTo("userId", userId) - .whereEqualTo("talkId", talkId) - .whereEqualTo("voteItemId", voteItemId) + .where { "userId" equalTo userId } + .where { "talkId" equalTo talkId } + .where { "voteItemId" equalTo voteItemId } .get() - .await() - if (querySnapshot.isEmpty) { - val documentReference = collectionReference.document() + if (querySnapshot.documents.isEmpty()) { + val documentReference = collectionReference.document documentReference.set( mapOf( "id" to documentReference.id, - "createdAt" to FieldValue.serverTimestamp(), + "createdAt" to FieldValue.serverTimestamp, "projectId" to projectId, "status" to status.value, "talkId" to talkId, - "updatedAt" to FieldValue.serverTimestamp(), + "updatedAt" to FieldValue.serverTimestamp, "userId" to userId, "voteItemId" to voteItemId ) @@ -147,7 +147,7 @@ class OpenFeedbackFirestore( .document(querySnapshot.documents[0].id) .update( mapOf( - "updatedAt" to FieldValue.serverTimestamp(), + "updatedAt" to FieldValue.serverTimestamp, "status" to status.value ) ) @@ -164,14 +164,13 @@ class OpenFeedbackFirestore( ) { val collectionReference = firestore.collection("projects/$projectId/userVotes") val querySnapshot = collectionReference - .whereEqualTo("userId", userId) - .whereEqualTo("talkId", talkId) - .whereEqualTo("voteItemId", voteItemId) - .whereEqualTo("voteId", voteId) + .where { "userId" equalTo userId } + .where { "talkId" equalTo talkId } + .where { "voteItemId" equalTo voteItemId } + .where { "voteId" equalTo voteId } .get() - .await() - if (querySnapshot.isEmpty) { - val documentReference = collectionReference.document() + if (querySnapshot.documents.isEmpty()) { + val documentReference = collectionReference.document documentReference.set( mapOf( "projectId" to projectId, @@ -179,8 +178,8 @@ class OpenFeedbackFirestore( "voteItemId" to voteItemId, "id" to documentReference.id, "voteId" to voteId, - "createdAt" to FieldValue.serverTimestamp(), - "updatedAt" to FieldValue.serverTimestamp(), + "createdAt" to FieldValue.serverTimestamp, + "updatedAt" to FieldValue.serverTimestamp, "voteType" to "textPlus", "userId" to userId, "status" to status.value @@ -191,7 +190,7 @@ class OpenFeedbackFirestore( .document(querySnapshot.documents[0].id) .update( mapOf( - "updatedAt" to FieldValue.serverTimestamp(), + "updatedAt" to FieldValue.serverTimestamp, "status" to status.value ) ) @@ -200,10 +199,8 @@ class OpenFeedbackFirestore( companion object Factory { fun create(app: FirebaseApp): OpenFeedbackFirestore { - val firestore = FirebaseFirestore.getInstance(app) - firestore.firestoreSettings = FirebaseFirestoreSettings.Builder() - .setPersistenceEnabled(true) - .build() + val firestore = Firebase.firestore(app) + firestore.setSettings(persistenceEnabled = true) return OpenFeedbackFirestore(firestore) } } diff --git a/openfeedback/src/main/res/values/strings.xml b/openfeedback/src/commonMain/moko-resources/base/strings.xml similarity index 100% rename from openfeedback/src/main/res/values/strings.xml rename to openfeedback/src/commonMain/moko-resources/base/strings.xml diff --git a/openfeedback/src/main/res/values-fr/strings.xml b/openfeedback/src/commonMain/moko-resources/fr/strings.xml similarity index 100% rename from openfeedback/src/main/res/values-fr/strings.xml rename to openfeedback/src/commonMain/moko-resources/fr/strings.xml diff --git a/openfeedback/src/main/res/drawable/openfeedback_dark.png b/openfeedback/src/commonMain/moko-resources/images/openfeedback_dark@1x.png similarity index 100% rename from openfeedback/src/main/res/drawable/openfeedback_dark.png rename to openfeedback/src/commonMain/moko-resources/images/openfeedback_dark@1x.png diff --git a/openfeedback/src/main/res/drawable/openfeedback_light.png b/openfeedback/src/commonMain/moko-resources/images/openfeedback_light@1x.png similarity index 100% rename from openfeedback/src/main/res/drawable/openfeedback_light.png rename to openfeedback/src/commonMain/moko-resources/images/openfeedback_light@1x.png diff --git a/openfeedback/src/iosMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.ios.kt b/openfeedback/src/iosMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.ios.kt new file mode 100644 index 0000000..fa1d34a --- /dev/null +++ b/openfeedback/src/iosMain/kotlin/io/openfeedback/mappers/FirestoreToModelMappers.ios.kt @@ -0,0 +1,21 @@ +package io.openfeedback.mappers + +import dev.gitlive.firebase.firestore.Timestamp +import io.openfeedback.model.Comment +import kotlinx.datetime.Instant + +actual fun Map.convertToModel( + id: String, + voteItemId: String +): Comment { + val createdAt = this["createdAt"] as Timestamp + val updatedAt = this["updatedAt"] as Timestamp + return Comment( + id = id, + voteItemId = voteItemId, + text = this["text"] as String, + plus = this["plus"] as Long, + createdAt = Instant.fromEpochSeconds(createdAt.seconds, createdAt.nanoseconds), + updatedAt = Instant.fromEpochSeconds(updatedAt.seconds, updatedAt.nanoseconds) + ) +} diff --git a/openfeedback/src/main/java/io/openfeedback/android/FirebaseExtensions.kt b/openfeedback/src/main/java/io/openfeedback/android/FirebaseExtensions.kt deleted file mode 100644 index 6fb1e14..0000000 --- a/openfeedback/src/main/java/io/openfeedback/android/FirebaseExtensions.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.openfeedback.android - -import com.google.firebase.firestore.DocumentReference -import com.google.firebase.firestore.DocumentSnapshot -import com.google.firebase.firestore.MetadataChanges -import com.google.firebase.firestore.Query -import com.google.firebase.firestore.QuerySnapshot -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.flow.conflate - -internal fun Query.toFlow(): Flow = callbackFlow { - val registration = addSnapshotListener(MetadataChanges.INCLUDE) { snapshot, exception -> - if (snapshot != null) { - runCatching { - trySend(snapshot) - } - } - if (exception != null) { - close(exception) - } - } - - awaitClose { - registration.remove() - } -}.conflate() - -internal fun DocumentReference.toFlow(): Flow = callbackFlow { - val registration = addSnapshotListener(MetadataChanges.INCLUDE) { snapshot, exception -> - if (snapshot != null) { - runCatching { - trySend(snapshot) - } - } - if (exception != null) { - close(exception) - } - } - - awaitClose { - registration.remove() - } -}.conflate() diff --git a/openfeedback/src/main/java/io/openfeedback/android/mappers/FirestoreToModelMappers.kt b/openfeedback/src/main/java/io/openfeedback/android/mappers/FirestoreToModelMappers.kt deleted file mode 100644 index 58efac0..0000000 --- a/openfeedback/src/main/java/io/openfeedback/android/mappers/FirestoreToModelMappers.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.openfeedback.android.mappers - -import com.google.firebase.Timestamp -import io.openfeedback.android.model.Comment - -internal fun Map.convertToModel( - id: String, - voteItemId: String -): Comment = Comment( - id = id, - voteItemId = voteItemId, - text = this["text"] as String, - plus = this["plus"] as Long, - createdAt = this["createdAt"] as Timestamp, - updatedAt = this["updatedAt"] as Timestamp -) diff --git a/sample-app/.gitignore b/sample-app-android/.gitignore similarity index 100% rename from sample-app/.gitignore rename to sample-app-android/.gitignore diff --git a/sample-app-android/build.gradle.kts b/sample-app-android/build.gradle.kts new file mode 100644 index 0000000..1725353 --- /dev/null +++ b/sample-app-android/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id("com.android.application") + id("org.jetbrains.kotlin.android") + id("org.jetbrains.compose") +} + +androidApp("io.openfeedback.android") + +android { + defaultConfig { + versionCode = 1 + versionName = "1" + } +} + +dependencies { + implementation(projects.openfeedbackM3) + implementation(projects.sampleAppShared) + + implementation(libs.androidx.core) + implementation(libs.appcompat) + implementation(libs.activity.compose) + + implementation(compose.material3) +} diff --git a/sample-app/proguard-rules.pro b/sample-app-android/proguard-rules.pro similarity index 100% rename from sample-app/proguard-rules.pro rename to sample-app-android/proguard-rules.pro diff --git a/sample-app/src/main/AndroidManifest.xml b/sample-app-android/src/main/AndroidManifest.xml similarity index 100% rename from sample-app/src/main/AndroidManifest.xml rename to sample-app-android/src/main/AndroidManifest.xml diff --git a/sample-app-android/src/main/java/io/openfeedback/android/MainActivity.kt b/sample-app-android/src/main/java/io/openfeedback/android/MainActivity.kt new file mode 100644 index 0000000..5c262dd --- /dev/null +++ b/sample-app-android/src/main/java/io/openfeedback/android/MainActivity.kt @@ -0,0 +1,23 @@ +package io.openfeedback.android + +import SampleApp +import android.annotation.SuppressLint +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.isSystemInDarkTheme + +class MainActivity : AppCompatActivity() { + @SuppressLint("UnusedMaterialScaffoldPaddingParameter") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + val isDark = isSystemInDarkTheme() + SampleApp( + isSystemLight = !isDark, + context = (application as MainApplication).context, + ) + } + } +} diff --git a/sample-app-android/src/main/java/io/openfeedback/android/MainApplication.kt b/sample-app-android/src/main/java/io/openfeedback/android/MainApplication.kt new file mode 100644 index 0000000..d7241a8 --- /dev/null +++ b/sample-app-android/src/main/java/io/openfeedback/android/MainApplication.kt @@ -0,0 +1,13 @@ +package io.openfeedback.android + +import android.app.Application +import android.content.Context + +class MainApplication: Application() { + lateinit var context: Context + + override fun onCreate() { + super.onCreate() + context = this + } +} \ No newline at end of file diff --git a/sample-app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample-app-android/src/main/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from sample-app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to sample-app-android/src/main/res/drawable-v24/ic_launcher_foreground.xml diff --git a/sample-app/src/main/res/drawable/ic_launcher_background.xml b/sample-app-android/src/main/res/drawable/ic_launcher_background.xml similarity index 100% rename from sample-app/src/main/res/drawable/ic_launcher_background.xml rename to sample-app-android/src/main/res/drawable/ic_launcher_background.xml diff --git a/sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample-app-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to sample-app-android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample-app-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from sample-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to sample-app-android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/sample-app/src/main/res/mipmap-hdpi/ic_launcher.png b/sample-app-android/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from sample-app/src/main/res/mipmap-hdpi/ic_launcher.png rename to sample-app-android/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/sample-app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/sample-app-android/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from sample-app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to sample-app-android/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/sample-app/src/main/res/mipmap-mdpi/ic_launcher.png b/sample-app-android/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from sample-app/src/main/res/mipmap-mdpi/ic_launcher.png rename to sample-app-android/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/sample-app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/sample-app-android/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from sample-app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to sample-app-android/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/sample-app/src/main/res/mipmap-xhdpi/ic_launcher.png b/sample-app-android/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from sample-app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to sample-app-android/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/sample-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/sample-app-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from sample-app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to sample-app-android/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/sample-app-android/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from sample-app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to sample-app-android/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/sample-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/sample-app-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from sample-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to sample-app-android/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/sample-app-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to sample-app-android/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample-app-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from sample-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to sample-app-android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/sample-app/src/main/res/values/colors.xml b/sample-app-android/src/main/res/values/colors.xml similarity index 100% rename from sample-app/src/main/res/values/colors.xml rename to sample-app-android/src/main/res/values/colors.xml diff --git a/sample-app/src/main/res/values/strings.xml b/sample-app-android/src/main/res/values/strings.xml similarity index 100% rename from sample-app/src/main/res/values/strings.xml rename to sample-app-android/src/main/res/values/strings.xml diff --git a/sample-app/src/main/res/values/styles.xml b/sample-app-android/src/main/res/values/styles.xml similarity index 100% rename from sample-app/src/main/res/values/styles.xml rename to sample-app-android/src/main/res/values/styles.xml diff --git a/sample-app-ios/io.openfeedback.ios.xcodeproj/project.pbxproj b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.pbxproj new file mode 100644 index 0000000..115f86f --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.pbxproj @@ -0,0 +1,433 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 1167ADE02BB0CFED001541D8 /* io_openfeedback_iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1167ADDF2BB0CFED001541D8 /* io_openfeedback_iosApp.swift */; }; + 1167ADE22BB0CFED001541D8 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1167ADE12BB0CFED001541D8 /* ContentView.swift */; }; + 1167ADE42BB0CFEE001541D8 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1167ADE32BB0CFEE001541D8 /* Assets.xcassets */; }; + 1167ADE72BB0CFEE001541D8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1167ADE62BB0CFEE001541D8 /* Preview Assets.xcassets */; }; + 11D2416F2BB0D96D00370624 /* FirebaseAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 11D2416E2BB0D96D00370624 /* FirebaseAuth */; }; + 11D241712BB0D96D00370624 /* FirebaseFirestore in Frameworks */ = {isa = PBXBuildFile; productRef = 11D241702BB0D96D00370624 /* FirebaseFirestore */; }; + 11D241762BB109C900370624 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 11D241752BB109C900370624 /* GoogleService-Info.plist */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1167ADDC2BB0CFED001541D8 /* io.openfeedback.ios.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = io.openfeedback.ios.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1167ADDF2BB0CFED001541D8 /* io_openfeedback_iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = io_openfeedback_iosApp.swift; sourceTree = ""; }; + 1167ADE12BB0CFED001541D8 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 1167ADE32BB0CFEE001541D8 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1167ADE62BB0CFEE001541D8 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 11D241752BB109C900370624 /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../../Downloads/GoogleService-Info.plist"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1167ADD92BB0CFED001541D8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 11D2416F2BB0D96D00370624 /* FirebaseAuth in Frameworks */, + 11D241712BB0D96D00370624 /* FirebaseFirestore in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1167ADD32BB0CFED001541D8 = { + isa = PBXGroup; + children = ( + 1167ADDE2BB0CFED001541D8 /* io.openfeedback.ios */, + 1167ADDD2BB0CFED001541D8 /* Products */, + ); + sourceTree = ""; + }; + 1167ADDD2BB0CFED001541D8 /* Products */ = { + isa = PBXGroup; + children = ( + 1167ADDC2BB0CFED001541D8 /* io.openfeedback.ios.app */, + ); + name = Products; + sourceTree = ""; + }; + 1167ADDE2BB0CFED001541D8 /* io.openfeedback.ios */ = { + isa = PBXGroup; + children = ( + 1167ADDF2BB0CFED001541D8 /* io_openfeedback_iosApp.swift */, + 1167ADE12BB0CFED001541D8 /* ContentView.swift */, + 1167ADE32BB0CFEE001541D8 /* Assets.xcassets */, + 11D241752BB109C900370624 /* GoogleService-Info.plist */, + 1167ADE52BB0CFEE001541D8 /* Preview Content */, + ); + path = io.openfeedback.ios; + sourceTree = ""; + }; + 1167ADE52BB0CFEE001541D8 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 1167ADE62BB0CFEE001541D8 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1167ADDB2BB0CFED001541D8 /* io.openfeedback.ios */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1167ADEA2BB0CFEE001541D8 /* Build configuration list for PBXNativeTarget "io.openfeedback.ios" */; + buildPhases = ( + 1167ADED2BB0D0EB001541D8 /* ShellScript */, + 1167ADD82BB0CFED001541D8 /* Sources */, + 1167ADD92BB0CFED001541D8 /* Frameworks */, + 1167ADDA2BB0CFED001541D8 /* Resources */, + 11D241742BB0F69300370624 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = io.openfeedback.ios; + packageProductDependencies = ( + 11D2416E2BB0D96D00370624 /* FirebaseAuth */, + 11D241702BB0D96D00370624 /* FirebaseFirestore */, + ); + productName = io.openfeedback.ios; + productReference = 1167ADDC2BB0CFED001541D8 /* io.openfeedback.ios.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1167ADD42BB0CFED001541D8 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + 1167ADDB2BB0CFED001541D8 = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = 1167ADD72BB0CFED001541D8 /* Build configuration list for PBXProject "io.openfeedback.ios" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1167ADD32BB0CFED001541D8; + packageReferences = ( + 11D2416D2BB0D96D00370624 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */, + ); + productRefGroup = 1167ADDD2BB0CFED001541D8 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1167ADDB2BB0CFED001541D8 /* io.openfeedback.ios */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1167ADDA2BB0CFED001541D8 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1167ADE72BB0CFEE001541D8 /* Preview Assets.xcassets in Resources */, + 1167ADE42BB0CFEE001541D8 /* Assets.xcassets in Resources */, + 11D241762BB109C900370624 /* GoogleService-Info.plist in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 1167ADED2BB0D0EB001541D8 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\ncd \"$SRCROOT/..\"\n./gradlew :sample-app-shared:embedAndSignAppleFrameworkForXcode\n"; + }; + 11D241742BB0F69300370624 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n\"$SRCROOT/../gradlew\" -p \"$SRCROOT/../\" :sample-app-shared:copyFrameworkResourcesToApp \\\n -Pmoko.resources.PLATFORM_NAME=\"$PLATFORM_NAME\" \\\n -Pmoko.resources.CONFIGURATION=\"$CONFIGURATION\" \\\n -Pmoko.resources.ARCHS=\"$ARCHS\" \\\n -Pmoko.resources.BUILT_PRODUCTS_DIR=\"$BUILT_PRODUCTS_DIR\" \\\n -Pmoko.resources.CONTENTS_FOLDER_PATH=\"$CONTENTS_FOLDER_PATH\"\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1167ADD82BB0CFED001541D8 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1167ADE22BB0CFED001541D8 /* ContentView.swift in Sources */, + 1167ADE02BB0CFED001541D8 /* io_openfeedback_iosApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1167ADE82BB0CFEE001541D8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1167ADE92BB0CFEE001541D8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.4; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1167ADEB2BB0CFEE001541D8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"io.openfeedback.ios/Preview Content\""; + DEVELOPMENT_TEAM = F9RAPYL9T3; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../sample-app-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.openfeedback.io-openfeedback-ios"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1167ADEC2BB0CFEE001541D8 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"io.openfeedback.ios/Preview Content\""; + DEVELOPMENT_TEAM = F9RAPYL9T3; + ENABLE_PREVIEWS = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../sample-app-shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "io.openfeedback.io-openfeedback-ios"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1167ADD72BB0CFED001541D8 /* Build configuration list for PBXProject "io.openfeedback.ios" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1167ADE82BB0CFEE001541D8 /* Debug */, + 1167ADE92BB0CFEE001541D8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1167ADEA2BB0CFEE001541D8 /* Build configuration list for PBXNativeTarget "io.openfeedback.ios" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1167ADEB2BB0CFEE001541D8 /* Debug */, + 1167ADEC2BB0CFEE001541D8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 11D2416D2BB0D96D00370624 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/firebase/firebase-ios-sdk.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 10.23.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 11D2416E2BB0D96D00370624 /* FirebaseAuth */ = { + isa = XCSwiftPackageProductDependency; + package = 11D2416D2BB0D96D00370624 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseAuth; + }; + 11D241702BB0D96D00370624 /* FirebaseFirestore */ = { + isa = XCSwiftPackageProductDependency; + package = 11D2416D2BB0D96D00370624 /* XCRemoteSwiftPackageReference "firebase-ios-sdk" */; + productName = FirebaseFirestore; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 1167ADD42BB0CFED001541D8 /* Project object */; +} diff --git a/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..04e7c42 --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,123 @@ +{ + "originHash" : "a1569f9895aa2be8e24832f98525d5da4eb90b5d158a82691c15b47eb72a13d7", + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "7ce7be095bc3ed3c98b009532fe2d7698c132614", + "version" : "1.2024011601.0" + } + }, + { + "identity" : "app-check", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/app-check.git", + "state" : { + "revision" : "3e464dad87dad2d29bb29a97836789bf0f8f67d2", + "version" : "10.18.1" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk.git", + "state" : { + "revision" : "fcf5ced6dae2d43fced2581e673cc3b59bdb8ffa", + "version" : "10.23.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "6ec4ca62b00a665fa09b594fab897753a8c635fa", + "version" : "10.23.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "a637d318ae7ae246b02d7305121275bc75ed5565", + "version" : "9.4.0" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "26c898aed8bed13b8a63057ee26500abbbcb8d55", + "version" : "7.13.1" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "67043f6389d0e28b38fa02d1c6952afeb04d807f", + "version" : "1.62.1" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "9534039303015a84837090d20fa21cae6e5eadb6", + "version" : "3.3.2" + } + }, + { + "identity" : "interop-ios-for-google-sdks", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/interop-ios-for-google-sdks.git", + "state" : { + "revision" : "2d12673670417654f08f5f90fdd62926dc3a2648", + "version" : "100.0.0" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "43aaef65e0c665daadf848761d560e446d350d3d", + "version" : "1.22.4" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "b7e1104502eca3a213b46303391ca4d3bc8ddec1", + "version" : "2.30910.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "540318ecedd63d883069ae7f1ed811a2df00b6ac", + "version" : "2.4.0" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "65e8f29b2d63c4e38e736b25c27b83e012159be8", + "version" : "1.25.2" + } + } + ], + "version" : 3 +} diff --git a/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcuserdata/mbonnin.xcuserdatad/UserInterfaceState.xcuserstate b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcuserdata/mbonnin.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000..2b905c4 Binary files /dev/null and b/sample-app-ios/io.openfeedback.ios.xcodeproj/project.xcworkspace/xcuserdata/mbonnin.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/sample-app-ios/io.openfeedback.ios.xcodeproj/xcshareddata/xcschemes/io.openfeedback.ios.xcscheme b/sample-app-ios/io.openfeedback.ios.xcodeproj/xcshareddata/xcschemes/io.openfeedback.ios.xcscheme new file mode 100644 index 0000000..d74dd7d --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios.xcodeproj/xcshareddata/xcschemes/io.openfeedback.ios.xcscheme @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sample-app-ios/io.openfeedback.ios.xcodeproj/xcuserdata/mbonnin.xcuserdatad/xcschemes/xcschememanagement.plist b/sample-app-ios/io.openfeedback.ios.xcodeproj/xcuserdata/mbonnin.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..b0a8241 --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios.xcodeproj/xcuserdata/mbonnin.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,43 @@ + + + + + SchemeUserState + + Promises (Playground) 1.xcscheme + + isShown + + orderHint + 2 + + Promises (Playground) 2.xcscheme + + isShown + + orderHint + 3 + + Promises (Playground).xcscheme + + isShown + + orderHint + 0 + + io.openfeedback.ios.xcscheme_^#shared#^_ + + orderHint + 1 + + + SuppressBuildableAutocreation + + 1167ADDB2BB0CFED001541D8 + + primary + + + + + diff --git a/sample-app-ios/io.openfeedback.ios/Assets.xcassets/AccentColor.colorset/Contents.json b/sample-app-ios/io.openfeedback.ios/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/sample-app-ios/io.openfeedback.ios/Assets.xcassets/AppIcon.appiconset/Contents.json b/sample-app-ios/io.openfeedback.ios/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/sample-app-ios/io.openfeedback.ios/Assets.xcassets/Contents.json b/sample-app-ios/io.openfeedback.ios/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/sample-app-ios/io.openfeedback.ios/ContentView.swift b/sample-app-ios/io.openfeedback.ios/ContentView.swift new file mode 100644 index 0000000..028489e --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios/ContentView.swift @@ -0,0 +1,30 @@ +// +// ContentView.swift +// io.openfeedback.ios +// +// Created by Martin on 24/03/2024. +// + +import SwiftUI +import UIKit +import SwiftUI +import SampleApp +import FirebaseCore + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { +// let options = FirebaseOptions(googleAppID: "1:635903227116:web:31de912f8bf29befb1e1c9", gcmSenderID: "lknlkn") +// FirebaseApp.configure(options: options) +// + return MainViewControllerKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +struct ContentView: View { + var body: some View { + ComposeView() + .ignoresSafeArea(.keyboard) // Compose has own keyboard handler + } +} diff --git a/sample-app-ios/io.openfeedback.ios/Preview Content/Preview Assets.xcassets/Contents.json b/sample-app-ios/io.openfeedback.ios/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/sample-app-ios/io.openfeedback.ios/io_openfeedback_iosApp.swift b/sample-app-ios/io.openfeedback.ios/io_openfeedback_iosApp.swift new file mode 100644 index 0000000..3a5d359 --- /dev/null +++ b/sample-app-ios/io.openfeedback.ios/io_openfeedback_iosApp.swift @@ -0,0 +1,29 @@ +// +// io_openfeedback_iosApp.swift +// io.openfeedback.ios +// +// Created by Martin on 24/03/2024. +// + +import SwiftUI +import FirebaseCore + +class AppDelegate: NSObject, UIApplicationDelegate { + + func application(_ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + return true + } +} + +@main +struct io_openfeedback_iosApp: App { + + @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate + + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/sample-app-shared/build.gradle.kts b/sample-app-shared/build.gradle.kts new file mode 100644 index 0000000..e0d4edd --- /dev/null +++ b/sample-app-shared/build.gradle.kts @@ -0,0 +1,38 @@ +import org.jetbrains.compose.ComposePlugin +import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget + +plugins { + id("com.android.library") +} + +library( + namespace = "io.openfeedback.shared", + moko = true, + compose = true +) { kotlinMultiplatformExtension -> + with(kotlinMultiplatformExtension) { + targets.forEach { + if (it is KotlinNativeTarget) { + it.binaries { + this.framework { + baseName = "SampleApp" + isStatic = true + } + } + } + } + + kotlinMultiplatformExtension.sourceSets { + getByName("commonMain") { + dependencies { + implementation(compose.ui) + implementation(compose.foundation) + implementation(compose.runtime) + implementation(projects.openfeedbackM3) + implementation(compose.material3) + } + } + } + } +} + diff --git a/sample-app-shared/src/commonMain/kotlin/io/openfeedback/shared/main.kt b/sample-app-shared/src/commonMain/kotlin/io/openfeedback/shared/main.kt new file mode 100644 index 0000000..a65a0d3 --- /dev/null +++ b/sample-app-shared/src/commonMain/kotlin/io/openfeedback/shared/main.kt @@ -0,0 +1,102 @@ +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import io.openfeedback.m3.OpenFeedback +import io.openfeedback.viewmodels.OpenFeedbackFirebaseConfig + +@Composable +fun SampleApp( + isSystemLight: Boolean, + context: Any?, +) { + var isLight by rememberSaveable(isSystemLight) { mutableStateOf(isSystemLight) } + OpenFeedbackTheme( + isLight = isLight + ) { + Scaffold { + LazyColumn(contentPadding = it) { + item { + ThemeSwitcher(isLight = isLight) { isLight = it } + } + item { + /** + * The firebase parameters are from the openfeedback.io project so we can + * access firestore directly + */ + val openFeedbackFirebaseConfig = OpenFeedbackFirebaseConfig( + context = context, + projectId = "open-feedback-42", + // Hack: I replaced :web: by :ios: for the iOS SDK to behave + applicationId = "1:635903227116:ios:31de912f8bf29befb1e1c9", + apiKey = "AIzaSyB3ELJsaiItrln0uDGSuuHE1CfOJO67Hb4", + databaseUrl = "https://open-feedback-42.firebaseio.com/" + ) + /** + * The project and session Ids are taken from openfeedback.io demo conference: + * https://openfeedback.io/eaJnyMXD3oNfhrrnBYDT/ + */ + OpenFeedback( + config = openFeedbackFirebaseConfig, + projectId = "eaJnyMXD3oNfhrrnBYDT", + sessionId = "100", + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } + } + } +} + +@Composable +fun OpenFeedbackTheme( + isLight: Boolean, + content: @Composable () -> Unit +) { + val colorScheme = when { + else -> if (isLight) lightColorScheme() else darkColorScheme() + } + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} + +@Composable +fun ThemeSwitcher( + isLight: Boolean, + onLightDarkChanged: (Boolean) -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth() + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text(text = "Dark") + Switch(checked = isLight, onCheckedChange = { + onLightDarkChanged(!isLight) + }) + Text(text = "Light") + } + } +} diff --git a/sample-app-shared/src/iosMain/kotlin/io/openfeedback/shared/MainViewController.kt b/sample-app-shared/src/iosMain/kotlin/io/openfeedback/shared/MainViewController.kt new file mode 100644 index 0000000..0ff011d --- /dev/null +++ b/sample-app-shared/src/iosMain/kotlin/io/openfeedback/shared/MainViewController.kt @@ -0,0 +1,11 @@ +package io.openfeedback.shared + +import SampleApp +import androidx.compose.ui.window.ComposeUIViewController + +fun MainViewController() = ComposeUIViewController { + SampleApp( + true, + null + ) +} \ No newline at end of file diff --git a/sample-app/build.gradle.kts b/sample-app/build.gradle.kts deleted file mode 100644 index fa9ec33..0000000 --- a/sample-app/build.gradle.kts +++ /dev/null @@ -1,24 +0,0 @@ -plugins { - id("io.openfeedback.plugins.app") -} - -android { - namespace = "io.openfeedback.android.sample" - defaultConfig { - versionCode = 1 - versionName = "1" - } -} - -dependencies { - implementation(libs.androidx.core) - implementation("androidx.appcompat:appcompat:1.5.1") - implementation("androidx.activity:activity-compose:1.7.0-alpha02") - - implementation(platform(libs.androidx.compose.bom)) - implementation(libs.androidx.compose.material) - implementation(libs.androidx.compose.material3) - - implementation(projects.openfeedbackM2) - implementation(projects.openfeedbackM3) -} diff --git a/sample-app/src/main/java/io/openfeedback/android/sample/MainActivity.kt b/sample-app/src/main/java/io/openfeedback/android/sample/MainActivity.kt deleted file mode 100644 index a56396c..0000000 --- a/sample-app/src/main/java/io/openfeedback/android/sample/MainActivity.kt +++ /dev/null @@ -1,117 +0,0 @@ -package io.openfeedback.android.sample - -import android.annotation.SuppressLint -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.appcompat.app.AppCompatActivity -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import io.openfeedback.android.m2.OpenFeedback -import io.openfeedback.android.sample.theme.DesignSystem -import io.openfeedback.android.sample.theme.OpenFeedbackTheme - -class MainActivity : AppCompatActivity() { - @SuppressLint("UnusedMaterialScaffoldPaddingParameter") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val projectId = "mMHR63ARZQpPidFQISyc" - val sessionId = "173222" - val openFeedbackFirebaseConfig = (application as MainApplication).openFeedbackFirebaseConfig - setContent { - var designSystem by rememberSaveable { mutableStateOf(DesignSystem.M3) } - val isDark = isSystemInDarkTheme() - var isLight by rememberSaveable(isDark) { mutableStateOf(isDark.not()) } - OpenFeedbackTheme( - designSystem = designSystem, - isLight = isLight - ) { - Scaffold { - LazyColumn(contentPadding = it) { - item { - ThemeSwitcher( - designSystem = designSystem, - isLight = isLight, - onDesignSystemChanged = { designSystem = it }, - onLightDarkChanged = { isLight = it } - ) - } - item { - when (designSystem) { - DesignSystem.M2 -> - OpenFeedback( - config = openFeedbackFirebaseConfig, - projectId = projectId, - sessionId = sessionId, - modifier = Modifier - .padding(horizontal = 8.dp, vertical = 4.dp) - ) - - DesignSystem.M3 -> - io.openfeedback.android.m3.OpenFeedback( - config = openFeedbackFirebaseConfig, - projectId = projectId, - sessionId = sessionId, - modifier = Modifier - .padding(horizontal = 8.dp, vertical = 4.dp) - ) - } - } - } - } - } - } - } -} - -@Composable -fun ThemeSwitcher( - designSystem: DesignSystem, - isLight: Boolean, - onDesignSystemChanged: (DesignSystem) -> Unit, - onLightDarkChanged: (Boolean) -> Unit -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.fillMaxWidth() - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = "Material 2") - Switch(checked = designSystem == DesignSystem.M3, onCheckedChange = { - onDesignSystemChanged( - if (designSystem == DesignSystem.M2) DesignSystem.M3 else DesignSystem.M2 - ) - }) - Text(text = "Material 3") - } - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text(text = "Dark") - Switch(checked = isLight, onCheckedChange = { - onLightDarkChanged(!isLight) - }) - Text(text = "Light") - } - } -} diff --git a/sample-app/src/main/java/io/openfeedback/android/sample/MainApplication.kt b/sample-app/src/main/java/io/openfeedback/android/sample/MainApplication.kt deleted file mode 100644 index bc4e0ee..0000000 --- a/sample-app/src/main/java/io/openfeedback/android/sample/MainApplication.kt +++ /dev/null @@ -1,19 +0,0 @@ -package io.openfeedback.android.sample - -import android.app.Application -import io.openfeedback.android.viewmodels.OpenFeedbackFirebaseConfig - -class MainApplication: Application() { - lateinit var openFeedbackFirebaseConfig: OpenFeedbackFirebaseConfig - - override fun onCreate() { - super.onCreate() - openFeedbackFirebaseConfig = OpenFeedbackFirebaseConfig( - context = this, - projectId = "openfeedback-b7ab9", - applicationId = "1:765209934800:android:a6bb09f3deabc2277297d5", - apiKey = "AIzaSyC_cfbh8xKwF8UPxCeasGcsHyK4s5yZFeA", - databaseUrl = "https://openfeedback-b7ab9.firebaseio.com" - ) - } -} diff --git a/sample-app/src/main/java/io/openfeedback/android/sample/theme/Colors.kt b/sample-app/src/main/java/io/openfeedback/android/sample/theme/Colors.kt deleted file mode 100644 index 6bbc02a..0000000 --- a/sample-app/src/main/java/io/openfeedback/android/sample/theme/Colors.kt +++ /dev/null @@ -1,17 +0,0 @@ -package io.openfeedback.android.sample.theme - -import androidx.compose.material.lightColors -import androidx.compose.ui.graphics.Color - -object ColorPalette { - val black = Color(52, 52, 52) - val greyLight = Color(237, 237, 237) - val white = Color.White -} - -val light = lightColors( - background = ColorPalette.white, - onBackground = ColorPalette.black, - surface = ColorPalette.greyLight, - onSurface = ColorPalette.black -) diff --git a/sample-app/src/main/java/io/openfeedback/android/sample/theme/OpenFeedbackTheme.kt b/sample-app/src/main/java/io/openfeedback/android/sample/theme/OpenFeedbackTheme.kt deleted file mode 100644 index ddea15c..0000000 --- a/sample-app/src/main/java/io/openfeedback/android/sample/theme/OpenFeedbackTheme.kt +++ /dev/null @@ -1,45 +0,0 @@ -package io.openfeedback.android.sample.theme - -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.platform.LocalContext - -enum class DesignSystem { - M2, M3 -} - -@Composable -fun OpenFeedbackTheme( - designSystem: DesignSystem, - isLight: Boolean, - content: @Composable () -> Unit -) { - when (designSystem) { - DesignSystem.M2 -> MaterialTheme( - colors = if (isLight) light else darkColors(), - content = content - ) - DesignSystem.M3 -> { - val colorScheme = when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (isLight) dynamicLightColorScheme(context) - else dynamicDarkColorScheme(context) - } - - else -> if (isLight) lightColorScheme() else darkColorScheme() - } - androidx.compose.material3.MaterialTheme( - colorScheme = colorScheme, - content = content - ) - } - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index a670f6a..8acadef 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,13 +1,8 @@ -dependencyResolutionManagement { - versionCatalogs { - create("libs") { - from(files("libs.versions.toml")) - } - } -} +rootProject.name = "openfeedback-android-sdk" pluginManagement { repositories { + gradlePluginPortal() google() mavenCentral() } @@ -17,9 +12,9 @@ includeBuild("build-logic") include( ":openfeedback", - ":openfeedback-m2", - ":openfeedback-m3", ":openfeedback-viewmodel", - ":sample-app" + ":openfeedback-m3", + ":sample-app-android", + ":sample-app-shared", )