From 584ce90b64a09c7e49d2e562c4d751a6a52d936b Mon Sep 17 00:00:00 2001 From: infi Date: Sat, 6 Jun 2026 23:36:25 +0200 Subject: [PATCH] feat: fix skin tone picker, show more emoji --- .../java/chat/stoat/internals/EmojiImpl.kt | 79 +++++++++++-------- 1 file changed, 44 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/chat/stoat/internals/EmojiImpl.kt b/app/src/main/java/chat/stoat/internals/EmojiImpl.kt index eb0d84d1..fe8a3cc8 100644 --- a/app/src/main/java/chat/stoat/internals/EmojiImpl.kt +++ b/app/src/main/java/chat/stoat/internals/EmojiImpl.kt @@ -106,24 +106,48 @@ class EmojiImpl { val category = UnicodeEmojiSection.entries.find { it.googleName == group.group } ?: continue list.add(EmojiPickerItem.Section(Category.UnicodeEmojiCategory(category))) - list.addAll( - group.emoji.map { emoji -> - EmojiPickerItem.UnicodeEmoji( - emoji.base.joinToString("") { String(Character.toChars(it.toInt())) }, - emoji.alternates.any { alternate -> - alternate.any { codepoint -> - codepoint in 0x1F3FB..0x1F3FF - } - }, - emoji.alternates - ) - } - ) + list.addAll(group.emoji.flatMap { emoji -> emojiToPickerItems(emoji) }) } return list } + private fun emojiToPickerItems(emoji: Emoji): List { + val items = mutableListOf() + + items.add( + EmojiPickerItem.UnicodeEmoji( + emoji.base.joinToString("") { String(Character.toChars(it.toInt())) }, + emoji.alternates.any { alternate -> + alternate.any { codepoint -> codepoint in 0x1F3FB..0x1F3FF } + }, + emoji.alternates + ) + ) + + // Non-skintone alternates (like gender variants) as separate picker items. + // Technically this breaks the Google metadata's assumption of 9 items per row. + // So far, this has not caused issues. + val nonSkintoneAlts = emoji.alternates.filter { alt -> + alt.none { cp -> cp in 0x1F3FB..0x1F3FF } && alt != emoji.base + } + for (nonSkintoneAlt in nonSkintoneAlts) { + val skintoneVariants = emoji.alternates.filter { alt -> + alt.any { cp -> cp in 0x1F3FB..0x1F3FF } && + alt.filter { cp -> cp !in 0x1F3FB..0x1F3FF } == nonSkintoneAlt + } + items.add( + EmojiPickerItem.UnicodeEmoji( + nonSkintoneAlt.joinToString("") { String(Character.toChars(it.toInt())) }, + skintoneVariants.isNotEmpty(), + skintoneVariants + ) + ) + } + + return items + } + /** * Returns a map of category to start and end index of the category in the flat picker list * Impl @@ -183,15 +207,12 @@ class EmojiImpl { ): String { if (!item.hasSkinTones || skinType == FitzpatrickSkinTone.None) return item.character - // HACK: We simply find the modifier version from metadata that - // contains the skin tone modifier codepoint. - val modifier = item.alternates.maxByOrNull { alternate -> - // HACK HACK: We find the alternate with the most frequency of our skin tone modifier. - // This is because some emoji have multiple skin tone modifier and we are taking the - // easy way here by only allowing a single skin tone change. This is not ideal. - // Users are encouraged to use the system emoji keyboard to get the full range of - // skin tone modifiers. - alternate.count { it == skinType.modifierCodepoint?.toLong() } + val targetModifier = skinType.modifierCodepoint!!.toLong() + // Pick the alternate that carries exactly one Fitzpatrick modifier and it is the target. + // This avoids selecting multi-skintone combo alternates (e.g. 🫱🏻‍🫲🏼) when the user + // only wants the simple single-tone variant (e.g. 🤝🏼). + val modifier = item.alternates.firstOrNull { alternate -> + alternate.count { it in 0x1F3FB..0x1F3FF } == 1 && targetModifier in alternate } return modifier?.joinToString("") { String(Character.toChars(it.toInt())) } @@ -228,19 +249,7 @@ class EmojiImpl { val category = UnicodeEmojiSection.entries.find { it.googleName == group.group } ?: continue list.add(EmojiPickerItem.Section(Category.UnicodeEmojiCategory(category))) - list.addAll( - matchingEmoji.map { emoji -> - EmojiPickerItem.UnicodeEmoji( - emoji.base.joinToString("") { String(Character.toChars(it.toInt())) }, - emoji.alternates.any { alternate -> - alternate.any { codepoint -> - codepoint in 0x1F3FB..0x1F3FF - } - }, - emoji.alternates - ) - } - ) + list.addAll(matchingEmoji.flatMap { emoji -> emojiToPickerItems(emoji) }) } }