Emoji picker gains recents and grouping, matching iOS
This was a flat grid while iOS kept a recents row and grouped the set the way people think about them - a gap found when a duplicate iOS picker nearly shadowed the better one. Two platforms offering the same emoji through different furniture is the drift nobody files a bug about and everybody notices. Mechanics mirror the iOS file: the last twelve picked, most recent first, in ordinary SharedPreferences - which emoji somebody reaches for is not a secret worth a vault entry, and the vault cannot be read before sign-in anyway. The set and its grouping are byte-identical to iOS's, with a note in both files to change them in the same sitting. Test tags are scoped by section, as on iOS, because the same emoji legitimately appears in Recent and in its own group. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
a68c9212d9
commit
09a4fb90fc
@@ -2,18 +2,27 @@ package io.flamenet.messenger.app
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
@@ -21,6 +30,7 @@ import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
@@ -85,45 +95,90 @@ fun ColumnScope.StickerPicker(onPick: (StickerPack.Sticker) -> Unit) {
|
||||
* ⚠️ The panel stays open on tap. Somebody picking three in a row expects three
|
||||
* characters in the box, and closing on each tap would make that impossible.
|
||||
*
|
||||
* ⚠️ A short curated list rather than every emoji Unicode defines. A full
|
||||
* picker is a search field, skin-tone variants, category tabs and a recents
|
||||
* list — a real feature, and one the system keyboard already ships. This is the
|
||||
* common set within reach of a thumb; the keyboard remains the way to reach the
|
||||
* rest.
|
||||
* 🔴 **Recents and grouping, matching iOS.** This was a flat grid while iOS
|
||||
* kept a recents row and grouped the set the way people think about them —
|
||||
* found when a duplicate iOS picker nearly shadowed the better one. Two
|
||||
* platforms offering the same emoji through different furniture is the drift
|
||||
* nobody files a bug about and everybody notices. The mechanics mirror the
|
||||
* iOS file: recents are the last twelve picked, most recent first, in ordinary
|
||||
* SharedPreferences — which emoji somebody reaches for is not a secret worth a
|
||||
* vault entry, and the vault cannot be read before sign-in anyway.
|
||||
*
|
||||
* ⚠️ Still a curated set, not all of Unicode. A full picker is search, skin
|
||||
* tones and category tabs — a real feature the system keyboard already ships.
|
||||
*/
|
||||
@Composable
|
||||
fun ColumnScope.EmojiPicker(onPick: (String) -> Unit) {
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(minSize = 44.dp),
|
||||
val context = LocalContext.current
|
||||
val prefs = remember {
|
||||
context.getSharedPreferences("flamenet-emoji", android.content.Context.MODE_PRIVATE)
|
||||
}
|
||||
var recents by remember {
|
||||
mutableStateOf(prefs.getString("recent", "")!!.split(" ").filter { it.isNotBlank() })
|
||||
}
|
||||
|
||||
fun pick(emoji: String) {
|
||||
val next = (listOf(emoji) + recents.filter { it != emoji }).take(12)
|
||||
recents = next
|
||||
prefs.edit().putString("recent", next.joinToString(" ")).apply()
|
||||
onPick(emoji)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 200.dp)
|
||||
.heightIn(max = 220.dp)
|
||||
.clip(RoundedCornerShape(Radius.large))
|
||||
.background(Palette.SurfaceAlt)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(Space.sm)
|
||||
.testTag("emoji-picker"),
|
||||
contentPadding = androidx.compose.foundation.layout.PaddingValues(Space.sm),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(Space.xs),
|
||||
) {
|
||||
items(EMOJI, key = { it }) { emoji ->
|
||||
if (recents.isNotEmpty()) EmojiSection("Recent", recents, ::pick)
|
||||
EMOJI_GROUPS.forEach { (name, emoji) -> EmojiSection(name, emoji, ::pick) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun EmojiSection(title: String, emoji: List<String>, onPick: (String) -> Unit) {
|
||||
Text(
|
||||
title,
|
||||
fontSize = 11.sp,
|
||||
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
|
||||
color = Palette.SecondaryText,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState()),
|
||||
horizontalArrangement = Arrangement.spacedBy(Space.xs),
|
||||
) {
|
||||
emoji.forEach { e ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(2.dp)
|
||||
.clip(RoundedCornerShape(Radius.small))
|
||||
.clickable { onPick(emoji) }
|
||||
.testTag("emoji-pick-$emoji"),
|
||||
contentAlignment = Alignment.Center,
|
||||
.clickable { onPick(e) }
|
||||
// Scoped by section, as on iOS: the same emoji can sit in
|
||||
// Recent AND its own group, and a tag on the glyph alone is
|
||||
// ambiguous to a test.
|
||||
.testTag("emoji-$title-$e"),
|
||||
) {
|
||||
Text(emoji, fontSize = 24.sp, modifier = Modifier.padding(Space.sm))
|
||||
Text(e, fontSize = 24.sp, modifier = Modifier.padding(Space.xs))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The common set, in the order people actually reach for them. */
|
||||
private val EMOJI = listOf(
|
||||
"😀", "😂", "🥹", "😊", "😍", "😘", "😎", "🤔", "😐", "🙄",
|
||||
"😴", "😭", "😡", "🥳", "🤝", "👍", "👎", "👌", "🙏", "👏",
|
||||
"💪", "👋", "🫡", "🤷", "❤️", "🧡", "💚", "💙", "💜", "🖤",
|
||||
"🔥", "⭐", "✨", "🎉", "🎂", "☕", "🍕", "🍺", "🚀", "✅",
|
||||
"❌", "⚠️", "💤", "👀", "🐛", "📎", "📷", "🎤", "📞", "💬",
|
||||
/**
|
||||
* The same set as iOS, grouped the same way — kept in step on purpose.
|
||||
* If one platform's list changes, change the other in the same sitting.
|
||||
*/
|
||||
private val EMOJI_GROUPS = listOf(
|
||||
"Smileys" to listOf("😀", "😂", "🙂", "😉", "😍", "🤔", "😅", "😭", "😡", "🥳",
|
||||
"😴", "🤯", "🫠", "😬", "🙃", "😎"),
|
||||
"Gestures" to listOf("👍", "👎", "👋", "🙏", "👏", "🤝", "💪", "🫶", "✌️", "🤞"),
|
||||
"Hearts" to listOf("❤️", "🧡", "💛", "💚", "💙", "💜", "🖤", "💔", "✨", "🔥"),
|
||||
"Things" to listOf("✅", "❌", "⚠️", "📌", "📎", "🔒", "🔑", "☕", "🍕", "🎉",
|
||||
"🎂", "🚀", "🌧️", "☀️", "🌙", "⭐"),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user