diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/Pickers.kt b/app/src/main/kotlin/io/flamenet/messenger/app/Pickers.kt index 5a31edc..e0b587e 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/Pickers.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/Pickers.kt @@ -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, 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("✅", "❌", "⚠️", "📌", "📎", "🔒", "🔑", "☕", "🍕", "🎉", + "🎂", "🚀", "🌧️", "☀️", "🌙", "⭐"), )