From a2c888c95677a27dc09d1d510f716d481106dbd5 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Sat, 31 Oct 2026 08:46:40 -0400 Subject: [PATCH 1/2] refactor(emojis): move sources into KMP source set directories --- .../getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt | 0 .../com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt | 0 .../com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt | 0 .../com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt | 0 .../getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt | 0 .../com/getcode/libs/emojis/reactions/SkinToneVariantsTest.kt | 0 .../kotlin/com/getcode/libs/emojis/EmojiQueryProvider.kt | 0 .../kotlin/com/getcode/libs/emojis/EmojiUsageController.kt | 0 .../kotlin/com/getcode/libs/emojis/EmojiUsageTracker.kt | 0 .../kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt | 0 .../kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt | 0 .../kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt | 0 .../kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt | 0 .../kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt | 0 .../com/getcode/libs/emojis/reactions/RecentReactionsStore.kt | 0 libs/emojis/src/{main => commonMain}/resources/emoji_catalog.json | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename libs/emojis/src/{androidTest/java => androidDeviceTest/kotlin}/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt (100%) rename libs/emojis/src/{test => androidHostTest}/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt (100%) rename libs/emojis/src/{test => androidHostTest}/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt (100%) rename libs/emojis/src/{test => androidHostTest}/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt (100%) rename libs/emojis/src/{test => androidHostTest}/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt (100%) rename libs/emojis/src/{test => androidHostTest}/kotlin/com/getcode/libs/emojis/reactions/SkinToneVariantsTest.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/EmojiQueryProvider.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/EmojiUsageTracker.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt (100%) rename libs/emojis/src/{main => commonMain}/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt (100%) rename libs/emojis/src/{main => commonMain}/resources/emoji_catalog.json (100%) diff --git a/libs/emojis/src/androidTest/java/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt b/libs/emojis/src/androidDeviceTest/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt similarity index 100% rename from libs/emojis/src/androidTest/java/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt rename to libs/emojis/src/androidDeviceTest/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt b/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt similarity index 100% rename from libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt rename to libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalogLoaderTest.kt diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt b/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt similarity index 100% rename from libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt rename to libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModelTest.kt diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt b/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt similarity index 100% rename from libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt rename to libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt b/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt similarity index 100% rename from libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt rename to libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsVectorTest.kt diff --git a/libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/SkinToneVariantsTest.kt b/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/SkinToneVariantsTest.kt similarity index 100% rename from libs/emojis/src/test/kotlin/com/getcode/libs/emojis/reactions/SkinToneVariantsTest.kt rename to libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/SkinToneVariantsTest.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiQueryProvider.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/EmojiQueryProvider.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiQueryProvider.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/EmojiQueryProvider.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/EmojiUsageController.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiUsageTracker.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/EmojiUsageTracker.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/EmojiUsageTracker.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/EmojiUsageTracker.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiCatalog.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiPickerModel.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/RecentReactions.kt diff --git a/libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt similarity index 100% rename from libs/emojis/src/main/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt rename to libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt diff --git a/libs/emojis/src/main/resources/emoji_catalog.json b/libs/emojis/src/commonMain/resources/emoji_catalog.json similarity index 100% rename from libs/emojis/src/main/resources/emoji_catalog.json rename to libs/emojis/src/commonMain/resources/emoji_catalog.json From 683fcbd5112b969a1777cf6f8ce1124dbaf396c9 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Sat, 31 Oct 2026 08:46:40 -0400 Subject: [PATCH 2/2] refactor(emojis): make libs:emojis a KMP module for Android and desktop Compose Desktop needs the reaction picker and recents. The data tables, catalog loader and recents store move to commonMain. DataStoreRecentReactionsStore takes PreferencesFiles instead of a Context, with the same recent_reactions file and keys. EmojiDrawability's Paint probe becomes an expect/actual: Paint.hasGlyph on Android, an AWT font check on the JVM. --- .../internal/screens/EmojiPickerViewModel.kt | 3 +- libs/emojis/.gitignore | 2 +- libs/emojis/build.gradle.kts | 89 ++++++++++++------- .../reactions/EmojiDrawabilityVectorTest.kt | 5 +- .../reactions/RecentReactionsStoreTest.kt | 6 +- .../libs/emojis/reactions/DeviceGlyphProbe.kt | 12 +++ .../libs/emojis/reactions/EmojiDrawability.kt | 22 ++--- .../libs/emojis/reactions/ReactionsModule.kt | 7 +- .../emojis/reactions/RecentReactionsStore.kt | 14 +-- .../libs/emojis/reactions/DeviceGlyphProbe.kt | 17 ++++ 10 files changed, 111 insertions(+), 66 deletions(-) create mode 100644 libs/emojis/src/androidMain/kotlin/com/getcode/libs/emojis/reactions/DeviceGlyphProbe.kt create mode 100644 libs/emojis/src/jvmMain/kotlin/com/getcode/libs/emojis/reactions/DeviceGlyphProbe.kt diff --git a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/EmojiPickerViewModel.kt b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/EmojiPickerViewModel.kt index 41a28be81f..671fe014eb 100644 --- a/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/EmojiPickerViewModel.kt +++ b/apps/flipcash/features/messenger/src/main/kotlin/com/flipcash/app/messenger/internal/screens/EmojiPickerViewModel.kt @@ -7,6 +7,7 @@ import com.flipcash.libs.coroutines.DispatcherProvider import com.getcode.libs.emojis.reactions.EmojiCatalog import com.getcode.libs.emojis.reactions.EmojiCatalogLoader import com.getcode.libs.emojis.reactions.EmojiDrawability +import com.getcode.libs.emojis.reactions.deviceGlyphProbe import com.getcode.libs.emojis.reactions.EmojiPickerModel import com.getcode.libs.emojis.reactions.RecentReactionsStore import com.getcode.view.BaseViewModel @@ -45,7 +46,7 @@ class EmojiPickerViewModel internal constructor( dispatchers: DispatcherProvider, emojiCatalogLoader: EmojiCatalogLoader, recentReactionsStore: RecentReactionsStore, - ) : this(dispatchers, emojiCatalogLoader, recentReactionsStore, EmojiDrawability.PaintGlyphProbe) + ) : this(dispatchers, emojiCatalogLoader, recentReactionsStore, deviceGlyphProbe()) data class State( val searchFieldState: TextFieldState = TextFieldState(), diff --git a/libs/emojis/.gitignore b/libs/emojis/.gitignore index d602cadd0a..c12be3ea0e 100644 --- a/libs/emojis/.gitignore +++ b/libs/emojis/.gitignore @@ -1,3 +1,3 @@ build/ .gradle/ -src/main/kotlin/com/getcode/libs/emojis/generated/ +src/commonMain/kotlin/com/getcode/libs/emojis/generated/ diff --git a/libs/emojis/build.gradle.kts b/libs/emojis/build.gradle.kts index 8b9dc57d65..a53986b4fd 100644 --- a/libs/emojis/build.gradle.kts +++ b/libs/emojis/build.gradle.kts @@ -1,49 +1,70 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + plugins { - alias(libs.plugins.flipcash.android.library) - id("com.google.devtools.ksp") - id("dagger.hilt.android.plugin") + alias(libs.plugins.flipcash.kmp.app.library) + alias(libs.plugins.kotlin.ksp) } -android { - namespace = "${Gradle.codeNamespace}.libs.emojis" -} +kotlin { + android { + namespace = "${Gradle.codeNamespace}.libs.emojis" + withDeviceTestBuilder {}.configure { + instrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + } -// reactions.json is read from the repo's canonical test-vectors/ rather than copied in. -androidComponents { - onVariants { variant -> - val vectors = rootProject.file("test-vectors").path - variant.hostTests.values.forEach { it.sources.resources?.addStaticSourceDirectory(vectors) } - variant.deviceTests.values.forEach { it.sources.assets?.addStaticSourceDirectory(vectors) } + sourceSets { + commonMain.dependencies { + implementation(project(":apps:flipcash:core")) + implementation(libs.bundles.hilt.common) + implementation(libs.bundles.kotlinx.serialization) + implementation(libs.kotlinx.datetime) + implementation(libs.androidx.datastore.preferences.core) + } + androidMain.dependencies { + implementation(libs.hilt.android) + } + // reactions.json is read from the repo's canonical test-vectors/ rather than copied in. + getByName("androidHostTest") { + resources.srcDir(rootProject.file("test-vectors")) + dependencies { + implementation(kotlin("test")) + implementation(libs.bundles.unit.testing) + implementation(libs.robolectric) + } + } + getByName("androidDeviceTest") { + resources.srcDir(rootProject.file("test-vectors")) + dependencies { + implementation(libs.androidx.junit) + implementation(libs.junit) + implementation(libs.androidx.test.runner) + } + } } } dependencies { - implementation(libs.bundles.hilt) - ksp(libs.bundles.hilt.compiler) - implementation(libs.bundles.kotlinx.serialization) - implementation(libs.kotlinx.datetime) - implementation(libs.androidx.datastore) - - testImplementation(kotlin("test")) - testImplementation(libs.bundles.unit.testing) - testImplementation(libs.robolectric) - - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.junit) - androidTestImplementation(libs.androidx.test.runner) + add("kspAndroid", libs.hilt.android.compiler) + add("kspAndroid", libs.hilt.compiler) + add("jvmMainImplementation", libs.dagger) } -// Define the task to fetch and generate emoji data. KSP reads src/main/kotlin too, so it +// Define the task to fetch and generate emoji data. KSP reads src/commonMain/kotlin too, so it // has to wait for the generated sources as well as the Kotlin compile. -afterEvaluate { - tasks.matching { it.name.matches(Regex("(compile|ksp).*Kotlin")) }.configureEach { - dependsOn("generateEmojiList") - } -} - -tasks.register("generateEmojiList") { +val generateEmojiList by tasks.registering(GenerateEmojiList::class) { emojiUrl.set("https://unicode.org/Public/emoji/16.0/emoji-test.txt") emojiKeywordsUrl.set("https://raw.githubusercontent.com/unicode-org/cldr-json/refs/heads/main/cldr-json/cldr-annotations-full/annotations/en/annotations.json") emojiCacheDir.set(layout.projectDirectory) - outputDir.set(layout.projectDirectory.dir("src/main/kotlin/com/getcode/libs/emojis/generated")) + outputDir.set(layout.projectDirectory.dir("src/commonMain/kotlin/com/getcode/libs/emojis/generated")) +} + +tasks.withType>().configureEach { dependsOn(generateEmojiList) } +tasks.matching { it.name.startsWith("ksp") || it.name.startsWith("lint") || it.name.endsWith("SourcesJar") } + .configureEach { dependsOn(generateEmojiList) } + +// :apps:flipcash:core drags in firebase's protolite-well-known-types, which duplicates classes in +// protobuf-javalite; the instrumented test never touches either, so keep the clash off its APK. +configurations.matching { it.name.contains("DeviceTest", ignoreCase = true) }.configureEach { + exclude(group = "com.google.firebase", module = "protolite-well-known-types") } diff --git a/libs/emojis/src/androidDeviceTest/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt b/libs/emojis/src/androidDeviceTest/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt index 5efb2e963e..0115496b59 100644 --- a/libs/emojis/src/androidDeviceTest/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt +++ b/libs/emojis/src/androidDeviceTest/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawabilityVectorTest.kt @@ -1,7 +1,6 @@ package com.getcode.libs.emojis.reactions import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry import org.json.JSONObject import org.junit.Assert.assertEquals import org.junit.Test @@ -18,8 +17,8 @@ class EmojiDrawabilityVectorTest { @Test fun drawabilityVectors() { - val json = InstrumentationRegistry.getInstrumentation().context.assets - .open("reactions.json").bufferedReader().use { it.readText() } + val json = javaClass.classLoader!! + .getResourceAsStream("reactions.json")!!.bufferedReader().use { it.readText() } val vectors = JSONObject(json).getJSONArray("drawability") val failures = (0 until vectors.length()).map { vectors.getJSONObject(it) }.mapNotNull { vector -> val emoji = vector.getString("emoji") diff --git a/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt b/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt index dbeb64d90d..244b83fd11 100644 --- a/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt +++ b/libs/emojis/src/androidHostTest/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStoreTest.kt @@ -1,10 +1,11 @@ package com.getcode.libs.emojis.reactions -import androidx.test.core.app.ApplicationProvider +import com.flipcash.app.core.storage.PreferencesFiles import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import java.io.File import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -13,8 +14,9 @@ class RecentReactionsStoreTest { // The DataStore file outlives each test in this process, so every test uses owners of its own. private var owner: String? = null + private val directory = File.createTempFile("recent-reactions", "").apply { delete(); mkdirs() } private val store = DataStoreRecentReactionsStore( - context = ApplicationProvider.getApplicationContext(), + preferencesFiles = PreferencesFiles { name -> File(directory, "$name.preferences_pb") }, owner = { owner }, ) diff --git a/libs/emojis/src/androidMain/kotlin/com/getcode/libs/emojis/reactions/DeviceGlyphProbe.kt b/libs/emojis/src/androidMain/kotlin/com/getcode/libs/emojis/reactions/DeviceGlyphProbe.kt new file mode 100644 index 0000000000..60b041eb44 --- /dev/null +++ b/libs/emojis/src/androidMain/kotlin/com/getcode/libs/emojis/reactions/DeviceGlyphProbe.kt @@ -0,0 +1,12 @@ +package com.getcode.libs.emojis.reactions + +import android.graphics.Paint + +actual fun deviceGlyphProbe(): EmojiDrawability.GlyphProbe = PaintGlyphProbe + +/** The real, device-backed probe: [android.graphics.Paint.hasGlyph]. */ +object PaintGlyphProbe : EmojiDrawability.GlyphProbe { + private val paint = Paint() + + override fun isSingleGlyph(text: String): Boolean = paint.hasGlyph(text) +} diff --git a/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt index e5d9868729..47aa019881 100644 --- a/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt +++ b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/EmojiDrawability.kt @@ -1,29 +1,23 @@ package com.getcode.libs.emojis.reactions -import android.graphics.Paint - /** Answers whether this device draws an emoji as one glyph, matching iOS's `EmojiDrawability`. */ object EmojiDrawability { /** * Returns whether this device draws [emoji] as a single glyph. * - * [Paint.hasGlyph] asks the platform's font fallback whether a string resolves to one glyph - * (a ligature for a ZWJ/skin-tone sequence, or the base glyph for a plain emoji) rather than - * falling apart into several tofu or component glyphs side by side — the same question iOS's - * `CTLine` run probe answers by counting AppleColorEmoji glyphs. + * On Android, `Paint.hasGlyph` asks the platform's font fallback whether a string resolves to + * one glyph (a ligature for a ZWJ/skin-tone sequence, or the base glyph for a plain emoji) + * rather than falling apart into several tofu or component glyphs side by side — the same + * question iOS's `CTLine` run probe answers by counting AppleColorEmoji glyphs. */ - fun isDrawable(emoji: String, probe: GlyphProbe = PaintGlyphProbe): Boolean = probe.isSingleGlyph(emoji) + fun isDrawable(emoji: String, probe: GlyphProbe = deviceGlyphProbe()): Boolean = probe.isSingleGlyph(emoji) /** Seam so the merge logic that consumes drawability can be tested without a real typeface. */ fun interface GlyphProbe { fun isSingleGlyph(text: String): Boolean } - - /** The real, device-backed probe: [android.graphics.Paint.hasGlyph]. */ - object PaintGlyphProbe : GlyphProbe { - private val paint = Paint() - - override fun isSingleGlyph(text: String): Boolean = paint.hasGlyph(text) - } } + +/** The probe backed by the running platform's fonts. */ +expect fun deviceGlyphProbe(): EmojiDrawability.GlyphProbe diff --git a/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt index 0040ce4a1b..c1a78caf09 100644 --- a/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt +++ b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/ReactionsModule.kt @@ -1,10 +1,9 @@ package com.getcode.libs.emojis.reactions -import android.content.Context +import com.flipcash.app.core.storage.PreferencesFiles import dagger.Module import dagger.Provides import dagger.hilt.InstallIn -import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import javax.inject.Singleton @@ -15,9 +14,9 @@ object ReactionsModule { @Provides @Singleton fun providesRecentReactionsStore( - @ApplicationContext context: Context, + preferencesFiles: PreferencesFiles, owner: RecentReactionsOwner, - ): RecentReactionsStore = DataStoreRecentReactionsStore(context, owner) + ): RecentReactionsStore = DataStoreRecentReactionsStore(preferencesFiles, owner) @Provides @Singleton diff --git a/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt index c78fe9f87c..1c6a8017f2 100644 --- a/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt +++ b/libs/emojis/src/commonMain/kotlin/com/getcode/libs/emojis/reactions/RecentReactionsStore.kt @@ -1,20 +1,17 @@ package com.getcode.libs.emojis.reactions -import android.content.Context import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import dagger.hilt.android.qualifiers.ApplicationContext +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import com.flipcash.app.core.storage.PreferencesFiles import kotlinx.coroutines.flow.first import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Clock import kotlin.time.Instant -private val Context.recentReactionsDataStore: DataStore by preferencesDataStore(name = "recent_reactions") - /** Per-emoji use counts and last-used times, backing [RecentReactions.rank]. */ interface RecentReactionsStore { @@ -39,11 +36,13 @@ fun interface RecentReactionsOwner { @Singleton class DataStoreRecentReactionsStore @Inject constructor( - @ApplicationContext context: Context, + preferencesFiles: PreferencesFiles, private val owner: RecentReactionsOwner, ) : RecentReactionsStore { - private val dataStore = context.recentReactionsDataStore + private val dataStore: DataStore = PreferenceDataStoreFactory.create( + produceFile = { preferencesFiles.file(DATASTORE_NAME) }, + ) override suspend fun record(emoji: String) { val owner = owner.current() ?: return @@ -86,6 +85,7 @@ class DataStoreRecentReactionsStore @Inject constructor( } private companion object { + const val DATASTORE_NAME = "recent_reactions" const val KEY_PREFIX = "emoji:" } } diff --git a/libs/emojis/src/jvmMain/kotlin/com/getcode/libs/emojis/reactions/DeviceGlyphProbe.kt b/libs/emojis/src/jvmMain/kotlin/com/getcode/libs/emojis/reactions/DeviceGlyphProbe.kt new file mode 100644 index 0000000000..9dd026523c --- /dev/null +++ b/libs/emojis/src/jvmMain/kotlin/com/getcode/libs/emojis/reactions/DeviceGlyphProbe.kt @@ -0,0 +1,17 @@ +package com.getcode.libs.emojis.reactions + +import java.awt.Font + +actual fun deviceGlyphProbe(): EmojiDrawability.GlyphProbe = AwtGlyphProbe + +/** + * Desktop probe: asks the logical `Dialog` font, which falls back across the installed fonts, + * whether it can display every code point. AWT has no equivalent of `Paint.hasGlyph`'s + * single-glyph check, so a ZWJ sequence a font would draw as separate components still reads as + * drawable here. + */ +object AwtGlyphProbe : EmojiDrawability.GlyphProbe { + private val font by lazy { Font(Font.DIALOG, Font.PLAIN, 12) } + + override fun isSingleGlyph(text: String): Boolean = text.isNotEmpty() && font.canDisplayUpTo(text) == -1 +}